aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Warrington <cmw@google.com>2016-10-18 12:29:21 +0100
committerChris Warrington <cmw@google.com>2016-10-18 12:34:18 +0100
commite3780081075c01aa1dff6d1f373cb43192b33e68 (patch)
treefb734615933a39f3d009210dc0d1457160479b35
parent7e05eb7e57827eddc885570bc00aed8a50320dbf (diff)
parent025b8b226c8d8edba2b309ca878572f40512eca7 (diff)
downloadgradle-perf-android-medium-e3780081075c01aa1dff6d1f373cb43192b33e68.tar.gz
Change-Id: I63f5e16d09297c48432192761b840310935eb903
-rw-r--r--.gitattributes7
-rw-r--r--.github/ISSUE_TEMPLATE.md10
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md3
-rw-r--r--.gitignore51
-rw-r--r--.travis.yml39
-rw-r--r--CODESTYLE.md12
-rw-r--r--CONTRIBUTING.md113
-rw-r--r--LICENSE.md264
-rw-r--r--README.md97
-rw-r--r--WordPress/build.gradle192
-rw-r--r--WordPress/gradle.properties-example16
-rw-r--r--WordPress/lint.xml28
-rw-r--r--WordPress/proguard.cfg30
-rw-r--r--WordPress/src/androidTest/.gitignore4
-rw-r--r--WordPress/src/androidTest/assets/1354-wp.getUsersBlogs.xml47
-rw-r--r--WordPress/src/androidTest/assets/comment-already-spammed-wp.editComment.xml21
-rw-r--r--WordPress/src/androidTest/assets/comment-already-spammed-wp.getComment.xml72
-rw-r--r--WordPress/src/androidTest/assets/corrupteddb-reproducing-814.sql21
-rw-r--r--WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.json744
-rw-r--r--WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.xml4203
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-clicks.json26
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-comments.json160
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-country-views.json113
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-followers.json58
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-insights.json1
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-post-123.json1376
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-referrers.json553
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-summary.json1
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-tags.json125
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-top-posts.json102
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-video-plays.json11
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-visits.json254
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats.json187
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-clicks.json65
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-followers.json156
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-top-posts.json101
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-video-plays.json18
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-plans-features.json203
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-sites-123456-plans.json191
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-me.json18
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new.json9
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new_1.json3
-rw-r--r--WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-users-new.json3
-rw-r--r--WordPress/src/androidTest/assets/default-wp.deletePost.xml10
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getComments.json482
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getComments.xml2654
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getMediaLibrary.xml2791
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getOptions.json32
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getOptions.xml161
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getPostFormats.json1
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getPostFormats.xml71
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getProfile.json1
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getProfile.xml89
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getUsersBlogs.json20
-rw-r--r--WordPress/src/androidTest/assets/default-wp.getUsersBlogs.xml115
-rw-r--r--WordPress/src/androidTest/assets/email-exists-public-api-wordpress-com-rest-v1-users-new.json4
-rw-r--r--WordPress/src/androidTest/assets/empty-wp.getProfile.json1
-rw-r--r--WordPress/src/androidTest/assets/empty_tables.sql16
-rw-r--r--WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json229
-rw-r--r--WordPress/src/androidTest/assets/health-check/index.html153
-rw-r--r--WordPress/src/androidTest/assets/health-check/index_with_redirect.html153
-rw-r--r--WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml91
-rw-r--r--WordPress/src/androidTest/assets/health-check/rsd.xml14
-rw-r--r--WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml14
-rw-r--r--WordPress/src/androidTest/assets/incorrect-password-wp.getOptions.xml21
-rw-r--r--WordPress/src/androidTest/assets/invalid-double-xmlrpc-wp.getMediaLibrary.xml37
-rw-r--r--WordPress/src/androidTest/assets/invalid-integer-xmlrpc-wp.getMediaLibrary.xml23
-rw-r--r--WordPress/src/androidTest/assets/malformed-getusersblog-wp.getUsersBlogs.xml3
-rw-r--r--WordPress/src/androidTest/assets/malformed-null-postid-metaWeblog.getRecentPosts.xml198
-rw-r--r--WordPress/src/androidTest/assets/malformed-software-version-wp.getOptions.json31
-rw-r--r--WordPress/src/androidTest/assets/malformed-software-version-wp.getProfile.json1
-rw-r--r--WordPress/src/androidTest/assets/malformed_category_parent_id.sql13
-rw-r--r--WordPress/src/androidTest/assets/one_category.sql13
-rw-r--r--WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-devices-new.json2
-rw-r--r--WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-users-new.json4
-rw-r--r--WordPress/src/androidTest/assets/rest-v1-notifications-num_note_items=20.json3453
-rw-r--r--WordPress/src/androidTest/assets/site-reserved-public-api-wordpress-com-rest-v1-sites-new.json4
-rw-r--r--WordPress/src/androidTest/assets/taliwutt-blogs-sample.sql48
-rw-r--r--WordPress/src/androidTest/assets/username-exists-public-api-wordpress-com-rest-v1-users-new.json4
-rw-r--r--WordPress/src/androidTest/java/URITest.java56
-rw-r--r--WordPress/src/androidTest/java/URLTest.java55
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/DefaultMocksInstrumentationTestCase.java42
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/FactoryUtils.java45
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/PostUploadServiceTest.java101
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java168
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java29
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/database/CommentTableTest.java61
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/database/WordPressDBTest.java17
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorEmptyMock.java11
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorFactoryTest.java24
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientCustomizableMock.java150
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientEmptyMock.java63
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientFactoryTest.java71
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/SystemServiceFactoryTest.java35
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableJSONMock.java94
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableMockAbstract.java23
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableXMLMock.java90
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientEmptyMock.java43
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCFactoryTest.java75
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/models/BlogTest.java293
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/models/CategoryNodeInstrumentationTest.java34
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/models/PostLocationTest.java115
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/models/PostTest.java55
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/networking/AuthenticatorRequestTest.java61
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java44
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/networking/WPNetworkImageViewTest.java58
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/networking/XMLRPCTest.java35
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/GCMIntentServiceTest.java66
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotesParseTest.java29
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java19
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/ui/plans/RemoteTests.java159
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/ui/posts/PostUtilsTest.java33
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/ui/stats/RemoteTests.java638
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/util/ApiHelperTest.java124
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/util/AutolinkUtilsTest.java79
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java227
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java25
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/util/WPHtmlTest.java38
-rw-r--r--WordPress/src/androidTest/java/org/wordpress/android/util/WPUrlUtilsTest.java272
-rw-r--r--WordPress/src/androidTest/monkeys/README.md36
-rw-r--r--WordPress/src/androidTest/monkeys/create_wpcom_blog_from_settings.py44
-rw-r--r--WordPress/src/androidTest/monkeys/customizable_monkey.py44
-rwxr-xr-xWordPress/src/androidTest/monkeys/playstore-screenshots.py202
-rw-r--r--WordPress/src/androidTest/monkeys/settings.py-example4
-rw-r--r--WordPress/src/androidTest/proguard-project.txt20
-rw-r--r--WordPress/src/androidTest/readme.md9
-rwxr-xr-xWordPress/src/androidTest/tools/dump-device-db.sh3
-rw-r--r--WordPress/src/future/svg/stats_widget_promo_header.svg1
-rw-r--r--WordPress/src/main/AndroidManifest.xml527
-rw-r--r--WordPress/src/main/assets/android_models.properties169
-rw-r--r--WordPress/src/main/assets/fonts/Merriweather-Bold.ttfbin0 -> 46796 bytes
-rw-r--r--WordPress/src/main/assets/fonts/Merriweather-BoldItalic.ttfbin0 -> 59316 bytes
-rw-r--r--WordPress/src/main/assets/fonts/Merriweather-Italic.ttfbin0 -> 53140 bytes
-rw-r--r--WordPress/src/main/assets/fonts/Merriweather-Regular.ttfbin0 -> 46576 bytes
-rw-r--r--WordPress/src/main/assets/fonts/Noticons.ttfbin0 -> 29604 bytes
-rw-r--r--WordPress/src/main/assets/licenses.html413
-rw-r--r--WordPress/src/main/assets/merriweather.css36
-rw-r--r--WordPress/src/main/assets/webview.css115
-rw-r--r--WordPress/src/main/java/org/wordpress/android/Constants.java23
-rw-r--r--WordPress/src/main/java/org/wordpress/android/GCMMessageService.java737
-rw-r--r--WordPress/src/main/java/org/wordpress/android/GCMRegistrationIntentService.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/InstanceIDService.java13
-rw-r--r--WordPress/src/main/java/org/wordpress/android/JavaScriptException.java21
-rw-r--r--WordPress/src/main/java/org/wordpress/android/WordPress.java898
-rw-r--r--WordPress/src/main/java/org/wordpress/android/WordPressDB.java2087
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/AccountTable.java124
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/CommentTable.java419
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java354
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderBlogTable.java382
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderCommentTable.java336
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java255
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java222
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java933
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java84
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java381
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderThumbnailTable.java56
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java211
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/SQLTable.java68
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java104
-rw-r--r--WordPress/src/main/java/org/wordpress/android/datasets/SuggestionTable.java173
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Account.java111
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java51
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/AccountModel.java248
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Blog.java576
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java52
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java25
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Capability.java21
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java65
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java110
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Comment.java244
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CommentList.java107
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java83
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java5
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java15
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Note.java590
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java115
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java24
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Person.java174
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Post.java505
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostLocation.java93
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostStatus.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java183
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java169
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java115
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java718
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java187
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java54
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java79
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java214
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java45
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java36
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java121
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java14
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java44
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Role.java102
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java418
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Suggestion.java71
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Tag.java51
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Theme.java183
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java130
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java35
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java12
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java5
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java7
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java42
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java267
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java41
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java287
-rw-r--r--WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java119
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/ActivityId.java57
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java304
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java230
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/AppLogViewerActivity.java164
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/CheckableFrameLayout.java61
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/CustomSpinner.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java82
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/EmptyViewMessageType.java14
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/ExpandableHeightGridView.java59
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/FadeInNetworkImageView.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java574
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/HelpshiftDeepLinkReceiver.java29
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java24
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java274
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/VisualEditorOptionsReceiver.java46
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java261
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java363
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java160
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java306
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/BlogUtils.java191
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.java100
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogActivity.java45
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogFragment.java326
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/NewUserFragment.java543
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java179
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInDialogFragment.java170
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java1010
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java150
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/CreateUserAndBlog.java263
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListAbstract.java28
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPCom.java95
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java81
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginAbstract.java29
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginWPCom.java112
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/PluginsCheckerWPOrg.java202
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/UpdateBlogListTask.java77
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkRequestFragment.java162
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSentFragment.java82
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInActivity.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInFragment.java264
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java503
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java486
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java92
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java1234
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java52
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java56
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java107
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java314
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java795
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java440
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java748
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java449
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java471
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java508
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java638
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabAdapter.java91
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabLayout.java121
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java281
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java583
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java385
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java186
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java140
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java191
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java275
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java370
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java519
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java836
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java42
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java380
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java538
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java264
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java209
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java379
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java121
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java51
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java247
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationDismissBroadcastReceiver.java28
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationEvents.java33
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationFragment.java19
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java225
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.java512
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java542
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/ShareAndDismissNotificationReceiver.java31
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/CursorRecyclerViewAdapter.java114
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteBlockAdapter.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.java309
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/BlockType.java34
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java232
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/FooterNoteBlock.java116
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.java164
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.java275
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockClickableSpan.java150
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockLinkMovementMethod.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockRangeType.java49
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/UserNoteBlock.java198
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java540
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/SimperiumUtils.java184
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java667
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java432
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java664
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java209
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java148
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java66
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java527
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlanEvents.java30
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java171
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseActivity.java243
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseFragment.java145
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlanUpdateService.java159
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java312
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlansConstants.java17
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java75
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/adapters/PlansPagerAdapter.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/models/Feature.java124
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java285
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/plans/models/PlanFeaturesHighlightSection.java53
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/AddCategoryActivity.java110
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/CategoryArrayAdapter.java41
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java2224
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPreviewFragment.java125
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java1054
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/ParentCategorySpinnerAdapter.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewActivity.java328
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewFragment.java133
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java130
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.java113
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListFragment.java522
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/PromoDialog.java74
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java415
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PageMenuAdapter.java97
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PostsListAdapter.java718
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostEvents.java78
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostMediaService.java127
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUpdateService.java161
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java1026
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java74
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java278
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java407
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java74
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java194
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java354
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java128
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java265
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java383
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java98
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java72
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java175
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java22
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java100
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java42
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java175
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java166
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java7
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java20
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java111
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java184
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java421
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java1392
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java871
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java58
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java211
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java65
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java82
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java76
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java171
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java451
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java263
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderAnim.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderBlogFragment.java175
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java547
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java53
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java135
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderInterfaces.java42
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java237
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerFragment.java94
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java1171
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostHistory.java52
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java181
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java1612
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java532
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java580
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java54
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java541
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java95
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java146
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderViewPagerTransformer.java104
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java477
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java181
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java359
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java168
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java263
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java497
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java103
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java962
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java184
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java174
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java112
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostId.java23
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostIdList.java40
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderImageList.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java86
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java206
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java391
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java331
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java86
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java131
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java34
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java117
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java103
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java221
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java163
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java74
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderCommentsPostHeaderView.java75
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java96
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderGapMarkerView.java79
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java91
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderLikingUsersView.java105
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPhotoView.java271
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRecyclerView.java30
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java205
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.java103
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderThumbnailStrip.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java368
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java118
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java159
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java361
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java85
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java297
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java1034
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java282
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java335
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java266
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java280
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java21
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java276
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java449
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java299
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java99
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java280
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java149
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java200
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java238
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java340
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java19
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java238
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java907
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java282
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java39
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java129
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java344
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java558
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java170
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java318
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java170
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java25
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java846
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java79
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java161
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java300
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java541
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java15
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java55
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java130
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java226
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java9
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java119
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java84
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java7
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java113
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java107
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java77
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java76
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java47
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java96
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java23
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java30
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java370
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java40
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java124
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java116
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java108
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java31
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java35
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java93
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java136
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java614
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/SuggestionAdapter.java150
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/TagSuggestionAdapter.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionEvents.java17
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionService.java162
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionServiceConnectionManager.java55
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionTokenizer.java52
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionUtils.java62
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java546
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java205
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.java383
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeSearchFragment.java159
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java191
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/AnalyticsUtils.java277
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/AniUtils.java252
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java100
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/AutolinkUtils.java79
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/BitmapLruCache.java42
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/CoreEvents.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/CrashlyticsUtils.java65
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/GenericCallback.java5
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/HelpshiftHelper.java246
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/RateLimitedTask.java37
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/URLFilteredWebViewClient.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/VolleyUtils.java131
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java143
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPHtml.java1225
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPLinkMovementMethod.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPMeShortlinks.java146
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java305
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java430
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPUrlUtils.java45
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPWebViewClient.java121
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/AuthErrorDialogFragment.java63
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/CheckedLinearLayout.java47
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/DividerItemDecoration.java104
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/FlowLayout.java135
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/HeaderGridView.java467
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/MultiUsernameEditText.java77
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/NoticonTextView.java27
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/PostListButton.java134
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/RecyclerItemDecoration.java40
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/SuggestionAutoCompleteText.java202
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/TextDrawable.java442
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java126
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPAlertDialogFragment.java140
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPAutoResizeTextView.java27
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPButton.java22
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPCheckBox.java25
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPEditText.java23
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPEditTextPreference.java28
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPLinearLayoutSizeBound.java45
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java449
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPRadioButton.java25
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPScrollView.java80
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java21
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPTextView.java82
-rw-r--r--WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager.java55
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java1189
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/LoggedInputStream.java111
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/NullOutputStream.java11
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/TrustUserSSLCertsSocketFactory.java162
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCCallback.java26
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java713
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCClientInterface.java17
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCException.java16
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactory.java14
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryAbstract.java7
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryDefault.java9
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCFault.java28
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCSerializer.java298
-rw-r--r--WordPress/src/main/java/org/xmlrpc/android/XMLRPCUtils.java570
-rw-r--r--WordPress/src/main/res/anim/activity_slide_in_from_left.xml11
-rw-r--r--WordPress/src/main/res/anim/activity_slide_in_from_right.xml11
-rw-r--r--WordPress/src/main/res/anim/activity_slide_out_to_left.xml11
-rw-r--r--WordPress/src/main/res/anim/activity_slide_out_to_right.xml11
-rw-r--r--WordPress/src/main/res/anim/blink.xml12
-rw-r--r--WordPress/src/main/res/anim/box_with_pages_slide_up_page1.xml8
-rw-r--r--WordPress/src/main/res/anim/box_with_pages_slide_up_page2.xml8
-rw-r--r--WordPress/src/main/res/anim/box_with_pages_slide_up_page3.xml8
-rw-r--r--WordPress/src/main/res/anim/cab_deselect.xml14
-rw-r--r--WordPress/src/main/res/anim/cab_select.xml15
-rw-r--r--WordPress/src/main/res/anim/cycle_5.xml17
-rw-r--r--WordPress/src/main/res/anim/do_nothing.xml5
-rw-r--r--WordPress/src/main/res/anim/fade_out.xml9
-rw-r--r--WordPress/src/main/res/anim/notifications_button_scale.xml15
-rw-r--r--WordPress/src/main/res/anim/pop.xml11
-rw-r--r--WordPress/src/main/res/anim/pressed_card.xml19
-rw-r--r--WordPress/src/main/res/anim/raise.xml19
-rw-r--r--WordPress/src/main/res/anim/reader_flyin.xml6
-rw-r--r--WordPress/src/main/res/anim/reader_flyout.xml6
-rw-r--r--WordPress/src/main/res/anim/reader_top_bar_in.xml11
-rw-r--r--WordPress/src/main/res/anim/reader_top_bar_out.xml11
-rw-r--r--WordPress/src/main/res/anim/shake.xml20
-rw-r--r--WordPress/src/main/res/anim/slide_up.xml6
-rw-r--r--WordPress/src/main/res/color/calypso_segmented_control_text.xml6
-rw-r--r--WordPress/src/main/res/color/calypso_subtitle_text.xml12
-rw-r--r--WordPress/src/main/res/color/calypso_title_text.xml12
-rw-r--r--WordPress/src/main/res/color/dialog_compound_button.xml14
-rw-r--r--WordPress/src/main/res/color/media_grid_item_checkstate_text_selector.xml8
-rw-r--r--WordPress/src/main/res/color/nux_primary_button.xml5
-rw-r--r--WordPress/src/main/res/color/reader_count_text.xml7
-rw-r--r--WordPress/src/main/res/color/reader_follow_button_text.xml5
-rw-r--r--WordPress/src/main/res/color/reader_like_text.xml7
-rw-r--r--WordPress/src/main/res/color/related_posts_list_header.xml13
-rw-r--r--WordPress/src/main/res/color/related_posts_preview_header.xml13
-rw-r--r--WordPress/src/main/res/color/tab_text_color.xml5
-rw-r--r--WordPress/src/main/res/drawable-hdpi-v4/action_mode_confirm_checkmark.pngbin0 -> 3572 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi-v4/ic_media_play.pngbin0 -> 1163 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi-v4/tab_icon_create_gallery.pngbin0 -> 3650 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/arrow.pngbin0 -> 215 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/blavatar_placeholder.pngbin0 -> 2059 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/box_with_pages_bottom.pngbin0 -> 287 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/box_with_pages_page1.pngbin0 -> 364 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/box_with_pages_page2.pngbin0 -> 313 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/box_with_pages_page3.pngbin0 -> 338 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/box_with_pages_top.pngbin0 -> 929 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/btn_cab_done_default_wordpress.9.pngbin0 -> 111 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/btn_cab_done_focused_wordpress.9.pngbin0 -> 118 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/btn_cab_done_pressed_wordpress.9.pngbin0 -> 118 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/button_blue.9.pngbin0 -> 704 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/button_blue_disabled.9.pngbin0 -> 614 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/button_blue_focus.9.pngbin0 -> 909 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/button_darkgray.9.pngbin0 -> 677 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/button_darkgray_disabled.9.pngbin0 -> 565 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/button_darkgray_focus.9.pngbin0 -> 725 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_admin_site.pngbin0 -> 1476 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_admin_users.pngbin0 -> 998 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_edit.pngbin0 -> 1131 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_email.pngbin0 -> 976 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_eye_closed.pngbin0 -> 1024 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_eye_open.pngbin0 -> 955 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_info.pngbin0 -> 1114 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_lock.pngbin0 -> 809 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dashicon_wordpress_alt.pngbin0 -> 1445 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/dialog_full_holo_light.9.pngbin0 -> 1537 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/drake_empty_results.pngbin0 -> 20024 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/endlist_logo.pngbin0 -> 1698 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_closed.pngbin0 -> 3098 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_open.pngbin0 -> 3118 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_checkbox_empty.pngbin0 -> 3014 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_circles.pngbin0 -> 5734 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_circles_selected.pngbin0 -> 5737 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow.pngbin0 -> 3315 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow_selected.pngbin0 -> 3313 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_squares.pngbin0 -> 3589 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_squares_selected.pngbin0 -> 3588 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid.pngbin0 -> 3672 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid_selected.pngbin0 -> 3699 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled.pngbin0 -> 3388 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled_selected.pngbin0 -> 3387 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gallery_tablet_move_file.pngbin0 -> 3091 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gravatar_placeholder.pngbin0 -> 1708 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gridicon_create_dark.pngbin0 -> 416 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/gridicon_create_light.pngbin0 -> 422 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_approve.pngbin0 -> 489 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_approve_active.pngbin0 -> 488 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_camera.pngbin0 -> 781 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_comment.pngbin0 -> 1221 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_like.pngbin0 -> 512 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_like_active.pngbin0 -> 511 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_location_found.pngbin0 -> 705 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_location_off.pngbin0 -> 760 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_location_searching.pngbin0 -> 641 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_more.pngbin0 -> 14805 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_more_grey.pngbin0 -> 354 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_reply.pngbin0 -> 700 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_restore.pngbin0 -> 940 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_spam.pngbin0 -> 386 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_trash.pngbin0 -> 418 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_action_video.pngbin0 -> 377 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_add_blue_24dp.pngbin0 -> 181 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_add_grey600_24dp.pngbin0 -> 222 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_add_white_24dp.pngbin0 -> 223 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.pngbin0 -> 287 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_arrow_up_white_24dp.pngbin0 -> 1131 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_bell_grey.pngbin0 -> 15414 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_check_white_24dp.pngbin0 -> 309 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_close_grey600_24dp.pngbin0 -> 329 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_close_white_24dp.pngbin0 -> 324 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_comment.pngbin0 -> 697 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_comment_active.pngbin0 -> 697 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_comment_disabled.pngbin0 -> 1241 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.pngbin0 -> 287 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_create_white_24dp.pngbin0 -> 339 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_delete_white_24dp.pngbin0 -> 246 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_email_grey.pngbin0 -> 15287 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_grey_24dp.pngbin0 -> 405 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_white_24dp.pngbin0 -> 1516 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_genericon_web_white_24dp.pngbin0 -> 1856 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_level_indicator.pngbin0 -> 2057 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_like.pngbin0 -> 989 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_like_active.pngbin0 -> 988 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_like_disabled.pngbin0 -> 1623 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_location_on_grey600_24dp.pngbin0 -> 506 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_phone_grey.pngbin0 -> 15167 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_post_settings.pngbin0 -> 1506 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_refresh_white_24dp.pngbin0 -> 531 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_remove_red_eye_white_24dp.pngbin0 -> 585 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_reply_white_24dp.pngbin0 -> 350 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_report_white_24dp.pngbin0 -> 346 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_save_white_24dp.pngbin0 -> 341 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_search.pngbin0 -> 867 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_search_white_24dp.pngbin0 -> 504 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_send_grey600_24dp.pngbin0 -> 352 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_send_white_24dp.pngbin0 -> 351 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_settings_white_24dp.pngbin0 -> 1515 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_share_white_24dp.pngbin0 -> 506 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_me_normal.pngbin0 -> 894 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_me_pressed.pngbin0 -> 655 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_normal.pngbin0 -> 521 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_pressed.pngbin0 -> 448 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_reader_normal.pngbin0 -> 251 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_reader_pressed.pngbin0 -> 235 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_sites_normal.pngbin0 -> 1033 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_tab_sites_pressed.pngbin0 -> 878 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_theme_customize.pngbin0 -> 418 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_theme_details.pngbin0 -> 305 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_theme_loading.pngbin0 -> 155 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_theme_support.pngbin0 -> 432 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.pngbin0 -> 372 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.pngbin0 -> 379 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/list_focused_wordpress.9.pngbin0 -> 147 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/me_icon_account_settings.pngbin0 -> 1035 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/me_icon_app_settings.pngbin0 -> 375 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/me_icon_login_logout.pngbin0 -> 665 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/me_icon_my_profile.pngbin0 -> 686 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/me_icon_notifications.pngbin0 -> 748 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/me_icon_support.pngbin0 -> 956 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/media_audio.pngbin0 -> 2789 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/media_document.pngbin0 -> 3681 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/media_image_placeholder.pngbin0 -> 3998 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/media_powerpoint.pngbin0 -> 4551 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/media_spreadsheet.pngbin0 -> 4297 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/menu_dropdown_panel_wordpress.9.pngbin0 -> 1264 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_comments.pngbin0 -> 506 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_media.pngbin0 -> 565 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_pages.pngbin0 -> 345 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_posts.pngbin0 -> 329 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_settings.pngbin0 -> 1187 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_stats.pngbin0 -> 247 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_themes.pngbin0 -> 1075 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_view_admin.pngbin0 -> 1816 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_icon_view_site.pngbin0 -> 1519 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/my_site_no_sites_drake.pngbin0 -> 16135 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/note_icon_reply.pngbin0 -> 3662 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_alert_big.pngbin0 -> 1416 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_back.pngbin0 -> 233 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_clock.pngbin0 -> 17807 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_edit.pngbin0 -> 417 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_more.pngbin0 -> 902 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_publish.pngbin0 -> 362 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_restore.pngbin0 -> 677 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_scheduled.pngbin0 -> 244 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_star_active.pngbin0 -> 925 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_star_disabled.pngbin0 -> 1525 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_star_unactive.pngbin0 -> 925 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_stats.pngbin0 -> 201 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_sticky.pngbin0 -> 249 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_trash.pngbin0 -> 389 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_trashed.pngbin0 -> 249 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_view.pngbin0 -> 980 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/noticon_warning_big_grey.pngbin0 -> 1241 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/notification_icon.pngbin0 -> 719 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/nux_icon_wp.pngbin0 -> 3622 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/penandink.pngbin0 -> 2989 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/progress_bg_wordpress.9.pngbin0 -> 133 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/progress_primary_wordpress.9.pngbin0 -> 805 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/progress_secondary_wordpress.9.pngbin0 -> 139 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_comment.pngbin0 -> 435 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_comment_active.pngbin0 -> 434 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_dropdown_arrow.pngbin0 -> 217 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_like.pngbin0 -> 880 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_like_empty.pngbin0 -> 1278 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_like_empty_active.pngbin0 -> 1240 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/reader_tear.pngbin0 -> 522 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/right_shadow.9.pngbin0 -> 95 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/spinner_ab_default_wordpress.9.pngbin0 -> 376 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/spinner_ab_disabled_wordpress.9.pngbin0 -> 358 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/spinner_ab_focused_wordpress.9.pngbin0 -> 567 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/spinner_ab_pressed_wordpress.9.pngbin0 -> 508 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_chevron_down.pngbin0 -> 1208 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_chevron_right.pngbin0 -> 1193 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_categories.pngbin0 -> 1153 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_comments.pngbin0 -> 1159 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_comments_active.pngbin0 -> 14633 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_default_site_avatar.pngbin0 -> 2254 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_info.pngbin0 -> 1654 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_likes.pngbin0 -> 1278 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_tags.pngbin0 -> 1198 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_trophy.pngbin0 -> 1650 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_views.pngbin0 -> 1305 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_icon_visitors.pngbin0 -> 1310 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/stats_link.pngbin0 -> 1195 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/switch_site_button_icon.pngbin0 -> 621 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/tab_unselected_pressed_wordpress.9.pngbin0 -> 100 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/theme_icon_current.pngbin0 -> 3181 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/theme_icon_premium.pngbin0 -> 3408 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/theme_icon_tag_current.pngbin0 -> 4201 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/theme_icon_tag_premium.pngbin0 -> 5129 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/theme_loading_background.pngbin0 -> 491 bytes
-rw-r--r--WordPress/src/main/res/drawable-hdpi/video_thumbnail.pngbin0 -> 283 bytes
-rw-r--r--WordPress/src/main/res/drawable-nodpi/stats_widget_preview.pngbin0 -> 35762 bytes
-rw-r--r--WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml8
-rw-r--r--WordPress/src/main/res/drawable-v21/nux_flat_button_selector.xml10
-rw-r--r--WordPress/src/main/res/drawable-v21/nux_primary_button_selector.xml10
-rw-r--r--WordPress/src/main/res/drawable-v21/ripple_oval.xml9
-rw-r--r--WordPress/src/main/res/drawable-v21/stats_top_pager_button_selector.xml10
-rw-r--r--WordPress/src/main/res/drawable-xhdpi-v4/action_mode_confirm_checkmark.pngbin0 -> 3805 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi-v4/ic_media_play.pngbin0 -> 1617 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi-v4/tab_icon_create_gallery.pngbin0 -> 3653 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/arrow.pngbin0 -> 240 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/blavatar_placeholder.pngbin0 -> 2822 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/box_with_pages_bottom.pngbin0 -> 97 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/box_with_pages_page1.pngbin0 -> 167 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/box_with_pages_page2.pngbin0 -> 117 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/box_with_pages_page3.pngbin0 -> 133 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/box_with_pages_top.pngbin0 -> 752 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/btn_cab_done_default_wordpress.9.pngbin0 -> 122 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/btn_cab_done_focused_wordpress.9.pngbin0 -> 124 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/btn_cab_done_pressed_wordpress.9.pngbin0 -> 124 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/button_blue.9.pngbin0 -> 995 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/button_blue_disabled.9.pngbin0 -> 756 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/button_blue_focus.9.pngbin0 -> 1245 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/button_darkgray.9.pngbin0 -> 926 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/button_darkgray_disabled.9.pngbin0 -> 665 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/button_darkgray_focus.9.pngbin0 -> 1021 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_admin_site.pngbin0 -> 1864 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_admin_users.pngbin0 -> 1196 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_edit.pngbin0 -> 1404 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_email.pngbin0 -> 1118 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_eye_closed.pngbin0 -> 1205 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_eye_open.pngbin0 -> 1151 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_info.pngbin0 -> 1310 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_lock.pngbin0 -> 869 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/dashicon_wordpress_alt.pngbin0 -> 1935 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/drake_empty_results.pngbin0 -> 30167 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/endlist_logo.pngbin0 -> 2380 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_closed.pngbin0 -> 3186 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_open.pngbin0 -> 3219 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_checkbox_empty.pngbin0 -> 3047 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles.pngbin0 -> 6468 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles_selected.pngbin0 -> 6567 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow.pngbin0 -> 3464 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow_selected.pngbin0 -> 3464 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares.pngbin0 -> 3569 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares_selected.pngbin0 -> 3568 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid.pngbin0 -> 3653 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid_selected.pngbin0 -> 3665 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled.pngbin0 -> 3325 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled_selected.pngbin0 -> 3323 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gallery_tablet_move_file.pngbin0 -> 3182 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gravatar_placeholder.pngbin0 -> 2320 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gridicon_create_dark.pngbin0 -> 364 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/gridicon_create_light.pngbin0 -> 374 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_approve.pngbin0 -> 633 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_approve_active.pngbin0 -> 630 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_camera.pngbin0 -> 1023 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_comment.pngbin0 -> 1267 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_like.pngbin0 -> 613 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_like_active.pngbin0 -> 643 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_location_found.pngbin0 -> 994 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_location_off.pngbin0 -> 998 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_location_searching.pngbin0 -> 854 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_more.pngbin0 -> 14884 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_more_grey.pngbin0 -> 308 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_reply.pngbin0 -> 831 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_restore.pngbin0 -> 1268 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_spam.pngbin0 -> 474 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_trash.pngbin0 -> 474 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_action_video.pngbin0 -> 481 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_add_blue_24dp.pngbin0 -> 187 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.pngbin0 -> 199 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_add_white_24dp.pngbin0 -> 198 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.pngbin0 -> 336 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_arrow_up_white_24dp.pngbin0 -> 1167 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_bell_grey.pngbin0 -> 15593 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_check_white_24dp.pngbin0 -> 363 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.pngbin0 -> 400 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_close_white_24dp.pngbin0 -> 402 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_comment.pngbin0 -> 810 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_comment_active.pngbin0 -> 810 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_comment_disabled.pngbin0 -> 1377 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.pngbin0 -> 330 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_create_white_24dp.pngbin0 -> 378 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_delete_white_24dp.pngbin0 -> 270 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_email_grey.pngbin0 -> 15444 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_grey_24dp.pngbin0 -> 457 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_white_24dp.pngbin0 -> 1448 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_genericon_web_white_24dp.pngbin0 -> 2260 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_level_indicator.pngbin0 -> 1338 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_like.pngbin0 -> 1316 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_like_active.pngbin0 -> 1310 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_like_disabled.pngbin0 -> 1994 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_location_on_grey600_24dp.pngbin0 -> 600 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_phone_grey.pngbin0 -> 15211 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_post_settings.pngbin0 -> 1687 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.pngbin0 -> 637 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_24dp.pngbin0 -> 757 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_reply_white_24dp.pngbin0 -> 443 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_report_white_24dp.pngbin0 -> 379 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_save_white_24dp.pngbin0 -> 359 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_search.pngbin0 -> 1089 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_search_white_24dp.pngbin0 -> 591 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.pngbin0 -> 448 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_send_white_24dp.pngbin0 -> 446 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_settings_white_24dp.pngbin0 -> 1861 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_share_white_24dp.pngbin0 -> 625 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_me_normal.pngbin0 -> 1187 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_me_pressed.pngbin0 -> 994 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_normal.pngbin0 -> 679 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_pressed.pngbin0 -> 562 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_normal.pngbin0 -> 380 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_pressed.pngbin0 -> 377 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_normal.pngbin0 -> 1353 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_pressed.pngbin0 -> 1147 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_theme_customize.pngbin0 -> 673 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_theme_details.pngbin0 -> 461 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_theme_loading.pngbin0 -> 219 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_theme_support.pngbin0 -> 672 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.pngbin0 -> 391 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.pngbin0 -> 403 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/list_focused_wordpress.9.pngbin0 -> 150 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/me_icon_account_settings.pngbin0 -> 1312 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/me_icon_app_settings.pngbin0 -> 427 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/me_icon_login_logout.pngbin0 -> 609 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/me_icon_my_profile.pngbin0 -> 895 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/me_icon_notifications.pngbin0 -> 905 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/me_icon_support.pngbin0 -> 1329 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/media_audio.pngbin0 -> 4533 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/media_document.pngbin0 -> 3820 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/media_image_placeholder.pngbin0 -> 4279 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/media_item_placeholder.xml9
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/media_powerpoint.pngbin0 -> 4920 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/media_spreadsheet.pngbin0 -> 4283 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/menu_dropdown_panel_wordpress.9.pngbin0 -> 1937 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_comments.pngbin0 -> 494 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_media.pngbin0 -> 564 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_pages.pngbin0 -> 311 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_posts.pngbin0 -> 243 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_settings.pngbin0 -> 1453 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_stats.pngbin0 -> 213 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_themes.pngbin0 -> 1341 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_admin.pngbin0 -> 2481 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_site.pngbin0 -> 2048 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/my_site_no_sites_drake.pngbin0 -> 22487 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/note_icon_reply.pngbin0 -> 3522 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_alert_big.pngbin0 -> 1854 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_back.pngbin0 -> 235 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_clock.pngbin0 -> 17721 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_edit.pngbin0 -> 460 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_more.pngbin0 -> 908 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_publish.pngbin0 -> 297 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_restore.pngbin0 -> 855 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_scheduled.pngbin0 -> 231 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_star_active.pngbin0 -> 1143 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_star_disabled.pngbin0 -> 1889 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_star_unactive.pngbin0 -> 1143 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_stats.pngbin0 -> 155 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_sticky.pngbin0 -> 232 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_trash.pngbin0 -> 295 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_trashed.pngbin0 -> 230 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_view.pngbin0 -> 956 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/noticon_warning_big_grey.pngbin0 -> 1569 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/notification_icon.pngbin0 -> 970 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/nux_icon_wp.pngbin0 -> 4533 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/penandink.pngbin0 -> 2671 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/progress_bg_wordpress.9.pngbin0 -> 134 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/progress_primary_wordpress.9.pngbin0 -> 1133 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/progress_secondary_wordpress.9.pngbin0 -> 134 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_comment.pngbin0 -> 515 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_comment_active.pngbin0 -> 511 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_dropdown_arrow.pngbin0 -> 238 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_like.pngbin0 -> 1177 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_like_empty.pngbin0 -> 1666 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_like_empty_active.pngbin0 -> 1653 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/reader_tear.pngbin0 -> 852 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/rppreview1.pngbin0 -> 65832 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/rppreview2.pngbin0 -> 58696 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/rppreview3.pngbin0 -> 80160 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/spinner_ab_default_wordpress.9.pngbin0 -> 476 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/spinner_ab_disabled_wordpress.9.pngbin0 -> 457 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/spinner_ab_focused_wordpress.9.pngbin0 -> 729 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/spinner_ab_pressed_wordpress.9.pngbin0 -> 666 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_chevron_down.pngbin0 -> 1424 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_chevron_right.pngbin0 -> 1244 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_categories.pngbin0 -> 1162 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_comments.pngbin0 -> 1191 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_comments_active.pngbin0 -> 14691 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_default_site_avatar.pngbin0 -> 2799 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_info.pngbin0 -> 2013 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_likes.pngbin0 -> 1369 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_tags.pngbin0 -> 1245 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_trophy.pngbin0 -> 1836 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_views.pngbin0 -> 1421 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_icon_visitors.pngbin0 -> 1392 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/stats_link.pngbin0 -> 1187 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/switch_site_button_icon.pngbin0 -> 764 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/theme_icon_current.pngbin0 -> 1261 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/theme_icon_premium.pngbin0 -> 3641 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_current.pngbin0 -> 1930 bytes
-rw-r--r--WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_premium.pngbin0 -> 5651 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi-v4/action_mode_confirm_checkmark.pngbin0 -> 1562 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi-v4/ic_media_play.pngbin0 -> 1183 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi-v4/tab_icon_create_gallery.pngbin0 -> 3579 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/arrow.pngbin0 -> 311 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/blavatar_placeholder.pngbin0 -> 4475 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/box_with_pages_bottom.pngbin0 -> 100 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page1.pngbin0 -> 186 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page2.pngbin0 -> 127 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page3.pngbin0 -> 154 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/box_with_pages_top.pngbin0 -> 1171 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/button_blue.9.pngbin0 -> 1495 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/button_blue_disabled.9.pngbin0 -> 1047 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/button_blue_focus.9.pngbin0 -> 1801 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/button_darkgray.9.pngbin0 -> 1370 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/button_darkgray_disabled.9.pngbin0 -> 1023 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/button_darkgray_focus.9.pngbin0 -> 1435 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_site.pngbin0 -> 2569 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_users.pngbin0 -> 1543 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_edit.pngbin0 -> 1749 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_email.pngbin0 -> 1546 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_closed.pngbin0 -> 1645 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_open.pngbin0 -> 1524 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_info.pngbin0 -> 1840 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_lock.pngbin0 -> 1180 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/dashicon_wordpress_alt.pngbin0 -> 2894 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/endlist_logo.pngbin0 -> 3805 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_closed.pngbin0 -> 1559 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_open.pngbin0 -> 1627 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_checkbox_empty.pngbin0 -> 1173 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles.pngbin0 -> 8174 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles_selected.pngbin0 -> 8289 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow.pngbin0 -> 1869 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow_selected.pngbin0 -> 1869 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares.pngbin0 -> 3482 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares_selected.pngbin0 -> 3473 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid.pngbin0 -> 1537 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid_selected.pngbin0 -> 1567 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled.pngbin0 -> 3323 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled_selected.pngbin0 -> 3318 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gallery_tablet_move_file.pngbin0 -> 1200 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gravatar_placeholder.pngbin0 -> 3347 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gridicon_create_dark.pngbin0 -> 475 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/gridicon_create_light.pngbin0 -> 525 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_approve.pngbin0 -> 851 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_approve_active.pngbin0 -> 851 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_camera.pngbin0 -> 1504 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_comment.pngbin0 -> 1473 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_like.pngbin0 -> 911 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_like_active.pngbin0 -> 908 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_location_found.pngbin0 -> 1432 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_location_off.pngbin0 -> 1423 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_location_searching.pngbin0 -> 1202 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_more.pngbin0 -> 15338 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_more_grey.pngbin0 -> 467 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_reply.pngbin0 -> 1120 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_restore.pngbin0 -> 1787 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_spam.pngbin0 -> 565 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_trash.pngbin0 -> 606 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_action_video.pngbin0 -> 666 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_add_blue_24dp.pngbin0 -> 234 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.pngbin0 -> 223 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_add_white_24dp.pngbin0 -> 222 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.pngbin0 -> 410 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_arrow_up_white_24dp.pngbin0 -> 1269 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_bell_grey.pngbin0 -> 15882 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_check_white_24dp.pngbin0 -> 460 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.pngbin0 -> 484 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_close_white_24dp.pngbin0 -> 492 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_comment.pngbin0 -> 1219 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_comment_active.pngbin0 -> 1220 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_comment_disabled.pngbin0 -> 1941 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.pngbin0 -> 436 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_create_white_24dp.pngbin0 -> 490 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.pngbin0 -> 338 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_email_grey.pngbin0 -> 15683 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_grey_24dp.pngbin0 -> 616 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_white_24dp.pngbin0 -> 1962 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_genericon_web_white_24dp.pngbin0 -> 3205 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_level_indicator.pngbin0 -> 1495 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_like.pngbin0 -> 2049 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_like_active.pngbin0 -> 2045 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_like_disabled.pngbin0 -> 2928 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_location_on_grey600_24dp.pngbin0 -> 876 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_phone_grey.pngbin0 -> 15390 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_post_settings.pngbin0 -> 2050 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.pngbin0 -> 875 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_24dp.pngbin0 -> 1033 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_reply_white_24dp.pngbin0 -> 567 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_report_white_24dp.pngbin0 -> 461 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_save_white_24dp.pngbin0 -> 489 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_search.pngbin0 -> 1515 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_search_white_24dp.pngbin0 -> 871 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.pngbin0 -> 565 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_send_white_24dp.pngbin0 -> 562 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.pngbin0 -> 3120 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_share_white_24dp.pngbin0 -> 857 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_normal.pngbin0 -> 1678 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_pressed.pngbin0 -> 1455 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_normal.pngbin0 -> 936 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_pressed.pngbin0 -> 808 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_normal.pngbin0 -> 387 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_pressed.pngbin0 -> 380 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_normal.pngbin0 -> 2046 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_pressed.pngbin0 -> 1756 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_theme_customize.pngbin0 -> 752 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_theme_details.pngbin0 -> 549 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_theme_loading.pngbin0 -> 227 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_theme_support.pngbin0 -> 796 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.pngbin0 -> 516 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.pngbin0 -> 552 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/me_icon_account_settings.pngbin0 -> 2059 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/me_icon_app_settings.pngbin0 -> 642 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/me_icon_login_logout.pngbin0 -> 822 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/me_icon_my_profile.pngbin0 -> 1395 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/me_icon_notifications.pngbin0 -> 1392 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/me_icon_support.pngbin0 -> 1953 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/media_audio.pngbin0 -> 7792 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/media_document.pngbin0 -> 2018 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/media_image_placeholder.pngbin0 -> 3051 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/media_item_placeholder.xml9
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/media_powerpoint.pngbin0 -> 3652 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/media_spreadsheet.pngbin0 -> 2296 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_comments.pngbin0 -> 791 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_media.pngbin0 -> 914 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_pages.pngbin0 -> 456 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_posts.pngbin0 -> 364 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_settings.pngbin0 -> 2173 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_stats.pngbin0 -> 382 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_themes.pngbin0 -> 2046 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_admin.pngbin0 -> 3945 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_site.pngbin0 -> 3309 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/my_site_no_sites_drake.pngbin0 -> 35813 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/note_icon_reply.pngbin0 -> 3632 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_alert_big.pngbin0 -> 2957 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_back.pngbin0 -> 337 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_clock.pngbin0 -> 17909 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_edit.pngbin0 -> 616 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_more.pngbin0 -> 1044 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_publish.pngbin0 -> 422 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_restore.pngbin0 -> 1266 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_scheduled.pngbin0 -> 307 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_star_active.pngbin0 -> 1444 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_star_disabled.pngbin0 -> 2422 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_star_unactive.pngbin0 -> 1444 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_stats.pngbin0 -> 210 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_sticky.pngbin0 -> 309 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_trash.pngbin0 -> 481 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_trashed.pngbin0 -> 306 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_view.pngbin0 -> 1041 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/noticon_warning_big_grey.pngbin0 -> 2424 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/notification_icon.pngbin0 -> 1477 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/nux_icon_wp.pngbin0 -> 7041 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/penandink.pngbin0 -> 7217 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_comment.pngbin0 -> 737 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_comment_active.pngbin0 -> 743 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_dropdown_arrow.pngbin0 -> 309 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_like.pngbin0 -> 1835 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_like_empty.pngbin0 -> 2604 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_like_empty_active.pngbin0 -> 2608 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/reader_tear.pngbin0 -> 1283 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/rppreview1.pngbin0 -> 139608 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/rppreview2.pngbin0 -> 107176 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/rppreview3.pngbin0 -> 170844 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_chevron_down.pngbin0 -> 1337 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_chevron_right.pngbin0 -> 1244 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_categories.pngbin0 -> 1256 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments.pngbin0 -> 1260 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments_active.pngbin0 -> 14757 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_default_site_avatar.pngbin0 -> 3862 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_info.pngbin0 -> 2558 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_likes.pngbin0 -> 1519 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_tags.pngbin0 -> 1359 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_trophy.pngbin0 -> 2162 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_views.pngbin0 -> 1558 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_icon_visitors.pngbin0 -> 1570 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/stats_link.pngbin0 -> 1318 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/switch_site_button_icon.pngbin0 -> 1079 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/theme_icon_current.pngbin0 -> 1421 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/theme_icon_premium.pngbin0 -> 2091 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_current.pngbin0 -> 2330 bytes
-rw-r--r--WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_premium.pngbin0 -> 3788 bytes
-rw-r--r--WordPress/src/main/res/drawable/badge.xml5
-rw-r--r--WordPress/src/main/res/drawable/badge_normal.xml8
-rw-r--r--WordPress/src/main/res/drawable/badge_pressed.xml8
-rw-r--r--WordPress/src/main/res/drawable/btn_cab_done_wordpress.xml27
-rw-r--r--WordPress/src/main/res/drawable/calypso_bordered_bg.xml14
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_background.xml13
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_button.xml7
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_button_end.xml7
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_button_start.xml7
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_normal.xml11
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_normal_start.xml11
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_selected.xml8
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_selected_end.xml11
-rw-r--r--WordPress/src/main/res/drawable/calypso_segmented_control_selected_start.xml11
-rw-r--r--WordPress/src/main/res/drawable/comment_reply_background.xml24
-rw-r--r--WordPress/src/main/res/drawable/comment_reply_unapproved_background.xml22
-rw-r--r--WordPress/src/main/res/drawable/comment_unapproved_background.xml16
-rw-r--r--WordPress/src/main/res/drawable/dialog_info_button_background.xml8
-rw-r--r--WordPress/src/main/res/drawable/fab_menu_label_background.xml11
-rw-r--r--WordPress/src/main/res/drawable/gallery_circles_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/gallery_slideshow_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/gallery_squares_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/gallery_thumbnail_grid_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/gallery_tiled_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/gridicons_clipboard.xml9
-rw-r--r--WordPress/src/main/res/drawable/gridicons_cog.xml9
-rw-r--r--WordPress/src/main/res/drawable/gridicons_cog_light.xml9
-rw-r--r--WordPress/src/main/res/drawable/gridicons_external.xml9
-rw-r--r--WordPress/src/main/res/drawable/gridicons_history.xml12
-rw-r--r--WordPress/src/main/res/drawable/gridicons_search.xml9
-rw-r--r--WordPress/src/main/res/drawable/gridicons_search_light.xml9
-rw-r--r--WordPress/src/main/res/drawable/gridicons_trash.xml9
-rw-r--r--WordPress/src/main/res/drawable/ic_select_all_white_24dp.xml9
-rw-r--r--WordPress/src/main/res/drawable/indicator_circle_selected.xml8
-rw-r--r--WordPress/src/main/res/drawable/indicator_circle_unselected.xml5
-rw-r--r--WordPress/src/main/res/drawable/invites_border.xml6
-rw-r--r--WordPress/src/main/res/drawable/list_divider.xml10
-rw-r--r--WordPress/src/main/res/drawable/main_tab_me.xml5
-rw-r--r--WordPress/src/main/res/drawable/main_tab_notifications.xml5
-rw-r--r--WordPress/src/main/res/drawable/main_tab_reader.xml5
-rw-r--r--WordPress/src/main/res/drawable/main_tab_sites.xml5
-rw-r--r--WordPress/src/main/res/drawable/media_blue_button_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/media_gallery_checkbox_selector.xml7
-rw-r--r--WordPress/src/main/res/drawable/media_gallery_grid_cell.xml25
-rw-r--r--WordPress/src/main/res/drawable/media_gallery_option_default.xml8
-rw-r--r--WordPress/src/main/res/drawable/media_gallery_option_selected.xml8
-rw-r--r--WordPress/src/main/res/drawable/media_gallery_option_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/media_grid_item_checkstate_selector.xml7
-rw-r--r--WordPress/src/main/res/drawable/media_item_frame_selector.xml11
-rw-r--r--WordPress/src/main/res/drawable/media_picker_background.xml9
-rw-r--r--WordPress/src/main/res/drawable/moderate_button_selector.xml28
-rw-r--r--WordPress/src/main/res/drawable/my_site_add_button.xml13
-rw-r--r--WordPress/src/main/res/drawable/my_site_add_button_text_color_selector.xml5
-rw-r--r--WordPress/src/main/res/drawable/new_editor_promo_header.xml33
-rw-r--r--WordPress/src/main/res/drawable/notifications_list_divider.xml10
-rw-r--r--WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml8
-rw-r--r--WordPress/src/main/res/drawable/nux_flat_button_grey_text_selector.xml5
-rw-r--r--WordPress/src/main/res/drawable/nux_flat_button_selector.xml15
-rw-r--r--WordPress/src/main/res/drawable/nux_flat_button_text_selector.xml5
-rw-r--r--WordPress/src/main/res/drawable/nux_primary_button_selector.xml31
-rw-r--r--WordPress/src/main/res/drawable/page_item_background.xml22
-rw-r--r--WordPress/src/main/res/drawable/passcode_logo.xml9
-rw-r--r--WordPress/src/main/res/drawable/people_list_divider.xml8
-rw-r--r--WordPress/src/main/res/drawable/plans_business_active.xml56
-rw-r--r--WordPress/src/main/res/drawable/plans_customize.xml35
-rw-r--r--WordPress/src/main/res/drawable/plans_premium_themes.xml35
-rw-r--r--WordPress/src/main/res/drawable/plans_video_upload.xml37
-rw-r--r--WordPress/src/main/res/drawable/preferences_divider.xml18
-rw-r--r--WordPress/src/main/res/drawable/progress_horizontal_wordpress.xml35
-rw-r--r--WordPress/src/main/res/drawable/progressbar_horizontal.xml11
-rw-r--r--WordPress/src/main/res/drawable/reader_button_comment.xml6
-rw-r--r--WordPress/src/main/res/drawable/reader_button_comment_like.xml7
-rw-r--r--WordPress/src/main/res/drawable/reader_button_like.xml6
-rw-r--r--WordPress/src/main/res/drawable/reader_follow.xml13
-rw-r--r--WordPress/src/main/res/drawable/reader_following.xml13
-rw-r--r--WordPress/src/main/res/drawable/reader_gap_marker_background.xml15
-rw-r--r--WordPress/src/main/res/drawable/reader_new_posts_bar_background.xml9
-rw-r--r--WordPress/src/main/res/drawable/reader_photo_count_background.xml12
-rw-r--r--WordPress/src/main/res/drawable/reader_post_background.xml12
-rw-r--r--WordPress/src/main/res/drawable/reader_related_posts_background.xml7
-rw-r--r--WordPress/src/main/res/drawable/reader_tear_repeat.xml5
-rw-r--r--WordPress/src/main/res/drawable/related_posts_divider.xml13
-rw-r--r--WordPress/src/main/res/drawable/scrollbar_transparent_black.xml15
-rw-r--r--WordPress/src/main/res/drawable/scrollbar_transparent_white.xml7
-rw-r--r--WordPress/src/main/res/drawable/selectable_background_wordpress.xml25
-rw-r--r--WordPress/src/main/res/drawable/shape_oval_blue.xml4
-rw-r--r--WordPress/src/main/res/drawable/shape_oval_blue_white_stroke.xml5
-rw-r--r--WordPress/src/main/res/drawable/shape_oval_grey.xml5
-rw-r--r--WordPress/src/main/res/drawable/shape_oval_grey_light.xml4
-rw-r--r--WordPress/src/main/res/drawable/shape_oval_orange.xml4
-rw-r--r--WordPress/src/main/res/drawable/shape_oval_translucent.xml4
-rw-r--r--WordPress/src/main/res/drawable/spinner_background_ab_wordpress.xml28
-rw-r--r--WordPress/src/main/res/drawable/stats_barchart_no_activity_background.xml21
-rw-r--r--WordPress/src/main/res/drawable/stats_list_item_background.xml17
-rw-r--r--WordPress/src/main/res/drawable/stats_list_item_child_background.xml17
-rw-r--r--WordPress/src/main/res/drawable/stats_list_item_expanded_background.xml17
-rw-r--r--WordPress/src/main/res/drawable/stats_pager_button_blue_light.xml8
-rw-r--r--WordPress/src/main/res/drawable/stats_pager_button_grey.xml8
-rw-r--r--WordPress/src/main/res/drawable/stats_pager_button_white.xml15
-rw-r--r--WordPress/src/main/res/drawable/stats_pagination_item_background.xml18
-rw-r--r--WordPress/src/main/res/drawable/stats_top_pager_button_selector.xml6
-rw-r--r--WordPress/src/main/res/drawable/stats_view_all_button_background.xml18
-rw-r--r--WordPress/src/main/res/drawable/stats_visitors_and_views_button_blue_light.xml18
-rw-r--r--WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_blue_light.xml18
-rw-r--r--WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_white.xml11
-rw-r--r--WordPress/src/main/res/drawable/stats_visitors_and_views_button_white.xml17
-rw-r--r--WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_primary.xml14
-rw-r--r--WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_secondary.xml14
-rw-r--r--WordPress/src/main/res/drawable/stats_white_background.xml23
-rw-r--r--WordPress/src/main/res/drawable/stats_widget_background.xml23
-rw-r--r--WordPress/src/main/res/drawable/stats_widget_header_background.xml12
-rw-r--r--WordPress/src/main/res/drawable/stats_widget_promo_header.xml24
-rw-r--r--WordPress/src/main/res/drawable/tab_divider_wordpress.xml6
-rw-r--r--WordPress/src/main/res/drawable/theme_activate_button_selector.xml8
-rw-r--r--WordPress/src/main/res/drawable/theme_feature_text_bg.xml5
-rw-r--r--WordPress/src/main/res/drawable/theme_loading.xml12
-rw-r--r--WordPress/src/main/res/drawable/view_post_toolbar.xml15
-rw-r--r--WordPress/src/main/res/drawable/view_post_toolbar_bottom.xml15
-rw-r--r--WordPress/src/main/res/layout-sw600dp-port/theme_details_fragment.xml197
-rw-r--r--WordPress/src/main/res/layout-sw600dp/media_gallery_activity.xml45
-rw-r--r--WordPress/src/main/res/layout-sw600dp/media_grid_progress.xml26
-rw-r--r--WordPress/src/main/res/layout-sw600dp/theme_details_fragment.xml194
-rw-r--r--WordPress/src/main/res/layout-sw600dp/theme_grid_cardview_header.xml147
-rw-r--r--WordPress/src/main/res/layout-sw720dp/stats_activity.xml264
-rw-r--r--WordPress/src/main/res/layout-sw720dp/stats_insights_all_time_item.xml203
-rw-r--r--WordPress/src/main/res/layout-sw720dp/theme_details_fragment.xml195
-rw-r--r--WordPress/src/main/res/layout/about_activity.xml88
-rw-r--r--WordPress/src/main/res/layout/actionbar_add_media.xml33
-rw-r--r--WordPress/src/main/res/layout/actionbar_add_media_cell.xml9
-rw-r--r--WordPress/src/main/res/layout/add_category.xml76
-rw-r--r--WordPress/src/main/res/layout/add_quickpress_shortcut.xml14
-rw-r--r--WordPress/src/main/res/layout/alert_http_auth.xml21
-rw-r--r--WordPress/src/main/res/layout/blog_preferences.xml139
-rw-r--r--WordPress/src/main/res/layout/categories_row.xml25
-rw-r--r--WordPress/src/main/res/layout/categories_row_parent.xml25
-rw-r--r--WordPress/src/main/res/layout/category_button.xml10
-rw-r--r--WordPress/src/main/res/layout/category_select_button.xml14
-rw-r--r--WordPress/src/main/res/layout/comment_action_footer.xml100
-rw-r--r--WordPress/src/main/res/layout/comment_activity.xml18
-rw-r--r--WordPress/src/main/res/layout/comment_activity_detail.xml6
-rw-r--r--WordPress/src/main/res/layout/comment_detail_fragment.xml142
-rw-r--r--WordPress/src/main/res/layout/comment_edit_activity.xml74
-rw-r--r--WordPress/src/main/res/layout/comment_list_fragment.xml6
-rw-r--r--WordPress/src/main/res/layout/comment_listitem.xml116
-rw-r--r--WordPress/src/main/res/layout/create_blog_fragment.xml173
-rw-r--r--WordPress/src/main/res/layout/date_range_dialog.xml53
-rw-r--r--WordPress/src/main/res/layout/delete_site_dialog.xml15
-rw-r--r--WordPress/src/main/res/layout/detail_list_preference.xml56
-rw-r--r--WordPress/src/main/res/layout/detail_list_preference_title.xml21
-rw-r--r--WordPress/src/main/res/layout/domain_removal_preference.xml15
-rw-r--r--WordPress/src/main/res/layout/edit_post_preview_fragment.xml26
-rw-r--r--WordPress/src/main/res/layout/edit_post_settings_fragment.xml154
-rw-r--r--WordPress/src/main/res/layout/endlist_indicator.xml37
-rw-r--r--WordPress/src/main/res/layout/filter_spinner_item.xml12
-rw-r--r--WordPress/src/main/res/layout/filtered_list_component.xml76
-rw-r--r--WordPress/src/main/res/layout/help_activity_with_helpshift.xml78
-rw-r--r--WordPress/src/main/res/layout/home_row.xml67
-rw-r--r--WordPress/src/main/res/layout/invite_username_button.xml27
-rw-r--r--WordPress/src/main/res/layout/learn_more_pref.xml38
-rw-r--r--WordPress/src/main/res/layout/learn_more_pref_screen.xml16
-rw-r--r--WordPress/src/main/res/layout/list_editor.xml64
-rw-r--r--WordPress/src/main/res/layout/list_footer_progress.xml16
-rw-r--r--WordPress/src/main/res/layout/logviewer_activity.xml9
-rw-r--r--WordPress/src/main/res/layout/logviewer_listitem.xml29
-rw-r--r--WordPress/src/main/res/layout/magic_link_request_fragment.xml112
-rw-r--r--WordPress/src/main/res/layout/magic_link_sent_fragment.xml110
-rw-r--r--WordPress/src/main/res/layout/main_activity.xml42
-rw-r--r--WordPress/src/main/res/layout/me_fragment.xml205
-rw-r--r--WordPress/src/main/res/layout/media_browser_activity.xml30
-rw-r--r--WordPress/src/main/res/layout/media_edit_fragment.xml101
-rw-r--r--WordPress/src/main/res/layout/media_gallery_activity.xml25
-rw-r--r--WordPress/src/main/res/layout/media_gallery_column_checkbox.xml10
-rw-r--r--WordPress/src/main/res/layout/media_gallery_edit_fragment.xml24
-rw-r--r--WordPress/src/main/res/layout/media_gallery_item.xml79
-rw-r--r--WordPress/src/main/res/layout/media_gallery_picker_layout.xml12
-rw-r--r--WordPress/src/main/res/layout/media_gallery_settings_fragment.xml198
-rw-r--r--WordPress/src/main/res/layout/media_grid_fragment.xml81
-rw-r--r--WordPress/src/main/res/layout/media_grid_image_local.xml6
-rw-r--r--WordPress/src/main/res/layout/media_grid_image_network.xml6
-rw-r--r--WordPress/src/main/res/layout/media_grid_item.xml88
-rw-r--r--WordPress/src/main/res/layout/media_grid_progress.xml25
-rw-r--r--WordPress/src/main/res/layout/media_item_wp_image.xml39
-rw-r--r--WordPress/src/main/res/layout/media_item_wp_video.xml52
-rw-r--r--WordPress/src/main/res/layout/media_listitem_details.xml118
-rw-r--r--WordPress/src/main/res/layout/media_picker_activity.xml22
-rw-r--r--WordPress/src/main/res/layout/media_picker_fragment.xml30
-rw-r--r--WordPress/src/main/res/layout/my_profile_dialog.xml35
-rw-r--r--WordPress/src/main/res/layout/my_profile_fragment.xml78
-rw-r--r--WordPress/src/main/res/layout/my_site_fragment.xml469
-rw-r--r--WordPress/src/main/res/layout/new_account_activity.xml13
-rw-r--r--WordPress/src/main/res/layout/new_account_user_fragment_screen.xml255
-rw-r--r--WordPress/src/main/res/layout/new_blog_activity.xml14
-rw-r--r--WordPress/src/main/res/layout/new_edit_post_activity.xml6
-rw-r--r--WordPress/src/main/res/layout/note_block_basic.xml21
-rw-r--r--WordPress/src/main/res/layout/note_block_comment_user.xml108
-rw-r--r--WordPress/src/main/res/layout/note_block_footer.xml44
-rw-r--r--WordPress/src/main/res/layout/note_block_header.xml100
-rw-r--r--WordPress/src/main/res/layout/note_block_user.xml82
-rw-r--r--WordPress/src/main/res/layout/notifications_detail_activity.xml6
-rw-r--r--WordPress/src/main/res/layout/notifications_fragment_detail_list.xml21
-rw-r--r--WordPress/src/main/res/layout/notifications_fragment_notes_list.xml147
-rw-r--r--WordPress/src/main/res/layout/notifications_list_item.xml176
-rw-r--r--WordPress/src/main/res/layout/notifications_settings_activity.xml35
-rw-r--r--WordPress/src/main/res/layout/notifications_settings_switch.xml54
-rw-r--r--WordPress/src/main/res/layout/number_picker_dialog.xml65
-rw-r--r--WordPress/src/main/res/layout/page_item.xml123
-rw-r--r--WordPress/src/main/res/layout/people_invite_error_view.xml11
-rw-r--r--WordPress/src/main/res/layout/people_invite_fragment.xml183
-rw-r--r--WordPress/src/main/res/layout/people_list_fragment.xml26
-rw-r--r--WordPress/src/main/res/layout/people_list_row.xml69
-rw-r--r--WordPress/src/main/res/layout/people_management_activity.xml16
-rw-r--r--WordPress/src/main/res/layout/person_detail_fragment.xml125
-rw-r--r--WordPress/src/main/res/layout/plan_feature_item.xml42
-rw-r--r--WordPress/src/main/res/layout/plan_fragment.xml63
-rw-r--r--WordPress/src/main/res/layout/plan_post_purchase_activity.xml103
-rw-r--r--WordPress/src/main/res/layout/plan_post_purchase_fragment.xml61
-rw-r--r--WordPress/src/main/res/layout/plan_section_title.xml27
-rw-r--r--WordPress/src/main/res/layout/plans_activity.xml75
-rw-r--r--WordPress/src/main/res/layout/popup_menu_item.xml25
-rw-r--r--WordPress/src/main/res/layout/post_cardview.xml164
-rw-r--r--WordPress/src/main/res/layout/post_list_activity.xml17
-rw-r--r--WordPress/src/main/res/layout/post_list_button.xml29
-rw-r--r--WordPress/src/main/res/layout/post_list_fragment.xml86
-rw-r--r--WordPress/src/main/res/layout/post_location_settings.xml26
-rw-r--r--WordPress/src/main/res/layout/post_location_settings_add.xml20
-rw-r--r--WordPress/src/main/res/layout/post_location_settings_search.xml29
-rw-r--r--WordPress/src/main/res/layout/post_location_settings_view.xml57
-rw-r--r--WordPress/src/main/res/layout/post_preview_activity.xml89
-rw-r--r--WordPress/src/main/res/layout/post_preview_fragment.xml13
-rw-r--r--WordPress/src/main/res/layout/preference_category.xml20
-rw-r--r--WordPress/src/main/res/layout/preference_coordinator.xml8
-rw-r--r--WordPress/src/main/res/layout/preference_layout.xml41
-rw-r--r--WordPress/src/main/res/layout/progressbar.xml6
-rw-r--r--WordPress/src/main/res/layout/promo_dialog.xml56
-rw-r--r--WordPress/src/main/res/layout/reader_activity_comment_list.xml73
-rw-r--r--WordPress/src/main/res/layout/reader_activity_photo_viewer.xml30
-rw-r--r--WordPress/src/main/res/layout/reader_activity_post_list.xml15
-rw-r--r--WordPress/src/main/res/layout/reader_activity_post_pager.xml26
-rw-r--r--WordPress/src/main/res/layout/reader_activity_subs.xml81
-rw-r--r--WordPress/src/main/res/layout/reader_activity_userlist.xml20
-rw-r--r--WordPress/src/main/res/layout/reader_cardview_post.xml231
-rw-r--r--WordPress/src/main/res/layout/reader_cardview_xpost.xml78
-rw-r--r--WordPress/src/main/res/layout/reader_comments_post_header_view.xml87
-rw-r--r--WordPress/src/main/res/layout/reader_empty_view.xml87
-rw-r--r--WordPress/src/main/res/layout/reader_follow_button.xml33
-rw-r--r--WordPress/src/main/res/layout/reader_fragment_list.xml29
-rw-r--r--WordPress/src/main/res/layout/reader_fragment_photo_viewer.xml6
-rw-r--r--WordPress/src/main/res/layout/reader_fragment_post_cards.xml60
-rw-r--r--WordPress/src/main/res/layout/reader_fragment_post_detail.xml132
-rw-r--r--WordPress/src/main/res/layout/reader_gap_marker_view.xml41
-rw-r--r--WordPress/src/main/res/layout/reader_icon_count_view.xml26
-rw-r--r--WordPress/src/main/res/layout/reader_include_comment_box.xml76
-rw-r--r--WordPress/src/main/res/layout/reader_include_post_detail_content.xml92
-rw-r--r--WordPress/src/main/res/layout/reader_include_post_detail_footer.xml75
-rw-r--r--WordPress/src/main/res/layout/reader_include_post_detail_header.xml62
-rw-r--r--WordPress/src/main/res/layout/reader_like_avatar.xml10
-rw-r--r--WordPress/src/main/res/layout/reader_listitem_blog.xml65
-rw-r--r--WordPress/src/main/res/layout/reader_listitem_comment.xml135
-rw-r--r--WordPress/src/main/res/layout/reader_listitem_suggestion.xml36
-rw-r--r--WordPress/src/main/res/layout/reader_listitem_tag.xml38
-rw-r--r--WordPress/src/main/res/layout/reader_listitem_user.xml53
-rw-r--r--WordPress/src/main/res/layout/reader_photo_view.xml31
-rw-r--r--WordPress/src/main/res/layout/reader_popup_menu_item.xml24
-rw-r--r--WordPress/src/main/res/layout/reader_related_post.xml51
-rw-r--r--WordPress/src/main/res/layout/reader_related_post_divider.xml5
-rw-r--r--WordPress/src/main/res/layout/reader_site_header_view.xml116
-rw-r--r--WordPress/src/main/res/layout/reader_tag_header_view.xml46
-rw-r--r--WordPress/src/main/res/layout/reader_thumbnail_strip.xml26
-rw-r--r--WordPress/src/main/res/layout/reader_thumbnail_strip_image.xml8
-rw-r--r--WordPress/src/main/res/layout/reader_thumbnail_strip_labels.xml24
-rw-r--r--WordPress/src/main/res/layout/related_posts_dialog.xml181
-rw-r--r--WordPress/src/main/res/layout/role_list_row.xml41
-rw-r--r--WordPress/src/main/res/layout/select_categories.xml36
-rw-r--r--WordPress/src/main/res/layout/share_intent_receiver_dialog.xml58
-rw-r--r--WordPress/src/main/res/layout/signin_dialog_fragment.xml105
-rw-r--r--WordPress/src/main/res/layout/signin_fragment.xml330
-rw-r--r--WordPress/src/main/res/layout/simple_spinner_item.xml16
-rw-r--r--WordPress/src/main/res/layout/site_picker_activity.xml15
-rw-r--r--WordPress/src/main/res/layout/site_picker_listitem.xml61
-rw-r--r--WordPress/src/main/res/layout/spinner_menu_dropdown_item.xml12
-rw-r--r--WordPress/src/main/res/layout/start_over_preference.xml61
-rw-r--r--WordPress/src/main/res/layout/start_over_preference_button.xml15
-rw-r--r--WordPress/src/main/res/layout/stats_activity.xml217
-rw-r--r--WordPress/src/main/res/layout/stats_activity_single_post_details.xml273
-rw-r--r--WordPress/src/main/res/layout/stats_activity_view_all.xml44
-rw-r--r--WordPress/src/main/res/layout/stats_bar_graph_empty.xml8
-rw-r--r--WordPress/src/main/res/layout/stats_empty_module_placeholder.xml16
-rw-r--r--WordPress/src/main/res/layout/stats_expandable_list_fragment.xml143
-rw-r--r--WordPress/src/main/res/layout/stats_insights_all_time_item.xml219
-rw-r--r--WordPress/src/main/res/layout/stats_insights_generic_fragment.xml55
-rw-r--r--WordPress/src/main/res/layout/stats_insights_header_line.xml5
-rw-r--r--WordPress/src/main/res/layout/stats_insights_latest_post_item.xml60
-rw-r--r--WordPress/src/main/res/layout/stats_insights_most_popular_item.xml100
-rw-r--r--WordPress/src/main/res/layout/stats_insights_today_item.xml48
-rw-r--r--WordPress/src/main/res/layout/stats_list_cell.xml104
-rw-r--r--WordPress/src/main/res/layout/stats_list_fragment.xml142
-rw-r--r--WordPress/src/main/res/layout/stats_pagination_item.xml35
-rw-r--r--WordPress/src/main/res/layout/stats_top_module_pager_button.xml9
-rw-r--r--WordPress/src/main/res/layout/stats_vertical_line.xml4
-rw-r--r--WordPress/src/main/res/layout/stats_visitors_and_views_fragment.xml166
-rw-r--r--WordPress/src/main/res/layout/stats_visitors_and_views_tab.xml55
-rw-r--r--WordPress/src/main/res/layout/stats_widget_config_activity.xml13
-rw-r--r--WordPress/src/main/res/layout/stats_widget_layout.xml264
-rw-r--r--WordPress/src/main/res/layout/suggestion_list_row.xml43
-rw-r--r--WordPress/src/main/res/layout/tab_icon.xml32
-rw-r--r--WordPress/src/main/res/layout/tab_text.xml15
-rw-r--r--WordPress/src/main/res/layout/tag_list_row.xml24
-rw-r--r--WordPress/src/main/res/layout/theme_browser_activity.xml16
-rw-r--r--WordPress/src/main/res/layout/theme_browser_fragment.xml74
-rw-r--r--WordPress/src/main/res/layout/theme_details_fragment.xml202
-rw-r--r--WordPress/src/main/res/layout/theme_feature_text.xml9
-rw-r--r--WordPress/src/main/res/layout/theme_grid_cardview_header.xml152
-rw-r--r--WordPress/src/main/res/layout/theme_grid_cardview_header_search.xml48
-rw-r--r--WordPress/src/main/res/layout/theme_grid_item.xml112
-rw-r--r--WordPress/src/main/res/layout/theme_web_activity.xml21
-rw-r--r--WordPress/src/main/res/layout/toolbar.xml28
-rw-r--r--WordPress/src/main/res/layout/toolbar_search.xml15
-rw-r--r--WordPress/src/main/res/layout/toolbar_spinner.xml10
-rw-r--r--WordPress/src/main/res/layout/toolbar_spinner_dropdown_item.xml11
-rw-r--r--WordPress/src/main/res/layout/toolbar_spinner_item.xml11
-rw-r--r--WordPress/src/main/res/layout/webview.xml26
-rw-r--r--WordPress/src/main/res/layout/welcome_activity.xml14
-rw-r--r--WordPress/src/main/res/layout/wp_simple_list_item_1.xml29
-rw-r--r--WordPress/src/main/res/menu/categories.xml9
-rw-r--r--WordPress/src/main/res/menu/comment_detail.xml9
-rw-r--r--WordPress/src/main/res/menu/edit_comment.xml9
-rw-r--r--WordPress/src/main/res/menu/edit_post.xml13
-rw-r--r--WordPress/src/main/res/menu/edit_post_legacy.xml13
-rw-r--r--WordPress/src/main/res/menu/list_editor.xml19
-rw-r--r--WordPress/src/main/res/menu/media.xml18
-rw-r--r--WordPress/src/main/res/menu/media_details.xml21
-rw-r--r--WordPress/src/main/res/menu/media_edit.xml11
-rw-r--r--WordPress/src/main/res/menu/media_gallery.xml16
-rw-r--r--WordPress/src/main/res/menu/media_multiselect.xml21
-rw-r--r--WordPress/src/main/res/menu/media_picker.xml25
-rw-r--r--WordPress/src/main/res/menu/menu_comments_cab.xml31
-rw-r--r--WordPress/src/main/res/menu/menu_media_picker_action_mode.xml19
-rw-r--r--WordPress/src/main/res/menu/notifications_settings.xml11
-rw-r--r--WordPress/src/main/res/menu/people_invite.xml8
-rw-r--r--WordPress/src/main/res/menu/people_list.xml9
-rw-r--r--WordPress/src/main/res/menu/person_detail.xml9
-rw-r--r--WordPress/src/main/res/menu/post_preview.xml10
-rw-r--r--WordPress/src/main/res/menu/reader_detail.xml17
-rw-r--r--WordPress/src/main/res/menu/reader_list.xml18
-rw-r--r--WordPress/src/main/res/menu/site_picker.xml23
-rw-r--r--WordPress/src/main/res/menu/site_picker_action_mode.xml25
-rw-r--r--WordPress/src/main/res/menu/theme.xml11
-rw-r--r--WordPress/src/main/res/menu/theme_more.xml28
-rw-r--r--WordPress/src/main/res/menu/theme_search.xml10
-rw-r--r--WordPress/src/main/res/menu/theme_web.xml9
-rw-r--r--WordPress/src/main/res/menu/webview.xml20
-rw-r--r--WordPress/src/main/res/mipmap-hdpi/app_icon.pngbin0 -> 5879 bytes
-rw-r--r--WordPress/src/main/res/mipmap-xhdpi/app_icon.pngbin0 -> 7949 bytes
-rw-r--r--WordPress/src/main/res/mipmap-xxhdpi/app_icon.pngbin0 -> 13128 bytes
-rw-r--r--WordPress/src/main/res/mipmap-xxxhdpi/app_icon.pngbin0 -> 18403 bytes
-rw-r--r--WordPress/src/main/res/values-ar/strings.xml1142
-rw-r--r--WordPress/src/main/res/values-az/strings.xml653
-rw-r--r--WordPress/src/main/res/values-bg/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-cs/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-cy/strings.xml1102
-rw-r--r--WordPress/src/main/res/values-da/strings.xml629
-rw-r--r--WordPress/src/main/res/values-de/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-el/strings.xml1102
-rw-r--r--WordPress/src/main/res/values-en-rAU/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-en-rCA/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-en-rGB/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-es-rCL/strings.xml728
-rw-r--r--WordPress/src/main/res/values-es-rVE/strings.xml1040
-rw-r--r--WordPress/src/main/res/values-es/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-eu/strings.xml793
-rw-r--r--WordPress/src/main/res/values-fr/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-gd/strings.xml685
-rw-r--r--WordPress/src/main/res/values-gl/strings.xml1149
-rw-r--r--WordPress/src/main/res/values-he/strings.xml1139
-rw-r--r--WordPress/src/main/res/values-hi/strings.xml598
-rw-r--r--WordPress/src/main/res/values-hr/strings.xml892
-rw-r--r--WordPress/src/main/res/values-hu/strings.xml241
-rw-r--r--WordPress/src/main/res/values-id/strings.xml1146
l---------WordPress/src/main/res/values-in1
-rw-r--r--WordPress/src/main/res/values-it/strings.xml1132
l---------WordPress/src/main/res/values-iw1
-rw-r--r--WordPress/src/main/res/values-ja/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-ko/strings.xml1136
-rw-r--r--WordPress/src/main/res/values-land/integers.xml4
-rw-r--r--WordPress/src/main/res/values-land/styles.xml10
-rw-r--r--WordPress/src/main/res/values-large-hdpi/dimens.xml4
-rw-r--r--WordPress/src/main/res/values-large-hdpi/styles.xml11
-rw-r--r--WordPress/src/main/res/values-large-tvdpi/dimens.xml5
-rw-r--r--WordPress/src/main/res/values-mk/strings.xml507
-rw-r--r--WordPress/src/main/res/values-ms/strings.xml1132
-rw-r--r--WordPress/src/main/res/values-nb/strings.xml398
-rw-r--r--WordPress/src/main/res/values-nl/strings.xml1119
-rw-r--r--WordPress/src/main/res/values-pl/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-pt-rBR/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-ro/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-ru/strings.xml1143
-rw-r--r--WordPress/src/main/res/values-sk/strings.xml793
-rw-r--r--WordPress/src/main/res/values-sq/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-sr/strings.xml677
-rw-r--r--WordPress/src/main/res/values-sv/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-sw600dp-land/dimens.xml3
-rw-r--r--WordPress/src/main/res/values-sw600dp-land/integers.xml4
-rw-r--r--WordPress/src/main/res/values-sw600dp/dimens.xml10
-rw-r--r--WordPress/src/main/res/values-sw600dp/integers.xml7
-rw-r--r--WordPress/src/main/res/values-sw600dp/styles.xml10
-rw-r--r--WordPress/src/main/res/values-sw720dp/dimens.xml10
-rw-r--r--WordPress/src/main/res/values-sw720dp/integers.xml5
-rw-r--r--WordPress/src/main/res/values-sw720dp/styles.xml27
-rw-r--r--WordPress/src/main/res/values-th/strings.xml833
-rw-r--r--WordPress/src/main/res/values-tr/strings.xml1146
-rw-r--r--WordPress/src/main/res/values-uz/strings.xml186
-rw-r--r--WordPress/src/main/res/values-v16/styles.xml6
-rw-r--r--WordPress/src/main/res/values-w400dp/dimens.xml4
-rw-r--r--WordPress/src/main/res/values-w600dp/dimens.xml6
-rw-r--r--WordPress/src/main/res/values-w720dp/dimens.xml8
l---------WordPress/src/main/res/values-zh1
-rw-r--r--WordPress/src/main/res/values-zh-rCN/strings.xml1136
-rw-r--r--WordPress/src/main/res/values-zh-rHK/strings.xml1122
-rw-r--r--WordPress/src/main/res/values-zh-rTW/strings.xml1122
-rw-r--r--WordPress/src/main/res/values/attrs.xml89
-rw-r--r--WordPress/src/main/res/values/available_languages.xml188
-rw-r--r--WordPress/src/main/res/values/colors.xml165
-rw-r--r--WordPress/src/main/res/values/dimens.xml292
-rw-r--r--WordPress/src/main/res/values/ids.xml15
-rw-r--r--WordPress/src/main/res/values/integers.xml32
-rw-r--r--WordPress/src/main/res/values/key_strings.xml255
-rw-r--r--WordPress/src/main/res/values/reader_styles.xml125
-rw-r--r--WordPress/src/main/res/values/stats_styles.xml73
-rw-r--r--WordPress/src/main/res/values/strings.xml1579
-rw-r--r--WordPress/src/main/res/values/styles.xml435
-rw-r--r--WordPress/src/main/res/values/styles_calypso.xml115
-rw-r--r--WordPress/src/main/res/xml/account_settings.xml27
-rw-r--r--WordPress/src/main/res/xml/app_settings.xml68
-rw-r--r--WordPress/src/main/res/xml/backup_scheme.xml8
-rw-r--r--WordPress/src/main/res/xml/notifications_settings.xml32
-rw-r--r--WordPress/src/main/res/xml/provider_paths.xml6
-rw-r--r--WordPress/src/main/res/xml/site_settings.xml322
-rw-r--r--WordPress/src/main/res/xml/stats_widget_info.xml12
-rw-r--r--WordPress/src/main/res/xml/wpcom_languages.xml115
-rw-r--r--WordPress/src/wasabi/res/mipmap-hdpi/app_icon.pngbin0 -> 9561 bytes
-rw-r--r--WordPress/src/wasabi/res/mipmap-xhdpi/app_icon.pngbin0 -> 13347 bytes
-rw-r--r--WordPress/src/wasabi/res/mipmap-xxhdpi/app_icon.pngbin0 -> 22254 bytes
-rw-r--r--WordPress/src/wasabi/res/mipmap-xxxhdpi/app_icon.pngbin0 -> 24299 bytes
-rw-r--r--WordPress/src/wasabi/res/values/strings.xml4
-rw-r--r--build.gradle11
-rw-r--r--cq-configs/checkstyle/checkstyle-html.xsl177
-rw-r--r--cq-configs/checkstyle/checkstyle.xml252
-rw-r--r--gradle.properties-example26
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 53324 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew164
-rw-r--r--gradlew.bat90
-rw-r--r--keystore/debug.keystorebin0 -> 1269 bytes
-rw-r--r--keystore/debug.keystore.properties4
-rw-r--r--libs/analytics/.gitignore25
-rw-r--r--libs/analytics/LICENSE22
-rw-r--r--libs/analytics/README.md6
-rw-r--r--libs/analytics/WordPressAnalytics/build.gradle89
-rw-r--r--libs/analytics/WordPressAnalytics/gradle.properties-example6
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml5
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java69
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java271
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java1172
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java140
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java589
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java77
-rw-r--r--libs/analytics/build.gradle0
-rw-r--r--libs/analytics/gradle/wrapper/gradle-wrapper.jarbin0 -> 51348 bytes
-rw-r--r--libs/analytics/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/analytics/gradlew164
-rw-r--r--libs/analytics/gradlew.bat90
-rw-r--r--libs/analytics/settings.gradle1
-rw-r--r--libs/editor/.gitignore56
-rw-r--r--libs/editor/.travis.yml24
-rw-r--r--libs/editor/LICENSE.md264
-rw-r--r--libs/editor/README.md35
-rw-r--r--libs/editor/WordPressEditor/build.gradle108
-rw-r--r--libs/editor/WordPressEditor/lint.xml10
-rw-r--r--libs/editor/WordPressEditor/proguard-rules.pro17
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml12
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java41
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java177
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java7
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java102
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java14
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java128
-rw-r--r--libs/editor/WordPressEditor/src/main/AndroidManifest.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js3847
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/android-editor.html48
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor-android.css95
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js158
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor-utils.js26
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor.css456
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttfbin0 -> 46796 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttfbin0 -> 59316 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttfbin0 -> 53140 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttfbin0 -> 46900 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttfbin0 -> 46576 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js821
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js4
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js1
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js766
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js11
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js32
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js32
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js358
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js6
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/wpload.js108
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js113
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg12
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg12
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/more-2x.pngbin0 -> 603 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.pngbin0 -> 835 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg11
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg11
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java1659
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java180
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java10
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java35
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java256
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java130
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java245
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java150
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java431
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java236
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java1194
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java76
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java5
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java20
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java95
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java60
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java247
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java76
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java74
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.pngbin0 -> 1340 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.pngbin0 -> 991 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.pngbin0 -> 324 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.pngbin0 -> 1506 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.pngbin0 -> 814 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.pngbin0 -> 995 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.pngbin0 -> 586 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.pngbin0 -> 587 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.pngbin0 -> 398 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.pngbin0 -> 398 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.pngbin0 -> 538 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.pngbin0 -> 558 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.pngbin0 -> 663 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.pngbin0 -> 798 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.pngbin0 -> 1230 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.pngbin0 -> 1235 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.pngbin0 -> 1122 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.pngbin0 -> 1491 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.pngbin0 -> 1564 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.pngbin0 -> 2404 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.pngbin0 -> 147 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.pngbin0 -> 483 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.pngbin0 -> 4208 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.pngbin0 -> 1264 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.pngbin0 -> 1271 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.pngbin0 -> 1439 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.pngbin0 -> 1103 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.pngbin0 -> 402 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.pngbin0 -> 1687 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.pngbin0 -> 1026 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.pngbin0 -> 1288 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.pngbin0 -> 653 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.pngbin0 -> 703 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.pngbin0 -> 379 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.pngbin0 -> 388 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.pngbin0 -> 576 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.pngbin0 -> 603 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.pngbin0 -> 773 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.pngbin0 -> 1041 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.pngbin0 -> 1279 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.pngbin0 -> 1309 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.pngbin0 -> 1287 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.pngbin0 -> 1668 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.pngbin0 -> 1781 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.pngbin0 -> 2774 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.pngbin0 -> 150 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.pngbin0 -> 706 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.pngbin0 -> 4292 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.pngbin0 -> 1376 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.pngbin0 -> 1371 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.pngbin0 -> 1713 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.pngbin0 -> 1810 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.pngbin0 -> 492 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.pngbin0 -> 2050 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.pngbin0 -> 1429 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.pngbin0 -> 1691 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.pngbin0 -> 883 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.pngbin0 -> 915 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.pngbin0 -> 432 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.pngbin0 -> 457 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.pngbin0 -> 796 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.pngbin0 -> 735 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.pngbin0 -> 1145 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.pngbin0 -> 1440 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.pngbin0 -> 1427 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.pngbin0 -> 1473 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.pngbin0 -> 2174 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.pngbin0 -> 2087 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.pngbin0 -> 2218 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.pngbin0 -> 3826 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.pngbin0 -> 1157 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.pngbin0 -> 2240 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.pngbin0 -> 1601 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.pngbin0 -> 1594 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml19
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml11
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml11
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml11
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml28
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml28
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml28
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml20
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml20
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml20
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml18
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml17
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml17
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml17
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml7
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml7
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml10
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml22
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml25
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml6
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml113
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml149
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml140
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml42
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml81
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml154
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml41
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml6
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml119
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml165
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml82
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml16
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml13
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml6
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml4
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml5
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml4
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml5
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/attrs.xml6
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/values/colors.xml18
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/dimens.xml55
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/layouts.xml5
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/strings.xml97
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/values/styles.xml49
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml13
-rw-r--r--libs/editor/build.gradle0
-rw-r--r--libs/editor/example/build.gradle56
-rw-r--r--libs/editor/example/proguard-rules.pro17
-rw-r--r--libs/editor/example/src/main/AndroidManifest.xml24
-rw-r--r--libs/editor/example/src/main/assets/example/cowboy-cat.jpgbin0 -> 23142 bytes
-rw-r--r--libs/editor/example/src/main/assets/example/example-content.html58
-rw-r--r--libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java348
-rw-r--r--libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java86
-rw-r--r--libs/editor/example/src/main/res/layout/activity_example.xml44
-rw-r--r--libs/editor/example/src/main/res/layout/activity_legacy_editor.xml13
-rw-r--r--libs/editor/example/src/main/res/layout/activity_new_editor.xml13
-rw-r--r--libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--libs/editor/example/src/main/res/values/colors.xml4
-rw-r--r--libs/editor/example/src/main/res/values/strings.xml21
-rw-r--r--libs/editor/example/src/main/res/values/styles.xml9
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java13
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java112
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java496
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java92
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java99
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java215
-rw-r--r--libs/editor/example/src/test/js/test-formatter.js135
-rw-r--r--libs/editor/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--libs/editor/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/editor/gradlew164
-rw-r--r--libs/editor/gradlew.bat90
-rw-r--r--libs/editor/settings.gradle2
-rw-r--r--libs/networking/.gitignore23
-rw-r--r--libs/networking/WordPressNetworking/build.gradle49
-rw-r--r--libs/networking/WordPressNetworking/gradle.properties-example2
-rw-r--r--libs/networking/WordPressNetworking/src/main/AndroidManifest.xml3
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java13
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java96
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java19
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java9
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java14
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java475
-rw-r--r--libs/networking/build.gradle0
-rw-r--r--libs/networking/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--libs/networking/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/networking/gradlew164
-rw-r--r--libs/networking/gradlew.bat90
-rw-r--r--libs/networking/settings.gradle1
-rw-r--r--libs/utils/.gitignore25
-rw-r--r--libs/utils/README.md27
-rw-r--r--libs/utils/WordPressUtils/README.md1
-rw-r--r--libs/utils/WordPressUtils/build.gradle65
-rw-r--r--libs/utils/WordPressUtils/gradle.properties-example6
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java32
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java25
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java108
-rw-r--r--libs/utils/WordPressUtils/src/main/AndroidManifest.xml5
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java16
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java100
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java272
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java74
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java246
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java94
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java91
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java75
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java106
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java35
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java116
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java84
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java31
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java156
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java649
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java251
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java52
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java107
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java334
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java89
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java45
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java97
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java104
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java87
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java16
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java31
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java142
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java327
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java14
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java7
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java9
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java37
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java257
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java38
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java58
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java144
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java339
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java87
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java21
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java72
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java47
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java59
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java177
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java140
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java44
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java34
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java45
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java299
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java33
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java62
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/attrs.xml7
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/strings.xml5
-rw-r--r--libs/utils/build.gradle0
-rw-r--r--libs/utils/gradle/wrapper/gradle-wrapper.jarbin0 -> 51348 bytes
-rw-r--r--libs/utils/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/utils/gradlew164
-rw-r--r--libs/utils/gradlew.bat90
-rw-r--r--libs/utils/settings.gradle1
-rw-r--r--settings.gradle5
-rwxr-xr-xtools/build-all-apks.sh71
-rwxr-xr-xtools/check-missing-drawables.py40
-rwxr-xr-xtools/checkstyle.sh9
-rwxr-xr-xtools/compare-with-develop-style.sh40
-rw-r--r--tools/exported-language-codes.csv45
-rwxr-xr-xtools/get-translated-release-notes.sh51
-rwxr-xr-xtools/inject_version_in_manifest.py37
-rw-r--r--tools/language-codes.csv40
-rwxr-xr-xtools/release-checks.sh93
-rw-r--r--tools/release-notes-language-codes.csv18
-rwxr-xr-xtools/remove-unused-strings.sh20
-rwxr-xr-xtools/update-translations.sh32
2034 files changed, 217508 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..6b59f8fb5
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+* text=auto
+gradlew text eol=lf
+*.sh text eol=lf
+*.py text eol=lf
+*.bat text eol=crlf
+*.png binary
+*.jpg binary
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..93597b447
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,10 @@
+### Expected behavior
+
+
+### Actual behavior
+
+
+### Steps to reproduce the behavior
+
+
+##### Tested on [device], Android [version]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..9ab0268d5
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,3 @@
+Fixes #
+
+To test:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..1180a715c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,51 @@
+# OS X generated file
+.DS_Store
+
+# built application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+build/
+build.log
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Eclipse project files
+.settings/
+.classpath
+.project
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+captures/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Generated by gradle
+crashlytics.properties
+
+# Generated by Crashlytics
+WordPress/src/main/res/values/com_crashlytics_export_strings.xml
+
+# Silver Searcher ignore file
+.agignore
+
+# Monkey runner settings
+WordPress/src/androidTest/monkeys/settings.py
+*.pyc
+WordPress/src/androidTest/monkeys/*.png
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..78829bf0f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,39 @@
+language: android
+jdk: oraclejdk8
+sudo: required
+
+notifications:
+ # Slack notification on failure (secured token).
+ slack:
+ rooms:
+ secure: sOTcwi1DT0lJykB/smJQwJ5lQblg9jc5PtLsTu4euI/P4nCv1CPu5DVZ1aDwXL6TPDUO5uEwbzbjVFZzrl2nFdAV9XvGT3wL3Zrm2Y5HBtwi8JWdbc4dYR/xobJlIg7HRczXwoFt6yls4BUayNJJbZFV9q0ftmUSe77Pag/ZWQw=
+ on_failure: always
+ on_success: change
+
+android:
+ components:
+ - extra-google-m2repository
+ - extra-android-m2repository
+ - extra-android-support
+ - platform-tools
+ - tools
+ - build-tools-24.0.2
+ - android-24
+
+env:
+ global:
+ - MALLOC_ARENA_MAX=2
+ - GRADLE_OPTS="-XX:MaxPermSize=4g -Xmx4g"
+ - ANDROID_SDKS=android-14
+ - ANDROID_TARGET=android-14
+
+before_install:
+ # TODO: Remove the following line when Travis' platform-tools are updated to v24+
+ - echo yes | android update sdk -a --filter platform-tools --no-ui --force
+
+install:
+ # Setup gradle.properties
+ - cp WordPress/gradle.properties-example WordPress/gradle.properties
+
+script:
+ - ./gradlew -PdisablePreDex assembleVanillaRelease lint || (grep -A20 -B2 'severity="Error"' WordPress/build/outputs/lint-results-vanillaDebug.xml; exit 1)
diff --git a/CODESTYLE.md b/CODESTYLE.md
new file mode 100644
index 000000000..a77399911
--- /dev/null
+++ b/CODESTYLE.md
@@ -0,0 +1,12 @@
+# Code Style Guidelines for WordPress-Android
+
+Our code style guidelines is based on the [Android Code Style Guidelines for Contributors](https://source.android.com/source/code-style.html). We only changed a few rules:
+
+* Line length is 120 characters
+* FIXME must not be committed in the repository use TODO instead. FIXME can be used in your own local repository only.
+
+You can run a checkstyle with most rules via a gradle command:
+
+ $ ./gradlew checkstyle
+
+It generates a HTML report in `build/reports/checkstyle/checkstyle-result.html`. \ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..7dfa40a91
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,113 @@
+# Contributing
+
+If you're looking to contribute to the project please keep reading, but if you want to help translate the app, jump to [Contribute to translations](#contribute-to-translations).
+
+Here's a quick guide to create a pull request for your WordPress-Android patch:
+
+1. Fork the github project by visiting this URL: https://github.com/wordpress-mobile/WordPress-Android/fork
+
+2. Clone the git repository
+
+ $ git clone git@github.com:YOUR-GITHUB-USERNAME/WordPress-Android.git
+
+3. Create a new branch in your git repository (branched from `develop` - see [Notes about branching](#notes-about-branching) below).
+
+ $ cd WordPress-Android/
+ $ git checkout develop
+ $ git checkout -b issue/123-fix-for-123 # use a better title
+
+4. Setup your build environment (see [build instructions in our README][build-instructions]) and start hacking the project. You must follow our [code style guidelines][style], write good commit messages, comment your code and write automated tests.
+
+5. When your patch is ready, [submit a pull request][pr]. Add some comments or screen shots to help us.
+
+6. Wait for us to review your pull request. If something is wrong or if we want you to make some changes before the merge, we'll let you know through commit comments or pull request comments.
+
+[build-instructions]: https://github.com/wordpress-mobile/WordPress-Android#build-instructions
+[pr]: https://github.com/wordpress-mobile/WordPress-Android/compare/
+[style]: https://github.com/wordpress-mobile/WordPress-Android/blob/develop/CODESTYLE.md
+
+# Versioning
+
+* Version `x.y` (2.8 or 4.0 for instance) are major releases. There is no distinction between a 2.9 version or a 3.0 version, we want to avoid naming like 2.142 so the version after `x.9` (2.9) is simply `x+1.0` (3.0). A new major version is released every ~4 weeks.
+
+* Version `x.y.z` (2.8.1 or 4.0.2 for instance) are hotfix releases. We release them only when a blocking or major bug is found in the currently released version.
+
+# Branching
+
+We use the [git flow branching model][git-flow].
+
+* `master` branch represents latest version released in the Google Play Store. HEAD of this branch should be equal to last tagged release.
+
+* `develop` branch represents the cutting edge version. This is probably the one you want to fork from and base your patch on. This is the default github branch.
+
+* Version tags. All released versions are tagged and pushed in the repository. For instance if you want to checkout the 2.5.1 version:
+
+ $ git checkout 2.5.1
+
+* Release branches. When a new version is going to be released, we'll branch from `develop` to `release/x.y`. This marks version x.y code freeze. Only blocking or major bug fixes will be merged to these branches. They represent beta and release candidates.
+
+* Hotfix branches. When one or several critical issues are found on current released version, we'll branch from `tags/x.y` to `hotfix/x.y.1` (or from `tags/x.y.z` to `hotfix/x.y.z+1` if a hotfix release has already been published)
+
+* Fix or feature branches. Proposed new features and bug fixes should live in their own branch. Use the following naming convention: if a github issue exists for this feature/bugfix, the branch will be named `issue/ISSUEID-comment` where ISSUEID is the corresponding github issue id. If a github issue doesn't exist, branch will be named `feature/comment`. These branches will be merged in:
+ * `hotfix/x.y.z` if the change is a fix for a released version,
+ * `release/x.y` if the change is a fix for a beta or release candidate,
+ * `develop` for all other cases.
+
+Note: `release/x.y` or `hotfix/x.y.z` will be merged back in `master` after a new version is released. A new tag will be created and pushed at the same time.
+
+[git-flow]: http://nvie.com/posts/a-successful-git-branching-model/
+
+# Subtree'd library projects
+
+A number of library dependencies are managed as separate open source projects and are git-subtree'd into the WordPress Android app source tree. Use the following command to updated (pull latest) from their respective repos:
+
+ $ git subtree pull --squash --prefix libs/library_name https://github.com/wordpress-mobile/WordPress-Library_Name-Android.git develop
+
+and substitute the `library_name` and `Library_Name` to match the library project. As an example, for the Analytics library use 'analytics' and 'Analytics' respectively.
+
+Similarly, issue a `subtree push` to push changes committed to the main app repo, upstream to the library repo:
+
+ $ git subtree push --prefix libs/library_name https://github.com/wordpress-mobile/WordPress-Library_Name-Android.git develop
+
+Here are the libraries currently maintained and subtree'd:
+
+* Analytics
+* Editor
+* Networking
+* Stores
+* Utils
+
+# String Resources
+
+We use `values/strings.xml` file for *ALL* translatable strings including string arrays. Each element in a string array should be defined as separate string resource first and then the string array should be defined with `translatable="false"` flag. This is due to a GlotPress limitation where translating arrays directly could generate smaller arrays if some elements are not translated. Here is a basic example:
+
+```
+<string name="element1">Element 1</string>
+<string name="element2">Element 2</string>
+<string-array name="elements_array" translatable="false">
+ <item>@string/element1</item>
+ <item>@string/element2</item>
+</string-array>
+```
+
+We also have string resources outside of `strings.xml` such as `key_strings`. These strings are not user-facing and should be used as static strings such as preference keys.
+
+To help ease the translation process we ask that you mark alias string resources - as well as other strings where appropriate - as not translatable. For example `<string name="foo" translatable="false">@string/bar</string>'
+
+# Drawable Resources
+
+The Android support library [v23.2.1](http://android-developers.blogspot.com/2016/02/android-support-library-232.html) added support for drawable resources to be provided exclusively in vector format. Adding a vector drawable (to `WordPress/src/main/res/drawable/`) should be the first option when adding assets. Only if a vector drawable is not available should pngs be added to the project. Also make sure to use `app:srcCompat` in place of `android:src` in XML files.
+
+Since Vector Drawable are not the easiest file type to edit, they're chances the Vector Drawable you'll add comes from a SVG file. If the SVG file is specific to the WPAndroid project (like a banner image or unlike a gridicon), then add the SVG source in `WordPress/src/future/svg/`. The argument behind this: make sure we can find and edit the SVG file and then export it in Vector Drawable format.
+
+# Subtree'd projects
+
+The [WordPress-HealthCheck-Common][healthcheck] project is used in the tests and loaded from `assets` on tests run. Use the following command to pull in newer commits from the external project:
+
+ $ git subtree pull --prefix=WordPress/src/androidTest/assets/health-check/ https://github.com/wordpress-mobile/WordPress-HealthCheck-Common.git develop
+
+[healthcheck]: https://github.com/wordpress-mobile/WordPress-HealthCheck-Common
+
+# Contribute to translations
+
+We use a tool called GlotPress to manage translations. The WordPress-Android GlotPress instance lives here: http://translate.wordpress.org/projects/apps/android/dev. To add new translations or fix existing ones, create an account over at GlotPress and submit your changes over at the GlotPress site.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000..0671f06ac
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,264 @@
+The GNU General Public License, Version 2, June 1991 (GPLv2)
+============================================================
+
+> Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+
+Preamble
+--------
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most
+of the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom to
+distribute copies of free software (and charge for this service if you wish),
+that you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs; and that you know you can
+do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show them
+these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually
+obtain patent licenses, in effect making the program proprietary. To prevent
+this, we have made it clear that any patent must be licensed for everyone's free
+use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+
+Terms And Conditions For Copying, Distribution And Modification
+---------------------------------------------------------------
+
+**0.** This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program or
+work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or
+translated into another language. (Hereinafter, translation is included without
+limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+**1.** You may copy and distribute verbatim copies of the Program's source code
+as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the Program
+a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+**2.** You may modify your copy or copies of the Program or any portion of it,
+thus forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+* **a)** You must cause the modified files to carry prominent notices stating
+ that you changed the files and the date of any change.
+
+* **b)** You must cause any work that you distribute or publish, that in whole
+ or in part contains or is derived from the Program or any part thereof, to
+ be licensed as a whole at no charge to all third parties under the terms of
+ this License.
+
+* **c)** If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use in the
+ most ordinary way, to print or display an announcement including an
+ appropriate copyright notice and a notice that there is no warranty (or
+ else, saying that you provide a warranty) and that users may redistribute
+ the program under these conditions, and telling the user how to view a copy
+ of this License. (Exception: if the Program itself is interactive but does
+ not normally print such an announcement, your work based on the Program is
+ not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License, and
+its terms, do not apply to those sections when you distribute them as separate
+works. But when you distribute the same sections as part of a whole which is a
+work based on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the entire whole,
+and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your
+rights to work written entirely by you; rather, the intent is to exercise the
+right to control the distribution of derivative or collective works based on the
+Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+**3.** You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+* **a)** Accompany it with the complete corresponding machine-readable source
+ code, which must be distributed under the terms of Sections 1 and 2 above on
+ a medium customarily used for software interchange; or,
+
+* **b)** Accompany it with a written offer, valid for at least three years, to
+ give any third party, for a charge no more than your cost of physically
+ performing source distribution, a complete machine-readable copy of the
+ corresponding source code, to be distributed under the terms of Sections 1
+ and 2 above on a medium customarily used for software interchange; or,
+
+* **c)** Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed only for
+ noncommercial distribution and only if you received the program in object
+ code or executable form with such an offer, in accord with Subsection b
+ above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface
+definition files, plus the scripts used to control compilation and installation
+of the executable. However, as a special exception, the source code distributed
+need not include anything that is normally distributed (in either source or
+binary form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component itself
+accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source code
+from the same place counts as distribution of the source code, even though third
+parties are not compelled to copy the source along with the object code.
+
+**4.** You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so
+long as such parties remain in full compliance.
+
+**5.** You are not required to accept this License, since you have not signed
+it. However, nothing else grants you permission to modify or distribute the
+Program or its derivative works. These actions are prohibited by law if you do
+not accept this License. Therefore, by modifying or distributing the Program (or
+any work based on the Program), you indicate your acceptance of this License to
+do so, and all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+**6.** Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these terms and
+conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties to this License.
+
+**7.** If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution of
+the Program by all those who receive copies directly or indirectly through you,
+then the only way you could satisfy both it and this License would be to refrain
+entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and the
+section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices. Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose that
+choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+**8.** If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded. In such
+case, this License incorporates the limitation as if written in the body of this
+License.
+
+**9.** The Free Software Foundation may publish revised and/or new versions of
+the General Public License from time to time. Such new versions will be similar
+in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that version
+or of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of this License, you may choose any
+version ever published by the Free Software Foundation.
+
+**10.** If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of
+all derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+
+No Warranty
+-----------
+
+**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
+INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
+BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
+OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..377f265e6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+# WordPress for Android #
+
+[![Build Status](https://travis-ci.org/wordpress-mobile/WordPress-Android.svg?branch=develop)](https://travis-ci.org/wordpress-mobile/WordPress-Android)
+
+If you're just looking to install WordPress for Android, you can find
+it on [Google Play][1]. If you're a developer wanting to contribute,
+read on.
+
+## Build Instructions ##
+
+You first need to generate the `local.properties` (replace YOUR_SDK_DIR with
+your actual android SDK directory) file and create the `gradle.properties` file:
+
+ $ echo "sdk.dir=YOUR_SDK_DIR" > local.properties
+ $ cp ./WordPress/gradle.properties-example ./WordPress/gradle.properties
+
+Note: this is the default `./WordPress/gradle.properties` file. If you
+want to use WordPress.com features (login to a WordPress.com account,
+access the Reader and Stats for example), you'll have to get a WordPress.com
+OAuth2 ID and secret. Please read the
+[OAuth2 Authentication](#oauth2-authentication) section.
+
+You can now build, install and test the project:
+
+ $ ./gradlew assembleVanillaDebug # assemble the debug .apk
+ $ ./gradlew installVanillaDebug # install the debug .apk if you have an
+ # emulator or an Android device connected
+ $ ./gradlew cAT # assemble, install and run unit tests
+
+You can use [Android Studio][3] by importing the project as a Gradle project.
+
+## Directory structure ##
+
+ |-- libs # dependencies used to build debug variants
+ |-- tools # script collection
+ `-- WordPress
+ |-- build.gradle # main build script
+ |-- gradle.properties # properties imported by the build script
+ `-- src # android specific Java code
+ |-- androidTest # test assets, resources and code
+ |-- main #
+ | |-- assets # main project assets
+ | |-- java # main project java code
+ | `-- res # main project resources
+ |-- vanilla # vanilla variant specific manifest
+ `-- wasabi # wasabi variant specific resources and manifest
+
+## OAuth2 Authentication ##
+
+In order to use WordPress.com functions you will need a client ID and
+a client secret key. These details will be used to authenticate your
+application and verify that the API calls being made are valid. You can
+create an application or view details for your existing applications with
+our [WordPress.com applications manager][5].
+
+When creating your application, you should select "Native client" for the
+application type. The applications manager currently requires a "redirect URL",
+but this isn't used for mobile apps. Just use "https://localhost".
+
+Once you've created your application in the [applications manager][5], you'll
+need to edit the `./WordPress/gradle.properties` file and change the
+`WP.OAUTH.APP.ID` and `WP.OAUTH.APP.SECRET` fields. Then you can compile and
+run the app on a device or an emulator and try to login with a WordPress.com
+account.
+
+Read more about [OAuth2][6] and the [WordPress.com REST endpoint][7].
+
+## How we work ##
+
+You can read more about [Code Style Guidelines](CODESTYLE.md) we adopted, and
+how we're organizing branches in our repository in the
+[Contribution Guide](CONTRIBUTING.md).
+
+## Need help to build or hack? ##
+
+Say hello on our [Slack][4] channel: `#mobile`.
+
+## FAQ ##
+
+* Q: I can't build/test/package the project because of a `PermGen space` error.
+* A: Create a `build.properties` file in the project root directory with the
+following: `org.gradle.jvmargs=-XX:MaxPermSize=1024m`.
+
+## License ##
+
+WordPress for Android is an Open Source project covered by the
+[GNU General Public License version 2](LICENSE.md). Note: code
+in the `libs/` directory comes from external libraries, which might
+be covered by a different license compatible with the GPLv2.
+
+[1]: https://play.google.com/store/apps/details?id=org.wordpress.android
+[3]: http://developer.android.com/sdk/installing/studio.html
+[4]: https://make.wordpress.org/chat/
+[5]: https://developer.wordpress.com/apps/
+[6]: https://developer.wordpress.com/docs/oauth2/
+[7]: https://developer.wordpress.com/docs/api/
+[9]: https://facebook.github.io/watchman/docs/install.html
diff --git a/WordPress/build.gradle b/WordPress/build.gradle
new file mode 100644
index 000000000..ea8ce96e0
--- /dev/null
+++ b/WordPress/build.gradle
@@ -0,0 +1,192 @@
+buildscript {
+ repositories {
+ jcenter()
+ maven { url 'https://maven.fabric.io/public' }
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ classpath 'io.fabric.tools:gradle:1.+'
+ }
+}
+
+repositories {
+ jcenter()
+ maven { url 'http://wordpress-mobile.github.io/WordPress-Android' }
+ maven { url 'https://maven.fabric.io/public' }
+ maven { url "https://jitpack.io" }
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'io.fabric'
+
+android {
+ useLibrary 'org.apache.http.legacy'
+
+ dexOptions {
+ jumboMode = true
+ javaMaxHeapSize = "6g"
+ dexInProcess = true
+ }
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ applicationId "org.wordpress.android"
+ versionName "6.0-rc-1"
+ versionCode 308
+ minSdkVersion 16
+ targetSdkVersion 24
+
+ multiDexEnabled true
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ productFlavors {
+ vanilla {} // used for release and beta
+
+ zalpha { // alpha version - enable experimental features
+ applicationId "org.wordpress.android"
+ }
+
+ wasabi { // "hot" version, can be installed along release, alpha or beta versions
+ applicationId "org.wordpress.android.beta"
+ minSdkVersion 21 // to take advantage of "fast" multi dex (pre-dex each module)
+ }
+ }
+
+ buildTypes {
+ release {
+ // Proguard is used to shrink our apk, and reduce the number of methods in our final apk,
+ // but we don't obfuscate the bytecode.
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.cfg'
+ }
+
+ debug {
+ minifyEnabled false
+ buildConfigField "String", "APP_PN_KEY", "\"org.wordpress.android.debug.build\""
+ ext.enableCrashlytics = false
+ }
+ }
+}
+
+dependencies {
+ compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') {
+ transitive = true;
+ }
+
+ // Provided by maven central
+ compile ('org.wordpress:mediapicker:1.2.4') {
+ exclude group:'com.android.support'
+ }
+ compile 'com.google.code.gson:gson:2.6.+'
+ compile 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'
+
+ compile 'com.android.support:support-compat:24.2.1'
+ compile 'com.android.support:support-core-ui:24.2.1'
+ compile 'com.android.support:support-fragment:24.2.1'
+
+ compile 'com.android.support:multidex:1.0.1'
+ compile 'com.android.support:appcompat-v7:24.2.1'
+ compile 'com.android.support:cardview-v7:24.2.1'
+ compile 'com.android.support:recyclerview-v7:24.2.1'
+ compile 'com.android.support:design:24.2.1'
+ compile 'com.android.support:percent:24.2.1'
+
+ compile 'com.google.android.gms:play-services-gcm:9.0.2'
+ compile 'com.google.android.gms:play-services-auth:9.0.2'
+ compile 'com.github.chrisbanes.photoview:library:1.2.4'
+ compile 'com.helpshift:android-helpshift-aar:4.7.0'
+ compile 'de.greenrobot:eventbus:2.4.0'
+ compile 'com.automattic:rest:1.0.7'
+ compile 'org.wordpress:graphview:3.4.0'
+ compile 'org.wordpress:persistentedittext:1.0.1'
+ compile 'org.wordpress:emailchecker2:1.1.0'
+
+ compile 'com.yalantis:ucrop:2.2.0'
+ compile 'com.github.xizzhu:simple-tool-tip:0.5.0'
+
+ androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0'
+ androidTestCompile 'org.objenesis:objenesis:2.1'
+ androidTestCompile 'org.mockito:mockito-core:+'
+ androidTestCompile 'com.squareup.okhttp:mockwebserver:2.7.5'
+ androidTestCompile 'com.squareup.okio:okio:1.9.0' // explicitly compile okio to match the version needed by ucrop
+
+ // Provided by the WordPress-Android Repository
+ compile 'org.wordpress:drag-sort-listview:0.6.1' // not found in maven central
+ compile 'org.wordpress:slidinguppanel:1.0.0' // not found in maven central
+ compile 'org.wordpress:passcodelock:1.3.0'
+
+ // Simperium
+ compile 'com.simperium.android:simperium:0.6.8'
+
+ releaseCompile project(path:':libs:utils:WordPressUtils', configuration: 'release')
+ debugCompile project(path:':libs:utils:WordPressUtils', configuration: 'debug')
+ releaseCompile project(path:':libs:networking:WordPressNetworking', configuration: 'release')
+ debugCompile project(path:':libs:networking:WordPressNetworking', configuration: 'debug')
+ releaseCompile project(path:':libs:analytics:WordPressAnalytics', configuration: 'release')
+ debugCompile project(path:':libs:analytics:WordPressAnalytics', configuration: 'debug')
+ releaseCompile project(path:':libs:editor:WordPressEditor', configuration: 'release')
+ debugCompile project(path:':libs:editor:WordPressEditor', configuration: 'debug')
+}
+
+configurations.all {
+ // Exclude packaged wordpress sub projects, force the use of the source project
+ // (eg. use :libs:utils:WordPressUtils instead of 'org.wordpress:utils')
+ exclude group: 'org.wordpress', module: 'utils'
+ exclude group: 'org.wordpress', module: 'analytics'
+}
+
+task generateCrashlyticsConfig(group: "generate", description: "Generate Crashlytics config") {
+ def outputFile = new File("${rootDir}/WordPress/crashlytics.properties")
+ def inputFile = file("${rootDir}/WordPress/gradle.properties")
+ if (!inputFile.exists()) {
+ throw new StopActionException("Build configuration file:" + inputFile
+ + " doesn't exist, follow README instructions")
+ }
+ outputs.file outputFile
+ inputs.file inputFile
+ doLast {
+ def properties = new Properties()
+ inputFile.withInputStream { stream ->
+ properties.load(stream)
+ }
+ def crashlyticsApiKey = properties.getProperty('wp.crashlytics.apikey', '0')
+ def writer = new FileWriter(outputFile)
+ writer.write("""// auto-generated file from ${rootDir}/gradle.properties do not modify
+apiKey=${crashlyticsApiKey}""")
+ writer.close()
+ }
+}
+
+// Add generateCrashlyticsConfig to all generateBuildConfig tasks (all variants)
+android.applicationVariants.all { variant ->
+ variant.generateBuildConfig.dependsOn(generateCrashlyticsConfig)
+}
+
+// Add properties named "wp.xxx" to our BuildConfig
+android.buildTypes.all { buildType ->
+ project.properties.any { property ->
+ if (property.key.toLowerCase().startsWith("wp.")) {
+ buildType.buildConfigField "String", property.key.replace("wp.", "").replace(".", "_").toUpperCase(),
+ "\"${property.value}\""
+ }
+ }
+}
+
+// For app signing
+if (["storeFile", "storePassword", "keyAlias", "keyPassword"].count { !project.hasProperty(it) } == 0) {
+ android {
+ signingConfigs {
+ release {
+ storeFile = file(project.storeFile)
+ storePassword = project.storePassword
+ keyAlias = project.keyAlias
+ keyPassword = project.keyPassword
+ }
+ }
+ }
+ android.buildTypes.release.signingConfig = android.signingConfigs.release
+}
diff --git a/WordPress/gradle.properties-example b/WordPress/gradle.properties-example
new file mode 100644
index 000000000..f80f9dec1
--- /dev/null
+++ b/WordPress/gradle.properties-example
@@ -0,0 +1,16 @@
+wp.oauth.app_id = wordpress
+wp.oauth.app_secret = wordpress
+wp.oauth.redirect_uri = http://android.wordpress.org/
+wp.gcm.id = wordpress
+wp.db_secret = wordpress
+wp.mixpanel_token = wordpress
+wp.simperium.app_secret = wordpress
+wp.simperium.app_name = wordpress
+wp.helpshift.api.key = wordpress
+wp.helpshift.api.domain = wordpress.org
+wp.helpshift.api.id = wordpress
+wp.app_license_key = wordpress
+
+# Optional: used to autofill username and password fields at login on debug build only
+wp.debug.dotcom_login_username =
+wp.debug.dotcom_login_password =
diff --git a/WordPress/lint.xml b/WordPress/lint.xml
new file mode 100644
index 000000000..02a1dcb73
--- /dev/null
+++ b/WordPress/lint.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+ <issue id="MissingTranslation" severity="ignore" />
+ <issue id="ExtraTranslation" severity="ignore" />
+ <issue id="RtlCompat" severity="warning" />
+
+ <issue id="InvalidPackage">
+ <ignore regexp="okio-1.9.0.jar" />
+ </issue>
+
+ <issue id="NewApi">
+ <warning path="src/main/res/values/reader_styles.xml" />
+ <warning path="src/main/res/values/styles.xml" />
+ </issue>
+ <issue id="InvalidPackage">
+ <ignore regexp="okio-1.6.0.jar" />
+ </issue>
+
+ <issue id="HardcodedText" severity="error" />
+ <issue id="IconDuplicates" severity="error" />
+ <issue id="IconDipSize" severity="error" />
+ <issue id="InconsistentArrays" severity="error" />
+ <issue id="StringFormatCount" severity="error" />
+
+ <issue id="MissingPrefix">
+ <ignore regexp="Unexpected namespace prefix &quot;app&quot; found for tag `Image.*`" />
+ </issue>
+</lint>
diff --git a/WordPress/proguard.cfg b/WordPress/proguard.cfg
new file mode 100644
index 000000000..615537188
--- /dev/null
+++ b/WordPress/proguard.cfg
@@ -0,0 +1,30 @@
+-dontobfuscate
+
+-keepclassmembers class ** {
+ public void onEvent*(**);
+}
+
+# Only required if you use AsyncExecutor
+-keepclassmembers class * extends de.greenrobot.event.util.ThrowableFailureEvent {
+ ** *(java.lang.Throwable);
+}
+
+# These classes generate a warning on setLatestEventInfo but target API < 11
+-dontwarn com.mixpanel.android.mpmetrics.GCMReceiver
+-dontwarn com.google.android.gms.auth.GoogleAuthUtil
+
+# TODO: Temporary fix - remove that when mixpanel fixes it
+-dontwarn com.mixpanel.android.mpmetrics.Tweaks
+
+-dontwarn okio.**
+
+-dontwarn com.squareup.okhttp.**
+-keep class com.squareup.okhttp.** { *; }
+-keep interface com.squareup.okhttp.** { *; }
+
+-keepattributes Signature
+-keepattributes *Annotation*
+
+-dontwarn com.yalantis.ucrop**
+-keep class com.yalantis.ucrop** { *; }
+-keep interface com.yalantis.ucrop** { *; }
diff --git a/WordPress/src/androidTest/.gitignore b/WordPress/src/androidTest/.gitignore
new file mode 100644
index 000000000..eb14c81a0
--- /dev/null
+++ b/WordPress/src/androidTest/.gitignore
@@ -0,0 +1,4 @@
+/gen/
+/bin/
+build.xml
+local.properties
diff --git a/WordPress/src/androidTest/assets/1354-wp.getUsersBlogs.xml b/WordPress/src/androidTest/assets/1354-wp.getUsersBlogs.xml
new file mode 100644
index 000000000..976cc9fab
--- /dev/null
+++ b/WordPress/src/androidTest/assets/1354-wp.getUsersBlogs.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>isAdmin</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>url</name>
+ <value>
+ <string>https://tataliwut.wordpress.com/</string>
+ </value>
+ </member>
+ <member>
+ <name>blogid</name>
+ <value>
+ <string>59073674</string>
+ </value>
+ </member>
+ <member>
+ <name>blogName</name>
+ <value>
+ <string>Empty blog stays empty</string>
+ </value>
+ </member>
+ <member>
+ <name>xmlrpc</name>
+ <value>
+ <string>pouët</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/comment-already-spammed-wp.editComment.xml b/WordPress/src/androidTest/assets/comment-already-spammed-wp.editComment.xml
new file mode 100644
index 000000000..de74a1055
--- /dev/null
+++ b/WordPress/src/androidTest/assets/comment-already-spammed-wp.editComment.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultCode</name>
+ <value>
+ <int>500</int>
+ </value>
+ </member>
+ <member>
+ <name>faultString</name>
+ <value>
+ <string>Sorry, the comment could not be edited. Something wrong happened.</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+</methodResponse>
diff --git a/WordPress/src/androidTest/assets/comment-already-spammed-wp.getComment.xml b/WordPress/src/androidTest/assets/comment-already-spammed-wp.getComment.xml
new file mode 100644
index 000000000..74af47107
--- /dev/null
+++ b/WordPress/src/androidTest/assets/comment-already-spammed-wp.getComment.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20160108T14:52:57</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>11057357</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>spam</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>a comment</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://stefanosuser.wordpress.com/2016/01/08/post1/comment-page-1/#comment-1
+ </string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>2</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Post1</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>Stefanos</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+ </params>
+</methodResponse>
diff --git a/WordPress/src/androidTest/assets/corrupteddb-reproducing-814.sql b/WordPress/src/androidTest/assets/corrupteddb-reproducing-814.sql
new file mode 100644
index 000000000..228502777
--- /dev/null
+++ b/WordPress/src/androidTest/assets/corrupteddb-reproducing-814.sql
@@ -0,0 +1,21 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE android_metadata (locale TEXT);
+INSERT INTO "android_metadata" VALUES('en_US');
+CREATE TABLE accounts (id integer primary key autoincrement, url text, blogName text, username text, password text, imagePlacement text, centerThumbnail boolean, fullSizeImage boolean, maxImageWidth text, maxImageWidthId integer, lastCommentId integer, runService boolean, blogId integer, location boolean default false, dotcom_username text, dotcom_password text, api_key text, api_blogid text, dotcomFlag boolean default false, wpVersion text, httpuser text, httppassword text, postFormats text default '', isScaledImage boolean default false, scaledImgWidth integer default 1024, homeURL text default '', blog_options text default '', isAdmin boolean default false, isHidden boolean default 0);
+INSERT INTO accounts VALUES(8,'',NULL,'','AS3vw/BNTdI=
+',NULL,0,0,'',0,NULL,0,0,'false',NULL,NULL,NULL,NULL,0,NULL,'','AS3vw/BNTdI=
+','','false',1024,NULL,'',0,0);
+INSERT INTO accounts VALUES(9,'',NULL,'','AS3vw/BNTdI=
+',NULL,0,0,'',0,NULL,0,0,'false',NULL,NULL,NULL,NULL,0,'3.9-alpha','','AS3vw/BNTdI=
+','','false',1024,NULL,'',0,0);
+CREATE TABLE posts (id integer primary key autoincrement, blogID text, postid text, title text default '', dateCreated date, date_created_gmt date, categories text default '', custom_fields text default '', description text default '', link text default '', mt_allow_comments boolean, mt_allow_pings boolean, mt_excerpt text default '', mt_keywords text default '', mt_text_more text default '', permaLink text default '', post_status text default '', userid integer default 0, wp_author_display_name text default '', wp_author_id text default '', wp_password text default '', wp_post_format text default '', wp_slug text default '', mediaPaths text default '', latitude real, longitude real, localDraft boolean default 0, uploaded boolean default 0, isPage boolean default 0, wp_page_parent_id text, wp_page_parent_title text, isLocalChange boolean default 0);
+CREATE TABLE comments (blogID text, postID text, iCommentID integer, author text, comment text, commentDate text, commentDateFormatted text, status text, url text, email text, postTitle text);
+CREATE TABLE cats (id integer primary key autoincrement, blog_id text, wp_id integer, category_name text not null, parent_id integer default 0);
+CREATE TABLE quickpress_shortcuts (id integer primary key autoincrement, accountId text, name text);
+CREATE TABLE media (id integer primary key autoincrement, postID integer not null, filePath text default '', fileName text default '', title text default '', description text default '', caption text default '', horizontalAlignment integer default 0, width integer default 0, height integer default 0, mimeType text default '', featured boolean default false, isVideo boolean default false, isFeaturedInPost boolean default false, fileURL text default '', thumbnailURL text default '', mediaId text default '', blogId text default '', date_created_gmt date, uploadState default '', videoPressShortcode text default '');
+CREATE TABLE themes (_id integer primary key autoincrement, themeId text, name text, description text, screenshotURL text, trendingRank integer default 0, popularityRank integer default 0, launchDate date, previewURL text, blogId text, isCurrent boolean default false, isPremium boolean default false, features text);
+CREATE TABLE notes (id integer primary key, note_id text, message text, type text, raw_note_data text, timestamp integer, placeholder boolean);
+DELETE FROM sqlite_sequence;
+INSERT INTO "sqlite_sequence" VALUES('accounts',9);
+COMMIT;
diff --git a/WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.json b/WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.json
new file mode 100644
index 000000000..d42a4e019
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.json
@@ -0,0 +1,744 @@
+[
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1250",
+ "key": "jabber_published",
+ "value": "1390395828"
+ }
+ ],
+ "dateCreated": "Jan 22, 2014 1:03:47 PM",
+ "date_created_gmt": "Jan 22, 2014 1:03:47 PM",
+ "date_modified": "Jan 22, 2014 1:03:48 PM",
+ "date_modified_gmt": "Jan 22, 2014 1:03:48 PM",
+ "description": "[caption id=\"\" align=\"alignnone\" width=\"2000\" caption=\"Mop\"]<a href=\"https://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg\"><img title=\"wpid-wp-1388141726961.jpeg\" class=\"alignnone size-full\" alt=\"image\" src=\"https://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg?w=2000\" /></a>[/caption]",
+ "link": "https://taliwutblog.wordpress.com/2014/01/22/cat-2/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/22/cat-2/",
+ "post_status": "publish",
+ "postid": "299",
+ "title": "Cat",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "cat-2"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1244",
+ "key": "jabber_published",
+ "value": "1390395578"
+ }
+ ],
+ "dateCreated": "Jan 22, 2014 12:59:38 PM",
+ "date_created_gmt": "Jan 22, 2014 12:59:38 PM",
+ "date_modified": "Jan 22, 2014 12:59:38 PM",
+ "date_modified_gmt": "Jan 22, 2014 12:59:38 PM",
+ "description": "<a href=\"http://www.google.com/search?hl=en&amp;biw=384&amp;bih=640&amp;tbm=isch&amp;sa=1&amp;ei=RbLKUv-lHYem0wW8oYHABA&amp;q=hd+wallpaper+photo&amp;oq=hd+wallpaper+photo&amp;gs_l=mobile-gws-serp.3..0l4j0i5.4612.5034.0.5279.4.4.0.0.0.0.203.203.2-1.1.0....0...1c.1.32.mobile-gws-serp..3.1.203.63ZBZ9KeMPs#biv=i%7C10%3Bd%7Cfmk-ugJVQIMDBM%3A\">http://www.google.com/search?hl=en&amp;biw=384&amp;bih=640&amp;tbm=isch&amp;sa=1&amp;ei=RbLKUv-lHYem0wW8oYHABA&amp;q=hd+wallpaper+photo&amp;oq=hd+wallpaper+photo&amp;gs_l=mobile-gws-serp.3..0l4j0i5.4612.5034.0.5279.4.4.0.0.0.0.203.203.2-1.1.0....0...1c.1.32.mobile-gws-serp..3.1.203.63ZBZ9KeMPs#biv=i%7C10%3Bd%7Cfmk-ugJVQIMDBM%3A</a>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/22/hd-wallpaper-photo-google-search/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/22/hd-wallpaper-photo-google-search/",
+ "post_status": "publish",
+ "postid": "297",
+ "title": "hd wallpaper photo - Google Search",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "hd-wallpaper-photo-google-search"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1241",
+ "key": "blog_id",
+ "value": "55267051"
+ },
+ {
+ "id": "1242",
+ "key": "is_reblog",
+ "value": "1"
+ },
+ {
+ "id": "1235",
+ "key": "jabber_published",
+ "value": "1390394182"
+ },
+ {
+ "id": "1240",
+ "key": "post_id",
+ "value": "642"
+ }
+ ],
+ "dateCreated": "Jan 22, 2014 12:36:20 PM",
+ "date_created_gmt": "Jan 22, 2014 12:36:20 PM",
+ "date_modified": "Jan 22, 2014 12:36:20 PM",
+ "date_modified_gmt": "Jan 22, 2014 12:36:20 PM",
+ "description": "Hai",
+ "link": "https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/",
+ "post_status": "publish",
+ "postid": "296",
+ "title": "The End of Unrecorded Life",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "the-end-of-unrecorded-life"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1228",
+ "key": "jabber_published",
+ "value": "1390386762"
+ }
+ ],
+ "dateCreated": "Jan 22, 2014 10:32:40 AM",
+ "date_created_gmt": "Jan 22, 2014 10:32:40 AM",
+ "date_modified": "Jan 22, 2014 10:32:40 AM",
+ "date_modified_gmt": "Jan 22, 2014 10:32:40 AM",
+ "description": "<a href=\"http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386748907.jpg\"><img title=\"wp-1390386748907.jpg\" class=\"alignnone size-full\" alt=\"image\" src=\"http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386748907.jpg\" /></a>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/22/geny/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/22/geny/",
+ "post_status": "publish",
+ "postid": "292",
+ "title": "Geny!",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "image",
+ "wp_post_thumbnail": "",
+ "wp_slug": "geny"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1216",
+ "key": "jabber_published",
+ "value": "1390386657"
+ }
+ ],
+ "dateCreated": "Jan 22, 2014 10:30:57 AM",
+ "date_created_gmt": "Jan 22, 2014 10:30:57 AM",
+ "date_modified": "Jan 22, 2014 10:32:16 AM",
+ "date_modified_gmt": "Jan 22, 2014 10:32:16 AM",
+ "description": "<a href=\"http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386640415.jpg\"><img title=\"wp-1390386640415.jpg\" class=\"alignnone size-full\" alt=\"image\" src=\"http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386640415.jpg\" /></a>\n\n[caption id=\"\" align=\"aligncenter\" width=\"990\" caption=\"Island\"]<a href=\"http://taliwutblog.files.wordpress.com/2014/01/wpid-love-island-1920c3971080-wallpapers-jpg.jpeg\"><img title=\"Love-Island-1920\u00d71080-wallpapers.jpg\" class=\"aligncenter size-full\" alt=\"image\" src=\"http://taliwutblog.files.wordpress.com/2014/01/wpid-love-island-1920c3971080-wallpapers-jpg.jpeg\" /></a>[/caption]",
+ "link": "https://taliwutblog.wordpress.com/2014/01/22/287/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/22/287/",
+ "post_status": "publish",
+ "postid": "287",
+ "title": "",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "image",
+ "wp_post_thumbnail": "",
+ "wp_slug": "287"
+ },
+ {
+ "categories": [
+ "bird",
+ "pigeon",
+ "raven"
+ ],
+ "custom_fields": [
+ {
+ "id": "1208",
+ "key": "jabber_published",
+ "value": "1390386102"
+ }
+ ],
+ "dateCreated": "Jan 22, 2014 10:21:42 AM",
+ "date_created_gmt": "Jan 22, 2014 10:21:42 AM",
+ "date_modified": "Jan 22, 2014 10:25:10 AM",
+ "date_modified_gmt": "Jan 22, 2014 10:25:10 AM",
+ "description": "<strong>Mop</strong> est un poney\nHaij\nEnorme poney!!",
+ "link": "https://taliwutblog.wordpress.com/?p=284",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "bird, raven",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/?p=284",
+ "post_status": "draft",
+ "postid": "284",
+ "title": "Mop",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "quote",
+ "wp_post_thumbnail": "",
+ "wp_slug": "mop"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1198",
+ "key": "jabber_published",
+ "value": "1389290705"
+ }
+ ],
+ "dateCreated": "Jan 9, 2014 6:05:04 PM",
+ "date_created_gmt": "Jan 9, 2014 6:05:04 PM",
+ "date_modified": "Jan 9, 2014 6:05:04 PM",
+ "date_modified_gmt": "Jan 9, 2014 6:05:04 PM",
+ "description": "<a href=\"mop@woot.com\">mop@wooot</a>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/09/mail-link/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/09/mail-link/",
+ "post_status": "publish",
+ "postid": "282",
+ "title": "Mail link",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "mail-link"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1189",
+ "key": "jabber_published",
+ "value": "1389290631"
+ }
+ ],
+ "dateCreated": "Jan 9, 2014 6:03:51 PM",
+ "date_created_gmt": "Jan 9, 2014 6:03:51 PM",
+ "date_modified": "Jan 9, 2014 6:03:51 PM",
+ "date_modified_gmt": "Jan 9, 2014 6:03:51 PM",
+ "description": "<a href=\"woot.com\">wooot</a>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/09/link-link-2/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/09/link-link-2/",
+ "post_status": "publish",
+ "postid": "280",
+ "title": "Link link 2",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "link-link-2"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1176",
+ "key": "jabber_published",
+ "value": "1389290505"
+ }
+ ],
+ "dateCreated": "Jan 9, 2014 6:01:45 PM",
+ "date_created_gmt": "Jan 9, 2014 6:01:45 PM",
+ "date_modified": "Jan 9, 2014 6:03:03 PM",
+ "date_modified_gmt": "Jan 9, 2014 6:03:03 PM",
+ "description": "<a href=\"woot.com\">wooot</a>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/09/link-link/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/09/link-link/",
+ "post_status": "publish",
+ "postid": "278",
+ "title": "Link link",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "link-link"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1169",
+ "key": "jabber_published",
+ "value": "1389289935"
+ }
+ ],
+ "dateCreated": "Jan 9, 2014 5:52:14 PM",
+ "date_created_gmt": "Jan 9, 2014 5:52:14 PM",
+ "date_modified": "Jan 9, 2014 5:52:14 PM",
+ "date_modified_gmt": "Jan 9, 2014 5:52:14 PM",
+ "description": "",
+ "link": "https://taliwutblog.wordpress.com/2014/01/09/magnifique/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/09/magnifique/",
+ "post_status": "publish",
+ "postid": "277",
+ "title": "Magnifique!",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "magnifique"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1158",
+ "key": "jabber_published",
+ "value": "1388755568"
+ }
+ ],
+ "dateCreated": "Jan 3, 2014 1:26:07 PM",
+ "date_created_gmt": "Jan 3, 2014 1:26:07 PM",
+ "date_modified": "Jan 3, 2014 1:26:07 PM",
+ "date_modified_gmt": "Jan 3, 2014 1:26:07 PM",
+ "description": "<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"gutter\"><div class=\"line number1 index0 alt2\">1</div><div class=\"line number2 index1 alt1\">2</div><div class=\"line number3 index2 alt2\">3</div><div class=\"line number4 index3 alt1\">4</div><div class=\"line number5 index4 alt2\">5</div><div class=\"line number6 index5 alt1\">6</div><div class=\"line number7 index6 alt2\">7</div><div class=\"line number8 index7 alt1\">8</div></td><td class=\"code\"><div class=\"container\"><div class=\"line number1 index0 alt2\"><code class=\"plain plain\">[02-Jan-2014 15:15:48 UTC] Wwwww1-02 15:15:48 Can't select gr -</code></div><div class=\"line number2 index1 alt1\"><code class=\"plain plain\">'referrer' =&amp;gt; 'pwwwwwcom/wwwww&amp;amp;fields=id%2Ctype%2Cunread%2Ctimestamp%2Csubject%2Cmeta&amp;amp;trap=true',</code></div><div class=\"line number3 index2 alt2\"><code class=\"plain plain\">'server' =&amp;gt; ,</code></div><div class=\"line number4 index3 alt1\"><code class=\"plain plain\">'host' =&amp;gt; wwwwwwww,</code></div><div class=\"line number5 index4 alt2\"><code class=\"plain plain\">'error' =&amp;gt; Too many connections,</code></div><div class=\"line number6 index5 alt1\"><code class=\"plain plain\">'errno' =&amp;gt; 1040,</code></div><div class=\"line number7 index6 alt2\"><code class=\"plain plain\">'server_state' =&amp;gt; up</code></div><div class=\"line number8 index7 alt1\"><code class=\"plain plain\">'lagged_status' =&amp;gt; 1 for query SELECT prepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprepprepreprep<br /> </code></div></div></td></tr></tbody></table>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/03/273/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/03/273/",
+ "post_status": "publish",
+ "postid": "273",
+ "title": "",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "273"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1147",
+ "key": "jabber_published",
+ "value": "1388755107"
+ }
+ ],
+ "dateCreated": "Jan 3, 2014 1:18:27 PM",
+ "date_created_gmt": "Jan 3, 2014 1:18:27 PM",
+ "date_modified": "Jan 3, 2014 1:18:58 PM",
+ "date_modified_gmt": "Jan 3, 2014 1:18:58 PM",
+ "description": "Pre:\n\n<pre>\npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre \npre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre pre \npre \npre \n</pre>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/03/pre-pre/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/03/pre-pre/",
+ "post_status": "publish",
+ "postid": "270",
+ "title": "Pre pre",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "pre-pre"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1133",
+ "key": "jabber_published",
+ "value": "1388754692"
+ }
+ ],
+ "dateCreated": "Jan 3, 2014 1:11:32 PM",
+ "date_created_gmt": "Jan 3, 2014 1:11:32 PM",
+ "date_modified": "Jan 3, 2014 1:15:47 PM",
+ "date_modified_gmt": "Jan 3, 2014 1:15:47 PM",
+ "description": "<pre>\npouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet pouet \npouet \npouet \npouet \npouet \n</pre>",
+ "link": "https://taliwutblog.wordpress.com/2014/01/03/test-2/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/03/test-2/",
+ "post_status": "publish",
+ "postid": "2",
+ "title": "Test",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "test-2"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1120",
+ "key": "jabber_published",
+ "value": "1388677774"
+ }
+ ],
+ "dateCreated": "Jan 2, 2014 3:49:33 PM",
+ "date_created_gmt": "Jan 2, 2014 3:49:33 PM",
+ "date_modified": "Jan 2, 2014 3:49:33 PM",
+ "date_modified_gmt": "Jan 2, 2014 3:49:33 PM",
+ "description": "[caption id=\"\" align=\"alignnone\" width=\"1067\"]<img title=\"Unicorn\" alt=\"Unicorn\" src=\"http://imgs.tuts.dragoart.com/how-to-draw-princess-cadence-my-little-pony_1_000000012723_5.png\" width=\"1067\" height=\"1073\" /> Unicorn[/caption]",
+ "link": "https://taliwutblog.wordpress.com/2014/01/02/unicorn/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/02/unicorn/",
+ "post_status": "publish",
+ "postid": "263",
+ "title": "Unicorn",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "unicorn"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1111",
+ "key": "jabber_published",
+ "value": "1388677368"
+ }
+ ],
+ "dateCreated": "Jan 2, 2014 3:42:46 PM",
+ "date_created_gmt": "Jan 2, 2014 3:42:46 PM",
+ "date_modified": "Jan 3, 2014 10:30:51 AM",
+ "date_modified_gmt": "Jan 3, 2014 10:30:51 AM",
+ "description": "Romeo and Juliet\nShakespeare homepage | Romeo and Juliet | Entire play\nACT I\n\nPROLOGUE\n\nTwo households, both alike in dignity,\nIn fair Verona, where we lay our scene,\nFrom ancient grudge break to new mutiny,\nWhere civil blood makes civil hands unclean.\nFrom forth the fatal loins of these two foes\nA pair of star-cross'd lovers take their life;\nWhose misadventured piteous overthrows\nDo with their death bury their parents' strife.\nThe fearful passage of their death-mark'd love,\nAnd the continuance of their parents' rage,\nWhich, but their children's end, nought could remove,\nIs now the two hours' traffic of our stage;\nThe which if you with patient ears attend,\nWhat here shall miss, our toil shall strive to mend.\nSCENE I. Verona. A public place.\n\nEnter SAMPSON and GREGORY, of the house of Capulet, armed with swords and bucklers\nSAMPSON\nGregory, o' my word, we'll not carry coals.\nGREGORY\nNo, for then we should be colliers.\nSAMPSON\nI mean, an we be in choler, we'll draw.\nGREGORY\nAy, while you live, draw your neck out o' the collar.\nSAMPSON\nI strike quickly, being moved.\nGREGORY\nBut thou art not quickly moved to strike.\nSAMPSON\nA dog of the house of Montague moves me.\nGREGORY\nTo move is to stir; and to be valiant is to stand:\ntherefore, if thou art moved, thou runn'st away.\nSAMPSON\nA dog of that house shall move me to stand: I will\ntake the wall of any man or maid of Montague's.\nGREGORY\nThat shows thee a weak slave; for the weakest goes\nto the wall.\nSAMPSON\nTrue; and therefore women, being the weaker vessels,\nare ever thrust to the wall: therefore I will push\nMontague's men from the wall, and thrust his maids\nto the wall.\nGREGORY\nThe quarrel is between our masters and us their men.\nSAMPSON\n'Tis all one, I will show myself a tyrant: when I\nhave fought with the men, I will be cruel with the\nmaids, and cut off their heads.\nGREGORY\nThe heads of the maids?\nSAMPSON\nAy, the heads of the maids, or their maidenheads;\ntake it in what sense thou wilt.\nGREGORY\nThey must take it in sense that feel it.\nSAMPSON\nMe they shall feel while I am able to stand: and\n'tis known I am a pretty piece of flesh.\nGREGORY\n'Tis well thou art not fish; if thou hadst, thou\nhadst been poor John. Draw thy tool! here comes\ntwo of the house of the Montagues.\nSAMPSON\nMy naked weapon is out: quarrel, I will back thee.\nGREGORY\nHow! turn thy back and run?\nSAMPSON\nFear me not.\nGREGORY\nNo, marry; I fear thee!\nSAMPSON\nLet us take the law of our sides; let them begin.\nGREGORY\nI will frown as I pass by, and let them take it as\nthey list.\nSAMPSON\nNay, as they dare. I will bite my thumb at them;\nwhich is a disgrace to them, if they bear it.\nEnter ABRAHAM and BALTHASAR\n\nABRAHAM\nDo you bite your thumb at us, sir?\nSAMPSON\nI do bite my thumb, sir.\nABRAHAM\nDo you bite your thumb at us, sir?\nSAMPSON\n[Aside to GREGORY] Is the law of our side, if I say\nay?\nGREGORY\nNo.\nSAMPSON\nNo, sir, I do not bite my thumb at you, sir, but I\nbite my thumb, sir.\nGREGORY\nDo you quarrel, sir?\nABRAHAM\nQuarrel sir! no, sir.\nSAMPSON\nIf you do, sir, I am for you: I serve as good a man as you.\nABRAHAM\nNo better.\nSAMPSON\nWell, sir.\nGREGORY\nSay 'better:' here comes one of my master's kinsmen.\nSAMPSON\nYes, better, sir.\nABRAHAM\nYou lie.\nSAMPSON\nDraw, if you be men. Gregory, remember thy swashing blow.\nThey fight\n\nEnter BENVOLIO\n\nBENVOLIO\nPart, fools!\nPut up your swords; you know not what you do.\nBeats down their swords\n\nEnter TYBALT\n\nTYBALT\nWhat, art thou drawn among these heartless hinds?\nTurn thee, Benvolio, look upon thy death.\nBENVOLIO\nI do but keep the peace: put up thy sword,\nOr manage it to part these men with me.\nTYBALT\nWhat, drawn, and talk of peace! I hate the word,\nAs I hate hell, all Montagues, and thee:\nHave at thee, coward!\nThey fight\n\nEnter, several of both houses, who join the fray; then enter Citizens, with clubs\n\nFirst Citizen\nClubs, bills, and partisans! strike! beat them down!\nDown with the Capulets! down with the Montagues!\nEnter CAPULET in his gown, and LADY CAPULET\n\nCAPULET\nWhat noise is this? Give me my long sword, ho!\nLADY CAPULET\nA crutch, a crutch! why call you for a sword?\nCAPULET\nMy sword, I say! Old Montague is come,\nAnd flourishes his blade in spite of me.\nEnter MONTAGUE and LADY MONTAGUE\n\nMONTAGUE\nThou villain Capulet,--Hold me not, let me go.\nLADY MONTAGUE\nThou shalt not stir a foot to seek a foe.\nEnter PRINCE, with Attendants\n\nPRINCE\nRebellious subjects, enemies to peace,\nProfaners of this neighbour-stained steel,--\nWill they not hear? What, ho! you men, you beasts,\nThat quench the fire of your pernicious rage\nWith purple fountains issuing from your veins,\nOn pain of torture, from those bloody hands\nThrow your mistemper'd weapons to the ground,\nAnd hear the sentence of your moved prince.\nThree civil brawls, bred of an airy word,\nBy thee, old Capulet, and Montague,\nHave thrice disturb'd the quiet of our streets,\nAnd made Verona's ancient citizens\nCast by their grave beseeming ornaments,\nTo wield old partisans, in hands as old,\nCanker'd with peace, to part your canker'd hate:\nIf ever you disturb our streets again,\nYour lives shall pay the forfeit of the peace.\nFor this time, all the rest depart away:\nYou Capulet; shall go along with me:\nAnd, Montague, come you this afternoon,\nTo know our further pleasure in this case,\nTo old Free-town, our common judgment-place.\nOnce more, on pain of death, all men depart.\nExeunt all but MONTAGUE, LADY MONTAGUE, and BENVOLIO\n\nMONTAGUE\nWho set this ancient quarrel new abroach?\nSpeak, nephew, were you by when it began?\nBENVOLIO\nHere were the servants of your adversary,\nAnd yours, close fighting ere I did approach:\nI drew to part them: in the instant came\nThe fiery Tybalt, with his sword prepared,\nWhich, as he breathed defiance to my ears,\nHe swung about his head and cut the winds,\nWho nothing hurt withal hiss'd him in scorn:\nWhile we were interchanging thrusts and blows,\nCame more and more and fought on part and part,\nTill the prince came, who parted either part.\nLADY MONTAGUE\nO, where is Romeo? saw you him to-day?\nRight glad I am he was not at this fray.\nBENVOLIO\nMadam, an hour before the worshipp'd sun\nPeer'd forth the golden window of the east,\nA troubled mind drave me to walk abroad;\nWhere, underneath the grove of sycamore\nThat westward rooteth from the city's side,\nSo early walking did I see your son:\nTowards him I made, but he was ware of me\nAnd stole into the covert of the wood:\nI, measuring his affections by my own,\nThat most are busied when they're most alone,\nPursued my humour not pursuing his,\nAnd gladly shunn'd who gladly fled from me.\nMONTAGUE\nMany a morning hath he there been seen,\nWith tears augmenting the fresh morning dew.\nAdding to clouds more clouds with his deep sighs;\nBut all so soon as the all-cheering sun\nShould in the furthest east begin to draw\nThe shady curtains from Aurora's bed,\nAway from the light steals home my heavy son,\nAnd private in his chamber pens himself,\nShuts up his windows, locks far daylight out\nAnd makes himself an artificial night:\nBlack and portentous must this humour prove,\nUnless good counsel may the cause remove.\nBENVOLIO\nMy noble uncle, do you know the cause?\nMONTAGUE\nI neither know it nor can learn of him.\nBENVOLIO\nHave you importuned him by any means?\nMONTAGUE\nBoth by myself and many other friends:\nBut he, his own affections' counsellor,\nIs to himself--I will not say how true--\nBut to himself so secret and so close,\nSo far from sounding and discovery,\nAs is the bud bit with an envious worm,\nEre he can spread his sweet leaves to the air,\nOr dedicate his beauty to the sun.\nCould we but learn from whence his sorrows grow.\nWe would as willingly give cure as know.\nEnter ROMEO\n\nBENVOLIO\nSee, where he comes: so please you, step aside;\nI'll know his grievance, or be much denied.\nMONTAGUE\nI would thou wert so happy by thy stay,\nTo hear true shrift. Come, madam, let's away.\nExeunt MONTAGUE and LADY MONTAGUE\n\nBENVOLIO\nGood-morrow, cousin.\nROMEO\nIs the day so young?\nBENVOLIO\nBut new struck nine.\nROMEO\nAy me! sad hours seem long.\nWas that my father that went hence so fast?\nBENVOLIO\nIt was. What sadness lengthens Romeo's hours?\nROMEO\nNot having that, which, having, makes them short.\nBENVOLIO\nIn love?\nROMEO\nOut--\nBENVOLIO\nOf love?\nROMEO\nOut of her favour, where I am in love.\nBENVOLIO\nAlas, that love, so gentle in his view,\nShould be so tyrannous and rough in proof!\nROMEO\nAlas, that love, whose view is muffled still,\nShould, without eyes, see pathways to his will!\nWhere shall we dine? O me! What fray was here?\nYet tell me not, for I have heard it all.\nHere's much to do with hate, but more with love.\nWhy, then, O brawling love! O loving hate!\nO any thing, of nothing first create!\nO heavy lightness! serious vanity!\nMis-shapen chaos of well-seeming forms!\nFeather of lead, bright smoke, cold fire,\nsick health!\nStill-waking sleep, that is not what it is!\nThis love feel I, that feel no love in this.\nDost thou not laugh?\nBENVOLIO\nNo, coz, I rather weep.\nROMEO\nGood heart, at what?\nBENVOLIO\nAt thy good heart's oppression.\nROMEO\nWhy, such is love's transgression.\nGriefs of mine own lie heavy in my breast,\nWhich thou wilt propagate, to have it prest\nWith more of thine: this love that thou hast shown\nDoth add more grief to too much of mine own.\nLove is a smoke raised with the fume of sighs;\nBeing purged, a fire sparkling in lovers' eyes;\nBeing vex'd a sea nourish'd with lovers' tears:\nWhat is it else? a madness most discreet,\nA choking gall and a preserving sweet.\nFarewell, my coz.\nBENVOLIO\nSoft! I will go along;\nAn if you leave me so, you do me wrong.\nROMEO\nTut, I have lost myself; I am not here;\nThis is not Romeo, he's some other where.\nBENVOLIO\nTell me in sadness, who is that you love.\nROMEO\nWhat, shall I groan and tell thee?\nBENVOLIO\nGroan! why, no.\nBut sadly tell me who.\nROMEO\nBid a sick man in sadness make his will:\nAh, word ill urged to one that is so ill!\nIn sadness, cousin, I do love a woman.\nBENVOLIO\nI aim'd so near, when I supposed you loved.\nROMEO\nA right good mark-man! And she's fair I love.\nBENVOLIO\nA right fair mark, fair coz, is soonest hit.\nROMEO\nWell, in that hit you miss: she'll not be hit\nWith Cupid's arrow; she hath Dian's wit;\nAnd, in strong proof of chastity well arm'd,\nFrom love's weak childish bow she lives unharm'd.\nShe will not stay the siege of loving terms,\nNor bide the encounter of assailing eyes,\nNor ope her lap to saint-seducing gold:\nO, she is rich in beauty, only poor,\nThat when she dies with beauty dies her store.\nBENVOLIO\nThen she hath sworn that she will still live chaste?\nROMEO\nShe hath, and in that sparing makes huge waste,\nFor beauty starved with her severity\nCuts beauty off from all posterity.\nShe is too fair, too wise, wisely too fair,\nTo merit bliss by making me despair:\nShe hath forsworn to love, and in that vow\nDo I live dead that live to tell it now.\nBENVOLIO\nBe ruled by me, forget to think of her.\nROMEO\nO, teach me how I should forget to think.\nBENVOLIO\nBy giving liberty unto thine eyes;\nExamine other beauties.\nROMEO\n'Tis the way\nTo call hers exquisite, in question more:\nThese happy masks that kiss fair ladies' brows\nBeing black put us in mind they hide the fair;\nHe that is strucken blind cannot forget\nThe precious treasure of his eyesight lost:\nShow me a mistress that is passing fair,\nWhat doth her beauty serve, but as a note\nWhere I may read who pass'd that passing fair?\nFarewell: thou canst not teach me to forget.\nBENVOLIO\nI'll pay that doctrine, or else die in debt.\nExeunt\n\nSCENE II. A street.\n\nEnter CAPULET, PARIS, and Servant\nCAPULET\nBut Montague is bound as well as I,\nIn penalty alike; and 'tis not hard, I think,\nFor men so old as we to keep the peace.\nPARIS\nOf honourable reckoning are you both;\nAnd pity 'tis you lived at odds so long.\nBut now, my lord, what say you to my suit?\nCAPULET\nBut saying o'er what I have said before:\nMy child is yet a stranger in the world;\nShe hath not seen the change of fourteen years,\nLet two more summers wither in their pride,\nEre we may think her ripe to be a bride.\nPARIS\nYounger than she are happy mothers made.\nCAPULET\nAnd too soon marr'd are those so early made.\nThe earth hath swallow'd all my hopes but she,\nShe is the hopeful lady of my earth:\nBut woo her, gentle Paris, get her heart,\nMy will to her consent is but a part;\nAn she agree, within her scope of choice\nLies my consent and fair according voice.\nThis night I hold an old accustom'd feast,\nWhereto I have invited many a guest,\nSuch as I love; and you, among the store,\nOne more, most welcome, makes my number more.\nAt my poor house look to behold this night\nEarth-treading stars that make dark heaven light:\nSuch comfort as do lusty young men feel\nWhen well-apparell'd April on the heel\nOf limping winter treads, even such delight\nAmong fresh female buds shall you this night\nInherit at my house; hear all, all see,\nAnd like her most whose merit most shall be:\nWhich on more view, of many mine being one\nMay stand in number, though in reckoning none,\nCome, go with me.\nTo Servant, giving a paper\n\nGo, sirrah, trudge about\nThrough fair Verona; find those persons out\nWhose names are written there, and to them say,\nMy house and welcome on their pleasure stay.\nExeunt CAPULET and PARIS\n\nServant\nFind them out whose names are written here! It is\nwritten, that the shoemaker should meddle with his\nyard, and the tailor with his last, the fisher with\nhis pencil, and the painter with his nets; but I am\nsent to find those persons whose names are here\nwrit, and can never find what names the writing\nperson hath here writ. I must to the learned.--In good time.\nEnter BENVOLIO and ROMEO\n\nBENVOLIO\nTut, man, one fire burns out another's burning,\nOne pain is lessen'd by another's anguish;\nTurn giddy, and be holp by backward turning;\nOne desperate grief cures with another's languish:\nTake thou some new infection to thy eye,\nAnd the rank poison of the old will die.\nROMEO\nYour plaintain-leaf is excellent for that.\nBENVOLIO\nFor what, I pray thee?\nROMEO\nFor your broken shin.\nBENVOLIO\nWhy, Romeo, art thou mad?\nROMEO\nNot mad, but bound more than a mad-man is;\nShut up in prison, kept without my food,\nWhipp'd and tormented and--God-den, good fellow.\nServant\nGod gi' god-den. I pray, sir, can you read?\nROMEO\nAy, mine own fortune in my misery.\nServant\nPerhaps you have learned it without book: but, I\npray, can you read any thing you see?\nROMEO\nAy, if I know the letters and the language.\nServant\nYe say honestly: rest you merry!\nROMEO\nStay, fellow; I can read.\nReads\n\n'Signior Martino and his wife and daughters;\nCounty Anselme and his beauteous sisters; the lady\nwidow of Vitravio; Signior Placentio and his lovely\nnieces; Mercutio and his brother Valentine; mine\nuncle Capulet, his wife and daughters; my fair niece\nRosaline; Livia; Signior Valentio and his cousin\nTybalt, Lucio and the lively Helena.' A fair\nassembly: whither should they come?\nServant\nUp.\nROMEO\nWhither?\nServant\nTo supper; to our house.\nROMEO\nWhose house?\nServant\nMy master's.\nROMEO\nIndeed, I should have ask'd you that before.\nServant\nNow I'll tell you without asking: my master is the\ngreat rich Capulet; and if you be not of the house\nof Montagues, I pray, come and crush a cup of wine.\nRest you merry!\nExit\n\nBENVOLIO\nAt this same ancient feast of Capulet's\nSups the fair Rosaline whom thou so lovest,\nWith all the admired beauties of Verona:\nGo thither; and, with unattainted eye,\nCompare her face with some that I shall show,\nAnd I will make thee think thy swan a crow.\nROMEO\nWhen the devout religion of mine eye\nMaintains such falsehood, then turn tears to fires;\nAnd these, who often drown'd could never die,\nTransparent heretics, be burnt for liars!\nOne fairer than my love! the all-seeing sun\nNe'er saw her match since first the world begun.\nBENVOLIO\nTut, you saw her fair, none else being by,\nHerself poised with herself in either eye:\nBut in that crystal scales let there be weigh'd\nYour lady's love against some other maid\nThat I will show you shining at this feast,\nAnd she shall scant show well that now shows best.\nROMEO\nI'll go along, no such sight to be shown,\nBut to rejoice in splendor of mine own.\nExeunt\n\nSCENE III. A room in Capulet's house.\n\nEnter LADY CAPULET and Nurse\nLADY CAPULET\nNurse, where's my daughter? call her forth to me.\nNurse\nNow, by my maidenhead, at twelve year old,\nI bade her come. What, lamb! what, ladybird!\nGod forbid! Where's this girl? What, Juliet!\nEnter JULIET\n\nJULIET\nHow now! who calls?\nNurse\nYour mother.\nJULIET\nMadam, I am here.\nWhat is your will?\nLADY CAPULET\nThis is the matter:--Nurse, give leave awhile,\nWe must talk in secret:--nurse, come back again;\nI have remember'd me, thou's hear our counsel.\nThou know'st my daughter's of a pretty age.\nNurse\nFaith, I can tell her age unto an hour.\nLADY CAPULET\nShe's not fourteen.\nNurse\nI'll lay fourteen of my teeth,--\nAnd yet, to my teeth be it spoken, I have but four--\nShe is not fourteen. How long is it now\nTo Lammas-tide?\nLADY CAPULET\nA fortnight and odd days.\nNurse\nEven or odd, of all days in the year,\nCome Lammas-eve at night shall she be fourteen.\nSusan and she--God rest all Christian souls!--\nWere of an age: well, Susan is with God;\nShe was too good for me: but, as I said,\nOn Lammas-eve at night shall she be fourteen;\nThat shall she, marry; I remember it well.\n'Tis since the earthquake now eleven years;\nAnd she was wean'd,--I never shall forget it,--\nOf all the days of the year, upon that day:\nFor I had then laid wormwood to my dug,\nSitting in the sun under the dove-house wall;\nMy lord and you were then at Mantua:--\nNay, I do bear a brain:--but, as I said,\nWhen it did taste the wormwood on the nipple\nOf my dug and felt it bitter, pretty fool,\nTo see it tetchy and fall out with the dug!\nShake quoth the dove-house: 'twas no need, I trow,\nTo bid me trudge:\nAnd since that time it is eleven years;\nFor then she could stand alone; nay, by the rood,\nShe could have run and waddled all about;\nFor even the day before, she broke her brow:\nAnd then my husband--God be with his soul!\nA' was a merry man--took up the child:\n'Yea,' quoth he, 'dost thou fall upon thy face?\nThou wilt fall backward when thou hast more wit;\nWilt thou not, Jule?' and, by my holidame,\nThe pretty wretch left crying and said 'Ay.'\nTo see, now, how a jest shall come about!\nI warrant, an I should live a thousand years,\nI never should forget it: 'Wilt thou not, Jule?' quoth he;\nAnd, pretty fool, it stinted and said 'Ay.'\nLADY CAPULET\nEnough of this; I pray thee, hold thy peace.\nNurse\nYes, madam: yet I cannot choose but laugh,\nTo think it should leave crying and say 'Ay.'\nAnd yet, I warrant, it had upon its brow\nA bump as big as a young cockerel's stone;\nA parlous knock; and it cried bitterly:\n'Yea,' quoth my husband,'fall'st upon thy face?\nThou wilt fall backward when thou comest to age;\nWilt thou not, Jule?' it stinted and said 'Ay.'\nJULIET\nAnd stint thou too, I pray thee, nurse, say I.\nNurse\nPeace, I have done. God mark thee to his grace!\nThou wast the prettiest babe that e'er I nursed:\nAn I might live to see thee married once,\nI have my wish.\nLADY CAPULET\nMarry, that 'marry' is the very theme\nI came to talk of. Tell me, daughter Juliet,\nHow stands your disposition to be married?\nJULIET\nIt is an honour that I dream not of.\nNurse\nAn honour! were not I thine only nurse,\nI would say thou hadst suck'd wisdom from thy teat.\nLADY CAPULET\nWell, think of marriage now; younger than you,\nHere in Verona, ladies of esteem,\nAre made already mothers: by my count,\nI was your mother much upon these years\nThat you are now a maid. Thus then in brief:\nThe valiant Paris seeks you for his love.\nNurse\nA man, young lady! lady, such a man\nAs all the world--why, he's a man of wax.\nLADY CAPULET\nVerona's summer hath not such a flower.\nNurse\nNay, he's a flower; in faith, a very flower.\nLADY CAPULET\nWhat say you? can you love the gentleman?\nThis night you shall behold him at our feast;\nRead o'er the volume of young Paris' face,\nAnd find delight writ there with beauty's pen;\nExamine every married lineament,\nAnd see how one another lends content\nAnd what obscured in this fair volume lies\nFind written in the margent of his eyes.\nThis precious book of love, this unbound lover,\nTo beautify him, only lacks a cover:\nThe fish lives in the sea, and 'tis much pride\nFor fair without the fair within to hide:\nThat book in many's eyes doth share the glory,\nThat in gold clasps locks in the golden story;\nSo shall you share all that he doth possess,\nBy having him, making yourself no less.\nNurse\nNo less! nay, bigger; women grow by men.\nLADY CAPULET\nSpeak briefly, can you like of Paris' love?\nJULIET\nI'll look to like, if looking liking move:\nBut no more deep will I endart mine eye\nThan your consent gives strength to make it fly.\nEnter a Servant\n\nServant\nMadam, the guests are come, supper served up, you\ncalled, my young lady asked for, the nurse cursed in\nthe pantry, and every thing in extremity. I must\nhence to wait; I beseech you, follow straight.\nLADY CAPULET\nWe follow thee.\nExit Servant\n\nJuliet, the county stays.\nNurse\nGo, girl, seek happy nights to happy days.\nExeunt\n\nSCENE IV. A street.\n\nEnter ROMEO, MERCUTIO, BENVOLIO, with five or six Maskers, Torch-bearers, and others\nROMEO\nWhat, shall this speech be spoke for our excuse?\nOr shall we on without a apology?\nBENVOLIO\nThe date is out of such prolixity:\nWe'll have no Cupid hoodwink'd with a scarf,\nBearing a Tartar's painted bow of lath,\nScaring the ladies like a crow-keeper;\nNor no without-book prologue, faintly spoke\nAfter the prompter, for our entrance:\nBut let them measure us by what they will;\nWe'll measure them a measure, and be gone.\nROMEO\nGive me a torch: I am not for this ambling;\nBeing but heavy, I will bear the light.\nMERCUTIO\nNay, gentle Romeo, we must have you dance.\nROMEO\nNot I, believe me: you have dancing shoes\nWith nimble soles: I have a soul of lead\nSo stakes me to the ground I cannot move.\nMERCUTIO\nYou are a lover; borrow Cupid's wings,\nAnd soar with them above a common bound.\nROMEO\nI am too sore enpierced with his shaft\nTo soar with his light feathers, and so bound,\nI cannot bound a pitch above dull woe:\nUnder love's heavy burden do I sink.\nMERCUTIO\nAnd, to sink in it, should you burden love;\nToo great oppression for a tender thing.\nROMEO\nIs love a tender thing? it is too rough,\nToo rude, too boisterous, and it pricks like thorn.\nMERCUTIO\nIf love be rough with you, be rough with love;\nPrick love for pricking, and you beat love down.\nGive me a case to put my visage in:\nA visor for a visor! what care I\nWhat curious eye doth quote deformities?\nHere are the beetle brows shall blush for me.\nBENVOLIO\nCome, knock and enter; and no sooner in,\nBut every man betake him to his legs.\nROMEO\nA torch for me: let wantons light of heart\nTickle the senseless rushes with their heels,\nFor I am proverb'd with a grandsire phrase;\nI'll be a candle-holder, and look on.\nThe game was ne'er so fair, and I am done.\nMERCUTIO\nTut, dun's the mouse, the constable's own word:\nIf thou art dun, we'll draw thee from the mire\nOf this sir-reverence love, wherein thou stick'st\nUp to the ears. Come, we burn daylight, ho!\nROMEO\nNay, that's not so.\nMERCUTIO\nI mean, sir, in delay\nWe waste our lights in vain, like lamps by day.\nTake our good meaning, for our judgment sits\nFive times in that ere once in our five wits.\nROMEO\nAnd we mean well in going to this mask;\nBut 'tis no wit to go.\nMERCUTIO\nWhy, may one ask?\nROMEO\nI dream'd a dream to-night.\nMERCUTIO\nAnd so did I.\nROMEO\nWell, what was yours?\nMERCUTIO\nThat dreamers often lie.\nROMEO\nIn bed asleep, while they do dream things true.\nMERCUTIO\nO, then, I see Queen Mab hath been with you.\nShe is the fairies' midwife, and she comes\nIn shape no bigger than an agate-stone\nOn the fore-finger of an alderman,\nDrawn with a team of little atomies\nAthwart men's noses as they lie asleep;\nHer wagon-spokes made of long spiders' legs,\nThe cover of the wings of grasshoppers,\nThe traces of the smallest spider's web,\nThe collars of the moonshine's watery beams,\nHer whip of cricket's bone, the lash of film,\nHer wagoner a small grey-coated gnat,\nNot so big as a round little worm\nPrick'd from the lazy finger of a maid;\nHer chariot is an empty hazel-nut\nMade by the joiner squirrel or old grub,\nTime out o' mind the fairies' coachmakers.\nAnd in this state she gallops night by night\nThrough lovers' brains, and then they dream of love;\nO'er courtiers' knees, that dream on court'sies straight,\nO'er lawyers' fingers, who straight dream on fees,\nO'er ladies ' lips, who straight on kisses dream,\nWhich oft the angry Mab with blisters plagues,\nBecause their breaths with sweetmeats tainted are:\nSometime she gallops o'er a courtier's nose,\nAnd then dreams he of smelling out a suit;\nAnd sometime comes she with a tithe-pig's tail\nTickling a parson's nose as a' lies asleep,\nThen dreams, he of another benefice:\nSometime she driveth o'er a soldier's neck,\nAnd then dreams he of cutting foreign throats,\nOf breaches, ambuscadoes, Spanish blades,\nOf healths five-fathom deep; and then anon\nDrums in his ear, at which he starts and wakes,\nAnd being thus frighted swears a prayer or two\nAnd sleeps again. This is that very Mab\nThat plats the manes of horses in the night,\nAnd bakes the elflocks in foul sluttish hairs,\nWhich once untangled, much misfortune bodes:\nThis is the hag, when maids lie on their backs,\nThat presses them and learns them first to bear,\nMaking them women of good carriage:\nThis is she--\nROMEO\nPeace, peace, Mercutio, peace!\nThou talk'st of nothing.\nMERCUTIO\nTrue, I talk of dreams,\nWhich are the children of an idle brain,\nBegot of nothing but vain fantasy,\nWhich is as thin of substance as the air\nAnd more inconstant than the wind, who wooes\nEven now the frozen bosom of the north,\nAnd, being anger'd, puffs away from thence,\nTurning his face to the dew-dropping south.\nBENVOLIO\nThis wind, you talk of, blows us from ourselves;\nSupper is done, and we shall come too late.\nROMEO\nI fear, too early: for my mind misgives\nSome consequence yet hanging in the stars\nShall bitterly begin his fearful date\nWith this night's revels and expire the term\nOf a despised life closed in my breast\nBy some vile forfeit of untimely death.\nBut He, that hath the steerage of my course,\nDirect my sail! On, lusty gentlemen.\nBENVOLIO\nStrike, drum.\nExeunt\n\nSCENE V. A hall in Capulet's house.\n\nMusicians waiting. Enter Servingmen with napkins\nFirst Servant\nWhere's Potpan, that he helps not to take away? He\nshift a trencher? he scrape a trencher!\nSecond Servant\nWhen good manners shall lie all in one or two men's\nhands and they unwashed too, 'tis a foul thing.\nFirst Servant\nAway with the joint-stools, remove the\ncourt-cupboard, look to the plate. Good thou, save\nme a piece of marchpane; and, as thou lovest me, let\nthe porter let in Susan Grindstone and Nell.\nAntony, and Potpan!\nSecond Servant\nAy, boy, ready.\nFirst Servant\nYou are looked for and called for, asked for and\nsought for, in the great chamber.\nSecond Servant\nWe cannot be here and there too. Cheerly, boys; be\nbrisk awhile, and the longer liver take all.\nEnter CAPULET, with JULIET and others of his house, meeting the Guests and Maskers\n\nCAPULET\nWelcome, gentlemen! ladies that have their toes\nUnplagued with corns will have a bout with you.\nAh ha, my mistresses! which of you all\nWill now deny to dance? she that makes dainty,\nShe, I'll swear, hath corns; am I come near ye now?\nWelcome, gentlemen! I have seen the day\nThat I have worn a visor and could tell\nA whispering tale in a fair lady's ear,\nSuch as would please: 'tis gone, 'tis gone, 'tis gone:\nYou are welcome, gentlemen! come, musicians, play.\nA hall, a hall! give room! and foot it, girls.\nMusic plays, and they dance\n\nMore light, you knaves; and turn the tables up,\nAnd quench the fire, the room is grown too hot.\nAh, sirrah, this unlook'd-for sport comes well.\nNay, sit, nay, sit, good cousin Capulet;\nFor you and I are past our dancing days:\nHow long is't now since last yourself and I\nWere in a mask?\nSecond Capulet\nBy'r lady, thirty years.\nCAPULET\nWhat, man! 'tis not so much, 'tis not so much:\n'Tis since the nuptials of Lucentio,\nCome pentecost as quickly as it will,\nSome five and twenty years; and then we mask'd.\nSecond Capulet\n'Tis more, 'tis more, his son is elder, sir;\nHis son is thirty.\nCAPULET\nWill you tell me that?\nHis son was but a ward two years ago.\nROMEO\n[To a Servingman] What lady is that, which doth\nenrich the hand\nOf yonder knight?\nServant\nI know not, sir.\nROMEO\nO, she doth teach the torches to burn bright!\nIt seems she hangs upon the cheek of night\nLike a rich jewel in an Ethiope's ear;\nBeauty too rich for use, for earth too dear!\nSo shows a snowy dove trooping with crows,\nAs yonder lady o'er her fellows shows.\nThe measure done, I'll watch her place of stand,\nAnd, touching hers, make blessed my rude hand.\nDid my heart love till now? forswear it, sight!\nFor I ne'er saw true beauty till this night.\nTYBALT\nThis, by his voice, should be a Montague.\nFetch me my rapier, boy. What dares the slave\nCome hither, cover'd with an antic face,\nTo fleer and scorn at our solemnity?\nNow, by the stock and honour of my kin,\nTo strike him dead, I hold it not a sin.\nCAPULET\nWhy, how now, kinsman! wherefore storm you so?\nTYBALT\nUncle, this is a Montague, our foe,\nA villain that is hither come in spite,\nTo scorn at our solemnity this night.\nCAPULET\nYoung Romeo is it?\nTYBALT\n'Tis he, that villain Romeo.\nCAPULET\nContent thee, gentle coz, let him alone;\nHe bears him like a portly gentleman;\nAnd, to say truth, Verona brags of him\nTo be a virtuous and well-govern'd youth:\nI would not for the wealth of all the town\nHere in my house do him disparagement:\nTherefore be patient, take no note of him:\nIt is my will, the which if thou respect,\nShow a fair presence and put off these frowns,\nAnd ill-beseeming semblance for a feast.\nTYBALT\nIt fits, when such a villain is a guest:\nI'll not endure him.\nCAPULET\nHe shall be endured:\nWhat, goodman boy! I say, he shall: go to;\nAm I the master here, or you? go to.\nYou'll not endure him! God shall mend my soul!\nYou'll make a mutiny among my guests!\nYou will set cock-a-hoop! you'll be the man!\nTYBALT\nWhy, uncle, 'tis a shame.\nCAPULET\nGo to, go to;\nYou are a saucy boy: is't so, indeed?\nThis trick may chance to scathe you, I know what:\nYou must contrary me! marry, 'tis time.\nWell said, my hearts! You are a princox; go:\nBe quiet, or--More light, more light! For shame!\nI'll make you quiet. What, cheerly, my hearts!\nTYBALT\nPatience perforce with wilful choler meeting\nMakes my flesh tremble in their different greeting.\nI will withdraw: but this intrusion shall\nNow seeming sweet convert to bitter gall.\nExit\n\nROMEO\n[To JULIET] If I profane with my unworthiest hand\nThis holy shrine, the gentle fine is this:\nMy lips, two blushing pilgrims, ready stand\nTo smooth that rough touch with a tender kiss.\nJULIET\nGood pilgrim, you do wrong your hand too much,\nWhich mannerly devotion shows in this;\nFor saints have hands that pilgrims' hands do touch,\nAnd palm to palm is holy palmers' kiss.\nROMEO\nHave not saints lips, and holy palmers too?\nJULIET\nAy, pilgrim, lips that they must use in prayer.\nROMEO\nO, then, dear saint, let lips do what hands do;\nThey pray, grant thou, lest faith turn to despair.\nJULIET\nSaints do not move, though grant for prayers' sake.\nROMEO\nThen move not, while my prayer's effect I take.\nThus from my lips, by yours, my sin is purged.\nJULIET\nThen have my lips the sin that they have took.\nROMEO\nSin from thy lips? O trespass sweetly urged!\nGive me my sin again.\nJULIET\nYou kiss by the book.\nNurse\nMadam, your mother craves a word with you.\nROMEO\nWhat is her mother?\nNurse\nMarry, bachelor,\nHer mother is the lady of the house,\nAnd a good lady, and a wise and virtuous\nI nursed her daughter, that you talk'd withal;\nI tell you, he that can lay hold of her\nShall have the chinks.\nROMEO\nIs she a Capulet?\nO dear account! my life is my foe's debt.\nBENVOLIO\nAway, begone; the sport is at the best.\nROMEO\nAy, so I fear; the more is my unrest.\nCAPULET\nNay, gentlemen, prepare not to be gone;\nWe have a trifling foolish banquet towards.\nIs it e'en so? why, then, I thank you all\nI thank you, honest gentlemen; good night.\nMore torches here! Come on then, let's to bed.\nAh, sirrah, by my fay, it waxes late:\nI'll to my rest.\nExeunt all but JULIET and Nurse\n\nJULIET\nCome hither, nurse. What is yond gentleman?\nNurse\nThe son and heir of old Tiberio.\nJULIET\nWhat's he that now is going out of door?\nNurse\nMarry, that, I think, be young Petrucio.\nJULIET\nWhat's he that follows there, that would not dance?\nNurse\nI know not.\nJULIET\nGo ask his name: if he be married.\nMy grave is like to be my wedding bed.\nNurse\nHis name is Romeo, and a Montague;\nThe only son of your great enemy.\nJULIET\nMy only love sprung from my only hate!\nToo early seen unknown, and known too late!\nProdigious birth of love it is to me,\nThat I must love a loathed enemy.\nNurse\nWhat's this? what's this?\nJULIET\nA rhyme I learn'd even now\nOf one I danced withal.\nOne calls within 'Juliet.'\n\nNurse\nAnon, anon!\nCome, let's away; the strangers all are gone.\nExeunt\n\nACT II\n\nPROLOGUE\n\nEnter Chorus\nChorus\nNow old desire doth in his death-bed lie,\nAnd young affection gapes to be his heir;\nThat fair for which love groan'd for and would die,\nWith tender Juliet match'd, is now not fair.\nNow Romeo is beloved and loves again,\nAlike betwitched by the charm of looks,\nBut to his foe supposed he must complain,\nAnd she steal love's sweet bait from fearful hooks:\nBeing held a foe, he may not have access\nTo breathe such vows as lovers use to swear;\nAnd she as much in love, her means much less\nTo meet her new-beloved any where:\nBut passion lends them power, time means, to meet\nTempering extremities with extreme sweet.\nExit\n\nSCENE I. A lane by the wall of Capulet's orchard.\n\nEnter ROMEO\nROMEO\nCan I go forward when my heart is here?\nTurn back, dull earth, and find thy centre out.\nHe climbs the wall, and leaps down within it\n\nEnter BENVOLIO and MERCUTIO\n\nBENVOLIO\nRomeo! my cousin Romeo!\nMERCUTIO\nHe is wise;\nAnd, on my lie, hath stol'n him home to bed.\nBENVOLIO\nHe ran this way, and leap'd this orchard wall:\nCall, good Mercutio.\nMERCUTIO\nNay, I'll conjure too.\nRomeo! humours! madman! passion! lover!\nAppear thou in the likeness of a sigh:\nSpeak but one rhyme, and I am satisfied;\nCry but 'Ay me!' pronounce but 'love' and 'dove;'\nSpeak to my gossip Venus one fair word,\nOne nick-name for her purblind son and heir,\nYoung Adam Cupid, he that shot so trim,\nWhen King Cophetua loved the beggar-maid!\nHe heareth not, he stirreth not, he moveth not;\nThe ape is dead, and I must conjure him.\nI conjure thee by Rosaline's bright eyes,\nBy her high forehead and her scarlet lip,\nBy her fine foot, straight leg and quivering thigh\nAnd the demesnes that there adjacent lie,\nThat in thy likeness thou appear to us!\nBENVOLIO\nAnd if he hear thee, thou wilt anger him.\nMERCUTIO\nThis cannot anger him: 'twould anger him\nTo raise a spirit in his mistress' circle\nOf some strange nature, letting it there stand\nTill she had laid it and conjured it down;\nThat were some spite: my invocation\nIs fair and honest, and in his mistres s' name\nI conjure only but to raise up him.\nBENVOLIO\nCome, he hath hid himself among these trees,\nTo be consorted with the humorous night:\nBlind is his love and best befits the dark.\nMERCUTIO\nIf love be blind, love cannot hit the mark.\nNow will he sit under a medlar tree,\nAnd wish his mistress were that kind of fruit\nAs maids call medlars, when they laugh alone.\nRomeo, that she were, O, that she were\nAn open et caetera, thou a poperin pear!\nRomeo, good night: I'll to my truckle-bed;\nThis field-bed is too cold for me to sleep:\nCome, shall we go?\nBENVOLIO\nGo, then; for 'tis in vain\nTo seek him here that means not to be found.\nExeunt\n\nSCENE II. Capulet's orchard.\n\nEnter ROMEO\nROMEO\nHe jests at scars that never felt a wound.\nJULIET appears above at a window\n\nBut, soft! what light through yonder window breaks?\nIt is the east, and Juliet is the sun.\nArise, fair sun, and kill the envious moon,\nWho is already sick and pale with grief,\nThat thou her maid art far more fair than she:\nBe not her maid, since she is envious;\nHer vestal livery is but sick and green\nAnd none but fools do wear it; cast it off.\nIt is my lady, O, it is my love!\nO, that she knew she were!\nShe speaks yet she says nothing: what of that?\nHer eye discourses; I will answer it.\nI am too bold, 'tis not to me she speaks:\nTwo of the fairest stars in all the heaven,\nHaving some business, do entreat her eyes\nTo twinkle in their spheres till they return.\nWhat if her eyes were there, they in her head?\nThe brightness of her cheek would shame those stars,\nAs daylight doth a lamp; her eyes in heaven\nWould through the airy region stream so bright\nThat birds would sing and think it were not night.\nSee, how she leans her cheek upon her hand!\nO, that I were a glove upon that hand,\nThat I might touch that cheek!\nJULIET\nAy me!\nROMEO\nShe speaks:\nO, speak again, bright angel! for thou art\nAs glorious to this night, being o'er my head\nAs is a winged messenger of heaven\nUnto the white-upturned wondering eyes\nOf mortals that fall back to gaze on him\nWhen he bestrides the lazy-pacing clouds\nAnd sails upon the bosom of the air.\nJULIET\nO Romeo, Romeo! wherefore art thou Romeo?\nDeny thy father and refuse thy name;\nOr, if thou wilt not, be but sworn my love,\nAnd I'll no longer be a Capulet.\nROMEO\n[Aside] Shall I hear more, or shall I speak at this?\nJULIET\n'Tis but thy name that is my enemy;\nThou art thyself, though not a Montague.\nWhat's Montague? it is nor hand, nor foot,\nNor arm, nor face, nor any other part\nBelonging to a man. O, be some other name!\nWhat's in a name? that which we call a rose\nBy any other name would smell as sweet;\nSo Romeo would, were he not Romeo call'd,\nRetain that dear perfection which he owes\nWithout that title. Romeo, doff thy name,\nAnd for that name which is no part of thee\nTake all myself.\nROMEO\nI take thee at thy word:\nCall me but love, and I'll be new baptized;\nHenceforth I never will be Romeo.\nJULIET\nWhat man art thou that thus bescreen'd in night\nSo stumblest on my counsel?\nROMEO\nBy a name\nI know not how to tell thee who I am:\nMy name, dear saint, is hateful to myself,\nBecause it is an enemy to thee;\nHad I it written, I would tear the word.\nJULIET\nMy ears have not yet drunk a hundred words\nOf that tongue's utterance, yet I know the sound:\nArt thou not Romeo and a Montague?\nROMEO\nNeither, fair saint, if either thee dislike.\nJULIET\nHow camest thou hither, tell me, and wherefore?\nThe orchard walls are high and hard to climb,\nAnd the place death, considering who thou art,\nIf any of my kinsmen find thee here.\nROMEO\nWith love's light wings did I o'er-perch these walls;\nFor stony limits cannot hold love out,\nAnd what love can do that dares love attempt;\nTherefore thy kinsmen are no let to me.\nJULIET\nIf they do see thee, they will murder thee.\nROMEO\nAlack, there lies more peril in thine eye\nThan twenty of their swords: look thou but sweet,\nAnd I am proof against their enmity.\nJULIET\nI would not for the world they saw thee here.\nROMEO\nI have night's cloak to hide me from their sight;\nAnd but thou love me, let them find me here:\nMy life were better ended by their hate,\nThan death prorogued, wanting of thy love.\nJULIET\nBy whose direction found'st thou out this place?\nROMEO\nBy love, who first did prompt me to inquire;\nHe lent me counsel and I lent him eyes.\nI am no pilot; yet, wert thou as far\nAs that vast shore wash'd with the farthest sea,\nI would adventure for such merchandise.\nJULIET\nThou know'st the mask of night is on my face,\nElse would a maiden blush bepaint my cheek\nFor that which thou hast heard me speak to-night\nFain would I dwell on form, fain, fain deny\nWhat I have spoke: but farewell compliment!\nDost thou love me? I know thou wilt say 'Ay,'\nAnd I will take thy word: yet if thou swear'st,\nThou mayst prove false; at lovers' perjuries\nThen say, Jove laughs. O gentle Romeo,\nIf thou dost love, pronounce it faithfully:\nOr if thou think'st I am too quickly won,\nI'll frown and be perverse an say thee nay,\nSo thou wilt woo; but else, not for the world.\nIn truth, fair Montague, I am too fond,\nAnd therefore thou mayst think my 'havior light:\nBut trust me, gentleman, I'll prove more true\nThan those that have more cunning to be strange.\nI should have been more strange, I must confess,\nBut that thou overheard'st, ere I was ware,\nMy true love's passion: therefore pardon me,\nAnd not impute this yielding to light love,\nWhich the dark night hath so discovered.\nROMEO\nLady, by yonder blessed moon I swear\nThat tips with silver all these fruit-tree tops--\nJULIET\nO, swear not by the moon, the inconstant moon,\nThat monthly changes in her circled orb,\nLest that thy love prove likewise variable.\nROMEO\nWhat shall I swear by?\nJULIET\nDo not swear at all;\nOr, if thou wilt, swear by thy gracious self,\nWhich is the god of my idolatry,\nAnd I'll believe thee.\nROMEO\nIf my heart's dear love--\nJULIET\nWell, do not swear: although I joy in thee,\nI have no joy of this contract to-night:\nIt is too rash, too unadvised, too sudden;\nToo like the lightning, which doth cease to be\nEre one can say 'It lightens.' Sweet, good night!\nThis bud of love, by summer's ripening breath,\nMay prove a beauteous flower when next we meet.\nGood night, good night! as sweet repose and rest\nCome to thy heart as that within my breast!\nROMEO\nO, wilt thou leave me so unsatisfied?\nJULIET\nWhat satisfaction canst thou have to-night?\nROMEO\nThe exchange of thy love's faithful vow for mine.\nJULIET\nI gave thee mine before thou didst request it:\nAnd yet I would it were to give again.\nROMEO\nWouldst thou withdraw it? for what purpose, love?\nJULIET\nBut to be frank, and give it thee again.\nAnd yet I wish but for the thing I have:\nMy bounty is as boundless as the sea,\nMy love as deep; the more I give to thee,\nThe more I have, for both are infinite.\nNurse calls within\n\nI hear some noise within; dear love, adieu!\nAnon, good nurse! Sweet Montague, be true.\nStay but a little, I will come again.\nExit, above\n\nROMEO\nO blessed, blessed night! I am afeard.\nBeing in night, all this is but a dream,\nToo flattering-sweet to be substantial.\nRe-enter JULIET, above\n\nJULIET\nThree words, dear Romeo, and good night indeed.\nIf that thy bent of love be honourable,\nThy purpose marriage, send me word to-morrow,\nBy one that I'll procure to come to thee,\nWhere and what time thou wilt perform the rite;\nAnd all my fortunes at thy foot I'll lay\nAnd follow thee my lord throughout the world.\nNurse\n[Within] Madam!\nJULIET\nI come, anon.--But if thou mean'st not well,\nI do beseech thee--\nNurse\n[Within] Madam!\nJULIET\nBy and by, I come:--\nTo cease thy suit, and leave me to my grief:\nTo-morrow will I send.\nROMEO\nSo thrive my soul--\nJULIET\nA thousand times good night!\nExit, above\n\nROMEO\nA thousand times the worse, to want thy light.\nLove goes toward love, as schoolboys from\ntheir books,\nBut love from love, toward school with heavy looks.\nRetiring\n\nRe-enter JULIET, above\n\nJULIET\nHist! Romeo, hist! O, for a falconer's voice,\nTo lure this tassel-gentle back again!\nBondage is hoarse, and may not speak aloud;\nElse would I tear the cave where Echo lies,\nAnd make her airy tongue more hoarse than mine,\nWith repetition of my Romeo's name.\nROMEO\nIt is my soul that calls upon my name:\nHow silver-sweet sound lovers' tongues by night,\nLike softest music to attending ears!\nJULIET\nRomeo!\nROMEO\nMy dear?\nJULIET\nAt what o'clock to-morrow\nShall I send to thee?\nROMEO\nAt the hour of nine.\nJULIET\nI will not fail: 'tis twenty years till then.\nI have forgot why I did call thee back.\nROMEO\nLet me stand here till thou remember it.\nJULIET\nI shall forget, to have thee still stand there,\nRemembering how I love thy company.\nROMEO\nAnd I'll still stay, to have thee still forget,\nForgetting any other home but this.\nJULIET\n'Tis almost morning; I would have thee gone:\nAnd yet no further than a wanton's bird;\nWho lets it hop a little from her hand,\nLike a poor prisoner in his twisted gyves,\nAnd with a silk thread plucks it back again,\nSo loving-jealous of his liberty.\nROMEO\nI would I were thy bird.\nJULIET\nSweet, so would I:\nYet I should kill thee with much cherishing.\nGood night, good night! parting is such\nsweet sorrow,\nThat I shall say good night till it be morrow.\nExit above\n\nROMEO\nSleep dwell upon thine eyes, peace in thy breast!\nWould I were sleep and peace, so sweet to rest!\nHence will I to my ghostly father's cell,\nHis help to crave, and my dear hap to tell.\nExit\n\nSCENE III. Friar Laurence's cell.\n\nEnter FRIAR LAURENCE, with a basket\nFRIAR LAURENCE\nThe grey-eyed morn smiles on the frowning night,\nChequering the eastern clouds with streaks of light,\nAnd flecked darkness like a drunkard reels\nFrom forth day's path and Titan's fiery wheels:\nNow, ere the sun advance his burning eye,\nThe day to cheer and night's dank dew to dry,\nI must up-fill this osier cage of ours\nWith baleful weeds and precious-juiced flowers.\nThe earth that's nature's mother is her tomb;\nWhat is her burying grave that is her womb,\nAnd from her womb children of divers kind\nWe sucking on her natural bosom find,\nMany for many virtues excellent,\nNone but for some and yet all different.\nO, mickle is the powerful grace that lies\nIn herbs, plants, stones, and their true qualities:\nFor nought so vile that on the earth doth live\nBut to the earth some special good doth give,\nNor aught so good but strain'd from that fair use\nRevolts from true birth, stumbling on abuse:\nVirtue itself turns vice, being misapplied;\nAnd vice sometimes by action dignified.\nWithin the infant rind of this small flower\nPoison hath residence and medicine power:\nFor this, being smelt, with that part cheers each part;\nBeing tasted, slays all senses with the heart.\nTwo such opposed kings encamp them still\nIn man as well as herbs, grace and rude will;\nAnd where the worser is predominant,\nFull soon the canker death eats up that plant.\nEnter ROMEO\n\nROMEO\nGood morrow, father.\nFRIAR LAURENCE\nBenedicite!\nWhat early tongue so sweet saluteth me?\nYoung son, it argues a distemper'd head\nSo soon to bid good morrow to thy bed:\nCare keeps his watch in every old man's eye,\nAnd where care lodges, sleep will never lie;\nBut where unbruised youth with unstuff'd brain\nDoth couch his limbs, there golden sleep doth reign:\nTherefore thy earliness doth me assure\nThou art up-roused by some distemperature;\nOr if not so, then here I hit it right,\nOur Romeo hath not been in bed to-night.\nROMEO\nThat last is true; the sweeter rest was mine.\nFRIAR LAURENCE\nGod pardon sin! wast thou with Rosaline?\nROMEO\nWith Rosaline, my ghostly father? no;\nI have forgot that name, and that name's woe.\nFRIAR LAURENCE\nThat's my good son: but where hast thou been, then?\nROMEO\nI'll tell thee, ere thou ask it me again.\nI have been feasting with mine enemy,\nWhere on a sudden one hath wounded me,\nThat's by me wounded: both our remedies\nWithin thy help and holy physic lies:\nI bear no hatred, blessed man, for, lo,\nMy intercession likewise steads my foe.\nFRIAR LAURENCE\nBe plain, good son, and homely in thy drift;\nRiddling confession finds but riddling shrift.\nROMEO\nThen plainly know my heart's dear love is set\nOn the fair daughter of rich Capulet:\nAs mine on hers, so hers is set on mine;\nAnd all combined, save what thou must combine\nBy holy marriage: when and where and how\nWe met, we woo'd and made exchange of vow,\nI'll tell thee as we pass; but this I pray,\nThat thou consent to marry us to-day.\nFRIAR LAURENCE\nHoly Saint Francis, what a change is here!\nIs Rosaline, whom thou didst love so dear,\nSo soon forsaken? young men's love then lies\nNot truly in their hearts, but in their eyes.\nJesu Maria, what a deal of brine\nHath wash'd thy sallow cheeks for Rosaline!\nHow much salt water thrown away in waste,\nTo season love, that of it doth not taste!\nThe sun not yet thy sighs from heaven clears,\nThy old groans ring yet in my ancient ears;\nLo, here upon thy cheek the stain doth sit\nOf an old tear that is not wash'd off yet:\nIf e'er thou wast thyself and these woes thine,\nThou and these woes were all for Rosaline:\nAnd art thou changed? pronounce this sentence then,\nWomen may fall, when there's no strength in men.\nROMEO\nThou chid'st me oft for loving Rosaline.\nFRIAR LAURENCE\nFor doting, not for loving, pupil mine.\nROMEO\nAnd bad'st me bury love.\nFRIAR LAURENCE\nNot in a grave,\nTo lay one in, another out to have.\nROMEO\nI pray thee, chide not; she whom I love now\nDoth grace for grace and love for love allow;\nThe other did not so.\nFRIAR LAURENCE\nO, she knew well\nThy love did read by rote and could not spell.\nBut come, young waverer, come, go with me,\nIn one respect I'll thy assistant be;\nFor this alliance may so happy prove,\nTo turn your households' rancour to pure love.\nROMEO\nO, let us hence; I stand on sudden haste.\nFRIAR LAURENCE\nWisely and slow; they stumble that run fast.\nExeunt\n\nSCENE IV. A street.\n\nEnter BENVOLIO and MERCUTIO\nMERCUTIO\nWhere the devil should this Romeo be?\nCame he not home to-night?\nBENVOLIO\nNot to his father's; I spoke with his man.\nMERCUTIO\nAh, that same pale hard-hearted wench, that Rosaline.\nTorments him so, that he will sure run mad.\nBENVOLIO\nTybalt, the kinsman of old Capulet,\nHath sent a letter to his father's house.\nMERCUTIO\nA challenge, on my life.\nBENVOLIO\nRomeo will answer it.\nMERCUTIO\nAny man that can write may answer a letter.\nBENVOLIO\nNay, he will answer the letter's master, how he\ndares, being dared.\nMERCUTIO\nAlas poor Romeo! he is already dead; stabbed with a\nwhite wench's black eye; shot through the ear with a\nlove-song; the very pin of his heart cleft with the\nblind bow-boy's butt-shaft: and is he a man to\nencounter Tybalt?\nBENVOLIO\nWhy, what is Tybalt?\nMERCUTIO\nMore than prince of cats, I can tell you. O, he is\nthe courageous captain of compliments. He fights as\nyou sing prick-song, keeps time, distance, and\nproportion; rests me his minim rest, one, two, and\nthe third in your bosom: the very butcher of a silk\nbutton, a duellist, a duellist; a gentleman of the\nvery first house, of the first and second cause:\nah, the immortal passado! the punto reverso! the\nhai!\nBENVOLIO\nThe what?\nMERCUTIO\nThe pox of such antic, lisping, affecting\nfantasticoes; these new tuners of accents! 'By Jesu,\na very good blade! a very tall man! a very good\nwhore!' Why, is not this a lamentable thing,\ngrandsire, that we should be thus afflicted with\nthese strange flies, these fashion-mongers, these\nperdona-mi's, who stand so much on the new form,\nthat they cannot at ease on the old bench? O, their\nbones, their bones!\nEnter ROMEO\n\nBENVOLIO\nHere comes Romeo, here comes Romeo.\nMERCUTIO\nWithout his roe, like a dried herring: flesh, flesh,\nhow art thou fishified! Now is he for the numbers\nthat Petrarch flowed in: Laura to his lady was but a\nkitchen-wench; marry, she had a better love to\nbe-rhyme her; Dido a dowdy; Cleopatra a gipsy;\nHelen and Hero hildings and harlots; Thisbe a grey\neye or so, but not to the purpose. Signior\nRomeo, bon jour! there's a French salutation\nto your French slop. You gave us the counterfeit\nfairly last night.\nROMEO\nGood morrow to you both. What counterfeit did I give you?\nMERCUTIO\nThe ship, sir, the slip; can you not conceive?\nROMEO\nPardon, good Mercutio, my business was great; and in\nsuch a case as mine a man may strain courtesy.\nMERCUTIO\nThat's as much as to say, such a case as yours\nconstrains a man to bow in the hams.\nROMEO\nMeaning, to court'sy.\nMERCUTIO\nThou hast most kindly hit it.\nROMEO\nA most courteous exposition.\nMERCUTIO\nNay, I am the very pink of courtesy.\nROMEO\nPink for flower.\nMERCUTIO\nRight.\nROMEO\nWhy, then is my pump well flowered.\nMERCUTIO\nWell said: follow me this jest now till thou hast\nworn out thy pump, that when the single sole of it\nis worn, the jest may remain after the wearing sole singular.\nROMEO\nO single-soled jest, solely singular for the\nsingleness.\nMERCUTIO\nCome between us, good Benvolio; my wits faint.\nROMEO\nSwitch and spurs, switch and spurs; or I'll cry a match.\nMERCUTIO\nNay, if thy wits run the wild-goose chase, I have\ndone, for thou hast more of the wild-goose in one of\nthy wits than, I am sure, I have in my whole five:\nwas I with you there for the goose?\nROMEO\nThou wast never with me for any thing when thou wast\nnot there for the goose.\nMERCUTIO\nI will bite thee by the ear for that jest.\nROMEO\nNay, good goose, bite not.\nMERCUTIO\nThy wit is a very bitter sweeting; it is a most\nsharp sauce.\nROMEO\nAnd is it not well served in to a sweet goose?\nMERCUTIO\nO here's a wit of cheveril, that stretches from an\ninch narrow to an ell broad!\nROMEO\nI stretch it out for that word 'broad;' which added\nto the goose, proves thee far and wide a broad goose.\nMERCUTIO\nWhy, is not this better now than groaning for love?\nnow art thou sociable, now art thou Romeo; now art\nthou what thou art, by art as well as by nature:\nfor this drivelling love is like a great natural,\nthat runs lolling up and down to hide his bauble in a hole.\nBENVOLIO\nStop there, stop there.\nMERCUTIO\nThou desirest me to stop in my tale against the hair.\nBENVOLIO\nThou wouldst else have made thy tale large.\nMERCUTIO\nO, thou art deceived; I would have made it short:\nfor I was come to the whole depth of my tale; and\nmeant, indeed, to occupy the argument no longer.\nROMEO\nHere's goodly gear!\nEnter Nurse and PETER\n\nMERCUTIO\nA sail, a sail!\nBENVOLIO\nTwo, two; a shirt and a smock.\nNurse\nPeter!\nPETER\nAnon!\nNurse\nMy fan, Peter.\nMERCUTIO\nGood Peter, to hide her face; for her fan's the\nfairer face.\nNurse\nGod ye good morrow, gentlemen.\nMERCUTIO\nGod ye good den, fair gentlewoman.\nNurse\nIs it good den?\nMERCUTIO\n'Tis no less, I tell you, for the bawdy hand of the\ndial is now upon the prick of noon.\nNurse\nOut upon you! what a man are you!\nROMEO\nOne, gentlewoman, that God hath made for himself to\nmar.\nNurse\nBy my troth, it is well said; 'for himself to mar,'\nquoth a'? Gentlemen, can any of you tell me where I\nmay find the young Romeo?\nROMEO\nI can tell you; but young Romeo will be older when\nyou have found him than he was when you sought him:\nI am the youngest of that name, for fault of a worse.\nNurse\nYou say well.\nMERCUTIO\nYea, is the worst well? very well took, i' faith;\nwisely, wisely.\nNurse\nif you be he, sir, I desire some confidence with\nyou.\nBENVOLIO\nShe will indite him to some supper.\nMERCUTIO\nA bawd, a bawd, a bawd! so ho!\nROMEO\nWhat hast thou found?\nMERCUTIO\nNo hare, sir; unless a hare, sir, in a lenten pie,\nthat is something stale and hoar ere it be spent.\nSings\n\nAn old hare hoar,\nAnd an old hare hoar,\nIs very good meat in lent\nBut a hare that is hoar\nIs too much for a score,\nWhen it hoars ere it be spent.\nRomeo, will you come to your father's? we'll\nto dinner, thither.\nROMEO\nI will follow you.\nMERCUTIO\nFarewell, ancient lady; farewell,\nSinging\n\n'lady, lady, lady.'\nExeunt MERCUTIO and BENVOLIO\n\nNurse\nMarry, farewell! I pray you, sir, what saucy\nmerchant was this, that was so full of his ropery?\nROMEO\nA gentleman, nurse, that loves to hear himself talk,\nand will speak more in a minute than he will stand\nto in a month.\nNurse\nAn a' speak any thing against me, I'll take him\ndown, an a' were lustier than he is, and twenty such\nJacks; and if I cannot, I'll find those that shall.\nScurvy knave! I am none of his flirt-gills; I am\nnone of his skains-mates. And thou must stand by\ntoo, and suffer every knave to use me at his pleasure?\nPETER\nI saw no man use you a pleasure; if I had, my weapon\nshould quickly have been out, I warrant you: I dare\ndraw as soon as another man, if I see occasion in a\ngood quarrel, and the law on my side.\nNurse\nNow, afore God, I am so vexed, that every part about\nme quivers. Scurvy knave! Pray you, sir, a word:\nand as I told you, my young lady bade me inquire you\nout; what she bade me say, I will keep to myself:\nbut first let me tell ye, if ye should lead her into\na fool's paradise, as they say, it were a very gross\nkind of behavior, as they say: for the gentlewoman\nis young; and, therefore, if you should deal double\nwith her, truly it were an ill thing to be offered\nto any gentlewoman, and very weak dealing.\nROMEO\nNurse, commend me to thy lady and mistress. I\nprotest unto thee--\nNurse\nGood heart, and, i' faith, I will tell her as much:\nLord, Lord, she will be a joyful woman.\nROMEO\nWhat wilt thou tell her, nurse? thou dost not mark me.\nNurse\nI will tell her, sir, that you do protest; which, as\nI take it, is a gentlemanlike offer.\nROMEO\nBid her devise\nSome means to come to shrift this afternoon;\nAnd there she shall at Friar Laurence' cell\nBe shrived and married. Here is for thy pains.\nNurse\nNo truly sir; not a penny.\nROMEO\nGo to; I say you shall.\nNurse\nThis afternoon, sir? well, she shall be there.\nROMEO\nAnd stay, good nurse, behind the abbey wall:\nWithin this hour my man shall be with thee\nAnd bring thee cords made like a tackled stair;\nWhich to the high top-gallant of my joy\nMust be my convoy in the secret night.\nFarewell; be trusty, and I'll quit thy pains:\nFarewell; commend me to thy mistress.\nNurse\nNow God in heaven bless thee! Hark you, sir.\nROMEO\nWhat say'st thou, my dear nurse?\nNurse\nIs your man secret? Did you ne'er hear say,\nTwo may keep counsel, putting one away?\nROMEO\nI warrant thee, my man's as true as steel.\nNURSE\nWell, sir; my mistress is the sweetest lady--Lord,\nLord! when 'twas a little prating thing:--O, there\nis a nobleman in town, one Paris, that would fain\nlay knife aboard; but she, good soul, had as lief\nsee a toad, a very toad, as see him. I anger her\nsometimes and tell her that Paris is the properer\nman; but, I'll warrant you, when I say so, she looks\nas pale as any clout in the versal world. Doth not\nrosemary and Romeo begin both with a letter?\nROMEO\nAy, nurse; what of that? both with an R.\nNurse\nAh. mocker! that's the dog's name; R is for\nthe--No; I know it begins with some other\nletter:--and she hath the prettiest sententious of\nit, of you and rosemary, that it would do you good\nto hear it.\nROMEO\nCommend me to thy lady.\nNurse\nAy, a thousand times.\nExit Romeo\n\nPeter!\nPETER\nAnon!\nNurse\nPeter, take my fan, and go before and apace.\nExeunt\n\nSCENE V. Capulet's orchard.\n\nEnter JULIET\nJULIET\nThe clock struck nine when I did send the nurse;\nIn half an hour she promised to return.\nPerchance she cannot meet him: that's not so.\nO, she is lame! love's heralds should be thoughts,\nWhich ten times faster glide than the sun's beams,\nDriving back shadows over louring hills:\nTherefore do nimble-pinion'd doves draw love,\nAnd therefore hath the wind-swift Cupid wings.\nNow is the sun upon the highmost hill\nOf this day's journey, and from nine till twelve\nIs three long hours, yet she is not come.\nHad she affections and warm youthful blood,\nShe would be as swift in motion as a ball;\nMy words would bandy her to my sweet love,\nAnd his to me:\nBut old folks, many feign as they were dead;\nUnwieldy, slow, heavy and pale as lead.\nO God, she comes!\nEnter Nurse and PETER\n\nO honey nurse, what news?\nHast thou met with him? Send thy man away.\nNurse\nPeter, stay at the gate.\nExit PETER\n\nJULIET\nNow, good sweet nurse,--O Lord, why look'st thou sad?\nThough news be sad, yet tell them merrily;\nIf good, thou shamest the music of sweet news\nBy playing it to me with so sour a face.\nNurse\nI am a-weary, give me leave awhile:\nFie, how my bones ache! what a jaunt have I had!\nJULIET\nI would thou hadst my bones, and I thy news:\nNay, come, I pray thee, speak; good, good nurse, speak.\nNurse\nJesu, what haste? can you not stay awhile?\nDo you not see that I am out of breath?\nJULIET\nHow art thou out of breath, when thou hast breath\nTo say to me that thou art out of breath?\nThe excuse that thou dost make in this delay\nIs longer than the tale thou dost excuse.\nIs thy news good, or bad? answer to that;\nSay either, and I'll stay the circumstance:\nLet me be satisfied, is't good or bad?\nNurse\nWell, you have made a simple choice; you know not\nhow to choose a man: Romeo! no, not he; though his\nface be better than any man's, yet his leg excels\nall men's; and for a hand, and a foot, and a body,\nthough they be not to be talked on, yet they are\npast compare: he is not the flower of courtesy,\nbut, I'll warrant him, as gentle as a lamb. Go thy\nways, wench; serve God. What, have you dined at home?\nJULIET\nNo, no: but all this did I know before.\nWhat says he of our marriage? what of that?\nNurse\nLord, how my head aches! what a head have I!\nIt beats as it would fall in twenty pieces.\nMy back o' t' other side,--O, my back, my back!\nBeshrew your heart for sending me about,\nTo catch my death with jaunting up and down!\nJULIET\nI' faith, I am sorry that thou art not well.\nSweet, sweet, sweet nurse, tell me, what says my love?\nNurse\nYour love says, like an honest gentleman, and a\ncourteous, and a kind, and a handsome, and, I\nwarrant, a virtuous,--Where is your mother?\nJULIET\nWhere is my mother! why, she is within;\nWhere should she be? How oddly thou repliest!\n'Your love says, like an honest gentleman,\nWhere is your mother?'\nNurse\nO God's lady dear!\nAre you so hot? marry, come up, I trow;\nIs this the poultice for my aching bones?\nHenceforward do your messages yourself.\nJULIET\nHere's such a coil! come, what says Romeo?\nNurse\nHave you got leave to go to shrift to-day?\nJULIET\nI have.\nNurse\nThen hie you hence to Friar Laurence' cell;\nThere stays a husband to make you a wife:\nNow comes the wanton blood up in your cheeks,\nThey'll be in scarlet straight at any news.\nHie you to church; I must another way,\nTo fetch a ladder, by the which your love\nMust climb a bird's nest soon when it is dark:\nI am the drudge and toil in your delight,\nBut you shall bear the burden soon at night.\nGo; I'll to dinner: hie you to the cell.\nJULIET\nHie to high fortune! Honest nurse, farewell.\nExeunt\n\nSCENE VI. Friar Laurence's cell.\n\nEnter FRIAR LAURENCE and ROMEO\nFRIAR LAURENCE\nSo smile the heavens upon this holy act,\nThat after hours with sorrow chide us not!\nROMEO\nAmen, amen! but come what sorrow can,\nIt cannot countervail the exchange of joy\nThat one short minute gives me in her sight:\nDo thou but close our hands with holy words,\nThen love-devouring death do what he dare;\nIt is enough I may but call her mine.\nFRIAR LAURENCE\nThese violent delights have violent ends\nAnd in their triumph die, like fire and powder,\nWhich as they kiss consume: the sweetest honey\nIs loathsome in his own deliciousness\nAnd in the taste confounds the appetite:\nTherefore love moderately; long love doth so;\nToo swift arrives as tardy as too slow.\nEnter JULIET\n\nHere comes the lady: O, so light a foot\nWill ne'er wear out the everlasting flint:\nA lover may bestride the gossamer\nThat idles in the wanton summer air,\nAnd yet not fall; so light is vanity.\nJULIET\nGood even to my ghostly confessor.\nFRIAR LAURENCE\nRomeo shall thank thee, daughter, for us both.\nJULIET\nAs much to him, else is his thanks too much.\nROMEO\nAh, Juliet, if the measure of thy joy\nBe heap'd like mine and that thy skill be more\nTo blazon it, then sweeten with thy breath\nThis neighbour air, and let rich music's tongue\nUnfold the imagined happiness that both\nReceive in either by this dear encounter.\nJULIET\nConceit, more rich in matter than in words,\nBrags of his substance, not of ornament:\nThey are but beggars that can count their worth;\nBut my true love is grown to such excess\nI cannot sum up sum of half my wealth.\nFRIAR LAURENCE\nCome, come with me, and we will make short work;\nFor, by your leaves, you shall not stay alone\nTill holy church incorporate two in one.\nExeunt\n\nACT III\n\nSCENE I. A public place.\n\nEnter MERCUTIO, BENVOLIO, Page, and Servants\nBENVOLIO\nI pray thee, good Mercutio, let's retire:\nThe day is hot, the Capulets abroad,\nAnd, if we meet, we shall not scape a b",
+ "link": "https://taliwutblog.wordpress.com/2014/01/02/romeo/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/02/romeo/",
+ "post_status": "publish",
+ "postid": "261",
+ "title": "Romeo",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "romeo"
+ },
+ {
+ "categories": [
+ "bitcoin"
+ ],
+ "custom_fields": [
+ {
+ "id": "1104",
+ "key": "jabber_published",
+ "value": "1388660489"
+ }
+ ],
+ "dateCreated": "Jan 2, 2014 11:01:28 AM",
+ "date_created_gmt": "Jan 2, 2014 11:01:28 AM",
+ "date_modified": "Jan 2, 2014 11:01:28 AM",
+ "date_modified_gmt": "Jan 2, 2014 11:01:28 AM",
+ "description": "oh yeah #bitcoin !",
+ "link": "https://taliwutblog.wordpress.com/2014/01/02/bitcoin/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "bitcoin",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2014/01/02/bitcoin/",
+ "post_status": "publish",
+ "postid": "259",
+ "title": "Bitcoin",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "bitcoin"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1098",
+ "key": "geo_latitude",
+ "value": "48.8428791"
+ },
+ {
+ "id": "1099",
+ "key": "geo_longitude",
+ "value": "2.3055229"
+ },
+ {
+ "id": "1100",
+ "key": "geo_public",
+ "value": "1"
+ }
+ ],
+ "dateCreated": "Dec 27, 2013 10:56:33 AM",
+ "date_created_gmt": "Dec 27, 2013 10:56:33 AM",
+ "date_modified": "Dec 27, 2013 10:57:19 AM",
+ "date_modified_gmt": "Dec 27, 2013 10:57:19 AM",
+ "description": "Gghh",
+ "link": "https://taliwutblog.wordpress.com/2013/12/27/gghh/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "hhyhg, jjhb",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2013/12/27/gghh/",
+ "post_status": "private",
+ "postid": "256",
+ "title": "Gghh",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "standard",
+ "wp_post_thumbnail": "",
+ "wp_slug": "gghh"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1088",
+ "key": "geo_latitude",
+ "value": "48.8429192"
+ },
+ {
+ "id": "1089",
+ "key": "geo_longitude",
+ "value": "2.3055496"
+ },
+ {
+ "id": "1090",
+ "key": "geo_public",
+ "value": "1"
+ },
+ {
+ "id": "1092",
+ "key": "jabber_published",
+ "value": "1388141763"
+ }
+ ],
+ "dateCreated": "Dec 27, 2013 10:56:02 AM",
+ "date_created_gmt": "Dec 27, 2013 10:56:02 AM",
+ "date_modified": "Dec 27, 2013 10:56:03 AM",
+ "date_modified_gmt": "Dec 27, 2013 10:56:03 AM",
+ "description": "<a href=\"http://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg\"><img title=\"wp-1388141726961\" class=\"alignnone size-full\" alt=\"image\" src=\"http://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg\" /></a>",
+ "link": "https://taliwutblog.wordpress.com/2013/12/27/254/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2013/12/27/254/",
+ "post_status": "publish",
+ "postid": "254",
+ "title": "",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "image",
+ "wp_post_thumbnail": "",
+ "wp_slug": "254"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1027",
+ "key": "jabber_published",
+ "value": "1387524751"
+ }
+ ],
+ "dateCreated": "Dec 20, 2013 7:32:31 AM",
+ "date_created_gmt": "Dec 20, 2013 7:32:31 AM",
+ "date_modified": "Dec 26, 2013 2:52:12 PM",
+ "date_modified_gmt": "Dec 26, 2013 2:52:12 PM",
+ "description": "<a href=\"http://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1387524739322.gif\"><img title=\"wp-1387524739322\" class=\"alignnone size-full\" alt=\"image\" src=\"http://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1387524739322.gif\" /></a>",
+ "link": "https://taliwutblog.wordpress.com/2013/12/20/pony-2/",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/2013/12/20/pony-2/",
+ "post_status": "private",
+ "postid": "234",
+ "title": "Pony",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "image",
+ "wp_post_thumbnail": "",
+ "wp_slug": "pony-2"
+ },
+ {
+ "categories": [
+ "Uncategorized"
+ ],
+ "custom_fields": [
+ {
+ "id": "1016",
+ "key": "jabber_published",
+ "value": "1387524660"
+ }
+ ],
+ "dateCreated": "Dec 20, 2013 7:31:00 AM",
+ "date_created_gmt": "Dec 20, 2013 7:31:00 AM",
+ "date_modified": "Dec 26, 2013 2:52:57 PM",
+ "date_modified_gmt": "Dec 26, 2013 2:52:57 PM",
+ "description": "<a href=\"http://taliwutblog.files.wordpress.com/2013/12/wpid-9117-animated_gif-derpy_hooves-mailbox-my_little_pony_friendship_is_magic-tagme.gif\"><img title=\"9117 - animated_gif derpy_hooves mailbox my_little_pony_friendship_is_magic tagme.gif\" class=\"alignnone size-full\" alt=\"image\" src=\"http://taliwutblog.files.wordpress.com/2013/12/wpid-9117-animated_gif-derpy_hooves-mailbox-my_little_pony_friendship_is_magic-tagme.gif\" /></a>",
+ "link": "https://taliwutblog.wordpress.com/?p=230",
+ "mt_allow_comments": 1,
+ "mt_allow_pings": 1,
+ "mt_excerpt": "",
+ "mt_keywords": "",
+ "mt_text_more": "",
+ "permaLink": "https://taliwutblog.wordpress.com/?p=230",
+ "post_status": "pending",
+ "postid": "230",
+ "title": "Gif test",
+ "userid": "55434822",
+ "wp_author_display_name": "taliwutt",
+ "wp_author_id": "55434822",
+ "wp_more_text": "",
+ "wp_password": "",
+ "wp_post_format": "image",
+ "wp_post_thumbnail": "",
+ "wp_slug": "gif-test"
+ }
+]
diff --git a/WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.xml b/WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.xml
new file mode 100644
index 000000000..e938c964b
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-metaWeblog.getRecentPosts.xml
@@ -0,0 +1,4203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140211T16:04:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>306</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>enft</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/11/enft/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/11/enft/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>enft</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140211T16:04:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1411</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1392048234</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140210T16:04:31</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T16:04:31</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140211T11:30:35</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>333</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;//&quot;&gt;test 1&lt;/a&gt; &lt;a href=&quot;//login&quot;&gt;Test 2&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Fun with URLs</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/11/fun-with-urls/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/11/fun-with-urls/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>fun-with-urls</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140211T11:30:35</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1425</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1392118236</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140211T11:30:36</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140211T11:30:36</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140210T16:40:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>331</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>hb</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wftfwtfw</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/10/wftfwtfw/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/10/wftfwtfw/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>wftfwtfw</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T16:40:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1418</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1392050448</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140210T16:40:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T16:40:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:44</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>330</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>jey*</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>#BookReview Bridget Jones: Mad About the Boy</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-4/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-4/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>bookreview-bridget-jones-mad-about-the-boy-4</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:44</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1406</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>57991476</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1407</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1400</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391606386</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1405</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>302</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:44</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:44</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>329</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>e*</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>#BookReview Bridget Jones: Mad About the Boy</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-3/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-3/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>bookreview-bridget-jones-mad-about-the-boy-3</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1397</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>57991476</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1398</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1391</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391606379</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1396</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>302</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T13:19:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140205T12:42:13</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>327</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://www.youtube.com/watch?v=GP7aP67qQjQ&quot;&gt;http://www.youtube.com/watch?v=GP7aP67qQjQ&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Watch &quot;NORMAN - LUIGI CLASH MARIO&quot; on YouTube</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/watch-norman-luigi-clash-mario-on-youtube/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/watch-norman-luigi-clash-mario-on-youtube/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>watch-norman-luigi-clash-mario-on-youtube</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T12:42:13</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1384</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391604135</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140205T12:42:14</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T12:42:14</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140205T07:14:33</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>325</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>[youtube https://www.youtube.com/watch?v=belUlgnhu9M&amp;w=560&amp;h=315]</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Test YouTube</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/test-youtube/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/test-youtube/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>test-youtube</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T07:14:33</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1377</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391584474</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140205T07:14:34</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T07:14:34</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:45</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>324</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Your Keyboard &amp;amp; You. I'll Stick With Colemak</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>your-keyboard-you-ill-stick-with-colemak-2</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:45</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1372</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>56042455</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1373</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1366</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391448767</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1371</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>631</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:45</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:45</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:36</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>323</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Your Keyboard &amp;amp; You. I'll Stick With Colemak</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>your-keyboard-you-ill-stick-with-colemak</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:36</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1363</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>56042455</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1364</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1357</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391448758</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1362</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>631</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:36</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T17:32:36</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140203T12:37:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>321</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://impactjs.com/forums/games/real-time-chess&quot;&gt;http://impactjs.com/forums/games/real-time-chess&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Forget about turns, this is real time chess. It's hectic, chaotic and short. The winner is the guy who is able to process everything and cope under stress. Welcome to the new chess experience</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/forget-about-turns-this-is-real-time-chess-its-hectic-chaotic-and-short-the-winner-is-the-guy-who-is-able-to-process-everything-and-cope-under-stress-welcome-to-the-new-chess-experience/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/forget-about-turns-this-is-real-time-chess-its-hectic-chaotic-and-short-the-winner-is-the-guy-who-is-able-to-process-everything-and-cope-under-stress-welcome-to-the-new-chess-experience/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>forget-about-turns-this-is-real-time-chess-its-hectic-chaotic-and-short-the-winner-is-the-guy-who-is-able-to-process-everything-and-cope-under-stress-welcome-to-the-new-chess-experience</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T12:37:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1350</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391431048</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140203T12:37:27</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T12:37:27</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140203T11:51:11</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>319</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://taliwutblog.files.wordpress.com/2014/02/wpid-urbanherovantbarreeltje.jpg&quot;&gt;&lt;img title=&quot;Urbanherovantbarreeltje.JPG&quot; class=&quot;alignnone size-full&quot; alt=&quot;image&quot; src=&quot;http://taliwutblog.files.wordpress.com/2014/02/wpid-urbanherovantbarreeltje.jpg&quot; /&gt;&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Another Pony</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/another-pony/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/another-pony/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>another-pony</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T11:51:11</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1342</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391428272</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>image</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140203T11:51:11</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T11:51:11</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140203T10:20:28</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>316</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://taliwutblog.files.wordpress.com/2014/02/wpid-pony1.jpg&quot;&gt;&lt;img title=&quot;pony.jpg&quot; class=&quot;alignnone size-full&quot; alt=&quot;image&quot; src=&quot;http://taliwutblog.files.wordpress.com/2014/02/wpid-pony1.jpg&quot; /&gt;&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Cute pony</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/cute-pony-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/cute-pony-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>cute-pony-2</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T10:20:28</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1333</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391422828</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>image</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140203T10:20:28</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T10:20:28</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140203T09:49:15</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>313</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://taliwutblog.files.wordpress.com/2014/02/wpid-pony.jpg&quot;&gt;&lt;img title=&quot;pony.jpg&quot; class=&quot;alignnone size-full&quot; alt=&quot;image&quot; src=&quot;http://taliwutblog.files.wordpress.com/2014/02/wpid-pony.jpg&quot; /&gt;&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Cute pony</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/cute-pony/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/cute-pony/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>cute-pony</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T09:49:15</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1324</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391420956</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>image</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140203T09:49:16</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T09:49:16</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140130T19:02:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>50540106</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>303</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>The New iOS and Android Apps Have Arrived!</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>the-new-ios-and-android-apps-have-arrived</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>50540106</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>Maxime</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140130T19:02:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1290</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>3584907</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1291</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1286</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391108569</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1289</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>23682</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1297</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>reblog_snapshot</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>O:8:&quot;stdClass&quot;:8:{s:5:&quot;title&quot;;s:42:&quot;The New iOS and Android Apps Have Arrived!&quot;;s:4:&quot;type&quot;;s:4:&quot;post&quot;;s:9:&quot;mime_type&quot;;s:0:&quot;&quot;;s:6:&quot;format&quot;;b:0;s:12:&quot;modified_gmt&quot;;s:19:&quot;2014-01-30 18:59:14&quot;;s:9:&quot;permalink&quot;;s:60:&quot;http://en.blog.wordpress.com/2014/01/30/updated-ios-android/&quot;;s:7:&quot;content&quot;;s:3728:&quot;&lt;div class=&quot;reblogged-content&quot;&gt;More and more of us are blogging from our mobile devices. Today, we’re thrilled to announce new versions of the WordPress mobile apps for Android and iOS. Here are some of the new versions' highlights. &lt;img class=&quot;aligncenter size-full wp-image-23687&quot; alt=&quot;android26-ios39-promo&quot; src=&quot;http://taliwutblog.files.wordpress.com/2014/01/android26-ios39-promo.png&quot; width=&quot;635&quot; height=&quot;423&quot; data-originalSrc=&quot;http://en.blog.files.wordpress.com/2014/01/android26-ios39-promo.png&quot; data-rehosted=&quot;1&quot;&gt;&lt;h3&gt;WordPress for iOS 3.9&lt;/h3&gt;
+ The latest WordPress for iOS update is one of our largest app releases to date. This update is remarkable both for the significant changes we've introduced, and for the level of dedication it received from our hard-working team members. Version 3.9 includes a major visual redesign of the app. We decided to drop the sidebar navigation and embrace a tab bar-based layout. The app's new design allowed us to add numerous visual improvements throughout, including revamped and enhanced Reader, Comments, and Notifications sections. We also created a seamless inline commenting experience to make it easier for you to engage with the content you love. Finally, we made visual improvements to the editing experience of posts and pages.
+ Our team has embraced the latest and greatest technologies that Apple has provided us with iOS 7 to deliver you the best app possible. Version 3.9 and future updates will require iOS 7. The app also includes several other changes. On top of various bug fixes and performance improvements, it now supports deep-linking from Twitter, and features an improved login screen. Be sure to try it out: &lt;a href=&quot;https://itunes.apple.com/us/app/wordpress/id335703880?mt=8&quot;&gt;Download from the App Store&lt;/a&gt; &lt;h3&gt;WordPress for Android 2.6&lt;/h3&gt;
+ The latest update to WordPress for Android includes a new reading and setup experience, as well as significant updates to the user interface. The app Reader has been completely redesigned, and now provides a much-improved, native reading experience. You'll definitely notice its speed -- posts appear in a snap, and images fade in as they load. You can also view users that have commented or liked posts, as well as edit the list of tags that you follow. We've revamped the like, reblog, and comment interfaces to make it easier than ever to respond to posts that strike your fancy.
+ When signing in to the app or creating an account on WordPress.com, you’ll notice a brand new user interface that makes it super-simple to start blogging. If you keep multiple blogs on your account, they will all be automatically added for you. You can also hide whichever blogs you don't wish to work on in the app. We've given the app a facelift, including a new color scheme, a refined navigation drawer layout, and sharp-looking lists in notifications, posts, pages and comments.
+ You’ll also notice some changes to the post editor, with larger images and a new Post Settings area where you'll manage post data such as status, post formats, and categories, among others. The post content area will now go full screen while you are editing, to give you maximum space to focus on your content. Give the app a try here: &lt;a href=&quot;https://play.google.com/store/apps/details?id=org.wordpress.android&quot;&gt;Download from Google Play&lt;/a&gt; &lt;h3&gt;What’s next?&lt;/h3&gt;
+ The mobile team isn’t stopping here! We have big plans for the months to come and for the rest of 2014. You can keep up with the development progress over at http://make.wordpress.org/mobile. You can also follow the apps on twitter &lt;a href=&quot;http://twitter.com/wpandroid&quot;&gt;@WPAndroid&lt;/a&gt; and &lt;a href=&quot;http://twitter.com/wordpressios&quot;&gt;@WordPressiOS&lt;/a&gt;.&lt;/div&gt;&quot;;s:15:&quot;images_mirrored&quot;;i:1;}</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140130T19:02:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140130T19:02:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string>304</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140130T10:00:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>308</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;strong&gt;wft&lt;/strong&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>art</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/art/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/art/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>art</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140130T10:00:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1304</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1391162423</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140131T10:01:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140131T10:01:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140128T10:01:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>302</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>ib/Tqg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>#BookReview Bridget Jones: Mad About the Boy</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/28/bookreview-bridget-jones-mad-about-the-boy-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/28/bookreview-bridget-jones-mad-about-the-boy-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>bookreview-bridget-jones-mad-about-the-boy-2</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140128T10:01:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1282</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>31639867</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1283</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1276</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1390903288</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1281</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>7493</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140128T10:01:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140128T10:01:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140122T13:03:47</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>299</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>[caption id=&quot;&quot; align=&quot;alignnone&quot; width=&quot;2000&quot; caption=&quot;Mop&quot;]&lt;a href=&quot;https://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg&quot;&gt;&lt;img title=&quot;wpid-wp-1388141726961.jpeg&quot; class=&quot;alignnone size-full&quot; alt=&quot;image&quot; src=&quot;https://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg?w=2000&quot; /&gt;&lt;/a&gt;[/caption]</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/cat-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/cat-2/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>cat-2</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T13:03:47</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1250</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1390395828</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140122T13:03:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T13:03:48</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140122T12:59:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>297</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://www.google.com/search?hl=en&amp;amp;biw=384&amp;amp;bih=640&amp;amp;tbm=isch&amp;amp;sa=1&amp;amp;ei=RbLKUv-lHYem0wW8oYHABA&amp;amp;q=hd+wallpaper+photo&amp;amp;oq=hd+wallpaper+photo&amp;amp;gs_l=mobile-gws-serp.3..0l4j0i5.4612.5034.0.5279.4.4.0.0.0.0.203.203.2-1.1.0....0...1c.1.32.mobile-gws-serp..3.1.203.63ZBZ9KeMPs#biv=i%7C10%3Bd%7Cfmk-ugJVQIMDBM%3A&quot;&gt;http://www.google.com/search?hl=en&amp;amp;biw=384&amp;amp;bih=640&amp;amp;tbm=isch&amp;amp;sa=1&amp;amp;ei=RbLKUv-lHYem0wW8oYHABA&amp;amp;q=hd+wallpaper+photo&amp;amp;oq=hd+wallpaper+photo&amp;amp;gs_l=mobile-gws-serp.3..0l4j0i5.4612.5034.0.5279.4.4.0.0.0.0.203.203.2-1.1.0....0...1c.1.32.mobile-gws-serp..3.1.203.63ZBZ9KeMPs#biv=i%7C10%3Bd%7Cfmk-ugJVQIMDBM%3A&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>hd wallpaper photo - Google Search</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/hd-wallpaper-photo-google-search/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/hd-wallpaper-photo-google-search/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>hd-wallpaper-photo-google-search</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T12:59:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1244</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1390395578</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140122T12:59:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T12:59:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140122T12:36:20</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>296</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>Hai</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>The End of Unrecorded Life</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>the-end-of-unrecorded-life</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T12:36:20</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1241</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>blog_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>55267051</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1242</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>is_reblog</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1235</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1390394182</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1240</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>post_id</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>642</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140122T12:36:20</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T12:36:20</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140122T10:32:40</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>292</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string>&lt;a href=&quot;http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386748907.jpg&quot;&gt;&lt;img title=&quot;wp-1390386748907.jpg&quot; class=&quot;alignnone size-full&quot; alt=&quot;image&quot; src=&quot;http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386748907.jpg&quot; /&gt;&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>Geny!</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/geny/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/geny/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>geny</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T10:32:40</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1228</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1390386762</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>image</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140131T09:59:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140131T09:59:38</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-clicks.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-clicks.json
new file mode 100644
index 000000000..9f1ebb72c
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-clicks.json
@@ -0,0 +1,26 @@
+{
+ "date": "2014-11-03",
+ "days": {
+ "2014-11-03": {
+ "clicks": [
+ {
+ "icon": null,
+ "url": "http://astralbodies.net/blog/2013/10/31/paying-attention-at-automattic/",
+ "name": "astralbodies.net/blog/2013/10/31/paying-attention-at-automattic/",
+ "views": 1,
+ "children": null
+ },
+ {
+ "icon": null,
+ "url": "https://devforums.apple.com/thread/86137",
+ "name": "devforums.apple.com/thread/86137",
+ "views": 1,
+ "children": null
+ }
+ ],
+ "other_clicks": 0,
+ "total_clicks": 2
+ }
+ },
+ "period": "day"
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-comments.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-comments.json
new file mode 100644
index 000000000..93f4abf79
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-comments.json
@@ -0,0 +1,160 @@
+{
+ "date": "2014-12-16",
+ "authors": [
+ {
+ "name": "Aaron Douglas",
+ "link": "?user_id=67137",
+ "gravatar": "https://1.gravatar.com/avatar/db127a496309f2717657d6f6167abd49?s=64&amp;d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "20",
+ "follow_data": false
+ },
+ {
+ "name": "Joe R.",
+ "link": "?s=joe@modmobile.net",
+ "gravatar": "https://1.gravatar.com/avatar/a56c4eaf07d062dd9f13f94956d87620?s=64&amp;d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "3",
+ "follow_data": null
+ },
+ {
+ "name": "Murray Sagal",
+ "link": "?s=murraysagal@gmail.com",
+ "gravatar": "https://1.gravatar.com/avatar/480b5bd2a5c318e277c20e434072a7c0?s=64&amp;d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "3",
+ "follow_data": null
+ },
+ {
+ "name": "Dan",
+ "link": "?user_id=5073742",
+ "gravatar": "https://2.gravatar.com/avatar/ed3eefbe2cdb55d5fa0ed5da44c9608a?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "3",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "danroundhill.wordpress.com",
+ "blog_url": "http://danroundhill.wordpress.com",
+ "blog_id": 4836651,
+ "site_id": 4836651,
+ "blog_title": "Around the Hill",
+ "is_following": true
+ },
+ "type": "follow"
+ }
+ },
+ {
+ "name": "Charles Araujo",
+ "link": "?s=charlesaaraujo@gmail.com",
+ "gravatar": "https://0.gravatar.com/avatar/0f410f110603f11a20746f19d9fee68c?s=64&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "2",
+ "follow_data": null
+ },
+ {
+ "name": "Stephane Daury",
+ "link": "?user_id=1494209",
+ "gravatar": "https://2.gravatar.com/avatar/5b8d74a711e183850bd70ccdd440d15e?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "2",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "stephdau.wordpress.com",
+ "blog_url": "http://stephdau.wordpress.com",
+ "blog_id": 1618138,
+ "site_id": 1618138,
+ "blog_title": "tekArtist",
+ "is_following": false
+ },
+ "type": "follow"
+ }
+ },
+ {
+ "name": "pacoverde",
+ "link": "?user_id=16593546",
+ "gravatar": "https://2.gravatar.com/avatar/54fe301056974f51b236ed64521fbb1e?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R",
+ "comments": "2",
+ "follow_data": false
+ }
+ ],
+ "posts": [
+ {
+ "name": "Mac Screen Sharing (VNC) & White Screen",
+ "link": "http://astralbodi.es/2010/05/02/mac-screen-sharing-vnc-white-screen/",
+ "id": "67",
+ "comments": "29"
+ },
+ {
+ "name": "Paying Attention at Automattic",
+ "link": "http://astralbodi.es/2013/10/31/paying-attention-at-automattic/",
+ "id": "267",
+ "comments": "17"
+ },
+ {
+ "name": "Xcode 4 - Problem submitting App with Static Library",
+ "link": "http://astralbodi.es/2011/03/27/xcode-4-problem-submitting-app-with-static-library/",
+ "id": "130",
+ "comments": "11"
+ },
+ {
+ "name": "Installing MySQL 5.5.8 on Mac OS X Snow Leopard",
+ "link": "http://astralbodi.es/2010/12/21/installing-mysql-5-5-8-on-mac-os-x-snow-leopard/",
+ "id": "74",
+ "comments": "11"
+ },
+ {
+ "name": "Fix ScanSnap on Mac not opening Evernote properly",
+ "link": "http://astralbodi.es/2013/12/27/scansnap-on-mac-not-opening-evernote-properly/",
+ "id": "297",
+ "comments": "10"
+ },
+ {
+ "name": "iOS Basics - UINavigation Controller & Back Button Text",
+ "link": "http://astralbodi.es/2011/03/20/ios-basics-uinavigation-controller-back-button-text/",
+ "id": "120",
+ "comments": "9"
+ },
+ {
+ "name": "Adium always presenting Apple Quarantine Message",
+ "link": "http://astralbodi.es/2012/01/01/adium-always-presenting-apple-quarantine-message/",
+ "id": "173",
+ "comments": "6"
+ },
+ {
+ "name": "Resizing a UITextView automatically with the keyboard",
+ "link": "http://astralbodi.es/2012/02/01/resizing-a-uitextview-automatically-with-the-keyboard/",
+ "id": "200",
+ "comments": "5"
+ },
+ {
+ "name": "Xcode SCM & build directory",
+ "link": "http://astralbodi.es/2009/08/03/xcode-scm-build-directory/",
+ "id": "43",
+ "comments": "5"
+ },
+ {
+ "name": "Loading a UIImage from a bundle",
+ "link": "http://astralbodi.es/2009/06/03/loading-a-uiimage-from-a-bundle/",
+ "id": "33",
+ "comments": "5"
+ },
+ {
+ "name": "Mac OS X 10.9 Mavericks Calendar + Google Sync Problems",
+ "link": "http://astralbodi.es/2014/06/05/mac-os-x-10-9-mavericks-calendar-google-sync-problems/",
+ "id": "725",
+ "comments": "4"
+ }
+ ],
+ "monthly_comments": 2,
+ "total_comments": "177",
+ "most_active_day": "",
+ "most_active_time": "08:00",
+ "most_commented_post": {
+ "name": "Mac Screen Sharing (VNC) & White Screen",
+ "link": "http://astralbodi.es/2010/05/02/mac-screen-sharing-vnc-white-screen/",
+ "id": "67",
+ "comments": "29"
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-country-views.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-country-views.json
new file mode 100644
index 000000000..60370bc2d
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-country-views.json
@@ -0,0 +1,113 @@
+{
+ "date": "2015-02-06",
+ "days": {
+ "2015-02-06": {
+ "views": [
+ {
+ "country_code": "US",
+ "views": 8
+ },
+ {
+ "country_code": "TW",
+ "views": 6
+ },
+ {
+ "country_code": "DE",
+ "views": 4
+ },
+ {
+ "country_code": "IN",
+ "views": 4
+ },
+ {
+ "country_code": "PH",
+ "views": 4
+ },
+ {
+ "country_code": "GR",
+ "views": 3
+ },
+ {
+ "country_code": "FR",
+ "views": 3
+ },
+ {
+ "country_code": "BR",
+ "views": 2
+ },
+ {
+ "country_code": "CA",
+ "views": 2
+ },
+ {
+ "country_code": "HU",
+ "views": 2
+ }
+ ],
+ "other_views": 17,
+ "total_views": 55
+ }
+ },
+ "country-info": {
+ "US": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/5a83891a81b057fed56930a6aaaf7b3c?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/9f4faa5ad0c723474f7a6d810172447c?s=48",
+ "country_full": "United States",
+ "map_region": "021"
+ },
+ "TW": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/f983fff0dda7387746b697cfd865e657?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/2c224480a40527ee89d7340d4396e8e6?s=48",
+ "country_full": "Taiwan",
+ "map_region": "030"
+ },
+ "PH": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/9fb5caa12630ee351f854526887036b5?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/b45edf6312236c2ed67e9d6943b41862?s=48",
+ "country_full": "Philippines",
+ "map_region": "035"
+ },
+ "DE": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/e13c43aa12cd8aada2ffb1663970374f?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/82f933cabd7491369097f681958bdaed?s=48",
+ "country_full": "Germany",
+ "map_region": "155"
+ },
+ "IN": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/217b6ac82c316e3a176351cef1d2d0b6?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/d449a857f065ec5ddf1e7a086001a541?s=48",
+ "country_full": "India",
+ "map_region": "034"
+ },
+ "FR": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/bff4fa191e38bc0a316410b8fd2958fd?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/8139b3de98c828078f8a0f7deec0c79b?s=48",
+ "country_full": "France",
+ "map_region": "155"
+ },
+ "GR": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/b6b7e68f84a52ab815467a6fbec1f3c0?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/9b9c3f808361ec2e84526c44eb42944c?s=48",
+ "country_full": "Greece",
+ "map_region": "039"
+ },
+ "CA": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/7f3085b2665ac78346be5923724ba4c6?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/685ac009247bf3378158ee41c3f8f250?s=48",
+ "country_full": "Canada",
+ "map_region": "021"
+ },
+ "HU": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/33a7eb058641623442e0f785b2b1e112?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/cc6139c667fe0f8c1a0b609c01c4c51e?s=48",
+ "country_full": "Hungary",
+ "map_region": "151"
+ },
+ "BR": {
+ "flag_icon": "https://secure.gravatar.com/blavatar/2eb39070460892f8d51479ce95484f09?s=48",
+ "flat_flag_icon": "https://secure.gravatar.com/blavatar/254e046ea74f30ab535952b4ce25f0cb?s=48",
+ "country_full": "Brazil",
+ "map_region": "005"
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-followers.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-followers.json
new file mode 100644
index 000000000..8e4ba45c1
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-followers.json
@@ -0,0 +1,58 @@
+{
+ "page": 1,
+ "pages": 419,
+ "total": 2931,
+ "total_email": 2931,
+ "total_wpcom": 7926165,
+ "subscribers": [
+ {
+ "avatar": "https://2.gravatar.com/avatar/e82142697283897ad7444810e5975895?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user1@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-16T11:24:41+00:00"
+ },
+ {
+ "avatar": "https://0.gravatar.com/avatar/c0886cabd7d010a69b49eab4b2749ae6?s=64&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user2@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-16T06:44:34+00:00"
+ },
+ {
+ "avatar": "https://0.gravatar.com/avatar/3804ce80c05eaa18d9d600f800a697b5?s=64&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user3@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-16T06:25:17+00:00"
+ },
+ {
+ "avatar": "https://2.gravatar.com/avatar/59d507351cc16802fcd0d36d03425e5b?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user4@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-15T22:30:39+00:00"
+ },
+ {
+ "avatar": "https://2.gravatar.com/avatar/5087090dedc4fb5e802a2ee541084dc1?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user5@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-15T22:14:37+00:00"
+ },
+ {
+ "avatar": "https://2.gravatar.com/avatar/e98a1902a1c1e947f040956916cc8fa1?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user6@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-15T20:20:33+00:00"
+ },
+ {
+ "avatar": "https://0.gravatar.com/avatar/3b37f38b63ce4f595cc5cfbaadb10938?s=64&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "user7@example.com",
+ "url": null,
+ "follow_data": null,
+ "date_subscribed": "2014-12-15T15:09:01+00:00"
+ }
+ ]
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-insights.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-insights.json
new file mode 100644
index 000000000..ed333eff2
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-insights.json
@@ -0,0 +1 @@
+{"highest_hour":9,"highest_day_of_week":5,"highest_day_percent":30.532081377152} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-post-123.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-post-123.json
new file mode 100644
index 000000000..834d3f6c6
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-post-123.json
@@ -0,0 +1,1376 @@
+{
+ "date": "2015-03-04",
+ "views": 1323,
+ "years": {
+ "2014": {
+ "months": {
+ "6": 47,
+ "7": 122,
+ "8": 173,
+ "9": 229,
+ "10": 278,
+ "11": 141,
+ "12": 107
+ },
+ "total": 1097
+ },
+ "2015": {
+ "months": {
+ "1": 125,
+ "2": 83,
+ "3": 18
+ },
+ "total": 226
+ }
+ },
+ "averages": {
+ "2014": {
+ "months": {
+ "6": 1,
+ "7": 3,
+ "8": 5,
+ "9": 7,
+ "10": 8,
+ "11": 4,
+ "12": 3
+ },
+ "overall": 5
+ },
+ "2015": {
+ "months": {
+ "1": 4,
+ "2": 2,
+ "3": 3
+ },
+ "overall": 3
+ }
+ },
+ "weeks": [
+ {
+ "days": [
+ {
+ "day": "2015-01-26",
+ "count": 6
+ },
+ {
+ "day": "2015-01-27",
+ "count": 3
+ },
+ {
+ "day": "2015-01-28",
+ "count": 6
+ },
+ {
+ "day": "2015-01-29",
+ "count": 2
+ },
+ {
+ "day": "2015-01-30",
+ "count": 4
+ },
+ {
+ "day": "2015-01-31",
+ "count": 1
+ },
+ {
+ "day": "2015-02-01",
+ "count": 3
+ }
+ ],
+ "total": 25,
+ "average": 3,
+ "change": null
+ },
+ {
+ "days": [
+ {
+ "day": "2015-02-02",
+ "count": 8
+ },
+ {
+ "day": "2015-02-03",
+ "count": 6
+ },
+ {
+ "day": "2015-02-04",
+ "count": 2
+ },
+ {
+ "day": "2015-02-05",
+ "count": 5
+ },
+ {
+ "day": "2015-02-06",
+ "count": 2
+ },
+ {
+ "day": "2015-02-07",
+ "count": 2
+ },
+ {
+ "day": "2015-02-08",
+ "count": 3
+ }
+ ],
+ "total": 28,
+ "average": 4,
+ "change": 12
+ },
+ {
+ "days": [
+ {
+ "day": "2015-02-09",
+ "count": 0
+ },
+ {
+ "day": "2015-02-10",
+ "count": 3
+ },
+ {
+ "day": "2015-02-11",
+ "count": 4
+ },
+ {
+ "day": "2015-02-12",
+ "count": 3
+ },
+ {
+ "day": "2015-02-13",
+ "count": 3
+ },
+ {
+ "day": "2015-02-14",
+ "count": 6
+ },
+ {
+ "day": "2015-02-15",
+ "count": 1
+ }
+ ],
+ "total": 20,
+ "average": 2,
+ "change": -28.571428571429
+ },
+ {
+ "days": [
+ {
+ "day": "2015-02-16",
+ "count": 4
+ },
+ {
+ "day": "2015-02-17",
+ "count": 4
+ },
+ {
+ "day": "2015-02-18",
+ "count": 2
+ },
+ {
+ "day": "2015-02-19",
+ "count": 1
+ },
+ {
+ "day": "2015-02-20",
+ "count": 3
+ },
+ {
+ "day": "2015-02-21",
+ "count": 0
+ },
+ {
+ "day": "2015-02-22",
+ "count": 1
+ }
+ ],
+ "total": 15,
+ "average": 2,
+ "change": -25
+ },
+ {
+ "days": [
+ {
+ "day": "2015-02-23",
+ "count": 2
+ },
+ {
+ "day": "2015-02-24",
+ "count": 4
+ },
+ {
+ "day": "2015-02-25",
+ "count": 2
+ },
+ {
+ "day": "2015-02-26",
+ "count": 6
+ },
+ {
+ "day": "2015-02-27",
+ "count": 2
+ },
+ {
+ "day": "2015-02-28",
+ "count": 1
+ },
+ {
+ "day": "2015-03-01",
+ "count": 3
+ }
+ ],
+ "total": 20,
+ "average": 2,
+ "change": 33.333333333333
+ },
+ {
+ "days": [
+ {
+ "day": "2015-03-02",
+ "count": 4
+ },
+ {
+ "day": "2015-03-03",
+ "count": 3
+ },
+ {
+ "day": "2015-03-04",
+ "count": 8
+ }
+ ],
+ "total": 15,
+ "average": 3,
+ "change": 22.5
+ }
+ ],
+ "fields": [
+ "period",
+ "views"
+ ],
+ "data": [
+ [
+ "2014-06-04",
+ 0
+ ],
+ [
+ "2014-06-05",
+ 6
+ ],
+ [
+ "2014-06-06",
+ 1
+ ],
+ [
+ "2014-06-07",
+ 0
+ ],
+ [
+ "2014-06-08",
+ 0
+ ],
+ [
+ "2014-06-09",
+ 0
+ ],
+ [
+ "2014-06-10",
+ 2
+ ],
+ [
+ "2014-06-11",
+ 1
+ ],
+ [
+ "2014-06-12",
+ 1
+ ],
+ [
+ "2014-06-13",
+ 0
+ ],
+ [
+ "2014-06-14",
+ 0
+ ],
+ [
+ "2014-06-15",
+ 1
+ ],
+ [
+ "2014-06-16",
+ 1
+ ],
+ [
+ "2014-06-17",
+ 2
+ ],
+ [
+ "2014-06-18",
+ 2
+ ],
+ [
+ "2014-06-19",
+ 2
+ ],
+ [
+ "2014-06-20",
+ 3
+ ],
+ [
+ "2014-06-21",
+ 2
+ ],
+ [
+ "2014-06-22",
+ 2
+ ],
+ [
+ "2014-06-23",
+ 3
+ ],
+ [
+ "2014-06-24",
+ 2
+ ],
+ [
+ "2014-06-25",
+ 4
+ ],
+ [
+ "2014-06-26",
+ 2
+ ],
+ [
+ "2014-06-27",
+ 2
+ ],
+ [
+ "2014-06-28",
+ 2
+ ],
+ [
+ "2014-06-29",
+ 1
+ ],
+ [
+ "2014-06-30",
+ 5
+ ],
+ [
+ "2014-07-01",
+ 7
+ ],
+ [
+ "2014-07-02",
+ 3
+ ],
+ [
+ "2014-07-03",
+ 2
+ ],
+ [
+ "2014-07-04",
+ 3
+ ],
+ [
+ "2014-07-05",
+ 2
+ ],
+ [
+ "2014-07-06",
+ 1
+ ],
+ [
+ "2014-07-07",
+ 9
+ ],
+ [
+ "2014-07-08",
+ 3
+ ],
+ [
+ "2014-07-09",
+ 1
+ ],
+ [
+ "2014-07-10",
+ 3
+ ],
+ [
+ "2014-07-11",
+ 4
+ ],
+ [
+ "2014-07-12",
+ 2
+ ],
+ [
+ "2014-07-13",
+ 1
+ ],
+ [
+ "2014-07-14",
+ 4
+ ],
+ [
+ "2014-07-15",
+ 4
+ ],
+ [
+ "2014-07-16",
+ 6
+ ],
+ [
+ "2014-07-17",
+ 5
+ ],
+ [
+ "2014-07-18",
+ 2
+ ],
+ [
+ "2014-07-19",
+ 2
+ ],
+ [
+ "2014-07-20",
+ 2
+ ],
+ [
+ "2014-07-21",
+ 2
+ ],
+ [
+ "2014-07-22",
+ 3
+ ],
+ [
+ "2014-07-23",
+ 3
+ ],
+ [
+ "2014-07-24",
+ 5
+ ],
+ [
+ "2014-07-25",
+ 4
+ ],
+ [
+ "2014-07-26",
+ 3
+ ],
+ [
+ "2014-07-27",
+ 9
+ ],
+ [
+ "2014-07-28",
+ 11
+ ],
+ [
+ "2014-07-29",
+ 10
+ ],
+ [
+ "2014-07-30",
+ 4
+ ],
+ [
+ "2014-07-31",
+ 2
+ ],
+ [
+ "2014-08-01",
+ 5
+ ],
+ [
+ "2014-08-02",
+ 0
+ ],
+ [
+ "2014-08-03",
+ 5
+ ],
+ [
+ "2014-08-04",
+ 11
+ ],
+ [
+ "2014-08-05",
+ 10
+ ],
+ [
+ "2014-08-06",
+ 7
+ ],
+ [
+ "2014-08-07",
+ 6
+ ],
+ [
+ "2014-08-08",
+ 5
+ ],
+ [
+ "2014-08-09",
+ 1
+ ],
+ [
+ "2014-08-10",
+ 8
+ ],
+ [
+ "2014-08-11",
+ 6
+ ],
+ [
+ "2014-08-12",
+ 9
+ ],
+ [
+ "2014-08-13",
+ 7
+ ],
+ [
+ "2014-08-14",
+ 6
+ ],
+ [
+ "2014-08-15",
+ 7
+ ],
+ [
+ "2014-08-16",
+ 2
+ ],
+ [
+ "2014-08-17",
+ 8
+ ],
+ [
+ "2014-08-18",
+ 5
+ ],
+ [
+ "2014-08-19",
+ 4
+ ],
+ [
+ "2014-08-20",
+ 9
+ ],
+ [
+ "2014-08-21",
+ 8
+ ],
+ [
+ "2014-08-22",
+ 9
+ ],
+ [
+ "2014-08-23",
+ 3
+ ],
+ [
+ "2014-08-24",
+ 3
+ ],
+ [
+ "2014-08-25",
+ 5
+ ],
+ [
+ "2014-08-26",
+ 4
+ ],
+ [
+ "2014-08-27",
+ 5
+ ],
+ [
+ "2014-08-28",
+ 4
+ ],
+ [
+ "2014-08-29",
+ 1
+ ],
+ [
+ "2014-08-30",
+ 1
+ ],
+ [
+ "2014-08-31",
+ 9
+ ],
+ [
+ "2014-09-01",
+ 2
+ ],
+ [
+ "2014-09-02",
+ 4
+ ],
+ [
+ "2014-09-03",
+ 10
+ ],
+ [
+ "2014-09-04",
+ 9
+ ],
+ [
+ "2014-09-05",
+ 6
+ ],
+ [
+ "2014-09-06",
+ 5
+ ],
+ [
+ "2014-09-07",
+ 4
+ ],
+ [
+ "2014-09-08",
+ 7
+ ],
+ [
+ "2014-09-09",
+ 8
+ ],
+ [
+ "2014-09-10",
+ 10
+ ],
+ [
+ "2014-09-11",
+ 4
+ ],
+ [
+ "2014-09-12",
+ 11
+ ],
+ [
+ "2014-09-13",
+ 5
+ ],
+ [
+ "2014-09-14",
+ 6
+ ],
+ [
+ "2014-09-15",
+ 13
+ ],
+ [
+ "2014-09-16",
+ 9
+ ],
+ [
+ "2014-09-17",
+ 3
+ ],
+ [
+ "2014-09-18",
+ 8
+ ],
+ [
+ "2014-09-19",
+ 8
+ ],
+ [
+ "2014-09-20",
+ 7
+ ],
+ [
+ "2014-09-21",
+ 13
+ ],
+ [
+ "2014-09-22",
+ 15
+ ],
+ [
+ "2014-09-23",
+ 8
+ ],
+ [
+ "2014-09-24",
+ 14
+ ],
+ [
+ "2014-09-25",
+ 4
+ ],
+ [
+ "2014-09-26",
+ 3
+ ],
+ [
+ "2014-09-27",
+ 2
+ ],
+ [
+ "2014-09-28",
+ 2
+ ],
+ [
+ "2014-09-29",
+ 15
+ ],
+ [
+ "2014-09-30",
+ 14
+ ],
+ [
+ "2014-10-01",
+ 7
+ ],
+ [
+ "2014-10-02",
+ 10
+ ],
+ [
+ "2014-10-03",
+ 4
+ ],
+ [
+ "2014-10-04",
+ 1
+ ],
+ [
+ "2014-10-05",
+ 9
+ ],
+ [
+ "2014-10-06",
+ 7
+ ],
+ [
+ "2014-10-07",
+ 17
+ ],
+ [
+ "2014-10-08",
+ 10
+ ],
+ [
+ "2014-10-09",
+ 8
+ ],
+ [
+ "2014-10-10",
+ 5
+ ],
+ [
+ "2014-10-11",
+ 5
+ ],
+ [
+ "2014-10-12",
+ 6
+ ],
+ [
+ "2014-10-13",
+ 12
+ ],
+ [
+ "2014-10-14",
+ 18
+ ],
+ [
+ "2014-10-15",
+ 11
+ ],
+ [
+ "2014-10-16",
+ 8
+ ],
+ [
+ "2014-10-17",
+ 14
+ ],
+ [
+ "2014-10-18",
+ 6
+ ],
+ [
+ "2014-10-19",
+ 5
+ ],
+ [
+ "2014-10-20",
+ 13
+ ],
+ [
+ "2014-10-21",
+ 11
+ ],
+ [
+ "2014-10-22",
+ 12
+ ],
+ [
+ "2014-10-23",
+ 6
+ ],
+ [
+ "2014-10-24",
+ 8
+ ],
+ [
+ "2014-10-25",
+ 6
+ ],
+ [
+ "2014-10-26",
+ 6
+ ],
+ [
+ "2014-10-27",
+ 8
+ ],
+ [
+ "2014-10-28",
+ 12
+ ],
+ [
+ "2014-10-29",
+ 10
+ ],
+ [
+ "2014-10-30",
+ 11
+ ],
+ [
+ "2014-10-31",
+ 12
+ ],
+ [
+ "2014-11-01",
+ 1
+ ],
+ [
+ "2014-11-02",
+ 3
+ ],
+ [
+ "2014-11-03",
+ 10
+ ],
+ [
+ "2014-11-04",
+ 3
+ ],
+ [
+ "2014-11-05",
+ 6
+ ],
+ [
+ "2014-11-06",
+ 4
+ ],
+ [
+ "2014-11-07",
+ 10
+ ],
+ [
+ "2014-11-08",
+ 7
+ ],
+ [
+ "2014-11-09",
+ 5
+ ],
+ [
+ "2014-11-10",
+ 7
+ ],
+ [
+ "2014-11-11",
+ 4
+ ],
+ [
+ "2014-11-12",
+ 6
+ ],
+ [
+ "2014-11-13",
+ 10
+ ],
+ [
+ "2014-11-14",
+ 4
+ ],
+ [
+ "2014-11-15",
+ 3
+ ],
+ [
+ "2014-11-16",
+ 3
+ ],
+ [
+ "2014-11-17",
+ 10
+ ],
+ [
+ "2014-11-18",
+ 4
+ ],
+ [
+ "2014-11-19",
+ 4
+ ],
+ [
+ "2014-11-20",
+ 4
+ ],
+ [
+ "2014-11-21",
+ 1
+ ],
+ [
+ "2014-11-22",
+ 3
+ ],
+ [
+ "2014-11-23",
+ 4
+ ],
+ [
+ "2014-11-24",
+ 10
+ ],
+ [
+ "2014-11-25",
+ 2
+ ],
+ [
+ "2014-11-26",
+ 2
+ ],
+ [
+ "2014-11-27",
+ 6
+ ],
+ [
+ "2014-11-28",
+ 4
+ ],
+ [
+ "2014-11-29",
+ 0
+ ],
+ [
+ "2014-11-30",
+ 1
+ ],
+ [
+ "2014-12-01",
+ 8
+ ],
+ [
+ "2014-12-02",
+ 2
+ ],
+ [
+ "2014-12-03",
+ 5
+ ],
+ [
+ "2014-12-04",
+ 3
+ ],
+ [
+ "2014-12-05",
+ 4
+ ],
+ [
+ "2014-12-06",
+ 0
+ ],
+ [
+ "2014-12-07",
+ 7
+ ],
+ [
+ "2014-12-08",
+ 6
+ ],
+ [
+ "2014-12-09",
+ 5
+ ],
+ [
+ "2014-12-10",
+ 5
+ ],
+ [
+ "2014-12-11",
+ 6
+ ],
+ [
+ "2014-12-12",
+ 2
+ ],
+ [
+ "2014-12-13",
+ 2
+ ],
+ [
+ "2014-12-14",
+ 5
+ ],
+ [
+ "2014-12-15",
+ 7
+ ],
+ [
+ "2014-12-16",
+ 2
+ ],
+ [
+ "2014-12-17",
+ 2
+ ],
+ [
+ "2014-12-18",
+ 1
+ ],
+ [
+ "2014-12-19",
+ 1
+ ],
+ [
+ "2014-12-20",
+ 6
+ ],
+ [
+ "2014-12-21",
+ 0
+ ],
+ [
+ "2014-12-22",
+ 3
+ ],
+ [
+ "2014-12-23",
+ 1
+ ],
+ [
+ "2014-12-24",
+ 1
+ ],
+ [
+ "2014-12-25",
+ 2
+ ],
+ [
+ "2014-12-26",
+ 3
+ ],
+ [
+ "2014-12-27",
+ 2
+ ],
+ [
+ "2014-12-28",
+ 4
+ ],
+ [
+ "2014-12-29",
+ 8
+ ],
+ [
+ "2014-12-30",
+ 2
+ ],
+ [
+ "2014-12-31",
+ 2
+ ],
+ [
+ "2015-01-01",
+ 3
+ ],
+ [
+ "2015-01-02",
+ 2
+ ],
+ [
+ "2015-01-03",
+ 4
+ ],
+ [
+ "2015-01-04",
+ 0
+ ],
+ [
+ "2015-01-05",
+ 9
+ ],
+ [
+ "2015-01-06",
+ 6
+ ],
+ [
+ "2015-01-07",
+ 3
+ ],
+ [
+ "2015-01-08",
+ 3
+ ],
+ [
+ "2015-01-09",
+ 3
+ ],
+ [
+ "2015-01-10",
+ 1
+ ],
+ [
+ "2015-01-11",
+ 7
+ ],
+ [
+ "2015-01-12",
+ 8
+ ],
+ [
+ "2015-01-13",
+ 9
+ ],
+ [
+ "2015-01-14",
+ 5
+ ],
+ [
+ "2015-01-15",
+ 3
+ ],
+ [
+ "2015-01-16",
+ 2
+ ],
+ [
+ "2015-01-17",
+ 3
+ ],
+ [
+ "2015-01-18",
+ 3
+ ],
+ [
+ "2015-01-19",
+ 3
+ ],
+ [
+ "2015-01-20",
+ 9
+ ],
+ [
+ "2015-01-21",
+ 4
+ ],
+ [
+ "2015-01-22",
+ 7
+ ],
+ [
+ "2015-01-23",
+ 3
+ ],
+ [
+ "2015-01-24",
+ 2
+ ],
+ [
+ "2015-01-25",
+ 1
+ ],
+ [
+ "2015-01-26",
+ 6
+ ],
+ [
+ "2015-01-27",
+ 3
+ ],
+ [
+ "2015-01-28",
+ 6
+ ],
+ [
+ "2015-01-29",
+ 2
+ ],
+ [
+ "2015-01-30",
+ 4
+ ],
+ [
+ "2015-01-31",
+ 1
+ ],
+ [
+ "2015-02-01",
+ 3
+ ],
+ [
+ "2015-02-02",
+ 8
+ ],
+ [
+ "2015-02-03",
+ 6
+ ],
+ [
+ "2015-02-04",
+ 2
+ ],
+ [
+ "2015-02-05",
+ 5
+ ],
+ [
+ "2015-02-06",
+ 2
+ ],
+ [
+ "2015-02-07",
+ 2
+ ],
+ [
+ "2015-02-08",
+ 3
+ ],
+ [
+ "2015-02-09",
+ 0
+ ],
+ [
+ "2015-02-10",
+ 3
+ ],
+ [
+ "2015-02-11",
+ 4
+ ],
+ [
+ "2015-02-12",
+ 3
+ ],
+ [
+ "2015-02-13",
+ 3
+ ],
+ [
+ "2015-02-14",
+ 6
+ ],
+ [
+ "2015-02-15",
+ 1
+ ],
+ [
+ "2015-02-16",
+ 4
+ ],
+ [
+ "2015-02-17",
+ 4
+ ],
+ [
+ "2015-02-18",
+ 2
+ ],
+ [
+ "2015-02-19",
+ 1
+ ],
+ [
+ "2015-02-20",
+ 3
+ ],
+ [
+ "2015-02-21",
+ 0
+ ],
+ [
+ "2015-02-22",
+ 1
+ ],
+ [
+ "2015-02-23",
+ 2
+ ],
+ [
+ "2015-02-24",
+ 4
+ ],
+ [
+ "2015-02-25",
+ 2
+ ],
+ [
+ "2015-02-26",
+ 6
+ ],
+ [
+ "2015-02-27",
+ 2
+ ],
+ [
+ "2015-02-28",
+ 1
+ ],
+ [
+ "2015-03-01",
+ 3
+ ],
+ [
+ "2015-03-02",
+ 4
+ ],
+ [
+ "2015-03-03",
+ 3
+ ],
+ [
+ "2015-03-04",
+ 8
+ ]
+ ],
+ "highest_month": 278,
+ "highest_day_average": 8,
+ "highest_week_average": 8,
+ "post": {
+ "ID": 725,
+ "post_author": "67137",
+ "post_date": "2014-06-05 10:23:24",
+ "post_date_gmt": "2014-06-05 15:23:24",
+ "post_content": "On occasion my Calendar on Mavericks gets hosed when syncing with Google.  If I look in the console, I see errors like the following mentioning \"An error exists on principal\":\r\n<pre>6/5/14 10:05:00.337 AM Calendar[59555]: [com.apple.calendar.ui.log] [An error exists on principal: [iCloud]]\r\n6/5/14 10:05:00.338 AM Calendar[59555]: [com.apple.calendar.ui.log] [An error exists on principal: [Time Off]]\r\n6/5/14 10:05:00.340 AM Calendar[59555]: [com.apple.calendar.ui.log] [An error exists on principal: [Events]]\r\n6/5/14 10:05:00.341 AM Calendar[59555]: [com.apple.calendar.ui.log] [An error exists on principal: [Launch]]\r\n6/5/14 10:05:00.341 AM Calendar[59555]: [com.apple.calendar.ui.log] [An error exists on principal: [Conferences]]\r\n6/5/14 10:05:00.342 AM Calendar[59555]: [com.apple.calendar.ui.log] [An error exists on principal: [Some Team]]</pre>\r\nHitting Command-R in calendar results in it sitting on Updating for some time (minutes?) and then an exclamation point appearing next to the calendars in question.  I finally found a solution to fix it until the next time it happens.  I'm not sure what the actual cause is but this can get you back up &amp; running.\r\n<ol>\r\n\t<li>Close the Calendar app.</li>\r\n\t<li>Go into System Preferences.</li>\r\n\t<li>Click on Internet Accounts.</li>\r\n\t<li>Click on the Google account (if you have multiple you may have to do this for each).</li>\r\n\t<li>Uncheck \"Calendars\".</li>\r\n\t<li>Click Show All or close preferences.</li>\r\n\t<li>Open Terminal.app (Applications &gt; Utilities)</li>\r\n\t<li>type: killall -9 CalendarAgent</li>\r\n\t<li>type exit or close the window.</li>\r\n\t<li>Go back into System Preferences and turn Calendar back on for the Google Account(s).</li>\r\n\t<li>Start up Calendar and hit command-R - everything should refresh properly.</li>\r\n</ol>\r\nLet me know if you have questions!",
+ "post_title": "Mac OS X 10.9 Mavericks Calendar + Google Sync Problems",
+ "post_excerpt": "",
+ "post_status": "publish",
+ "comment_status": "open",
+ "ping_status": "open",
+ "post_password": "",
+ "post_name": "mac-os-x-10-9-mavericks-calendar-google-sync-problems",
+ "to_ping": "",
+ "pinged": "",
+ "post_modified": "2014-06-05 10:23:24",
+ "post_modified_gmt": "2014-06-05 15:23:24",
+ "post_content_filtered": "",
+ "post_parent": 0,
+ "guid": "http://astralbodi.es/?p=725",
+ "menu_order": 0,
+ "post_type": "post",
+ "post_mime_type": "",
+ "comment_count": "4",
+ "filter": "raw",
+ "permalink": "http://astralbodi.es/2014/06/05/mac-os-x-10-9-mavericks-calendar-google-sync-problems/"
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-referrers.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-referrers.json
new file mode 100644
index 000000000..d2dddf2de
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-referrers.json
@@ -0,0 +1,553 @@
+{
+ "date": "2014-11-06",
+ "days": {
+ "2014-11-06": {
+ "groups": [
+ {
+ "group": "Search Engines",
+ "name": "Search Engines",
+ "icon": "https://wordpress.com/i/stats/search-engine.png",
+ "total": 480,
+ "follow_data": null,
+ "results": [
+ {
+ "name": "Google Search",
+ "icon": "https://secure.gravatar.com/blavatar/6741a05f4bc6e5b65f504c4f3df388a1?s=48",
+ "views": 461,
+ "children": [
+ {
+ "name": "google.com",
+ "url": "http://www.google.com/",
+ "icon": "https://secure.gravatar.com/blavatar/ff90821feeb2b02a33a6f9fc8e5f3fcd?s=48",
+ "views": 176
+ },
+ {
+ "name": "google.com/search",
+ "url": "http://www.google.com/search",
+ "icon": null,
+ "views": 72
+ },
+ {
+ "name": "google.co.uk",
+ "url": "http://www.google.co.uk",
+ "icon": "https://secure.gravatar.com/blavatar/d5d4cf8ec8dc8fddc90b7024afa3ddb3?s=48",
+ "views": 29
+ },
+ {
+ "name": "google.co.in",
+ "url": "http://www.google.co.in",
+ "icon": "https://secure.gravatar.com/blavatar/b8b1615bdc37f756888332cc17e0a5bf?s=48",
+ "views": 18
+ },
+ {
+ "name": "google.ca",
+ "url": "http://www.google.ca",
+ "icon": "https://secure.gravatar.com/blavatar/3eac48a51cb5302e35fe68a819220647?s=48",
+ "views": 16
+ },
+ {
+ "name": "google.co.in/search",
+ "url": "http://www.google.co.in/search",
+ "icon": null,
+ "views": 15
+ },
+ {
+ "name": "google.fr",
+ "url": "http://www.google.fr",
+ "icon": "https://secure.gravatar.com/blavatar/f50e99a317404504fe68699742bd5814?s=48",
+ "views": 10
+ },
+ {
+ "name": "google.co.uk/search",
+ "url": "http://www.google.co.uk/search",
+ "icon": null,
+ "views": 10
+ },
+ {
+ "name": "google.es",
+ "url": "http://www.google.es",
+ "icon": "https://secure.gravatar.com/blavatar/7ec3dec14a88a3b50bab9a2b2d8e9e83?s=48",
+ "views": 10
+ },
+ {
+ "name": "google.it",
+ "url": "http://www.google.it",
+ "icon": "https://secure.gravatar.com/blavatar/dda15434615ed3debc02fef8bbea9236?s=48",
+ "views": 8
+ },
+ {
+ "name": "google.co.jp",
+ "url": "http://www.google.co.jp",
+ "icon": "https://secure.gravatar.com/blavatar/a28b8206a6562f6098688508d4665905?s=48",
+ "views": 6
+ }
+ ]
+ },
+ {
+ "name": "Bing",
+ "icon": "https://secure.gravatar.com/blavatar/112a7e096595d1c32c4ecdfd9e56b66c?s=48",
+ "views": 6,
+ "children": [
+ {
+ "name": "bing.com",
+ "url": "http://www.bing.com",
+ "icon": "https://secure.gravatar.com/blavatar/9cbc5ee4b61e0acb335d56e96c6b2586?s=48",
+ "views": 5
+ },
+ {
+ "name": "bing.com/cr?h=de8g1Y2DWzfXUvtXcC2IcDZyxDoXB_nRU1LMdIYgbeE&v=1&r=http%3A%2F%2Fautomattic.com%2F&IG=db7e274448164b20819b47ff4e6c69cf&CID=1658D39F37DA68C1226DD6A133DA6A0D&p=SERP%2C5133.1",
+ "url": "http://www.bing.com/cr?h=de8g1Y2DWzfXUvtXcC2IcDZyxDoXB_nRU1LMdIYgbeE&v=1&r=http%3A%2F%2Fautomattic.com%2F&IG=db7e274448164b20819b47ff4e6c69cf&CID=1658D39F37DA68C1226DD6A133DA6A0D&p=SERP%2C5133.1",
+ "icon": null,
+ "views": 1
+ }
+ ]
+ },
+ {
+ "name": "Google Mobile",
+ "icon": "https://secure.gravatar.com/blavatar/6c3cff486df5f19e5aacc916f4bab366?s=48",
+ "views": 4,
+ "children": [
+ {
+ "name": "google.com/m",
+ "url": "http://www.google.com/m",
+ "icon": null,
+ "views": 3
+ },
+ {
+ "name": "google.co.in/m",
+ "url": "http://www.google.co.in/m",
+ "icon": null,
+ "views": 1
+ }
+ ]
+ },
+ {
+ "name": "Yahoo Search",
+ "icon": "https://secure.gravatar.com/blavatar/5029a4a8e7da221ae517ddaa0dd5422b?s=48",
+ "views": 3,
+ "children": [
+ {
+ "name": "r.search.yahoo.com/_ylt=A0SO8znbiFtUDxEAQ6bBGOd_;_ylu=X3oDMTE0aWk0c25qBHNlYwNzcgRwb3MDOQRjb2xvA2dxMQR2dGlkA1RBVVMwNTBfMQ--/RV=2/RE=1415313756/RO=10/RU=http%3a%2f%2fautomattic.com%2fdmca-notice%2f/RK=0/RS=FPBw2GWPvFyNODDsl2y.a9QKVGQ-",
+ "url": "http://r.search.yahoo.com/_ylt=A0SO8znbiFtUDxEAQ6bBGOd_;_ylu=X3oDMTE0aWk0c25qBHNlYwNzcgRwb3MDOQRjb2xvA2dxMQR2dGlkA1RBVVMwNTBfMQ--/RV=2/RE=1415313756/RO=10/RU=http%3a%2f%2fautomattic.com%2fdmca-notice%2f/RK=0/RS=FPBw2GWPvFyNODDsl2y.a9QKVGQ-",
+ "icon": null,
+ "views": 2
+ },
+ {
+ "name": "search.yahoo.com",
+ "url": "http://search.yahoo.com",
+ "icon": "https://secure.gravatar.com/blavatar/3fb3c06e746f9d563d7a3ea387b7c74f?s=48",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "name": "Baidu",
+ "icon": null,
+ "views": 2,
+ "children": [
+ {
+ "name": "m.baidu.com/from=282e/bd_page_type=1/ssid=0/uid=0/pu=sz%401320_480%2Cosname%40android%2Cta%40zbios_2_2.3_3_533/baiduid=DA869511F4FA2796F09AB1106814EC23/w=0_10_Newest.Free.Japanese.Tee.porn.videos%EF%BC%BFpaye7/t=zbios/l=3/tc?lid=11481103778344937834&order=7&vit=osres&tj=www_normal_7_0_10&m=8&srd=1&rr=2&cltj=cloud_title&phoneos=bd_search_android&dict=20&fm=wnor&sec=42727&di=6375fb7ed63d6728&bdenc=1&nsrc=IlPT2AEptyoA_yixCFOxXnANedT62v3IIxjPMjxZBDeanFvte4viZQRAWzvgNXyXEUH5umOQqBW",
+ "url": "http://m.baidu.com/from=282e/bd_page_type=1/ssid=0/uid=0/pu=sz%401320_480%2Cosname%40android%2Cta%40zbios_2_2.3_3_533/baiduid=DA869511F4FA2796F09AB1106814EC23/w=0_10_Newest.Free.Japanese.Tee.porn.videos%EF%BC%BFpaye7/t=zbios/l=3/tc?lid=11481103778344937834&order=7&vit=osres&tj=www_normal_7_0_10&m=8&srd=1&rr=2&cltj=cloud_title&phoneos=bd_search_android&dict=20&fm=wnor&sec=42727&di=6375fb7ed63d6728&bdenc=1&nsrc=IlPT2AEptyoA_yixCFOxXnANedT62v3IIxjPMjxZBDeanFvte4viZQRAWzvgNXyXEUH5umOQqBW",
+ "icon": null,
+ "views": 1
+ },
+ {
+ "name": "m5.baidu.com/from=1089a/bd_page_type=1/ssid=0/uid=coolpad7060/pu=usm%400%2Csz%401320_1001%2Cta%40iphone_2_4.1_3_534/baiduid=B73C28336A671201339ECA6A9806BC5F/w=0_10_xnxx.c/t=iphone/l=3/tc?lid=4839328262923482007&order=1&vit=osres&tj=www_normal_1_0_10&m=8&srd=1&cltj=cloud_title&dict=20&fm=wnor&sec=42729&di=f459593862ba8040&bdenc=1&nsrc=IlPT2AEptyoA_yixCFOxXnANedT62v3IIxjPMjxZBDeanFvte4viZQRAWCLaOWq",
+ "url": "http://m5.baidu.com/from=1089a/bd_page_type=1/ssid=0/uid=coolpad7060/pu=usm%400%2Csz%401320_1001%2Cta%40iphone_2_4.1_3_534/baiduid=B73C28336A671201339ECA6A9806BC5F/w=0_10_xnxx.c/t=iphone/l=3/tc?lid=4839328262923482007&order=1&vit=osres&tj=www_normal_1_0_10&m=8&srd=1&cltj=cloud_title&dict=20&fm=wnor&sec=42729&di=f459593862ba8040&bdenc=1&nsrc=IlPT2AEptyoA_yixCFOxXnANedT62v3IIxjPMjxZBDeanFvte4viZQRAWCLaOWq",
+ "icon": null,
+ "views": 1
+ }
+ ]
+ },
+ {
+ "name": "Yandex",
+ "url": "http://yandex.ru",
+ "icon": null,
+ "views": 2
+ },
+ {
+ "name": "duckduckgo.com",
+ "url": "https://duckduckgo.com/",
+ "icon": null,
+ "views": 2
+ }
+ ]
+ },
+ {
+ "group": "WordPress.com Reader",
+ "name": "WordPress.com Reader",
+ "url": "https://wordpress.com/read/",
+ "icon": "https://secure.gravatar.com/blavatar/236c008da9dc0edb4b3464ecebb3fc1d?s=48",
+ "total": 138,
+ "follow_data": null,
+ "results": {
+ "views": 138
+ }
+ },
+ {
+ "group": "en.wordpress.com",
+ "name": "en.wordpress.com",
+ "icon": "https://secure.gravatar.com/blavatar/d6777146f78461ee95d5aed3b81fb852?s=48",
+ "total": 130,
+ "follow_data": null,
+ "results": [
+ {
+ "name": "en.wordpress.com/tos/",
+ "url": "http://en.wordpress.com/tos/",
+ "views": 41
+ },
+ {
+ "name": "en.wordpress.com/about/",
+ "url": "http://en.wordpress.com/about/",
+ "views": 19
+ },
+ {
+ "name": "en.wordpress.com/complaints/",
+ "url": "http://en.wordpress.com/complaints/",
+ "views": 4
+ },
+ {
+ "name": "en.wordpress.com/features/",
+ "url": "http://en.wordpress.com/features/",
+ "views": 4
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=jadesexyx",
+ "url": "http://en.wordpress.com/typo/?subdomain=jadesexyx",
+ "views": 4
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=giustizieredellarete",
+ "url": "http://en.wordpress.com/typo/?subdomain=giustizieredellarete",
+ "views": 3
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=abchudasama",
+ "url": "http://en.wordpress.com/typo/?subdomain=abchudasama",
+ "views": 2
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=mp3",
+ "url": "http://en.wordpress.com/typo/?subdomain=mp3",
+ "views": 2
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=kried202",
+ "url": "http://en.wordpress.com/typo/?subdomain=kried202",
+ "views": 2
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=madameghamrauoi",
+ "url": "http://en.wordpress.com/typo/?subdomain=madameghamrauoi",
+ "views": 2
+ },
+ {
+ "name": "en.wordpress.com/typo/?subdomain=nephitestalentcenteq",
+ "url": "http://en.wordpress.com/typo/?subdomain=nephitestalentcenteq",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "group": "WordPress Dashboard",
+ "name": "WordPress Dashboard",
+ "url": "https://wordpress.com/",
+ "icon": "https://secure.gravatar.com/blavatar/4cdb265b8260a6a032a1ed197e39b92d?s=48",
+ "total": 129,
+ "follow_data": null,
+ "results": {
+ "views": 129
+ }
+ },
+ {
+ "group": "en.gravatar.com",
+ "name": "en.gravatar.com",
+ "icon": "https://secure.gravatar.com/blavatar/90f2527e399855d3bc583b65f35821e7?s=48",
+ "total": 89,
+ "follow_data": null,
+ "results": [
+ {
+ "name": "en.gravatar.com",
+ "url": "http://en.gravatar.com/",
+ "views": 27
+ },
+ {
+ "name": "en.gravatar.com/support/what-is-gravatar/",
+ "url": "http://en.gravatar.com/support/what-is-gravatar/",
+ "views": 26
+ },
+ {
+ "name": "en.gravatar.com/profiles/no-such-user",
+ "url": "http://en.gravatar.com/profiles/no-such-user",
+ "views": 3
+ },
+ {
+ "name": "en.gravatar.com/depayoyevu",
+ "url": "http://en.gravatar.com/depayoyevu",
+ "views": 2
+ },
+ {
+ "name": "en.gravatar.com/mzellepika",
+ "url": "http://en.gravatar.com/mzellepika",
+ "views": 1
+ },
+ {
+ "name": "en.gravatar.com/bewofawegu",
+ "url": "http://en.gravatar.com/bewofawegu",
+ "views": 1
+ },
+ {
+ "name": "en.gravatar.com/adynamaryan",
+ "url": "http://en.gravatar.com/adynamaryan",
+ "views": 1
+ },
+ {
+ "name": "en.gravatar.com/arminabrahamyan",
+ "url": "http://en.gravatar.com/arminabrahamyan",
+ "views": 1
+ },
+ {
+ "name": "en.gravatar.com/jeqyeqema",
+ "url": "http://en.gravatar.com/jeqyeqema",
+ "views": 1
+ },
+ {
+ "name": "en.gravatar.com/momsextubea",
+ "url": "http://en.gravatar.com/momsextubea",
+ "views": 1
+ },
+ {
+ "name": "en.gravatar.com/xanthuis",
+ "url": "http://en.gravatar.com/xanthuis",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "group": "vip.wordpress.com",
+ "name": "vip.wordpress.com",
+ "icon": "https://secure.gravatar.com/blavatar/c0a70310ea07fb03e415f74916b37b35?s=48",
+ "total": 66,
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_referrer",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "vip.wordpress.com",
+ "blog_url": "http://vip.wordpress.com",
+ "blog_id": 2235322,
+ "site_id": 2235322,
+ "blog_title": "WordPress.com VIP",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "results": [
+ {
+ "name": "vip.wordpress.com",
+ "url": "http://vip.wordpress.com/",
+ "views": 38
+ },
+ {
+ "name": "vip.wordpress.com/our-services/",
+ "url": "http://vip.wordpress.com/our-services/",
+ "views": 7
+ },
+ {
+ "name": "vip.wordpress.com/contact/",
+ "url": "http://vip.wordpress.com/contact/",
+ "views": 6
+ },
+ {
+ "name": "vip.wordpress.com/2014/11/06/code-for-the-people/",
+ "url": "http://vip.wordpress.com/2014/11/06/code-for-the-people/",
+ "views": 3
+ },
+ {
+ "name": "vip.wordpress.com/partners/",
+ "url": "http://vip.wordpress.com/partners/",
+ "views": 3
+ },
+ {
+ "name": "vip.wordpress.com/spotlight/",
+ "url": "http://vip.wordpress.com/spotlight/",
+ "views": 2
+ },
+ {
+ "name": "vip.wordpress.com/why-vip/",
+ "url": "http://vip.wordpress.com/why-vip/",
+ "views": 1
+ },
+ {
+ "name": "vip.wordpress.com/clients/",
+ "url": "http://vip.wordpress.com/clients/",
+ "views": 1
+ },
+ {
+ "name": "vip.wordpress.com/2014/09/03/big-media-enterprise-talks-wordpresstv/",
+ "url": "http://vip.wordpress.com/2014/09/03/big-media-enterprise-talks-wordpresstv/",
+ "views": 1
+ },
+ {
+ "name": "vip.wordpress.com/stats/",
+ "url": "http://vip.wordpress.com/stats/",
+ "views": 1
+ },
+ {
+ "name": "vip.wordpress.com/events/",
+ "url": "http://vip.wordpress.com/events/",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "group": "ma.tt",
+ "name": "ma.tt",
+ "icon": "https://secure.gravatar.com/blavatar/733a27a6b983dd89d6dd64d0445a3e8e?s=48",
+ "total": 56,
+ "follow_data": null,
+ "results": [
+ {
+ "name": "ma.tt",
+ "url": "http://ma.tt/",
+ "views": 34
+ },
+ {
+ "name": "ma.tt/themes/",
+ "url": "http://ma.tt/themes/",
+ "views": 5
+ },
+ {
+ "name": "ma.tt/about/",
+ "url": "http://ma.tt/about/",
+ "views": 5
+ },
+ {
+ "name": "ma.tt/2009/07/not-lonely-at-all/?relatedposts_hit=1&relatedposts_origin=35223&relatedposts_position=1",
+ "url": "http://ma.tt/2009/07/not-lonely-at-all/?relatedposts_hit=1&relatedposts_origin=35223&relatedposts_position=1",
+ "views": 2
+ },
+ {
+ "name": "ma.tt/gallery/",
+ "url": "http://ma.tt/gallery/",
+ "views": 1
+ },
+ {
+ "name": "ma.tt/contact/",
+ "url": "http://ma.tt/contact/",
+ "views": 1
+ },
+ {
+ "name": "ma.tt/page/2/",
+ "url": "http://ma.tt/page/2/",
+ "views": 1
+ },
+ {
+ "name": "ma.tt/page/7/",
+ "url": "http://ma.tt/page/7/",
+ "views": 1
+ },
+ {
+ "name": "ma.tt/page/6/",
+ "url": "http://ma.tt/page/6/",
+ "views": 1
+ },
+ {
+ "name": "ma.tt/2014/09/five-for-the-future/",
+ "url": "http://ma.tt/2014/09/five-for-the-future/",
+ "views": 1
+ },
+ {
+ "name": "ma.tt/2014/11/texas-landslide/",
+ "url": "http://ma.tt/2014/11/texas-landslide/",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "group": "flexjobs.com",
+ "name": "flexjobs.com",
+ "icon": null,
+ "total": 55,
+ "follow_data": null,
+ "results": [
+ {
+ "name": "flexjobs.com/blog/post/25-virtual-companies-that-thrive-on-remote-work/",
+ "url": "http://www.flexjobs.com/blog/post/25-virtual-companies-that-thrive-on-remote-work/",
+ "views": 52
+ },
+ {
+ "name": "flexjobs.com/HostedJob.aspx?id=132900",
+ "url": "http://www.flexjobs.com/HostedJob.aspx?id=132900",
+ "views": 1
+ },
+ {
+ "name": "flexjobs.com/HostedJob.aspx?id=132898",
+ "url": "http://www.flexjobs.com/HostedJob.aspx?id=132898",
+ "views": 1
+ },
+ {
+ "name": "flexjobs.com/HostedJob.aspx?id=87787",
+ "url": "http://www.flexjobs.com/HostedJob.aspx?id=87787",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "group": "wordpress.tv",
+ "name": "wordpress.tv",
+ "icon": null,
+ "total": 46,
+ "follow_data": null,
+ "results": [
+ {
+ "name": "wordpress.tv",
+ "url": "http://wordpress.tv/",
+ "views": 39
+ },
+ {
+ "name": "wordpress.tv/2014/10/26/matt-mullenweg-state-of-the-word-2014-qa/",
+ "url": "http://wordpress.tv/2014/10/26/matt-mullenweg-state-of-the-word-2014-qa/",
+ "views": 3
+ },
+ {
+ "name": "wordpress.tv/2014/11/05/luke-wroblewski-from-the-front-lines-of-multi-device-web-design/",
+ "url": "http://wordpress.tv/2014/11/05/luke-wroblewski-from-the-front-lines-of-multi-device-web-design/",
+ "views": 1
+ },
+ {
+ "name": "wordpress.tv/2014/11/05/luca-sartoni-growth-tips-for-any-wordpress-site/",
+ "url": "http://wordpress.tv/2014/11/05/luca-sartoni-growth-tips-for-any-wordpress-site/",
+ "views": 1
+ },
+ {
+ "name": "wordpress.tv/2014/11/05/petya-raykovska-translators/",
+ "url": "http://wordpress.tv/2014/11/05/petya-raykovska-translators/",
+ "views": 1
+ },
+ {
+ "name": "wordpress.tv/2014/09/24/jonathan-daggerhart-introduction-to-wordpress-plugin-development/",
+ "url": "http://wordpress.tv/2014/09/24/jonathan-daggerhart-introduction-to-wordpress-plugin-development/",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "group": "twitter.com",
+ "name": "Twitter",
+ "url": "http://twitter.com/",
+ "icon": "https://secure.gravatar.com/blavatar/7905d1c4e12c54933a44d19fcd5f9356?s=48",
+ "total": 34,
+ "follow_data": null,
+ "results": {
+ "views": 34
+ }
+ }
+ ],
+ "other_views": 938,
+ "total_views": 2161
+ }
+ },
+ "period": "day"
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-summary.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-summary.json
new file mode 100644
index 000000000..35198db0e
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-summary.json
@@ -0,0 +1 @@
+{"date":"2014-10-28","period":"day","views":56,"visitors":44,"likes":1,"reblogs":2,"comments":3,"followers":56}
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-tags.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-tags.json
new file mode 100644
index 000000000..b73186923
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-tags.json
@@ -0,0 +1,125 @@
+{
+ "date": "2014-12-16",
+ "tags": [
+ {
+ "tags": [
+ {
+ "type": "category",
+ "name": "Uncategorized",
+ "link": "http://astralbodi.es/category/uncategorized/"
+ }
+ ],
+ "views": 461
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "apple",
+ "link": "http://astralbodi.es/tag/apple/"
+ }
+ ],
+ "views": 151
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "ios",
+ "link": "http://astralbodi.es/tag/ios/"
+ }
+ ],
+ "views": 142
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "howto",
+ "link": "http://astralbodi.es/tag/howto/"
+ }
+ ],
+ "views": 126
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "macintosh",
+ "link": "http://astralbodi.es/tag/macintosh/"
+ }
+ ],
+ "views": 88
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "iphone",
+ "link": "http://astralbodi.es/tag/iphone/"
+ }
+ ],
+ "views": 80
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "core data",
+ "link": "http://astralbodi.es/tag/core-data/"
+ }
+ ],
+ "views": 63
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "xcode",
+ "link": "http://astralbodi.es/tag/xcode/"
+ }
+ ],
+ "views": 54
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "unit test",
+ "link": "http://astralbodi.es/tag/unit-test/"
+ },
+ {
+ "type": "tag",
+ "name": "XCTest",
+ "link": "http://astralbodi.es/tag/xctest/"
+ },
+ {
+ "type": "tag",
+ "name": "asynchronous",
+ "link": "http://astralbodi.es/tag/asynchronous/"
+ },
+ {
+ "type": "tag",
+ "name": "testing",
+ "link": "http://astralbodi.es/tag/testing/"
+ }
+ ],
+ "views": 43
+ },
+ {
+ "tags": [
+ {
+ "type": "tag",
+ "name": "networking",
+ "link": "http://astralbodi.es/tag/networking/"
+ },
+ {
+ "type": "tag",
+ "name": "unix",
+ "link": "http://astralbodi.es/tag/unix/"
+ }
+ ],
+ "views": 41
+ }
+ ]
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-top-posts.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-top-posts.json
new file mode 100644
index 000000000..81753bbbb
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-top-posts.json
@@ -0,0 +1,102 @@
+{
+ "date": "2014-11-06",
+ "days": {
+ "2014-11-06": {
+ "postviews": [
+ {
+ "id": 39806,
+ "href": "http://automattic.com/home/",
+ "date": "2011-08-30 21:47:38",
+ "title": "Home",
+ "views": 2420,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 12,
+ "href": "http://automattic.com/work-with-us/",
+ "date": "2005-12-20 08:18:50",
+ "title": "Work With Us",
+ "views": 724,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 13,
+ "href": "http://automattic.com/privacy/",
+ "date": "2006-03-24 20:26:35",
+ "title": "Privacy Policy",
+ "views": 683,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 60,
+ "href": "http://automattic.com/about/",
+ "date": "2005-07-23 17:27:23",
+ "title": "About Us",
+ "views": 350,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 7,
+ "href": "http://automattic.com/contact/",
+ "date": "2005-07-23 17:34:12",
+ "title": "Contact Us",
+ "views": 139,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 39251,
+ "href": "http://automattic.com/work-with-us/happiness-engineer/",
+ "date": "2011-08-25 19:35:46",
+ "title": "Happiness Engineer",
+ "views": 136,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 10,
+ "href": "http://automattic.com/news/",
+ "date": "2005-12-19 15:53:51",
+ "title": "News",
+ "views": 115,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 5882,
+ "href": "http://automattic.com/wordpress-plugins/",
+ "date": "2010-06-11 19:28:11",
+ "title": "WordPress Plugins",
+ "views": 114,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 118218,
+ "href": "http://automattic.com/dmca-notice/",
+ "date": "2013-05-02 00:14:00",
+ "title": "Digital Millennium Copyright Act (DMCA) Notice",
+ "views": 109,
+ "type": "page",
+ "video_play": false
+ },
+ {
+ "id": 39254,
+ "href": "http://automattic.com/work-with-us/growth-explorer/",
+ "date": "2011-08-25 19:37:27",
+ "title": "Growth Explorer",
+ "views": 56,
+ "type": "page",
+ "video_play": false
+ }
+ ],
+ "other_views": 535,
+ "total_views": 5381
+ }
+ },
+ "period": "day"
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-video-plays.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-video-plays.json
new file mode 100644
index 000000000..fba3173e1
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-video-plays.json
@@ -0,0 +1,11 @@
+{
+ "date": "2014-12-16",
+ "period": "day",
+ "days": {
+ "2014-12-16": {
+ "plays": [],
+ "other_plays": 0,
+ "total_plays": 0
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-visits.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-visits.json
new file mode 100644
index 000000000..0ae0d9cb3
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats-visits.json
@@ -0,0 +1,254 @@
+{
+ "date": "2014-11-06",
+ "unit": "day",
+ "fields": [
+ "period",
+ "views",
+ "visitors",
+ "likes",
+ "reblogs",
+ "comments"
+ ],
+ "data": [
+ [
+ "2014-10-08",
+ 7808,
+ 4331,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-09",
+ 7657,
+ 4258,
+ 4,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-10",
+ 6772,
+ 3912,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-11",
+ 5795,
+ 3407,
+ 3,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-12",
+ 5925,
+ 3497,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-13",
+ 9907,
+ 4321,
+ 3,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-14",
+ 10396,
+ 4328,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-15",
+ 8183,
+ 4374,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-16",
+ 8362,
+ 4723,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-17",
+ 7103,
+ 4078,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-18",
+ 5287,
+ 3210,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-19",
+ 5063,
+ 3047,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-20",
+ 6698,
+ 3863,
+ 3,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-21",
+ 7654,
+ 4353,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-22",
+ 6710,
+ 3949,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-23",
+ 7039,
+ 3927,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-24",
+ 6079,
+ 3475,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-25",
+ 5067,
+ 2966,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-26",
+ 6543,
+ 4045,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-27",
+ 8708,
+ 5301,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-28",
+ 8645,
+ 5407,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-29",
+ 8425,
+ 5139,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-30",
+ 8262,
+ 5078,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-10-31",
+ 7257,
+ 4549,
+ 2,
+ 0,
+ 0
+ ],
+ [
+ "2014-11-01",
+ 6703,
+ 4198,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-11-02",
+ 6982,
+ 4439,
+ 3,
+ 0,
+ 0
+ ],
+ [
+ "2014-11-03",
+ 7140,
+ 4047,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-11-04",
+ 6610,
+ 3692,
+ 3,
+ 0,
+ 0
+ ],
+ [
+ "2014-11-05",
+ 6411,
+ 3591,
+ 1,
+ 0,
+ 0
+ ],
+ [
+ "2014-11-06",
+ 5331,
+ 2948,
+ 1,
+ 0,
+ 0
+ ]
+ ]
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats.json
new file mode 100644
index 000000000..98ab89915
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-123456-stats.json
@@ -0,0 +1,187 @@
+{
+ "date": "2015-05-14",
+ "stats": {
+ "visitors_today": 33,
+ "visitors_yesterday": 48,
+ "visitors": 42893,
+ "views_today": 34,
+ "views_yesterday": 64,
+ "views_best_day": "2015-04-04",
+ "views_best_day_total": 3485,
+ "views": 56687,
+ "comments": 205,
+ "posts": 128,
+ "followers_blog": 183,
+ "followers_comments": 12,
+ "comments_per_month": 5,
+ "comments_most_active_recent_day": "2013-05-29 19:47:00",
+ "comments_most_active_time": "16:00",
+ "comments_spam": 153759,
+ "categories": 2,
+ "tags": 176,
+ "shares": 65,
+ "shares_facebook": 43,
+ "shares_twitter": 22,
+ "shares_google-plus-1": 0
+ },
+ "visits": {
+ "unit": "day",
+ "fields": [
+ "period",
+ "views",
+ "visitors"
+ ],
+ "data": [
+ [
+ "2015-04-15",
+ 107,
+ 86
+ ],
+ [
+ "2015-04-16",
+ 103,
+ 77
+ ],
+ [
+ "2015-04-17",
+ 105,
+ 83
+ ],
+ [
+ "2015-04-18",
+ 65,
+ 51
+ ],
+ [
+ "2015-04-19",
+ 47,
+ 44
+ ],
+ [
+ "2015-04-20",
+ 73,
+ 60
+ ],
+ [
+ "2015-04-21",
+ 114,
+ 90
+ ],
+ [
+ "2015-04-22",
+ 194,
+ 169
+ ],
+ [
+ "2015-04-23",
+ 135,
+ 125
+ ],
+ [
+ "2015-04-24",
+ 77,
+ 70
+ ],
+ [
+ "2015-04-25",
+ 36,
+ 32
+ ],
+ [
+ "2015-04-26",
+ 43,
+ 41
+ ],
+ [
+ "2015-04-27",
+ 111,
+ 99
+ ],
+ [
+ "2015-04-28",
+ 95,
+ 82
+ ],
+ [
+ "2015-04-29",
+ 84,
+ 63
+ ],
+ [
+ "2015-04-30",
+ 81,
+ 67
+ ],
+ [
+ "2015-05-01",
+ 44,
+ 41
+ ],
+ [
+ "2015-05-02",
+ 43,
+ 34
+ ],
+ [
+ "2015-05-03",
+ 40,
+ 38
+ ],
+ [
+ "2015-05-04",
+ 72,
+ 57
+ ],
+ [
+ "2015-05-05",
+ 111,
+ 93
+ ],
+ [
+ "2015-05-06",
+ 75,
+ 62
+ ],
+ [
+ "2015-05-07",
+ 62,
+ 60
+ ],
+ [
+ "2015-05-08",
+ 69,
+ 60
+ ],
+ [
+ "2015-05-09",
+ 40,
+ 31
+ ],
+ [
+ "2015-05-10",
+ 34,
+ 30
+ ],
+ [
+ "2015-05-11",
+ 88,
+ 77
+ ],
+ [
+ "2015-05-12",
+ 72,
+ 63
+ ],
+ [
+ "2015-05-13",
+ 64,
+ 48
+ ],
+ [
+ "2015-05-14",
+ 34,
+ 33
+ ]
+ ]
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-clicks.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-clicks.json
new file mode 100644
index 000000000..166be0fed
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-clicks.json
@@ -0,0 +1,65 @@
+{
+ "date": "2014-11-03",
+ "days": {
+ "2014-11-01": {
+ "clicks": [
+ {
+ "icon": null,
+ "url": "http://wp.com/",
+ "name": "wp.com",
+ "views": 3,
+ "children": null
+ },
+ {
+ "icon": null,
+ "url": null,
+ "name": "blog.wordpress.tv",
+ "views": 2,
+ "children": [
+ {
+ "url": "http://blog.wordpress.tv/2014/10/03/build-your-audience-recent-wordcamp-videos-from-experienced-content-creators/",
+ "name": "blog.wordpress.tv/2014/10/03/build-your-audience-recent-wordcamp-videos-from-experienced-content-creators/",
+ "views": 1
+ },
+ {
+ "url": "http://blog.wordpress.tv/2014/10/29/wordcamp-san-francisco-2014-state-of-the-word-keynote/",
+ "name": "blog.wordpress.tv/2014/10/29/wordcamp-san-francisco-2014-state-of-the-word-keynote/",
+ "views": 1
+ }
+ ]
+ },
+ {
+ "icon": "https://secure.gravatar.com/blavatar/c0a70310ea07fb03e415f74916b37b35?s=48",
+ "url": "http://vip.wordpress.com/2014/10/08/why-choose-wordpress-a-government-perspective-now-with-full-transcript/",
+ "name": "vip.wordpress.com/2014/10/08/why-choose-wordpress-a-government-perspective-now-with-full-transcript/",
+ "views": 1,
+ "children": null
+ },
+ {
+ "icon": null,
+ "url": "http://en.blog.wordpress.com/2014/10/15/blogging-201-fall-2014/",
+ "name": "en.blog.wordpress.com/2014/10/15/blogging-201-fall-2014/",
+ "views": 1,
+ "children": null
+ },
+ {
+ "icon": "https://secure.gravatar.com/blavatar/0ec2f14c9e007ba464c230b3ddd98384?s=48",
+ "url": "http://wordpress.org/",
+ "name": "wordpress.org",
+ "views": 1,
+ "children": null
+ },
+ {
+ "icon": "https://secure.gravatar.com/blavatar/2343ec78a04c6ea9d80806345d31fd78?s=48",
+ "url": "http://www.facebook.com/",
+ "name": "Facebook",
+ "views": 1,
+ "children": null
+ }
+ ],
+ "other_clicks": 0,
+ "total_clicks": 9
+ }
+ },
+ "period": "month"
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-followers.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-followers.json
new file mode 100644
index 000000000..69734c382
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-followers.json
@@ -0,0 +1,156 @@
+{
+ "page": 1,
+ "pages": 1132258,
+ "total": 7925800,
+ "total_email": 2930,
+ "total_wpcom": 7925800,
+ "subscribers": [
+ {
+ "avatar": "https://0.gravatar.com/avatar/624b89cb0c8b9136f9629dd7bcab0517?s=64&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "ritu929",
+ "url": "http://ritu9blog.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "ritu9blog.wordpress.com",
+ "blog_url": "http://ritu9blog.wordpress.com",
+ "blog_id": 80982396,
+ "site_id": 80982396,
+ "blog_title": "ritu929&#039;s Blog",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:53:21+00:00"
+ },
+ {
+ "avatar": "https://1.gravatar.com/avatar/d9ece92eea64b1090108895fea6133c4?s=64&amp;d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "sotogiovannysotogiovanny",
+ "url": "http://sotogiovanny.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "sotogiovanny.wordpress.com",
+ "blog_url": "http://sotogiovanny.wordpress.com",
+ "blog_id": 80982334,
+ "site_id": 80982334,
+ "blog_title": "sotogiovanny",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:53:02+00:00"
+ },
+ {
+ "avatar": "https://1.gravatar.com/avatar/7949ad6afe7d0a666427170569ef14c9?s=64&amp;d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "zoe12296",
+ "url": "http://zoe12296.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "zoe12296.wordpress.com",
+ "blog_url": "http://zoe12296.wordpress.com",
+ "blog_id": 80982352,
+ "site_id": 80982352,
+ "blog_title": "zoe12296",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:52:57+00:00"
+ },
+ {
+ "avatar": "https://2.gravatar.com/avatar/2c572d6e19015b6db0d55bdf335caf53?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "oisin1min",
+ "url": "http://oisin18curran.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "oisin18curran.wordpress.com",
+ "blog_url": "http://oisin18curran.wordpress.com",
+ "blog_id": 80982349,
+ "site_id": 80982349,
+ "blog_title": "oisin18curran",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:52:52+00:00"
+ },
+ {
+ "avatar": "https://0.gravatar.com/avatar/f7a40fd8ac387756167d6d36c58213da?s=64&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "linistle",
+ "url": "http://solitarystory.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "solitarystory.wordpress.com",
+ "blog_url": "http://solitarystory.wordpress.com",
+ "blog_id": 80982340,
+ "site_id": 80982340,
+ "blog_title": "solitarystory",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:52:49+00:00"
+ },
+ {
+ "avatar": "https://1.gravatar.com/avatar/1689bf5175c2aa9f2c13a0d36acc944d?s=64&amp;d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "ambxblog",
+ "url": "http://ambxblog.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "ambxblog.wordpress.com",
+ "blog_url": "http://ambxblog.wordpress.com",
+ "blog_id": 80982342,
+ "site_id": 80982342,
+ "blog_title": "ambxblog",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:52:48+00:00"
+ },
+ {
+ "avatar": "https://2.gravatar.com/avatar/b98fab16a514e036f27a6371d5ee3324?s=64&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G",
+ "label": "graphene101010",
+ "url": "http://graphene101010.wordpress.com",
+ "follow_data": {
+ "params": {
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "following-text": "Following",
+ "following-hover-text": "Unfollow",
+ "blog_domain": "graphene101010.wordpress.com",
+ "blog_url": "http://graphene101010.wordpress.com",
+ "blog_id": 80982314,
+ "site_id": 80982314,
+ "blog_title": "graphene101010",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "date_subscribed": "2014-12-16T14:52:47+00:00"
+ }
+ ]
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-top-posts.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-top-posts.json
new file mode 100644
index 000000000..9b33f044c
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-top-posts.json
@@ -0,0 +1,101 @@
+{
+ "date": "2014-11-04",
+ "days": {
+ "2014-11-04": {
+ "postviews": [
+ {
+ "id": 750,
+ "href": false,
+ "date": "2014-08-06 14:52:11",
+ "title": "Asynchronous unit testing Core Data with Xcode 6",
+ "views": 7,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 200,
+ "href": "http://astralbodi.es/2012/02/01/resizing-a-uitextview-automatically-with-the-keyboard/",
+ "date": "2012-02-01 09:34:11",
+ "title": "Resizing a UITextView automatically with the keyboard",
+ "views": 4,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 120,
+ "href": "http://astralbodi.es/2011/03/20/ios-basics-uinavigation-controller-back-button-text/",
+ "date": "2011-03-20 10:16:09",
+ "title": "iOS Basics - UINavigation Controller & Back Button Text",
+ "views": 4,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 0,
+ "href": "http://astralbodi.es/",
+ "date": null,
+ "title": "Home page / Archives",
+ "views": 2,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 111,
+ "href": "http://astralbodi.es/2011/02/04/mac-os-x-adding-a-loopback-alias/",
+ "date": "2011-02-04 10:13:33",
+ "title": "Mac OS X - Adding a loopback alias",
+ "views": 2,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 802,
+ "href": "http://astralbodi.es/2014/10/30/stabby/",
+ "date": "2014-10-30 14:00:38",
+ "title": "Stabby",
+ "views": 2,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 151,
+ "href": "http://astralbodi.es/2011/07/07/ios-pull-app-version-from-bundle-configuration/",
+ "date": "2011-07-07 08:46:42",
+ "title": "iOS - Pull App Version From Bundle Configuration",
+ "views": 1,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 725,
+ "href": "http://astralbodi.es/2014/06/05/mac-os-x-10-9-mavericks-calendar-google-sync-problems/",
+ "date": "2014-06-05 10:23:24",
+ "title": "Mac OS X 10.9 Mavericks Calendar + Google Sync Problems",
+ "views": 1,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 59,
+ "href": "http://astralbodi.es/2009/11/16/xcode-wtf-are-you-doing/",
+ "date": "2009-11-16 09:36:02",
+ "title": "Xcode WTF are you doing?!",
+ "views": 1,
+ "type": "post",
+ "video_play": false
+ },
+ {
+ "id": 149,
+ "href": "http://astralbodi.es/2011/07/05/time-warner-cable-power-snr-acceptable-values/",
+ "date": "2011-07-05 10:20:03",
+ "title": "Time Warner Cable Power / SNR Acceptable Values",
+ "views": 1,
+ "type": "post",
+ "video_play": false
+ }
+ ],
+ "total_views": 25
+ }
+ },
+ "period": "day"
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-video-plays.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-video-plays.json
new file mode 100644
index 000000000..6e8c251f4
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-1-sites-1234567890-stats-video-plays.json
@@ -0,0 +1,18 @@
+{
+ "date": "2014-12-16",
+ "period": "day",
+ "days": {
+ "2014-12-16": {
+ "plays": [
+ {
+ "post_id": 144,
+ "url": "http://maplebaconyummies.wordpress.com/wp-admin/media.php?action=edit&attachment_id=144",
+ "title": "Test Video",
+ "plays": 2
+ }
+ ],
+ "other_plays": 0,
+ "total_plays": 2
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-plans-features.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-plans-features.json
new file mode 100644
index 000000000..576eb51bd
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-plans-features.json
@@ -0,0 +1,203 @@
+{
+ "originalResponse": [
+ {
+ "product_slug": "free-blog",
+ "title": "WordPress.com Site",
+ "description": "Your own space to create posts and pages with basic customization.",
+ "icon": "",
+ "plans": {
+ "1": true,
+ "1003": true,
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "ecommerce",
+ "title": "eCommerce",
+ "description": "Sell stuff right on your blog with Ecwid and Shopify.",
+ "icon": "",
+ "plans": {
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "custom-domain",
+ "title": "Custom site address",
+ "description": "Make your site easier to find and easier to remember.",
+ "icon": "",
+ "not_part_of_free_trial": true,
+ "plans": {
+ "1003": true,
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "space",
+ "title": "Space",
+ "description": "Increase your available storage space and add the ability to upload audio files.",
+ "icon": "",
+ "plans": {
+ "1": {
+ "title": "Media storage",
+ "description": "Upload up to 3GB of photos, videos, or music.",
+ "icon": ""
+ },
+ "1003": {
+ "title": "Expanded media storage",
+ "description": "Upload up to 13GB of photos, videos, or music.",
+ "icon": ""
+ },
+ "1008": {
+ "title": "Unlimited media storage",
+ "description": "You can upload unlimited photos, videos, or music.",
+ "icon": ""
+ }
+ }
+ },
+ {
+ "product_slug": "no-adverts\/no-adverts.php",
+ "title": "No ads",
+ "description": "WordPress.com ads will not display on your site.",
+ "icon": "",
+ "plans": {
+ "1003": true,
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "custom-design",
+ "title": "Custom fonts, colors, & CSS",
+ "description": "Change your theme's font, colors, and CSS for a unique look",
+ "icon": "",
+ "plans": {
+ "1003": true,
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "videopress",
+ "title": "Video storage & hosting",
+ "description": "Upload and host videos on your site without advertising.",
+ "icon": "",
+ "plans": {
+ "1003": true,
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "premium-themes",
+ "title": "Unlimited Premium Themes",
+ "description": "Exclusive access to our best themes. Try many as you like.",
+ "icon": "",
+ "plans": {
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "google-analytics",
+ "title": "Google Analytics",
+ "description": "Google Analytics is a free service that offers a complementary view of your traffic to our built-in Stats feature.",
+ "icon": "",
+ "plans": {
+ "1008": true
+ }
+ },
+ {
+ "product_slug": "akismet",
+ "title": "Spam protection",
+ "description": "Spam protection",
+ "icon": "",
+ "plans": {
+ "2002": "Akismet Basic",
+ "2000": "Akismet Plus",
+ "2001": "Akismet Unlimited"
+ }
+ },
+ {
+ "product_slug": "vaultpress-backups",
+ "title": "Database backups",
+ "description": "Database backups",
+ "icon": "",
+ "plans": {
+ "2000": "VaultPress Daily",
+ "2001": "VaultPress Real-time"
+ }
+ },
+ {
+ "product_slug": "vaultpress-backup-archive",
+ "title": "Backup archive",
+ "description": "Backup archive",
+ "icon": "",
+ "plans": {
+ "2000": "30 days",
+ "2001": "Full archive"
+ }
+ },
+ {
+ "product_slug": "vaultpress-automated-restores",
+ "title": "Automated Restores",
+ "description": "Automated Restores",
+ "icon": "",
+ "plans": {
+ "2000": true,
+ "2001": true
+ }
+ },
+ {
+ "product_slug": "vaultpress-security-scanning",
+ "title": "Security scanning",
+ "description": "Security scanning",
+ "icon": "",
+ "plans": {
+ "2001": true
+ }
+ },
+ {
+ "product_slug": "polldaddy",
+ "title": "Polls and Ratings",
+ "description": "Polls and Ratings",
+ "icon": "",
+ "plans": {
+ "2001": "PollDaddy Pro"
+ }
+ },
+ {
+ "product_slug": "support",
+ "title": "Support",
+ "description": "For those times when you can't find an answer on our Support site",
+ "icon": "",
+ "plans": {
+ "1": {
+ "title": "Community support",
+ "description": "Find answers to your questions in our community forum.",
+ "icon": ""
+ },
+ "1003": {
+ "title": "Priority support",
+ "description": "Ask our happiness engineers questions anytime you need.",
+ "icon": ""
+ },
+ "1008": {
+ "title": "Live chat support",
+ "description": "Chat live with our happiness engineers questions anytime you need.",
+ "icon": ""
+ },
+ "2002": {
+ "title": "Direct email",
+ "description": "The kind of support we offer for Jetpack Free.",
+ "icon": ""
+ },
+ "2000": {
+ "title": "Expert security support",
+ "description": "The kind of support we offer for Jetpack Premium.",
+ "icon": ""
+ },
+ "2001": {
+ "title": "Priority security support",
+ "description": "The kind of support we offer for Jetpack Business.",
+ "icon": ""
+ }
+ }
+ }
+ ]
+}
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-sites-123456-plans.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-sites-123456-plans.json
new file mode 100644
index 000000000..35d6e3590
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-2-sites-123456-plans.json
@@ -0,0 +1,191 @@
+{
+ "originalResponse":
+ [
+ {
+ "product_id": 1,
+ "product_name": "WordPress.com Free",
+ "prices": {
+ "USD": 0,
+ "AUD": 0,
+ "CAD": 0,
+ "EUR": 0,
+ "GBP": 0,
+ "JPY": 0
+ },
+ "product_name_short": "Free",
+ "product_slug": "free_plan",
+ "tagline": "Perfect for anyone creating a basic blog or site",
+ "description": "Get a free blog and be on your way to publishing your first post in less than five minutes.",
+ "icon": "https:\/\/s0.wordpress.com\/i\/store\/plan-free.png",
+ "icon_active": "https:\/\/s0.wordpress.com\/i\/store\/plan-free-active.png",
+ "capability": "manage_options",
+ "cost": 0,
+ "apple_sku": "",
+ "android_sku": "",
+ "bill_period": -1,
+ "product_type": "bundle",
+ "available": "yes",
+ "store": 0,
+ "features_highlight": [
+ {
+ "items": [
+ "free-blog",
+ "space",
+ "support"
+ ]
+ }
+ ],
+ "bill_period_label": "for life",
+ "price": "$0",
+ "formatted_price": "$0",
+ "raw_price": 0,
+ "raw_discount": 0,
+ "formatted_discount": "$0"
+ },
+ {
+ "product_id": 1003,
+ "product_name": "WordPress.com Premium",
+ "prices": {
+ "USD": 99,
+ "NZD": 139,
+ "AUD": 129,
+ "CAD": 129,
+ "JPY": 11800,
+ "EUR": 99,
+ "GBP": 85
+ },
+ "product_name_short": "Premium",
+ "product_slug": "value_bundle",
+ "tagline": "Perfect for bloggers, creatives, and other professionals",
+ "description": "Your own domain name, powerful customization options, and lots of space for audio and video.",
+ "icon": "https:\/\/s0.wordpress.com\/i\/store\/plan-premium.png",
+ "icon_active": "https:\/\/s0.wordpress.com\/i\/store\/plan-premium-active.png",
+ "capability": "manage_options",
+ "cost": 99,
+ "apple_sku": "",
+ "android_sku": "",
+ "bill_period": 365,
+ "product_type": "bundle",
+ "available": "yes",
+ "store": 0,
+ "features_highlight": [
+ {
+ "items": [
+ "custom-design",
+ "videopress",
+ "support",
+ "space",
+ "custom-domain",
+ "no-adverts\/no-adverts.php"
+ ]
+ },
+ {
+ "title": "Included with all plans",
+ "items": [
+ "free-blog"
+ ]
+ }
+ ],
+ "width": 500,
+ "height": 250,
+ "multi": 0,
+ "support_document": "bundles",
+ "bundle_product_ids": [
+ 9,
+ 12,
+ 45,
+ 15,
+ 5,
+ 16,
+ 6,
+ 46,
+ 54
+ ],
+ "bill_period_label": "per year",
+ "price": "$99",
+ "formatted_price": "$99",
+ "raw_price": 99,
+ "raw_discount": 0,
+ "formatted_discount": "$0",
+ "current_plan": true,
+ "bundle_subscription_id": "5683566",
+ "expiry": "2017-03-07",
+ "free_trial": false,
+ "user_facing_expiry": "2017-03-04",
+ "subscribed_date": "2016-03-07 08:56:13"
+ },
+ {
+ "product_id": 1008,
+ "product_name": "WordPress.com Business",
+ "prices": {
+ "USD": 299,
+ "NZD": 399,
+ "AUD": 399,
+ "CAD": 389,
+ "JPY": 35800,
+ "EUR": 299,
+ "GBP": 250
+ },
+ "product_name_short": "Business",
+ "product_slug": "business-bundle",
+ "tagline": "Perfect for power users, business websites and blogs",
+ "description": "Everything included with Premium, as well as live chat support, and unlimited access to our premium themes.",
+ "icon": "https:\/\/s0.wordpress.com\/i\/store\/plan-business.png",
+ "icon_active": "https:\/\/s0.wordpress.com\/i\/store\/plan-business-active.png",
+ "capability": "manage_options",
+ "cost": 199,
+ "apple_sku": "",
+ "android_sku": "",
+ "features_highlight": [
+ {
+ "items": [
+ "premium-themes",
+ "space",
+ "support"
+ ]
+ },
+ {
+ "title": "Includes WordPress.com Premium features:",
+ "items": [
+ "custom-design",
+ "videopress",
+ "no-adverts\/no-adverts.php",
+ "custom-domain"
+ ]
+ },
+ {
+ "title": "Included with all plans:",
+ "items": [
+ "free-blog"
+ ]
+ }
+ ],
+ "bill_period": 365,
+ "width": 500,
+ "height": 435,
+ "product_type": "bundle",
+ "available": "yes",
+ "bundle_product_ids": [
+ 12,
+ 45,
+ 15,
+ 5,
+ 48,
+ 50,
+ 49,
+ 16,
+ 6,
+ 46,
+ 54
+ ],
+ "orig_cost": 299,
+ "bill_period_label": "per year",
+ "price": "$199",
+ "formatted_price": "$199",
+ "raw_price": 199,
+ "raw_discount": 100,
+ "formatted_discount": "$100",
+ "can_start_trial": false
+ }
+ ]
+}
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-me.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-me.json
new file mode 100644
index 000000000..bb0369932
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-me.json
@@ -0,0 +1,18 @@
+{
+ "display_name":"taliwutt",
+ "username":"taliwutt",
+ "profile_URL":"http:\/\/en.gravatar.com\/taliwutt",
+ "email":"taliwut@gmail.com",
+ "verified":true,
+ "ID":55434822,
+ "token_site_id":false,
+ "primary_blog":57991476,
+ "avatar_URL":"http:\/\/0.gravatar.com\/avatar\/098a6277aeca89a789a1d3be00db59f8?s=96&d=identicon",
+ "meta":{
+ "links":{
+ "site":"https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/5836086",
+ "help":"https:\/\/public-api.wordpress.com\/rest\/v1\/me\/help",
+ "self":"https:\/\/public-api.wordpress.com\/rest\/v1\/me"
+ }
+ }
+}
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new.json
new file mode 100644
index 000000000..196ec18c8
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new.json
@@ -0,0 +1,9 @@
+{
+ "success": true,
+ "blog_details": {
+ "url": "http:\/\/taliwutt88.wordpress.com\/",
+ "blogid": "63236571",
+ "blogname": "taliwutt88",
+ "xmlrpc": "https:\/\/taliwutt88.wordpress.com\/xmlrpc.php"
+ }
+}
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new_1.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new_1.json
new file mode 100644
index 000000000..7ea4fef09
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-sites-new_1.json
@@ -0,0 +1,3 @@
+{
+ "success": true
+}
diff --git a/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-users-new.json b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-users-new.json
new file mode 100644
index 000000000..7ea4fef09
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-public-api-wordpress-com-rest-v1-users-new.json
@@ -0,0 +1,3 @@
+{
+ "success": true
+}
diff --git a/WordPress/src/androidTest/assets/default-wp.deletePost.xml b/WordPress/src/androidTest/assets/default-wp.deletePost.xml
new file mode 100644
index 000000000..e83369471
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.deletePost.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getComments.json b/WordPress/src/androidTest/assets/default-wp.getComments.json
new file mode 100644
index 000000000..45b36278c
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getComments.json
@@ -0,0 +1,482 @@
+[
+ {
+ "comment_id":"146",
+ "status":"approve",
+ "author_url":"http://google.com",
+ "link":"https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/comment-page-1/#comment-146",
+ "parent":"0",
+ "type":"",
+ "post_id":"296",
+ "author_ip":"88.191.153.166",
+ "content":"Cool",
+ "author":"Renardo",
+ "author_email":"",
+ "date_created_gmt":"Jan 22, 2014 12:46:28 PM",
+ "user_id":"0",
+ "post_title":"The End of Unrecorded Life"
+ },
+ {
+ "comment_id":"145",
+ "status":"hold",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-145",
+ "parent":"139",
+ "type":"",
+ "post_id":"234",
+ "author_ip":"82.236.36.146",
+ "content":"Oki",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Jan 22, 2014 11:39:47 AM",
+ "user_id":"55434822",
+ "post_title":"Privé\u0026nbsp;: Pony"
+ },
+ {
+ "comment_id":"144",
+ "status":"hold",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-144",
+ "parent":"139",
+ "type":"",
+ "post_id":"234",
+ "author_ip":"82.236.36.146",
+ "content":"Ok!",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Jan 22, 2014 11:39:35 AM",
+ "user_id":"55434822",
+ "post_title":"Privé\u0026nbsp;: Pony"
+ },
+ {
+ "comment_id":"143",
+ "status":"hold",
+ "author_url":"http://biais.org",
+ "link":"https://taliwutblog.wordpress.com/2014/01/09/magnifique/comment-page-1/#comment-143",
+ "parent":"0",
+ "type":"",
+ "post_id":"277",
+ "author_ip":"82.236.36.146",
+ "content":"\u003ca href\u003d\"http://google.com\" rel\u003d\"nofollow\"\u003ego to google\u003c/a\u003e",
+ "author":"Maxime",
+ "author_email":"maxime@automattic.com",
+ "date_created_gmt":"Jan 9, 2014 5:55:32 PM",
+ "user_id":"50540106",
+ "post_title":"Magnifique!"
+ },
+ {
+ "comment_id":"142",
+ "status":"approve",
+ "author_url":"http://biais.org",
+ "link":"https://taliwutblog.wordpress.com/2014/01/09/magnifique/comment-page-1/#comment-142",
+ "parent":"0",
+ "type":"",
+ "post_id":"277",
+ "author_ip":"82.236.36.146",
+ "content":"\u003ca href\u003d\"woot.com\" rel\u003d\"nofollow\"\u003ego to woot\u003c/a\u003e",
+ "author":"Maxime",
+ "author_email":"maxime@automattic.com",
+ "date_created_gmt":"Jan 9, 2014 5:52:34 PM",
+ "user_id":"50540106",
+ "post_title":"Magnifique!"
+ },
+ {
+ "comment_id":"141",
+ "status":"approve",
+ "author_url":"http://biais.org",
+ "link":"https://taliwutblog.wordpress.com/2014/01/03/273/comment-page-1/#comment-141",
+ "parent":"0",
+ "type":"",
+ "post_id":"273",
+ "author_ip":"82.236.36.146",
+ "content":"\u003ca href\u003d\"woot.com\" rel\u003d\"nofollow\"\u003ego to woot\u003c/a\u003e",
+ "author":"Maxime",
+ "author_email":"maxime@automattic.com",
+ "date_created_gmt":"Jan 9, 2014 5:50:18 PM",
+ "user_id":"50540106",
+ "post_title":""
+ },
+ {
+ "comment_id":"139",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-139",
+ "parent":"0",
+ "type":"",
+ "post_id":"234",
+ "author_ip":"82.236.36.146",
+ "content":"ij",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 23, 2013 9:53:29 AM",
+ "user_id":"55434822",
+ "post_title":"Privé\u0026nbsp;: Pony"
+ },
+ {
+ "comment_id":"138",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-138",
+ "parent":"0",
+ "type":"",
+ "post_id":"234",
+ "author_ip":"82.236.36.146",
+ "content":"Test",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 23, 2013 9:31:31 AM",
+ "user_id":"55434822",
+ "post_title":"Privé\u0026nbsp;: Pony"
+ },
+ {
+ "comment_id":"137",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-137",
+ "parent":"135",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"Cool",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 20, 2013 11:18:03 AM",
+ "user_id":"55434822",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"136",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-136",
+ "parent":"134",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"OK!",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 20, 2013 7:42:29 AM",
+ "user_id":"55434822",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"135",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-135",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"g \u0026amp; h",
+ "author":"taliwuttalot",
+ "author_email":"",
+ "date_created_gmt":"Dec 19, 2013 12:01:06 PM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"134",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-134",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"e \u0026amp; f",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 19, 2013 12:00:59 PM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"133",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-133",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"c \u0026amp; d",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 19, 2013 11:55:53 AM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"132",
+ "status":"hold",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-132",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"a \u0026amp; b",
+ "author":"taliwuttalot",
+ "author_email":"",
+ "date_created_gmt":"Dec 19, 2013 11:55:44 AM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"131",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-131",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"Toi \u0026amp; Moi",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 19, 2013 11:48:02 AM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"130",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/03/226/comment-page-1/#comment-130",
+ "parent":"0",
+ "type":"",
+ "post_id":"226",
+ "author_ip":"82.236.36.146",
+ "content":"\u0026amp;",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 19, 2013 11:47:16 AM",
+ "user_id":"58499323",
+ "post_title":""
+ },
+ {
+ "comment_id":"129",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/03/226/comment-page-1/#comment-129",
+ "parent":"0",
+ "type":"",
+ "post_id":"226",
+ "author_ip":"82.236.36.146",
+ "content":"\u0026amp;\u0026amp; éé",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 19, 2013 11:45:36 AM",
+ "user_id":"58499323",
+ "post_title":""
+ },
+ {
+ "comment_id":"128",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-128",
+ "parent":"127",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"Pouet",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 18, 2013 4:39:17 PM",
+ "user_id":"55434822",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"127",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-127",
+ "parent":"126",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"Hoy",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 18, 2013 4:38:41 PM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"126",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-126",
+ "parent":"124",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"Nice",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 18, 2013 4:38:22 PM",
+ "user_id":"55434822",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"125",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-125",
+ "parent":"124",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"J\u0027aime \u0026amp; j\u0027adore",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 18, 2013 4:27:09 PM",
+ "user_id":"55434822",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"124",
+ "status":"approve",
+ "author_url":"http://taliwuttalot1.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-124",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"A reblogué ceci sur \u003ca href\u003d\"http://taliwuttalot1.wordpress.com/2013/12/18/cat/\" rel\u003d\"nofollow\"\u003etaliwuttalot1\u003c/a\u003eet a ajout\u0026eacute;:\nAnother",
+ "author":"taliwuttalot",
+ "author_email":"taliwut+alot@gmail.com",
+ "date_created_gmt":"Dec 18, 2013 3:58:09 PM",
+ "user_id":"58499323",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"123",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-123",
+ "parent":"0",
+ "type":"",
+ "post_id":"228",
+ "author_ip":"82.236.36.146",
+ "content":"\u003cp\u003eA reblogué ceci sur \u003ca href\u003d\"http://ilovecutecats.wordpress.com/2013/12/18/cat/\" rel\u003d\"nofollow\"\u003eI Love Cute Cats\u003c/a\u003eet a ajout\u0026eacute;:\u003c/p\u003e\n\u003cp\u003eCute cat\u003c/p\u003e",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 18, 2013 3:57:49 PM",
+ "user_id":"55434822",
+ "post_title":"Cat"
+ },
+ {
+ "comment_id":"120",
+ "status":"approve",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/10/10/hey-13/comment-page-1/#comment-120",
+ "parent":"89",
+ "type":"",
+ "post_id":"162",
+ "author_ip":"82.236.36.146",
+ "content":"k",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 5, 2013 4:14:39 PM",
+ "user_id":"55434822",
+ "post_title":"Hey 13"
+ },
+ {
+ "comment_id":"119",
+ "status":"hold",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/11/04/daily-prompt-placebo-effect/comment-page-1/#comment-119",
+ "parent":"118",
+ "type":"",
+ "post_id":"212",
+ "author_ip":"82.236.36.146",
+ "content":"oi",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 5, 2013 12:18:26 PM",
+ "user_id":"55434822",
+ "post_title":"Daily Prompt: Placebo Effect"
+ },
+ {
+ "comment_id":"118",
+ "status":"approve",
+ "author_url":"http://apzld",
+ "link":"https://taliwutblog.wordpress.com/2013/11/04/daily-prompt-placebo-effect/comment-page-1/#comment-118",
+ "parent":"0",
+ "type":"",
+ "post_id":"212",
+ "author_ip":"82.236.36.146",
+ "content":"great bike",
+ "author":"azdald",
+ "author_email":"",
+ "date_created_gmt":"Dec 5, 2013 12:15:47 PM",
+ "user_id":"0",
+ "post_title":"Daily Prompt: Placebo Effect"
+ },
+ {
+ "comment_id":"117",
+ "status":"hold",
+ "author_url":"http://zeopfk",
+ "link":"https://taliwutblog.wordpress.com/2013/11/04/hghj/comment-page-1/#comment-117",
+ "parent":"0",
+ "type":"",
+ "post_id":"214",
+ "author_ip":"82.236.36.146",
+ "content":"Very nice mug",
+ "author":"mugman",
+ "author_email":"",
+ "date_created_gmt":"Dec 5, 2013 12:13:53 PM",
+ "user_id":"0",
+ "post_title":"Hghj"
+ },
+ {
+ "comment_id":"116",
+ "status":"hold",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/11/06/test/comment-page-1/#comment-116",
+ "parent":"114",
+ "type":"",
+ "post_id":"216",
+ "author_ip":"82.236.36.146",
+ "content":"Test",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 5, 2013 11:18:00 AM",
+ "user_id":"55434822",
+ "post_title":"Test"
+ },
+ {
+ "comment_id":"115",
+ "status":"hold",
+ "author_url":"http://taliwutblog.wordpress.com",
+ "link":"https://taliwutblog.wordpress.com/2013/11/06/test/comment-page-1/#comment-115",
+ "parent":"114",
+ "type":"",
+ "post_id":"216",
+ "author_ip":"82.236.36.146",
+ "content":"???",
+ "author":"taliwutt",
+ "author_email":"taliwut@gmail.com",
+ "date_created_gmt":"Dec 5, 2013 11:17:29 AM",
+ "user_id":"55434822",
+ "post_title":"Test"
+ },
+ {
+ "comment_id":"114",
+ "status":"approve",
+ "author_url":"http://zeopfk",
+ "link":"https://taliwutblog.wordpress.com/2013/11/06/test/comment-page-1/#comment-114",
+ "parent":"0",
+ "type":"",
+ "post_id":"216",
+ "author_ip":"82.236.36.146",
+ "content":"Ah bah oui...",
+ "author":"zepofkezpf",
+ "author_email":"azdazd@frrfrfzefe.fr",
+ "date_created_gmt":"Dec 5, 2013 11:13:37 AM",
+ "user_id":"0",
+ "post_title":"Test"
+ }
+] \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getComments.xml b/WordPress/src/androidTest/assets/default-wp.getComments.xml
new file mode 100644
index 000000000..e0cac51e5
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getComments.xml
@@ -0,0 +1,2654 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T13:46:27</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>165</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>ouh ouh</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/cute-pony/comment-page-1/#comment-165</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>313</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cute pony</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>ARST</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>arst@srt.ts</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T13:45:53</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>164</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>cute</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/another-pony/comment-page-1/#comment-164</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>319</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Another Pony</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>ARST</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>arst@srt.ts</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T13:44:51</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>163</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>haha</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak-2/comment-page-1/#comment-163</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>324</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Your Keyboard &amp;amp; You. I&amp;rsquo;ll Stick With Colemak</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>ARST</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>arst@srt.ts</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T13:42:22</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>160</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>ioesnrt</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-4/comment-page-1/#comment-160</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>330</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>#BookReview Bridget Jones: Mad About the Boy</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>ARST</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>arst@srt.ts</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140205T09:24:28</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>154</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>152</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>&lt;a href=&quot;google.fr&quot; rel=&quot;nofollow&quot;&gt;test&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/comment-page-/#comment-154</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>303</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>The New iOS and Android Apps Have Arrived!</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140131T10:31:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>151</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>super</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/comment-page-1/#comment-151</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>303</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>The New iOS and Android Apps Have Arrived!</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>qws</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>qws@sqws.tp</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140131T10:30:10</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>150</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>cool</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/comment-page-1/#comment-150</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>303</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>The New iOS and Android Apps Have Arrived!</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>qws</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>qws@sqws.tp</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T12:46:28</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>146</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Cool</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/comment-page-1/#comment-146</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>296</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>The End of Unrecorded Life</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>Renardo</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://google.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>88.191.153.166</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T11:39:35</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>144</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>139</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Ok!</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-144</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>234</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Privé&amp;nbsp;: Pony</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140109T17:55:32</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>50540106</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>143</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>&lt;a href=&quot;http://google.com&quot; rel=&quot;nofollow&quot;&gt;go to google&lt;/a&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/01/09/magnifique/comment-page-1/#comment-143</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>277</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Magnifique!</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>Maxime</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://biais.org</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>maxime@automattic.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T09:53:29</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>139</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>ij</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-139</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>234</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Privé&amp;nbsp;: Pony</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T09:31:31</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>138</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Test</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/20/pony-2/comment-page-1/#comment-138</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>234</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Privé&amp;nbsp;: Pony</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131220T11:18:03</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>137</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>135</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Cool</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-137</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131220T07:42:29</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>136</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>134</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>OK!</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-136</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131219T12:01:06</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>135</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>g &amp;amp; h</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-135</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131219T12:00:59</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>134</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>e &amp;amp; f</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-134</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut+alot@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131219T11:55:53</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>133</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>c &amp;amp; d</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-133</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut+alot@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131219T11:55:44</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>132</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>a &amp;amp; b</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-132</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131219T11:48:02</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>131</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Toi &amp;amp; Moi</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-131</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut+alot@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131219T11:47:16</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>130</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>&amp;amp;</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/03/226/comment-page-1/#comment-130</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>226</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut+alot@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131218T16:39:17</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>128</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>127</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Pouet</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-128</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131218T16:38:41</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>127</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>126</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Hoy</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-127</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut+alot@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131218T16:38:22</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>126</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>124</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Nice</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-126</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131218T16:27:09</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>125</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>124</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>J'aime &amp;amp; j'adore</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-125</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131218T15:58:09</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>58499323</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>124</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>A reblogué ceci sur &lt;a href=&quot;http://taliwuttalot1.wordpress.com/2013/12/18/cat/&quot; rel=&quot;nofollow&quot;&gt;taliwuttalot1&lt;/a&gt;et a ajout&amp;eacute;:
+ Another</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-124</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwuttalot</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwuttalot1.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut+alot@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131218T15:57:49</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>123</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>&lt;p&gt;A reblogué ceci sur &lt;a href=&quot;http://ilovecutecats.wordpress.com/2013/12/18/cat/&quot; rel=&quot;nofollow&quot;&gt;I Love Cute Cats&lt;/a&gt;et a ajout&amp;eacute;:&lt;/p&gt; &lt;p&gt;Cute cat&lt;/p&gt;</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/12/17/cat/comment-page-1/#comment-123</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>228</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Cat</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131205T16:14:39</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>120</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>89</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>k</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/10/10/hey-13/comment-page-1/#comment-120</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>162</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Hey 13</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131205T12:18:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>119</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>118</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>oi</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/11/04/daily-prompt-placebo-effect/comment-page-1/#comment-119</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>212</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Daily Prompt: Placebo Effect</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131205T12:15:47</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>118</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>approve</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>great bike</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/11/04/daily-prompt-placebo-effect/comment-page-1/#comment-118</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>212</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Daily Prompt: Placebo Effect</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>azdald</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://apzld</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131205T12:13:53</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>comment_id</name>
+ <value>
+ <string>117</string>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <string>0</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>hold</string>
+ </value>
+ </member>
+ <member>
+ <name>content</name>
+ <value>
+ <string>Very nice mug</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2013/11/04/hghj/comment-page-1/#comment-117</string>
+ </value>
+ </member>
+ <member>
+ <name>post_id</name>
+ <value>
+ <string>214</string>
+ </value>
+ </member>
+ <member>
+ <name>post_title</name>
+ <value>
+ <string>Hghj</string>
+ </value>
+ </member>
+ <member>
+ <name>author</name>
+ <value>
+ <string>mugman</string>
+ </value>
+ </member>
+ <member>
+ <name>author_url</name>
+ <value>
+ <string>http://zeopfk</string>
+ </value>
+ </member>
+ <member>
+ <name>author_email</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>author_ip</name>
+ <value>
+ <string>82.236.36.146</string>
+ </value>
+ </member>
+ <member>
+ <name>type</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getMediaLibrary.xml b/WordPress/src/androidTest/assets/default-wp.getMediaLibrary.xml
new file mode 100644
index 000000000..13769ba47
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getMediaLibrary.xml
@@ -0,0 +1,2791 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>318</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T11:51:09</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>319</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/02/wpid-urbanherovantbarreeltje.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-Urbanherovantbarreeltje.JPG</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>2000</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>1497</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2014/02/wpid-urbanherovantbarreeltje.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/02/wpid-urbanherovantbarreeltje.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>315</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T10:20:26</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>316</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/02/wpid-pony1.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2014/02/wpid-pony1.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/02/wpid-pony1.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>312</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140203T09:49:13</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>313</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/02/wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2014/02/wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/02/wpid-pony.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>304</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140130T19:02:53</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>303</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/01/android26-ios39-promo.png</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>android26-ios39-promo</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1200</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>800</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2014/01/android26-ios39-promo.png</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/01/android26-ios39-promo.png?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>289</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140122T10:32:04</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>287</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/01/wpid-love-island-1920c3971080-wallpapers-jpg.jpeg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-Love-Island-1920×1080-wallpapers.jpg.jpeg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>990</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>557</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2014/01/wpid-love-island-1920c3971080-wallpapers-jpg.jpeg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/01/wpid-love-island-1920c3971080-wallpapers-jpg.jpeg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>276</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140107T16:31:16</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/01/wpid-storageemulated0download9117-animated_gif-derpy_hooves-mailbox-my_little_pony_friendship_is_magic-tagme.gif</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-storageemulated0Download9117-animated_gif-derpy_hooves-mailbox-my_little_pony_friendship_is_magic-tagme.gif.gif</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>500</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>500</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2014/01/wpid-storageemulated0download9117-animated_gif-derpy_hooves-mailbox-my_little_pony_friendship_is_magic-tagme.gif</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2014/01/wpid-storageemulated0download9117-animated_gif-derpy_hooves-mailbox-my_little_pony_friendship_is_magic-tagme.gif?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>253</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131227T10:56:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>254</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-wp-1388141726961.jpeg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string>Mop</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>2000</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>1480</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/wpid-wp-1388141726961.jpeg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>250</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T15:03:40</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony14.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/wpid-pony14.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony14.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>249</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T15:03:25</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony13.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/wpid-pony13.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony13.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>248</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T14:58:57</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony12.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/wpid-pony12.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony12.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>247</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T14:55:59</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony11.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/wpid-pony11.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony11.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>246</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131223T14:27:32</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony10.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-pony.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1280</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>945</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/wpid-pony10.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/wpid-pony10.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>223</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131203T21:12:07</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/20131203-221158.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>20131203-221158.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>1200</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>1600</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/12/20131203-221158.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <double>2.8</double>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>1270249457</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>latitude</name>
+ <value>
+ <double>48.865166666667</double>
+ </value>
+ </member>
+ <member>
+ <name>longitude</name>
+ <value>
+ <double>2.3748333333333</double>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/12/20131203-221158.jpg?w=112</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>213</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131104T10:22:14</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>214</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/11/wpid-wp-1383559215850.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>wpid-wp-1383559215850.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>640</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>480</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/11/wpid-wp-1383559215850.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/11/wpid-wp-1383559215850.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>203</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131010T14:27:42</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>179</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-162740.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>20131010-162740.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>2048</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>1536</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/10/20131010-162740.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-162740.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>200</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131010T14:24:54</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>175</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-162454.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>20131010-162454.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>224</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>300</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/10/20131010-162454.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <double>2.4</double>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>1381422290</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <string>4.28</string>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <string>250</string>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <string>0.066666666666667</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-162454.jpg?w=112</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>199</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131010T14:24:46</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>175</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-162446.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>20131010-162446.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>150</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>112</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/10/20131010-162446.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-162446.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>attachment_id</name>
+ <value>
+ <string>188</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20131010T13:42:18</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-154156.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>20131010-154156.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>width</name>
+ <value>
+ <int>2592</int>
+ </value>
+ </member>
+ <member>
+ <name>height</name>
+ <value>
+ <int>1936</int>
+ </value>
+ </member>
+ <member>
+ <name>file</name>
+ <value>
+ <string>/home/wpcom/public_html/wp-content/blogs.dir/ebf/57991476/files/2013/10/20131010-154156.jpg</string>
+ </value>
+ </member>
+ <member>
+ <name>sizes</name>
+ <value>
+ <array>
+ <data>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <double>2.4</double>
+ </value>
+ </member>
+ <member>
+ <name>credit</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>camera</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>caption</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>created_timestamp</name>
+ <value>
+ <int>1360088720</int>
+ </value>
+ </member>
+ <member>
+ <name>copyright</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>focal_length</name>
+ <value>
+ <string>4.28</string>
+ </value>
+ </member>
+ <member>
+ <name>iso</name>
+ <value>
+ <string>800</string>
+ </value>
+ </member>
+ <member>
+ <name>shutter_speed</name>
+ <value>
+ <string>0.066666666666667</string>
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>latitude</name>
+ <value>
+ <double>48.843</double>
+ </value>
+ </member>
+ <member>
+ <name>longitude</name>
+ <value>
+ <double>2.3053333333333</double>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>thumbnail</name>
+ <value>
+ <string>http://taliwutblog.files.wordpress.com/2013/10/20131010-154156.jpg?w=150</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getOptions.json b/WordPress/src/androidTest/assets/default-wp.getOptions.json
new file mode 100644
index 000000000..674890c41
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getOptions.json
@@ -0,0 +1,32 @@
+{
+ "admin_url": {
+ "desc": "L&rsquo;adresse web de la zone d&rsquo;administration",
+ "readonly": true,
+ "value": "https://tataliwut.wordpress.com/wp-admin/"
+ },
+ "blog_public": {
+ "desc": "Acc\u00e8s aux renseignements confidentiels",
+ "readonly": true,
+ "value": "1"
+ },
+ "home_url": {
+ "desc": "Adresse web du site (URL)",
+ "readonly": true,
+ "value": "http://tataliwut.wordpress.com"
+ },
+ "login_url": {
+ "desc": "Adresse de connexion (URL)",
+ "readonly": true,
+ "value": "https://tataliwut.wordpress.com/wp-login.php"
+ },
+ "post_thumbnail": {
+ "desc": "Miniature d&rsquo;article",
+ "readonly": true,
+ "value": true
+ },
+ "software_version": {
+ "desc": "Software Version",
+ "readonly": true,
+ "value": "3.9-alpha"
+ }
+}
diff --git a/WordPress/src/androidTest/assets/default-wp.getOptions.xml b/WordPress/src/androidTest/assets/default-wp.getOptions.xml
new file mode 100644
index 000000000..254b6d5e6
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getOptions.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>home_url</name>
+ <value>
+ <struct>
+ <member>
+ <name>desc</name>
+ <value>
+ <string>Adresse web du site (URL)</string>
+ </value>
+ </member>
+ <member>
+ <name>readonly</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>http://tataliwut.wordpress.com</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>post_thumbnail</name>
+ <value>
+ <struct>
+ <member>
+ <name>desc</name>
+ <value>
+ <string>Miniature d&amp;rsquo;article</string>
+ </value>
+ </member>
+ <member>
+ <name>readonly</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>software_version</name>
+ <value>
+ <struct>
+ <member>
+ <name>desc</name>
+ <value>
+ <string>Software Version</string>
+ </value>
+ </member>
+ <member>
+ <name>readonly</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>3.8.1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>login_url</name>
+ <value>
+ <struct>
+ <member>
+ <name>desc</name>
+ <value>
+ <string>Adresse de connexion (URL)</string>
+ </value>
+ </member>
+ <member>
+ <name>readonly</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>https://tataliwut.wordpress.com/wp-login.php</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>admin_url</name>
+ <value>
+ <struct>
+ <member>
+ <name>desc</name>
+ <value>
+ <string>L&amp;rsquo;adresse web de la zone d&amp;rsquo;administration</string>
+ </value>
+ </member>
+ <member>
+ <name>readonly</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>https://tataliwut.wordpress.com/wp-admin/</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ <member>
+ <name>blog_public</name>
+ <value>
+ <struct>
+ <member>
+ <name>desc</name>
+ <value>
+ <string>Accès aux renseignements confidentiels</string>
+ </value>
+ </member>
+ <member>
+ <name>readonly</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getPostFormats.json b/WordPress/src/androidTest/assets/default-wp.getPostFormats.json
new file mode 100644
index 000000000..97329b917
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getPostFormats.json
@@ -0,0 +1 @@
+{"audio":"Son","standard":"Par défaut","status":"État","gallery":"Galerie","quote":"Citation","link":"Lien","image":"Image","chat":"Discussion","aside":"En passant","video":"Vidéo"}
diff --git a/WordPress/src/androidTest/assets/default-wp.getPostFormats.xml b/WordPress/src/androidTest/assets/default-wp.getPostFormats.xml
new file mode 100644
index 000000000..d20ae61af
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getPostFormats.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>standard</name>
+ <value>
+ <string>Par défaut</string>
+ </value>
+ </member>
+ <member>
+ <name>aside</name>
+ <value>
+ <string>En passant</string>
+ </value>
+ </member>
+ <member>
+ <name>chat</name>
+ <value>
+ <string>Discussion</string>
+ </value>
+ </member>
+ <member>
+ <name>gallery</name>
+ <value>
+ <string>Galerie</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>Lien</string>
+ </value>
+ </member>
+ <member>
+ <name>image</name>
+ <value>
+ <string>Image</string>
+ </value>
+ </member>
+ <member>
+ <name>quote</name>
+ <value>
+ <string>Citation</string>
+ </value>
+ </member>
+ <member>
+ <name>status</name>
+ <value>
+ <string>Statut</string>
+ </value>
+ </member>
+ <member>
+ <name>video</name>
+ <value>
+ <string>Vidéo</string>
+ </value>
+ </member>
+ <member>
+ <name>audio</name>
+ <value>
+ <string>Son</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getProfile.json b/WordPress/src/androidTest/assets/default-wp.getProfile.json
new file mode 100644
index 000000000..9382d7632
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getProfile.json
@@ -0,0 +1 @@
+{"display_name":"taliwutt","first_name":"","username":"taliwutt","bio":"","nickname":"taliwutt","email":"taliwut@gmail.com","registered":"Sep 17, 2013 11:13:54 AM","roles":["administrator"],"last_name":"","nicename":"taliwutt","user_id":"55434822","url":"http://taliwutblog.wordpress.com"}
diff --git a/WordPress/src/androidTest/assets/default-wp.getProfile.xml b/WordPress/src/androidTest/assets/default-wp.getProfile.xml
new file mode 100644
index 000000000..a3e178909
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getProfile.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>user_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>username</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>first_name</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>last_name</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>registered</name>
+ <value>
+ <dateTime.iso8601>20130917T11:13:54</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>bio</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>email</name>
+ <value>
+ <string>taliwut@gmail.com</string>
+ </value>
+ </member>
+ <member>
+ <name>nickname</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>nicename</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>url</name>
+ <value>
+ <string>http://taliwutblog.wordpress.com</string>
+ </value>
+ </member>
+ <member>
+ <name>display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>roles</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>administrator</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/default-wp.getUsersBlogs.json b/WordPress/src/androidTest/assets/default-wp.getUsersBlogs.json
new file mode 100644
index 000000000..6ef68d55b
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getUsersBlogs.json
@@ -0,0 +1,20 @@
+[{"blogName":"taliwutblog","xmlrpc":"https://taliwutblog.wordpress.com/xmlrpc.php",
+ "blogid":"57991476",
+ "isAdmin":true,
+ "url":"https://taliwutblog.wordpress.com/"
+ },
+ {
+ "blogName":"Empty blog stays empty",
+ "xmlrpc":"https://tataliwut.wordpress.com/xmlrpc.php",
+ "blogid":"59073674",
+ "isAdmin":true,
+ "url":"https://tataliwut.wordpress.com/"
+ },
+ {
+ "blogName":"I Love Cute Cats",
+ "xmlrpc":"https://ilovecutecats.wordpress.com/xmlrpc.php",
+ "blogid":"61784930",
+ "isAdmin":true,
+ "url":"https://ilovecutecats.wordpress.com/"
+ }
+]
diff --git a/WordPress/src/androidTest/assets/default-wp.getUsersBlogs.xml b/WordPress/src/androidTest/assets/default-wp.getUsersBlogs.xml
new file mode 100644
index 000000000..350ca6ce1
--- /dev/null
+++ b/WordPress/src/androidTest/assets/default-wp.getUsersBlogs.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>isAdmin</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>url</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/</string>
+ </value>
+ </member>
+ <member>
+ <name>blogid</name>
+ <value>
+ <string>57991476</string>
+ </value>
+ </member>
+ <member>
+ <name>blogName</name>
+ <value>
+ <string>taliwut &amp;amp; blog</string>
+ </value>
+ </member>
+ <member>
+ <name>xmlrpc</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/xmlrpc.php</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>isAdmin</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>url</name>
+ <value>
+ <string>https://tataliwut.wordpress.com/</string>
+ </value>
+ </member>
+ <member>
+ <name>blogid</name>
+ <value>
+ <string>59073674</string>
+ </value>
+ </member>
+ <member>
+ <name>blogName</name>
+ <value>
+ <string>Empty blog stays empty</string>
+ </value>
+ </member>
+ <member>
+ <name>xmlrpc</name>
+ <value>
+ <string>https://tataliwut.wordpress.com/xmlrpc.php</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ <value>
+ <struct>
+ <member>
+ <name>isAdmin</name>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </member>
+ <member>
+ <name>url</name>
+ <value>
+ <string>https://ilovecutecats.wordpress.com/</string>
+ </value>
+ </member>
+ <member>
+ <name>blogid</name>
+ <value>
+ <string>61784930</string>
+ </value>
+ </member>
+ <member>
+ <name>blogName</name>
+ <value>
+ <string>I Love Cute Cats</string>
+ </value>
+ </member>
+ <member>
+ <name>xmlrpc</name>
+ <value>
+ <string>https://ilovecutecats.wordpress.com/xmlrpc.php</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/email-exists-public-api-wordpress-com-rest-v1-users-new.json b/WordPress/src/androidTest/assets/email-exists-public-api-wordpress-com-rest-v1-users-new.json
new file mode 100644
index 000000000..3bcd20ab8
--- /dev/null
+++ b/WordPress/src/androidTest/assets/email-exists-public-api-wordpress-com-rest-v1-users-new.json
@@ -0,0 +1,4 @@
+{
+ "error": "email_exists",
+ "message": "Invalid email input"
+}
diff --git a/WordPress/src/androidTest/assets/empty-wp.getProfile.json b/WordPress/src/androidTest/assets/empty-wp.getProfile.json
new file mode 100644
index 000000000..0637a088a
--- /dev/null
+++ b/WordPress/src/androidTest/assets/empty-wp.getProfile.json
@@ -0,0 +1 @@
+[] \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/empty_tables.sql b/WordPress/src/androidTest/assets/empty_tables.sql
new file mode 100644
index 000000000..7f7ec34b3
--- /dev/null
+++ b/WordPress/src/androidTest/assets/empty_tables.sql
@@ -0,0 +1,16 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE android_metadata (locale TEXT);
+INSERT INTO "android_metadata" VALUES('fr_FR');
+CREATE TABLE accounts (id integer primary key autoincrement, url text, blogName text, username text, password text, imagePlacement text, centerThumbnail boolean, fullSizeImage boolean, maxImageWidth text, maxImageWidthId integer, lastCommentId integer, runService boolean, blogId integer, location boolean default false, dotcom_username text, dotcom_password text, api_key text, api_blogid text, dotcomFlag boolean default false, wpVersion text, httpuser text, httppassword text, postFormats text default '', isScaledImage boolean default false, scaledImgWidth integer default 1024, homeURL text default '', blog_options text default '', isAdmin boolean default false);
+
+CREATE TABLE posts (id integer primary key autoincrement, blogID text, postid text, title text default '', dateCreated date, date_created_gmt date, categories text default '', custom_fields text default '', description text default '', link text default '', mt_allow_comments boolean, mt_allow_pings boolean, mt_excerpt text default '', mt_keywords text default '', mt_text_more text default '', permaLink text default '', post_status text default '', userid integer default 0, wp_author_display_name text default '', wp_author_id text default '', wp_password text default '', wp_post_format text default '', wp_slug text default '', mediaPaths text default '', latitude real, longitude real, localDraft boolean default 0, uploaded boolean default 0, isPage boolean default 0, wp_page_parent_id text, wp_page_parent_title text, isLocalChange boolean default 0);
+CREATE TABLE comments (blogID text, postID text, iCommentID integer, author text, comment text, commentDate text, commentDateFormatted text, status text, url text, email text, postTitle text);
+CREATE TABLE cats (id integer primary key autoincrement, blog_id text, wp_id integer, category_name text not null, parent_id integer default 0);
+CREATE TABLE quickpress_shortcuts (id integer primary key autoincrement, accountId text, name text);
+CREATE TABLE media (id integer primary key autoincrement, postID integer not null, filePath text default '', fileName text default '', title text default '', description text default '', caption text default '', horizontalAlignment integer default 0, width integer default 0, height integer default 0, mimeType text default '', featured boolean default false, isVideo boolean default false, isFeaturedInPost boolean default false, fileURL text default '', thumbnailURL text default '', mediaId text default '', blogId text default '', date_created_gmt date, uploadState default '');
+CREATE TABLE themes (_id integer primary key autoincrement, themeId text, name text, description text, screenshotURL text, trendingRank integer default 0, popularityRank integer default 0, launchDate date, previewURL text, blogId text, isCurrent boolean default false, isPremium boolean default false, features text);
+CREATE TABLE notes (id integer primary key, note_id text, message text, type text, raw_note_data text, timestamp integer, placeholder boolean);
+DELETE FROM sqlite_sequence;
+INSERT INTO "sqlite_sequence" VALUES('accounts',3);
+COMMIT;
diff --git a/WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json b/WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json
new file mode 100644
index 000000000..49cb953e8
--- /dev/null
+++ b/WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json
@@ -0,0 +1,229 @@
+{
+ "testcases": [
+ {
+ "comment": "testing empty url",
+ "realm": "URL_CANONICALIZATION",
+ "setup": {
+ "input": {
+ "siteUrl": ""
+ },
+ "output": {
+ "error": {
+ "type": "SITE_URL_CANNOT_BE_EMPTY",
+ "message": "Check that the site URL entered is valid"
+ }
+ }
+ }
+ },
+ {
+ "comment": "testing whitespace url",
+ "realm": "URL_CANONICALIZATION",
+ "setup": {
+ "input": {
+ "siteUrl": " "
+ },
+ "output": {
+ "error": {
+ "type": "SITE_URL_CANNOT_BE_EMPTY",
+ "message": "Check that the site URL entered is valid"
+ }
+ }
+ }
+ },
+ {
+ "comment": "testing null url",
+ "realm": "URL_CANONICALIZATION",
+ "setup": {
+ "input": {
+ "siteUrl": null
+ },
+ "output": {
+ "error": {
+ "type": "SITE_URL_CANNOT_BE_EMPTY",
+ "message": "Check that the site URL entered is valid"
+ }
+ }
+ }
+ },
+ {
+ "comment": "testing padding whitespace",
+ "realm": "URL_CANONICALIZATION",
+ "setup": {
+ "input": {
+ "siteUrl": " \t http://wordpress.com \t "
+ },
+ "output": {
+ "siteUrl": "http://wordpress.com"
+ }
+ }
+ },
+ {
+ "comment": "testing xmlrpc.php missing",
+ "realm": "XMLPRC_DISCOVERY",
+ "setup": {
+ "input": {
+ "serverMock": [
+ {
+ "request": {
+ "method": "GET",
+ "path": "/"
+ },
+ "response": {
+ "statusCode": 404
+ }
+ },
+ {
+ "request": {
+ "method": "GET",
+ "path": "/xmlrpc.php"
+ },
+ "response": {
+ "statusCode": 404
+ }
+ },
+ {
+ "request": {
+ "method": "POST",
+ "path": "/"
+ },
+ "response": {
+ "statusCode": 404
+ }
+ },
+ {
+ "request": {
+ "method": "POST",
+ "path": "/xmlrpc.php"
+ },
+ "response": {
+ "statusCode": 404
+ }
+ }
+ ]
+ },
+ "output": {
+ "error": {
+ "type": "XMLRPC_MISSING",
+ "message": "Couldn't connect to the WordPress site"
+ }
+ }
+ }
+ },
+ {
+ "comment": "testing xmlrpc.php found",
+ "realm": "XMLPRC_DISCOVERY",
+ "setup": {
+ "input": {
+ "serverMock": [
+ {
+ "request": {
+ "method": "POST",
+ "path": "/xmlrpc.php"
+ },
+ "response": {
+ "statusCode": 200,
+ "body": "asset:listMethodsResponse.xml"
+ }
+ }
+ ]
+ },
+ "output": {
+ "xmlrpcEndpoint": "http://mockserver/xmlrpc.php"
+ }
+ }
+ },
+ {
+ "comment": "testing xmlrpc.php discovered after redirect",
+ "realm": "XMLPRC_DISCOVERY",
+ "setup": {
+ "input": {
+ "siteUrl": "http://mockserver/wp",
+ "serverMock": [
+ {
+ "request": {
+ "method": "GET",
+ "path": "/wp"
+ },
+ "response": {
+ "statusCode": 301,
+ "headers": {
+ "Location": "/wpnew/"
+ },
+ "body": "Page has moved!"
+ }
+ },
+ {
+ "request": {
+ "method": "POST",
+ "path": "/wp"
+ },
+ "response": {
+ "statusCode": 307,
+ "headers": {
+ "Location": "/wpnew/"
+ },
+ "body": "Page has moved! POST to new address!"
+ }
+ },
+ {
+ "request": {
+ "method": "POST",
+ "path": "/wp/xmlrpc.php"
+ },
+ "response": {
+ "statusCode": 307,
+ "headers": {
+ "Location": "/wpnew/xmlrpc.php"
+ },
+ "body": "Page has moved! POST to new address!"
+ }
+ },
+ {
+ "request": {
+ "method": "GET",
+ "path": "/wpnew/"
+ },
+ "response": {
+ "statusCode": 200,
+ "body": "asset:index_with_redirect.html"
+ }
+ },
+ {
+ "request": {
+ "method": "GET",
+ "path": "/wpnew/xmlrpc.php?rsd"
+ },
+ "response": {
+ "statusCode": 200,
+ "body": "asset:rsd_with_redirect.xml"
+ }
+ },
+ {
+ "request": {
+ "method": "GET",
+ "path": "/wpnew/xmlrpc.php"
+ },
+ "response": {
+ "statusCode": 405,
+ "body": "XML-RPC server accepts POST requests only."
+ }
+ },
+ {
+ "request": {
+ "method": "POST",
+ "path": "/wpnew/xmlrpc.php"
+ },
+ "response": {
+ "statusCode": 200,
+ "body": "asset:listMethodsResponse.xml"
+ }
+ }
+ ]
+ },
+ "output": {
+ "xmlrpcEndpoint": "http://mockserver/wpnew/xmlrpc.php"
+ }
+ }
+ }
+ ]
+}
diff --git a/WordPress/src/androidTest/assets/health-check/index.html b/WordPress/src/androidTest/assets/health-check/index.html
new file mode 100644
index 000000000..e4c7f314b
--- /dev/null
+++ b/WordPress/src/androidTest/assets/health-check/index.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html lang="en-US" class="no-js">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width">
+ <link rel="profile" href="http://gmpg.org/xfn/11">
+ <link rel="pingback" href="http://mockserver/xmlrpc.php">
+ <!--[if lt IE 9]>
+ <script src="http://mockserver/wp-content/themes/twentyfifteen/js/html5.js"></script>
+ <![endif]-->
+ <script>(function(html){html.className = html.className.replace(/\bno-js\b/,'js')})(document.documentElement);</script>
+ <title>wplogin &#8211; Just another WordPress site</title>
+ <meta name='robots' content='noindex,follow' />
+ <link rel="alternate" type="application/rss+xml" title="wplogin &raquo; Feed" href="http://mockserver/feed/" />
+ <link rel="alternate" type="application/rss+xml" title="wplogin &raquo; Comments Feed" href="http://mockserver/comments/feed/" />
+ <script type="text/javascript">
+ window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/72x72\/","ext":".png","source":{"concatemoji":"https:\/\/mockserver\/wp-includes\/js\/wp-emoji-release.min.js?ver=4.4.2"}};
+ !function(a,b,c){function d(a){var c,d=b.createElement("canvas"),e=d.getContext&&d.getContext("2d"),f=String.fromCharCode;return e&&e.fillText?(e.textBaseline="top",e.font="600 32px Arial","flag"===a?(e.fillText(f(55356,56806,55356,56826),0,0),d.toDataURL().length>3e3):"diversity"===a?(e.fillText(f(55356,57221),0,0),c=e.getImageData(16,16,1,1).data.toString(),e.fillText(f(55356,57221,55356,57343),0,0),c!==e.getImageData(16,16,1,1).data.toString()):("simple"===a?e.fillText(f(55357,56835),0,0):e.fillText(f(55356,57135),0,0),0!==e.getImageData(16,16,1,1).data[0])):!1}function e(a){var c=b.createElement("script");c.src=a,c.type="text/javascript",b.getElementsByTagName("head")[0].appendChild(c)}var f,g;c.supports={simple:d("simple"),flag:d("flag"),unicode8:d("unicode8"),diversity:d("diversity")},c.DOMReady=!1,c.readyCallback=function(){c.DOMReady=!0},c.supports.simple&&c.supports.flag&&c.supports.unicode8&&c.supports.diversity||(g=function(){c.readyCallback()},b.addEventListener?(b.addEventListener("DOMContentLoaded",g,!1),a.addEventListener("load",g,!1)):(a.attachEvent("onload",g),b.attachEvent("onreadystatechange",function(){"complete"===b.readyState&&c.readyCallback()})),f=c.source||{},f.concatemoji?e(f.concatemoji):f.wpemoji&&f.twemoji&&(e(f.twemoji),e(f.wpemoji)))}(window,document,window._wpemojiSettings);
+ </script>
+ <style type="text/css">
+ img.wp-smiley,
+ img.emoji {
+ display: inline !important;
+ border: none !important;
+ box-shadow: none !important;
+ height: 1em !important;
+ width: 1em !important;
+ margin: 0 .07em !important;
+ vertical-align: -0.1em !important;
+ background: none !important;
+ padding: 0 !important;
+ }
+ </style>
+ <link rel='stylesheet' id='twentyfifteen-fonts-css' href='https://fonts.googleapis.com/css?family=Noto+Sans%3A400italic%2C700italic%2C400%2C700%7CNoto+Serif%3A400italic%2C700italic%2C400%2C700%7CInconsolata%3A400%2C700&#038;subset=latin%2Clatin-ext' type='text/css' media='all' />
+ <link rel='stylesheet' id='genericons-css' href='http://mockserver/wp-content/themes/twentyfifteen/genericons/genericons.css?ver=3.2' type='text/css' media='all' />
+ <link rel='stylesheet' id='twentyfifteen-style-css' href='http://mockserver/wp-content/themes/twentyfifteen/style.css?ver=4.4.2' type='text/css' media='all' />
+ <!--[if lt IE 9]>
+ <link rel='stylesheet' id='twentyfifteen-ie-css' href='http://mockserver/wp-content/themes/twentyfifteen/css/ie.css?ver=20141010' type='text/css' media='all' />
+ <![endif]-->
+ <!--[if lt IE 8]>
+ <link rel='stylesheet' id='twentyfifteen-ie7-css' href='http://mockserver/wp-content/themes/twentyfifteen/css/ie7.css?ver=20141010' type='text/css' media='all' />
+ <![endif]-->
+ <script type='text/javascript' src='http://mockserver/wp-includes/js/jquery/jquery.js?ver=1.11.3'></script>
+ <script type='text/javascript' src='http://mockserver/wp-includes/js/jquery/jquery-migrate.min.js?ver=1.2.1'></script>
+ <link rel='https://api.w.org/' href='http://mockserver/wp-json/' />
+ <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://mockserver/xmlrpc.php?rsd" />
+ <link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://mockserver/wp-includes/wlwmanifest.xml" />
+ <meta name="generator" content="WordPress 4.4.2" />
+ <style type="text/css">.recentcomments a{display:inline !important;padding:0 !important;margin:0 !important;}</style>
+ <script type='text/javascript' src='http://mockserver/wp-includes/js/tw-sack.min.js?ver=1.6.1'></script>
+</head>
+
+<body class="home blog">
+<div id="page" class="hfeed site">
+ <a class="skip-link screen-reader-text" href="#content">Skip to content</a>
+
+ <div id="sidebar" class="sidebar">
+ <header id="masthead" class="site-header" role="banner">
+ <div class="site-branding">
+ <h1 class="site-title"><a href="http://mockserver/" rel="home">wplogin</a></h1>
+ <p class="site-description">Just another WordPress site</p>
+ <button class="secondary-toggle">Menu and widgets</button>
+ </div><!-- .site-branding -->
+ </header><!-- .site-header -->
+
+ <div id="secondary" class="secondary">
+
+
+
+ <div id="widget-area" class="widget-area" role="complementary">
+ <aside id="search-2" class="widget widget_search"><form role="search" method="get" class="search-form" action="http://mockserver/">
+ <label>
+ <span class="screen-reader-text">Search for:</span>
+ <input type="search" class="search-field" placeholder="Search &hellip;" value="" name="s" title="Search for:" />
+ </label>
+ <input type="submit" class="search-submit screen-reader-text" value="Search" />
+ </form></aside> <aside id="recent-posts-2" class="widget widget_recent_entries"> <h2 class="widget-title">Recent Posts</h2> <ul>
+ <li>
+ <a href="http://mockserver/2016/03/03/hello-world/">Hello world!</a>
+ </li>
+ </ul>
+ </aside> <aside id="recent-comments-2" class="widget widget_recent_comments"><h2 class="widget-title">Recent Comments</h2><ul id="recentcomments"><li class="recentcomments"><span class="comment-author-link"><a href='https://wordpress.org/' rel='external nofollow' class='url'>Mr WordPress</a></span> on <a href="http://mockserver/2016/03/03/hello-world/#comment-1">Hello world!</a></li></ul></aside><aside id="archives-2" class="widget widget_archive"><h2 class="widget-title">Archives</h2> <ul>
+ <li><a href='http://mockserver/2016/03/'>March 2016</a></li>
+ </ul>
+ </aside><aside id="categories-2" class="widget widget_categories"><h2 class="widget-title">Categories</h2> <ul>
+ <li class="cat-item cat-item-1"><a href="http://mockserver/category/uncategorized/" >Uncategorized</a>
+ </li>
+ </ul>
+ </aside><aside id="meta-2" class="widget widget_meta"><h2 class="widget-title">Meta</h2> <ul>
+ <li><a href="http://mockserver/wp-login.php">Log in</a></li>
+ <li><a href="http://mockserver/feed/">Entries <abbr title="Really Simple Syndication">RSS</abbr></a></li>
+ <li><a href="http://mockserver/comments/feed/">Comments <abbr title="Really Simple Syndication">RSS</abbr></a></li>
+ <li><a href="https://wordpress.org/" title="Powered by WordPress, state-of-the-art semantic personal publishing platform.">WordPress.org</a></li> </ul>
+ </aside> </div><!-- .widget-area -->
+
+ </div><!-- .secondary -->
+
+ </div><!-- .sidebar -->
+
+ <div id="content" class="site-content">
+
+ <div id="primary" class="content-area">
+ <main id="main" class="site-main" role="main">
+
+
+
+
+ <article id="post-1" class="post-1 post type-post status-publish format-standard hentry category-uncategorized">
+
+ <header class="entry-header">
+ <h2 class="entry-title"><a href="http://mockserver/2016/03/03/hello-world/" rel="bookmark">Hello world!</a></h2> </header><!-- .entry-header -->
+
+ <div class="entry-content">
+ <p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
+ </div><!-- .entry-content -->
+
+
+ <footer class="entry-footer">
+ <span class="posted-on"><span class="screen-reader-text">Posted on </span><a href="http://mockserver/2016/03/03/hello-world/" rel="bookmark"><time class="entry-date published updated" datetime="2016-03-03T10:07:52+00:00">March 3, 2016</time></a></span><span class="comments-link"><a href="http://mockserver/2016/03/03/hello-world/#comments">1 Comment<span class="screen-reader-text"> on Hello world!</span></a></span> </footer><!-- .entry-footer -->
+
+ </article><!-- #post-## -->
+
+ </main><!-- .site-main -->
+ </div><!-- .content-area -->
+
+
+ </div><!-- .site-content -->
+
+ <footer id="colophon" class="site-footer" role="contentinfo">
+ <div class="site-info">
+ <a href="https://wordpress.org/">Proudly powered by WordPress</a>
+ </div><!-- .site-info -->
+ </footer><!-- .site-footer -->
+
+</div><!-- .site -->
+
+<script type='text/javascript' src='http://mockserver/wp-content/themes/twentyfifteen/js/skip-link-focus-fix.js?ver=20141010'></script>
+<script type='text/javascript'>
+/* <![CDATA[ */
+var screenReaderText = {"expand":"<span class=\"screen-reader-text\">expand child menu<\/span>","collapse":"<span class=\"screen-reader-text\">collapse child menu<\/span>"};
+/* ]]> */
+</script>
+<script type='text/javascript' src='http://mockserver/wp-content/themes/twentyfifteen/js/functions.js?ver=20150330'></script>
+<script type='text/javascript' src='http://mockserver/wp-includes/js/wp-embed.min.js?ver=4.4.2'></script>
+<script type="text/javascript">
+/* <![CDATA[ */
+jQuery(document).ready( function($) {
+ $("ul.menu").not(":has(li)").closest('div').prev('h3.widget-title').hide();
+});
+/* ]]> */
+</script>
+</body>
+</html>
diff --git a/WordPress/src/androidTest/assets/health-check/index_with_redirect.html b/WordPress/src/androidTest/assets/health-check/index_with_redirect.html
new file mode 100644
index 000000000..7c5ea729e
--- /dev/null
+++ b/WordPress/src/androidTest/assets/health-check/index_with_redirect.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html lang="en-US" class="no-js">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width">
+ <link rel="profile" href="http://gmpg.org/xfn/11">
+ <link rel="pingback" href="http://mockserver/wpnew/xmlrpc.php">
+ <!--[if lt IE 9]>
+ <script src="http://mockserver/wpnew/wp-content/themes/twentyfifteen/js/html5.js"></script>
+ <![endif]-->
+ <script>(function(html){html.className = html.className.replace(/\bno-js\b/,'js')})(document.documentElement);</script>
+ <title>wplogin &#8211; Just another WordPress site</title>
+ <meta name='robots' content='noindex,follow' />
+ <link rel="alternate" type="application/rss+xml" title="wplogin &raquo; Feed" href="http://mockserver/wpnew/feed/" />
+ <link rel="alternate" type="application/rss+xml" title="wplogin &raquo; Comments Feed" href="http://mockserver/wpnew/comments/feed/" />
+ <script type="text/javascript">
+ window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/72x72\/","ext":".png","source":{"concatemoji":"https:\/\/mockserver/api\/wp-includes\/js\/wp-emoji-release.min.js?ver=4.4.2"}};
+ !function(a,b,c){function d(a){var c,d=b.createElement("canvas"),e=d.getContext&&d.getContext("2d"),f=String.fromCharCode;return e&&e.fillText?(e.textBaseline="top",e.font="600 32px Arial","flag"===a?(e.fillText(f(55356,56806,55356,56826),0,0),d.toDataURL().length>3e3):"diversity"===a?(e.fillText(f(55356,57221),0,0),c=e.getImageData(16,16,1,1).data.toString(),e.fillText(f(55356,57221,55356,57343),0,0),c!==e.getImageData(16,16,1,1).data.toString()):("simple"===a?e.fillText(f(55357,56835),0,0):e.fillText(f(55356,57135),0,0),0!==e.getImageData(16,16,1,1).data[0])):!1}function e(a){var c=b.createElement("script");c.src=a,c.type="text/javascript",b.getElementsByTagName("head")[0].appendChild(c)}var f,g;c.supports={simple:d("simple"),flag:d("flag"),unicode8:d("unicode8"),diversity:d("diversity")},c.DOMReady=!1,c.readyCallback=function(){c.DOMReady=!0},c.supports.simple&&c.supports.flag&&c.supports.unicode8&&c.supports.diversity||(g=function(){c.readyCallback()},b.addEventListener?(b.addEventListener("DOMContentLoaded",g,!1),a.addEventListener("load",g,!1)):(a.attachEvent("onload",g),b.attachEvent("onreadystatechange",function(){"complete"===b.readyState&&c.readyCallback()})),f=c.source||{},f.concatemoji?e(f.concatemoji):f.wpemoji&&f.twemoji&&(e(f.twemoji),e(f.wpemoji)))}(window,document,window._wpemojiSettings);
+ </script>
+ <style type="text/css">
+ img.wp-smiley,
+ img.emoji {
+ display: inline !important;
+ border: none !important;
+ box-shadow: none !important;
+ height: 1em !important;
+ width: 1em !important;
+ margin: 0 .07em !important;
+ vertical-align: -0.1em !important;
+ background: none !important;
+ padding: 0 !important;
+ }
+ </style>
+ <link rel='stylesheet' id='twentyfifteen-fonts-css' href='https://fonts.googleapis.com/css?family=Noto+Sans%3A400italic%2C700italic%2C400%2C700%7CNoto+Serif%3A400italic%2C700italic%2C400%2C700%7CInconsolata%3A400%2C700&#038;subset=latin%2Clatin-ext' type='text/css' media='all' />
+ <link rel='stylesheet' id='genericons-css' href='http://mockserver/wpnew/wp-content/themes/twentyfifteen/genericons/genericons.css?ver=3.2' type='text/css' media='all' />
+ <link rel='stylesheet' id='twentyfifteen-style-css' href='http://mockserver/wpnew/wp-content/themes/twentyfifteen/style.css?ver=4.4.2' type='text/css' media='all' />
+ <!--[if lt IE 9]>
+ <link rel='stylesheet' id='twentyfifteen-ie-css' href='http://mockserver/wpnew/wp-content/themes/twentyfifteen/css/ie.css?ver=20141010' type='text/css' media='all' />
+ <![endif]-->
+ <!--[if lt IE 8]>
+ <link rel='stylesheet' id='twentyfifteen-ie7-css' href='http://mockserver/wpnew/wp-content/themes/twentyfifteen/css/ie7.css?ver=20141010' type='text/css' media='all' />
+ <![endif]-->
+ <script type='text/javascript' src='http://mockserver/wpnew/wp-includes/js/jquery/jquery.js?ver=1.11.3'></script>
+ <script type='text/javascript' src='http://mockserver/wpnew/wp-includes/js/jquery/jquery-migrate.min.js?ver=1.2.1'></script>
+ <link rel='https://api.w.org/' href='http://mockserver/wpnew/wp-json/' />
+ <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://mockserver/wpnew/xmlrpc.php?rsd" />
+ <link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://mockserver/wpnew/wp-includes/wlwmanifest.xml" />
+ <meta name="generator" content="WordPress 4.4.2" />
+ <style type="text/css">.recentcomments a{display:inline !important;padding:0 !important;margin:0 !important;}</style>
+ <script type='text/javascript' src='http://mockserver/wpnew/wp-includes/js/tw-sack.min.js?ver=1.6.1'></script>
+</head>
+
+<body class="home blog">
+<div id="page" class="hfeed site">
+ <a class="skip-link screen-reader-text" href="#content">Skip to content</a>
+
+ <div id="sidebar" class="sidebar">
+ <header id="masthead" class="site-header" role="banner">
+ <div class="site-branding">
+ <h1 class="site-title"><a href="http://mockserver/wpnew/" rel="home">wplogin</a></h1>
+ <p class="site-description">Just another WordPress site</p>
+ <button class="secondary-toggle">Menu and widgets</button>
+ </div><!-- .site-branding -->
+ </header><!-- .site-header -->
+
+ <div id="secondary" class="secondary">
+
+
+
+ <div id="widget-area" class="widget-area" role="complementary">
+ <aside id="search-2" class="widget widget_search"><form role="search" method="get" class="search-form" action="http://mockserver/wpnew/">
+ <label>
+ <span class="screen-reader-text">Search for:</span>
+ <input type="search" class="search-field" placeholder="Search &hellip;" value="" name="s" title="Search for:" />
+ </label>
+ <input type="submit" class="search-submit screen-reader-text" value="Search" />
+ </form></aside> <aside id="recent-posts-2" class="widget widget_recent_entries"> <h2 class="widget-title">Recent Posts</h2> <ul>
+ <li>
+ <a href="http://mockserver/wpnew/2016/03/03/hello-world/">Hello world!</a>
+ </li>
+ </ul>
+ </aside> <aside id="recent-comments-2" class="widget widget_recent_comments"><h2 class="widget-title">Recent Comments</h2><ul id="recentcomments"><li class="recentcomments"><span class="comment-author-link"><a href='https://wordpress.org/' rel='external nofollow' class='url'>Mr WordPress</a></span> on <a href="http://mockserver/wpnew/2016/03/03/hello-world/#comment-1">Hello world!</a></li></ul></aside><aside id="archives-2" class="widget widget_archive"><h2 class="widget-title">Archives</h2> <ul>
+ <li><a href='http://mockserver/wpnew/2016/03/'>March 2016</a></li>
+ </ul>
+ </aside><aside id="categories-2" class="widget widget_categories"><h2 class="widget-title">Categories</h2> <ul>
+ <li class="cat-item cat-item-1"><a href="http://mockserver/wpnew/category/uncategorized/" >Uncategorized</a>
+ </li>
+ </ul>
+ </aside><aside id="meta-2" class="widget widget_meta"><h2 class="widget-title">Meta</h2> <ul>
+ <li><a href="http://mockserver/wpnew/wp-login.php">Log in</a></li>
+ <li><a href="http://mockserver/wpnew/feed/">Entries <abbr title="Really Simple Syndication">RSS</abbr></a></li>
+ <li><a href="http://mockserver/wpnew/comments/feed/">Comments <abbr title="Really Simple Syndication">RSS</abbr></a></li>
+ <li><a href="https://wordpress.org/" title="Powered by WordPress, state-of-the-art semantic personal publishing platform.">WordPress.org</a></li> </ul>
+ </aside> </div><!-- .widget-area -->
+
+ </div><!-- .secondary -->
+
+ </div><!-- .sidebar -->
+
+ <div id="content" class="site-content">
+
+ <div id="primary" class="content-area">
+ <main id="main" class="site-main" role="main">
+
+
+
+
+ <article id="post-1" class="post-1 post type-post status-publish format-standard hentry category-uncategorized">
+
+ <header class="entry-header">
+ <h2 class="entry-title"><a href="http://mockserver/wpnew/2016/03/03/hello-world/" rel="bookmark">Hello world!</a></h2> </header><!-- .entry-header -->
+
+ <div class="entry-content">
+ <p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
+ </div><!-- .entry-content -->
+
+
+ <footer class="entry-footer">
+ <span class="posted-on"><span class="screen-reader-text">Posted on </span><a href="http://mockserver/wpnew/2016/03/03/hello-world/" rel="bookmark"><time class="entry-date published updated" datetime="2016-03-03T10:07:52+00:00">March 3, 2016</time></a></span><span class="comments-link"><a href="http://mockserver/wpnew/2016/03/03/hello-world/#comments">1 Comment<span class="screen-reader-text"> on Hello world!</span></a></span> </footer><!-- .entry-footer -->
+
+ </article><!-- #post-## -->
+
+ </main><!-- .site-main -->
+ </div><!-- .content-area -->
+
+
+ </div><!-- .site-content -->
+
+ <footer id="colophon" class="site-footer" role="contentinfo">
+ <div class="site-info">
+ <a href="https://wordpress.org/">Proudly powered by WordPress</a>
+ </div><!-- .site-info -->
+ </footer><!-- .site-footer -->
+
+</div><!-- .site -->
+
+<script type='text/javascript' src='http://mockserver/wpnew/wp-content/themes/twentyfifteen/js/skip-link-focus-fix.js?ver=20141010'></script>
+<script type='text/javascript'>
+/* <![CDATA[ */
+var screenReaderText = {"expand":"<span class=\"screen-reader-text\">expand child menu<\/span>","collapse":"<span class=\"screen-reader-text\">collapse child menu<\/span>"};
+/* ]]> */
+</script>
+<script type='text/javascript' src='http://mockserver/wpnew/wp-content/themes/twentyfifteen/js/functions.js?ver=20150330'></script>
+<script type='text/javascript' src='http://mockserver/wpnew/wp-includes/js/wp-embed.min.js?ver=4.4.2'></script>
+<script type="text/javascript">
+/* <![CDATA[ */
+jQuery(document).ready( function($) {
+ $("ul.menu").not(":has(li)").closest('div').prev('h3.widget-title').hide();
+});
+/* ]]> */
+</script>
+</body>
+</html>
diff --git a/WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml b/WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml
new file mode 100644
index 000000000..d7b363def
--- /dev/null
+++ b/WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array><data>
+ <value><string>system.multicall</string></value>
+ <value><string>system.listMethods</string></value>
+ <value><string>system.getCapabilities</string></value>
+ <value><string>demo.addTwoNumbers</string></value>
+ <value><string>demo.sayHello</string></value>
+ <value><string>pingback.extensions.getPingbacks</string></value>
+ <value><string>pingback.ping</string></value>
+ <value><string>mt.publishPost</string></value>
+ <value><string>mt.getTrackbackPings</string></value>
+ <value><string>mt.supportedTextFilters</string></value>
+ <value><string>mt.supportedMethods</string></value>
+ <value><string>mt.setPostCategories</string></value>
+ <value><string>mt.getPostCategories</string></value>
+ <value><string>mt.getRecentPostTitles</string></value>
+ <value><string>mt.getCategoryList</string></value>
+ <value><string>metaWeblog.getUsersBlogs</string></value>
+ <value><string>metaWeblog.deletePost</string></value>
+ <value><string>metaWeblog.newMediaObject</string></value>
+ <value><string>metaWeblog.getCategories</string></value>
+ <value><string>metaWeblog.getRecentPosts</string></value>
+ <value><string>metaWeblog.getPost</string></value>
+ <value><string>metaWeblog.editPost</string></value>
+ <value><string>metaWeblog.newPost</string></value>
+ <value><string>blogger.deletePost</string></value>
+ <value><string>blogger.editPost</string></value>
+ <value><string>blogger.newPost</string></value>
+ <value><string>blogger.getRecentPosts</string></value>
+ <value><string>blogger.getPost</string></value>
+ <value><string>blogger.getUserInfo</string></value>
+ <value><string>blogger.getUsersBlogs</string></value>
+ <value><string>wp.restoreRevision</string></value>
+ <value><string>wp.getRevisions</string></value>
+ <value><string>wp.getPostTypes</string></value>
+ <value><string>wp.getPostType</string></value>
+ <value><string>wp.getPostFormats</string></value>
+ <value><string>wp.getMediaLibrary</string></value>
+ <value><string>wp.getMediaItem</string></value>
+ <value><string>wp.getCommentStatusList</string></value>
+ <value><string>wp.newComment</string></value>
+ <value><string>wp.editComment</string></value>
+ <value><string>wp.deleteComment</string></value>
+ <value><string>wp.getComments</string></value>
+ <value><string>wp.getComment</string></value>
+ <value><string>wp.setOptions</string></value>
+ <value><string>wp.getOptions</string></value>
+ <value><string>wp.getPageTemplates</string></value>
+ <value><string>wp.getPageStatusList</string></value>
+ <value><string>wp.getPostStatusList</string></value>
+ <value><string>wp.getCommentCount</string></value>
+ <value><string>wp.deleteFile</string></value>
+ <value><string>wp.uploadFile</string></value>
+ <value><string>wp.suggestCategories</string></value>
+ <value><string>wp.deleteCategory</string></value>
+ <value><string>wp.newCategory</string></value>
+ <value><string>wp.getTags</string></value>
+ <value><string>wp.getCategories</string></value>
+ <value><string>wp.getAuthors</string></value>
+ <value><string>wp.getPageList</string></value>
+ <value><string>wp.editPage</string></value>
+ <value><string>wp.deletePage</string></value>
+ <value><string>wp.newPage</string></value>
+ <value><string>wp.getPages</string></value>
+ <value><string>wp.getPage</string></value>
+ <value><string>wp.editProfile</string></value>
+ <value><string>wp.getProfile</string></value>
+ <value><string>wp.getUsers</string></value>
+ <value><string>wp.getUser</string></value>
+ <value><string>wp.getTaxonomies</string></value>
+ <value><string>wp.getTaxonomy</string></value>
+ <value><string>wp.getTerms</string></value>
+ <value><string>wp.getTerm</string></value>
+ <value><string>wp.deleteTerm</string></value>
+ <value><string>wp.editTerm</string></value>
+ <value><string>wp.newTerm</string></value>
+ <value><string>wp.getPosts</string></value>
+ <value><string>wp.getPost</string></value>
+ <value><string>wp.deletePost</string></value>
+ <value><string>wp.editPost</string></value>
+ <value><string>wp.newPost</string></value>
+ <value><string>wp.getUsersBlogs</string></value>
+ </data></array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/health-check/rsd.xml b/WordPress/src/androidTest/assets/health-check/rsd.xml
new file mode 100644
index 000000000..3ce327393
--- /dev/null
+++ b/WordPress/src/androidTest/assets/health-check/rsd.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?><rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
+ <service>
+ <engineName>WordPress</engineName>
+ <engineLink>https://wordpress.org/</engineLink>
+ <homePageLink>http://mockserver</homePageLink>
+ <apis>
+ <api name="WordPress" blogID="1" preferred="true" apiLink="http://mockserver/xmlrpc.php" />
+ <api name="Movable Type" blogID="1" preferred="false" apiLink="http://mockserver/xmlrpc.php" />
+ <api name="MetaWeblog" blogID="1" preferred="false" apiLink="http://mockserver/xmlrpc.php" />
+ <api name="Blogger" blogID="1" preferred="false" apiLink="http://mockserver/xmlrpc.php" />
+ <api name="WP-API" blogID="1" preferred="false" apiLink="http://mockserver/wp-json/" />
+ </apis>
+ </service>
+</rsd>
diff --git a/WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml b/WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml
new file mode 100644
index 000000000..4de183be4
--- /dev/null
+++ b/WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?><rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
+ <service>
+ <engineName>WordPress</engineName>
+ <engineLink>https://wordpress.org/</engineLink>
+ <homePageLink>http://mockserver</homePageLink>
+ <apis>
+ <api name="WordPress" blogID="1" preferred="true" apiLink="http://mockserver/wpnew/xmlrpc.php" />
+ <api name="Movable Type" blogID="1" preferred="false" apiLink="http://mockserver/wpnew/xmlrpc.php" />
+ <api name="MetaWeblog" blogID="1" preferred="false" apiLink="http://mockserver/wpnew/xmlrpc.php" />
+ <api name="Blogger" blogID="1" preferred="false" apiLink="http://mockserver/wpnew/xmlrpc.php" />
+ <api name="WP-API" blogID="1" preferred="false" apiLink="http://mockserver/wpnew/wp-json/" />
+ </apis>
+ </service>
+</rsd>
diff --git a/WordPress/src/androidTest/assets/incorrect-password-wp.getOptions.xml b/WordPress/src/androidTest/assets/incorrect-password-wp.getOptions.xml
new file mode 100644
index 000000000..2f0d7a4a3
--- /dev/null
+++ b/WordPress/src/androidTest/assets/incorrect-password-wp.getOptions.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultCode</name>
+ <value>
+ <int>403</int>
+ </value>
+ </member>
+ <member>
+ <name>faultString</name>
+ <value>
+ <string>Incorrect username or password.</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/invalid-double-xmlrpc-wp.getMediaLibrary.xml b/WordPress/src/androidTest/assets/invalid-double-xmlrpc-wp.getMediaLibrary.xml
new file mode 100644
index 000000000..ecc0d968d
--- /dev/null
+++ b/WordPress/src/androidTest/assets/invalid-double-xmlrpc-wp.getMediaLibrary.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>metadata</name>
+ <value>
+ <struct>
+ <member>
+ <name>image_meta</name>
+ <value>
+ <struct>
+ <member>
+ <name>aperture</name>
+ <value>
+ <double>2,8</double>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/invalid-integer-xmlrpc-wp.getMediaLibrary.xml b/WordPress/src/androidTest/assets/invalid-integer-xmlrpc-wp.getMediaLibrary.xml
new file mode 100644
index 000000000..ce9fca8be
--- /dev/null
+++ b/WordPress/src/androidTest/assets/invalid-integer-xmlrpc-wp.getMediaLibrary.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>parent</name>
+ <value>
+ <int>arst</int>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/malformed-getusersblog-wp.getUsersBlogs.xml b/WordPress/src/androidTest/assets/malformed-getusersblog-wp.getUsersBlogs.xml
new file mode 100644
index 000000000..36a2aa24d
--- /dev/null
+++ b/WordPress/src/androidTest/assets/malformed-getusersblog-wp.getUsersBlogs.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ I'm a malformed xml file :( \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/malformed-null-postid-metaWeblog.getRecentPosts.xml b/WordPress/src/androidTest/assets/malformed-null-postid-metaWeblog.getRecentPosts.xml
new file mode 100644
index 000000000..dbfdea49c
--- /dev/null
+++ b/WordPress/src/androidTest/assets/malformed-null-postid-metaWeblog.getRecentPosts.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>dateCreated</name>
+ <value>
+ <dateTime.iso8601>20140211T16:04:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>userid</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>postid</name>
+ <value>
+ <string>333333</string>
+ </value>
+ </member>
+ <member>
+ <name>description</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>title</name>
+ <value>
+ <string>null postid</string>
+ </value>
+ </member>
+ <member>
+ <name>link</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/11/enft/</string>
+ </value>
+ </member>
+ <member>
+ <name>permaLink</name>
+ <value>
+ <string>https://taliwutblog.wordpress.com/2014/02/11/enft/</string>
+ </value>
+ </member>
+ <member>
+ <name>categories</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <string>Uncategorized</string>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>mt_excerpt</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_text_more</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_more_text</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_comments</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_allow_pings</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>mt_keywords</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_slug</name>
+ <value>
+ <string>enft</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_password</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ <member>
+ <name>wp_author_id</name>
+ <value>
+ <string>55434822</string>
+ </value>
+ </member>
+ <member>
+ <name>wp_author_display_name</name>
+ <value>
+ <string>taliwutt</string>
+ </value>
+ </member>
+ <member>
+ <name>date_created_gmt</name>
+ <value>
+ <dateTime.iso8601>20140211T16:04:00</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>post_status</name>
+ <value>
+ <string>publish</string>
+ </value>
+ </member>
+ <member>
+ <name>custom_fields</name>
+ <value>
+ <array>
+ <data>
+ <value>
+ <struct>
+ <member>
+ <name>id</name>
+ <value>
+ <string>1411</string>
+ </value>
+ </member>
+ <member>
+ <name>key</name>
+ <value>
+ <string>jabber_published</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>1392048234</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_format</name>
+ <value>
+ <string>standard</string>
+ </value>
+ </member>
+ <member>
+ <name>date_modified</name>
+ <value>
+ <dateTime.iso8601>20140210T16:04:31</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>date_modified_gmt</name>
+ <value>
+ <dateTime.iso8601>20140210T16:04:31</dateTime.iso8601>
+ </value>
+ </member>
+ <member>
+ <name>wp_post_thumbnail</name>
+ <value>
+ <string />
+ </value>
+ </member>
+ </struct>
+ </value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+</methodResponse> \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/malformed-software-version-wp.getOptions.json b/WordPress/src/androidTest/assets/malformed-software-version-wp.getOptions.json
new file mode 100644
index 000000000..d7a6cae10
--- /dev/null
+++ b/WordPress/src/androidTest/assets/malformed-software-version-wp.getOptions.json
@@ -0,0 +1,31 @@
+{
+ "admin_url": {
+ "desc": "L&rsquo;adresse web de la zone d&rsquo;administration",
+ "readonly": true,
+ "value": "https://tataliwut.wordpress.com/wp-admin/"
+ },
+ "blog_public": {
+ "desc": "Acc\u00e8s aux renseignements confidentiels",
+ "readonly": true,
+ "value": "1"
+ },
+ "home_url": {
+ "desc": "Adresse web du site (URL)",
+ "readonly": true,
+ "value": "http://tataliwut.wordpress.com"
+ },
+ "login_url": {
+ "desc": "Adresse de connexion (URL)",
+ "readonly": true,
+ "value": "https://tataliwut.wordpress.com/wp-login.php"
+ },
+ "post_thumbnail": {
+ "desc": "Miniature d&rsquo;article",
+ "readonly": true,
+ "value": true
+ },
+ "software_version": {
+ "desc": "Software Version",
+ "readonly": true
+ }
+}
diff --git a/WordPress/src/androidTest/assets/malformed-software-version-wp.getProfile.json b/WordPress/src/androidTest/assets/malformed-software-version-wp.getProfile.json
new file mode 100644
index 000000000..9382d7632
--- /dev/null
+++ b/WordPress/src/androidTest/assets/malformed-software-version-wp.getProfile.json
@@ -0,0 +1 @@
+{"display_name":"taliwutt","first_name":"","username":"taliwutt","bio":"","nickname":"taliwutt","email":"taliwut@gmail.com","registered":"Sep 17, 2013 11:13:54 AM","roles":["administrator"],"last_name":"","nicename":"taliwutt","user_id":"55434822","url":"http://taliwutblog.wordpress.com"}
diff --git a/WordPress/src/androidTest/assets/malformed_category_parent_id.sql b/WordPress/src/androidTest/assets/malformed_category_parent_id.sql
new file mode 100644
index 000000000..aefb9ddaf
--- /dev/null
+++ b/WordPress/src/androidTest/assets/malformed_category_parent_id.sql
@@ -0,0 +1,13 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE android_metadata (locale TEXT);
+INSERT INTO "android_metadata" VALUES('en_US');
+CREATE TABLE accounts (id integer primary key autoincrement, url text, blogName text, username text, password text, imagePlacement text, centerThumbnail boolean, fullSizeImage boolean, maxImageWidth text, maxImageWidthId integer, lastCommentId integer, runService boolean, blogId integer, location boolean default false, dotcom_username text, dotcom_password text, api_key text, api_blogid text, dotcomFlag boolean default false, wpVersion text, httpuser text, httppassword text, postFormats text default '', isScaledImage boolean default false, scaledImgWidth integer default 1024, homeURL text default '', blog_options text default '');
+CREATE TABLE eula (id integer primary key autoincrement, read integer not null, interval text, statsdate integer, sound boolean default false, vibrate boolean default false, light boolean default false, tagline text, tagline_flag boolean default false, uuid text, last_blog_id text);
+CREATE TABLE posts (id integer primary key autoincrement, blogID text, postid text, title text default '', dateCreated date, date_created_gmt date, categories text default '', custom_fields text default '', description text default '', link text default '', mt_allow_comments boolean, mt_allow_pings boolean, mt_excerpt text default '', mt_keywords text default '', mt_text_more text default '', permaLink text default '', post_status text default '', userid integer default 0, wp_author_display_name text default '', wp_author_id text default '', wp_password text default '', wp_post_format text default '', wp_slug text default '', mediaPaths text default '', latitude real, longitude real, localDraft boolean default 0, uploaded boolean default 0, isPage boolean default 0, wp_page_parent_id text, wp_page_parent_title text, isLocalChange boolean default 0);
+CREATE TABLE comments (blogID text, postID text, iCommentID integer, author text, comment text, commentDate text, commentDateFormatted text, status text, url text, email text, postTitle text);
+CREATE TABLE cats (id integer primary key autoincrement, blog_id text, wp_id integer, category_name text not null, parent_id integer default 0);
+INSERT INTO cats VALUES(73,'1',78,'Test category - malformed parent', 999);
+CREATE TABLE quickpress_shortcuts (id integer primary key autoincrement, accountId text, name text);
+CREATE TABLE media (id integer primary key autoincrement, postID integer not null, filePath text default '', fileName text default '', title text default '', description text default '', caption text default '', horizontalAlignment integer default 0, width integer default 0, height integer default 0, mimeType text default '', featured boolean default false, isVideo boolean default false, isFeaturedInPost boolean default false);
+COMMIT;
diff --git a/WordPress/src/androidTest/assets/one_category.sql b/WordPress/src/androidTest/assets/one_category.sql
new file mode 100644
index 000000000..fa5becb95
--- /dev/null
+++ b/WordPress/src/androidTest/assets/one_category.sql
@@ -0,0 +1,13 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE android_metadata (locale TEXT);
+INSERT INTO "android_metadata" VALUES('en_US');
+CREATE TABLE accounts (id integer primary key autoincrement, url text, blogName text, username text, password text, imagePlacement text, centerThumbnail boolean, fullSizeImage boolean, maxImageWidth text, maxImageWidthId integer, lastCommentId integer, runService boolean, blogId integer, location boolean default false, dotcom_username text, dotcom_password text, api_key text, api_blogid text, dotcomFlag boolean default false, wpVersion text, httpuser text, httppassword text, postFormats text default '', isScaledImage boolean default false, scaledImgWidth integer default 1024, homeURL text default '', blog_options text default '');
+CREATE TABLE eula (id integer primary key autoincrement, read integer not null, interval text, statsdate integer, sound boolean default false, vibrate boolean default false, light boolean default false, tagline text, tagline_flag boolean default false, uuid text, last_blog_id text);
+CREATE TABLE posts (id integer primary key autoincrement, blogID text, postid text, title text default '', dateCreated date, date_created_gmt date, categories text default '', custom_fields text default '', description text default '', link text default '', mt_allow_comments boolean, mt_allow_pings boolean, mt_excerpt text default '', mt_keywords text default '', mt_text_more text default '', permaLink text default '', post_status text default '', userid integer default 0, wp_author_display_name text default '', wp_author_id text default '', wp_password text default '', wp_post_format text default '', wp_slug text default '', mediaPaths text default '', latitude real, longitude real, localDraft boolean default 0, uploaded boolean default 0, isPage boolean default 0, wp_page_parent_id text, wp_page_parent_title text, isLocalChange boolean default 0);
+CREATE TABLE comments (blogID text, postID text, iCommentID integer, author text, comment text, commentDate text, commentDateFormatted text, status text, url text, email text, postTitle text);
+CREATE TABLE cats (id integer primary key autoincrement, blog_id text, wp_id integer, category_name text not null, parent_id integer default 0);
+INSERT INTO cats VALUES(1,'1',1,'test category', 0);
+CREATE TABLE quickpress_shortcuts (id integer primary key autoincrement, accountId text, name text);
+CREATE TABLE media (id integer primary key autoincrement, postID integer not null, filePath text default '', fileName text default '', title text default '', description text default '', caption text default '', horizontalAlignment integer default 0, width integer default 0, height integer default 0, mimeType text default '', featured boolean default false, isVideo boolean default false, isFeaturedInPost boolean default false);
+COMMIT;
diff --git a/WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-devices-new.json b/WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-devices-new.json
new file mode 100644
index 000000000..2c63c0851
--- /dev/null
+++ b/WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-devices-new.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-users-new.json b/WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-users-new.json
new file mode 100644
index 000000000..a9810936d
--- /dev/null
+++ b/WordPress/src/androidTest/assets/password-invalid-public-api-wordpress-com-rest-v1-users-new.json
@@ -0,0 +1,4 @@
+{
+ "error": "password_invalid",
+ "message": "Invalid password"
+}
diff --git a/WordPress/src/androidTest/assets/rest-v1-notifications-num_note_items=20.json b/WordPress/src/androidTest/assets/rest-v1-notifications-num_note_items=20.json
new file mode 100644
index 000000000..d006da2c4
--- /dev/null
+++ b/WordPress/src/androidTest/assets/rest-v1-notifications-num_note_items=20.json
@@ -0,0 +1,3453 @@
+{
+ "notes": [{
+ "id": "1004352684",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/google.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Renardo<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/22\/the-end-of-unrecorded-life\/#comment-146\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">22 jan, 12 &#104; 46<\/a><\/em>",
+ "header_text": "Renardo",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/22\/the-end-of-unrecorded-life\/#comment-146",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/146": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Cool<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/22\/the-end-of-unrecorded-life\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">The End of Unrecorded Life<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/146",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 146,
+ "post_id": 296,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/146",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 146,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/146",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 146,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/146",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 146,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Renardo vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"The End of Unrecorded Life\"<\/span>",
+ "text": " Renardo vous a r\u00e9pondu sur \"The End of Unrecorded Life\""
+ },
+ "timestamp": "1390394788",
+ "meta": {
+ "ids": {
+ "self": 1004352684,
+ "site": 57991476,
+ "post": 296,
+ "comment": 146,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/1004352684",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/296",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/146",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "985853533",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mashime.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Maxime<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/09\/magnifique\/#comment-143\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">09 jan, 17 &#104; 55<\/a><\/em>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/09\/magnifique\/#comment-143",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/143": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p><a href=\"http:\/\/google.com\" rel=\"nofollow\" target=\"_blank\">go to google<\/a><\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/09\/magnifique\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Magnifique!<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/143",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "Approuver et r\u00e9pondre",
+ "comment_id": 143,
+ "post_id": 277,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "approve-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/143",
+ "rest_body": {
+ "status": "approved"
+ },
+ "ajax_arg": "approve",
+ "title_text": "Approuver ce commentaire.",
+ "text": "Approuver",
+ "comment_id": 143,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/143",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 143,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/143",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 143,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-pending\"><\/span> Maxime vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Magnifique!\"<\/span>",
+ "text": " Maxime vous a r\u00e9pondu sur \"Magnifique!\""
+ },
+ "timestamp": "1389290132",
+ "meta": {
+ "ids": {
+ "self": 985853533,
+ "site": 57991476,
+ "post": 277,
+ "comment": 143,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/985853533",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/277",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/143",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "pending",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "985850114",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mashime.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Maxime<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/09\/magnifique\/#comment-142\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">09 jan, 17 &#104; 52<\/a><\/em>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/09\/magnifique\/#comment-142",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/142": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p><a href=\"woot.com\" rel=\"nofollow\" target=\"_blank\">go to woot<\/a><\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/09\/magnifique\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Magnifique!<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/142",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 142,
+ "post_id": 277,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/142",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 142,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/142",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 142,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/142",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 142,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Maxime vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Magnifique!\"<\/span>",
+ "text": " Maxime vous a r\u00e9pondu sur \"Magnifique!\""
+ },
+ "timestamp": "1389289954",
+ "meta": {
+ "ids": {
+ "self": 985850114,
+ "site": 57991476,
+ "post": 277,
+ "comment": 142,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/985850114",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/277",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/142",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "985847451",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mashime.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Maxime<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/03\/273\/#comment-141\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">09 jan, 17 &#104; 50<\/a><\/em>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/03\/273\/#comment-141",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/141": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p><a href=\"woot.com\" rel=\"nofollow\" target=\"_blank\">go to woot<\/a><\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2014\/01\/03\/273\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Article sans titre<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/141",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 141,
+ "post_id": 273,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/141",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 141,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/141",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 141,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/141",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 141,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Maxime vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Article sans titre\"<\/span>",
+ "text": " Maxime vous a r\u00e9pondu sur \"Article sans titre\""
+ },
+ "timestamp": "1389289818",
+ "meta": {
+ "ids": {
+ "self": 985847451,
+ "site": 57991476,
+ "post": 273,
+ "comment": 141,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/985847451",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/273",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/141",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "975758156",
+ "type": "like",
+ "unread": "0",
+ "body": {
+ "template": "single-line-list",
+ "header": "Cette personne aime <a href='http:\/\/taliwutblog.wordpress.com\/2014\/01\/02\/romeo\/' target='_blank' notes-data-click='like_note_post'>votre article<\/a> :",
+ "header_text": "Cette personne aime votre article :",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/02\/romeo\/",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/261": false
+ },
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/mashime.wordpress.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"like_note_likers_blog\">Maxime<\/a>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/mashime.wordpress.com",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_liked_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ }
+ }],
+ "footer": "",
+ "footer_text": "",
+ "footer_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/02\/romeo\/#wpl-likebox"
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s1.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-like.png",
+ "html": "Maxime aime votre article <span class=\"wpn-subject-title\">\"Romeo\"<\/span>",
+ "text": "Maxime aime votre article \"Romeo\""
+ },
+ "timestamp": "1388680702",
+ "meta": {
+ "ids": {
+ "self": 975758156,
+ "site": 57991476,
+ "post": 261,
+ "likers": ["50540106"]
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/975758156",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/261",
+ "likes": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/261\/likes\/"
+ }
+ }
+ }, {
+ "id": "975752469",
+ "type": "like",
+ "unread": "0",
+ "body": {
+ "template": "single-line-list",
+ "header": "Cette personne aime <a href='http:\/\/taliwutblog.wordpress.com\/2014\/01\/02\/unicorn\/' target='_blank' notes-data-click='like_note_post'>votre article<\/a> :",
+ "header_text": "Cette personne aime votre article :",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/02\/unicorn\/",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/263": false
+ },
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/mashime.wordpress.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"like_note_likers_blog\">Maxime<\/a>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/mashime.wordpress.com",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_liked_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ }
+ }],
+ "footer": "",
+ "footer_text": "",
+ "footer_link": "http:\/\/taliwutblog.wordpress.com\/2014\/01\/02\/unicorn\/#wpl-likebox"
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s1.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-like.png",
+ "html": "Maxime et une autre personne aiment votre article <span class=\"wpn-subject-title\">\"Unicorn\"<\/span>",
+ "text": "Maxime et une autre personne aiment votre article \"Unicorn\""
+ },
+ "timestamp": "1388680407",
+ "meta": {
+ "ids": {
+ "self": 975752469,
+ "site": 57991476,
+ "post": 263,
+ "likers": ["50540106"]
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/975752469",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/263",
+ "likes": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/263\/likes\/"
+ }
+ }
+ }, {
+ "id": "959824618",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-135\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 12 &#104; 01<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-135",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/135": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>g &amp; h<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/135",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 135,
+ "post_id": 228,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/135",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 135,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/135",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 135,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/135",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 135,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387454466",
+ "meta": {
+ "ids": {
+ "self": 959824618,
+ "site": 57991476,
+ "post": 228,
+ "comment": 135,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959824618",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/135",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "959824497",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-134\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 12 &#104; 00<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-134",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/134": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>e &amp; f<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/134",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 134,
+ "post_id": 228,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/134",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 134,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/134",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 134,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/134",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 134,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387454459",
+ "meta": {
+ "ids": {
+ "self": 959824497,
+ "site": 57991476,
+ "post": 228,
+ "comment": 134,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959824497",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/134",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "959820013",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-133\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 11 &#104; 55<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-133",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/133": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>c &amp; d<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/133",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 133,
+ "post_id": 228,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/133",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 133,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/133",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 133,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/133",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 133,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387454153",
+ "meta": {
+ "ids": {
+ "self": 959820013,
+ "site": 57991476,
+ "post": 228,
+ "comment": 133,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959820013",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/133",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "959819893",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-132\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 11 &#104; 55<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-132",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/132": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>a &amp; b<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/132",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "Approuver et r\u00e9pondre",
+ "comment_id": 132,
+ "post_id": 228,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "approve-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/132",
+ "rest_body": {
+ "status": "approved"
+ },
+ "ajax_arg": "approve",
+ "title_text": "Approuver ce commentaire.",
+ "text": "Approuver",
+ "comment_id": 132,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/132",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 132,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/132",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 132,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-pending\"><\/span> taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387454145",
+ "meta": {
+ "ids": {
+ "self": 959819893,
+ "site": 57991476,
+ "post": 228,
+ "comment": 132,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959819893",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/132",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "pending",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "959813370",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-131\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 11 &#104; 48<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-131",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/131": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>Toi &amp; Moi<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/131",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 131,
+ "post_id": 228,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/131",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 131,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/131",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 131,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/131",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 131,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387453682",
+ "meta": {
+ "ids": {
+ "self": 959813370,
+ "site": 57991476,
+ "post": 228,
+ "comment": 131,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959813370",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/131",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "959812730",
+ "type": "comment",
+ "unread": "1",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/03\/226\/#comment-130\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 11 &#104; 47<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/03\/226\/#comment-130",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/130": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>&amp;<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/03\/226\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Article sans titre<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/130",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 130,
+ "post_id": 226,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/130",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 130,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/130",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 130,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/130",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 130,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Article sans titre\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Article sans titre\""
+ },
+ "timestamp": "1387453636",
+ "meta": {
+ "ids": {
+ "self": 959812730,
+ "site": 57991476,
+ "post": 226,
+ "comment": 130,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959812730",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/226",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/130",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "959811251",
+ "type": "comment",
+ "unread": "1",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/03\/226\/#comment-129\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">19 d\u00e9c, 11 &#104; 45<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/03\/226\/#comment-129",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/129": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>&amp;&amp; \u00e9\u00e9<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/03\/226\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Article sans titre<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/129",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 129,
+ "post_id": 226,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/129",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 129,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/129",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 129,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/129",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 129,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Article sans titre\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Article sans titre\""
+ },
+ "timestamp": "1387453537",
+ "meta": {
+ "ids": {
+ "self": 959811251,
+ "site": 57991476,
+ "post": 226,
+ "comment": 129,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/959811251",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/226",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/129",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "958818021",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "header": "En r\u00e9ponse \u00e0 <a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-126\" target=\"_blank\" notes-data-click=\"comment_note_comment_parent\">votre commentaire<\/a>",
+ "html": "Nice",
+ "header_text": "En r\u00e9ponse \u00e0 votre commentaire",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-126",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/126": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/55434822": false
+ }
+ }, {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-127\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">18 d\u00e9c, 16 &#104; 38<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#comment-127",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/127": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>Hoy<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/127",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 127,
+ "post_id": 228,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/127",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 127,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/127",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 127,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/127",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 127,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387384721",
+ "meta": {
+ "ids": {
+ "self": 958818021,
+ "site": 57991476,
+ "post": 228,
+ "comment": 127,
+ "comment_parent": 126
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/958818021",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/127",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/126"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "958770112",
+ "type": "reblog",
+ "unread": "0",
+ "body": {
+ "template": "single-line-list",
+ "header": "These people reblogged <a href='http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/' target='_blank' notes-data-click='reblog_note_post'>your post<\/a>:",
+ "header_text": "These people reblogged your post:",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/taliwuttalot1.wordpress.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"reblog_note_rebloggers_blog\">taliwuttalot<\/a>",
+ "action": {
+ "params": {
+ "stat-source": "note_reblog_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ }
+ }, {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/098a6277aeca89a789a1d3be00db59f8?s=256&d=identicon&r=G",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"reblog_note_rebloggers_blog\">taliwutt<\/a>",
+ "action": false
+ }],
+ "footer": "<p>2 reblogs actuellement. Bravo !<\/p>",
+ "footer_text": "2 reblogs actuellement. Bravo !",
+ "footer_link": "http:\/\/taliwutblog.wordpress.com\/2013\/12\/17\/cat\/#wpl-likebox"
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s1.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-reblog.png",
+ "html": "taliwuttalot et taliwutt ont reblogu\u00e9 votre article <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": "taliwuttalot et taliwutt ont reblogu\u00e9 votre article \"Cat\""
+ },
+ "timestamp": "1387382290",
+ "meta": {
+ "ids": {
+ "self": 958770112,
+ "site": 57991476,
+ "post": 228,
+ "rebloggers": [55434822, 58499323]
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/958770112",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/228"
+ }
+ }
+ }, {
+ "id": "958722652",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "header": "En r\u00e9ponse \u00e0 <a href=\"http:\/\/taliwuttalot1.wordpress.com\/2013\/11\/29\/renard\/#comment-1\" target=\"_blank\" notes-data-click=\"comment_note_comment_parent\">votre commentaire<\/a>",
+ "html": "J'adore les renards",
+ "header_text": "En r\u00e9ponse \u00e0 votre commentaire",
+ "header_link": "http:\/\/taliwuttalot1.wordpress.com\/2013\/11\/29\/renard\/#comment-1",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/61100702\/comments\/1": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/55434822": false
+ }
+ }, {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttalot1.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttalot<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwuttalot1.wordpress.com\/2013\/11\/29\/renard\/#comment-2\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">18 d\u00e9c, 15 &#104; 17<\/a><\/em>",
+ "header_text": "taliwuttalot",
+ "header_link": "http:\/\/taliwuttalot1.wordpress.com\/2013\/11\/29\/renard\/#comment-2",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/61100702\/comments\/2": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58499323": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "taliwuttalot1.wordpress.com",
+ "blog_url": "http:\/\/taliwuttalot1.wordpress.com",
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "blog_title": "taliwuttalot1",
+ "is_following": false
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>Je n&rsquo;aime pas les poneys.<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwuttalot1.wordpress.com\/2013\/11\/29\/renard\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Renard<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/61100702\/comments\/2",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 2,
+ "post_id": 2,
+ "blog_id": 61100702,
+ "site_id": 61100702,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/35454b2c01cf2670fab815e1c91eafa8?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " taliwuttalot vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Renard\"<\/span>",
+ "text": " taliwuttalot vous a r\u00e9pondu sur \"Renard\""
+ },
+ "timestamp": "1387379829",
+ "meta": {
+ "ids": {
+ "self": 958722652,
+ "site": 61100702,
+ "post": 2,
+ "comment": 2,
+ "comment_parent": 1
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/958722652",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/61100702",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/61100702\/posts\/2",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/61100702\/comments\/2",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/61100702\/comments\/1"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "958477392",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mashime.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Maxime<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/mashime.wordpress.com\/2013\/12\/09\/hello\/#comment-9\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">18 d\u00e9c, 11 &#104; 02<\/a><\/em>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/mashime.wordpress.com\/2013\/12\/09\/hello\/#comment-9",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/9": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>Thank you!<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/mashime.wordpress.com\/2013\/12\/09\/hello\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">hello<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/52673824\/comments\/9",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 9,
+ "post_id": 12,
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> Maxime commented on <span class=\"wpn-subject-title\">\"hello\"<\/span>",
+ "text": " Maxime commented on \"hello\""
+ },
+ "timestamp": "1387364578",
+ "meta": {
+ "ids": {
+ "self": 958477392,
+ "site": 52673824,
+ "post": 12,
+ "comment": 9,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/958477392",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/posts\/12",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/9",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "957454508",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "header": "En r\u00e9ponse \u00e0 <a href=\"http:\/\/mashime.wordpress.com\/2013\/09\/25\/tyh\/#comment-6\" target=\"_blank\" notes-data-click=\"comment_note_comment_parent\">votre commentaire<\/a>",
+ "html": "Reblogged this on taliwutblog and commented: \nCute cat!",
+ "header_text": "En r\u00e9ponse \u00e0 votre commentaire",
+ "header_link": "http:\/\/mashime.wordpress.com\/2013\/09\/25\/tyh\/#comment-6",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/6": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/55434822": false
+ }
+ }, {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mashime.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Maxime<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/mashime.wordpress.com\/2013\/09\/25\/tyh\/#comment-7\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">17 d\u00e9c, 15 &#104; 29<\/a><\/em>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/mashime.wordpress.com\/2013\/09\/25\/tyh\/#comment-7",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/7": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_commented_post",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ },
+ "html": "<div class=\"wpn-comment\"><p>thanks!<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/mashime.wordpress.com\/2013\/09\/25\/tyh\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Cat<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/52673824\/comments\/7",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 7,
+ "post_id": 8,
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Maxime vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Cat\"<\/span>",
+ "text": " Maxime vous a r\u00e9pondu sur \"Cat\""
+ },
+ "timestamp": "1387294144",
+ "meta": {
+ "ids": {
+ "self": 957454508,
+ "site": 52673824,
+ "post": 8,
+ "comment": 7,
+ "comment_parent": 6
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/957454508",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/posts\/8",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/7",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824\/comments\/6"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "955039955",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/1.gravatar.com\/avatar\/499913033b0478a2f6018fe925efcef6?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/taliwuttnoblog.wordpress.com\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">taliwuttnoblog<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/#comment-121\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">15 d\u00e9c, 18 &#104; 45<\/a><\/em>",
+ "header_text": "taliwuttnoblog",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/#comment-121",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/121": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/58252902": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Hey!<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hello<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/121",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "Approuver et r\u00e9pondre",
+ "comment_id": 121,
+ "post_id": 209,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "approve-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/121",
+ "rest_body": {
+ "status": "approved"
+ },
+ "ajax_arg": "approve",
+ "title_text": "Approuver ce commentaire.",
+ "text": "Approuver",
+ "comment_id": 121,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/121",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 121,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/121",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 121,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/499913033b0478a2f6018fe925efcef6?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-pending\"><\/span> taliwuttnoblog vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hello\"<\/span>",
+ "text": " taliwuttnoblog vous a r\u00e9pondu sur \"Hello\""
+ },
+ "timestamp": "1387133152",
+ "meta": {
+ "ids": {
+ "self": 955039955,
+ "site": 57991476,
+ "post": 209,
+ "comment": 121,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/955039955",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/209",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/121",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "pending",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "845858845",
+ "type": "follow",
+ "unread": "0",
+ "body": {
+ "template": "single-line-list",
+ "header": "Ces personnes suivent <a href='http:\/\/taliwutblog.wordpress.com' title='taliwutblog (http:\/\/taliwutblog.wordpress.com)' target='_blank' notes-data-click='follow_note_blog'>votre blog<\/a>\u00a0:",
+ "header_text": "Ces personnes suivent votre blog\u00a0:",
+ "header_link": "http:\/\/taliwutblog.wordpress.com",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476": false
+ },
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/f799c5ce011617592ec5aec631aa3a49?s=256&d=identicon",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/tanydigraphicresources.wordpress.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"follow_note_followers_blog\">tanydi<\/a>",
+ "header_text": "tanydi",
+ "header_link": "http:\/\/tanydigraphicresources.wordpress.com",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/26783616": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/41747324": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_followed",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "tanydigraphicresources.wordpress.com",
+ "blog_url": "http:\/\/tanydigraphicresources.wordpress.com",
+ "blog_id": 41747324,
+ "site_id": 41747324,
+ "blog_title": "TanyDi Graphic Resources",
+ "is_following": false
+ },
+ "type": "follow"
+ }
+ }, {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/397f1451ec81cf663d1f0c7ecd63f855?s=256&d=identicon",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/imagesprovence.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"follow_note_followers_blog\">imagesprovence<\/a>",
+ "header_text": "imagesprovence",
+ "header_link": "http:\/\/imagesprovence.com",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/17042859": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/16431677": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_followed",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "imagesprovence.wordpress.com",
+ "blog_url": "http:\/\/imagesprovence.wordpress.com",
+ "blog_id": 16431677,
+ "site_id": 16431677,
+ "blog_title": "Images Provence \u00ae",
+ "is_following": false
+ },
+ "type": "follow"
+ }
+ }, {
+ "icon": "https:\/\/1.gravatar.com\/avatar\/d4eb59f69d63e5ca6d54ae410b256bb0?s=256&d=identicon",
+ "icon_width": 32,
+ "icon_height": 32,
+ "header": "<a href=\"http:\/\/mashime.wordpress.com\" class=\"wpn-user-blog-link\" target=\"_blank\" notes-data-click=\"follow_note_followers_blog\">Maxime<\/a>",
+ "header_text": "Maxime",
+ "header_link": "http:\/\/mashime.wordpress.com",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/users\/50540106": false,
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/52673824": false
+ },
+ "action": {
+ "params": {
+ "stat-source": "note_followed",
+ "follow-text": "Suivre",
+ "following-text": "Abonn\u00e9",
+ "following-hover-text": "Ne plus suivre",
+ "blog_domain": "mashime.wordpress.com",
+ "blog_url": "http:\/\/mashime.wordpress.com",
+ "blog_id": 52673824,
+ "site_id": 52673824,
+ "blog_title": "mashime",
+ "is_following": true
+ },
+ "type": "follow"
+ }
+ }],
+ "footer": "<p><a href='https:\/\/fr.wordpress.com\/my-stats\/?blog=57991476&#038;blog_subscribers' target=\"_blank\" notes-data-click=\"follow_note_view_all\" alt='Followers of 'taliwutblog''>View all followers<\/a><\/p>",
+ "footer_text": "View all followers",
+ "footer_link": "https:\/\/fr.wordpress.com\/my-stats\/?blog=57991476&#038;blog_subscribers"
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/f799c5ce011617592ec5aec631aa3a49?s=256&d=identicon",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-follow.png",
+ "html": "tanydi et imagesprovence et une autre personne suivent votre blog <span class=\"wpn-subject-title\">\"taliwutblog\"<\/span>",
+ "text": "tanydi et imagesprovence et une autre personne suivent votre blog \"taliwutblog\""
+ },
+ "timestamp": "1386345092",
+ "meta": {
+ "ids": {
+ "self": 845858845,
+ "site": 57991476,
+ "follows": [26783616, 17042859, 50540106]
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/845858845",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "follows": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/follows\/"
+ }
+ }
+ }, {
+ "id": "941311229",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/apzld\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">azdald<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/daily-prompt-placebo-effect\/#comment-118\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">05 d\u00e9c, 12 &#104; 15<\/a><\/em>",
+ "header_text": "azdald",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/daily-prompt-placebo-effect\/#comment-118",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/118": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>great bike<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/daily-prompt-placebo-effect\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Daily Prompt: Placebo Effect<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/118",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 118,
+ "post_id": 212,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/118",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 118,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/118",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 118,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/118",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 118,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> azdald vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Daily Prompt: Placebo Effect\"<\/span>",
+ "text": " azdald vous a r\u00e9pondu sur \"Daily Prompt: Placebo Effect\""
+ },
+ "timestamp": "1386245747",
+ "meta": {
+ "ids": {
+ "self": 941311229,
+ "site": 57991476,
+ "post": 212,
+ "comment": 118,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/941311229",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/212",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/118",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "941309501",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/zeopfk\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">mugman<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/hghj\/#comment-117\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">05 d\u00e9c, 12 &#104; 13<\/a><\/em>",
+ "header_text": "mugman",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/hghj\/#comment-117",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/117": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Very nice mug<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/hghj\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hghj<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/117",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "Approuver et r\u00e9pondre",
+ "comment_id": 117,
+ "post_id": 214,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "approve-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/117",
+ "rest_body": {
+ "status": "approved"
+ },
+ "ajax_arg": "approve",
+ "title_text": "Approuver ce commentaire.",
+ "text": "Approuver",
+ "comment_id": 117,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/117",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 117,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/117",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 117,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-pending\"><\/span> mugman vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hghj\"<\/span>",
+ "text": " mugman vous a r\u00e9pondu sur \"Hghj\""
+ },
+ "timestamp": "1386245633",
+ "meta": {
+ "ids": {
+ "self": 941309501,
+ "site": 57991476,
+ "post": 214,
+ "comment": 117,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/941309501",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/214",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/117",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "pending",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "941251355",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/f0b8ec52e6e70277a02c8d8fb9eb162c?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/zeopfk\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">zepofkezpf<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/06\/test\/#comment-114\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">05 d\u00e9c, 11 &#104; 13<\/a><\/em>",
+ "header_text": "zepofkezpf",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/11\/06\/test\/#comment-114",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/114": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Ah bah oui&#8230;<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/06\/test\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Test<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/114",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 114,
+ "post_id": 216,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/114",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 114,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/114",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 114,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/114",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 114,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/f0b8ec52e6e70277a02c8d8fb9eb162c?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> zepofkezpf vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Test\"<\/span>",
+ "text": " zepofkezpf vous a r\u00e9pondu sur \"Test\""
+ },
+ "timestamp": "1386242017",
+ "meta": {
+ "ids": {
+ "self": 941251355,
+ "site": 57991476,
+ "post": 216,
+ "comment": 114,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/941251355",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/216",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/114",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "939132508",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0bebf79ebcd87d7110e071a45c27960c?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>zeoifjzeiofjnardo<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/29\/test-19\/#comment-107\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">03 d\u00e9c, 21 &#104; 17<\/a><\/em>",
+ "header_text": "zeoifjzeiofjnardo",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/11\/29\/test-19\/#comment-107",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/107": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Magnifique<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/29\/test-19\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Test 19<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/107",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 107,
+ "post_id": 218,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/107",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 107,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/107",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 107,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/107",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 107,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0bebf79ebcd87d7110e071a45c27960c?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> zeoifjzeiofjnardo vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Test 19\"<\/span>",
+ "text": " zeoifjzeiofjnardo vous a r\u00e9pondu sur \"Test 19\""
+ },
+ "timestamp": "1386105472",
+ "meta": {
+ "ids": {
+ "self": 939132508,
+ "site": 57991476,
+ "post": 218,
+ "comment": 107,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/939132508",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/218",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/107",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "917062451",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>Renardo<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/#comment-106\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">17 nov, 10 &#104; 41<\/a><\/em>",
+ "header_text": "Renardo",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/#comment-106",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/106": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Oh yeah!<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hello<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/106",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "Approuver et r\u00e9pondre",
+ "comment_id": 106,
+ "post_id": 209,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "approve-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/106",
+ "rest_body": {
+ "status": "approved"
+ },
+ "ajax_arg": "approve",
+ "title_text": "Approuver ce commentaire.",
+ "text": "Approuver",
+ "comment_id": 106,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/106",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 106,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/106",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 106,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-pending\"><\/span> Renardo vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hello\"<\/span>",
+ "text": " Renardo vous a r\u00e9pondu sur \"Hello\""
+ },
+ "timestamp": "1384684890",
+ "meta": {
+ "ids": {
+ "self": 917062451,
+ "site": 57991476,
+ "post": 209,
+ "comment": 106,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/917062451",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/209",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/106",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "pending",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "899480284",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mop\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Mop<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/hghj\/#comment-95\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">04 nov, 13 &#104; 16<\/a><\/em>",
+ "header_text": "Mop",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/hghj\/#comment-95",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/95": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Test 2<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/11\/04\/hghj\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hghj<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/95",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 95,
+ "post_id": 214,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/95",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 95,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/95",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 95,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/95",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 95,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Mop vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hghj\"<\/span>",
+ "text": " Mop vous a r\u00e9pondu sur \"Hghj\""
+ },
+ "timestamp": "1383571018",
+ "meta": {
+ "ids": {
+ "self": 899480284,
+ "site": 57991476,
+ "post": 214,
+ "comment": 95,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/899480284",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/214",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/95",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "899478891",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span><a href=\"http:\/\/mop\" target=\"_blank\" notes-data-click=\"comment_note_commenters_blog\">Mop<\/a><\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/#comment-92\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">04 nov, 13 &#104; 15<\/a><\/em>",
+ "header_text": "Mop",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/#comment-92",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/92": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Hola<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/17\/hello\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hello<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/92",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "Approuver et r\u00e9pondre",
+ "comment_id": 92,
+ "post_id": 209,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "approve-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/92",
+ "rest_body": {
+ "status": "approved"
+ },
+ "ajax_arg": "approve",
+ "title_text": "Approuver ce commentaire.",
+ "text": "Approuver",
+ "comment_id": 92,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/92",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 92,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/92",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 92,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-pending\"><\/span> Mop vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hello\"<\/span>",
+ "text": " Mop vous a r\u00e9pondu sur \"Hello\""
+ },
+ "timestamp": "1383570928",
+ "meta": {
+ "ids": {
+ "self": 899478891,
+ "site": 57991476,
+ "post": 209,
+ "comment": 92,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/899478891",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/209",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/92",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "pending",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "899321949",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>Joe<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/11\/jhjkk\/#comment-91\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">04 nov, 10 &#104; 16<\/a><\/em>",
+ "header_text": "Joe",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/11\/jhjkk\/#comment-91",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/91": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>magnifique<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/11\/jhjkk\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Jhjkk<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/91",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 91,
+ "post_id": 207,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/91",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 91,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/91",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 91,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/91",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 91,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Joe vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Jhjkk\"<\/span>",
+ "text": " Joe vous a r\u00e9pondu sur \"Jhjkk\""
+ },
+ "timestamp": "1383560162",
+ "meta": {
+ "ids": {
+ "self": 899321949,
+ "site": 57991476,
+ "post": 207,
+ "comment": 91,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/899321949",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/207",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/91",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "867149786",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>Mop<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-17\/#comment-90\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">11 oct, 09 &#104; 35<\/a><\/em>",
+ "header_text": "Mop",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-17\/#comment-90",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/90": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Kikoo<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-17\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hey 17<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/90",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 90,
+ "post_id": 170,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/90",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 90,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/90",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 90,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/90",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 90,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Mop vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hey 17\"<\/span>",
+ "text": " Mop vous a r\u00e9pondu sur \"Hey 17\""
+ },
+ "timestamp": "1381484109",
+ "meta": {
+ "ids": {
+ "self": 867149786,
+ "site": 57991476,
+ "post": 170,
+ "comment": 90,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/867149786",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/170",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/90",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "867148474",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>Mop<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/#comment-89\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">11 oct, 09 &#104; 33<\/a><\/em>",
+ "header_text": "Mop",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/#comment-89",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/89": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>Fine ?<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hey 13<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/89",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 89,
+ "post_id": 162,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/89",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 89,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/89",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 89,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/89",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 89,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": "<span class=\"wpn-subject-status wpn-subject-status-replied\"><\/span> Mop vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hey 13\"<\/span>",
+ "text": " Mop vous a r\u00e9pondu sur \"Hey 13\""
+ },
+ "timestamp": "1381484026",
+ "meta": {
+ "ids": {
+ "self": 867148474,
+ "site": 57991476,
+ "post": 162,
+ "comment": 89,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/867148474",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/162",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/89",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": true
+ }
+ }, {
+ "id": "867148106",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>Mop<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/#comment-88\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">11 oct, 09 &#104; 33<\/a><\/em>",
+ "header_text": "Mop",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/#comment-88",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/88": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>How is it going ?<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hey 13<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/88",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 88,
+ "post_id": 162,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/88",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 88,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/88",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 88,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/88",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 88,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Mop vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hey 13\"<\/span>",
+ "text": " Mop vous a r\u00e9pondu sur \"Hey 13\""
+ },
+ "timestamp": "1381484001",
+ "meta": {
+ "ids": {
+ "self": 867148106,
+ "site": 57991476,
+ "post": 162,
+ "comment": 88,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/867148106",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/162",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/88",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "867147723",
+ "type": "comment",
+ "unread": "0",
+ "body": {
+ "template": "multi-line-list",
+ "items": [{
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "icon_width": 48,
+ "icon_height": 48,
+ "header": "<span>Mop<\/span> <em class=\"wpn-comment-date\"><a target=\"_blank\" href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/#comment-87\" target=\"_blank\" notes-data-click=\"comment_note_new_comment\">11 oct, 09 &#104; 32<\/a><\/em>",
+ "header_text": "Mop",
+ "header_link": "http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/#comment-87",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/87": false
+ },
+ "html": "<div class=\"wpn-comment\"><p>What&rsquo;s up ?<\/p>\n<\/div>"
+ }],
+ "header": "<a href=\"http:\/\/taliwutblog.wordpress.com\/2013\/10\/10\/hey-13\/\" target=\"_blank\" notes-data-click=\"comment_note_orig_post\">Hey 13<\/a>",
+ "actions": [{
+ "type": "replyto-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/87",
+ "rest_body": [],
+ "button_title_text": "R\u00e9pondre \u00e0 ce commentaire.",
+ "button_text": "R\u00e9ponse",
+ "reply_header_text": "R\u00e9pondre au commentaire",
+ "submit_button_text": "R\u00e9ponse",
+ "comment_id": 87,
+ "post_id": 162,
+ "blog_id": 57991476,
+ "site_id": 57991476,
+ "user_id": "55434822",
+ "approve_parent": 1
+ }
+ }, {
+ "type": "unapprove-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/87",
+ "rest_body": {
+ "status": "unapproved"
+ },
+ "title_text": "D\u00e9sapprouver ce commentaire.",
+ "text": "Unapprove",
+ "comment_id": 87,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "spam-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/87",
+ "rest_body": {
+ "status": "spam"
+ },
+ "ajax_arg": "spam",
+ "title_text": "Marquer ce commentaire comme ind\u00e9sirable.",
+ "text": "Ind\u00e9sirable",
+ "comment_id": 87,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }, {
+ "type": "trash-comment",
+ "params": {
+ "rest_path": "\/sites\/57991476\/comments\/87",
+ "rest_body": {
+ "status": "trash"
+ },
+ "title_text": "Mettre ce commentaire \u00e0 la corbeille.",
+ "text": "Corbeille",
+ "comment_id": 87,
+ "blog_id": 57991476,
+ "site_id": 57991476
+ }
+ }]
+ },
+ "subject": {
+ "icon": "https:\/\/0.gravatar.com\/avatar\/0403628896534a3d9037ffa0902de870?s=256&d=identicon&r=G",
+ "noticon": "https:\/\/s2.wp.com\/wp-content\/mu-plugins\/notes\/images\/noticon-comment.png",
+ "html": " Mop vous a r\u00e9pondu sur <span class=\"wpn-subject-title\">\"Hey 13\"<\/span>",
+ "text": " Mop vous a r\u00e9pondu sur \"Hey 13\""
+ },
+ "timestamp": "1381483974",
+ "meta": {
+ "ids": {
+ "self": 867147723,
+ "site": 57991476,
+ "post": 162,
+ "comment": 87,
+ "comment_parent": 0
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/867147723",
+ "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/help",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476",
+ "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/posts\/162",
+ "comment": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/87",
+ "comment_parent": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476\/comments\/0"
+ },
+ "approval_status": "approved",
+ "undo_status": "0",
+ "has_replied": false
+ }
+ }, {
+ "id": "839183136",
+ "type": "best_followed_day_feat",
+ "unread": "0",
+ "body": {
+ "template": "big-badge",
+ "header": "Le + de follows en un jour",
+ "header_text": "Le + de follows en un jour",
+ "header_link": "",
+ "html": "<p>Au Vendredi 20 septembre 2013 vous avez pulv\u00e9ris\u00e9 votre pr\u00e9c\u00e9dent record de follows enregistr\u00e9s en un seul jour, sur votre blog <a href=\"http:\/\/taliwutblog.wordpress.com\" title=\"taliwutblog\" target=\"_blank\" notes-data-click=\"best_period_ever_feat\">taliwutblog<\/a>. Super!<\/p><ul><li><span class=\"wpn-feat-current-record-title\">Current Record: <\/span><span class=\"wpn-feat-new-record-count\">20<\/span><\/li><li><span class=\"wpn-feat-old-record-title\">Old Record: <\/span><span class=\"wpn-feat-old-record-count\">8<\/span><\/li><\/ul>",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476": false
+ }
+ },
+ "subject": {
+ "icon": "https:\/\/wordpress.com\/wp-content\/mu-plugins\/achievements\/bestday-follows-2x.png",
+ "noticon": "https:\/\/wordpress.com\/wp-content\/mu-plugins\/notes\/images\/noticon-milestone.png",
+ "html": "septembre 20: Your best day for follows on <span class=\"wpn-subject-title\">&quot;taliwutblog&quot;<\/span>",
+ "text": "septembre 20: Your best day for follows on &quot;taliwutblog&quot;"
+ },
+ "timestamp": "1379664368",
+ "meta": {
+ "ids": {
+ "self": 839183136,
+ "site": 57991476
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/839183136",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476"
+ },
+ "blog_id": "57991476"
+ }
+ }, {
+ "id": "838222030",
+ "type": "best_followed_day_feat",
+ "unread": "0",
+ "body": {
+ "template": "big-badge",
+ "header": "Le + de follows en un jour",
+ "header_text": "Le + de follows en un jour",
+ "header_link": "",
+ "html": "<p>Au Jeudi 19 septembre 2013 vous avez pulv\u00e9ris\u00e9 votre pr\u00e9c\u00e9dent record de follows enregistr\u00e9s en un seul jour, sur votre blog <a href=\"http:\/\/taliwutblog.wordpress.com\" title=\"taliwutblog\" target=\"_blank\" notes-data-click=\"best_period_ever_feat\">taliwutblog<\/a>. Super!<\/p><ul><li><span class=\"wpn-feat-current-record-title\">Current Record: <\/span><span class=\"wpn-feat-new-record-count\">20<\/span><\/li><li><span class=\"wpn-feat-old-record-title\">Old Record: <\/span><span class=\"wpn-feat-old-record-count\">2<\/span><\/li><\/ul>",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476": false
+ }
+ },
+ "subject": {
+ "icon": "https:\/\/wordpress.com\/wp-content\/mu-plugins\/achievements\/bestday-follows-2x.png",
+ "noticon": "https:\/\/wordpress.com\/wp-content\/mu-plugins\/notes\/images\/noticon-milestone.png",
+ "html": "septembre 19: Your best day for follows on <span class=\"wpn-subject-title\">&quot;taliwutblog&quot;<\/span>",
+ "text": "septembre 19: Your best day for follows on &quot;taliwutblog&quot;"
+ },
+ "timestamp": "1379602173",
+ "meta": {
+ "ids": {
+ "self": 838222030,
+ "site": 57991476
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/838222030",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476"
+ },
+ "blog_id": "57991476"
+ }
+ }, {
+ "id": "836484714",
+ "type": "best_followed_day_feat",
+ "unread": "0",
+ "body": {
+ "template": "big-badge",
+ "header": "Le + de follows en un jour",
+ "header_text": "Le + de follows en un jour",
+ "header_link": "",
+ "html": "<p>Au Mercredi 18 septembre 2013 vous avez pulv\u00e9ris\u00e9 votre pr\u00e9c\u00e9dent record de follows enregistr\u00e9s en un seul jour, sur votre blog <a href=\"http:\/\/taliwutblog.wordpress.com\" title=\"taliwutblog\" target=\"_blank\" notes-data-click=\"best_period_ever_feat\">taliwutblog<\/a>. Super!<\/p><ul><li><span class=\"wpn-feat-current-record-title\">Current Record: <\/span><span class=\"wpn-feat-new-record-count\">20<\/span><\/li><li><span class=\"wpn-feat-old-record-title\">Old Record: <\/span><span class=\"wpn-feat-old-record-count\">1<\/span><\/li><\/ul>",
+ "objects": {
+ "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476": false
+ }
+ },
+ "subject": {
+ "icon": "https:\/\/wordpress.com\/wp-content\/mu-plugins\/achievements\/bestday-follows-2x.png",
+ "noticon": "https:\/\/wordpress.com\/wp-content\/mu-plugins\/notes\/images\/noticon-milestone.png",
+ "html": "septembre 18: Your best day for follows on <span class=\"wpn-subject-title\">&quot;taliwutblog&quot;<\/span>",
+ "text": "septembre 18: Your best day for follows on &quot;taliwutblog&quot;"
+ },
+ "timestamp": "1379492390",
+ "meta": {
+ "ids": {
+ "self": 836484714,
+ "site": 57991476
+ },
+ "links": {
+ "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/notifications\/836484714",
+ "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/57991476"
+ },
+ "blog_id": "57991476"
+ }
+ }],
+ "last_seen_time": "2147483647",
+ "number": 35
+}
diff --git a/WordPress/src/androidTest/assets/site-reserved-public-api-wordpress-com-rest-v1-sites-new.json b/WordPress/src/androidTest/assets/site-reserved-public-api-wordpress-com-rest-v1-sites-new.json
new file mode 100644
index 000000000..5077f3775
--- /dev/null
+++ b/WordPress/src/androidTest/assets/site-reserved-public-api-wordpress-com-rest-v1-sites-new.json
@@ -0,0 +1,4 @@
+{
+ "error": "blog_name_reserved",
+ "message": "Invalid blog name"
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/assets/taliwutt-blogs-sample.sql b/WordPress/src/androidTest/assets/taliwutt-blogs-sample.sql
new file mode 100644
index 000000000..d3d4bcfba
--- /dev/null
+++ b/WordPress/src/androidTest/assets/taliwutt-blogs-sample.sql
@@ -0,0 +1,48 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE android_metadata (locale TEXT);
+INSERT INTO "android_metadata" VALUES('en_US');
+CREATE TABLE accounts (id integer primary key autoincrement, url text, blogName text, username text, password text, imagePlacement text, centerThumbnail boolean, fullSizeImage boolean, maxImageWidth text, maxImageWidthId integer, lastCommentId integer, runService boolean, blogId integer, location boolean default false, dotcom_username text, dotcom_password text, api_key text, api_blogid text, dotcomFlag boolean default false, wpVersion text, httpuser text, httppassword text, postFormats text default '', isScaledImage boolean default false, scaledImgWidth integer default 1024, homeURL text default '', blog_options text default '', isAdmin boolean default false, isHidden boolean default 0);
+INSERT INTO accounts VALUES(31,'https://tataliwut.wordpress.com/xmlrpc.php','Empty blog stays empty','test','','',1,0,'2000',0,NULL,0,59073674,0,NULL,NULL,NULL,NULL,1,'','','','{"audio":"Son","standard":"Par défaut","status":"Statut","gallery":"Galerie","quote":"Citation","link":"Lien","image":"Image","chat":"Discussion","aside":"En passant","video":"Vidéo"}',0,1024,'https://tataliwut.wordpress.com/','{"home_url":{"value":"http://tataliwut.wordpress.com","readonly":true,"desc":"Adresse web du site (URL)"},"post_thumbnail":{"value":true,"readonly":true,"desc":"Miniature d\u0026rsquo;article"},"software_version":{"value":"3.8.1","readonly":true,"desc":"Software Version"},"blog_public":{"value":"1","readonly":true,"desc":"Accès aux renseignements confidentiels"},"admin_url":{"value":"https://tataliwut.wordpress.com/wp-admin/","readonly":true,"desc":"L\u0026rsquo;adresse web de la zone d\u0026rsquo;administration"},"login_url":{"value":"https://tataliwut.wordpress.com/wp-login.php","readonly":true,"desc":"Adresse de connexion (URL)"}}',1,0);
+INSERT INTO accounts VALUES(32,'https://ilovecutecats.wordpress.com/xmlrpc.php','I Love Cute Cats','test','','',0,0,'2000',0,NULL,0,61784930,'false',NULL,NULL,NULL,NULL,1,'','','','','false',1024,'https://ilovecutecats.wordpress.com/','',1,0);
+INSERT INTO accounts VALUES(22,'https://taliwutblog.wordpress.com/xmlrpc.php','taliwut & blog','test','','',0,0,'2000',0,NULL,0,57991476,'false',NULL,NULL,NULL,NULL,1,'','','','','false',1024,'https://taliwutblog.wordpress.com/','',1,0);
+CREATE TABLE posts (id integer primary key autoincrement, blogID text, postid text, title text default '', dateCreated date, date_created_gmt date, categories text default '', custom_fields text default '', description text default '', link text default '', mt_allow_comments boolean, mt_allow_pings boolean, mt_excerpt text default '', mt_keywords text default '', mt_text_more text default '', permaLink text default '', post_status text default '', userid integer default 0, wp_author_display_name text default '', wp_author_id text default '', wp_password text default '', wp_post_format text default '', wp_slug text default '', mediaPaths text default '', latitude real, longitude real, localDraft boolean default 0, uploaded boolean default 0, isPage boolean default 0, wp_page_parent_id text, wp_page_parent_title text, isLocalChange boolean default 0);
+INSERT INTO posts VALUES(1,'1',NULL,'qswq',NULL,0,'[]','','<p>qwswqs<br></p>','',NULL,NULL,'','','','','publish',0,'','','','standard','','',0.0,0.0,1,1,0,NULL,NULL,0);
+INSERT INTO posts VALUES(5,'1','23','qswq',1392633989000,1392633989000,'["Uncategorized"]','["{value=1392633989, id=91, key=jabber_published}"]','qwswqsarst','https://tataliwut.wordpress.com/2014/02/17/qswq/',1,1,'','','','https://tataliwut.wordpress.com/2014/02/17/qswq/','publish',55434822,'taliwutt','55434822','','standard','qswq','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(26,'16','306','enft',1392134640000,1392134640000,'["Uncategorized"]','["{value=1392048234, id=1411, key=jabber_published}"]','','https://taliwutblog.wordpress.com/2014/02/11/enft/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/11/enft/','publish',55434822,'taliwutt','55434822','','standard','enft','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(27,'22','333','Fun with URLs',1392118235000,1392118235000,'["Uncategorized"]','["{value=1392118236, id=1425, key=jabber_published}"]','<a href="//">test 1</a> <a href="//login">Test 2</a>','https://taliwutblog.wordpress.com/2014/02/11/fun-with-urls/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/11/fun-with-urls/','publish',55434822,'taliwutt','55434822','','standard','fun-with-urls','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(28,'22','331','wftfwtfw',1392050448000,1392050448000,'["Uncategorized"]','["{value=1392050448, id=1418, key=jabber_published}"]','hb','https://taliwutblog.wordpress.com/2014/02/10/wftfwtfw/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/10/wftfwtfw/','publish',55434822,'taliwutt','55434822','','standard','wftfwtfw','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(29,'22','330','#BookReview Bridget Jones: Mad About the Boy',1391606384000,1391606384000,'["Uncategorized"]','["{value=57991476, id=1406, key=blog_id}","{value=1, id=1407, key=is_reblog}","{value=1391606386, id=1400, key=jabber_published}","{value=302, id=1405, key=post_id}"]','jey*','https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-4/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-4/','publish',55434822,'taliwutt','55434822','','standard','bookreview-bridget-jones-mad-about-the-boy-4','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(30,'22','329','#BookReview Bridget Jones: Mad About the Boy',1391606378000,1391606378000,'["Uncategorized"]','["{value=57991476, id=1397, key=blog_id}","{value=1, id=1398, key=is_reblog}","{value=1391606379, id=1391, key=jabber_published}","{value=302, id=1396, key=post_id}"]','e*','https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-3/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/05/bookreview-bridget-jones-mad-about-the-boy-3/','publish',55434822,'taliwutt','55434822','','standard','bookreview-bridget-jones-mad-about-the-boy-3','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(31,'22','327','Watch "NORMAN - LUIGI CLASH MARIO" on YouTube',1391604133000,1391604133000,'["Uncategorized"]','["{value=1391604135, id=1384, key=jabber_published}"]','<a href="http://www.youtube.com/watch?v=GP7aP67qQjQ">http://www.youtube.com/watch?v=GP7aP67qQjQ</a>','https://taliwutblog.wordpress.com/2014/02/05/watch-norman-luigi-clash-mario-on-youtube/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/05/watch-norman-luigi-clash-mario-on-youtube/','publish',55434822,'taliwutt','55434822','','standard','watch-norman-luigi-clash-mario-on-youtube','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(32,'22','325','Test YouTube',1391584473000,1391584473000,'["Uncategorized"]','["{value=1391584474, id=1377, key=jabber_published}"]','[youtube https://www.youtube.com/watch?v=belUlgnhu9M&w=560&h=315]','https://taliwutblog.wordpress.com/2014/02/05/test-youtube/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/05/test-youtube/','publish',55434822,'taliwutt','55434822','','standard','test-youtube','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(33,'22','324','Your Keyboard &amp; You. I''ll Stick With Colemak',1391448765000,1391448765000,'["Uncategorized"]','["{value=56042455, id=1372, key=blog_id}","{value=1, id=1373, key=is_reblog}","{value=1391448767, id=1366, key=jabber_published}","{value=631, id=1371, key=post_id}"]','','https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak-2/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak-2/','publish',55434822,'taliwutt','55434822','','standard','your-keyboard-you-ill-stick-with-colemak-2','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(34,'22','323','Your Keyboard &amp; You. I''ll Stick With Colemak',1391448756000,1391448756000,'["Uncategorized"]','["{value=56042455, id=1363, key=blog_id}","{value=1, id=1364, key=is_reblog}","{value=1391448758, id=1357, key=jabber_published}","{value=631, id=1362, key=post_id}"]','','https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/03/your-keyboard-you-ill-stick-with-colemak/','publish',55434822,'taliwutt','55434822','','standard','your-keyboard-you-ill-stick-with-colemak','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(35,'22','321','Forget about turns, this is real time chess. It''s hectic, chaotic and short. The winner is the guy who is able to process everything and cope under stress. Welcome to the new chess experience',1391431046000,1391431046000,'["Uncategorized"]','["{value=1391431048, id=1350, key=jabber_published}"]','<a href="http://impactjs.com/forums/games/real-time-chess">http://impactjs.com/forums/games/real-time-chess</a>','https://taliwutblog.wordpress.com/2014/02/03/forget-about-turns-this-is-real-time-chess-its-hectic-chaotic-and-short-the-winner-is-the-guy-who-is-able-to-process-everything-and-cope-under-stress-welcome-to-the-new-chess-experience/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/03/forget-about-turns-this-is-real-time-chess-its-hectic-chaotic-and-short-the-winner-is-the-guy-who-is-able-to-process-everything-and-cope-under-stress-welcome-to-the-new-chess-experience/','publish',55434822,'taliwutt','55434822','','standard','forget-about-turns-this-is-real-time-chess-its-hectic-chaotic-and-short-the-winner-is-the-guy-who-is-able-to-process-everything-and-cope-under-stress-welcome-to-the-new-chess-experience','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(36,'22','319','Another Pony',1391428271000,1391428271000,'["Uncategorized"]','["{value=1391428272, id=1342, key=jabber_published}"]','<a href="http://taliwutblog.files.wordpress.com/2014/02/wpid-urbanherovantbarreeltje.jpg"><img title="Urbanherovantbarreeltje.JPG" class="alignnone size-full" alt="image" src="http://taliwutblog.files.wordpress.com/2014/02/wpid-urbanherovantbarreeltje.jpg" /></a>','https://taliwutblog.wordpress.com/2014/02/03/another-pony/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/03/another-pony/','publish',55434822,'taliwutt','55434822','','image','another-pony','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(37,'22','316','Cute pony',1391422828000,1391422828000,'["Uncategorized"]','["{value=1391422828, id=1333, key=jabber_published}"]','<a href="http://taliwutblog.files.wordpress.com/2014/02/wpid-pony1.jpg"><img title="pony.jpg" class="alignnone size-full" alt="image" src="http://taliwutblog.files.wordpress.com/2014/02/wpid-pony1.jpg" /></a>','https://taliwutblog.wordpress.com/2014/02/03/cute-pony-2/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/03/cute-pony-2/','publish',55434822,'taliwutt','55434822','','image','cute-pony-2','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(38,'22','313','Cute pony',1391420955000,1391420955000,'["Uncategorized"]','["{value=1391420956, id=1324, key=jabber_published}"]','<a href="http://taliwutblog.files.wordpress.com/2014/02/wpid-pony.jpg"><img title="pony.jpg" class="alignnone size-full" alt="image" src="http://taliwutblog.files.wordpress.com/2014/02/wpid-pony.jpg" /></a>','https://taliwutblog.wordpress.com/2014/02/03/cute-pony/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/03/cute-pony/','publish',55434822,'taliwutt','55434822','','image','cute-pony','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(39,'22','303','The New iOS and Android Apps Have Arrived!',1391108568000,1391108568000,'["Uncategorized"]','["{value=3584907, id=1290, key=blog_id}","{value=1, id=1291, key=is_reblog}","{value=1391108569, id=1286, key=jabber_published}","{value=23682, id=1289, key=post_id}","{value=O:8:\"stdClass\":8:{s:5:\"title\";s:42:\"The New iOS and Android Apps Have Arrived!\";s:4:\"type\";s:4:\"post\";s:9:\"mime_type\";s:0:\"\";s:6:\"format\";b:0;s:12:\"modified_gmt\";s:19:\"2014-01-30 18:59:14\";s:9:\"permalink\";s:60:\"http:\/\/en.blog.wordpress.com\/2014\/01\/30\/updated-ios-android\/\";s:7:\"content\";s:3728:\"<div class=\"reblogged-content\">More and more of us are blogging from our mobile devices. Today, we’re thrilled to announce new versions of the WordPress mobile apps for Android and iOS. Here are some of the new versions'' highlights. <img class=\"aligncenter size-full wp-image-23687\" alt=\"android26-ios39-promo\" src=\"http:\/\/taliwutblog.files.wordpress.com\/2014\/01\/android26-ios39-promo.png\" width=\"635\" height=\"423\" data-originalSrc=\"http:\/\/en.blog.files.wordpress.com\/2014\/01\/android26-ios39-promo.png\" data-rehosted=\"1\"><h3>WordPress for iOS 3.9<\/h3>\n The latest WordPress for iOS update is one of our largest app releases to date. This update is remarkable both for the significant changes we''ve introduced, and for the level of dedication it received from our hard-working team members. Version 3.9 includes a major visual redesign of the app. We decided to drop the sidebar navigation and embrace a tab bar-based layout. The app''s new design allowed us to add numerous visual improvements throughout, including revamped and enhanced Reader, Comments, and Notifications sections. We also created a seamless inline commenting experience to make it easier for you to engage with the content you love. Finally, we made visual improvements to the editing experience of posts and pages.\n Our team has embraced the latest and greatest technologies that Apple has provided us with iOS 7 to deliver you the best app possible. Version 3.9 and future updates will require iOS 7. The app also includes several other changes. On top of various bug fixes and performance improvements, it now supports deep-linking from Twitter, and features an improved login screen. Be sure to try it out: <a href=\"https:\/\/itunes.apple.com\/us\/app\/wordpress\/id335703880?mt=8\">Download from the App Store<\/a> <h3>WordPress for Android 2.6<\/h3>\n The latest update to WordPress for Android includes a new reading and setup experience, as well as significant updates to the user interface. The app Reader has been completely redesigned, and now provides a much-improved, native reading experience. You''ll definitely notice its speed -- posts appear in a snap, and images fade in as they load. You can also view users that have commented or liked posts, as well as edit the list of tags that you follow. We''ve revamped the like, reblog, and comment interfaces to make it easier than ever to respond to posts that strike your fancy.\n When signing in to the app or creating an account on WordPress.com, you’ll notice a brand new user interface that makes it super-simple to start blogging. If you keep multiple blogs on your account, they will all be automatically added for you. You can also hide whichever blogs you don''t wish to work on in the app. We''ve given the app a facelift, including a new color scheme, a refined navigation drawer layout, and sharp-looking lists in notifications, posts, pages and comments.\n You’ll also notice some changes to the post editor, with larger images and a new Post Settings area where you''ll manage post data such as status, post formats, and categories, among others. The post content area will now go full screen while you are editing, to give you maximum space to focus on your content. Give the app a try here: <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=org.wordpress.android\">Download from Google Play<\/a> <h3>What’s next?<\/h3>\n The mobile team isn’t stopping here! We have big plans for the months to come and for the rest of 2014. You can keep up with the development progress over at http:\/\/make.wordpress.org\/mobile. You can also follow the apps on twitter <a href=\"http:\/\/twitter.com\/wpandroid\">@WPAndroid<\/a> and <a href=\"http:\/\/twitter.com\/wordpressios\">@WordPressiOS<\/a>.<\/div>\";s:15:\"images_mirrored\";i:1;}, id=1297, key=reblog_snapshot}"]','','https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/30/the-new-ios-and-android-apps-have-arrived/','publish',50540106,'Maxime','50540106','','standard','the-new-ios-and-android-apps-have-arrived','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(40,'22','308','art',1391076000000,1391076000000,'["Uncategorized"]','["{value=1391162423, id=1304, key=jabber_published}"]','<strong>wft</strong>','https://taliwutblog.wordpress.com/2014/01/30/art/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/30/art/','publish',55434822,'taliwutt','55434822','','standard','art','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(41,'22','302','#BookReview Bridget Jones: Mad About the Boy',1390903286000,1390903286000,'["Uncategorized"]','["{value=31639867, id=1282, key=blog_id}","{value=1, id=1283, key=is_reblog}","{value=1390903288, id=1276, key=jabber_published}","{value=7493, id=1281, key=post_id}"]','ib/Tqg','https://taliwutblog.wordpress.com/2014/01/28/bookreview-bridget-jones-mad-about-the-boy-2/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/28/bookreview-bridget-jones-mad-about-the-boy-2/','publish',55434822,'taliwutt','55434822','','standard','bookreview-bridget-jones-mad-about-the-boy-2','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(42,'22','299','Cat',1390395827000,1390395827000,'["Uncategorized"]','["{value=1390395828, id=1250, key=jabber_published}"]','[caption id="" align="alignnone" width="2000" caption="Mop"]<a href="https://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg"><img title="wpid-wp-1388141726961.jpeg" class="alignnone size-full" alt="image" src="https://taliwutblog.files.wordpress.com/2013/12/wpid-wp-1388141726961.jpeg?w=2000" /></a>[/caption]','https://taliwutblog.wordpress.com/2014/01/22/cat-2/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/22/cat-2/','publish',55434822,'taliwutt','55434822','','standard','cat-2','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(43,'22','297','hd wallpaper photo - Google Search',1390395578000,1390395578000,'["Uncategorized"]','["{value=1390395578, id=1244, key=jabber_published}"]','<a href="http://www.google.com/search?hl=en&amp;biw=384&amp;bih=640&amp;tbm=isch&amp;sa=1&amp;ei=RbLKUv-lHYem0wW8oYHABA&amp;q=hd+wallpaper+photo&amp;oq=hd+wallpaper+photo&amp;gs_l=mobile-gws-serp.3..0l4j0i5.4612.5034.0.5279.4.4.0.0.0.0.203.203.2-1.1.0....0...1c.1.32.mobile-gws-serp..3.1.203.63ZBZ9KeMPs#biv=i%7C10%3Bd%7Cfmk-ugJVQIMDBM%3A">http://www.google.com/search?hl=en&amp;biw=384&amp;bih=640&amp;tbm=isch&amp;sa=1&amp;ei=RbLKUv-lHYem0wW8oYHABA&amp;q=hd+wallpaper+photo&amp;oq=hd+wallpaper+photo&amp;gs_l=mobile-gws-serp.3..0l4j0i5.4612.5034.0.5279.4.4.0.0.0.0.203.203.2-1.1.0....0...1c.1.32.mobile-gws-serp..3.1.203.63ZBZ9KeMPs#biv=i%7C10%3Bd%7Cfmk-ugJVQIMDBM%3A</a>','https://taliwutblog.wordpress.com/2014/01/22/hd-wallpaper-photo-google-search/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/22/hd-wallpaper-photo-google-search/','publish',55434822,'taliwutt','55434822','','standard','hd-wallpaper-photo-google-search','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(44,'22','296','The End of Unrecorded Life',1390394180000,1390394180000,'["Uncategorized"]','["{value=55267051, id=1241, key=blog_id}","{value=1, id=1242, key=is_reblog}","{value=1390394182, id=1235, key=jabber_published}","{value=642, id=1240, key=post_id}"]','Hai','https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/22/the-end-of-unrecorded-life/','publish',55434822,'taliwutt','55434822','','standard','the-end-of-unrecorded-life','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(45,'22','292','Geny!',1390386760000,1390386760000,'["Uncategorized"]','["{value=1390386762, id=1228, key=jabber_published}"]','<a href="http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386748907.jpg"><img title="wp-1390386748907.jpg" class="alignnone size-full" alt="image" src="http://taliwutblog.files.wordpress.com/2014/01/wpid-wp-1390386748907.jpg" /></a>','https://taliwutblog.wordpress.com/2014/01/22/geny/',1,1,'','','','https://taliwutblog.wordpress.com/2014/01/22/geny/','publish',55434822,'taliwutt','55434822','','image','geny','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(47,'59073674',NULL,NULL,NULL,0,'','',NULL,'',NULL,NULL,NULL,'','','',NULL,0,'','',NULL,NULL,'','',0.0,0.0,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(48,'31','333333','null postid',1392134640000,1392134640000,'["Uncategorized"]','["{value=1392048234, id=1411, key=jabber_published}"]','','https://taliwutblog.wordpress.com/2014/02/11/enft/',1,1,'','','','https://taliwutblog.wordpress.com/2014/02/11/enft/','publish',55434822,'taliwutt','55434822','','standard','enft','',NULL,NULL,0,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(49,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(50,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(51,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(52,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(53,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(54,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+INSERT INTO posts VALUES(55,'1',NULL,'',NULL,0,'','','','',NULL,NULL,'','','','','',0,'','','','','','',0.0,0.0,1,0,0,NULL,NULL,0);
+CREATE TABLE cats (id integer primary key autoincrement, blog_id text, wp_id integer, category_name text not null, parent_id integer default 0);
+CREATE TABLE quickpress_shortcuts (id integer primary key autoincrement, accountId text, name text);
+CREATE TABLE media (id integer primary key autoincrement, postID integer not null, filePath text default '', fileName text default '', title text default '', description text default '', caption text default '', horizontalAlignment integer default 0, width integer default 0, height integer default 0, mimeType text default '', featured boolean default false, isVideo boolean default false, isFeaturedInPost boolean default false, fileURL text default '', thumbnailURL text default '', mediaId text default '', blogId text default '', date_created_gmt date, uploadState default '', videoPressShortcode text default '');
+CREATE TABLE themes (_id integer primary key autoincrement, themeId text, name text, description text, screenshotURL text, trendingRank integer default 0, popularityRank integer default 0, launchDate date, previewURL text, blogId text, isCurrent boolean default false, isPremium boolean default false, features text);
+CREATE TABLE notes (id integer primary key, note_id text, message text, type text, raw_note_data text, timestamp integer, placeholder boolean);
+CREATE TABLE comments ( blog_id INTEGER DEFAULT 0, post_id INTEGER DEFAULT 0, comment_id INTEGER DEFAULT 0, comment TEXT, published TEXT, status TEXT, author_name TEXT, author_url TEXT, author_email TEXT, post_title TEXT, profile_image_url TEXT, PRIMARY KEY (blog_id, post_id, comment_id) );
+INSERT INTO comments VALUES(4,313,165,'ouh ouh','2014-02-10T14:46:27+0100','approve','ARST','','arst@srt.ts','Cute pony','');
+COMMIT;
diff --git a/WordPress/src/androidTest/assets/username-exists-public-api-wordpress-com-rest-v1-users-new.json b/WordPress/src/androidTest/assets/username-exists-public-api-wordpress-com-rest-v1-users-new.json
new file mode 100644
index 000000000..988732345
--- /dev/null
+++ b/WordPress/src/androidTest/assets/username-exists-public-api-wordpress-com-rest-v1-users-new.json
@@ -0,0 +1,4 @@
+{
+ "error": "username_exists",
+ "message": "Invalid user input"
+}
diff --git a/WordPress/src/androidTest/java/URITest.java b/WordPress/src/androidTest/java/URITest.java
new file mode 100644
index 000000000..f0d2c73f6
--- /dev/null
+++ b/WordPress/src/androidTest/java/URITest.java
@@ -0,0 +1,56 @@
+import android.test.InstrumentationTestCase;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class URITest extends InstrumentationTestCase {
+ public void testGetHost1() {
+ URI uri = null;
+ try {
+ uri = new URI("https://wordpress.com");
+ } catch (URISyntaxException e) {}
+ assertNotNull(uri);
+
+ assertEquals("wordpress.com", uri.getHost());
+ }
+
+ public void testGetHost2() {
+ URI uri = null;
+ try {
+ uri = new URI("http://a.com#.b.com/test");
+ } catch (URISyntaxException e) {}
+ assertNotNull(uri);
+
+ assertEquals("a.com", uri.getHost());
+ }
+
+ public void testGetHost3() {
+ URI uri = null;
+ try {
+ uri = new URI("https://a.com");
+ } catch (URISyntaxException e) {}
+ assertNotNull(uri);
+
+ assertEquals("a.com", uri.getHost());
+ }
+
+ public void testGetHost4() {
+ URI uri = null;
+ try {
+ uri = new URI("https://a.com/test#test");
+ } catch (URISyntaxException e) {}
+ assertNotNull(uri);
+
+ assertEquals("a.com", uri.getHost());
+ }
+
+ public void testGetHost5() {
+ URI uri = null;
+ try {
+ uri = new URI("a.com");
+ } catch (URISyntaxException e) {}
+ assertNotNull(uri);
+
+ assertNull(uri.getHost());
+ }
+}
diff --git a/WordPress/src/androidTest/java/URLTest.java b/WordPress/src/androidTest/java/URLTest.java
new file mode 100644
index 000000000..04a225029
--- /dev/null
+++ b/WordPress/src/androidTest/java/URLTest.java
@@ -0,0 +1,55 @@
+import android.test.InstrumentationTestCase;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class URLTest extends InstrumentationTestCase {
+ public void testGetHost1() {
+ URL url = null;
+ try {
+ url = new URL("https://wordpress.com");
+ } catch (MalformedURLException e) {}
+ assertNotNull(url);
+
+ assertEquals("wordpress.com", url.getHost());
+ }
+
+ public void testGetHost2() {
+ URL url = null;
+ try {
+ url = new URL("http://a.com#.b.com/test");
+ } catch (MalformedURLException e) {}
+ assertNotNull(url);
+
+ assertEquals("a.com", url.getHost());
+ }
+
+ public void testGetHost3() {
+ URL url = null;
+ try {
+ url = new URL("https://a.com");
+ } catch (MalformedURLException e) {}
+ assertNotNull(url);
+
+ assertEquals("a.com", url.getHost());
+ }
+
+ public void testGetHost4() {
+ URL url = null;
+ try {
+ url = new URL("https://a.com/test#test");
+ } catch (MalformedURLException e) {}
+ assertNotNull(url);
+
+ assertEquals("a.com", url.getHost());
+ }
+
+ public void testGetHost5() {
+ URL url = null;
+ try {
+ url = new URL("a.com");
+ } catch (MalformedURLException e) {}
+
+ assertNull(url);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/DefaultMocksInstrumentationTestCase.java b/WordPress/src/androidTest/java/org/wordpress/android/DefaultMocksInstrumentationTestCase.java
new file mode 100644
index 000000000..5443c82fa
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/DefaultMocksInstrumentationTestCase.java
@@ -0,0 +1,42 @@
+package org.wordpress.android;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+
+import org.wordpress.android.mocks.RestClientFactoryTest;
+import org.wordpress.android.mocks.XMLRPCFactoryTest;
+import org.wordpress.android.util.AppLog;
+
+public class DefaultMocksInstrumentationTestCase extends InstrumentationTestCase {
+ protected Context mTargetContext;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ FactoryUtils.initWithTestFactories();
+
+ mTargetContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_");
+ TestUtils.clearApplicationState(mTargetContext);
+
+ // Init contexts
+ XMLRPCFactoryTest.sContext = getInstrumentation().getContext();
+ RestClientFactoryTest.sContext = getInstrumentation().getContext();
+ AppLog.v(AppLog.T.TESTS, "Contexts set");
+
+ // Set mode to Customizable
+ XMLRPCFactoryTest.sMode = XMLRPCFactoryTest.Mode.CUSTOMIZABLE_XML;
+ RestClientFactoryTest.sMode = RestClientFactoryTest.Mode.CUSTOMIZABLE;
+ AppLog.v(AppLog.T.TESTS, "Modes set to customizable");
+
+ // Set default variant
+ RestClientFactoryTest.setPrefixAllInstances("default");
+ XMLRPCFactoryTest.setPrefixAllInstances("default");
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ FactoryUtils.clearFactories();
+ super.tearDown();
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/FactoryUtils.java b/WordPress/src/androidTest/java/org/wordpress/android/FactoryUtils.java
new file mode 100644
index 000000000..57dadc937
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/FactoryUtils.java
@@ -0,0 +1,45 @@
+package org.wordpress.android;
+
+import org.wordpress.android.mocks.OAuthAuthenticatorFactoryTest;
+import org.wordpress.android.mocks.RestClientFactoryTest;
+import org.wordpress.android.mocks.SystemServiceFactoryTest;
+import org.wordpress.android.mocks.XMLRPCFactoryTest;
+import org.wordpress.android.networking.OAuthAuthenticatorFactory;
+import org.wordpress.android.networking.RestClientFactory;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.SystemServiceFactory;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.lang.reflect.Field;
+
+public class FactoryUtils {
+ public static void clearFactories() {
+ // clear factories
+ forceFactoryInjection(XMLRPCFactory.class, null);
+ forceFactoryInjection(RestClientFactory.class, null);
+ forceFactoryInjection(OAuthAuthenticatorFactory.class, null);
+ forceFactoryInjection(SystemServiceFactory.class, null);
+ AppLog.v(T.TESTS, "Null factories set");
+ }
+
+ public static void initWithTestFactories() {
+ // create test factories
+ forceFactoryInjection(XMLRPCFactory.class, new XMLRPCFactoryTest());
+ forceFactoryInjection(RestClientFactory.class, new RestClientFactoryTest());
+ forceFactoryInjection(OAuthAuthenticatorFactory.class, new OAuthAuthenticatorFactoryTest());
+ forceFactoryInjection(SystemServiceFactory.class, new SystemServiceFactoryTest());
+ AppLog.v(T.TESTS, "Mocks factories instantiated");
+ }
+
+ private static void forceFactoryInjection(Class klass, Object factory) {
+ try {
+ Field field = klass.getDeclaredField("sFactory");
+ field.setAccessible(true);
+ field.set(null, factory);
+ AppLog.v(T.TESTS, "Factory " + klass + " injected");
+ } catch (Exception e) {
+ AppLog.e(T.TESTS, "Can't inject test factory " + klass);
+ }
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/PostUploadServiceTest.java b/WordPress/src/androidTest/java/org/wordpress/android/PostUploadServiceTest.java
new file mode 100644
index 000000000..29c28f9d4
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/PostUploadServiceTest.java
@@ -0,0 +1,101 @@
+package org.wordpress.android;
+
+import android.content.Context;
+import android.content.Intent;
+import android.test.RenamingDelegatingContext;
+import android.test.ServiceTestCase;
+
+import org.wordpress.android.mocks.RestClientFactoryTest;
+import org.wordpress.android.mocks.XMLRPCFactoryTest;
+import org.wordpress.android.ui.posts.services.PostUploadService;
+import org.wordpress.android.util.AppLog;
+
+public class PostUploadServiceTest extends ServiceTestCase<PostUploadService> {
+ protected Context testContext;
+ protected Context targetContext;
+
+ public PostUploadServiceTest() {
+ super(PostUploadService.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ FactoryUtils.initWithTestFactories();
+
+ String namespace = BuildConfig.FLAVOR.equals("wasabi") ? "org.wordpress.android.beta"
+ : "org.wordpress.android";
+ testContext = getContext().createPackageContext(namespace, Context.CONTEXT_IGNORE_SECURITY);
+ targetContext = new RenamingDelegatingContext(getContext(), "test_");
+
+ // Init contexts
+ XMLRPCFactoryTest.sContext = getContext();
+ RestClientFactoryTest.sContext = getContext();
+ AppLog.v(AppLog.T.TESTS, "Contexts set");
+
+ // Set mode to Customizable
+ XMLRPCFactoryTest.sMode = XMLRPCFactoryTest.Mode.CUSTOMIZABLE_XML;
+ RestClientFactoryTest.sMode = RestClientFactoryTest.Mode.CUSTOMIZABLE;
+ AppLog.v(AppLog.T.TESTS, "Modes set to customizable");
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ FactoryUtils.clearFactories();
+ super.tearDown();
+ }
+
+ public void testStartable() {
+ Intent startIntent = new Intent();
+ startIntent.setClass(getContext(), PostUploadService.class);
+ startService(startIntent);
+ }
+
+ // test reproducing https://github.com/wordpress-mobile/WordPress-Android/issues/884
+ // Following test is valid but won't be fixed now (it will with the post editor rewrite)
+ /*
+ public void testUploadMalformedPostNullPostId() throws Exception {
+ // init a test db containing a few blogs and posts
+ SQLiteDatabase db = TestUtils.loadDBFromDump(targetContext, testContext, "taliwutt-blogs-sample.sql");
+ WordPressDB wpdb = WordPress.wpDB;
+
+ // callback should be called 3 times
+ final CountDownLatch countDownLatch = new CountDownLatch(3);
+
+ // trick to have a mutable final int
+ final int[] notifyCount = {0};
+ final int[] cancelCount = {0};
+ SystemServiceFactoryTest.sNotificationCallback = new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ if ("notify".equals(invocation.getMethod().getName())) {
+ notifyCount[0] += 1;
+ }
+ if ("cancel".equals(invocation.getMethod().getName())) {
+ cancelCount[0] += 1;
+ }
+ countDownLatch.countDown();
+ return null;
+ }
+ };
+
+ // get an existing uploaded post (defined in the previously loaded db dump)
+ int postId = 27;
+ Post post = wpdb.getPostForLocalTablePostId(postId);
+
+ // fake the remote post id to null
+ post.setRemotePostId(null);
+
+ // push it to the PostUploadService
+ PostUploadService.addPostToUpload(post);
+ startService(new Intent(getContext(), PostUploadService.class));
+
+ // wait for the response
+ countDownLatch.await(15, TimeUnit.SECONDS);
+ assertTrue("NotificationManager.cancel must be called at least once - see #884",
+ cancelCount[0] == 1 && notifyCount[0] == 2);
+ }
+ */
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java b/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java
new file mode 100644
index 000000000..0bf1d8563
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/TestUtils.java
@@ -0,0 +1,168 @@
+package org.wordpress.android;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.sqlite.SQLiteDatabase;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.DateTimeUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+public class TestUtils {
+ private static String DATABASE_NAME = "wordpress";
+
+ public static SQLiteDatabase loadDBFromDump(Context targetContext, Context testContext, String filename) {
+ targetContext.deleteDatabase(DATABASE_NAME);
+ WordPress.wpDB = new WordPressDB(targetContext);
+
+ Field dbField;
+ try {
+ dbField = WordPressDB.class.getDeclaredField("db");
+ dbField.setAccessible(true);
+ SQLiteDatabase db = (SQLiteDatabase) dbField.get(WordPress.wpDB);
+ assertNotNull(db);
+
+ // Load file
+ InputStream is = testContext.getAssets().open(filename);
+ InputStreamReader inputStreamReader = new InputStreamReader(is);
+ BufferedReader f = new BufferedReader(inputStreamReader);
+ for (String line = f.readLine(); line != null; line = f.readLine()) {
+ if (TextUtils.isEmpty(line)) {
+ continue;
+ }
+ try {
+ db.execSQL(line);
+ } catch (android.database.sqlite.SQLiteException e) {
+ // ignore import errors
+ }
+ }
+ f.close();
+ return db;
+ } catch (NoSuchFieldException e) {
+ assertTrue(e.toString(), false);
+ } catch (IllegalAccessException e) {
+ assertTrue(e.toString(), false);
+ } catch (IOException e) {
+ assertTrue(e.toString(), false);
+ }
+ return null;
+ }
+
+ public static void resetEventBus() {
+ Field dbField;
+ try {
+ dbField = EventBus.class.getDeclaredField("defaultInstance");
+ dbField.setAccessible(true);
+ dbField.set(EventBus.class, null);
+ } catch (NoSuchFieldException e) {
+ assertTrue(e.toString(), false);
+ } catch (IllegalAccessException e) {
+ assertTrue(e.toString(), false);
+ }
+ }
+
+ public static void clearDefaultSharedPreferences(Context targetContext) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(targetContext);
+ Editor editor = settings.edit();
+ editor.clear();
+ editor.commit();
+ }
+
+ public static void dropDB(Context targetContext) {
+ targetContext.deleteDatabase(DATABASE_NAME);
+ }
+
+ public static void clearApplicationState(Context context) {
+ WordPress.currentBlog = null;
+ if (WordPress.getContext() != null) {
+ try {
+ WordPress.WordPressComSignOut(context);
+ } catch (Exception e) {
+ // noop
+ }
+ }
+ TestUtils.clearDefaultSharedPreferences(context);
+ TestUtils.dropDB(context);
+ }
+
+ public static String convertStreamToString(java.io.InputStream is) {
+ java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+
+ public static Date gsonStringToJavaDate(final String strDate) {
+ try {
+ SimpleDateFormat df = new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a", Locale.ENGLISH);
+ return df.parse(strDate);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ public static Date parseStringToDate(String value) {
+ // try do parseit as a Date
+ Date newValue = DateTimeUtils.dateFromIso8601(value);
+ if (newValue != null) {
+ return newValue;
+ }
+ newValue = gsonStringToJavaDate(value);
+ if (newValue != null) {
+ return newValue;
+ }
+ return null;
+ }
+
+ public static Object castIt(Object value) {
+ if (value instanceof HashMap) {
+ return injectDateInMap((Map<String, Object>) value);
+ } else if (value instanceof String) {
+ Date newValue = parseStringToDate((String) value);
+ if (newValue != null) {
+ return newValue;
+ } else {
+ return value;
+ }
+ } else if (value instanceof Double) {
+ return (int) Math.round((Double) value);
+ } else if (value instanceof Object[]) {
+ return injectDateInArray((Object[]) value);
+ }
+ return value;
+ }
+
+ public static Object[] injectDateInArray(Object[] array) {
+ HashSet<Object> res = new HashSet<Object>();
+ for (Object value : array) {
+ res.add(castIt(value));
+ }
+ return res.toArray();
+ }
+
+ public static Map<String, Object> injectDateInMap(Map<String, Object> hashMap) {
+ Map<String, Object> res = new HashMap<String, Object>();
+ for (String key : hashMap.keySet()) {
+ Object value = hashMap.get(key);
+ res.put(key, castIt(value));
+ }
+ return res;
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java b/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java
new file mode 100644
index 000000000..ec2fd467d
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java
@@ -0,0 +1,29 @@
+package org.wordpress.android;
+
+import junit.framework.TestCase;
+
+public class UserAgentTest extends TestCase {
+
+ /**
+ * Copy of {@link WordPress#USER_AGENT_APPNAME}.
+ * Copied here in order to be able to catch User-Agent changes and verify that they're intentional.
+ */
+ private static final String USER_AGENT_APPNAME = "wp-android";
+
+ public void testGetDefaultUserAgent() {
+ String defaultUserAgent = WordPress.getDefaultUserAgent();
+ assertNotNull("Default User-Agent must be set", defaultUserAgent);
+ assertTrue("Default User-Agent must not be an empty string", defaultUserAgent.length() > 0);
+ assertFalse("Default User-Agent must not contain app name", defaultUserAgent.contains(USER_AGENT_APPNAME));
+ }
+
+ public void testGetUserAgent() {
+ String userAgent = WordPress.getUserAgent();
+ assertNotNull("User-Agent must be set", userAgent);
+ assertTrue("User-Agent must not be an empty string", userAgent.length() > 0);
+ assertTrue("User-Agent must contain app name substring", userAgent.contains(USER_AGENT_APPNAME));
+
+ String defaultUserAgent = WordPress.getDefaultUserAgent();
+ assertTrue("User-Agent must be derived from default User-Agent", userAgent.contains(defaultUserAgent));
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/database/CommentTableTest.java b/WordPress/src/androidTest/java/org/wordpress/android/database/CommentTableTest.java
new file mode 100644
index 000000000..1f3aaee86
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/database/CommentTableTest.java
@@ -0,0 +1,61 @@
+package org.wordpress.android.database;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Comment;
+
+public class CommentTableTest extends InstrumentationTestCase {
+ protected Context mTargetContext;
+ protected Context mTestContext;
+
+ @Override
+ protected void setUp() throws Exception {
+ // Clean application state
+ mTargetContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_");
+ mTestContext = getInstrumentation().getContext();
+ TestUtils.clearApplicationState(mTargetContext);
+ TestUtils.resetEventBus();
+ }
+
+ public void testGetCommentEqualTo1024K() {
+ createAndGetComment(1024 * 1024);
+ }
+
+ public void testGetCommentEqualTo2096550() {
+ createAndGetComment(2096550); // 1024 * 1024 * 2 - 603
+ }
+
+ public void testGetCommentEqualTo2096549() {
+ createAndGetComment(2096549); // 1024 * 1024 * 2 - 602
+ }
+
+ public void testGetCommentEqualTo2048K() {
+ createAndGetComment(1024 * 1024 * 2);
+ }
+
+ private void createAndGetComment(int commentLength) {
+ // Load a sample DB and inject it into WordPress.wpdb
+ TestUtils.loadDBFromDump(mTargetContext, mTestContext, "taliwutt-blogs-sample.sql");
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < commentLength; ++i) {
+ sb.append('a');
+ }
+ Comment bigComment = new Comment(0,
+ 1,
+ "author",
+ "0",
+ sb.toString(),
+ "approve",
+ "arst",
+ "http://mop.com",
+ "mop@mop.com",
+ "");
+ CommentTable.addComment(0, bigComment);
+ CommentTable.getCommentsForBlog(0);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/database/WordPressDBTest.java b/WordPress/src/androidTest/java/org/wordpress/android/database/WordPressDBTest.java
new file mode 100644
index 000000000..0e8b7de50
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/database/WordPressDBTest.java
@@ -0,0 +1,17 @@
+package org.wordpress.android.database;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+
+public class WordPressDBTest extends InstrumentationTestCase {
+ protected Context testContext;
+ protected Context targetContext;
+
+ @Override
+ protected void setUp() {
+ // Run tests in an isolated context
+ targetContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_");
+ testContext = getInstrumentation().getContext();
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorEmptyMock.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorEmptyMock.java
new file mode 100644
index 000000000..2895cf0ed
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorEmptyMock.java
@@ -0,0 +1,11 @@
+package org.wordpress.android.mocks;
+
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.networking.AuthenticatorRequest;
+import org.wordpress.android.networking.OAuthAuthenticator;
+
+public class OAuthAuthenticatorEmptyMock extends OAuthAuthenticator {
+ public void authenticate(AuthenticatorRequest request) {
+ AccountHelper.getDefaultAccount().setAccessToken("dead-parrot");
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorFactoryTest.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorFactoryTest.java
new file mode 100644
index 000000000..d84962500
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/OAuthAuthenticatorFactoryTest.java
@@ -0,0 +1,24 @@
+package org.wordpress.android.mocks;
+
+import android.content.Context;
+
+import org.wordpress.android.networking.OAuthAuthenticator;
+import org.wordpress.android.networking.OAuthAuthenticatorFactoryAbstract;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class OAuthAuthenticatorFactoryTest implements OAuthAuthenticatorFactoryAbstract {
+ public enum Mode {EMPTY}
+
+ public static Mode sMode = Mode.EMPTY;
+ public static Context sContext;
+
+ public OAuthAuthenticator make() {
+ switch (sMode) {
+ case EMPTY:
+ default:
+ AppLog.v(T.TESTS, "make: OAuthAuthenticatorEmptyMock");
+ return new OAuthAuthenticatorEmptyMock();
+ }
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientCustomizableMock.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientCustomizableMock.java
new file mode 100644
index 000000000..6f066d6d1
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientCustomizableMock.java
@@ -0,0 +1,150 @@
+package org.wordpress.android.mocks;
+
+import android.content.Context;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request.Method;
+import com.android.volley.TimeoutError;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class RestClientCustomizableMock extends RestClient {
+ private Context mContext;
+ private String mPrefix;
+
+ public void setContextAndPrefix(Context context, String prefix) {
+ mContext = context;
+ mPrefix = prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ mPrefix = prefix;
+ }
+
+ public void setContext(Context context) {
+ mContext = context;
+ }
+
+ public RestClientCustomizableMock(com.android.volley.RequestQueue queue) {
+ super(queue);
+ }
+
+ public RestClientCustomizableMock(com.android.volley.RequestQueue queue, String token) {
+ super(queue, token, REST_API_ENDPOINT_URL_V1);
+ }
+
+ public String getAbsoluteURL(String url) {
+ return null;
+ }
+
+ public String getAbsoluteURL(String path, java.util.Map<String, String> params) {
+ return null;
+ }
+
+ public RestRequest get(String path, RestRequest.Listener listener, RestRequest.ErrorListener errorListener) {
+ AppLog.v(T.TESTS, this.getClass() + ": get(" + path + ")");
+ return new RestRequest(Method.GET, path, null, listener, errorListener);
+ }
+
+ public RestRequest post(String path, java.util.Map<String, String> body, RestRequest.Listener listener,
+ RestRequest.ErrorListener errorListener) {
+ AppLog.v(T.TESTS, this.getClass() + ": post(" + path + ")");
+ return new RestRequest(Method.POST, path, body, listener, errorListener);
+ }
+
+ private VolleyError forgeVolleyErrorFromFilename(String filename) {
+ String strData = fileToString(filename);
+ byte[] data = new byte[0];
+ if (strData != null) {
+ data = strData.getBytes();
+ }
+ NetworkResponse networkResponse = new NetworkResponse(400, data, null, false);
+ VolleyError ve = new VolleyError(networkResponse);
+ return ve;
+ }
+
+ private TimeoutError forgeVolleyTimeoutError() {
+ TimeoutError te = new TimeoutError();
+ return te;
+ }
+
+ private String fileToString(String filename) {
+ try {
+ InputStream is = mContext.getAssets().open(filename);
+ String data = TestUtils.convertStreamToString(is);
+ AppLog.v(T.TESTS, "file read:" + filename);
+ return data;
+ } catch (IOException e) {
+ AppLog.e(T.TESTS, "can't read file: " + filename + " - " + e.toString());
+ }
+ return null;
+ }
+
+ public RestRequest makeRequest(int method, String url, java.util.Map<String, String> params,
+ RestRequest.Listener listener, RestRequest.ErrorListener errorListener) {
+ AppLog.v(T.TESTS, this.getClass() + ": makeRequest(" + url + ")");
+ RestRequest dummyReturnValue = new RestRequest(method, url, params, listener, errorListener);
+ // URL example: https://public-api.wordpress.com/rest/v1/me
+ // Filename: default-public-api-wordpress-com-rest-v1-me.json
+ String filename = mPrefix + "-" + url.replace("https://", "").replace("/", "-").replace(".", "-").replace("?",
+ "-") + ".json";
+
+ if ("password-invalid".equals(mPrefix) && errorListener != null) {
+ errorListener.onErrorResponse(forgeVolleyErrorFromFilename(filename));
+ return dummyReturnValue;
+ }
+
+ if ("username-exists".equals(mPrefix) && errorListener != null) {
+ errorListener.onErrorResponse(forgeVolleyErrorFromFilename(filename));
+ return dummyReturnValue;
+ }
+
+ if ("timeout".equals(mPrefix) && errorListener != null) {
+ errorListener.onErrorResponse(forgeVolleyTimeoutError());
+ return dummyReturnValue;
+ }
+
+ if ("site-reserved".equals(mPrefix) && errorListener != null) {
+ errorListener.onErrorResponse(forgeVolleyErrorFromFilename(filename));
+ return dummyReturnValue;
+ }
+
+ String data = fileToString(filename);
+ if (data == null) {
+ AppLog.e(T.TESTS, "Can't read file: " + filename);
+ throw new RuntimeException("Can't read file: " + filename);
+ }
+
+ try {
+ JSONObject jsonObj = new JSONObject(data);
+ listener.onResponse(jsonObj);
+ } catch (JSONException je) {
+ AppLog.e(T.TESTS, je.toString());
+ }
+ return dummyReturnValue;
+ }
+
+ public RestRequest send(RestRequest request) {
+ return request;
+ }
+
+ public void setUserAgent(String userAgent) {
+ }
+
+ public void setAccessToken(String token) {
+ }
+
+ public boolean isAuthenticated() {
+ return true;
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientEmptyMock.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientEmptyMock.java
new file mode 100644
index 000000000..b07b1b2a3
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientEmptyMock.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.mocks;
+
+import com.android.volley.Request.Method;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class RestClientEmptyMock extends RestClient {
+ public RestClientEmptyMock(com.android.volley.RequestQueue queue) {
+ super(queue);
+ }
+
+ public RestClientEmptyMock(com.android.volley.RequestQueue queue, java.lang.String token) {
+ super(queue, token, REST_API_ENDPOINT_URL_V1);
+ }
+
+ public java.lang.String getAbsoluteURL(java.lang.String url) {
+ return null;
+ }
+
+ public java.lang.String getAbsoluteURL(java.lang.String path,
+ java.util.Map<java.lang.String, java.lang.String> params) {
+ return null;
+ }
+
+ public com.wordpress.rest.RestRequest get(java.lang.String path, com.wordpress.rest.RestRequest.Listener listener,
+ com.wordpress.rest.RestRequest.ErrorListener errorListener) {
+ AppLog.v(T.TESTS, this.getClass() + ": get(" + path + ")");
+ return new RestRequest(Method.GET, path, null, listener, errorListener);
+ }
+
+ public com.wordpress.rest.RestRequest post(java.lang.String path,
+ java.util.Map<java.lang.String, java.lang.String> body,
+ com.wordpress.rest.RestRequest.Listener listener,
+ com.wordpress.rest.RestRequest.ErrorListener errorListener) {
+ AppLog.v(T.TESTS, this.getClass() + ": post(" + path + ")");
+ return new RestRequest(Method.POST, path, body, listener, errorListener);
+ }
+
+ public com.wordpress.rest.RestRequest makeRequest(int method, java.lang.String url,
+ java.util.Map<java.lang.String, java.lang.String> params,
+ com.wordpress.rest.RestRequest.Listener listener,
+ com.wordpress.rest.RestRequest.ErrorListener errorListener) {
+ AppLog.v(T.TESTS, this.getClass() + ": makeRequest(" + url + ")");
+ return new RestRequest(method, url, params, listener, errorListener);
+ }
+
+ public com.wordpress.rest.RestRequest send(com.wordpress.rest.RestRequest request) {
+ return request;
+ }
+
+ public void setUserAgent(java.lang.String userAgent) {
+ }
+
+ public void setAccessToken(java.lang.String token) {
+ }
+
+ public boolean isAuthenticated() {
+ return true;
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientFactoryTest.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientFactoryTest.java
new file mode 100644
index 000000000..f403e5126
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/RestClientFactoryTest.java
@@ -0,0 +1,71 @@
+package org.wordpress.android.mocks;
+
+import android.content.Context;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestClient.REST_CLIENT_VERSIONS;
+
+import org.wordpress.android.networking.RestClientFactoryAbstract;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class RestClientFactoryTest implements RestClientFactoryAbstract {
+ public static String sPrefix = "default";
+ public static RestClient.REST_CLIENT_VERSIONS sVersion = REST_CLIENT_VERSIONS.V1;
+ public static Context sContext;
+ // keep a reference to each instances so we can update contexts and prefixes after instantiation
+ public static Set<RestClientCustomizableMock> sInstances = new HashSet<RestClientCustomizableMock>();
+
+ public static void setContextAllInstances(Context context) {
+ sContext = context;
+ if (sMode != Mode.CUSTOMIZABLE) {
+ AppLog.e(T.TESTS, "You try to change context on a non-customizable RestClient mock");
+ }
+ for (RestClientCustomizableMock client : sInstances) {
+ client.setContext(context);
+ }
+ }
+
+ public static void setPrefixAllInstances(String prefix) {
+ sPrefix = prefix;
+ if (sMode != Mode.CUSTOMIZABLE) {
+ AppLog.e(T.TESTS, "You try to change prefix on a non-customizable RestClient mock");
+ }
+ for (RestClientCustomizableMock client : sInstances) {
+ client.setPrefix(prefix);
+ }
+ }
+
+ public static Mode sMode = Mode.EMPTY;
+
+ public RestClient make(RequestQueue queue) {
+ switch (sMode) {
+ case CUSTOMIZABLE:
+ RestClientCustomizableMock client = new RestClientCustomizableMock(queue);
+ if (sContext != null) {
+ client.setContextAndPrefix(sContext, sPrefix);
+ } else {
+ AppLog.e(T.TESTS, "You have to set RestClientFactoryTest.sContext field before running tests");
+ throw new IllegalStateException();
+ }
+ AppLog.v(T.TESTS, "make: RestClientCustomizableMock");
+ sInstances.add(client);
+ return client;
+ case EMPTY:
+ default:
+ AppLog.v(T.TESTS, "make: RestClientEmptyMock");
+ return new RestClientEmptyMock(queue);
+ }
+ }
+
+ public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) {
+ sVersion = version;
+ return make(queue);
+ }
+
+ public enum Mode {EMPTY, CUSTOMIZABLE}
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/SystemServiceFactoryTest.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/SystemServiceFactoryTest.java
new file mode 100644
index 000000000..a099de8ef
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/SystemServiceFactoryTest.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.mocks;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+
+import org.mockito.stubbing.Answer;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.SystemServiceFactoryAbstract;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+public class SystemServiceFactoryTest implements SystemServiceFactoryAbstract {
+ public static Answer sNotificationCallback;
+
+ public Object get(Context context, String name) {
+ System.setProperty("dexmaker.dexcache", context.getCacheDir().getPath());
+ if (Context.NOTIFICATION_SERVICE.equals(name)) {
+ NotificationManager notificationManager = mock(NotificationManager.class);
+ if (sNotificationCallback != null) {
+ doAnswer(sNotificationCallback).when(notificationManager).notify(anyInt(), any(Notification.class));
+ doAnswer(sNotificationCallback).when(notificationManager).cancel(anyInt());
+ }
+ return notificationManager;
+ } else {
+ AppLog.e(T.TESTS, "SystemService:" + name + "No supported in SystemServiceFactoryTest");
+ }
+ return null;
+ }
+
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableJSONMock.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableJSONMock.java
new file mode 100644
index 000000000..d68746c4a
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableJSONMock.java
@@ -0,0 +1,94 @@
+package org.wordpress.android.mocks;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.xmlrpc.android.LoggedInputStream;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCException;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.HashMap;
+
+public class XMLRPCClientCustomizableJSONMock extends XMLRPCClientCustomizableMockAbstract {
+ private LoggedInputStream mLoggedInputStream;
+
+ public XMLRPCClientCustomizableJSONMock(URI uri, String httpUser, String httpPassword) {
+ }
+
+ public void addQuickPostHeader(String type) {
+ }
+
+ public void setAuthorizationHeader(String authToken) {
+ }
+
+ private Object readFile(String method, String prefix) {
+ // method example: wp.getUsersBlogs
+ // Filename: default-wp.getUsersBlogs.json
+ String filename = prefix + "-" + method + ".json";
+ try {
+ Gson gson = new Gson();
+ mLoggedInputStream = new LoggedInputStream(mContext.getAssets().open(filename));
+ String jsonString = TestUtils.convertStreamToString(mLoggedInputStream);
+ AppLog.i(T.TESTS, "loading: " + filename);
+ try {
+ // Try to load a JSONArray
+ return TestUtils.injectDateInArray(gson.fromJson(jsonString, Object[].class));
+ } catch (Exception e) {
+ // If that fails, try to load a JSONObject
+ Type type = new TypeToken<HashMap<String, Object>>(){}.getType();
+ HashMap<String, Object> map = gson.fromJson(jsonString, type);
+ return TestUtils.injectDateInMap(map);
+ }
+ } catch (IOException e) {
+ AppLog.e(T.TESTS, "can't read file: " + filename);
+ }
+ return null;
+ }
+
+ public Object call(String method, Object[] params) throws XMLRPCException {
+ mLoggedInputStream = null;
+ AppLog.v(T.TESTS, "XMLRPCClientCustomizableJSONMock: call: " + method);
+ if ("login-failure".equals(mPrefix)) {
+ // Wrong login
+ throw new XMLRPCException("code 403");
+ }
+
+ Object retValue = readFile(method, mPrefix);
+ if (retValue == null) {
+ // failback to default
+ AppLog.w(T.TESTS, "failback to default");
+ retValue = readFile(method, "default");
+ }
+ return retValue;
+ }
+
+ public Object call(String method) throws XMLRPCException {
+ return null;
+ }
+
+ public Object call(String method, Object[] params, File tempFile) throws XMLRPCException {
+ return null;
+ }
+
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params) {
+ return 0;
+ }
+
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params, File tempFile) {
+ return 0;
+ }
+
+ public String getResponse() {
+ if (mLoggedInputStream == null) {
+ return "";
+ }
+ return mLoggedInputStream.getResponseDocument();
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableMockAbstract.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableMockAbstract.java
new file mode 100644
index 000000000..c8690f6b8
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableMockAbstract.java
@@ -0,0 +1,23 @@
+package org.wordpress.android.mocks;
+
+import android.content.Context;
+
+import org.xmlrpc.android.XMLRPCClientInterface;
+
+public abstract class XMLRPCClientCustomizableMockAbstract implements XMLRPCClientInterface {
+ protected Context mContext;
+ protected String mPrefix;
+
+ public void setContextAndPrefix(Context context, String prefix) {
+ mContext = context;
+ mPrefix = prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ mPrefix = prefix;
+ }
+
+ public void setContext(Context context) {
+ mContext = context;
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableXMLMock.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableXMLMock.java
new file mode 100644
index 000000000..57e8291f9
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientCustomizableXMLMock.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.mocks;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.LoggedInputStream;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClient;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFault;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+
+public class XMLRPCClientCustomizableXMLMock extends XMLRPCClientCustomizableMockAbstract {
+ XMLRPCClient mXmlRpcClient;
+ private LoggedInputStream mLoggedInputStream;
+
+ public XMLRPCClientCustomizableXMLMock(URI uri, String httpUser, String httpPassword) {
+ // Used to test ctor and preparePostMethod
+ mXmlRpcClient = new XMLRPCClient("", "", "");
+ }
+
+ public void addQuickPostHeader(String type) {
+ }
+
+ public void setAuthorizationHeader(String authToken) {
+ }
+
+ private Object readFile(String method, String prefix) throws IOException, XMLRPCException, XmlPullParserException {
+ // method example: wp.getUsersBlogs
+ // Filename: default-wp.getUsersBlogs.xml
+ String filename = prefix + "-" + method + ".xml";
+ try {
+ mLoggedInputStream = new LoggedInputStream(mContext.getAssets().open(filename));
+ return XMLRPCClient.parseXMLRPCResponse(mLoggedInputStream, null);
+ } catch (FileNotFoundException e) {
+ AppLog.e(T.TESTS, "file not found: " + filename);
+ }
+ return null;
+ }
+
+ public Object call(String method, Object[] params) throws XMLRPCException, IOException, XmlPullParserException {
+ mLoggedInputStream = null;
+ try {
+ mXmlRpcClient.preparePostMethod(method, params, null);
+ } catch (IOException e) {
+ // unexpected error, test must fail
+ throw new XMLRPCException("preparePostMethod failed");
+ }
+ AppLog.v(T.TESTS, "XMLRPCClientCustomizableXMLMock call: " + method);
+ if ("login-failure".equals(mPrefix)) {
+ // Wrong login
+ throw new XMLRPCFault("code 403", 403);
+ }
+
+ Object retValue = readFile(method, mPrefix);
+ if (retValue == null) {
+ // failback to default
+ AppLog.w(T.TESTS, "failback to default");
+ retValue = readFile(method, "default");
+ }
+ return retValue;
+ }
+
+ public Object call(String method) throws XMLRPCException {
+ return null;
+ }
+
+ public Object call(String method, Object[] params, File tempFile) throws XMLRPCException {
+ return null;
+ }
+
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params) {
+ return 0;
+ }
+
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params, File tempFile) {
+ return 0;
+ }
+
+ public String getResponse() {
+ if (mLoggedInputStream == null) {
+ return "";
+ }
+ return mLoggedInputStream.getResponseDocument();
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientEmptyMock.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientEmptyMock.java
new file mode 100644
index 000000000..f995e2446
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCClientEmptyMock.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.mocks;
+
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+
+import java.io.File;
+import java.net.URI;
+
+public class XMLRPCClientEmptyMock implements XMLRPCClientInterface {
+ public XMLRPCClientEmptyMock(URI uri, String httpUser, String httpPassword) {
+ }
+
+ public void addQuickPostHeader(String type) {
+ }
+
+ public void setAuthorizationHeader(String authToken) {
+ }
+
+ public Object call(String method, Object[] params) throws XMLRPCException {
+ return null;
+ }
+
+ public Object call(String method) throws XMLRPCException {
+ return null;
+ }
+
+ public Object call(String method, Object[] params, File tempFile) throws XMLRPCException {
+ return null;
+ }
+
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params) {
+ return 0;
+ }
+
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params, File tempFile) {
+ return 0;
+ }
+
+ public String getResponse() {
+ return null;
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCFactoryTest.java b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCFactoryTest.java
new file mode 100644
index 000000000..60852f11c
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/mocks/XMLRPCFactoryTest.java
@@ -0,0 +1,75 @@
+package org.wordpress.android.mocks;
+
+import android.content.Context;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCFactoryAbstract;
+
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+
+public class XMLRPCFactoryTest implements XMLRPCFactoryAbstract {
+ public static String sPrefix = "default";
+ public static Context sContext;
+ public static Mode sMode = Mode.EMPTY;
+ public static Set<XMLRPCClientCustomizableMockAbstract> sInstances =
+ new HashSet<XMLRPCClientCustomizableMockAbstract>();
+
+ public static void setContextAllInstances(Context context) {
+ sContext = context;
+ if (sMode != Mode.CUSTOMIZABLE_JSON && sMode != Mode.CUSTOMIZABLE_XML) {
+ AppLog.e(T.TESTS, "You tried to change context on a non-customizable XMLRPCClient mock");
+ }
+ for (XMLRPCClientCustomizableMockAbstract client : sInstances) {
+ client.setContext(context);
+ }
+ }
+
+ public static void setPrefixAllInstances(String prefix) {
+ sPrefix = prefix;
+ if (sMode != Mode.CUSTOMIZABLE_JSON && sMode != Mode.CUSTOMIZABLE_XML) {
+ AppLog.e(T.TESTS, "You tried to change prefix on a non-customizable XMLRPCClient mock");
+ }
+ for (XMLRPCClientCustomizableMockAbstract client : sInstances) {
+ client.setPrefix(prefix);
+ }
+ }
+
+ public XMLRPCClientInterface make(URI uri, String httpUser, String httpPassword) {
+ switch (sMode) {
+ case CUSTOMIZABLE_JSON:
+ XMLRPCClientCustomizableJSONMock clientJSONMock = new XMLRPCClientCustomizableJSONMock(uri, httpUser,
+ httpPassword);
+ if (sContext != null) {
+ clientJSONMock.setContextAndPrefix(sContext, sPrefix);
+ } else {
+ AppLog.e(T.TESTS, "You have to set XMLRPCFactoryTest.sContext field before running tests");
+ throw new IllegalStateException();
+ }
+ AppLog.v(T.TESTS, "make: XMLRPCClientCustomizableJSONMock");
+ sInstances.add(clientJSONMock);
+ return clientJSONMock;
+ case CUSTOMIZABLE_XML:
+ XMLRPCClientCustomizableXMLMock clientXMLMock = new XMLRPCClientCustomizableXMLMock(uri, httpUser,
+ httpPassword);
+ if (sContext != null) {
+ clientXMLMock.setContextAndPrefix(sContext, sPrefix);
+ } else {
+ AppLog.e(T.TESTS, "You have to set XMLRPCFactoryTest.sContext field before running tests");
+ throw new IllegalStateException();
+ }
+ AppLog.v(T.TESTS, "make: XMLRPCClientCustomizableXMLMock");
+ sInstances.add(clientXMLMock);
+ return clientXMLMock;
+ case EMPTY:
+ default:
+ AppLog.v(T.TESTS, "make: XMLRPCClientEmptyMock");
+ return new XMLRPCClientEmptyMock(uri, httpUser, httpPassword);
+ }
+ }
+
+ public enum Mode {EMPTY, CUSTOMIZABLE_JSON, CUSTOMIZABLE_XML}
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/models/BlogTest.java b/WordPress/src/androidTest/java/org/wordpress/android/models/BlogTest.java
new file mode 100644
index 000000000..d1e5ba071
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/models/BlogTest.java
@@ -0,0 +1,293 @@
+package org.wordpress.android.models;
+
+import android.test.InstrumentationTestCase;
+
+public class BlogTest extends InstrumentationTestCase {
+ private Blog blog;
+
+ @Override
+ protected void setUp() throws Exception {
+ blog = new Blog("http://www.example.com", "username", "password");
+
+ super.setUp();
+ }
+
+ public void testBlogTestUrlUsernamePassword() {
+ assertEquals("http://www.example.com", blog.getUrl());
+ assertEquals("username", blog.getUsername());
+ assertEquals("password", blog.getPassword());
+ assertEquals(-1, blog.getLocalTableBlogId());
+ }
+
+ public void testGetSetLocalTableBlogId() {
+ assertEquals(-1, blog.getLocalTableBlogId());
+ blog.setLocalTableBlogId(0);
+ assertEquals(0, blog.getLocalTableBlogId());
+ }
+
+ public void testGetSetUrl() {
+ assertEquals("http://www.example.com", blog.getUrl());
+ blog.setUrl(null);
+ assertNull(blog.getUrl());
+ blog.setUrl("http://example.com/two");
+ assertEquals("http://example.com/two", blog.getUrl());
+ }
+
+ public void testGetSetHomeURL() {
+ assertNull(blog.getHomeURL());
+ blog.setHomeURL("http://www.homeurl.com");
+ assertEquals("http://www.homeurl.com", blog.getHomeURL());
+ }
+
+ public void testGetSetBlogName() {
+ assertNull(blog.getBlogName());
+ blog.setBlogName("blogName");
+ assertEquals("blogName", blog.getBlogName());
+ }
+
+ public void testGetSetUsername() {
+ assertEquals("username", blog.getUsername());
+ blog.setUsername(null);
+ // getUsername never returns null
+ assertEquals("", blog.getUsername());
+ }
+
+ public void testGetSetPassword() {
+ assertEquals("password", blog.getPassword());
+ blog.setPassword(null);
+ // getPassword never returns null
+ assertEquals("", blog.getPassword());
+ }
+
+ public void testGetSetImagePlacement() {
+ assertNull(blog.getImagePlacement());
+ blog.setImagePlacement("test");
+ assertEquals("test", blog.getImagePlacement());
+ }
+
+ public void testGetSetFeaturedImageCapable() {
+ assertFalse(blog.isFeaturedImageCapable());
+ blog.setFeaturedImageCapable(true);
+ assertTrue(blog.isFeaturedImageCapable());
+ }
+
+ public void testBsetFeaturedImageCapable() {
+ assertFalse(blog.isFeaturedImageCapable());
+ boolean val = blog.bsetFeaturedImageCapable(false);
+ assertFalse(val);
+ assertFalse(blog.isFeaturedImageCapable());
+ val = blog.bsetFeaturedImageCapable(true);
+ assertTrue(val);
+ assertTrue(blog.isFeaturedImageCapable());
+ val = blog.bsetFeaturedImageCapable(false);
+ assertTrue(val);
+ }
+
+ public void testGetSetFullSizeImage() {
+ assertFalse(blog.isFullSizeImage());
+ blog.setFullSizeImage(true);
+ assertTrue(blog.isFullSizeImage());
+ }
+
+ public void testGetSetMaxImageWidth() {
+ assertEquals("", blog.getMaxImageWidth());
+ blog.setMaxImageWidth("1");
+ assertEquals("1", blog.getMaxImageWidth());
+ }
+
+ public void testGetSetMaxImageWidthId() {
+ assertEquals(0, blog.getMaxImageWidthId());
+ blog.setMaxImageWidthId(1);
+ assertEquals(1, blog.getMaxImageWidthId());
+ }
+
+ public void testGetSetRemoteBlogId() {
+ assertEquals(0, blog.getRemoteBlogId());
+ blog.setRemoteBlogId(1);
+ assertEquals(1, blog.getRemoteBlogId());
+ }
+
+ public void testGetSetDotcom_username() {
+ assertNull(blog.getDotcom_username());
+ blog.setDotcom_username("username");
+ assertEquals("username", blog.getDotcom_username());
+ }
+
+ public void testGetSetDotcom_password() {
+ assertNull(blog.getDotcom_password());
+ blog.setDotcom_password("password");
+ assertEquals("password", blog.getDotcom_password());
+ }
+
+ public void testGetSetApi_key() {
+ assertNull(blog.getApi_key());
+ blog.setApi_key("123");
+ assertEquals("123", blog.getApi_key());
+ }
+
+ public void testGetSetApi_blogid() {
+ assertNull(blog.getApi_blogid());
+ blog.setApi_blogid("123");
+ assertEquals("123", blog.getApi_blogid());
+ }
+
+ public void testGetSetDotcomFlag() {
+ assertFalse(blog.isDotcomFlag());
+ blog.setDotcomFlag(true);
+ assertTrue(blog.isDotcomFlag());
+ }
+
+ public void testGetSetWpVersion() {
+ assertNull(blog.getWpVersion());
+ blog.setWpVersion("123");
+ assertEquals("123", blog.getWpVersion());
+ }
+
+ public void testBsetWpVersion() {
+ assertNull(blog.getWpVersion());
+ boolean val = blog.bsetWpVersion("123");
+ assertTrue(val);
+ assertEquals("123", blog.getWpVersion());
+ val = blog.bsetWpVersion("123");
+ assertFalse(val);
+ }
+
+ public void testGetSetHttpuser() {
+ assertEquals(blog.getHttpuser(), "");
+ blog.setHttpuser("user");
+ assertEquals("user", blog.getHttpuser());
+ }
+
+ public void testGetSetHttppassword() {
+ assertEquals(blog.getHttppassword(), "");
+ blog.setHttppassword("password");
+ assertEquals("password", blog.getHttppassword());
+ }
+
+ public void testGetSetHidden() {
+ assertFalse(blog.isHidden());
+ blog.setHidden(true);
+ assertTrue(blog.isHidden());
+ }
+
+ public void testGetSetPostFormats() {
+ assertNull(blog.getPostFormats());
+ blog.setPostFormats("test");
+ assertEquals("test", blog.getPostFormats());
+ }
+
+ public void testBSetPostFormats() {
+ assertNull(blog.getPostFormats());
+ boolean val = blog.bsetPostFormats("test");
+ assertTrue(val);
+ assertEquals("test", blog.getPostFormats());
+ val = blog.bsetPostFormats("test");
+ assertFalse(val);
+ val = blog.bsetPostFormats("test2");
+ assertTrue(val);
+ }
+
+ public void testGetSetScaledImage() {
+ assertFalse(blog.isScaledImage());
+ blog.setScaledImage(true);
+ assertTrue(blog.isScaledImage());
+ }
+
+ public void testGetSetScaledImageWidth() {
+ assertEquals(0, blog.getScaledImageWidth());
+ blog.setScaledImageWidth(1);
+ assertEquals(1, blog.getScaledImageWidth());
+ }
+
+ public void testGetSetBlogOptions() {
+ assertEquals("{}", blog.getBlogOptions());
+ blog.setBlogOptions("{option:1}");
+ assertEquals("{option:1}", blog.getBlogOptions());
+ }
+
+ public void testBSetBlogOptions() {
+ assertEquals("{}", blog.getBlogOptions());
+ boolean val = blog.bsetBlogOptions("{option:1}");
+ assertTrue(val);
+ val = blog.bsetBlogOptions("{option:1}");
+ assertFalse(val);
+ val = blog.bsetBlogOptions("{option:2}");
+ assertTrue(val);
+ }
+
+ public void testGetSetAdmin() {
+ assertFalse(blog.isAdmin());
+ blog.setAdmin(true);
+ assertTrue(blog.isAdmin());
+ }
+
+ public void testBSetAdmin() {
+ assertFalse(blog.isAdmin());
+ boolean val = blog.bsetAdmin(false);
+ assertFalse(val);
+ val = blog.bsetAdmin(true);
+ assertTrue(val);
+ val = blog.bsetAdmin(true);
+ assertFalse(val);
+ }
+
+ public void testGetSetAdminUrl() {
+ blog.setBlogOptions("{\"admin_url\": {\"value\": \"https://muppets.com/wp-admin/\" } }");
+ assertEquals("https://muppets.com/wp-admin/", blog.getAdminUrl());
+ }
+
+ public void testGetSetPrivate() {
+ assertFalse(blog.isPrivate());
+ blog.setBlogOptions("{ \"blog_public\" : { \"value\" : \"-1\" } }");
+
+ // blog cannot be private if not a wpcom one
+ assertFalse(blog.isPrivate());
+
+ // set the blog as a WPCom one
+ blog.setDotcomFlag(true);
+ // blog should now appear as private
+ assertTrue(blog.isPrivate());
+ }
+
+ public void testGetSetJetpackPowered() {
+ assertFalse(blog.isJetpackPowered());
+ blog.setBlogOptions("{ jetpack_client_id : {} }");
+ assertTrue(blog.isJetpackPowered());
+ }
+
+ public void testIsPhotonCapableJetpack() {
+ assertFalse(blog.isPhotonCapable());
+
+ blog.setBlogOptions("{ jetpack_client_id : {} }");
+ assertTrue(blog.isPhotonCapable());
+ }
+
+ public void testIsPhotonCapableWPComPublic() {
+ assertFalse(blog.isPhotonCapable());
+ assertFalse(blog.isPrivate());
+ blog.setBlogOptions("");
+ blog.setDotcomFlag(true);
+ assertTrue(blog.isPhotonCapable());
+ }
+
+ public void testIsPhotonCapableWPComPrivate() {
+ assertFalse(blog.isPhotonCapable());
+
+ blog.setBlogOptions("{ \"blog_public\" : { \"value\" : \"-1\" } }");
+ assertFalse(blog.isPhotonCapable());
+ }
+
+ public void testGetSetHasValidJetpackCredentials() {
+ assertFalse(blog.hasValidJetpackCredentials());
+ }
+
+ public void testGetSetDotComBlogId() {
+ assertNull(blog.getDotComBlogId());
+ assertFalse(blog.isDotcomFlag());
+ blog.setApi_blogid("1");
+ blog.setRemoteBlogId(2);
+ assertEquals("1", blog.getDotComBlogId());
+ blog.setDotcomFlag(true);
+ assertEquals("2", blog.getDotComBlogId());
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/models/CategoryNodeInstrumentationTest.java b/WordPress/src/androidTest/java/org/wordpress/android/models/CategoryNodeInstrumentationTest.java
new file mode 100644
index 000000000..8eef7cff3
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/models/CategoryNodeInstrumentationTest.java
@@ -0,0 +1,34 @@
+package org.wordpress.android.models;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+
+import org.wordpress.android.TestUtils;
+
+public class CategoryNodeInstrumentationTest extends InstrumentationTestCase {
+ protected Context testContext;
+ protected Context targetContext;
+
+ @Override
+ protected void setUp() {
+ // Run tests in an isolated context
+ targetContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_");
+ testContext = getInstrumentation().getContext();
+ }
+
+ public void testLoadDB_MalformedCategoryParentId() {
+ SQLiteDatabase db = TestUtils.loadDBFromDump(targetContext, testContext,
+ "malformed_category_parent_id.sql");
+
+ // This line failed before #36 was solved
+ CategoryNode node = CategoryNode.createCategoryTreeFromDB(1);
+ }
+
+ public void tearDown() throws Exception {
+ targetContext = null;
+ testContext = null;
+ super.tearDown();
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/models/PostLocationTest.java b/WordPress/src/androidTest/java/org/wordpress/android/models/PostLocationTest.java
new file mode 100644
index 000000000..af8de62ba
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/models/PostLocationTest.java
@@ -0,0 +1,115 @@
+package org.wordpress.android.models;
+
+import android.test.InstrumentationTestCase;
+
+import junit.framework.Assert;
+
+public class PostLocationTest extends InstrumentationTestCase {
+ public static final double MAX_LAT = 90;
+ public static final double MIN_LAT = -90;
+ public static final double MAX_LNG = 180;
+ public static final double MIN_LNG = -180;
+ public static final double INVALID_LAT_MAX = 91;
+ public static final double INVALID_LAT_MIN = -91;
+ public static final double INVALID_LNG_MAX = 181;
+ public static final double INVALID_LNG_MIN = -181;
+ public static final double EQUATOR_LAT = 0;
+ public static final double EQUATOR_LNG = 0;
+
+ public void testInstantiateValidLocation() {
+ PostLocation locationZero = new PostLocation(EQUATOR_LAT, EQUATOR_LNG);
+ assertTrue("ZeroLoc did not instantiate valid location", locationZero.isValid());
+ assertEquals("ZeroLoc did not return correct lat", EQUATOR_LAT, locationZero.getLatitude());
+ assertEquals("ZeroLoc did not return correct lng", EQUATOR_LNG, locationZero.getLongitude());
+
+ PostLocation locationMax = new PostLocation(MAX_LAT, MAX_LNG);
+ assertTrue("MaxLoc did not instantiate valid location", locationMax.isValid());
+ assertEquals("MaxLoc did not return correct lat", MAX_LAT, locationMax.getLatitude());
+ assertEquals("MaxLoc did not return correct lng", MAX_LNG, locationMax.getLongitude());
+
+ PostLocation locationMin = new PostLocation(MIN_LAT, MIN_LNG);
+ assertTrue("MinLoc did not instantiate valid location", locationMin.isValid());
+ assertEquals("MinLoc did not return correct lat", MIN_LAT, locationMin.getLatitude());
+ assertEquals("MinLoc did not return correct lng", MIN_LNG, locationMin.getLongitude());
+
+ double miscLat = 34;
+ double miscLng = -60;
+ PostLocation locationMisc = new PostLocation(miscLat, miscLng);
+ assertTrue("MiscLoc did not instantiate valid location", locationMisc.isValid());
+ assertEquals("MiscLoc did not return correct lat", miscLat, locationMisc.getLatitude());
+ assertEquals("MiscLoc did not return correct lng", miscLng, locationMisc.getLongitude());
+ }
+
+ public void testDefaultLocationInvalid() {
+ PostLocation location = new PostLocation();
+ assertFalse("Empty location should be invalid", location.isValid());
+ }
+
+ public void testInvalidLatitude() {
+ PostLocation maxLoc = null;
+ try {
+ maxLoc = new PostLocation(INVALID_LAT_MAX, 0);
+ Assert.fail("Lat more than max should have failed on instantiation");
+ } catch (IllegalArgumentException e) {
+ assertNull("Invalid instantiation and not null", maxLoc);
+ }
+
+ PostLocation minLoc = null;
+ try {
+ minLoc = new PostLocation(INVALID_LAT_MIN, 0);
+ Assert.fail("Lat less than min should have failed on instantiation");
+ } catch (IllegalArgumentException e) {
+ assertNull("Invalid instantiation and not null", minLoc);
+ }
+
+ PostLocation location = new PostLocation();
+
+ try {
+ location.setLatitude(INVALID_LAT_MAX);
+ Assert.fail("Lat less than min should have failed");
+ } catch (IllegalArgumentException e) {
+ assertFalse("Invalid setLatitude and still valid", location.isValid());
+ }
+
+ try {
+ location.setLatitude(INVALID_LAT_MIN);
+ Assert.fail("Lat less than min should have failed");
+ } catch (IllegalArgumentException e) {
+ assertFalse("Invalid setLatitude and still valid", location.isValid());
+ }
+ }
+
+ public void testInvalidLongitude() {
+ PostLocation maxLoc = null;
+ try {
+ maxLoc = new PostLocation(0, INVALID_LNG_MAX);
+ Assert.fail("Lng more than max should have failed on instantiation");
+ } catch (IllegalArgumentException e) {
+ assertNull("Invalid instantiation and not null", maxLoc);
+ }
+
+ PostLocation minLoc = null;
+ try {
+ minLoc = new PostLocation(0, INVALID_LNG_MIN);
+ Assert.fail("Lng less than min should have failed on instantiation");
+ } catch (IllegalArgumentException e) {
+ assertNull("Invalid instantiation and not null", minLoc);
+ }
+
+ PostLocation location = new PostLocation();
+
+ try {
+ location.setLongitude(INVALID_LNG_MAX);
+ Assert.fail("Lng less than min should have failed");
+ } catch (IllegalArgumentException e) {
+ assertFalse("Invalid setLongitude and still valid", location.isValid());
+ }
+
+ try {
+ location.setLongitude(INVALID_LNG_MIN);
+ Assert.fail("Lat less than min should have failed");
+ } catch (IllegalArgumentException e) {
+ assertFalse("Invalid setLongitude and still valid", location.isValid());
+ }
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/models/PostTest.java b/WordPress/src/androidTest/java/org/wordpress/android/models/PostTest.java
new file mode 100644
index 000000000..09148d07d
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/models/PostTest.java
@@ -0,0 +1,55 @@
+package org.wordpress.android.models;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+
+import org.json.JSONObject;
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.WordPress;
+
+public class PostTest extends InstrumentationTestCase {
+ protected Context mTestContext;
+ protected Context mTargetContext;
+
+ @Override
+ protected void setUp() throws Exception {
+
+ mTargetContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_");
+ mTestContext = getInstrumentation().getContext();
+
+ super.setUp();
+ }
+
+ public void testInvalidPostIdLoad() {
+ SQLiteDatabase db = TestUtils.loadDBFromDump(mTargetContext, mTestContext, "taliwutt-blogs-sample.sql");
+ Post post = WordPress.wpDB.getPostForLocalTablePostId(-1);
+
+ assertNull(post);
+ }
+
+ public void testPostSaveAndLoad() {
+ SQLiteDatabase db = TestUtils.loadDBFromDump(mTargetContext, mTestContext, "taliwutt-blogs-sample.sql");
+ Post post = new Post(1, false);
+ post.setTitle("test-post");
+ WordPress.wpDB.savePost(post);
+
+ Post loadedPost = WordPress.wpDB.getPostForLocalTablePostId(post.getLocalTablePostId());
+
+ assertNotNull(loadedPost);
+ assertEquals(loadedPost.getTitle(), post.getTitle());
+ }
+
+ // reproduce issue #1544
+ public void testGetNullCustomFields() {
+ Post post = new Post(1, false);
+ assertEquals(post.getCustomFields(), null);
+ }
+
+ public void testGetNullCustomField() {
+ Post post = new Post(1, false);
+ JSONObject remoteGeoLatitude = post.getCustomField("geo_latitude");
+ assertEquals(remoteGeoLatitude, null);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/networking/AuthenticatorRequestTest.java b/WordPress/src/androidTest/java/org/wordpress/android/networking/AuthenticatorRequestTest.java
new file mode 100644
index 000000000..c5936fd61
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/networking/AuthenticatorRequestTest.java
@@ -0,0 +1,61 @@
+package org.wordpress.android.networking;
+
+import android.test.InstrumentationTestCase;
+
+import com.wordpress.rest.RestClient;
+
+import org.wordpress.android.FactoryUtils;
+
+public class AuthenticatorRequestTest extends InstrumentationTestCase {
+ RestClient mRestClient;
+ AuthenticatorRequest mAuthenticatorRequest;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ FactoryUtils.initWithTestFactories();
+ mRestClient = RestClientFactory.instantiate(null);
+ mAuthenticatorRequest = new AuthenticatorRequest(null, null, mRestClient, null);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ FactoryUtils.clearFactories();
+ super.tearDown();
+ }
+
+ public void testExtractSiteIdFromUrl1() {
+ String url = "";
+ assertEquals(null, mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+
+ public void testExtractSiteIdFromUrl2() {
+ String url = null;
+ assertEquals(null, mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+
+ public void testExtractSiteIdFromUrl3() {
+ String url = "https://public-api.wordpress.com/rest/v1/batch/?urls%5B%5D=%2Fsites%2F57991476%2Fstats%2Freferrers%3Fdate%3D2014-05-08&urls%5B%5D=%2Fsites%2F57991476%2Fstats%2Freferrers%3Fdate%3D2014-05-07";
+ assertEquals("57991476", mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+
+ public void testExtractSiteIdFromUrl4() {
+ String url = "https://public-api.wordpress.com/rest/v1/sites/59073674/stats";
+ assertEquals("59073674", mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+
+ public void testExtractSiteIdFromUrl5() {
+ String url = "https://public-api.wordpress.com/rest/v1/sites//stats";
+ assertEquals("", mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+
+ public void testExtractSiteIdFromUrl6() {
+ String url = "https://public-api.wordpress.com/rest/v1/batch/?urls%5B%5D=%2Fsites%2F";
+ assertEquals(null, mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+
+ public void testExtractSiteIdFromUrl7() {
+ String url = "https://public-api.wordpress.com/rest/v1/sites/";
+ assertEquals(null, mAuthenticatorRequest.extractSiteIdFromUrl(mRestClient.getEndpointURL(), url));
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java b/WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java
new file mode 100644
index 000000000..dc8ac78c8
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.networking;
+
+import android.test.InstrumentationTestCase;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okio.Buffer;
+
+public class GravatarApiTest extends InstrumentationTestCase {
+
+ public void testGravatarUploadRequest() throws IOException {
+ final String fileContent = "abcdefg";
+
+ File tempFile = new File(getInstrumentation().getTargetContext().getCacheDir(), "tempFile.jpg");
+ FileOutputStream fos = new FileOutputStream(tempFile);
+ fos.write(fileContent.getBytes());
+ fos.flush();
+ fos.close();
+
+ final String email = "a@b.com";
+ Request uploadRequest = GravatarApi.prepareGravatarUpload(email, tempFile);
+
+ assertEquals("POST", uploadRequest.method());
+
+ RequestBody requestBody = uploadRequest.body();
+ assertTrue(requestBody.contentType().toString().startsWith("multipart/form-data"));
+
+ final Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ final String body = buffer.readUtf8();
+
+ assertTrue(body.contains("Content-Disposition: form-data; name=\"account\""));
+ assertTrue(body.contains("Content-Length: " + email.length()));
+ assertTrue(body.contains(email));
+
+ assertTrue(body.contains("Content-Disposition: form-data; name=\"filedata\"; filename=\"" + tempFile.getName() + "\""));
+ assertTrue(body.contains("Content-Type: multipart/form-data"));
+ assertTrue(body.contains(fileContent));
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/networking/WPNetworkImageViewTest.java b/WordPress/src/androidTest/java/org/wordpress/android/networking/WPNetworkImageViewTest.java
new file mode 100644
index 000000000..3e0f0fb50
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/networking/WPNetworkImageViewTest.java
@@ -0,0 +1,58 @@
+package org.wordpress.android.networking;
+
+import android.os.Handler;
+import android.test.InstrumentationTestCase;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader.ImageContainer;
+import com.android.volley.toolbox.ImageLoader.ImageListener;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class WPNetworkImageViewTest extends InstrumentationTestCase {
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ // https://github.com/wordpress-mobile/WordPress-Android/issues/1549
+ public void testVolleyImageLoaderGetNullHost() throws InterruptedException {
+ Handler mainLooperHandler = new Handler(WordPress.getContext().getMainLooper());
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ final boolean success[] = new boolean[1];
+ Runnable getImage = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // This call crash on old volley versions
+ WordPress.imageLoader.get("http;///hello/null/host", new ImageListener() {
+ @Override
+ public void onResponse(ImageContainer imageContainer, boolean b) {}
+
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {}
+ }, 1, 1);
+ success[0] = true;
+ } catch (Exception e) {
+ AppLog.e(T.TESTS, e);
+ success[0] = false;
+ } finally {
+ countDownLatch.countDown();
+ }
+ }
+ };
+ mainLooperHandler.post(getImage);
+ countDownLatch.await(1, TimeUnit.SECONDS);
+ assertTrue("Invalid Volley library version", success[0]);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/networking/XMLRPCTest.java b/WordPress/src/androidTest/java/org/wordpress/android/networking/XMLRPCTest.java
new file mode 100644
index 000000000..4b5c8256c
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/networking/XMLRPCTest.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.networking;
+
+import org.wordpress.android.DefaultMocksInstrumentationTestCase;
+import org.wordpress.android.mocks.XMLRPCFactoryTest;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.net.URI;
+
+public class XMLRPCTest extends DefaultMocksInstrumentationTestCase {
+ public void testNumberExceptionWithInvalidDouble() throws Exception {
+ XMLRPCFactoryTest.setPrefixAllInstances("invalid-double-xmlrpc");
+ XMLRPCClientInterface xmlrpcClientInterface = XMLRPCFactory.instantiate(URI.create("http://test.com/ast"), "",
+ "");
+ try {
+ xmlrpcClientInterface.call(Method.GET_MEDIA_LIBRARY, null);
+ } catch (NumberFormatException e) {
+ return;
+ }
+ assertTrue("invalid double format should trigger a NumberException", false);
+ }
+
+ public void testNumberExceptionWithInvalidInteger() throws Exception {
+ XMLRPCFactoryTest.setPrefixAllInstances("invalid-integer-xmlrpc");
+ XMLRPCClientInterface xmlrpcClientInterface = XMLRPCFactory.instantiate(URI.create("http://test.com/ast"), "",
+ "");
+ try {
+ xmlrpcClientInterface.call(Method.GET_MEDIA_LIBRARY, null);
+ } catch (NumberFormatException e) {
+ return;
+ }
+ assertTrue("invalid double format should trigger a NumberException", false);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/GCMIntentServiceTest.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/GCMIntentServiceTest.java
new file mode 100644
index 000000000..ae2dd501f
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/GCMIntentServiceTest.java
@@ -0,0 +1,66 @@
+package org.wordpress.android.ui.notifications;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.test.RenamingDelegatingContext;
+import android.test.ServiceTestCase;
+
+import org.wordpress.android.FactoryUtils;
+import org.wordpress.android.GCMMessageService;
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.models.AccountHelper;
+
+public class GCMIntentServiceTest extends ServiceTestCase<GCMMessageService> {
+ protected Context mTargetContext;
+
+ public GCMIntentServiceTest() {
+ super(GCMMessageService.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ FactoryUtils.initWithTestFactories();
+
+ mTargetContext = new RenamingDelegatingContext(getContext(), "test_");
+ TestUtils.clearApplicationState(mTargetContext);
+
+ setupService();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ FactoryUtils.clearFactories();
+ super.tearDown();
+ }
+
+ public void testShouldCircularizeNoteIcon() {
+ GCMMessageService intentService = new GCMMessageService();
+
+ String type = "c";
+ assertTrue(intentService.shouldCircularizeNoteIcon(type));
+
+ assertFalse(intentService.shouldCircularizeNoteIcon(null));
+
+ type = "invalidType";
+ assertFalse(intentService.shouldCircularizeNoteIcon(type));
+ }
+
+ public void testOnMessageReceived() throws InterruptedException {
+ org.wordpress.android.models.Account account = AccountHelper.getDefaultAccount();
+ account.setAccessToken("secret token");
+ account.setUserId(1);
+ final Bundle bundle = new Bundle();
+ bundle.putString("user", "1");
+ for (int i = 0; i < 1000; i++) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ getService().onMessageReceived("from", bundle);
+ }
+ }).start();
+ }
+
+ Thread.sleep(10000);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotesParseTest.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotesParseTest.java
new file mode 100644
index 000000000..0667ce023
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotesParseTest.java
@@ -0,0 +1,29 @@
+package org.wordpress.android.ui.notifications;
+
+import android.text.Spanned;
+
+import junit.framework.TestCase;
+
+import org.wordpress.android.util.HtmlUtils;
+
+public class NotesParseTest extends TestCase {
+ public void testParagraphInListItem1() {
+ String text = "<li><p>Paragraph in li</p></li>";
+ Spanned spanned = HtmlUtils.fromHtml(text);
+ // if this didn't throw a RuntimeException we're ok
+ assertNotNull(spanned);
+ }
+
+ // Trying to reproduce https://github.com/wordpress-mobile/WordPress-Android/issues/900
+ public void testSpanInListItem1() {
+ String text = "<ul><li><span>Current Record: </span><span>20</span></li><li><span>Old Record: </span><span>1</span></li></ul>";
+ Spanned spanned = HtmlUtils.fromHtml(text);
+ assertEquals("Current Record: 20\nOld Record: 1\n", spanned.toString());
+ }
+
+ public void testSpanInListItemFullTest() {
+ String text = "<p>Au Mercredi 18 septembre 2013 vous avez pulvérisé votre précédent record de follows enregistrés en un seul jour, sur votre blog <a href=\"http://taliwutblog.wordpress.com\" title=\"taliwut &amp; blog\" target=\"_blank\" notes-data-click=\"best_period_ever_feat\">taliwut &amp; blog</a>. Super!</p><ul><li><span class=\"wpn-feat-current-record-title\">Current Record: </span><span class=\"wpn-feat-new-record-count\">20</span></li><li><span class=\"wpn-feat-old-record-title\">Old Record: </span><span class=\"wpn-feat-old-record-count\">1</span></li></ul>";
+ Spanned spanned = HtmlUtils.fromHtml(text);
+ assertTrue(spanned.toString().contains("Current Record: 20\nOld Record: 1\n"));
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java
new file mode 100644
index 000000000..daef7f0ba
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java
@@ -0,0 +1,19 @@
+package org.wordpress.android.ui.notifications;
+
+import android.test.AndroidTestCase;
+import android.text.SpannableStringBuilder;
+
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+
+public class NotificationsUtilsTest extends AndroidTestCase {
+ public void testSpannableHasCharacterAtIndex() {
+ SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("This is only a test.");
+
+ assertTrue(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 3));
+ assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 4));
+
+ // Test with bogus params
+ assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(null, 'b', -1));
+ }
+
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/plans/RemoteTests.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/plans/RemoteTests.java
new file mode 100644
index 000000000..410aebc50
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/plans/RemoteTests.java
@@ -0,0 +1,159 @@
+package org.wordpress.android.ui.plans;
+
+import com.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.DefaultMocksInstrumentationTestCase;
+import org.wordpress.android.mocks.RestClientCustomizableMock;
+import org.wordpress.android.mocks.RestClientFactoryTest;
+import org.wordpress.android.networking.RestClientFactory;
+import org.wordpress.android.ui.plans.models.Feature;
+import org.wordpress.android.ui.plans.models.Plan;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RemoteTests extends DefaultMocksInstrumentationTestCase {
+ private RestClientCustomizableMock mRestClientV1_2;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // Set the version of the REST client to v1.2
+ RestClientFactoryTest.sVersion = RestClient.REST_CLIENT_VERSIONS.V1_2;
+ mRestClientV1_2 = (RestClientCustomizableMock) RestClientFactory.instantiate(null, RestClient.REST_CLIENT_VERSIONS.V1_2);
+ }
+
+ private RestRequest.ErrorListener errListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError response) {
+ AppLog.e(AppLog.T.PLANS, "The Rest Client returned an error from a mock call: " + response.getMessage());
+ assertFalse(response.getMessage(), true); // force the test to fails in this case
+ }
+ };
+
+ // Just a Utility class that wraps the main logic for the OK listener
+ private abstract class PlansRestRequestAbstractListener implements RestRequest.Listener {
+ @Override
+ public void onResponse(JSONObject response) {
+ boolean parseError = false;
+ try {
+ parseResponse(response);
+ } catch (JSONException e) {
+ parseError = true;
+ AppLog.e(AppLog.T.PLANS, e);
+ }
+ assertFalse(parseError);
+ }
+ abstract void parseResponse(JSONObject response) throws JSONException;
+ }
+
+ public void testSitePlans() throws Exception {
+ PlansRestRequestAbstractListener listener = new PlansRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ List<Plan> plans = new ArrayList<>();
+ JSONArray plansArray = response.getJSONArray("originalResponse");
+ for (int i=0; i < plansArray.length(); i ++) {
+ JSONObject currentPlanJSON = plansArray.getJSONObject(i);
+ Plan currentPlan = new Plan(currentPlanJSON);
+ plans.add(currentPlan);
+ }
+
+ assertEquals(3, plans.size());
+
+ Plan currentPlan = plans.get(0);
+ assertEquals(currentPlan.getDescription(), "Get a free blog and be on your way to publishing your first post in less than five minutes.");
+ assertEquals(currentPlan.getProductID(), 1L);
+ assertEquals(currentPlan.getProductName(), "WordPress.com Free");
+ assertEquals(currentPlan.getBillPeriod(), -1);
+ assertEquals(currentPlan.getRawPrice(), 0);
+ assertEquals(currentPlan.getCost(), 0);
+ assertEquals(currentPlan.isAvailable(), true);
+
+ currentPlan = plans.get(1);
+ assertEquals(currentPlan.isFreeTrial(), false);
+ assertEquals(currentPlan.getBundleSubscriptionID(), "5683566");
+ assertEquals(currentPlan.getExpiry(), "2017-03-07");
+ assertEquals(currentPlan.getUserFacingExpiry(), "2017-03-04");
+ assertEquals(currentPlan.getSubscribedDate(), "2016-03-07 08:56:13");
+
+ currentPlan = plans.get(2);
+ assertEquals(currentPlan.getDescription(), "Everything included with Premium, as well as live chat support, and unlimited access to our premium themes.");
+ assertEquals(currentPlan.getProductID(), 1008L);
+ assertEquals(currentPlan.getProductName(), "WordPress.com Business");
+ assertEquals(currentPlan.getBillPeriod(), 365);
+ assertEquals(currentPlan.getRawPrice(), 199);
+ assertEquals(currentPlan.getCost(), 199);
+ assertEquals(currentPlan.isAvailable(), true);
+ }
+ };
+
+
+ mRestClientV1_2.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.2/sites/123456/plans",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testFeatures() throws Exception {
+ PlansRestRequestAbstractListener listener = new PlansRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ // Parse the response from the server
+ List<Feature> features = new ArrayList<>();
+ JSONArray featuresArray = response.getJSONArray("originalResponse");
+ for (int i = 0; i < featuresArray.length(); i++) {
+ JSONObject currentFeatureJSON = featuresArray.getJSONObject(i);
+ Feature currentFeature = new Feature(currentFeatureJSON);
+ features.add(currentFeature);
+ }
+
+ assertEquals(16, features.size());
+
+ // Test the 1st object in the response
+ Feature currentFeatures = features.get(0);
+ assertEquals("WordPress.com Site", currentFeatures.getTitle());
+ assertEquals("free-blog", currentFeatures.getProductSlug());
+ assertEquals("Your own space to create posts and pages with basic customization.", currentFeatures.getDescription());
+ assertEquals("Your own space to create posts and pages with basic customization.",
+ currentFeatures.getDescriptionForPlan(1L));
+ assertEquals("Your own space to create posts and pages with basic customization.",
+ currentFeatures.getDescriptionForPlan(1003L));
+ assertEquals("Your own space to create posts and pages with basic customization.",
+ currentFeatures.getDescriptionForPlan(1008L));
+
+ assertEquals(false, currentFeatures.isNotPartOfFreeTrial());
+
+ // Test the latest object in the response
+ currentFeatures = features.get(15);
+ assertEquals("Support", currentFeatures.getTitle());
+ assertEquals("support", currentFeatures.getProductSlug());
+ assertEquals("For those times when you can't find an answer on our Support site", currentFeatures.getDescription());
+ assertEquals("Find answers to your questions in our community forum.",
+ currentFeatures.getDescriptionForPlan(1L));
+ assertEquals("Community support",
+ currentFeatures.getTitleForPlan(1L));
+ assertEquals("The kind of support we offer for Jetpack Business.",
+ currentFeatures.getDescriptionForPlan(2001L));
+ assertEquals("Priority security support",
+ currentFeatures.getTitleForPlan(2001L));
+ assertEquals(false, currentFeatures.isNotPartOfFreeTrial());
+ }
+ };
+
+ mRestClientV1_2.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.2/plans/features",
+ null,
+ listener,
+ errListener
+ );
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/posts/PostUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/posts/PostUtilsTest.java
new file mode 100644
index 000000000..d251d9f28
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/posts/PostUtilsTest.java
@@ -0,0 +1,33 @@
+package org.wordpress.android.ui.posts;
+
+import android.test.AndroidTestCase;
+
+public class PostUtilsTest extends AndroidTestCase {
+ public void testCollapseShortcodes() {
+ String postContent = "Text before first gallery [gallery number=\"one\"]"
+ + " text between galleries"
+ + " [gallery number=\"two\"]"
+ + " text after second gallery"
+ + " [unknown shortcode].";
+ String collapsedContent = PostUtils.collapseShortcodes(postContent);
+
+ // make sure [gallery] now exists and [gallery number] does not
+ assertTrue(collapsedContent.contains("[gallery]"));
+ assertFalse(collapsedContent.contains("[gallery number]"));
+
+ // make sure the unknown shortcode is intact
+ assertTrue(collapsedContent.contains("[unknown shortcode]"));
+ }
+
+ public void testShortcodeSpaces() {
+ String postContent = "[ gallery number=\"arst\" /]";
+ String collapsedContent = PostUtils.collapseShortcodes(postContent);
+ assertEquals("[gallery]", collapsedContent);
+ }
+
+ public void testOpeningClosingShortcode() {
+ String postContent = "[recipe difficulty=\"easy\"]Put your recipe here.[/recipe]";
+ String collapsedContent = PostUtils.collapseShortcodes(postContent);
+ assertEquals("[recipe]Put your recipe here.[/recipe]", collapsedContent);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/stats/RemoteTests.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/stats/RemoteTests.java
new file mode 100644
index 000000000..ff8a9505d
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/stats/RemoteTests.java
@@ -0,0 +1,638 @@
+package org.wordpress.android.ui.stats;
+
+
+import com.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.DefaultMocksInstrumentationTestCase;
+import org.wordpress.android.mocks.RestClientCustomizableMock;
+import org.wordpress.android.mocks.RestClientFactoryTest;
+import org.wordpress.android.networking.RestClientFactory;
+import org.wordpress.android.ui.stats.models.AuthorModel;
+import org.wordpress.android.ui.stats.models.ClickGroupModel;
+import org.wordpress.android.ui.stats.models.ClicksModel;
+import org.wordpress.android.ui.stats.models.CommentsModel;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.FollowerModel;
+import org.wordpress.android.ui.stats.models.FollowersModel;
+import org.wordpress.android.ui.stats.models.GeoviewModel;
+import org.wordpress.android.ui.stats.models.GeoviewsModel;
+import org.wordpress.android.ui.stats.models.InsightsAllTimeModel;
+import org.wordpress.android.ui.stats.models.InsightsPopularModel;
+import org.wordpress.android.ui.stats.models.InsightsTodayModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.models.PostViewsModel;
+import org.wordpress.android.ui.stats.models.ReferrerGroupModel;
+import org.wordpress.android.ui.stats.models.ReferrerResultModel;
+import org.wordpress.android.ui.stats.models.ReferrersModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.models.TagsContainerModel;
+import org.wordpress.android.ui.stats.models.TagsModel;
+import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel;
+import org.wordpress.android.ui.stats.models.VideoPlaysModel;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.util.AppLog;
+
+
+public class RemoteTests extends DefaultMocksInstrumentationTestCase {
+
+ private RestClientCustomizableMock mRestClient;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // Set the version of the REST client to 1.1
+ RestClientFactoryTest.sVersion = RestClient.REST_CLIENT_VERSIONS.V1_1;
+
+ mRestClient = (RestClientCustomizableMock) RestClientFactory.instantiate(null, RestClient.REST_CLIENT_VERSIONS.V1_1);
+ }
+
+ private RestRequest.ErrorListener errListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError response) {
+ AppLog.e(AppLog.T.STATS, "The Rest Client returned an error from a mock call: " + response.getMessage());
+ assertFalse(response.getMessage(), true); // force the test to fails in this case
+ }
+ };
+
+ // Just a Utility class that wraps the main logic for the OK listener
+ private abstract class StatsRestRequestAbstractListener implements RestRequest.Listener {
+ @Override
+ public void onResponse(JSONObject response) {
+ boolean parseError = false;
+ try {
+ parseResponse(response);
+ } catch (JSONException e) {
+ parseError = true;
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ assertFalse(parseError);
+ }
+ abstract void parseResponse(JSONObject response) throws JSONException;
+ }
+
+ public void testClicks() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ ClicksModel model = new ClicksModel("123456",response);
+ assertEquals(model.getTotalClicks(), 2);
+ assertEquals(model.getOtherClicks(), 0);
+ assertNotNull(model.getClickGroups());
+ assertEquals(model.getClickGroups().size(), 2);
+
+ ClickGroupModel first = model.getClickGroups().get(0);
+ assertEquals(first.getIcon(), "");
+ assertEquals(first.getUrl(), "http://astralbodies.net/blog/2013/10/31/paying-attention-at-automattic/");
+ assertEquals(first.getName(), "astralbodies.net/blog/2013/10/31/paying-attention-at-automattic/");
+ assertEquals(first.getViews(), 1);
+ assertNull(first.getClicks());
+
+ ClickGroupModel second = model.getClickGroups().get(1);
+ assertEquals(second.getIcon(), "");
+ assertEquals(second.getUrl(), "https://devforums.apple.com/thread/86137");
+ assertEquals(second.getName(), "devforums.apple.com/thread/86137");
+ assertEquals(second.getViews(), 1);
+ assertNull(second.getClicks());
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/clicks",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testClicksForMonth() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ ClicksModel model = new ClicksModel("1234567890",response);
+ assertEquals(model.getTotalClicks(), 9);
+ assertEquals(model.getOtherClicks(), 0);
+ assertNotNull(model.getClickGroups());
+ assertEquals(model.getClickGroups().size(), 6);
+
+ ClickGroupModel first = model.getClickGroups().get(0);
+ assertEquals(first.getIcon(), "");
+ assertEquals(first.getUrl(), "http://wp.com/");
+ assertEquals(first.getName(), "wp.com");
+ assertEquals(first.getViews(), 3);
+ assertNull(first.getClicks());
+
+ ClickGroupModel second = model.getClickGroups().get(1);
+ assertEquals(second.getIcon(), "");
+ assertNull(second.getUrl());
+ assertEquals(second.getName(), "blog.wordpress.tv");
+ assertEquals(second.getViews(), 2);
+ assertNotNull(second.getClicks());
+ assertEquals(second.getClicks().size(), 2);
+
+ SingleItemModel firstChild = second.getClicks().get(0);
+ assertNotNull(firstChild);
+ assertEquals(firstChild.getUrl(), "http://blog.wordpress.tv/2014/10/03/build-your-audience-recent-wordcamp-videos-from-experienced-content-creators/");
+ assertEquals(firstChild.getTitle(), "blog.wordpress.tv/2014/10/03/build-your-audience-recent-wordcamp-videos-from-experienced-content-creators/");
+ assertEquals(firstChild.getTotals(), 1);
+ assertEquals(firstChild.getIcon(), "");
+
+
+ SingleItemModel secondChild = second.getClicks().get(1);
+ assertNotNull(secondChild);
+ assertEquals(secondChild.getUrl(), "http://blog.wordpress.tv/2014/10/29/wordcamp-san-francisco-2014-state-of-the-word-keynote/");
+ assertEquals(secondChild.getTitle(), "blog.wordpress.tv/2014/10/29/wordcamp-san-francisco-2014-state-of-the-word-keynote/");
+ assertEquals(secondChild.getTotals(), 1);
+ assertEquals(secondChild.getIcon(), "");
+
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/1234567890/stats/clicks",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testCommentsDay() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ CommentsModel model = new CommentsModel("123456", response);
+ assertEquals(model.getTotalComments(), 177);
+ assertEquals(model.getMonthlyComments(), 2);
+ assertEquals(model.getMostActiveTime(), "08:00");
+ assertEquals(model.getMostActiveDay(), "");
+
+ assertNotNull(model.getAuthors());
+ assertTrue(model.getAuthors().size() == 7);
+ AuthorModel author = model.getAuthors().get(0);
+ assertEquals(author.getName(), "Aaron Douglas");
+ assertEquals(author.getViews(), 20);
+ assertEquals(author.getAvatar(),
+ "https://1.gravatar.com/avatar/db127a496309f2717657d6f6167abd49?s=64&amp;" +
+ "d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=R"
+ );
+ assertNull(author.getFollowData());
+ assertNull(author.getPosts());
+
+ assertNotNull(model.getPosts());
+ assertTrue(model.getPosts().size() == 11);
+ SingleItemModel mostCommentedPost = model.getPosts().get(0);
+ assertEquals(mostCommentedPost.getItemID(), "67");
+ assertEquals(mostCommentedPost.getTotals(), 29);
+ assertEquals(mostCommentedPost.getTitle(), "Mac Screen Sharing (VNC) & White Screen");
+ assertEquals(mostCommentedPost.getUrl(), "http://astralbodi.es/2010/05/02/mac-screen-sharing-vnc-white-screen/");
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/comments",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testCountryViewsDay() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ GeoviewsModel model = new GeoviewsModel("123456", response);
+ assertEquals(model.getOtherViews(), 17);
+ assertEquals(model.getTotalViews(), 55);
+
+ assertNotNull(model.getCountries());
+ assertEquals(model.getCountries().size(), 10);
+ GeoviewModel first = model.getCountries().get(0);
+ assertEquals(first.getCountryFullName(), "United States");
+ assertEquals(first.getFlagIconURL(), "https://secure.gravatar.com/blavatar/5a83891a81b057fed56930a6aaaf7b3c?s=48");
+ assertEquals(first.getFlatFlagIconURL(), "https://secure.gravatar.com/blavatar/9f4faa5ad0c723474f7a6d810172447c?s=48");
+ assertEquals(first.getViews(), 8);
+ GeoviewModel second = model.getCountries().get(1);
+ assertEquals(second.getCountryFullName(), "Taiwan");
+ assertEquals(second.getFlagIconURL(), "https://secure.gravatar.com/blavatar/f983fff0dda7387746b697cfd865e657?s=48");
+ assertEquals(second.getFlatFlagIconURL(), "https://secure.gravatar.com/blavatar/2c224480a40527ee89d7340d4396e8e6?s=48");
+ assertEquals(second.getViews(), 6);
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/country-views",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testFollowersEmail() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ FollowersModel model = new FollowersModel("123456", response);
+ assertEquals(model.getTotalEmail(), 2931);
+ assertEquals(model.getTotalWPCom(), 7926165);
+ assertEquals(model.getTotal(), 2931);
+ assertEquals(model.getPage(), 1);
+ assertEquals(model.getPages(), 419);
+
+ assertNotNull(model.getFollowers());
+ assertEquals(model.getFollowers().size(), 7);
+ FollowerModel first = model.getFollowers().get(0);
+ assertEquals(first.getAvatar(), "https://2.gravatar.com/avatar/e82142697283897ad7444810e5975895?s=64" +
+ "&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G");
+ assertEquals(first.getLabel(), "user1@example.com");
+ assertNull(first.getURL());
+ assertNull(first.getFollowData());
+ assertEquals(first.getDateSubscribed(), "2014-12-16T11:24:41+00:00");
+ FollowerModel last = model.getFollowers().get(6);
+ assertEquals(last.getAvatar(), "https://0.gravatar.com/avatar/3b37f38b63ce4f595cc5cfbaadb10938?s=64" +
+ "&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G");
+ assertEquals(last.getLabel(), "user7@example.com");
+ assertNull(last.getURL());
+ assertNull(last.getFollowData());
+ assertEquals(last.getDateSubscribed(), "2014-12-15T15:09:01+00:00");
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/followers",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testFollowersWPCOM() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ FollowersModel model = new FollowersModel("1234567890", response);
+ assertEquals(model.getTotalEmail(), 2930);
+ assertEquals(model.getTotalWPCom(), 7925800);
+ assertEquals(model.getTotal(), 7925800);
+ assertEquals(model.getPage(), 1);
+ assertEquals(model.getPages(), 1132258);
+
+ assertNotNull(model.getFollowers());
+ assertEquals(model.getFollowers().size(), 7);
+ FollowerModel first = model.getFollowers().get(0);
+ assertEquals(first.getAvatar(), "https://0.gravatar.com/avatar/624b89cb0c8b9136f9629dd7bcab0517?s=64" +
+ "&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&amp;r=G");
+ assertEquals(first.getLabel(), "ritu929");
+ assertEquals(first.getURL(), "http://ritu9blog.wordpress.com");
+ assertEquals(first.getDateSubscribed(), "2014-12-16T14:53:21+00:00");
+ assertNotNull(first.getFollowData());
+ FollowDataModel followDatamodel = first.getFollowData();
+ assertFalse(followDatamodel.isFollowing());
+ assertEquals(followDatamodel.getType(), "follow");
+
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/1234567890/stats/followers",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testPostDetails() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ PostViewsModel model = new PostViewsModel(response);
+ assertNotNull(model.getOriginalResponse());
+
+ assertEquals(model.getDate(), "2015-03-04");
+ assertEquals(model.getHighestMonth(), 278);
+ assertEquals(model.getHighestDayAverage(), 8);
+ assertEquals(model.getHighestWeekAverage(), 8);
+
+ assertNotNull(model.getDayViews());
+ assertEquals(model.getDayViews()[0].getViews(), 0);
+ assertEquals(model.getDayViews()[0].getPeriod(), "2014-06-04");
+ assertEquals(model.getDayViews()[model.getDayViews().length-1].getViews(), 8);
+ assertEquals(model.getDayViews()[model.getDayViews().length - 1].getPeriod(), "2015-03-04");
+
+ assertNotNull(model.getYears().size());
+ assertEquals(model.getYears().size(), 2);
+ assertEquals(model.getYears().get(0).getTotal(), 1097);
+ assertEquals(model.getYears().get(0).getLabel(), "2014");
+ assertEquals(model.getYears().get(0).getMonths().size(), 7);
+ assertEquals(model.getYears().get(0).getMonths().get(0).getMonth(), "6");
+ assertEquals(model.getYears().get(1).getTotal(), 226);
+ assertEquals(model.getYears().get(1).getLabel(), "2015");
+
+ assertNotNull(model.getWeeks().size());
+ assertEquals(model.getWeeks().size(), 6);
+
+ assertNotNull(model.getAverages());
+ assertEquals(model.getAverages().size(), 2);
+ assertEquals(model.getAverages().get(0).getTotal(), 5);
+ assertEquals(model.getAverages().get(0).getLabel(), "2014");
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/post/123",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testReferrers() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ ReferrersModel model = new ReferrersModel("123456", response);
+ assertEquals(model.getTotalViews(), 2161);
+ assertEquals(model.getOtherViews(), 938);
+ assertNotNull(model.getGroups());
+ assertEquals(model.getGroups().size(), 10);
+
+ // first group in the response
+ ReferrerGroupModel gModel = model.getGroups().get(0);
+ assertEquals(gModel.getName(), "Search Engines");
+ assertEquals(gModel.getGroupId(), "Search Engines");
+ assertEquals(gModel.getIcon(), "https://wordpress.com/i/stats/search-engine.png");
+ assertEquals(gModel.getTotal(), 480);
+ assertNotNull(gModel.getResults());
+ assertEquals(gModel.getResults().size(), 7);
+
+ // 2nd level item
+ ReferrerResultModel refResultModel = gModel.getResults().get(0);
+ assertEquals(refResultModel.getName(), "Google Search");
+ assertEquals(refResultModel.getIcon(), "https://secure.gravatar.com/blavatar/6741a05f4bc6e5b65f504c4f3df388a1?s=48");
+ assertEquals(refResultModel.getViews(), 461);
+ assertNotNull(refResultModel.getChildren());
+ assertNull(refResultModel.getUrl()); //has childs. No URL.
+
+ // 3rd level items
+ SingleItemModel child = refResultModel.getChildren().get(0);
+ assertEquals(child.getUrl(), "http://www.google.com/");
+ assertEquals(child.getTitle(), "google.com");
+ assertEquals(child.getIcon(), "https://secure.gravatar.com/blavatar/ff90821feeb2b02a33a6f9fc8e5f3fcd?s=48");
+ assertEquals(child.getTotals(), 176);
+ child = refResultModel.getChildren().get(10);
+ assertEquals(child.getUrl(), "http://www.google.co.jp");
+ assertEquals(child.getTitle(), "google.co.jp");
+ assertEquals(child.getIcon(), "https://secure.gravatar.com/blavatar/a28b8206a6562f6098688508d4665905?s=48");
+ assertEquals(child.getTotals(), 6);
+
+
+ // 7th group in the response
+ gModel = model.getGroups().get(6);
+ assertEquals(gModel.getName(), "ma.tt");
+ assertEquals(gModel.getGroupId(), "ma.tt");
+ assertEquals(gModel.getIcon(), "https://secure.gravatar.com/blavatar/733a27a6b983dd89d6dd64d0445a3e8e?s=48");
+ assertEquals(gModel.getTotal(), 56);
+ assertNotNull(gModel.getResults());
+ assertEquals(gModel.getResults().size(), 11);
+
+ // 2nd level item
+ refResultModel = gModel.getResults().get(0);
+ assertEquals(refResultModel.getName(), "ma.tt");
+ assertEquals(refResultModel.getUrl(), "http://ma.tt/");
+ assertEquals(refResultModel.getIcon(), "");
+ assertEquals(refResultModel.getViews(), 34); // No childs. Has URL.
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/referrers",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testTagsCategories() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ TagsContainerModel model = new TagsContainerModel("123456", response);
+ assertEquals(model.getDate(), "2014-12-16");
+ assertNotNull(model.getTags());
+ assertEquals(model.getTags().size(), 10);
+
+ TagsModel tag = model.getTags().get(0);
+ assertEquals(tag.getViews(), 461);
+ assertNotNull(tag.getTags());
+ assertEquals(tag.getTags().size(), 1);
+ assertNotNull(tag.getTags());
+ assertEquals(tag.getTags().get(0).getName(), "Uncategorized");
+ assertEquals(tag.getTags().get(0).getType(), "category");
+ assertEquals(tag.getTags().get(0).getLink(), "http://astralbodi.es/category/uncategorized/");
+
+ tag = model.getTags().get(9);
+ assertEquals(tag.getViews(), 41);
+ assertEquals(tag.getTags().get(0).getName(), "networking");
+ assertEquals(tag.getTags().get(0).getType(), "tag");
+ assertEquals(tag.getTags().get(0).getLink(), "http://astralbodi.es/tag/networking/");
+ assertEquals(tag.getTags().get(1).getName(), "unix");
+ assertEquals(tag.getTags().get(1).getType(), "tag");
+ assertEquals(tag.getTags().get(1).getLink(), "http://astralbodi.es/tag/unix/");
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/tags",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testTopPost() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ TopPostsAndPagesModel model = new TopPostsAndPagesModel("123456", response);
+ assertNotNull(model.getTopPostsAndPages());
+ assertEquals(model.getTopPostsAndPages().size(), 10);
+
+ PostModel postModel = model.getTopPostsAndPages().get(0);
+ assertEquals(postModel.getItemID(), "39806");
+ assertEquals(postModel.getTotals(), 2420);
+ assertEquals(postModel.getTitle(), "Home");
+ assertEquals(postModel.getUrl(), "http://automattic.com/home/");
+ assertEquals(postModel.getDate(), StatsUtils.toMs("2011-08-30 21:47:38"));
+ assertEquals(postModel.getPostType(), "page");
+
+ postModel = model.getTopPostsAndPages().get(9);
+ assertEquals(postModel.getItemID(), "39254");
+ assertEquals(postModel.getTotals(), 56);
+ assertEquals(postModel.getTitle(), "Growth Explorer");
+ assertEquals(postModel.getUrl(), "http://automattic.com/work-with-us/growth-explorer/");
+ assertEquals(postModel.getDate(), StatsUtils.toMs("2011-08-25 19:37:27"));
+ assertEquals(postModel.getPostType(), "page");
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/top-posts",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testTopPostEmptyURL() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ TopPostsAndPagesModel model = new TopPostsAndPagesModel("1234567890", response);
+ assertNotNull(model.getTopPostsAndPages());
+ assertEquals(model.getTopPostsAndPages().size(), 10);
+
+ PostModel postModel = model.getTopPostsAndPages().get(0);
+ assertEquals(postModel.getItemID(), "750");
+ assertEquals(postModel.getTotals(), 7);
+ assertEquals(postModel.getTitle(), "Asynchronous unit testing Core Data with Xcode 6");
+ assertEquals(postModel.getUrl(), ""); // This post has no URL?!? Unpublished post that was prev published?
+ assertEquals(postModel.getDate(), StatsUtils.toMs("2014-08-06 14:52:11"));
+ assertEquals(postModel.getPostType(), "post");
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/1234567890/stats/top-posts",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testInsightsAllTime() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ InsightsAllTimeModel model = new InsightsAllTimeModel("12345",response);
+ assertEquals(model.getPosts(), 128);
+ assertEquals(model.getViews(), 56687);
+ assertEquals(model.getVisitors(), 42893);
+ assertEquals(model.getViewsBestDayTotal(), 3485);
+ assertNotNull(model.getViewsBestDay());
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testInsightsToday() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ InsightsTodayModel model = new InsightsTodayModel("123456", response);
+ assertEquals(model.getDate(), "2014-10-28");
+ assertEquals(model.getBlogID(), "123456");
+ assertEquals(model.getViews(), 56);
+ assertEquals(model.getVisitors(), 44);
+ assertEquals(model.getLikes(), 1);
+ assertEquals(model.getReblogs(), 2);
+ assertEquals(model.getComments(), 3);
+ assertEquals(model.getFollowers(), 56);
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/summary",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testInsightsPopular() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ InsightsPopularModel model = new InsightsPopularModel("123456", response);
+ assertEquals(model.getHighestHour(), 9);
+ assertEquals(model.getHighestDayOfWeek(), 5);
+ assertEquals(model.getHighestDayPercent(), 30.532081377152);
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/insights",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testVideoPlaysNoData() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ VideoPlaysModel model = new VideoPlaysModel("123456", response);
+ assertEquals(model.getOtherPlays(), 0);
+ assertEquals(model.getTotalPlays(), 0);
+ assertNotNull(model.getPlays());
+ assertEquals(model.getPlays().size(), 0);
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/video-plays",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testVideoPlays() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ VideoPlaysModel model = new VideoPlaysModel("1234567890", response);
+ assertEquals(model.getOtherPlays(), 0);
+ assertEquals(model.getTotalPlays(), 2);
+ assertNotNull(model.getPlays());
+ assertEquals(model.getPlays().size(), 1);
+ SingleItemModel videoItemModel = model.getPlays().get(0);
+ assertEquals(videoItemModel.getTitle(), "Test Video");
+ assertEquals(videoItemModel.getUrl(), "http://maplebaconyummies.wordpress.com/wp-admin/media.php?action=edit&attachment_id=144");
+ assertEquals(videoItemModel.getItemID(), "144");
+ assertEquals(videoItemModel.getTotals(), 2);
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/1234567890/stats/video-plays",
+ null,
+ listener,
+ errListener
+ );
+ }
+
+ public void testVisits() throws Exception {
+ StatsRestRequestAbstractListener listener = new StatsRestRequestAbstractListener() {
+ @Override
+ void parseResponse(JSONObject response) throws JSONException {
+ VisitsModel model = new VisitsModel("123456", response);
+ assertNotNull(model.getVisits());
+ assertNotNull(model.getUnit());
+ assertNotNull(model.getDate());
+
+ assertEquals(model.getVisits().size(), 30);
+ assertEquals(model.getUnit(), "day");
+
+ VisitModel visitModel = model.getVisits().get(0);
+ assertEquals(visitModel.getViews(), 7808);
+ assertEquals(visitModel.getVisitors(), 4331);
+ assertEquals(visitModel.getLikes(), 0);
+ assertEquals(visitModel.getComments(), 0);
+ assertEquals(visitModel.getPeriod(), "2014-10-08");
+
+ }
+ };
+
+ mRestClient.makeRequest(Request.Method.POST, "https://public-api.wordpress.com/rest/v1.1/sites/123456/stats/visits",
+ null,
+ listener,
+ errListener
+ );
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/ApiHelperTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/ApiHelperTest.java
new file mode 100644
index 000000000..82b3ba214
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/ApiHelperTest.java
@@ -0,0 +1,124 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+
+import org.wordpress.android.FactoryUtils;
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.mocks.RestClientFactoryTest;
+import org.wordpress.android.mocks.XMLRPCFactoryTest;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.util.AppLog.T;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.ErrorType;
+import org.xmlrpc.android.ApiHelper.GenericCallback;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class ApiHelperTest extends InstrumentationTestCase {
+ protected Context mTargetContext;
+
+ @Override
+ protected void setUp() {
+ FactoryUtils.initWithTestFactories();
+
+ // Clean application state
+ mTargetContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_");
+ TestUtils.clearApplicationState(mTargetContext);
+
+ // Init contexts
+ XMLRPCFactoryTest.sContext = getInstrumentation().getContext();
+ RestClientFactoryTest.sContext = getInstrumentation().getContext();
+ AppLog.v(T.TESTS, "Contexts set");
+
+ // Set mode to Customizable
+ XMLRPCFactoryTest.sMode = XMLRPCFactoryTest.Mode.CUSTOMIZABLE_JSON;
+ RestClientFactoryTest.sMode = RestClientFactoryTest.Mode.CUSTOMIZABLE;
+ AppLog.v(T.TESTS, "Modes set to customizable");
+ }
+
+ @Override
+ protected void tearDown() {
+ FactoryUtils.clearFactories();
+ }
+
+ private void countDownAfterOtherAsyncTasks(final CountDownLatch countDownLatch) {
+ AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
+ @Override
+ public void run() {
+ countDownLatch.countDown();
+ }
+ });
+ }
+
+ // This test failed before #773 was fixed
+ public void testRefreshBlogContent() throws InterruptedException {
+ XMLRPCFactoryTest.setPrefixAllInstances("malformed-software-version");
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ Blog dummyBlog = new Blog("", "", "");
+ new ApiHelper.RefreshBlogContentTask(dummyBlog, new GenericCallback() {
+ @Override
+ public void onSuccess() {
+ assertTrue(true);
+ // countDown() after the serially invoked (nested) AsyncTask in RefreshBlogContentTask.
+ countDownAfterOtherAsyncTasks(countDownLatch);
+ }
+
+ @Override
+ public void onFailure(ErrorType errorType, String errorMessage, Throwable throwable) {
+ assertTrue(false);
+ // countDown() after the serially invoked (nested) AsyncTask in RefreshBlogContentTask.
+ countDownAfterOtherAsyncTasks(countDownLatch);
+ }
+ }).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, false);
+ countDownLatch.await(5000, TimeUnit.SECONDS);
+ }
+
+ // This test failed before #799 was fixed
+ public void testRefreshBlogContentEmptyResponse() throws InterruptedException {
+ XMLRPCFactoryTest.setPrefixAllInstances("empty");
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ Blog dummyBlog = new Blog("", "", "");
+ new ApiHelper.RefreshBlogContentTask(dummyBlog, new GenericCallback() {
+ @Override
+ public void onSuccess() {
+ assertTrue(false);
+ // countDown() after the serially invoked (nested) AsyncTask in RefreshBlogContentTask.
+ countDownAfterOtherAsyncTasks(countDownLatch);
+ }
+
+ @Override
+ public void onFailure(ErrorType errorType, String errorMessage, Throwable throwable) {
+ assertTrue(true);
+ // countDown() after the serially invoked (nested) AsyncTask in RefreshBlogContentTask.
+ countDownAfterOtherAsyncTasks(countDownLatch);
+ }
+ }).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, false);
+ countDownLatch.await(5000, TimeUnit.SECONDS);
+ }
+
+ public void testSpamSpammedComment() {
+ XMLRPCFactoryTest.sMode = XMLRPCFactoryTest.Mode.CUSTOMIZABLE_XML;
+ XMLRPCFactoryTest.setPrefixAllInstances("comment-already-spammed");
+ Blog dummyBlog = new Blog("", "", "");
+ // contrstust a dummy (albeit invalid) comment object to pass the comment id
+ Comment comment = new Comment(1, 2, null, null, null, null, null, null, null, null);
+
+ assertTrue(ApiHelper.editComment(dummyBlog, comment, CommentStatus.SPAM));
+ }
+
+ public void testGetSpammedCommentStatus() {
+ XMLRPCFactoryTest.sMode = XMLRPCFactoryTest.Mode.CUSTOMIZABLE_XML;
+ XMLRPCFactoryTest.setPrefixAllInstances("comment-already-spammed");
+ Blog dummyBlog = new Blog("", "", "");
+ // contrstust a dummy (albeit invalid) comment object to pass the comment id
+ Comment comment = new Comment(1, 2, null, null, null, null, null, null, null, null);
+
+ assertEquals(CommentStatus.SPAM, ApiHelper.getCommentStatus(dummyBlog, comment));
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/AutolinkUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/AutolinkUtilsTest.java
new file mode 100644
index 000000000..5341c96bd
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/AutolinkUtilsTest.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+public class AutolinkUtilsTest extends InstrumentationTestCase {
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void testNullString() {
+ AutolinkUtils.autoCreateLinks(null);
+ }
+
+ public void testEmptyString() {
+ String sourceTest = "";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ assertEquals(sourceTest, output);
+ }
+
+ public void testNonBlacklistedUrl1() {
+ String sourceTest = "http://test.com";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ String expected = "<a href=\"http://test.com\">http://test.com</a>";
+ assertEquals(expected, output);
+ }
+
+ public void testNonBlacklistedUrl2() {
+ String sourceTest = "http://test.com http://test.com";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ String expected = "<a href=\"http://test.com\">http://test.com</a> <a href=\"http://test.com\">http://test.com</a>";
+ assertEquals(expected, output);
+ }
+
+ public void testNonBlacklistedUrl3() {
+ String sourceTest = "http://test.com\nhttp://test.com";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ String expected = "<a href=\"http://test.com\">http://test.com</a>\n<a href=\"http://test.com\">http://test.com</a>";
+ assertEquals(expected, output);
+ }
+
+ public void testBlacklistedUrl1() {
+ String sourceTest = "http://youtube.com/watch?test";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ assertEquals(sourceTest, output);
+ }
+
+ public void testMixedUrls1() {
+ String sourceTest = "hey http://youtube.com/watch?test salut http://test.com hello";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ String expected = "hey http://youtube.com/watch?test salut <a href=\"http://test.com\">http://test.com</a> hello";
+ assertEquals(expected, output);
+ }
+
+ public void testExistingAHref1() {
+ String sourceTest = "<a href=\"http://test.com\">http://test.com</a>";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ assertEquals(sourceTest, output);
+ }
+
+ public void testUndetectable1() {
+ String sourceTest = "testhttp://test.com";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ assertEquals(sourceTest, output);
+ }
+
+ public void testUndetectable2() {
+ String sourceTest = "\"http://test.com\"";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ assertEquals(sourceTest, output);
+ }
+
+ public void testMixedUrls2() {
+ String sourceTest = "http://test.com http://www.youtube.com/watch?test http://test.com http://youtu.be/wat";
+ String output = AutolinkUtils.autoCreateLinks(sourceTest);
+ String expected = "<a href=\"http://test.com\">http://test.com</a> http://www.youtube.com/watch?test <a href=\"http://test.com\">http://test.com</a> http://youtu.be/wat";
+ assertEquals(expected, output);
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java
new file mode 100644
index 000000000..0e94c079e
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java
@@ -0,0 +1,227 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.test.InstrumentationTestCase;
+
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.mockwebserver.Dispatcher;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.TestUtils;
+import org.wordpress.android.WordPress;
+import org.xmlrpc.android.LoggedInputStream;
+import org.xmlrpc.android.XMLRPCUtils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+public class HealthCheckTest extends InstrumentationTestCase {
+ private static final String sAssetPathBase = "health-check/";
+ private static final String sServerAddressMagicString = "mockserver";
+ private static final String sServerResponsesMagicScheme = "asset:";
+
+ private void setLocale(String language, String country) {
+ Locale locale = new Locale(language, country);
+ Locale.setDefault(locale);
+ Resources res = getInstrumentation().getTargetContext().getResources();
+ Configuration config = res.getConfiguration();
+ config.locale = locale;
+ res.updateConfiguration(config, res.getDisplayMetrics());
+ }
+
+ @Override
+ protected void setUp() {
+ WordPress.setupVolleyQueue();
+
+ // set the app locale to english since the tests only support English for now
+ setLocale("en", "US");
+ }
+
+ @Override
+ protected void tearDown() {
+ }
+
+ private static String stringFromAsset(Context context, String assetFilename) throws IOException {
+ LoggedInputStream mLoggedInputStream = new LoggedInputStream(context.getAssets().open(assetFilename));
+ return TestUtils.convertStreamToString(mLoggedInputStream);
+ }
+
+ private static JSONObject jsonFromAsset(Context context, String assetFilename) throws IOException, JSONException {
+ return new JSONObject(stringFromAsset(context, assetFilename));
+ }
+
+ public void testHealthCheckXplat() throws JSONException, IOException {
+ JSONArray testCases = jsonFromAsset(getInstrumentation().getContext(), sAssetPathBase +
+ "health-check-xplat-testcases.json").getJSONArray("testcases");
+
+ for (int i = 0; i < testCases.length(); i++) {
+ final JSONObject testCase = testCases.getJSONObject(i);
+ final String testCaseComment = testCase.getString("comment");
+
+ final JSONObject testSetup = testCase.getJSONObject("setup");
+ final String realm = testCase.getString("realm");
+
+ switch (realm) {
+ case "URL_CANONICALIZATION":
+ runUrlCanonicalization(testCaseComment, testSetup);
+ break;
+ case "XMLPRC_DISCOVERY":
+ runXmlrpcDiscovery(testCaseComment, testSetup);
+ break;
+ default:
+ // fail the testsuite
+ assertTrue("health-check realm " + realm + " is not supported!", false);
+ break;
+ }
+ }
+ }
+
+ private void runUrlCanonicalization(String testCaseComment, JSONObject testSetup) throws JSONException {
+ final JSONObject input = testSetup.getJSONObject("input");
+
+ final String inputUrl = input.isNull("siteUrl") ? null : input.getString("siteUrl");
+
+ final JSONObject output = testSetup.getJSONObject("output");
+
+ final String outputUrl = output.optString("siteUrl", null);
+ final JSONObject error = output.optJSONObject("error");
+
+ String canonicalizedUrl = null;
+ try {
+ canonicalizedUrl = XMLRPCUtils.sanitizeSiteUrl(inputUrl, true);
+
+ // if we reached this point, it means that no error occurred
+ assertNull(testCaseMessage("Testcase defines an error but no error occurred!", testCaseComment), error);
+ } catch (XMLRPCUtils.XMLRPCUtilsException hce) {
+ assertNotNull(testCaseMessage("Error occurred but testcase does not define an error!", testCaseComment),
+ error);
+
+ assertEquals(testCaseMessage("Error message does not match the defined one!", testCaseComment), error
+ .getString("message"), getInstrumentation().getTargetContext().getString(hce.errorMsgId));
+ }
+
+ assertEquals(testCaseMessage("Canonicalized URL does not match the defined one!", testCaseComment),
+ outputUrl, canonicalizedUrl);
+ }
+
+ private void runXmlrpcDiscovery(String testCaseComment, JSONObject testSetup) throws JSONException, IOException {
+ final MockWebServer server = new MockWebServer();
+
+ testSetup = new JSONObject(replaceServerMagicName(server, testSetup.toString()));
+
+ final JSONObject input = testSetup.getJSONObject("input");
+
+ setupMockHttpServer(server, input);
+
+ final String inputUrl = input.isNull("siteUrl") ? server.url("").toString() : input.getString("siteUrl");
+
+ final JSONObject output = testSetup.getJSONObject("output");
+
+ final String outputUrl = output.optString("xmlrpcEndpoint", null);
+ final JSONObject error = output.optJSONObject("error");
+
+ String xmlrpcUrl = null;
+ try {
+ xmlrpcUrl = XMLRPCUtils.verifyOrDiscoverXmlRpcUrl(inputUrl, input.optString("username", null), input
+ .optString("username", null));
+
+ // if we reached this point, it means that no error occurred
+ assertNull(testCaseMessage("Testcase defines an error but no error occurred!", testCaseComment), error);
+ } catch (XMLRPCUtils.XMLRPCUtilsException hce) {
+ assertNotNull(testCaseMessage("Error occurred but testcase does not define an error!", testCaseComment),
+ error);
+
+ assertEquals(testCaseMessage("Error message does not match the defined one!", testCaseComment), error
+ .getString("message"), getInstrumentation().getTargetContext().getString(hce.errorMsgId));
+ }
+
+ assertEquals(testCaseMessage("XMLRPC URL does not match the defined one!", testCaseComment), outputUrl,
+ xmlrpcUrl);
+
+ server.shutdown();
+ }
+
+ private MockWebServer setupMockHttpServer(MockWebServer server, JSONObject requestResponsesJson) throws
+ JSONException, IOException {
+ final Map<RecordedRequest, MockResponse> mockRequestResponses = new HashMap<>();
+
+ final JSONArray serverMock = requestResponsesJson.getJSONArray("serverMock");
+ for (int i = 0; i < serverMock.length(); i++) {
+ final JSONObject reqRespJson = serverMock.getJSONObject(i);
+
+ final JSONObject reqJson = reqRespJson.getJSONObject("request");
+ Headers reqHeaders = json2Headers(reqJson.optJSONObject("headers"));
+
+ RecordedRequest recordedRequest = new RecordedRequest(reqJson.getString("method") + " " + reqJson
+ .getString("path") + " HTTP/1.1", reqHeaders, null, 0, null, 0, null);
+
+ final JSONObject respJson = reqRespJson.getJSONObject("response");
+ Headers respHeaders = json2Headers(respJson.optJSONObject("headers"));
+
+ String body = respJson.optString("body");
+ if (body.startsWith(sServerResponsesMagicScheme)) {
+ body = stringFromAsset(getInstrumentation().getContext(), sAssetPathBase + body.substring
+ (sServerResponsesMagicScheme.length()));
+ }
+
+ body = replaceServerMagicName(server, body);
+
+ final MockResponse resp = new MockResponse()
+ .setResponseCode(respJson.getInt("statusCode"))
+ .setHeaders(respHeaders)
+ .setBody(body);
+
+ mockRequestResponses.put(recordedRequest, resp);
+ }
+
+ server.setDispatcher(new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+
+ for (Map.Entry<RecordedRequest, MockResponse> reqResp : mockRequestResponses.entrySet()) {
+ final RecordedRequest mockRequest = reqResp.getKey();
+ if (mockRequest.getRequestLine().equals(request.getRequestLine())) {
+ return reqResp.getValue();
+ }
+ }
+ return new MockResponse().setResponseCode(404).setBody("");
+ }
+ });
+
+ return server;
+ }
+
+ private String replaceServerMagicName(MockWebServer server, String str) {
+ return str.replaceAll(sServerAddressMagicString, server.getHostName() + ":" + server.getPort());
+
+ }
+
+ private Headers json2Headers(JSONObject headersJson) throws JSONException {
+ if (headersJson != null) {
+ Headers.Builder headBuilder = new Headers.Builder();
+ Iterator<String> headerKeys = headersJson.keys();
+ while (headerKeys.hasNext()) {
+ final String headerName = headerKeys.next();
+ headBuilder.add(headerName, headersJson.getString(headerName));
+ }
+
+ return headBuilder.build();
+ }
+
+ return new Headers.Builder().build();
+ }
+
+ private String testCaseMessage(String message, String testCaseComment) {
+ return message + " (on testCase: '" + testCaseComment + "')";
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
new file mode 100644
index 000000000..25398b9d4
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+public class UrlUtilsTest extends InstrumentationTestCase {
+ public void testGetHost1() {
+ assertEquals("a.com", UrlUtils.getHost("http://a.com/test"));
+ }
+
+ public void testGetHost2() {
+ assertEquals("a.com", UrlUtils.getHost("http://a.com#.b.com/test"));
+ }
+
+ public void testGetHost3() {
+ assertEquals("a.com", UrlUtils.getHost("https://a.com"));
+ }
+
+ public void testGetHost4() {
+ assertEquals("a.com", UrlUtils.getHost("https://a.com/test#test"));
+ }
+
+ public void testGetHost5() {
+ assertEquals("", UrlUtils.getHost("a.com"));
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/WPHtmlTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/WPHtmlTest.java
new file mode 100644
index 000000000..b9ddce162
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/WPHtmlTest.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+import android.text.SpannableStringBuilder;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.helpers.AttributesImpl;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class WPHtmlTest extends InstrumentationTestCase {
+ @Override
+ protected void setUp() {
+ }
+
+ @Override
+ protected void tearDown() {
+ }
+
+ // This test failed before #685 was fixed (throws a InvocationTargetException)
+ public void testStartImg() throws NoSuchMethodException, IllegalAccessException {
+ SpannableStringBuilder text = new SpannableStringBuilder();
+ Attributes attributes = new AttributesImpl();
+
+ HtmlToSpannedConverter converter = new HtmlToSpannedConverter(null, null, null, null, null, null, 0);
+
+ // startImg is private, we use reflection to change accessibility and invoke it from here
+ Method method = HtmlToSpannedConverter.class.getDeclaredMethod("startImg", SpannableStringBuilder.class,
+ Attributes.class, WPHtml.ImageGetter.class);
+ method.setAccessible(true);
+ try {
+ method.invoke(converter, text, attributes, null);
+ } catch (InvocationTargetException e) {
+ assertTrue("startImg failed see #685", false);
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/WPUrlUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/WPUrlUtilsTest.java
new file mode 100644
index 000000000..a250198e3
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/WPUrlUtilsTest.java
@@ -0,0 +1,272 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+public class WPUrlUtilsTest extends InstrumentationTestCase {
+
+ private static final String wpcomAddress1 = "http://wordpress.com/xmlrpc.php";
+ private static final String wpcomAddress2 = "http://wordpress.com#.b.com/test";
+ private static final String wpcomAddress3 = "http://wordpress.com/xmlrpc.php";
+ private static final String wpcomAddress4 = "https://wordpress.com";
+ private static final String wpcomAddress5 = "https://wordpress.com/test#test";
+ private static final String wpcomAddress6 = "https://developer.wordpress.com";
+ private static final String notWpcomAddress1 = "http://i2.wp.com.eritreo.it#.files.wordpress.com/testpicture.gif?strip=all&quality=100&resize=1024,768";
+ private static final String notWpcomAddress2 = "wordpress.com";
+ private static final String notWpcomAddress3 = "https://thisisnotwordpress.com";
+
+
+ public void testSafeToAddAuthToken1() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(wpcomAddress1));
+ }
+
+ public void testSafeToAddAuthToken2() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(wpcomAddress2));
+ }
+
+ public void testSafeToAddAuthToken3() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(wpcomAddress3));
+ }
+
+ public void testSafeToAddAuthToken4() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(wpcomAddress4));
+ }
+ public void testSafeToAddAuthToken5() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(wpcomAddress5));
+ }
+
+ public void testSafeToAddAuthToken6() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(notWpcomAddress1));
+ }
+
+ public void testSafeToAddAuthToken7() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(notWpcomAddress2));
+ }
+
+ public void testSafeToAddAuthToken8() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(wpcomAddress1)));
+ }
+
+ public void testSafeToAddAuthToken9() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(wpcomAddress2)));
+ }
+
+ public void testSafeToAddAuthToken10() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(wpcomAddress3)));
+ }
+
+ public void testSafeToAddAuthToken11() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(wpcomAddress4)));
+ }
+ public void testSafeToAddAuthToken12() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(wpcomAddress5)));
+ }
+
+ public void testSafeToAddAuthToken13() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(notWpcomAddress1)));
+ }
+
+ public void testSafeToAddAuthToken14() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(notWpcomAddress2)));
+ }
+
+ public void testSafeToAddAuthToken15() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(wpcomAddress1)));
+ }
+
+ public void testSafeToAddAuthToken16() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(wpcomAddress2)));
+ }
+
+ public void testSafeToAddAuthToken17() {
+ // Not HTTPS
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(wpcomAddress3)));
+ }
+
+ public void testSafeToAddAuthToken18() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(wpcomAddress4)));
+ }
+ public void testSafeToAddAuthToken19() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(wpcomAddress5)));
+ }
+
+ public void testSafeToAddAuthToken20() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(notWpcomAddress1)));
+ }
+
+ public void testSafeToAddAuthToken21() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(notWpcomAddress2)));
+ }
+
+ public void testSafeToAddAuthToken22() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(notWpcomAddress3));
+ }
+
+ public void testSafeToAddAuthToken23() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(notWpcomAddress3)));
+ }
+
+ public void testSafeToAddAuthToken24() {
+ // Not wpcom
+ assertFalse(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(notWpcomAddress3)));
+ }
+
+ public void testSafeToAddAuthToken25() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(wpcomAddress6));
+ }
+
+ public void testSafeToAddAuthToken26() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(buildURL(wpcomAddress6)));
+ }
+
+ public void testSafeToAddAuthToken27() {
+ assertTrue(WPUrlUtils.safeToAddWordPressComAuthToken(buildURI(wpcomAddress6)));
+ }
+
+
+ public void testIsWPCOMString1() {
+ assertTrue(WPUrlUtils.isWordPressCom(wpcomAddress1));
+ }
+
+ public void testIsWPCOMString2() {
+ assertTrue(WPUrlUtils.isWordPressCom(wpcomAddress2));
+ }
+
+ public void testIsWPCOMString3() {
+ assertTrue(WPUrlUtils.isWordPressCom(wpcomAddress3));
+ }
+
+ public void testIsWPCOMString4() {
+ assertTrue(WPUrlUtils.isWordPressCom(wpcomAddress4));
+ }
+
+ public void testIsWPCOMString5() {
+ assertTrue(WPUrlUtils.isWordPressCom(wpcomAddress5));
+ }
+
+ public void testIsWPCOMString6() {
+ assertTrue(WPUrlUtils.isWordPressCom(wpcomAddress6));
+ }
+
+ private URL buildURL(String address) {
+ URL url = null;
+ try {
+ url = new URL(address);
+ } catch (MalformedURLException e) {}
+ return url;
+ }
+
+ public void testIsWPCOMURL1() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURL(wpcomAddress1)));
+ }
+
+ public void testIsWPCOMURL2() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURL(wpcomAddress2)));
+ }
+
+ public void testIsWPCOMURL3() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURL(wpcomAddress3)));
+ }
+
+ public void testIsWPCOMURL4() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURL(wpcomAddress4)));
+ }
+
+ public void testIsWPCOMURL5() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURL(wpcomAddress5)));
+ }
+
+ public void testIsWPCOMURL6() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURL(wpcomAddress6)));
+ }
+
+
+ private URI buildURI(String address) {
+ URI uri = null;
+ try {
+ uri = new URI(address);
+ } catch (URISyntaxException e) {}
+ return uri;
+ }
+
+ public void testIsWPCOMURI1() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURI(wpcomAddress1)));
+ }
+
+ public void testIsWPCOMURI2() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURI(wpcomAddress2)));
+ }
+
+ public void testIsWPCOMURI3() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURI(wpcomAddress3)));
+ }
+
+ public void testIsWPCOMURI4() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURI(wpcomAddress4)));
+ }
+
+ public void testIsWPCOMURI5() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURI(wpcomAddress5)));
+ }
+
+ public void testIsWPCOMURI6() {
+ assertTrue(WPUrlUtils.isWordPressCom(buildURI(wpcomAddress6)));
+ }
+
+ public void testIsNOTWPCOM1() {
+ assertFalse(WPUrlUtils.isWordPressCom(notWpcomAddress1));
+ }
+
+ public void testIsNOTWPCOM2() {
+ assertFalse(WPUrlUtils.isWordPressCom(notWpcomAddress2));
+ }
+
+ public void testIsNOTWPCOM3() {
+ assertFalse(WPUrlUtils.isWordPressCom(buildURL(notWpcomAddress1)));
+ }
+
+ public void testIsNOTWPCOM4() {
+ assertFalse(WPUrlUtils.isWordPressCom(buildURL(notWpcomAddress2)));
+ }
+
+ public void testIsNOTWPCOM5() {
+ assertFalse(WPUrlUtils.isWordPressCom(buildURI(notWpcomAddress1)));
+ }
+
+ public void testIsNOTWPCOM6() {
+ assertFalse(WPUrlUtils.isWordPressCom(buildURI(notWpcomAddress2)));
+ }
+
+ public void testIsNOTWPCOM7() {
+ assertFalse(WPUrlUtils.isWordPressCom(notWpcomAddress3));
+ }
+
+ public void testIsNOTWPCOM8() {
+ assertFalse(WPUrlUtils.isWordPressCom(buildURL(notWpcomAddress3)));
+ }
+
+ public void testIsNOTWPCOM9() {
+ assertFalse(WPUrlUtils.isWordPressCom(buildURI(notWpcomAddress3)));
+ }
+
+} \ No newline at end of file
diff --git a/WordPress/src/androidTest/monkeys/README.md b/WordPress/src/androidTest/monkeys/README.md
new file mode 100644
index 000000000..82fbc47d8
--- /dev/null
+++ b/WordPress/src/androidTest/monkeys/README.md
@@ -0,0 +1,36 @@
+# Tool `playstore-screenshots.py`
+
+This tool helps you take screenshots for the Play Store. It uses the [AndroidViewClient](https://github.com/dtmilano/AndroidViewClient) python library to control and send commands to emulators or devices. It connects to a real blog and change the emulator or device language to take localized screenshots.
+
+## Prerequisites
+
+* your devices or emulators must be connected to the Internet and rooted.
+* `adb` binary must be in your `PATH` when you run the tool.
+
+## Setup
+
+Install dependencies: Python 2.x and easy_install, then you have to install AndroidViewClient
+
+ $ easy_install --upgrade androidviewclient
+
+Then edit the `settings.py` file. Copy the example file and edit it. Change the username / password, languages and sample text.
+
+ $ cp settings.py-example settings.py
+
+## Run
+
+ $ ./playstore-screenshots.py PACKAGE_NAME APK_FILE
+ $ # Example: ./playstore-screenshots.py org.wordpress.android WordPress-vanilla-release.apk
+ $ # Example: ./playstore-screenshots.py org.wordpress.android.beta WordPress-zbetagroup-release.apk
+
+## Example usage
+
+1. Unplug real devices from your computer
+1. Start 3 Genymotion emulators: a Nexus 5 emulated screen, a Nexus 7 emulated screen and a Nexus 9 emulated screen.
+1. Set the Nexus 7 and 9 in landscape mode.
+1. Run the script:
+
+ ./playstore-screenshots.py org.wordpress.android ../../../../WordPress/build/outputs/apk/WordPress-vanilla-release.apk
+
+1. You'll find the screenshot files (eg. `fr-drawer-opened-Google_Nexus_5___5_0_0___API_21___1080x1920.png`) in the same directory.
+
diff --git a/WordPress/src/androidTest/monkeys/create_wpcom_blog_from_settings.py b/WordPress/src/androidTest/monkeys/create_wpcom_blog_from_settings.py
new file mode 100644
index 000000000..b6f9ad44a
--- /dev/null
+++ b/WordPress/src/androidTest/monkeys/create_wpcom_blog_from_settings.py
@@ -0,0 +1,44 @@
+import re
+import sys
+import os
+import time
+
+sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
+from com.dtmilano.android.viewclient import ViewClient, View
+FLAG_ACTIVITY_NEW_TASK = 0x10000000
+
+def init_view_client():
+ package = 'org.wordpress.android'
+ #activity = '.ui.posts.PostsActivity'
+ activity = '.ui.accounts.NewBlogActivity'
+ device, serialno = ViewClient.connectToDeviceOrExit()
+ component = package + '/' + activity
+ device.startActivity(component=component, flags=FLAG_ACTIVITY_NEW_TASK)
+ ViewClient.sleep(2)
+ return ViewClient(device, serialno), device
+
+vc, device = init_view_client()
+ViewClient.sleep(1)
+
+def createABlog(name):
+ vc.dump(0.1)
+ settingsButton = vc.findViewWithText('Create a WordPress.com blog')
+ while settingsButton == None:
+ device.press('KEYCODE_DPAD_DOWN')
+ device.press('KEYCODE_DPAD_DOWN')
+ device.press('KEYCODE_DPAD_DOWN')
+ device.press('KEYCODE_DPAD_DOWN')
+ vc.dump(0.1)
+ settingsButton = vc.findViewWithText('Create a WordPress.com blog')
+ settingsButton.touch()
+ vc.dump(0.5)
+ device.type(name)
+ time.sleep(1)
+ device.press('KEYCODE_DPAD_DOWN')
+ device.press('KEYCODE_DPAD_DOWN')
+ device.press('KEYCODE_DPAD_CENTER')
+ time.sleep(30)
+
+for i in xrange(90, 1001):
+ createABlog("taliwuttalot" + str(i))
+ time.sleep(2)
diff --git a/WordPress/src/androidTest/monkeys/customizable_monkey.py b/WordPress/src/androidTest/monkeys/customizable_monkey.py
new file mode 100644
index 000000000..45a80ed83
--- /dev/null
+++ b/WordPress/src/androidTest/monkeys/customizable_monkey.py
@@ -0,0 +1,44 @@
+import re
+import sys
+import os
+import time
+import random
+import shlex
+import subprocess
+
+sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
+from com.dtmilano.android.viewclient import ViewClient, View
+FLAG_ACTIVITY_NEW_TASK = 0x10000000
+
+
+class CMonkey:
+ def __init__(self):
+ self.package = 'org.wordpress.android'
+ self.activity = '.ui.themes.ThemeBrowserActivity'
+ self.component = self.package + '/' + self.activity
+ self.init_device()
+
+ def init_device(self):
+ self.device, self.serialno = ViewClient.connectToDeviceOrExit()
+ self.vc = ViewClient(self.device, self.serialno)
+
+ def start_activity(self):
+ self.device.startActivity(component=self.component,
+ flags=FLAG_ACTIVITY_NEW_TASK)
+
+ def random_tap(self):
+ x, y = random.randint(0, 2000), random.randint(0, 2000)
+ self.device.touch(x, y)
+
+ def test_comments(self):
+ for i in range(20):
+ self.random_tap()
+
+def test():
+ cm = CMonkey()
+ for i in range(1000):
+ cm.start_activity()
+ args = shlex.split("adb shell monkey -p org.wordpress.android 500")
+ p = subprocess.Popen(args)
+
+test() \ No newline at end of file
diff --git a/WordPress/src/androidTest/monkeys/playstore-screenshots.py b/WordPress/src/androidTest/monkeys/playstore-screenshots.py
new file mode 100755
index 000000000..68ed7d4a7
--- /dev/null
+++ b/WordPress/src/androidTest/monkeys/playstore-screenshots.py
@@ -0,0 +1,202 @@
+#! /usr/bin/env python
+
+import sys
+import os
+import time
+import settings
+from subprocess import Popen, PIPE
+from com.dtmilano.android.viewclient import ViewClient
+
+# App actions
+
+def action_login(device, serialno):
+ print("login")
+ device.type(settings.username)
+ device.press('KEYCODE_DPAD_DOWN')
+ device.type(settings.password)
+ device.press('KEYCODE_ENTER')
+ # time to login
+ time.sleep(10)
+ # lose focus
+ device.press('KEYCODE_DPAD_DOWN')
+ time.sleep(1)
+
+def action_open_my_sites(device, serialno):
+ print("open my sites")
+ device.press('KEYCODE_TAB')
+ for i in range(5):
+ device.press('KEYCODE_DPAD_LEFT')
+ # Wait for gravatars to load
+ time.sleep(2)
+
+def action_open_reader_freshlypressed(device, serialno):
+ print("open reader")
+ # Open the reader
+ for i in range(5):
+ device.press('KEYCODE_DPAD_LEFT')
+ device.press('KEYCODE_DPAD_RIGHT')
+ device.press('KEYCODE_ENTER')
+ # Wait for the reader to load articles / pictures
+ time.sleep(5)
+
+def action_open_notifications(device, serialno):
+ print("open notifications tab")
+ # Open the reader
+ for i in range(5):
+ device.press('KEYCODE_DPAD_LEFT')
+ for i in range(4):
+ device.press('KEYCODE_DPAD_RIGHT')
+ device.press('KEYCODE_ENTER')
+ time.sleep(5)
+
+def action_open_me(device, serialno):
+ print("open me tab")
+ # Open the reader
+ for i in range(5):
+ device.press('KEYCODE_DPAD_LEFT')
+ for i in range(2):
+ device.press('KEYCODE_DPAD_RIGHT')
+ device.press('KEYCODE_ENTER')
+ time.sleep(5)
+
+def action_open_editor_and_type_text(device, serialno):
+ print("open editor")
+ # Open My Sites
+ for i in range(5):
+ device.press('KEYCODE_DPAD_LEFT')
+
+ viewclient = ViewClient(device, serialno)
+ viewclient.dump()
+ view = viewclient.findViewById("org.wordpress.android:id/fab_button")
+ view.touch()
+ time.sleep(2)
+
+ viewclient.dump()
+ view = viewclient.findViewById("org.wordpress.android:id/post_content")
+
+ # Type a sample text (spaces can't be entered via device.type())
+ view.type(settings.example_post_content)
+ time.sleep(2)
+
+def action_open_stats(device, serialno):
+ print("open stats tab")
+ device.press('KEYCODE_TAB')
+ for i in range(5):
+ device.press('KEYCODE_DPAD_LEFT')
+ viewclient = ViewClient(device, serialno)
+ viewclient.dump()
+ view = viewclient.findViewById("org.wordpress.android:id/row_stats")
+ view.touch()
+ time.sleep(5)
+
+# Utilities
+
+def lose_focus(serialno):
+ # tap point 0,0 to lose focus
+ _adb_shell(serialno, " input tap 0 0")
+ time.sleep(1)
+
+def take_screenshot(serialno, filename):
+ os.popen("adb -s '%s' shell /system/bin/screencap -p /sdcard/screenshot.png" % (serialno))
+ os.popen("adb -s '%s' pull /sdcard/screenshot.png '%s'" % (serialno, filename))
+
+def launch_activity(device, package, activity):
+ component = package + "/" + activity
+ FLAG_ACTIVITY_NEW_TASK = 0x10000000
+ device.startActivity(component=component, flags=FLAG_ACTIVITY_NEW_TASK)
+ time.sleep(2)
+
+def _adb_shell(serialno, command):
+ print("adb -s '%s' shell \"%s\"" % (serialno, command))
+ os.popen("adb -s '%s' shell \"%s\"" % (serialno, command))
+
+def change_lang_settings(serialno, lang):
+ adb_command = "su -c 'setprop persist.sys.language %s; setprop persist.sys.country %s; stop; start'"
+ _adb_shell(serialno, adb_command % (lang, lang))
+ # time to reload
+ time.sleep(15)
+ unlock_screen(serialno)
+
+def unlock_screen(serialno):
+ _adb_shell(serialno, "input keyevent 82")
+ time.sleep(1)
+
+def reinstall_apk(serialno, packagename, apk):
+ os.popen("adb -s '%s' uninstall '%s'" % (serialno, packagename))
+ os.popen("adb -s '%s' install '%s'" % (serialno, apk))
+
+def back(device):
+ device.press('KEYCODE_BACK')
+
+# Main scenario + screenshots
+
+def run_tests_for_device_and_lang(device, serialno, filename, lang, packagename, apk):
+ # Install the apk`
+ reinstall_apk(serialno, packagename, apk)
+
+ # Change language setting
+ change_lang_settings(serialno, lang)
+
+ # Launch the app
+ launch_activity(device, packagename, "org.wordpress.android.ui.WPLaunchActivity")
+ take_screenshot(serialno, lang + "-99-login-screen-" + filename)
+
+ # Login into the app
+ action_login(device, serialno)
+
+ # Action!
+ action_open_my_sites(device, serialno)
+ take_screenshot(serialno, lang + "-1-my-sites-" + filename)
+ action_open_reader_freshlypressed(device, serialno)
+ take_screenshot(serialno, lang + "-2-reader-discover-" + filename)
+ action_open_me(device, serialno)
+ take_screenshot(serialno, lang + "-3-me-tab-" + filename)
+ action_open_notifications(device, serialno)
+ take_screenshot(serialno, lang + "-4-notifications-" + filename)
+ action_open_stats(device, serialno)
+ take_screenshot(serialno, lang + "-5-stats-" + filename)
+ back(device)
+ action_open_editor_and_type_text(device, serialno)
+ take_screenshot(serialno, lang + "-6-editor-" + filename)
+ # Close virtual keyboard
+ back(device)
+ # Close the editor
+ back(device)
+
+def list_devices():
+ devices = []
+ process = Popen("adb devices -l", stdout=PIPE, shell=True)
+ for line in iter(process.stdout.readline, ''):
+ split = line.split()
+ if len(split) <= 1 or split[0] == "List":
+ continue
+ devices.append({"name": split[3].replace("model:", ""), "serialno": split[0]})
+ process.communicate()
+ return devices
+
+def run_tests_on_device(packagename, apk, serialno, name, lang):
+ device, serialno = ViewClient.connectToDeviceOrExit(verbose=False, serialno=serialno)
+ filename = name + ".png"
+ run_tests_for_device_and_lang(device, serialno, filename, lang, packagename, apk)
+
+def run_tests_on_all_devices(packagename, apk, lang):
+ devices = list_devices()
+ if not devices:
+ print("No device found")
+ return
+ for device in devices:
+ print("Running on %s - language: %s" % (device, lang))
+ run_tests_on_device(packagename, apk, device["serialno"], device["name"], lang)
+
+def run_tests_on_all_devices_for_all_languages(packagename, apk):
+ for lang in settings.languages:
+ run_tests_on_all_devices(packagename, apk, lang)
+
+def main():
+ if len(sys.argv) < 3:
+ sys.exit("usage: %s packagename apk" % sys.argv[0])
+ packagename = sys.argv.pop(1)
+ apk = sys.argv.pop(1)
+ run_tests_on_all_devices_for_all_languages(packagename, apk)
+
+main()
diff --git a/WordPress/src/androidTest/monkeys/settings.py-example b/WordPress/src/androidTest/monkeys/settings.py-example
new file mode 100644
index 000000000..5cbcf761f
--- /dev/null
+++ b/WordPress/src/androidTest/monkeys/settings.py-example
@@ -0,0 +1,4 @@
+username = "wordpress"
+password = "wordpress"
+languages = ("en", "fr")
+example_post_content = "Content"
diff --git a/WordPress/src/androidTest/proguard-project.txt b/WordPress/src/androidTest/proguard-project.txt
new file mode 100644
index 000000000..f2fe1559a
--- /dev/null
+++ b/WordPress/src/androidTest/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# 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 *;
+#}
diff --git a/WordPress/src/androidTest/readme.md b/WordPress/src/androidTest/readme.md
new file mode 100644
index 000000000..7d72476f7
--- /dev/null
+++ b/WordPress/src/androidTest/readme.md
@@ -0,0 +1,9 @@
+# WordPress Android - Test Project #
+
+## Run tests ##
+
+ $ ./gradlew cIT
+
+## Dump a test database ##
+
+ $ adb shell su -c "echo .dump | sqlite3 /data/data/org.wordpress.android/databases/wordpress"
diff --git a/WordPress/src/androidTest/tools/dump-device-db.sh b/WordPress/src/androidTest/tools/dump-device-db.sh
new file mode 100755
index 000000000..27d2ce2e3
--- /dev/null
+++ b/WordPress/src/androidTest/tools/dump-device-db.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+adb shell su -c "echo .dump | sqlite3 /data/data/org.wordpress.android/databases/wordpress"
diff --git a/WordPress/src/future/svg/stats_widget_promo_header.svg b/WordPress/src/future/svg/stats_widget_promo_header.svg
new file mode 100644
index 000000000..b8e502460
--- /dev/null
+++ b/WordPress/src/future/svg/stats_widget_promo_header.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 420 180" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><rect id="XMLID_32_" x="-0.2" y="0" width="420.2" height="180" style="fill:#02aadc;"/><g id="XMLID_12_"><g id="XMLID_3_"><g id="XMLID_1_"><path id="XMLID_53_" d="M209.9,68c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c-0.1,0.7 -0.6,1.3 -1.3,1.3ZM209.9,50.1c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c-0.1,0.8 -0.6,1.3 -1.3,1.3ZM209.9,32.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-0.9c0,-3.5 2.9,-6.4 6.4,-6.4c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3c-2.1,0 -3.8,1.7 -3.8,3.8l0,0.9c-0.1,0.7 -0.6,1.3 -1.3,1.3l2.84217e-14,0ZM268.5,26.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c-0.1,0.7 -0.6,1.3 -1.3,1.3ZM250.6,26.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3ZM232.8,26.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c-2.84217e-14,0.7 -0.6,1.3 -1.3,1.3ZM317.1,66.7l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM317.1,84.6l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,1.42109e-14 -1.3,-0.6 -1.3,-1.3ZM317.1,48.9l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,-0.1 -1.3,-0.6 -1.3,-1.3ZM317.1,31l0,-0.9c0,-2.1 -1.7,-3.8 -3.8,-3.8c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3c3.5,0 6.4,2.9 6.4,6.4l0,0.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l5.68434e-14,3.55271e-15ZM258.5,25c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM276.3,25c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM294.1,25c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3Z" style="fill:#fff;fill-rule:nonzero;"/><path id="XMLID_69_" d="M211.1,91.5l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3ZM211.1,109.4l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3ZM211.1,127.2l0,0.9c0,2.1 1.7,3.8 3.8,3.8c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3c-3.5,0 -6.4,-2.9 -6.4,-6.4l0,-0.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l-2.84217e-14,-1.42109e-14ZM269.7,133.2c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.8,2.84217e-14 1.3,0.6 1.3,1.3ZM251.9,133.2c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,2.84217e-14 1.3,0.6 1.3,1.3ZM234.1,133.2c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,2.84217e-14 1.3,0.6 1.3,1.3ZM318.4,92.2c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c5.68434e-14,-0.7 0.6,-1.3 1.3,-1.3ZM318.4,108.1c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c5.68434e-14,-0.7 0.6,-1.3 1.3,-1.3ZM318.4,125.9c0.7,0 1.3,0.6 1.3,1.3l0,0.9c0,3.5 -2.9,6.4 -6.4,6.4c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3c2.1,0 3.8,-1.7 3.8,-3.8l0,-0.9c0,-0.7 0.6,-1.3 1.3,-1.3l-5.68434e-14,4.26326e-14ZM259.8,131.9l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3ZM277.6,131.9l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3ZM295.4,131.9l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c5.68434e-14,-0.7 0.6,-1.3 1.3,-1.3Z" style="fill:#fff;fill-rule:nonzero;"/><path id="XMLID_30_" d="M268.9,76.4l-5.6,0l0,-5.6c0,-0.8 -0.6,-1.4 -1.4,-1.4c-0.8,0 -1.4,0.6 -1.4,1.4l0,5.6l-5.6,0c-0.8,0 -1.4,0.6 -1.4,1.4c0,0.8 0.6,1.4 1.4,1.4l5.6,0l0,4.9c0,0.8 0.6,1.4 1.4,1.4c0.8,0 1.4,-0.6 1.4,-1.4l0,-4.9l5.6,0c0.8,0 1.4,-0.6 1.4,-1.4c0,-0.8 -0.7,-1.4 -1.4,-1.4Z" style="fill:#fff;fill-rule:nonzero;"/></g><g id="XMLID_2_"><g id="XMLID_90_"><path id="XMLID_101_" d="M221.2,148.4l-88.8,0c-4.4,0 -8,-3.6 -8,-8l0,-88.8c0,-4.4 3.6,-8 8,-8l88.8,0c4.4,0 8,3.6 8,8l0,88.8c0,4.4 -3.6,8 -8,8Z" style="fill:#eaeff2;fill-rule:nonzero;"/><rect id="XMLID_100_" x="140.5" y="85.1" width="20.1" height="54.4" style="fill:#92afc2;"/><rect id="XMLID_99_" x="140.5" y="85.1" width="10.1" height="54.4" style="fill:#b1c6d4;"/><rect id="XMLID_98_" x="166.7" y="59.7" width="20.1" height="79.7" style="fill:#92afc2;"/><rect id="XMLID_97_" x="166.7" y="59.7" width="10.1" height="79.7" style="fill:#b1c6d4;"/><rect id="XMLID_96_" x="193" y="72.7" width="20.1" height="66.7" style="fill:#92afc2;"/><rect id="XMLID_95_" x="193" y="72.7" width="10.1" height="66.7" style="fill:#b1c6d4;"/><path id="XMLID_25_" d="M229.2,74.2l-10.2,13.2c-14.6,19.2 -23.5,29.6 -31.5,33.4c-6.6,3.1 -11.2,4.5 -21.4,-6.7c-1.2,-1.3 -2.3,-2.6 -3.7,-4.2c-0.5,-0.6 -1.1,-1.2 -1.7,-1.9c-5.3,-6.1 -11.1,-8.9 -17.6,-8.5c-7.4,0.5 -14.2,5.2 -18.7,9.8l0,7.5c4,-4.8 11.2,-11.7 19.1,-12.2c4.9,-0.3 9.2,1.9 13.4,6.7c0.6,0.7 1.1,1.3 1.6,1.9c1.4,1.6 2.5,2.9 3.8,4.3c7,7.6 12.4,10.5 17.7,10.5c3.2,0 6.3,-1 9.8,-2.7c9,-4.2 17.9,-14.6 33.4,-34.9l6.1,-7.9l0,-8.3l-0.1,0l-5.68434e-14,0Z" style="fill:#4a5668;fill-rule:nonzero;"/><circle id="XMLID_93_" cx="177.1" cy="124.6" r="6" style="fill:#fff;"/><circle id="XMLID_92_" cx="203.3" cy="110.5" r="6" style="fill:#fff;"/><circle id="XMLID_91_" cx="150.9" cy="102.8" r="6" style="fill:#fff;"/></g></g></g><g id="XMLID_10_"><path id="XMLID_7_" d="M160,141.8l-4.9,38.2l-25.9,0l30.8,-38.2Z" style="fill:#495668;fill-opacity:0.4;fill-rule:nonzero;"/><path id="XMLID_11_" d="M140.6,180.1l16.7,-28.9c5.2,-9 2.6,-20.3 -5.7,-25.1c-8.3,-4.8 -19.4,-1.4 -24.6,7.6l-26.8,46.4l40.4,0Z" style="fill:#e2d1ae;fill-rule:nonzero;"/><path id="XMLID_8_" d="M152.8,148.3c3.5,-6.1 1.7,-13.8 -4.1,-17.1c-5.8,-3.3 -13.4,-1.1 -16.9,5c-0.1,0.1 -0.1,0.2 -0.2,0.3l-0.1,0.1l-5.1,8.8c-0.7,1.2 -0.2,2.8 1.1,3.6l16.2,9.3c1.3,0.8 3,0.4 3.7,-0.8l5.1,-8.8c0,0 0,-0.1 0.1,-0.1c0,-0.1 0.1,-0.2 0.2,-0.3l2.84217e-14,0Z" style="fill:#ece3ce;fill-rule:nonzero;"/><path id="XMLID_9_" d="M143.6,158.3c0.9,-2.3 -1.6,-5.9 -5.8,-8.4c-4.2,-2.4 -8.6,-2.8 -10.1,-0.9l15.9,9.3Z" style="fill:#fff;fill-rule:nonzero;"/><g id="XMLID_4_"><path id="XMLID_5_" d="M139.8,167.6l-18.8,-10.8c-0.6,-0.4 -0.8,-1.1 -0.5,-1.8c0.4,-0.6 1.1,-0.8 1.8,-0.5l18.8,10.8c0.6,0.4 0.8,1.1 0.5,1.8c-0.4,0.7 -1.2,0.9 -1.8,0.5Z" style="fill:#cfbc96;fill-rule:nonzero;"/><path id="XMLID_6_" d="M137,172.5l-18.8,-10.8c-0.6,-0.4 -0.8,-1.1 -0.5,-1.8c0.4,-0.6 1.1,-0.8 1.8,-0.5l18.8,10.8c0.6,0.4 0.8,1.1 0.5,1.8c-0.4,0.6 -1.2,0.9 -1.8,0.5Z" style="fill:#cfbc96;fill-rule:nonzero;"/></g></g></g></svg> \ No newline at end of file
diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1c0c1fb85
--- /dev/null
+++ b/WordPress/src/main/AndroidManifest.xml
@@ -0,0 +1,527 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="org.wordpress.android"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:installLocation="auto">
+
+ <supports-screens
+ android:anyDensity="true"
+ android:largeScreens="true"
+ android:normalScreens="true"
+ android:smallScreens="true" />
+
+ <!-- Normal permissions, access automatically granted to app -->
+ <uses-permission android:name="android.permission.VIBRATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
+ <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />
+
+ <!-- Dangerous permissions, access must be requested at runtime -->
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+ <!-- GCM all build types configuration -->
+ <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <!-- self-defined permission prevents other apps to hijack PNs -->
+ <permission
+ android:name="${applicationId}.permission.C2D_MESSAGE"
+ android:protectionLevel="signature" />
+
+ <uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
+
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.location"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.location.gps"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.location.network"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.portrait"
+ android:required="false" />
+
+ <application
+ android:name=".WordPress"
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_scheme"
+ android:hardwareAccelerated="true"
+ android:icon="@mipmap/app_icon"
+ android:label="@string/app_name"
+ android:largeHeap="true"
+ android:theme="@style/WordPress"
+ tools:replace="allowBackup, icon">
+ <activity
+ android:name=".ui.WPLaunchActivity"
+ android:noHistory="true"
+ android:theme="@android:style/Theme.NoDisplay">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <!-- Main tab activity -->
+ <activity
+ android:name=".ui.main.WPMainActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+
+ <!-- Account activities -->
+ <activity
+ android:name=".ui.accounts.SignInActivity"
+ android:configChanges="orientation|keyboardHidden|screenSize"
+ android:theme="@style/SignInTheme"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="signin"
+ android:scheme="wordpress" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".ui.accounts.NewBlogActivity"
+ android:configChanges="orientation|keyboardHidden|screenSize"
+ android:theme="@style/SignInTheme"
+ android:windowSoftInputMode="adjustResize" />
+ <activity
+ android:name=".ui.accounts.HelpActivity"
+ android:label=""
+ android:theme="@style/CalypsoTheme.NoActionBarShadow" />
+ <!-- empty title -->
+
+ <!-- Preferences activities -->
+ <activity
+ android:name=".ui.prefs.AboutActivity"
+ android:theme="@style/Theme.AppCompat.NoActionBar" />
+ <activity
+ android:name=".ui.prefs.BlogPreferencesActivity"
+ android:configChanges="orientation|screenSize"
+ android:label="@string/settings"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".ui.prefs.LicensesActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.prefs.AccountSettingsActivity"
+ android:configChanges="orientation|screenSize"
+ android:label="@string/account_settings"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".ui.prefs.AppSettingsActivity"
+ android:configChanges="locale|orientation|screenSize"
+ android:label="@string/me_btn_app_settings"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".ui.prefs.notifications.NotificationsSettingsActivity"
+ android:configChanges="orientation|screenSize"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".networking.SSLCertsViewActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+
+ <!-- Comments activities -->
+ <activity
+ android:name=".ui.comments.CommentsActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.comments.CommentDetailActivity"
+ android:theme="@style/CalypsoTheme"
+ android:windowSoftInputMode="stateHidden" />
+ <activity
+ android:name=".ui.comments.EditCommentActivity"
+ android:theme="@style/CalypsoTheme" />
+
+ <!-- Posts activities -->
+ <activity
+ android:name=".ui.posts.AddCategoryActivity"
+ android:label="@string/add_new_category"
+ android:theme="@style/Theme.AppCompat.Light.Dialog" />
+ <activity
+ android:name=".editor.legacy.EditLinkActivity"
+ android:label="@string/create_a_link"
+ android:theme="@style/Theme.AppCompat.Light.Dialog"
+ android:windowSoftInputMode="stateVisible" />
+ <activity
+ android:name=".ui.posts.EditPostActivity"
+ android:configChanges="orientation|keyboardHidden|screenSize"
+ android:theme="@style/CalypsoTheme"
+ android:windowSoftInputMode="stateHidden|adjustResize">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".ui.posts.PostsListActivity" />
+
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+ <!-- Workaround for old launcher icon pointing to .ui.posts.PostsActivity -->
+ <activity-alias
+ android:name=".ui.posts.PostsActivity"
+ android:enabled="true"
+ android:targetActivity=".ui.WPLaunchActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity-alias>
+
+ <activity
+ android:name=".ui.posts.PostsListActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.posts.PostPreviewActivity"
+ android:label="@string/preview_post"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.posts.SelectCategoriesActivity"
+ android:theme="@style/CalypsoTheme" />
+
+ <!-- plans -->
+ <activity android:name=".ui.plans.PlansActivity"
+ android:theme="@style/Calypso.NoActionBar"/>
+ <activity android:name=".ui.plans.PlanPostPurchaseActivity"
+ android:theme="@style/Calypso.NoActionBar"/>
+
+ <!-- Stats Activities -->
+ <activity
+ android:name=".ui.stats.StatsActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.stats.StatsViewAllActivity"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".ui.stats.StatsSingleItemDetailsActivity"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".ui.stats.StatsWidgetConfigureActivity"
+ android:label="@string/site_picker_title"
+ android:theme="@style/CalypsoTheme">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".ui.WPWebViewActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+
+ <!-- Media Activities -->
+ <activity
+ android:name=".ui.media.MediaPickerActivity"
+ android:theme="@style/CalypsoTheme"
+ android:windowSoftInputMode="stateHidden" />
+ <activity
+ android:name=".ui.media.MediaBrowserActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity android:name=".ui.media.MediaGalleryActivity" />
+ <activity android:name=".ui.media.MediaGalleryPickerActivity" />
+
+ <!-- Theme Activities -->
+ <activity
+ android:name=".ui.themes.ThemeBrowserActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.themes.ThemeWebActivity"
+ android:label="@string/selected_theme"
+ android:parentActivityName=".ui.themes.ThemeBrowserActivity">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".ui.themes.ThemeBrowserActivity" />
+ </activity>
+
+ <!-- Deep Linking Activity -->
+ <activity
+ android:name=".ui.DeepLinkingIntentReceiverActivity"
+ android:theme="@style/Calypso.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data
+ android:host="viewpost"
+ android:scheme="wordpress" />
+ </intent-filter>
+ </activity>
+
+ <!-- Reader Activities -->
+ <activity
+ android:name=".ui.reader.ReaderPostListActivity"
+ android:label="@string/reader"
+ android:theme="@style/Calypso.NoActionBar">
+ </activity>
+ <activity
+ android:name=".ui.reader.ReaderPostPagerActivity"
+ android:label="@string/reader_title_post_detail"
+ android:theme="@style/Calypso.NoActionBar"
+ android:windowSoftInputMode="adjustResize" />
+ <activity
+ android:name=".ui.reader.ReaderCommentListActivity"
+ android:label="@string/reader_title_comments"
+ android:theme="@style/Calypso.NoActionBar"
+ android:windowSoftInputMode="adjustResize|stateHidden" />
+ <activity
+ android:name=".ui.AppLogViewerActivity"
+ android:label="@string/reader_title_applog"
+ android:theme="@style/CalypsoTheme" />
+ <activity
+ android:name=".ui.reader.ReaderUserListActivity"
+ android:theme="@style/Calypso.NoActionBar" />
+ <activity
+ android:name=".ui.reader.ReaderSubsActivity"
+ android:label="@string/reader_title_subs"
+ android:theme="@style/Calypso.NoActionBar"
+ android:windowSoftInputMode="stateHidden" />
+ <activity
+ android:name=".ui.reader.ReaderPhotoViewerActivity"
+ android:theme="@style/ReaderPhotoViewerTheme" />
+
+ <!-- Other activities -->
+
+ <activity
+ android:name=".ui.ShareIntentReceiverActivity"
+ android:theme="@style/Calypso.FloatingActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <action android:name="android.intent.action.SEND_MULTIPLE" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:mimeType="text/plain" />
+ <data android:mimeType="image/*" />
+ <data android:mimeType="video/*" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".ui.AddQuickPressShortcutActivity"
+ android:label="WordPress QuickPress">
+ <intent-filter>
+ <action android:name="android.intent.action.CREATE_SHORTCUT" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".ui.main.SitePickerActivity"
+ android:label="@string/site_picker_title"
+ android:theme="@style/CalypsoTheme" />
+
+ <!-- Notifications activities -->
+ <activity
+ android:name=".ui.notifications.NotificationsDetailActivity"
+ android:theme="@style/CalypsoTheme" />
+
+ <!-- Passcode lock activities -->
+ <activity
+ android:name="org.wordpress.passcodelock.PasscodeUnlockActivity"
+ android:theme="@style/CalypsoTheme"
+ android:windowSoftInputMode="stateHidden" />
+ <activity
+ android:name="org.wordpress.passcodelock.PasscodeManagePasswordActivity"
+ android:theme="@style/CalypsoTheme"
+ android:windowSoftInputMode="stateHidden" />
+
+ <!--People Management-->
+ <activity
+ android:name=".ui.people.PeopleManagementActivity"
+ android:label="@string/people"
+ android:theme="@style/Calypso.NoActionBar"/>
+
+ <!-- Me activities -->
+ <activity
+ android:name=".ui.prefs.MyProfileActivity"
+ android:label="@string/my_profile"
+ android:theme="@style/CalypsoTheme" />
+
+ <activity android:name=".ui.VisualEditorOptionsReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data
+ android:host="editor"
+ android:scheme="wordpress" />
+ </intent-filter>
+ </activity>
+
+ <!-- Lib activities-->
+ <activity
+ android:name="com.yalantis.ucrop.UCropActivity"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
+
+ <!-- Services -->
+ <service
+ android:name=".ui.posts.services.PostUploadService"
+ android:label="Post Upload Service" />
+ <service
+ android:name=".ui.posts.services.PostMediaService"
+ android:exported="false"
+ android:label="Post Media Service" />
+ <service
+ android:name=".ui.posts.services.PostUpdateService"
+ android:exported="false"
+ android:label="Post Update Service" />
+ <service
+ android:name=".ui.media.services.MediaUploadService"
+ android:label="Media Upload Service" />
+ <service
+ android:name=".ui.media.services.MediaDeleteService"
+ android:label="Media Delete Service" />
+ <service
+ android:name=".ui.stats.service.StatsService"
+ android:exported="false"
+ android:label="Stats Update Service" />
+ <service
+ android:name=".ui.reader.services.ReaderUpdateService"
+ android:exported="false"
+ android:label="Reader Update Service" />
+ <service
+ android:name=".ui.reader.services.ReaderPostService"
+ android:exported="false"
+ android:label="Reader Post Service" />
+ <service
+ android:name=".ui.reader.services.ReaderSearchService"
+ android:exported="false"
+ android:label="Reader Search Service" />
+ <service
+ android:name=".ui.reader.services.ReaderCommentService"
+ android:exported="false"
+ android:label="Reader Comment Service" />
+ <service
+ android:name=".ui.suggestion.service.SuggestionService"
+ android:exported="false"
+ android:label="Suggestion Service" />
+ <service
+ android:name=".ui.plans.PlanUpdateService"
+ android:exported="false"
+ android:label="Plans Update Service" />
+
+ <uses-library
+ android:name="com.sec.android.app.multiwindow"
+ android:required="false" />
+
+ <meta-data
+ android:name="com.sec.android.support.multiwindow"
+ android:value="true" />
+
+ <receiver android:name=".ui.notifications.NotificationDismissBroadcastReceiver" />
+ <receiver android:name=".ui.notifications.ShareAndDismissNotificationReceiver" />
+ <receiver
+ android:name=".networking.ConnectionChangeReceiver"
+ android:enabled="false">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ </intent-filter>
+ </receiver>
+ <receiver
+ android:name=".ui.stats.StatsWidgetProvider"
+ android:label="@string/stats_widget_name">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/stats_widget_info" />
+ </receiver>
+
+ <!-- Google Cloud Messaging receiver and services -->
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+
+ <receiver
+ android:name="com.google.android.gms.gcm.GcmReceiver"
+ android:exported="true"
+ android:permission="com.google.android.c2dm.permission.SEND">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <category android:name="${applicationId}" />
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name=".GCMRegistrationIntentService"
+ android:exported="false" />
+
+ <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
+
+ <service
+ android:name=".GCMMessageService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ </intent-filter>
+ </service>
+ <service
+ android:name=".InstanceIDService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.android.gms.iid.InstanceID" />
+ </intent-filter>
+ </service>
+
+ <activity
+ android:theme="@style/Calypso.NoActionBar"
+ android:name=".ui.accounts.login.MagicLinkSignInActivity"
+ android:label="@string/sign_in"
+ android:windowSoftInputMode="stateAlwaysHidden">
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="magic-login"
+ android:scheme="wordpress" >
+ </data>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".ui.HelpshiftDeepLinkReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="helpshift"
+ android:scheme="wordpress" />
+ </intent-filter>
+ </activity>
+
+ <!-- Smart Lock for Passwords -->
+ <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
+
+ <!-- Provider for exposing file URIs on Android 7+ -->
+ <provider
+ android:name="android.support.v4.content.FileProvider"
+ android:authorities="${applicationId}.provider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/provider_paths"/>
+ </provider>
+ </application>
+</manifest>
diff --git a/WordPress/src/main/assets/android_models.properties b/WordPress/src/main/assets/android_models.properties
new file mode 100644
index 000000000..e0fe425c1
--- /dev/null
+++ b/WordPress/src/main/assets/android_models.properties
@@ -0,0 +1,169 @@
+ADR6300 = HTC Droid Incredible
+ADR6330VW = HTC Rhyme
+ADR6350 = HTC Droid Incredible 2
+ADR6400L = HTC Thunderbolt
+ADR6410LVW = HTC Droid Incredible 4G
+ADR6425LVW = HTC Rezound 4G
+ASUS_Transformer_Pad_TF300T= Asus Transformer Pad
+Desire_HD = HTC Desire HD
+D800 = LG G2
+D801 = LG G2
+D802 = LG G2
+D802TA = LG G2
+D803 = LG G2
+D800_4G = LG G2
+D801_4G = LG G2
+D802_4G = LG G2
+D802TA_4G = LG G2
+D803_4G = LG G2
+DROID2_GLOBAL = Motorola Droid 2 Global
+DROID2 = Motorola Droid 2
+DROID3 = Motorola Droid 3
+DROID4 = Motorola Droid 4
+DROID_BIONIC = Motorola Droid Bionic
+Droid = Motorola Droid
+DROID_Pro = Motorola Droid Pro
+DROID_RAZR = Motorola Droid Razr
+DROID_X2 = Motorola Droid X2
+DROIDX = Motorola Droid X
+EVO = HTC Evo
+Galaxy_Nexus = Samsung Galaxy Nexus
+google_sdk = Android Emulator
+GT-I9000 = Samsung Galaxy S
+GT-I9100M = Samsung Galaxy S II
+GT-I9100 = Samsung Galaxy S II
+GT-I9100T = Samsung Galaxy S II
+GT-I9300 = Samsung Galaxy S III
+GT-I9500 = Samsung Galaxy S4
+GT-I9502 = Samsung Galaxy S4
+GT-I9505 = Samsung Galaxy S4
+GT-I9506 = Samsung Galaxy S4
+GT-I9508 = Samsung Galaxy S4
+GT-I9500G = Samsung Galaxy S4
+GT-I9505G = Samsung Galaxy S4
+GT-I9506G = Samsung Galaxy S4
+GT-I9508G = Samsung Galaxy S4
+GT-N7000 = Samsung Galaxy Note
+GT-N7100 = Samsung Galaxy Note II
+GT-N7102 = Samsung Galaxy Note II
+GT-N7105 = Samsung Galaxy Note II
+GT-N7108 = Samsung Galaxy Note II
+GT-N9005 = Samsung Galaxy Note III
+N9005 = Samsung Galaxy Note III
+GT-N9006 = Samsung Galaxy Note III
+N9006 = Samsung Galaxy Note III
+GT-P3113 = Samsung Galaxy Tab 2 7.0
+GT-P7510 = Samsung Galaxy Tab 10.1
+GT-S5830 = Samsung Galaxy Ace
+HTC_Amaze_4G =
+HTC_Desire =
+HTC_Desire_HD_A9191 = HTC Desire HD
+HTC_Desire_S =
+HTCEVOV4G = HTC Evo V 4G
+HTC_Glacier =
+HTC_Incredible_S =
+HTC_One_S =
+HTC_One_X =
+HTC_PH39100 = HTC Vivid 4G
+HTC_Sensation_4G =
+HTC_Sensation_Z710e = HTC Sensation
+HTC_VLE_U = HTC One S
+KFJWA = Kindle Fire HD 8.9
+KFJWI = Kindle Fire HD 8.9
+KFOT = Kindle Fire
+KFTT = Kindle Fire HD 7
+Kindle_Fire =
+LG-C800 = LG myTouch Q
+LG-E739 = LG MyTouch e739
+LGL55C = LG LGL55C
+LG-MS910 = LG Esteem
+LG-P509 = LG Optimus T
+LG-P880 = LG Optimus 4X HD
+LG-P999 = LG G2X P999
+LG-VM696 = LG Optimus Elite
+LS670 = LG Optimus S
+LS980 = LG G2
+LS980_4G = LG G2
+MB855 = Motorola Photon 4G
+MB860 = Motorola Atrix 4G
+MB865 = Motorola Atrix 2
+Motorola_Electrify =
+MOTWX435KT = Motorola Triumph
+myTouch_4G_Slide = HTC myTouch 4G Slide
+N860 = ZTE Warp N860
+Nexus_7 = Asus Nexus 7
+Nexus_S_4G = Samsung Nexus S 4G
+Nexus_S = Samsung Nexus S
+PantechP9070 = Pantech Burst
+PC36100 = HTC Evo 4G
+PG06100 = HTC EVO Shift 4G
+PG86100 = HTC Evo 3D
+PH44100 = HTC Evo Design 4G
+SAMSUNG-SGH-I717 = Samsung Galaxy Note
+SAMSUNG-SGH-I727 = Samsung Skyrocket
+SAMSUNG-SGH-I747 = Samsung Galaxy S III
+SAMSUNG-SGH-I777 = Samsung Galaxy S II
+SAMSUNG-SGH-I897 = Samsung Captivate
+SAMSUNG-SGH-I927 = Samsung Captivate Glide
+SAMSUNG-SGH-I997 = Samsung Infuse 4G
+SCH-I405 = Samsung Stratosphere
+SCH-I500 = Samsung Fascinate
+SCH-I510 = Samsung Droid Charge
+SCH-I535 = Samsung Galaxy S III
+SCH-I545 = Samsung Galaxy S4
+SCH-I605 = Samsung Galaxy Note II
+SCH-I800 = Samsung Galaxy Tab 7.0
+SCH-I959 = Samsung Galaxy S4
+SCH-N719 = Samsung Galaxy Note II
+SCH-R720 = Samsung Admire
+SCH-R950 = Samsung Galaxy Note II
+SCH-R970 = Samsung Galaxy S4
+SGH-I317 = Samsung Galaxy Note II
+SAMSUNG-SGH-I317 = Samsung Galaxy Note II
+SGH-I317M = Samsung Galaxy Note II
+SGH-I337 = Samsung Galaxy S4
+SGH-I727R = Samsung Galaxy S II
+SGH-I747M = Samsung Galaxy S III
+SGH-M919 = Samsung Galaxy S4
+SGH-N025 = Samsung Galaxy Note II
+SGH-N045 = Samsung Galaxy S4
+SGH-T679 = Samsung Exhibit II
+SGH-T769 = Samsung Galaxy S Blaze
+SGH-T959 = Samsung Galaxy S Vibrant
+SGH-T959V = Samsung Galaxy S 4G
+SGH-T989 = Samsung Galaxy S II
+SGH-T989D = Samsung Galaxy S II
+SGH-T999 = Samsung Galaxy S III
+SGH-T889 = Samsung Galaxy Note II
+SGH-T889v = Samsung Galaxy Note II
+SHV-E250K = Samsung Galaxy Note II
+SHV-E250L = Samsung Galaxy Note II
+SHV-E250S = Samsung Galaxy Note II
+SM-N900A = Samsung Galaxy Note III
+SM-N900P = Samsung Galaxy Note III
+SM-N900R4 = Samsung Galaxy Note III
+SM-N900T = Samsung Galaxy Note III
+SM-N900V = Samsung Galaxy Note III
+SM-T210 = Samsung Galaxy Tab 3 7.0
+SM-T210R = Samsung Galaxy Tab 3 7.0
+SPH-D600 = Samsung Conquer 4G
+SPH-D700 = Samsung Epic 4G
+SPH-D710 = Samsung Epic
+SPH-L710 = Samsung Galaxy S III
+SPH-L720 = Samsung Galaxy S4
+SPH-L900 = Samsung Galaxy Note II
+SPH-M930BST = Samsung Transform Ultra
+SPH-R970 = Samsung Galaxy S4
+T-Mobile_G2 =
+Transformer_Prime_TF201= Asus Eee Pad Transformer Prime
+Transformer_TF101 = Asus Eee Pad Transformer
+VM670 = LG Optimus V
+VS840_4G = LG Lucid 4G
+VS910_4G = LG Revolution 4G
+VS920_4G = LG Spectrum 4G
+VS980 = LG G2
+VS980_4G = LG G2
+Xoom = Motorola Xoom
+XT897 = Motorola Photon Q 4G LTE
+XT907 = Motorola DROID RAZR M
+XT1058 = Motorola Moto X \ No newline at end of file
diff --git a/WordPress/src/main/assets/fonts/Merriweather-Bold.ttf b/WordPress/src/main/assets/fonts/Merriweather-Bold.ttf
new file mode 100644
index 000000000..2340777c8
--- /dev/null
+++ b/WordPress/src/main/assets/fonts/Merriweather-Bold.ttf
Binary files differ
diff --git a/WordPress/src/main/assets/fonts/Merriweather-BoldItalic.ttf b/WordPress/src/main/assets/fonts/Merriweather-BoldItalic.ttf
new file mode 100644
index 000000000..f67d7e588
--- /dev/null
+++ b/WordPress/src/main/assets/fonts/Merriweather-BoldItalic.ttf
Binary files differ
diff --git a/WordPress/src/main/assets/fonts/Merriweather-Italic.ttf b/WordPress/src/main/assets/fonts/Merriweather-Italic.ttf
new file mode 100644
index 000000000..583358652
--- /dev/null
+++ b/WordPress/src/main/assets/fonts/Merriweather-Italic.ttf
Binary files differ
diff --git a/WordPress/src/main/assets/fonts/Merriweather-Regular.ttf b/WordPress/src/main/assets/fonts/Merriweather-Regular.ttf
new file mode 100644
index 000000000..28a80e487
--- /dev/null
+++ b/WordPress/src/main/assets/fonts/Merriweather-Regular.ttf
Binary files differ
diff --git a/WordPress/src/main/assets/fonts/Noticons.ttf b/WordPress/src/main/assets/fonts/Noticons.ttf
new file mode 100644
index 000000000..7cbcabe84
--- /dev/null
+++ b/WordPress/src/main/assets/fonts/Noticons.ttf
Binary files differ
diff --git a/WordPress/src/main/assets/licenses.html b/WordPress/src/main/assets/licenses.html
new file mode 100644
index 000000000..07a083b55
--- /dev/null
+++ b/WordPress/src/main/assets/licenses.html
@@ -0,0 +1,413 @@
+<html>
+ <head>
+ <style>
+ body {
+ margin: 1em;
+ }
+ pre {
+ background: #DDD;
+ padding: 1em;
+ white-space: pre-wrap;
+ }
+ </style>
+ </head>
+ <body>
+ <h2>WordPress for Android</h2>
+
+ <p>Source code available from
+ <a href="http://android.wordpress.org/">android.wordpress.org</a>.</p>
+
+ <p>WordPress for Android is released under the terms of the GNU General
+ Public License:</p>
+
+ <pre>Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+Preamble
+
+The licenses for most software are designed to take away your freedom to
+share and change it. By contrast, the GNU General Public License is
+intended to guarantee your freedom to share and change free software--to
+make sure the software is free for all its users. This General Public
+License applies to most of the Free Software Foundation's software and
+to any other program whose authors commit to using it. (Some other Free
+Software Foundation software is covered by the GNU Lesser General Public
+License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price.
+Our General Public Licenses are designed to make sure that you have the
+freedom to distribute copies of free software (and charge for this
+service if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone
+to deny you these rights or to ask you to surrender the rights. These
+restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis
+or for a fee, you must give the recipients all the rights that you have.
+You must make sure that they, too, receive or can get the source code.
+And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+Finally, any free program is threatened constantly by software patents.
+We wish to avoid the danger that redistributors of a free program will
+individually obtain patent licenses, in effect making the program
+proprietary. To prevent this, we have made it clear that any patent must
+be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a
+notice placed by the copyright holder saying it may be distributed under
+the terms of this General Public License. The "Program", below, refers
+to any such program or work, and a "work based on the Program" means
+either the Program or any derivative work under copyright law: that is
+to say, a work containing the Program or a portion of it, either
+verbatim or with modifications and/or translated into another language.
+(Hereinafter, translation is included without limitation in the term
+"modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of running
+the Program is not restricted, and the output from the Program is
+covered only if its contents constitute a work based on the Program
+(independent of having been made by running the Program). Whether that
+is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously
+and appropriately publish on each copy an appropriate copyright notice
+and disclaimer of warranty; keep intact all the notices that refer to
+this License and to the absence of any warranty; and give any other
+recipients of the Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of
+it, thus forming a work based on the Program, and copy and distribute
+such modifications or work under the terms of Section 1 above, provided
+that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any part
+ thereof, to be licensed as a whole at no charge to all third parties
+ under the terms of this License.
+
+ c) If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use
+ in the most ordinary way, to print or display an announcement
+ including an appropriate copyright notice and a notice that there is
+ no warranty (or else, saying that you provide a warranty) and that
+ users may redistribute the program under these conditions, and telling
+ the user how to view a copy of this License. (Exception: if the
+ Program itself is interactive but does not normally print such an
+ announcement, your work based on the Program is not required to print
+ an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program, and
+can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based on
+the Program, the distribution of the whole must be on the terms of this
+License, whose permissions for other licensees extend to the entire
+whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of a
+storage or distribution medium does not bring the other work under the
+scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections
+1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections 1
+ and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three years,
+ to give any third party, for a charge no more than your cost of
+ physically performing source distribution, a complete machine-readable
+ copy of the corresponding source code, to be distributed under the
+ terms of Sections 1 and 2 above on a medium customarily used for
+ software interchange; or,
+
+ c) Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed
+ only for noncommercial distribution and only if you received the
+ program in object code or executable form with such an offer, in
+ accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source code
+means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to control
+compilation and installation of the executable. However, as a special
+exception, the source code distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies the
+executable.
+
+If distribution of executable or object code is made by offering access
+to copy from a designated place, then offering equivalent access to copy
+the source code from the same place counts as distribution of the source
+code, even though third parties are not compelled to copy the source
+along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt otherwise
+to copy, modify, sublicense or distribute the Program is void, and will
+automatically terminate your rights under this License. However, parties
+who have received copies, or rights, from you under this License will
+not have their licenses terminated so long as such parties remain in
+full compliance.
+
+5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and all
+its terms and conditions for copying, distributing or modifying the
+Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further restrictions
+on the recipients' exercise of the rights granted herein. You are not
+responsible for enforcing compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot distribute
+so as to satisfy simultaneously your obligations under this License and
+any other pertinent obligations, then as a consequence you may not
+distribute the Program at all. For example, if a patent license would
+not permit royalty-free redistribution of the Program by all those who
+receive copies directly or indirectly through you, then the only way you
+could satisfy both it and this License would be to refrain entirely from
+distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is implemented
+by public license practices. Many people have made generous
+contributions to the wide range of software distributed through that
+system in reliance on consistent application of that system; it is up to
+the author/donor to decide if he or she is willing to distribute
+software through any other system and a licensee cannot impose that
+choice.
+
+This section is intended to make thoroughly clear what is believed to be
+a consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License may
+add an explicit geographical distribution limitation excluding those
+countries, so that distribution is permitted only in or among countries
+not thus excluded. In such case, this License incorporates the
+limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Program does not specify a version
+number of this License, you may choose any version ever published by the
+Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the
+author to ask for permission. For software which is copyrighted by the
+Free Software Foundation, write to the Free Software Foundation; we
+sometimes make exceptions for this. Our decision will be guided by the
+two goals of preserving the free status of all derivatives of our free
+software and of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH
+YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
+DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
+DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
+THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR
+OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</pre>
+
+ <h2>Additional Libraries</h2>
+
+ <p>WordPress for Android includes additional libraries that are released
+ under the terms of the following licenses.</p>
+
+ <h3>Notices for library:</h3>
+
+ <ul>
+ <li>GraphView</li>
+ </ul>
+
+ <pre>Copyright (C) 2013 Jonas Gehring
+
+Licensed under the GNU Lesser General Public License (LGPL)
+http://www.gnu.org/licenses/lgpl.html</pre>
+
+ <h3>Notices for library:</h3>
+
+ <ul>
+ <li>DragSortListView</li>
+ </ul>
+
+ <pre>Copyright 2012 Carl Bauer
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.</pre>
+
+ <h3>Notices for library:</h3>
+
+ <ul>
+ <li>photoview</li>
+ </ul>
+
+ <pre>Copyright 2011, 2012 Chris Banes
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.</pre>
+\
+ <h3>Notices for library:</h3>
+
+ <ul>
+ <li>gson</li>
+ </ul>
+
+ <pre>Copyright 2002-2008 Google
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.</pre>
+
+ <h3>Notices for library:</h3>
+
+ <ul>
+ <li>httpmime</li>
+ </ul>
+
+ <pre>Copyright 1999-2013 The Apache Software Foundation
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.</pre>
+
+ <h3>Notices for library:</h3>
+
+ <ul>
+ <li>tagsoup</li>
+ </ul>
+
+ <pre>Copyright 2002-2008 John Cowan
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.</pre>
+
+ </body>
+</html>
diff --git a/WordPress/src/main/assets/merriweather.css b/WordPress/src/main/assets/merriweather.css
new file mode 100644
index 000000000..984ebae16
--- /dev/null
+++ b/WordPress/src/main/assets/merriweather.css
@@ -0,0 +1,36 @@
+/*
+ * CSS to enable WebViews to use Merriweather font assets, ex:
+ * <link href='file:///android_asset/merriweather.css' rel='stylesheet' type='text/css'>
+ */
+
+/* normal */
+@font-face {
+ font-family: 'Merriweather';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Merriweather Regular'), local('Merriweather-Regular'), url(file:///android_asset/fonts/Merriweather-Regular.ttf);
+}
+
+/* bold */
+@font-face {
+ font-family: 'Merriweather';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Merriweather Bold'), local('Merriweather-Bold'), url(file:///android_asset/fonts/Merriweather-Bold.ttf);
+}
+
+/* italic */
+@font-face {
+ font-family: 'Merriweather';
+ font-style: italic;
+ font-weight: 400;
+ src: local('Merriweather Italic'), local('Merriweather-Italic'), url(file:///android_asset/fonts/Merriweather-Italic.ttf);
+}
+
+/* bold italic */
+@font-face {
+ font-family: 'Merriweather';
+ font-style: italic;
+ font-weight: 700;
+ src: local('Merriweather Bold Italic'), local('Merriweather-BoldItalic'), url(file:///android_asset/fonts/Merriweather-BoldItalic.ttf);
+}
diff --git a/WordPress/src/main/assets/webview.css b/WordPress/src/main/assets/webview.css
new file mode 100644
index 000000000..ee7b02be4
--- /dev/null
+++ b/WordPress/src/main/assets/webview.css
@@ -0,0 +1,115 @@
+/* The Reset ---------------- */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {line-height: 1;}
+/* -------------------------- */
+
+body {
+ font-family: sans-serif;
+ color: #2e4453;
+ font-size: 14pt;
+ width: 100%;
+ background: #fff;
+}
+
+strong {
+ font-weight: bold;
+}
+
+embed,object,img{display: block; max-width:100%; height: auto;}
+.frame {width: 100%}
+#templates{display:none}
+.subs-loading {width: 90%; margin: 2em auto; font-size: 1.5em; }
+#subscriptions {
+ position: relative;
+}
+
+#container {
+ padding: 16px;
+}
+
+p {
+ margin: 1.12em 0;
+}
+
+h1 {
+ font-size: 2em;
+ line-height: 1.2em;
+ font-weight: bold;
+ color: #222;
+ margin: .1em 0 .2em;
+}
+
+ol,ul {
+ padding: 0 0 0 30px;
+}
+
+li {
+ margin: 0 0 .5em;
+}
+
+b, strong {
+ font-weight: bold;
+}
+
+em, i {
+ font-style: italic;
+}
+
+code {
+ font-family: Monaco, monospace;
+ background: #eee;
+ padding: 1px 2px;
+}
+
+a {
+ color: #21759B;
+}
+
+a:active {
+ color: #d54e21;
+}
+
+blockquote {
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size: 1.1em;
+ line-height: 1.2em;
+ padding: 0 0 0 20px;
+}
+
+ul, ol, blockquote, p {
+ margin: 0 0 .8em;
+}
+
+/* Images */
+
+img {
+ -webkit-box-sizing:border-box;
+ box-sizing:border-box;
+ background:#fff;
+ padding:5px;
+ border:1px solid #fff;
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/Constants.java b/WordPress/src/main/java/org/wordpress/android/Constants.java
new file mode 100644
index 000000000..59e20ecfe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/Constants.java
@@ -0,0 +1,23 @@
+
+package org.wordpress.android;
+
+public class Constants {
+ public static String readerURL = "https://en.wordpress.com/reader/mobile/v2";
+ public static String readerLoginURL = "https://wordpress.com/wp-login.php";
+
+ public static String readerURL_v3 = "https://en.wordpress.com/reader/mobile/v2/?chrome=no";
+ public static String authorizedHybridHost = "en.wordpress.com";
+ public static String readerTopicsURL = "http://en.wordpress.com/reader/mobile/v2/?template=topics";
+ public static String readerDetailURL = "https://en.wordpress.com/wp-admin/admin-ajax.php?action=wpcom_load_mobile&template=details&v=2";
+
+ public static String wpcomXMLRPCURL = "https://wordpress.com/xmlrpc.php";
+ public static String wpcomLoginURL = "https://wordpress.com/wp-login.php";
+
+ public static final String URL_TOS = "http://en.wordpress.com/tos";
+ public static String videoPressURL = "http://videopress.com";
+
+ public static int QUICK_POST_PHOTO_CAMERA = 0;
+ public static int QUICK_POST_PHOTO_LIBRARY = 1;
+
+ public static final int INTENT_COMMENT_EDITOR = 1010;
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/GCMMessageService.java b/WordPress/src/main/java/org/wordpress/android/GCMMessageService.java
new file mode 100644
index 000000000..de0e92aa9
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/GCMMessageService.java
@@ -0,0 +1,737 @@
+
+package org.wordpress.android;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.util.ArrayMap;
+import android.text.TextUtils;
+
+import com.google.android.gms.gcm.GcmListenerService;
+import com.simperium.client.BucketObjectMissingException;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.analytics.AnalyticsTrackerMixpanel;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.notifications.NotificationDismissBroadcastReceiver;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+import org.wordpress.android.ui.notifications.NotificationsListFragment;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import de.greenrobot.event.EventBus;
+
+public class GCMMessageService extends GcmListenerService {
+ private static final ArrayMap<Integer, Bundle> sActiveNotificationsMap = new ArrayMap<>();
+ private static String sPreviousNoteId = null;
+ private static long sPreviousNoteTime = 0L;
+
+ private static final String NOTIFICATION_GROUP_KEY = "notification_group_key";
+ private static final int PUSH_NOTIFICATION_ID = 10000;
+ private static final int AUTH_PUSH_NOTIFICATION_ID = 20000;
+ public static final int GROUP_NOTIFICATION_ID = 30000;
+ private static final int MAX_INBOX_ITEMS = 5;
+
+ private static final String PUSH_ARG_USER = "user";
+ private static final String PUSH_ARG_TYPE = "type";
+ private static final String PUSH_ARG_TITLE = "title";
+ private static final String PUSH_ARG_MSG = "msg";
+ private static final String PUSH_ARG_NOTE_ID = "note_id";
+
+ private static final String PUSH_TYPE_COMMENT = "c";
+ private static final String PUSH_TYPE_LIKE = "like";
+ private static final String PUSH_TYPE_COMMENT_LIKE = "comment_like";
+ private static final String PUSH_TYPE_AUTOMATTCHER = "automattcher";
+ private static final String PUSH_TYPE_FOLLOW = "follow";
+ private static final String PUSH_TYPE_REBLOG = "reblog";
+ private static final String PUSH_TYPE_PUSH_AUTH = "push_auth";
+ private static final String PUSH_TYPE_BADGE_RESET = "badge-reset";
+
+ private static final String KEY_CATEGORY_COMMENT_LIKE = "comment-like";
+ private static final String KEY_CATEGORY_COMMENT_REPLY = "comment-reply";
+ private static final String KEY_CATEGORY_COMMENT_MODERATE = "comment-moderate";
+
+
+ // Add to the analytics properties map a subset of the push notification payload.
+ private static String[] propertiesToCopyIntoAnalytics = {PUSH_ARG_NOTE_ID, PUSH_ARG_TYPE, "blog_id", "post_id",
+ "comment_id"};
+
+ private void synchronizedHandleDefaultPush(String from, @NonNull Bundle data) {
+ // sActiveNotificationsMap being static, we can't just synchronize the method
+ synchronized (GCMMessageService.class) {
+ handleDefaultPush(from, data);
+ }
+ }
+
+ private void handleDefaultPush(String from, @NonNull Bundle data) {
+ // Ensure Simperium is running so that notes sync
+ SimperiumUtils.configureSimperium(this, AccountHelper.getDefaultAccount().getAccessToken());
+
+ long wpcomUserId = AccountHelper.getDefaultAccount().getUserId();
+ String pushUserId = data.getString(PUSH_ARG_USER);
+ // pushUserId is always set server side, but better to double check it here.
+ if (!String.valueOf(wpcomUserId).equals(pushUserId)) {
+ AppLog.e(T.NOTIFS, "wpcom userId found in the app doesn't match with the ID in the PN. Aborting.");
+ return;
+ }
+
+ String noteType = StringUtils.notNullStr(data.getString(PUSH_ARG_TYPE));
+
+ // Check for wpcom auth push, if so we will process this push differently
+ if (noteType.equals(PUSH_TYPE_PUSH_AUTH)) {
+ handlePushAuth(from, data);
+ return;
+ }
+
+ if (noteType.equals(PUSH_TYPE_BADGE_RESET)) {
+ handleBadgeResetPN(data);
+ return;
+ }
+
+ buildAndShowNotificationFromNoteData(data);
+
+ EventBus.getDefault().post(new NotificationEvents.NotificationsChanged());
+ }
+
+ private void buildAndShowNotificationFromNoteData(Bundle data) {
+
+ if (data == null)
+ return;
+
+ String noteType = StringUtils.notNullStr(data.getString(PUSH_ARG_TYPE));
+
+ String title = StringEscapeUtils.unescapeHtml(data.getString(PUSH_ARG_TITLE));
+ if (title == null) {
+ title = getString(R.string.app_name);
+ }
+ String message = StringEscapeUtils.unescapeHtml(data.getString(PUSH_ARG_MSG));
+ String noteId = data.getString(PUSH_ARG_NOTE_ID, "");
+
+ /*
+ * if this has the same note_id as the previous notification, and the previous notification
+ * was received within the last second, then skip showing it - this handles duplicate
+ * notifications being shown due to the device being registered multiple times with different tokens.
+ * (still investigating how this could happen - 21-Oct-13)
+ *
+ * this also handles the (rare) case where the user receives rapid-fire sub-second like notifications
+ * due to sudden popularity (post gets added to FP and is liked by many people all at once, etc.),
+ * which we also want to avoid since it would drain the battery and annoy the user
+ *
+ * NOTE: different comments on the same post will have a different note_id, but different likes
+ * on the same post will have the same note_id, so don't assume that the note_id is unique
+ */
+ long thisTime = System.currentTimeMillis();
+ if (sPreviousNoteId != null && sPreviousNoteId.equals(noteId)) {
+ long seconds = TimeUnit.MILLISECONDS.toSeconds(thisTime - sPreviousNoteTime);
+ if (seconds <= 1) {
+ AppLog.w(T.NOTIFS, "skipped potential duplicate notification");
+ return;
+ }
+ }
+
+ sPreviousNoteId = noteId;
+ sPreviousNoteTime = thisTime;
+
+ // Update notification content for the same noteId if it is already showing
+ int pushId = 0;
+ for (Integer id : sActiveNotificationsMap.keySet()) {
+ if (id == null) {
+ continue;
+ }
+ Bundle noteBundle = sActiveNotificationsMap.get(id);
+ if (noteBundle != null && noteBundle.getString(PUSH_ARG_NOTE_ID, "").equals(noteId)) {
+ pushId = id;
+ sActiveNotificationsMap.put(pushId, data);
+ break;
+ }
+ }
+
+ if (pushId == 0) {
+ pushId = PUSH_NOTIFICATION_ID + sActiveNotificationsMap.size();
+ sActiveNotificationsMap.put(pushId, data);
+ }
+
+ // Bump Analytics for PNs if "Show notifications" setting is checked (default). Skip otherwise.
+ if (NotificationsUtils.isNotificationsEnabled(this)) {
+ Map<String, Object> properties = new HashMap<>();
+ if (!TextUtils.isEmpty(noteType)) {
+ // 'comment' and 'comment_pingback' types are sent in PN as type = "c"
+ if (noteType.equals(PUSH_TYPE_COMMENT)) {
+ properties.put("notification_type", "comment");
+ } else {
+ properties.put("notification_type", noteType);
+ }
+ }
+
+ bumpPushNotificationsAnalytics(Stat.PUSH_NOTIFICATION_RECEIVED, data, properties);
+ AnalyticsTracker.flush();
+ }
+
+ showGroupNotificationForActiveNotificationsMap(pushId, noteId, noteType, data.getString("icon"), title, message);
+ }
+
+ private void showGroupNotificationForActiveNotificationsMap(int pushId, String noteId, String noteType,
+ String largeIconUri, String title, String message) {
+
+ // Build the new notification, add group to support wearable stacking
+ NotificationCompat.Builder builder = getNotificationBuilder(title, message);
+
+ Bitmap largeIconBitmap = getLargeIconBitmap(largeIconUri, shouldCircularizeNoteIcon(noteType));
+ if (largeIconBitmap != null) {
+ builder.setLargeIcon(largeIconBitmap);
+ }
+
+ showIndividualNotificationForBuilder(builder, noteType, noteId, pushId);
+
+ // Also add a group summary notification, which is required for non-wearable devices
+ showGroupNotificationForBuilder(builder, message);
+ }
+
+ private void addActionsForCommentNotification(NotificationCompat.Builder builder, String noteId) {
+ // Add some actions if this is a comment notification
+
+ boolean areActionsSet = false;
+
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(noteId);
+ if (note != null) {
+ //if note can be replied to, we'll always add this action first
+ if (note.canReply()) {
+ addCommentReplyActionForCommentNotification(builder, noteId);
+ }
+
+ // if the comment is lacking approval, offer moderation actions
+ if (note.getCommentStatus().equals(CommentStatus.UNAPPROVED)) {
+ if (note.canModerate()) {
+ addCommentApproveActionForCommentNotification(builder, noteId);
+ }
+ } else {
+ //else offer REPLY / LIKE actions
+ if (note.canLike()) {
+ addCommentLikeActionForCommentNotification(builder, noteId);
+ }
+ }
+ }
+ areActionsSet = true;
+ } catch (BucketObjectMissingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // if we could not set the actions, set the default ones REPLY / LIKE
+ if (!areActionsSet) {
+ addCommentReplyActionForCommentNotification(builder, noteId);
+ addCommentLikeActionForCommentNotification(builder, noteId);
+ }
+ }
+
+ private void addCommentReplyActionForCommentNotification(NotificationCompat.Builder builder, String noteId) {
+ // adding comment reply action
+ Intent commentReplyIntent = getCommentActionIntent();
+ commentReplyIntent.addCategory(KEY_CATEGORY_COMMENT_REPLY);
+ commentReplyIntent.putExtra(NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA, true);
+ if (noteId != null) {
+ commentReplyIntent.putExtra(NotificationsListFragment.NOTE_ID_EXTRA, noteId);
+ }
+ PendingIntent commentReplyPendingIntent = PendingIntent.getActivity(this, 0, commentReplyIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ builder.addAction(R.drawable.ic_reply_white_24dp, getText(R.string.reply),
+ commentReplyPendingIntent);
+ }
+
+ private void addCommentLikeActionForCommentNotification(NotificationCompat.Builder builder, String noteId) {
+ // adding comment like action
+ Intent commentLikeIntent = getCommentActionIntent();
+ commentLikeIntent.addCategory(KEY_CATEGORY_COMMENT_LIKE);
+ commentLikeIntent.putExtra(NotificationsListFragment.NOTE_INSTANT_LIKE_EXTRA, true);
+ if (noteId != null) {
+ commentLikeIntent.putExtra(NotificationsListFragment.NOTE_ID_EXTRA, noteId);
+ }
+ PendingIntent commentLikePendingIntent = PendingIntent.getActivity(this, 0, commentLikeIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ builder.addAction(R.drawable.ic_action_like, getText(R.string.like),
+ commentLikePendingIntent);
+ }
+
+ private void addCommentApproveActionForCommentNotification(NotificationCompat.Builder builder, String noteId) {
+ // adding comment approve action
+ Intent commentApproveIntent = getCommentActionIntent();
+ commentApproveIntent.addCategory(KEY_CATEGORY_COMMENT_MODERATE);
+ commentApproveIntent.putExtra(NotificationsListFragment.NOTE_INSTANT_APPROVE_EXTRA, true);
+ if (noteId != null) {
+ commentApproveIntent.putExtra(NotificationsListFragment.NOTE_ID_EXTRA, noteId);
+ }
+ PendingIntent commentApprovePendingIntent = PendingIntent.getActivity(this, 0, commentApproveIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ builder.addAction(R.drawable.ic_action_approve, getText(R.string.approve),
+ commentApprovePendingIntent);
+ }
+
+ private Intent getCommentActionIntent(){
+ Intent intent = new Intent(this, WPMainActivity.class);
+ intent.putExtra(WPMainActivity.ARG_OPENED_FROM_PUSH, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.setAction("android.intent.action.MAIN");
+ intent.addCategory("android.intent.category.LAUNCHER");
+ return intent;
+ }
+
+ private Bitmap getLargeIconBitmap(String iconUrl, boolean shouldCircularizeIcon){
+ Bitmap largeIconBitmap = null;
+ if (iconUrl != null) {
+ try {
+ iconUrl = URLDecoder.decode(iconUrl, "UTF-8");
+ int largeIconSize = getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_height);
+ String resizedUrl = PhotonUtils.getPhotonImageUrl(iconUrl, largeIconSize, largeIconSize);
+ largeIconBitmap = ImageUtils.downloadBitmap(resizedUrl);
+ if (largeIconBitmap != null && shouldCircularizeIcon) {
+ largeIconBitmap = ImageUtils.getCircularBitmap(largeIconBitmap);
+ }
+ } catch (UnsupportedEncodingException e) {
+ AppLog.e(T.NOTIFS, e);
+ }
+ }
+ return largeIconBitmap;
+ }
+
+ private NotificationCompat.Builder getNotificationBuilder(String title, String message){
+ // Build the new notification, add group to support wearable stacking
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.notification_icon)
+ .setColor(getResources().getColor(R.color.blue_wordpress))
+ .setContentTitle(title)
+ .setContentText(message)
+ .setTicker(message)
+ .setAutoCancel(true)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
+ .setGroup(NOTIFICATION_GROUP_KEY);
+
+ return builder;
+ }
+
+ private void showGroupNotificationForBuilder(NotificationCompat.Builder builder, String message) {
+
+ if (builder == null) {
+ return;
+ }
+
+ if (sActiveNotificationsMap.size() > 1) {
+ NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
+ int noteCtr = 1;
+ for (Bundle pushBundle : sActiveNotificationsMap.values()) {
+ // InboxStyle notification is limited to 5 lines
+ if (noteCtr > MAX_INBOX_ITEMS) {
+ break;
+ }
+ if (pushBundle == null || pushBundle.getString(PUSH_ARG_MSG) == null) {
+ continue;
+ }
+
+ if (pushBundle.getString(PUSH_ARG_TYPE, "").equals(PUSH_TYPE_COMMENT)) {
+ String pnTitle = StringEscapeUtils.unescapeHtml((pushBundle.getString(PUSH_ARG_TITLE)));
+ String pnMessage = StringEscapeUtils.unescapeHtml((pushBundle.getString(PUSH_ARG_MSG)));
+ inboxStyle.addLine(pnTitle + ": " + pnMessage);
+ } else {
+ String pnMessage = StringEscapeUtils.unescapeHtml((pushBundle.getString(PUSH_ARG_MSG)));
+ inboxStyle.addLine(pnMessage);
+ }
+
+ noteCtr++;
+ }
+
+ if (sActiveNotificationsMap.size() > MAX_INBOX_ITEMS) {
+ inboxStyle.setSummaryText(String.format(getString(R.string.more_notifications),
+ sActiveNotificationsMap.size() - MAX_INBOX_ITEMS));
+ }
+
+ String subject = String.format(getString(R.string.new_notifications), sActiveNotificationsMap.size());
+ NotificationCompat.Builder groupBuilder = new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.notification_icon)
+ .setColor(getResources().getColor(R.color.blue_wordpress))
+ .setGroup(NOTIFICATION_GROUP_KEY)
+ .setGroupSummary(true)
+ .setAutoCancel(true)
+ .setTicker(message)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(subject)
+ .setStyle(inboxStyle);
+
+ showNotificationForBuilder(groupBuilder, this, GROUP_NOTIFICATION_ID);
+
+ } else {
+ // Set the individual notification we've already built as the group summary
+ builder.setGroupSummary(true);
+ showNotificationForBuilder(builder, this, GROUP_NOTIFICATION_ID);
+ }
+ }
+
+ private void showIndividualNotificationForBuilder(NotificationCompat.Builder builder, String noteType, String noteId, int pushId) {
+ if (builder == null) {
+ return;
+ }
+
+ if (noteType.equals(PUSH_TYPE_COMMENT)) {
+ addActionsForCommentNotification(builder, noteId);
+ }
+ showNotificationForBuilder(builder, this, pushId);
+ }
+
+ // Displays a notification to the user
+ private void showNotificationForBuilder(NotificationCompat.Builder builder, Context context, int notificationId) {
+ if (builder == null || context == null) {
+ return;
+ }
+
+ Intent resultIntent = new Intent(this, WPMainActivity.class);
+ resultIntent.putExtra(WPMainActivity.ARG_OPENED_FROM_PUSH, true);
+ resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ resultIntent.setAction("android.intent.action.MAIN");
+ resultIntent.addCategory("android.intent.category.LAUNCHER");
+ if (sPreviousNoteId != null) {
+ resultIntent.putExtra(NotificationsListFragment.NOTE_ID_EXTRA, sPreviousNoteId);
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ boolean shouldPlaySound = prefs.getBoolean("wp_pref_notification_sound", false);
+ boolean shouldVibrate = prefs.getBoolean("wp_pref_notification_vibrate", false);
+ boolean shouldBlinkLight = prefs.getBoolean("wp_pref_notification_light", false);
+ String notificationSound = prefs.getString("wp_pref_custom_notification_sound", null); //"" if None is selected
+
+
+ // use default sound if the legacy sound preference was ON but the custom sound was not selected (null)
+ if (shouldPlaySound && notificationSound == null) {
+ builder.setSound(Uri.parse("content://settings/system/notification_sound"));
+ } else if (!TextUtils.isEmpty(notificationSound)) {
+ builder.setSound(Uri.parse(notificationSound));
+ }
+
+ if (shouldVibrate) {
+ builder.setVibrate(new long[]{500, 500, 500});
+ }
+ if (shouldBlinkLight) {
+ builder.setLights(0xff0000ff, 1000, 5000);
+ }
+
+ // Call broadcast receiver when notification is dismissed
+ Intent notificationDeletedIntent = new Intent(this, NotificationDismissBroadcastReceiver.class);
+ notificationDeletedIntent.putExtra("notificationId", notificationId);
+ notificationDeletedIntent.setAction(String.valueOf(notificationId));
+ PendingIntent pendingDeleteIntent =
+ PendingIntent.getBroadcast(context, notificationId, notificationDeletedIntent, 0);
+ builder.setDeleteIntent(pendingDeleteIntent);
+
+ builder.setCategory(NotificationCompat.CATEGORY_SOCIAL);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationId, resultIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_UPDATE_CURRENT);
+ builder.setContentIntent(pendingIntent);
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
+ notificationManager.notify(notificationId, builder.build());
+ }
+
+ private void rebuildAndUpdateNotificationsOnSystemBar(Bundle data) {
+ Bitmap largeIconBitmap = null;
+ // here notify the existing group notification by eliminating the line that is now gone
+ String title = getNotificationTitleOrAppNameFromBundle(data);
+ String message = StringEscapeUtils.unescapeHtml(data.getString(PUSH_ARG_MSG));
+
+ NotificationCompat.Builder builder = null;
+
+ if (sActiveNotificationsMap.size() == 1) {
+ //only one notification remains, so get the proper message for it and re-instate in the system dashboard
+ Bundle remainingNote = sActiveNotificationsMap.values().iterator().next();
+ if (remainingNote != null) {
+ String remainingNoteTitle = StringEscapeUtils.unescapeHtml(remainingNote.getString(PUSH_ARG_TITLE));
+ if (!TextUtils.isEmpty(remainingNoteTitle)) {
+ title = remainingNoteTitle;
+ }
+ String remainingNoteMessage = StringEscapeUtils.unescapeHtml(remainingNote.getString(PUSH_ARG_MSG));
+ if (!TextUtils.isEmpty(remainingNoteMessage)) {
+ message = remainingNoteMessage;
+ }
+ largeIconBitmap = getLargeIconBitmap(remainingNote.getString("icon"),
+ shouldCircularizeNoteIcon(remainingNote.getString(PUSH_ARG_TYPE)));
+
+ builder = getNotificationBuilder(title, message);
+
+ String noteType = StringUtils.notNullStr(remainingNote.getString(PUSH_ARG_TYPE));
+ String noteId = remainingNote.getString(PUSH_ARG_NOTE_ID, "");
+ if (!sActiveNotificationsMap.isEmpty()) {
+ showIndividualNotificationForBuilder(builder, noteType, noteId, sActiveNotificationsMap.keyAt(0));
+ }
+ }
+ }
+
+ if (builder == null) {
+ builder = getNotificationBuilder(title, message);
+ }
+
+ if (largeIconBitmap == null) {
+ largeIconBitmap = getLargeIconBitmap(data.getString("icon"), shouldCircularizeNoteIcon(PUSH_TYPE_BADGE_RESET));
+ }
+
+ if (largeIconBitmap != null) {
+ builder.setLargeIcon(largeIconBitmap);
+ }
+
+ showGroupNotificationForBuilder(builder, message);
+
+ }
+
+ private String getNotificationTitleOrAppNameFromBundle(Bundle data){
+ String title = StringEscapeUtils.unescapeHtml(data.getString(PUSH_ARG_TITLE));
+ if (title == null) {
+ title = getString(R.string.app_name);
+ }
+ return title;
+ }
+
+ // Clear all notifications
+ private void handleBadgeResetPN(Bundle data) {
+ if (data == null || !data.containsKey(PUSH_ARG_NOTE_ID)) {
+ // ignore the reset-badge PN if it's a global one
+ return;
+ }
+
+ removeNotificationWithNoteIdFromSystemBar(this, data.getString(PUSH_ARG_NOTE_ID, ""));
+ //now that we cleared the specific notif, we can check and make any visual updates
+ if (sActiveNotificationsMap.size() > 0) {
+ rebuildAndUpdateNotificationsOnSystemBar(data);
+ }
+
+ EventBus.getDefault().post(new NotificationEvents.NotificationsChanged());
+ }
+
+ // Show a notification for two-step auth users who sign in from a web browser
+ private void handlePushAuth(String from, Bundle data) {
+ if (data == null) {
+ return;
+ }
+
+ String pushAuthToken = data.getString("push_auth_token", "");
+ String title = data.getString("title", "");
+ String message = data.getString("msg", "");
+ long expirationTimestamp = Long.valueOf(data.getString("expires", "0"));
+
+ // No strings, no service
+ if (TextUtils.isEmpty(pushAuthToken) || TextUtils.isEmpty(title) || TextUtils.isEmpty(message)) {
+ return;
+ }
+
+ // Show authorization intent
+ Intent pushAuthIntent = new Intent(this, WPMainActivity.class);
+ pushAuthIntent.putExtra(WPMainActivity.ARG_OPENED_FROM_PUSH, true);
+ pushAuthIntent.putExtra(NotificationsUtils.ARG_PUSH_AUTH_TOKEN, pushAuthToken);
+ pushAuthIntent.putExtra(NotificationsUtils.ARG_PUSH_AUTH_TITLE, title);
+ pushAuthIntent.putExtra(NotificationsUtils.ARG_PUSH_AUTH_MESSAGE, message);
+ pushAuthIntent.putExtra(NotificationsUtils.ARG_PUSH_AUTH_EXPIRES, expirationTimestamp);
+ pushAuthIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ pushAuthIntent.setAction("android.intent.action.MAIN");
+ pushAuthIntent.addCategory("android.intent.category.LAUNCHER");
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.notification_icon)
+ .setColor(getResources().getColor(R.color.blue_wordpress))
+ .setContentTitle(title)
+ .setContentText(message)
+ .setAutoCancel(true)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
+ .setPriority(NotificationCompat.PRIORITY_MAX);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, pushAuthIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_UPDATE_CURRENT);
+ builder.setContentIntent(pendingIntent);
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
+ notificationManager.notify(AUTH_PUSH_NOTIFICATION_ID, builder.build());
+ }
+
+ @Override
+ public void onMessageReceived(String from, Bundle data) {
+ AppLog.v(T.NOTIFS, "Received Message");
+
+ if (data == null) {
+ AppLog.v(T.NOTIFS, "No notification message content received. Aborting.");
+ return;
+ }
+
+ // Handle helpshift PNs
+ if (TextUtils.equals(data.getString("origin"), "helpshift")) {
+ HelpshiftHelper.getInstance().handlePush(this, new Intent().putExtras(data));
+ return;
+ }
+
+ // Handle mixpanel PNs
+ if (data.containsKey("mp_message")) {
+ String mpMessage = data.getString("mp_message");
+ String title = getString(R.string.app_name);
+ Intent resultIntent = new Intent(this, WPMainActivity.class);
+ resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, resultIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ AnalyticsTrackerMixpanel.showNotification(this, pendingIntent,
+ R.drawable.notification_icon, title, mpMessage);
+ return;
+ }
+
+ if (!AccountHelper.isSignedInWordPressDotCom()) {
+ return;
+ }
+
+ synchronizedHandleDefaultPush(from, data);
+ }
+
+ // Returns true if the note type is known to have a gravatar
+ public boolean shouldCircularizeNoteIcon(String noteType) {
+ if (TextUtils.isEmpty(noteType)) {
+ return false;
+ }
+
+ switch (noteType) {
+ case PUSH_TYPE_COMMENT:
+ case PUSH_TYPE_LIKE:
+ case PUSH_TYPE_COMMENT_LIKE:
+ case PUSH_TYPE_AUTOMATTCHER:
+ case PUSH_TYPE_FOLLOW:
+ case PUSH_TYPE_REBLOG:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static synchronized void clearNotifications() {
+ sActiveNotificationsMap.clear();
+ }
+
+ public static synchronized int getNotificationsCount() {
+ return sActiveNotificationsMap.size();
+ }
+
+ public static synchronized boolean hasNotifications() {
+ return !sActiveNotificationsMap.isEmpty();
+ }
+
+ // Removes a specific notification from the internal map - only use this when we know
+ // the user has dismissed the app by swiping it off the screen
+ public static synchronized void removeNotification(int notificationId) {
+ sActiveNotificationsMap.remove(notificationId);
+ }
+
+ // Removes a specific notification from the system bar
+ public static synchronized void removeNotificationWithNoteIdFromSystemBar(Context context, String noteID) {
+ if (context == null || TextUtils.isEmpty(noteID) || !hasNotifications()) {
+ return;
+ }
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ // here we loop with an Iterator as there might be several Notifications with the same Note ID (i.e. likes on the same Note)
+ // so we need to keep cancelling them and removing them from our activeNotificationsMap as we find it suitable
+ for(Iterator<Map.Entry<Integer, Bundle>> it = sActiveNotificationsMap.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry<Integer, Bundle> row = it.next();
+ Integer pushId = row.getKey();
+ Bundle noteBundle = row.getValue();
+ if (noteBundle.getString(PUSH_ARG_NOTE_ID, "").equals(noteID)) {
+ notificationManager.cancel(pushId);
+ it.remove();
+ }
+ }
+
+ if (sActiveNotificationsMap.size() == 0) {
+ notificationManager.cancel(GCMMessageService.GROUP_NOTIFICATION_ID);
+ }
+ }
+
+ // Removes all app notifications from the system bar
+ public static synchronized void removeAllNotifications(Context context) {
+ if (context == null || !hasNotifications()) {
+ return;
+ }
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ for (Integer pushId : sActiveNotificationsMap.keySet()) {
+ notificationManager.cancel(pushId);
+ }
+ notificationManager.cancel(GCMMessageService.GROUP_NOTIFICATION_ID);
+
+ clearNotifications();
+ }
+
+ // NoteID is the ID if the note in WordPress
+ public static synchronized void bumpPushNotificationsTappedAnalytics(String noteID) {
+ for (int id : sActiveNotificationsMap.keySet()) {
+ Bundle noteBundle = sActiveNotificationsMap.get(id);
+ if (noteBundle.getString(PUSH_ARG_NOTE_ID, "").equals(noteID)) {
+ bumpPushNotificationsAnalytics(Stat.PUSH_NOTIFICATION_TAPPED, noteBundle, null);
+ AnalyticsTracker.flush();
+ return;
+ }
+ }
+ }
+
+ // Mark all notifications as tapped
+ public static synchronized void bumpPushNotificationsTappedAllAnalytics() {
+ for (int id : sActiveNotificationsMap.keySet()) {
+ Bundle noteBundle = sActiveNotificationsMap.get(id);
+ bumpPushNotificationsAnalytics(Stat.PUSH_NOTIFICATION_TAPPED, noteBundle, null);
+ }
+ AnalyticsTracker.flush();
+ }
+
+ private static void bumpPushNotificationsAnalytics(Stat stat, Bundle noteBundle,
+ Map<String, Object> properties) {
+ // Bump Analytics for PNs if "Show notifications" setting is checked (default). Skip otherwise.
+ if (!NotificationsUtils.isNotificationsEnabled(WordPress.getContext())) {
+ return;
+ }
+ if (properties == null) {
+ properties = new HashMap<>();
+ }
+
+ String notificationID = noteBundle.getString(PUSH_ARG_NOTE_ID, "");
+ if (!TextUtils.isEmpty(notificationID)) {
+ for (String currentPropertyToCopy : propertiesToCopyIntoAnalytics) {
+ if (noteBundle.containsKey(currentPropertyToCopy)) {
+ properties.put("push_notification_" + currentPropertyToCopy, noteBundle.get(currentPropertyToCopy));
+ }
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
+ String lastRegisteredGCMToken = preferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_TOKEN, null);
+ properties.put("push_notification_token", lastRegisteredGCMToken);
+ AnalyticsTracker.track(stat, properties);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/GCMRegistrationIntentService.java b/WordPress/src/main/java/org/wordpress/android/GCMRegistrationIntentService.java
new file mode 100644
index 000000000..67b4901ea
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/GCMRegistrationIntentService.java
@@ -0,0 +1,69 @@
+package org.wordpress.android;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.HelpshiftHelper;
+
+import java.util.UUID;
+
+public class GCMRegistrationIntentService extends IntentService {
+
+ public GCMRegistrationIntentService() {
+ super("GCMRegistrationIntentService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ try {
+ InstanceID instanceID = InstanceID.getInstance(this);
+ String gcmId = BuildConfig.GCM_ID;
+ if (TextUtils.isEmpty(gcmId)) {
+ AppLog.e(T.NOTIFS, "GCM_ID must be configured in gradle.properties");
+ return;
+ }
+ String token = instanceID.getToken(gcmId, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
+ sendRegistrationToken(token);
+ } catch (Exception e) {
+ // SecurityException can happen on some devices without Google services (these devices probably strip
+ // the AndroidManifest.xml and remove unsupported permissions).
+ AppLog.e(T.NOTIFS, "Google Play Services unavailable: ", e);
+ }
+ }
+
+ public void sendRegistrationToken(String gcmToken) {
+ if (!TextUtils.isEmpty(gcmToken)) {
+ AppLog.i(T.NOTIFS, "Sending GCM token to our remote services: " + gcmToken);
+ // Register to WordPress.com notifications
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // Get or create UUID for WP.com notes api
+ String uuid = preferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_UUID, null);
+ if (uuid == null) {
+ uuid = UUID.randomUUID().toString();
+ preferences.edit().putString(NotificationsUtils.WPCOM_PUSH_DEVICE_UUID, uuid).apply();
+ }
+ preferences.edit().putString(NotificationsUtils.WPCOM_PUSH_DEVICE_TOKEN, gcmToken).apply();
+ NotificationsUtils.registerDeviceForPushNotifications(this, gcmToken);
+ }
+
+ // Register to other kind of notifications
+ HelpshiftHelper.getInstance().registerDeviceToken(this, gcmToken);
+ AnalyticsTracker.registerPushNotificationToken(gcmToken);
+ } else {
+ AppLog.w(T.NOTIFS, "Empty GCM token, can't register the id on remote services");
+ PreferenceManager.getDefaultSharedPreferences(this).edit().remove(NotificationsUtils.WPCOM_PUSH_DEVICE_TOKEN).apply();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/InstanceIDService.java b/WordPress/src/main/java/org/wordpress/android/InstanceIDService.java
new file mode 100644
index 000000000..807254796
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/InstanceIDService.java
@@ -0,0 +1,13 @@
+package org.wordpress.android;
+
+import android.content.Intent;
+
+import com.google.android.gms.iid.InstanceIDListenerService;
+
+public class InstanceIDService extends InstanceIDListenerService {
+ @Override
+ public void onTokenRefresh() {
+ // Register for Cloud messaging
+ startService(new Intent(this, GCMRegistrationIntentService.class));
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/JavaScriptException.java b/WordPress/src/main/java/org/wordpress/android/JavaScriptException.java
new file mode 100644
index 000000000..e9cf72d95
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/JavaScriptException.java
@@ -0,0 +1,21 @@
+package org.wordpress.android;
+
+public class JavaScriptException extends Throwable {
+ String mFile;
+ int mLine;
+
+ public JavaScriptException(String file, int line, String message) {
+ super(message);
+ mFile = file;
+ mLine = line;
+ fillInStackTrace();
+ }
+
+ @Override
+ public Throwable fillInStackTrace() {
+ setStackTrace(new StackTraceElement[] {
+ new StackTraceElement("JavaScriptException", "", mFile, mLine)
+ });
+ return this;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.java b/WordPress/src/main/java/org/wordpress/android/WordPress.java
new file mode 100644
index 000000000..b449c1054
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/WordPress.java
@@ -0,0 +1,898 @@
+package org.wordpress.android;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Dialog;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.http.HttpResponseCache;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.support.multidex.MultiDexApplication;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+
+import com.android.volley.RequestQueue;
+import com.android.volley.VolleyLog;
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.Volley;
+import com.crashlytics.android.Crashlytics;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.analytics.AnalyticsTrackerMixpanel;
+import org.wordpress.android.analytics.AnalyticsTrackerNosara;
+import org.wordpress.android.datasets.ReaderDatabase;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.ConnectionChangeReceiver;
+import org.wordpress.android.networking.OAuthAuthenticator;
+import org.wordpress.android.networking.OAuthAuthenticatorFactory;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.networking.SelfSignedSSLCertsManager;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.accounts.helpers.UpdateBlogListTask.GenericUpdateBlogListTask;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsDatabaseHelper;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.BitmapLruCache;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.CoreEvents.UserSignedOutCompletely;
+import org.wordpress.android.util.CoreEvents.UserSignedOutWordPressCom;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.PackageUtils;
+import org.wordpress.android.util.ProfilingUtils;
+import org.wordpress.android.util.RateLimitedTask;
+import org.wordpress.android.util.SqlUtils;
+import org.wordpress.android.util.VolleyUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.passcodelock.AbstractAppLock;
+import org.wordpress.passcodelock.AppLockManager;
+import org.xmlrpc.android.ApiHelper;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+import java.security.GeneralSecurityException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import de.greenrobot.event.EventBus;
+import io.fabric.sdk.android.Fabric;
+
+public class WordPress extends MultiDexApplication {
+ public static String versionName;
+ public static Blog currentBlog;
+ public static WordPressDB wpDB;
+
+ public static RequestQueue requestQueue;
+ public static ImageLoader imageLoader;
+
+ private static RestClientUtils mRestClientUtils;
+ private static RestClientUtils mRestClientUtilsVersion1_1;
+ private static RestClientUtils mRestClientUtilsVersion1_2;
+ private static RestClientUtils mRestClientUtilsVersion1_3;
+ private static RestClientUtils mRestClientUtilsVersion0;
+
+ private static final int SECONDS_BETWEEN_OPTIONS_UPDATE = 10 * 60;
+ private static final int SECONDS_BETWEEN_BLOGLIST_UPDATE = 6 * 60 * 60;
+ private static final int SECONDS_BETWEEN_DELETE_STATS = 5 * 60; // 5 minutes
+
+ private static Context mContext;
+ private static BitmapLruCache mBitmapCache;
+
+ /**
+ * Updates Options for the current blog in background.
+ */
+ public static RateLimitedTask sUpdateCurrentBlogOption = new RateLimitedTask(SECONDS_BETWEEN_OPTIONS_UPDATE) {
+ protected boolean run() {
+ Blog currentBlog = WordPress.getCurrentBlog();
+ if (currentBlog != null) {
+ new ApiHelper.RefreshBlogContentTask(currentBlog, null).executeOnExecutor(
+ AsyncTask.THREAD_POOL_EXECUTOR, false);
+ return true;
+ }
+ return false;
+ }
+ };
+
+ /**
+ * Update blog list in a background task. Broadcast WordPress.BROADCAST_ACTION_BLOG_LIST_CHANGED if the
+ * list changed.
+ */
+ public static RateLimitedTask sUpdateWordPressComBlogList = new RateLimitedTask(SECONDS_BETWEEN_BLOGLIST_UPDATE) {
+ protected boolean run() {
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ new GenericUpdateBlogListTask(getContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Delete stats cache that is already expired
+ */
+ public static RateLimitedTask sDeleteExpiredStats = new RateLimitedTask(SECONDS_BETWEEN_DELETE_STATS) {
+ protected boolean run() {
+ // Offload to a separate thread. We don't want to slown down the app on startup/resume.
+ new Thread(new Runnable() {
+ public void run() {
+ // subtracts to the current time the cache TTL
+ long timeToDelete = System.currentTimeMillis() - (StatsTable.CACHE_TTL_MINUTES * 60 * 1000);
+ StatsTable.deleteOldStats(WordPress.getContext(), timeToDelete);
+ }
+ }).start();
+ return true;
+ }
+ };
+
+ public static BitmapLruCache getBitmapCache() {
+ if (mBitmapCache == null) {
+ // The cache size will be measured in kilobytes rather than
+ // number of items. See http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
+ int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ int cacheSize = maxMemory / 16; //Use 1/16th of the available memory for this memory cache.
+ mBitmapCache = new BitmapLruCache(cacheSize);
+ }
+ return mBitmapCache;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ long startDate = SystemClock.elapsedRealtime();
+
+ mContext = this;
+
+ ProfilingUtils.start("App Startup");
+ // Enable log recording
+ AppLog.enableRecording(true);
+ AppLog.i(T.UTILS, "WordPress.onCreate");
+
+ if (!PackageUtils.isDebugBuild()) {
+ Fabric.with(this, new Crashlytics());
+ }
+
+ versionName = PackageUtils.getVersionName(this);
+ initWpDb();
+ enableHttpResponseCache(mContext);
+
+ // EventBus setup
+ EventBus.TAG = "WordPress-EVENT";
+ EventBus.builder()
+ .logNoSubscriberMessages(false)
+ .sendNoSubscriberEvent(false)
+ .throwSubscriberException(true)
+ .installDefaultEventBus();
+ EventBus.getDefault().register(this);
+
+ RestClientUtils.setUserAgent(getUserAgent());
+
+ // Volley networking setup
+ setupVolleyQueue();
+
+ AppLockManager.getInstance().enableDefaultAppLockIfAvailable(this);
+ if (AppLockManager.getInstance().isAppLockFeatureEnabled()) {
+ AppLockManager.getInstance().getAppLock().setExemptActivities(
+ new String[]{"org.wordpress.android.ui.ShareIntentReceiverActivity"});
+ }
+
+ HelpshiftHelper.init(this);
+
+ ApplicationLifecycleMonitor applicationLifecycleMonitor = new ApplicationLifecycleMonitor();
+ registerComponentCallbacks(applicationLifecycleMonitor);
+ registerActivityLifecycleCallbacks(applicationLifecycleMonitor);
+
+ initAnalytics(SystemClock.elapsedRealtime() - startDate);
+
+ // If users uses a custom locale set it on start of application
+ WPActivityUtils.applyLocale(getContext());
+ }
+
+ private void initAnalytics(final long elapsedTimeOnCreate) {
+ AnalyticsTracker.registerTracker(new AnalyticsTrackerMixpanel(getContext(), BuildConfig.MIXPANEL_TOKEN));
+ AnalyticsTracker.registerTracker(new AnalyticsTrackerNosara(getContext()));
+ AnalyticsTracker.init(getContext());
+ AnalyticsUtils.refreshMetadata();
+
+ // Track app upgrade and install
+ int versionCode = PackageUtils.getVersionCode(getContext());
+
+ int oldVersionCode = AppPrefs.getLastAppVersionCode();
+ if (oldVersionCode == 0) {
+ // Track application installed if there isn't old version code
+ AnalyticsTracker.track(Stat.APPLICATION_INSTALLED);
+ AppPrefs.setVisualEditorPromoRequired(false);
+ }
+ if (oldVersionCode != 0 && oldVersionCode < versionCode) {
+ Map<String, Long> properties = new HashMap<String, Long>(1);
+ properties.put("elapsed_time_on_create", elapsedTimeOnCreate);
+ // app upgraded
+ AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_UPGRADED, properties);
+ }
+ AppPrefs.setLastAppVersionCode(versionCode);
+ }
+
+ /**
+ * Application.onCreate is called before any activity, service, or receiver - it can be called while the app
+ * is in background by a sticky service or a receiver, so we don't want Application.onCreate to make network request
+ * or other heavy tasks.
+ *
+ * This deferredInit method is called when a user starts an activity for the first time, ie. when he sees a
+ * screen for the first time. This allows us to have heavy calls on first activity startup instead of app startup.
+ */
+ public void deferredInit(Activity activity) {
+ AppLog.i(T.UTILS, "Deferred Initialisation");
+
+ if (isGooglePlayServicesAvailable(activity)) {
+ // Register for Cloud messaging
+ startService(new Intent(this, GCMRegistrationIntentService.class));
+ }
+ configureSimperium();
+
+ // Refresh account informations
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ AccountHelper.getDefaultAccount().fetchAccountDetails();
+ }
+ }
+
+ // Configure Simperium and start buckets if we are signed in to WP.com
+ private void configureSimperium() {
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ AppLog.i(T.NOTIFS, "Configuring Simperium");
+ SimperiumUtils.configureSimperium(this, AccountHelper.getDefaultAccount().getAccessToken());
+ }
+ }
+
+ public static void setupVolleyQueue() {
+ requestQueue = Volley.newRequestQueue(mContext, VolleyUtils.getHTTPClientStack(mContext));
+ imageLoader = new ImageLoader(requestQueue, getBitmapCache());
+ VolleyLog.setTag(AppLog.TAG);
+ // http://stackoverflow.com/a/17035814
+ imageLoader.setBatchedResponseDelay(0);
+ }
+
+ private void initWpDb() {
+ if (!createAndVerifyWpDb()) {
+ AppLog.e(T.DB, "Invalid database, sign out user and delete database");
+ currentBlog = null;
+ if (wpDB != null) {
+ wpDB.updateLastBlogId(-1);
+ }
+ // Force DB deletion
+ WordPressDB.deleteDatabase(this);
+ wpDB = new WordPressDB(this);
+ }
+ }
+
+ private boolean createAndVerifyWpDb() {
+ try {
+ wpDB = new WordPressDB(this);
+ // verify account data - query will return 1 if any blog names or urls are null
+ int result = SqlUtils.intForQuery(wpDB.getDatabase(),
+ "SELECT 1 FROM accounts WHERE blogName IS NULL OR url IS NULL LIMIT 1", null);
+ return result != 1;
+ } catch (RuntimeException e) {
+ AppLog.e(T.DB, e);
+ return false;
+ }
+ }
+
+ public static Context getContext() {
+ return mContext;
+ }
+
+ public static RestClientUtils getRestClientUtils() {
+ if (mRestClientUtils == null) {
+ OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
+ mRestClientUtils = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener);
+ }
+ return mRestClientUtils;
+ }
+
+ private static RestRequest.OnAuthFailedListener mOnAuthFailedListener = new RestRequest.OnAuthFailedListener() {
+ @Override
+ public void onAuthFailed() {
+ if (getContext() == null) return;
+ // If this is called, it means the WP.com token is no longer valid.
+ EventBus.getDefault().post(new CoreEvents.RestApiUnauthorized());
+ }
+ };
+
+ public static RestClientUtils getRestClientUtilsV1_1() {
+ if (mRestClientUtilsVersion1_1 == null) {
+ OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
+ mRestClientUtilsVersion1_1 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1_1);
+ }
+ return mRestClientUtilsVersion1_1;
+ }
+
+ public static RestClientUtils getRestClientUtilsV1_2() {
+ if (mRestClientUtilsVersion1_2 == null) {
+ OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
+ mRestClientUtilsVersion1_2 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1_2);
+ }
+ return mRestClientUtilsVersion1_2;
+ }
+
+ public static RestClientUtils getRestClientUtilsV1_3() {
+ if (mRestClientUtilsVersion1_3 == null) {
+ OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
+ mRestClientUtilsVersion1_3 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1_3);
+ }
+ return mRestClientUtilsVersion1_3;
+ }
+
+ public static RestClientUtils getRestClientUtilsV0() {
+ if (mRestClientUtilsVersion0 == null) {
+ OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
+ mRestClientUtilsVersion0 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V0);
+ }
+ return mRestClientUtilsVersion0;
+ }
+
+ /**
+ * enables "strict mode" for testing - should NEVER be used in release builds
+ */
+ private static void enableStrictMode() {
+ // return if the build is not a debug build
+ if (!BuildConfig.DEBUG) {
+ AppLog.e(T.UTILS, "You should not call enableStrictMode() on a non debug build");
+ return;
+ }
+
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .detectDiskWrites()
+ .detectNetwork()
+ .penaltyLog()
+ .penaltyFlashScreen()
+ .build());
+
+ StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+ .detectActivityLeaks()
+ .detectLeakedSqlLiteObjects()
+ .detectLeakedClosableObjects()
+ .detectLeakedRegistrationObjects() // <-- requires Jelly Bean
+ .penaltyLog()
+ .build());
+
+ AppLog.w(T.UTILS, "Strict mode enabled");
+ }
+
+ public boolean isGooglePlayServicesAvailable(Activity activity) {
+ GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
+ int connectionResult = googleApiAvailability.isGooglePlayServicesAvailable(activity);
+ switch (connectionResult) {
+ // Success: return true
+ case ConnectionResult.SUCCESS:
+ return true;
+ // Play Services unavailable, show an error dialog is the Play Services Lib needs an update
+ case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
+ Dialog dialog = googleApiAvailability.getErrorDialog(activity, connectionResult, 0);
+ if (dialog != null) {
+ dialog.show();
+ }
+ default:
+ case ConnectionResult.SERVICE_MISSING:
+ case ConnectionResult.SERVICE_DISABLED:
+ case ConnectionResult.SERVICE_INVALID:
+ AppLog.w(T.NOTIFS, "Google Play Services unavailable, connection result: "
+ + googleApiAvailability.getErrorString(connectionResult));
+ }
+ return false;
+ }
+
+ /**
+ * Get the currently active blog.
+ * <p/>
+ * If the current blog is not already set, try and determine the last active blog from the last
+ * time the application was used. If we're not able to determine the last active blog, try to
+ * select the first visible blog. If there are no more visible blogs, try to select the first
+ * hidden blog. If there are no blogs at all, return null.
+ */
+ public static Blog getCurrentBlog() {
+ if (currentBlog == null || !wpDB.isDotComBlogVisible(currentBlog.getRemoteBlogId())) {
+ attemptToRestoreLastActiveBlog();
+ }
+
+ return currentBlog;
+ }
+
+ /**
+ * Get the blog with the specified ID.
+ *
+ * @param id ID of the blog to retrieve.
+ * @return the blog with the specified ID, or null if blog could not be retrieved.
+ */
+ public static Blog getBlog(int id) {
+ try {
+ return wpDB.instantiateBlogByLocalId(id);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Set the last active blog as the current blog.
+ *
+ * @return the current blog
+ */
+ public static Blog setCurrentBlogToLastActive() {
+ List<Map<String, Object>> accounts = WordPress.wpDB.getVisibleBlogs();
+
+ int lastBlogId = WordPress.wpDB.getLastBlogId();
+ if (lastBlogId != -1) {
+ for (Map<String, Object> account : accounts) {
+ int id = Integer.valueOf(account.get("id").toString());
+ if (id == lastBlogId) {
+ setCurrentBlog(id);
+ return currentBlog;
+ }
+ }
+ }
+ // Previous active blog is hidden or deleted
+ currentBlog = null;
+ return null;
+ }
+
+ /**
+ * Set the blog with the specified id as the current blog.
+ *
+ * @param id id of the blog to set as current
+ */
+ public static void setCurrentBlog(int id) {
+ currentBlog = getBlog(id);
+ }
+
+ public static void setCurrentBlogAndSetVisible(int id) {
+ setCurrentBlog(id);
+
+ if (currentBlog != null && currentBlog.isHidden()) {
+ wpDB.setDotComBlogsVisibility(id, true);
+ currentBlog.setHidden(false);
+ }
+ }
+
+ /**
+ * returns the blogID of the current blog or null if current blog is null or remoteID is null.
+ */
+ public static String getCurrentRemoteBlogId() {
+ return (getCurrentBlog() != null ? getCurrentBlog().getDotComBlogId() : null);
+ }
+
+ public static int getCurrentLocalTableBlogId() {
+ return (getCurrentBlog() != null ? getCurrentBlog().getLocalTableBlogId() : -1);
+ }
+
+ /**
+ * Sign out from wpcom account.
+ * Note: This method must not be called on UI Thread.
+ */
+ public static void WordPressComSignOut(Context context) {
+ // Keep the analytics tracking at the beginning, before the account data is actual removed.
+ AnalyticsTracker.track(Stat.ACCOUNT_LOGOUT);
+
+ removeWpComUserRelatedData(context);
+
+ // broadcast an event: wpcom user signed out
+ EventBus.getDefault().post(new UserSignedOutWordPressCom());
+
+ // broadcast an event only if the user is completely signed out
+ if (!AccountHelper.isSignedIn()) {
+ EventBus.getDefault().post(new UserSignedOutCompletely());
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(UserSignedOutCompletely event) {
+ try {
+ SelfSignedSSLCertsManager.getInstance(getContext()).emptyLocalKeyStoreFile();
+ } catch (GeneralSecurityException e) {
+ AppLog.e(T.UTILS, "Error while cleaning the Local KeyStore File", e);
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, "Error while cleaning the Local KeyStore File", e);
+ }
+
+ flushHttpCache();
+
+ // Analytics resets
+ AnalyticsTracker.endSession(false);
+ AnalyticsTracker.clearAllData();
+
+ // disable passcode lock
+ AbstractAppLock appLock = AppLockManager.getInstance().getAppLock();
+ if (appLock != null) {
+ appLock.setPassword(null);
+ }
+
+ // dangerously delete all content!
+ wpDB.dangerouslyDeleteAllContent();
+ }
+
+
+ public static void removeWpComUserRelatedData(Context context) {
+ // cancel all Volley requests - do this before unregistering push since that uses
+ // a Volley request
+ VolleyUtils.cancelAllRequests(requestQueue);
+
+ NotificationsUtils.unregisterDevicePushNotifications(context);
+ try {
+ String gcmId = BuildConfig.GCM_ID;
+ if (!TextUtils.isEmpty(gcmId)) {
+ InstanceID.getInstance(context).deleteToken(gcmId, GoogleCloudMessaging.INSTANCE_ID_SCOPE);
+ }
+ } catch (Exception e) {
+ AppLog.e(T.NOTIFS, "Could not delete GCM Token", e);
+ }
+
+ // delete wpcom blogs
+ wpDB.deleteWordPressComBlogs(context);
+
+ // reset default account
+ AccountHelper.getDefaultAccount().signout();
+
+ // reset all reader-related prefs & data
+ AppPrefs.reset();
+ ReaderDatabase.reset();
+
+ // Reset Stats Data
+ StatsDatabaseHelper.getDatabase(context).reset();
+ StatsWidgetProvider.updateWidgetsOnLogout(context);
+
+ // Reset Simperium buckets (removes local data)
+ SimperiumUtils.resetBucketsAndDeauthorize();
+ }
+
+ public static String getLoginUrl(Blog blog) {
+ String loginURL = null;
+ Gson gson = new Gson();
+ Type type = new TypeToken<Map<?, ?>>() {
+ }.getType();
+ Map<?, ?> blogOptions = gson.fromJson(blog.getBlogOptions(), type);
+ if (blogOptions != null) {
+ Map<?, ?> homeURLMap = (Map<?, ?>) blogOptions.get("login_url");
+ if (homeURLMap != null)
+ loginURL = homeURLMap.get("value").toString();
+ }
+ // Try to guess the login URL if blogOptions is null (blog not added to the app), or WP version is < 3.6
+ if (loginURL == null) {
+ if (blog.getUrl().lastIndexOf("/") != -1) {
+ return blog.getUrl().substring(0, blog.getUrl().lastIndexOf("/"))
+ + "/wp-login.php";
+ } else {
+ return blog.getUrl().replace("xmlrpc.php", "wp-login.php");
+ }
+ }
+
+ return loginURL;
+ }
+
+ /**
+ * Device's default User-Agent string.
+ * E.g.:
+ * "Mozilla/5.0 (Linux; Android 6.0; Android SDK built for x86_64 Build/MASTER; wv)
+ * AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile
+ * Safari/537.36"
+ */
+ private static String mDefaultUserAgent;
+ public static String getDefaultUserAgent() {
+ if (mDefaultUserAgent == null) {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ mDefaultUserAgent = WebSettings.getDefaultUserAgent(getContext());
+ } else {
+ mDefaultUserAgent = new WebView(getContext()).getSettings().getUserAgentString();
+ }
+ } catch (AndroidRuntimeException | NullPointerException e) {
+ // Catch AndroidRuntimeException that could be raised by the WebView() constructor.
+ // See https://github.com/wordpress-mobile/WordPress-Android/issues/3594
+ // Catch NullPointerException that could be raised by WebSettings.getDefaultUserAgent()
+ // See https://github.com/wordpress-mobile/WordPress-Android/issues/3838
+
+ // init with the empty string, it's a rare issue
+ mDefaultUserAgent = "";
+ }
+
+ }
+ return mDefaultUserAgent;
+ }
+
+ /**
+ * User-Agent string when making HTTP connections, for both API traffic and WebViews.
+ * Appends "wp-android/version" to WebView's default User-Agent string for the webservers
+ * to get the full feature list of the browser and serve content accordingly, e.g.:
+ * "Mozilla/5.0 (Linux; Android 6.0; Android SDK built for x86_64 Build/MASTER; wv)
+ * AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile
+ * Safari/537.36 wp-android/4.7"
+ * Note that app versions prior to 2.7 simply used "wp-android" as the user agent
+ **/
+ private static final String USER_AGENT_APPNAME = "wp-android";
+ private static String mUserAgent;
+ public static String getUserAgent() {
+ if (mUserAgent == null) {
+ String defaultUserAgent = getDefaultUserAgent();
+ if (TextUtils.isEmpty(defaultUserAgent)) {
+ mUserAgent = USER_AGENT_APPNAME + "/" + PackageUtils.getVersionName(getContext());
+ } else {
+ mUserAgent = defaultUserAgent + " "+ USER_AGENT_APPNAME + "/"
+ + PackageUtils.getVersionName(getContext());
+ }
+ }
+ return mUserAgent;
+ }
+
+ /*
+ * enable caching for HttpUrlConnection
+ * http://developer.android.com/training/efficient-downloads/redundant_redundant.html
+ */
+ private static void enableHttpResponseCache(Context context) {
+ try {
+ long httpCacheSize = 5 * 1024 * 1024; // 5MB
+ File httpCacheDir = new File(context.getCacheDir(), "http");
+ HttpResponseCache.install(httpCacheDir, httpCacheSize);
+ } catch (IOException e) {
+ AppLog.w(T.UTILS, "Failed to enable http response cache");
+ }
+ }
+
+ private static void flushHttpCache() {
+ HttpResponseCache cache = HttpResponseCache.getInstalled();
+ if (cache != null) {
+ cache.flush();
+ }
+ }
+
+ private static void attemptToRestoreLastActiveBlog() {
+ if (setCurrentBlogToLastActive() == null) {
+ int blogId = WordPress.wpDB.getFirstVisibleBlogId();
+ if (blogId == 0) {
+ blogId = WordPress.wpDB.getFirstHiddenBlogId();
+ }
+
+ setCurrentBlogAndSetVisible(blogId);
+ wpDB.updateLastBlogId(blogId);
+ }
+ }
+
+ /**
+ * Gets a field from the project's BuildConfig using reflection. This is useful when flavors
+ * are used at the project level to set custom fields.
+ * based on: https://code.google.com/p/android/issues/detail?id=52962#c38
+ * @param application Used to find the correct file
+ * @param fieldName The name of the field-to-access
+ * @return The value of the field, or {@code null} if the field is not found.
+ */
+ public static Object getBuildConfigValue(Application application, String fieldName) {
+ try {
+ String packageName = application.getClass().getPackage().getName();
+ Class<?> clazz = Class.forName(packageName + ".BuildConfig");
+ Field field = clazz.getField(fieldName);
+ return field.get(null);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Detect when the app goes to the background and come back to the foreground.
+ *
+ * Turns out that when your app has no more visible UI, a callback is triggered.
+ * The callback, implemented in this custom class, is called ComponentCallbacks2 (yes, with a two).
+ *
+ * This class also uses ActivityLifecycleCallbacks and a timer used as guard,
+ * to make sure to detect the send to background event and not other events.
+ *
+ */
+ private class ApplicationLifecycleMonitor implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 {
+ private final int DEFAULT_TIMEOUT = 2 * 60; // 2 minutes
+ private Date mLastPingDate;
+ private Date mApplicationOpenedDate;
+ boolean mFirstActivityResumed = true;
+ private Timer mActivityTransitionTimer;
+ private TimerTask mActivityTransitionTimerTask;
+ private final long MAX_ACTIVITY_TRANSITION_TIME_MS = 2000;
+ boolean mIsInBackground = true;
+
+ @Override
+ public void onConfigurationChanged(final Configuration newConfig) {
+ // Reapply locale on configuration change
+ WPActivityUtils.applyLocale(getContext());
+ }
+
+ @Override
+ public void onLowMemory() {
+ }
+
+ @Override
+ public void onTrimMemory(final int level) {
+ boolean evictBitmaps = false;
+ switch (level) {
+ case TRIM_MEMORY_COMPLETE:
+ case TRIM_MEMORY_MODERATE:
+ case TRIM_MEMORY_RUNNING_MODERATE:
+ case TRIM_MEMORY_RUNNING_CRITICAL:
+ case TRIM_MEMORY_RUNNING_LOW:
+ evictBitmaps = true;
+ break;
+ default:
+ break;
+ }
+
+ if (evictBitmaps && mBitmapCache != null) {
+ mBitmapCache.evictAll();
+ }
+ }
+
+ private boolean isPushNotificationPingNeeded() {
+ if (mLastPingDate == null) {
+ // first startup
+ return false;
+ }
+
+ Date now = new Date();
+ if (DateTimeUtils.secondsBetween(now, mLastPingDate) >= DEFAULT_TIMEOUT) {
+ mLastPingDate = now;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if user has valid credentials, and that at least 2 minutes are passed
+ * since the last ping, then try to update the PN token.
+ */
+ private void updatePushNotificationTokenIfNotLimited() {
+ // Synch Push Notifications settings
+ if (isPushNotificationPingNeeded() && AccountHelper.isSignedInWordPressDotCom()) {
+ // Register for Cloud messaging
+ startService(new Intent(getContext(), GCMRegistrationIntentService.class));
+ }
+ }
+
+ /**
+ * The two methods below (startActivityTransitionTimer and stopActivityTransitionTimer)
+ * are used to track when the app goes to background.
+ *
+ * Our implementation uses `onActivityPaused` and `onActivityResumed` of ApplicationLifecycleMonitor
+ * to start and stop the timer that detects when the app goes to background.
+ *
+ * So when the user is simply navigating between the activities, the onActivityPaused() calls `startActivityTransitionTimer`
+ * and starts the timer, but almost immediately the new activity being entered, the ApplicationLifecycleMonitor cancels the timer
+ * in its onActivityResumed method, that in order calls `stopActivityTransitionTimer`.
+ * And so mIsInBackground would be false.
+ *
+ * In the case the app is sent to background, the TimerTask is instead executed, and the code that handles all the background logic is run.
+ */
+ private void startActivityTransitionTimer() {
+ this.mActivityTransitionTimer = new Timer();
+ this.mActivityTransitionTimerTask = new TimerTask() {
+ public void run() {
+ AppLog.i(T.UTILS, "App goes to background");
+ // We're in the Background
+ mIsInBackground = true;
+ String lastActivityString = AppPrefs.getLastActivityStr();
+ ActivityId lastActivity = ActivityId.getActivityIdFromName(lastActivityString);
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("last_visible_screen", lastActivity.toString());
+ if (mApplicationOpenedDate != null) {
+ Date now = new Date();
+ properties.put("time_in_app", DateTimeUtils.secondsBetween(now, mApplicationOpenedDate));
+ mApplicationOpenedDate = null;
+ }
+ AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_CLOSED, properties);
+ AnalyticsTracker.endSession(false);
+ ConnectionChangeReceiver.setEnabled(WordPress.this, false);
+ }
+ };
+
+ this.mActivityTransitionTimer.schedule(mActivityTransitionTimerTask,
+ MAX_ACTIVITY_TRANSITION_TIME_MS);
+ }
+
+ private void stopActivityTransitionTimer() {
+ if (this.mActivityTransitionTimerTask != null) {
+ this.mActivityTransitionTimerTask.cancel();
+ }
+
+ if (this.mActivityTransitionTimer != null) {
+ this.mActivityTransitionTimer.cancel();
+ }
+
+ mIsInBackground = false;
+ }
+
+ /**
+ * This method is called when:
+ * 1. the app starts (but it's not opened by a service or a broadcast receiver, i.e. an activity is resumed)
+ * 2. the app was in background and is now foreground
+ */
+ private void onAppComesFromBackground() {
+ AppLog.i(T.UTILS, "App comes from background");
+ ConnectionChangeReceiver.setEnabled(WordPress.this, true);
+ AnalyticsUtils.refreshMetadata();
+ mApplicationOpenedDate = new Date();
+ AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_OPENED);
+ if (NetworkUtils.isNetworkAvailable(mContext)) {
+ // Rate limited PN Token Update
+ updatePushNotificationTokenIfNotLimited();
+
+ // Rate limited WPCom blog list Update
+ sUpdateWordPressComBlogList.runIfNotLimited();
+
+ // Rate limited blog options Update
+ sUpdateCurrentBlogOption.runIfNotLimited();
+ }
+ sDeleteExpiredStats.runIfNotLimited();
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ if (mIsInBackground) {
+ // was in background before
+ onAppComesFromBackground();
+ }
+ stopActivityTransitionTimer();
+
+ mIsInBackground = false;
+ if (mFirstActivityResumed) {
+ deferredInit(activity);
+ }
+ mFirstActivityResumed = false;
+ }
+
+ @Override
+ public void onActivityCreated(Activity arg0, Bundle arg1) {
+ }
+
+ @Override
+ public void onActivityDestroyed(Activity arg0) {
+ }
+
+ @Override
+ public void onActivityPaused(Activity arg0) {
+ mLastPingDate = new Date();
+ startActivityTransitionTimer();
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity arg0, Bundle arg1) {
+ }
+
+ @Override
+ public void onActivityStarted(Activity arg0) {
+ }
+
+ @Override
+ public void onActivityStopped(Activity arg0) {
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/WordPressDB.java b/WordPress/src/main/java/org/wordpress/android/WordPressDB.java
new file mode 100644
index 000000000..760004d7a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/WordPressDB.java
@@ -0,0 +1,2087 @@
+package org.wordpress.android;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.util.Base64;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.json.JSONArray;
+import org.wordpress.android.datasets.AccountTable;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.datasets.PeopleTable;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.datasets.SuggestionTable;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.MediaUploadState;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostLocation;
+import org.wordpress.android.models.PostsListPost;
+import org.wordpress.android.models.PostsListPostList;
+import org.wordpress.android.models.Theme;
+import org.wordpress.android.ui.media.services.MediaEvents.MediaChanged;
+import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.ShortcodeUtils;
+import org.wordpress.android.util.SqlUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPUrlUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESKeySpec;
+
+import de.greenrobot.event.EventBus;
+
+public class WordPressDB {
+ public static final String COLUMN_NAME_ID = "_id";
+ public static final String COLUMN_NAME_POST_ID = "postID";
+ public static final String COLUMN_NAME_FILE_PATH = "filePath";
+ public static final String COLUMN_NAME_FILE_NAME = "fileName";
+ public static final String COLUMN_NAME_TITLE = "title";
+ public static final String COLUMN_NAME_DESCRIPTION = "description";
+ public static final String COLUMN_NAME_CAPTION = "caption";
+ public static final String COLUMN_NAME_HORIZONTAL_ALIGNMENT = "horizontalAlignment";
+ public static final String COLUMN_NAME_WIDTH = "width";
+ public static final String COLUMN_NAME_HEIGHT = "height";
+ public static final String COLUMN_NAME_MIME_TYPE = "mimeType";
+ public static final String COLUMN_NAME_FEATURED = "featured";
+ public static final String COLUMN_NAME_IS_VIDEO = "isVideo";
+ public static final String COLUMN_NAME_IS_FEATURED_IN_POST = "isFeaturedInPost";
+ public static final String COLUMN_NAME_FILE_URL = "fileURL";
+ public static final String COLUMN_NAME_THUMBNAIL_URL = "thumbnailURL";
+ public static final String COLUMN_NAME_MEDIA_ID = "mediaId";
+ public static final String COLUMN_NAME_BLOG_ID = "blogId";
+ public static final String COLUMN_NAME_DATE_CREATED_GMT = "date_created_gmt";
+ public static final String COLUMN_NAME_VIDEO_PRESS_SHORTCODE = "videoPressShortcode";
+ public static final String COLUMN_NAME_UPLOAD_STATE = "uploadState";
+
+ private static final int DATABASE_VERSION = 49;
+
+ private static final String CREATE_TABLE_BLOGS = "create table if not exists accounts (id integer primary key autoincrement, "
+ + "url text, blogName text, username text, password text, imagePlacement text, centerThumbnail boolean, fullSizeImage boolean, maxImageWidth text, maxImageWidthId integer);";
+ private static final String CREATE_TABLE_MEDIA = "create table if not exists media (id integer primary key autoincrement, "
+ + "postID integer not null, filePath text default '', fileName text default '', title text default '', description text default '', caption text default '', horizontalAlignment integer default 0, width integer default 0, height integer default 0, mimeType text default '', featured boolean default false, isVideo boolean default false);";
+ public static final String BLOGS_TABLE = "accounts";
+
+ // Warning if you rename DATABASE_NAME, that could break previous App backups (see: xml/backup_scheme.xml)
+ private static final String DATABASE_NAME = "wordpress";
+ private static final String MEDIA_TABLE = "media";
+ private static final String NOTES_TABLE = "notes";
+
+ private static final String CREATE_TABLE_POSTS =
+ "create table if not exists posts ("
+ + "id integer primary key autoincrement,"
+ + "blogID text,"
+ + "postid text,"
+ + "title text default '',"
+ + "dateCreated date,"
+ + "date_created_gmt date,"
+ + "categories text default '',"
+ + "custom_fields text default '',"
+ + "description text default '',"
+ + "link text default '',"
+ + "mt_allow_comments boolean,"
+ + "mt_allow_pings boolean,"
+ + "mt_excerpt text default '',"
+ + "mt_keywords text default '',"
+ + "mt_text_more text default '',"
+ + "permaLink text default '',"
+ + "post_status text default '',"
+ + "userid integer default 0,"
+ + "wp_author_display_name text default '',"
+ + "wp_author_id text default '',"
+ + "wp_password text default '',"
+ + "wp_post_format text default '',"
+ + "wp_slug text default '',"
+ + "mediaPaths text default '',"
+ + "latitude real,"
+ + "longitude real,"
+ + "localDraft boolean default 0,"
+ + "isPage boolean default 0,"
+ + "wp_page_parent_id text,"
+ + "wp_page_parent_title text);";
+
+ private static final String POSTS_TABLE = "posts";
+
+ private static final String THEMES_TABLE = "themes";
+ private static final String CREATE_TABLE_THEMES = "create table if not exists themes ("
+ + COLUMN_NAME_ID + " integer primary key autoincrement, "
+ + Theme.ID + " text, "
+ + Theme.AUTHOR + " text, "
+ + Theme.SCREENSHOT + " text, "
+ + Theme.AUTHOR_URI + " text, "
+ + Theme.DEMO_URI + " text, "
+ + Theme.NAME + " text, "
+ + Theme.STYLESHEET + " text, "
+ + Theme.PRICE + " text, "
+ + Theme.BLOG_ID + " text, "
+ + Theme.IS_CURRENT + " boolean default false);";
+
+ // categories
+ private static final String CREATE_TABLE_CATEGORIES = "create table if not exists cats (id integer primary key autoincrement, "
+ + "blog_id text, wp_id integer, category_name text not null);";
+ private static final String CATEGORIES_TABLE = "cats";
+
+ // for capturing blogID
+ private static final String ADD_BLOGID = "alter table accounts add blogId integer;";
+ private static final String UPDATE_BLOGID = "update accounts set blogId = 1;";
+
+ // for capturing blogID, trac ticket #
+ private static final String ADD_LOCATION_FLAG = "alter table accounts add location boolean default false;";
+
+ // add wordpress.com stats login info
+ private static final String ADD_DOTCOM_USERNAME = "alter table accounts add dotcom_username text;";
+ private static final String ADD_DOTCOM_PASSWORD = "alter table accounts add dotcom_password text;";
+ private static final String ADD_API_KEY = "alter table accounts add api_key text;";
+ private static final String ADD_API_BLOGID = "alter table accounts add api_blogid text;";
+
+ // add wordpress.com flag and version column
+ private static final String ADD_DOTCOM_FLAG = "alter table accounts add dotcomFlag boolean default false;";
+ private static final String ADD_WP_VERSION = "alter table accounts add wpVersion text;";
+
+ // add httpuser and httppassword
+ private static final String ADD_HTTPUSER = "alter table accounts add httpuser text;";
+ private static final String ADD_HTTPPASSWORD = "alter table accounts add httppassword text;";
+
+ // add new table for QuickPress homescreen shortcuts
+ private static final String CREATE_TABLE_QUICKPRESS_SHORTCUTS = "create table if not exists quickpress_shortcuts (id integer primary key autoincrement, accountId text, name text);";
+ private static final String QUICKPRESS_SHORTCUTS_TABLE = "quickpress_shortcuts";
+
+ // add field to store last used blog
+ private static final String ADD_POST_FORMATS = "alter table accounts add postFormats text default '';";
+
+ //add scaled image settings
+ private static final String ADD_SCALED_IMAGE = "alter table accounts add isScaledImage boolean default false;";
+ private static final String ADD_SCALED_IMAGE_IMG_WIDTH = "alter table accounts add scaledImgWidth integer default 1024;";
+
+ //add boolean to posts to check uploaded posts that have local changes
+ private static final String ADD_LOCAL_POST_CHANGES = "alter table posts add isLocalChange boolean default 0";
+
+ // add wp_post_thumbnail to posts table
+ private static final String ADD_POST_THUMBNAIL = "alter table posts add wp_post_thumbnail integer default 0;";
+
+ // add postid and blogID indexes to posts table
+ private static final String ADD_POST_ID_INDEX = "CREATE INDEX idx_posts_post_id ON posts(postid);";
+ private static final String ADD_BLOG_ID_INDEX = "CREATE INDEX idx_posts_blog_id ON posts(blogID);";
+
+ //add boolean to track if featured image should be included in the post content
+ private static final String ADD_FEATURED_IN_POST = "alter table media add isFeaturedInPost boolean default false;";
+
+ // add home url to blog settings
+ private static final String ADD_HOME_URL = "alter table accounts add homeURL text default '';";
+
+ private static final String ADD_BLOG_OPTIONS = "alter table accounts add blog_options text default '';";
+
+ // add category parent id to keep track of category hierarchy
+ private static final String ADD_PARENTID_IN_CATEGORIES = "alter table cats add parent_id integer default 0;";
+
+ // add admin flag to blog settings
+ private static final String ADD_BLOGS_ADMIN_FLAG = "alter table accounts add isAdmin boolean default false;";
+
+ // add thumbnailURL, thumbnailPath and fileURL to media
+ private static final String ADD_MEDIA_THUMBNAIL_URL = "alter table media add thumbnailURL text default '';";
+ private static final String ADD_MEDIA_FILE_URL = "alter table media add fileURL text default '';";
+ private static final String ADD_MEDIA_UNIQUE_ID = "alter table media add mediaId text default '';";
+ private static final String ADD_MEDIA_BLOG_ID = "alter table media add blogId text default '';";
+ private static final String ADD_MEDIA_DATE_GMT = "alter table media add date_created_gmt date;";
+ private static final String ADD_MEDIA_UPLOAD_STATE = "alter table media add uploadState default '';";
+ private static final String ADD_MEDIA_VIDEOPRESS_SHORTCODE = "alter table media add videoPressShortcode text default '';";
+
+ // add hidden flag to blog settings (accounts)
+ private static final String ADD_BLOGS_HIDDEN_FLAG = "alter table accounts add isHidden boolean default 0;";
+
+ // add plan_product_id to blog
+ private static final String ADD_BLOGS_PLAN_ID = "alter table accounts add plan_product_id integer default 0;";
+
+ // add plan_product_name_short to blog
+ private static final String ADD_BLOGS_PLAN_PRODUCT_NAME_SHORT = "alter table accounts add plan_product_name_short text default '';";
+
+ // add capabilities to blog
+ private static final String ADD_BLOGS_CAPABILITIES = "alter table accounts add capabilities text default '';";
+
+ // used for migration
+ private static final String DEPRECATED_WPCOM_USERNAME_PREFERENCE = "wp_pref_wpcom_username";
+ private static final String DEPRECATED_ACCESS_TOKEN_PREFERENCE = "wp_pref_wpcom_access_token";
+
+ private static final String DROP_TABLE_PREFIX = "DROP TABLE IF EXISTS ";
+
+ private SQLiteDatabase db;
+
+ protected static final String PASSWORD_SECRET = BuildConfig.DB_SECRET;
+ private Context context;
+
+ public WordPressDB(Context ctx) {
+ this.context = ctx;
+ db = ctx.openOrCreateDatabase(DATABASE_NAME, 0, null);
+
+ // Create tables if they don't exist
+ db.execSQL(CREATE_TABLE_BLOGS);
+ db.execSQL(CREATE_TABLE_POSTS);
+ db.execSQL(CREATE_TABLE_CATEGORIES);
+ db.execSQL(CREATE_TABLE_QUICKPRESS_SHORTCUTS);
+ db.execSQL(CREATE_TABLE_MEDIA);
+ db.execSQL(CREATE_TABLE_THEMES);
+ SiteSettingsTable.createTable(db);
+ CommentTable.createTables(db);
+ SuggestionTable.createTables(db);
+
+ // Update tables for new installs and app updates
+ int currentVersion = db.getVersion();
+ boolean isNewInstall = (currentVersion == 0);
+
+ if (!isNewInstall && currentVersion != DATABASE_VERSION) {
+ AppLog.d(T.DB, "upgrading database from version " + currentVersion + " to " + DATABASE_VERSION);
+ }
+
+ switch (currentVersion) {
+ case 0:
+ // New install
+ currentVersion++;
+ case 1:
+ // Add columns that were added in very early releases, then move on to version 9
+ db.execSQL(ADD_BLOGID);
+ db.execSQL(UPDATE_BLOGID);
+ db.execSQL(ADD_LOCATION_FLAG);
+ db.execSQL(ADD_DOTCOM_USERNAME);
+ db.execSQL(ADD_DOTCOM_PASSWORD);
+ db.execSQL(ADD_API_KEY);
+ db.execSQL(ADD_API_BLOGID);
+ db.execSQL(ADD_DOTCOM_FLAG);
+ db.execSQL(ADD_WP_VERSION);
+ currentVersion = 9;
+ case 9:
+ db.execSQL(ADD_HTTPUSER);
+ db.execSQL(ADD_HTTPPASSWORD);
+ migratePasswords();
+ currentVersion++;
+ case 10:
+ db.delete(POSTS_TABLE, null, null);
+ db.execSQL(CREATE_TABLE_POSTS);
+ db.execSQL(ADD_POST_FORMATS);
+ currentVersion++;
+ case 11:
+ db.execSQL(ADD_SCALED_IMAGE);
+ db.execSQL(ADD_SCALED_IMAGE_IMG_WIDTH);
+ db.execSQL(ADD_LOCAL_POST_CHANGES);
+ currentVersion++;
+ case 12:
+ db.execSQL(ADD_FEATURED_IN_POST);
+ currentVersion++;
+ case 13:
+ db.execSQL(ADD_HOME_URL);
+ currentVersion++;
+ case 14:
+ db.execSQL(ADD_BLOG_OPTIONS);
+ currentVersion++;
+ case 15:
+ // No longer used (preferences migration)
+ currentVersion++;
+ case 16:
+ migrateWPComAccount();
+ currentVersion++;
+ case 17:
+ db.execSQL(ADD_PARENTID_IN_CATEGORIES);
+ currentVersion++;
+ case 18:
+ db.execSQL(ADD_BLOGS_ADMIN_FLAG);
+ db.execSQL(ADD_MEDIA_FILE_URL);
+ db.execSQL(ADD_MEDIA_THUMBNAIL_URL);
+ db.execSQL(ADD_MEDIA_UNIQUE_ID);
+ db.execSQL(ADD_MEDIA_BLOG_ID);
+ db.execSQL(ADD_MEDIA_DATE_GMT);
+ db.execSQL(ADD_MEDIA_UPLOAD_STATE);
+ currentVersion++;
+ case 19:
+ // revision 20: create table "notes"
+ currentVersion++;
+ case 20:
+ db.execSQL(ADD_BLOGS_HIDDEN_FLAG);
+ currentVersion++;
+ case 21:
+ db.execSQL(ADD_MEDIA_VIDEOPRESS_SHORTCODE);
+ currentVersion++;
+ // version 23 added CommentTable.java, version 24 changed the comment table schema
+ case 22:
+ currentVersion++;
+ case 23:
+ CommentTable.reset(db);
+ currentVersion++;
+ case 24:
+ currentVersion++;
+ case 25:
+ //ver 26 "virtually" remove columns 'lastCommentId' and 'runService' from the DB
+ //SQLite supports a limited subset of ALTER TABLE.
+ //The ALTER TABLE command in SQLite allows the user to rename a table or to add a new column to an existing table.
+ //It is not possible to rename a column, remove a column, or add or remove constraints from a table.
+ currentVersion++;
+ case 26:
+ // Drop the notes table, no longer needed with Simperium.
+ db.execSQL(DROP_TABLE_PREFIX + NOTES_TABLE);
+ currentVersion++;
+ case 27:
+ // versions prior to v4.5 added an "isUploading" column here, but that's no longer used
+ // so we don't bother to add it
+ currentVersion++;
+ case 28:
+ // Remove WordPress.com credentials
+ removeDotComCredentials();
+ currentVersion++;
+ case 29:
+ // Migrate WordPress.com token and infos to the DB
+ AccountTable.createTables(db);
+ if (!isNewInstall) {
+ migratePreferencesToAccountTable(context);
+ }
+ currentVersion++;
+ case 30:
+ // Fix big comments issue #2855
+ CommentTable.deleteBigComments(db);
+ currentVersion++;
+ case 31:
+ // add wp_post_thumbnail to posts table
+ db.execSQL(ADD_POST_THUMBNAIL);
+ currentVersion++;
+ case 32:
+ // add postid index and blogID index to posts table
+ db.execSQL(ADD_POST_ID_INDEX);
+ db.execSQL(ADD_BLOG_ID_INDEX);
+ currentVersion++;
+ case 33:
+ deleteUploadedLocalDrafts();
+ currentVersion++;
+ case 34:
+ AccountTable.migrationAddEmailAddressField(db);
+ currentVersion++;
+ case 35:
+ // Delete simperium DB - from 4.6 to 4.6.1
+ // Fix an issue when note id > MAX_INT
+ ctx.deleteDatabase("simperium-store");
+ currentVersion++;
+ case 36:
+ // Delete simperium DB again - from 4.6.1 to 4.7
+ // Fix a sync issue happening for users who have both wpios and wpandroid active clients
+ ctx.deleteDatabase("simperium-store");
+ currentVersion++;
+ case 37:
+ resetThemeTable();
+ currentVersion++;
+ case 38:
+ updateDotcomFlag();
+ currentVersion++;
+ case 39:
+ AccountTable.migrationAddFirstNameLastNameAboutMeFields(db);
+ currentVersion++;
+ case 40:
+ AccountTable.migrationAddDateFields(db);
+ currentVersion++;
+ case 41:
+ AccountTable.migrationAddAccountSettingsFields(db);
+ currentVersion++;
+ case 42:
+ db.execSQL(ADD_BLOGS_PLAN_ID);
+ currentVersion++;
+ case 43:
+ db.execSQL(ADD_BLOGS_PLAN_PRODUCT_NAME_SHORT);
+ currentVersion++;
+ case 44:
+ PeopleTable.createTables(db);
+ currentVersion++;
+ case 45:
+ db.execSQL(ADD_BLOGS_CAPABILITIES);
+ currentVersion++;
+ case 46:
+ AppPrefs.setVisualEditorAvailable(true);
+ AppPrefs.setVisualEditorEnabled(true);
+ currentVersion++;
+ case 47:
+ PeopleTable.reset(db);
+ currentVersion++;
+ case 48:
+ PeopleTable.createViewersTable(db);
+ currentVersion++;
+ }
+ db.setVersion(DATABASE_VERSION);
+ }
+
+ private void updateDotcomFlag() {
+ // Loop over all .com blogs in the app and check that are really hosted on wpcom
+ List<Map<String, Object>> allBlogs = getBlogsBy("dotcomFlag=1", null, 0, false);
+ for (Map<String, Object> blog : allBlogs) {
+ String xmlrpcURL = MapUtils.getMapStr(blog, "url");
+ if (!WPUrlUtils.isWordPressCom(xmlrpcURL)) {
+ // .org blog marked as .com. Fix it.
+ int blogID = MapUtils.getMapInt(blog, "id");
+ if (blogID > 0) {
+ ContentValues values = new ContentValues();
+ values.put("dotcomFlag", false); // Mark as .org blog
+ db.update(BLOGS_TABLE, values, "id=" + blogID, null);
+ }
+ }
+ }
+ }
+
+ /*
+ * v4.5 (db version 34) no longer uses the "uploaded" column, and it's no longer added to the
+ * db upon creation - however, earlier versions would set "uploaded=1" for local drafts after
+ * they were uploaded and then exclude these "uploaded local drafts" from the post list - so
+ * we must delete these posts to avoid having them appear (as dups) in the post list.
+ */
+ private void deleteUploadedLocalDrafts() {
+ try {
+ int numDeleted = db.delete(POSTS_TABLE, "uploaded=1 AND localDraft=1", null);
+ if (numDeleted > 0) {
+ AppLog.i(T.DB, "deleted " + numDeleted + " uploaded local drafts");
+ }
+ } catch (SQLiteException e) {
+ // ignore - "uploaded" column doesn't exist
+ }
+ }
+
+ private void resetThemeTable() {
+ db.execSQL(DROP_TABLE_PREFIX + THEMES_TABLE);
+ db.execSQL(CREATE_TABLE_THEMES);
+ }
+
+ private void migratePreferencesToAccountTable(Context context) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ String oldAccessToken = settings.getString(DEPRECATED_ACCESS_TOKEN_PREFERENCE, null);
+ String oldUsername = settings.getString(DEPRECATED_WPCOM_USERNAME_PREFERENCE, null);
+ Account account = new Account();
+ account.setUserName(oldUsername);
+ if (oldAccessToken != null) {
+ account.setAccessToken(oldAccessToken);
+ }
+ AccountTable.save(account, db);
+
+ // Remove preferences
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
+ editor.remove(DEPRECATED_WPCOM_USERNAME_PREFERENCE);
+ editor.remove(DEPRECATED_ACCESS_TOKEN_PREFERENCE);
+ editor.apply();
+ }
+
+ public SQLiteDatabase getDatabase() {
+ return db;
+ }
+
+ public static void deleteDatabase(Context ctx) {
+ ctx.deleteDatabase(DATABASE_NAME);
+ }
+
+ private void migrateWPComAccount() {
+ Cursor c = db.query(BLOGS_TABLE, new String[]{"username"}, "dotcomFlag=1", null, null,
+ null, null);
+
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ String username = c.getString(0);
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this.context);
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(DEPRECATED_WPCOM_USERNAME_PREFERENCE, username);
+ editor.commit();
+ }
+
+ c.close();
+ }
+
+ public boolean addBlog(Blog blog) {
+ ContentValues values = new ContentValues();
+ values.put("url", blog.getUrl());
+ values.put("homeURL", blog.getHomeURL());
+ values.put("blogName", blog.getBlogName());
+ values.put("username", blog.getUsername());
+ values.put("password", encryptPassword(blog.getPassword()));
+ values.put("httpuser", blog.getHttpuser());
+ values.put("httppassword", encryptPassword(blog.getHttppassword()));
+ values.put("imagePlacement", blog.getImagePlacement());
+ values.put("centerThumbnail", false);
+ values.put("fullSizeImage", false);
+ values.put("maxImageWidth", blog.getMaxImageWidth());
+ values.put("maxImageWidthId", blog.getMaxImageWidthId());
+ values.put("blogId", blog.getRemoteBlogId());
+ values.put("dotcomFlag", blog.isDotcomFlag());
+ values.put("plan_product_id", blog.getPlanID());
+ values.put("plan_product_name_short", blog.getPlanShortName());
+ if (blog.getWpVersion() != null) {
+ values.put("wpVersion", blog.getWpVersion());
+ } else {
+ values.putNull("wpVersion");
+ }
+ values.put("isAdmin", blog.isAdmin());
+ values.put("isHidden", blog.isHidden());
+ values.put("capabilities", blog.getCapabilities());
+ return db.insert(BLOGS_TABLE, null, values) > -1;
+ }
+
+ public List<Integer> getAllBlogsIDs() {
+ Cursor c = db.rawQuery("SELECT DISTINCT id FROM " + BLOGS_TABLE, null);
+ try {
+ List<Integer> ids = new ArrayList<Integer>();
+ if (c.moveToFirst()) {
+ do {
+ ids.add(c.getInt(0));
+ } while (c.moveToNext());
+ }
+ return ids;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public List<Map<String, Object>> getBlogsBy(String byString, String[] extraFields) {
+ return getBlogsBy(byString, extraFields, 0, true);
+ }
+
+ public List<Map<String, Object>> getBlogsBy(String byString, String[] extraFields,
+ int limit, boolean hideJetpackWithoutCredentials) {
+ if (db == null) {
+ return new Vector<>();
+ }
+
+ if (hideJetpackWithoutCredentials) {
+ // Hide Jetpack blogs that were added in FetchBlogListWPCom
+ // They will have a false dotcomFlag and an empty (but encrypted) password
+ String hideJetpackArgs = String.format("NOT(dotcomFlag=0 AND password='%s')", encryptPassword(""));
+ if (TextUtils.isEmpty(byString)) {
+ byString = hideJetpackArgs;
+ } else {
+ byString = hideJetpackArgs + " AND " + byString;
+ }
+ }
+
+ String limitStr = null;
+ if (limit != 0) {
+ limitStr = String.valueOf(limit);
+ }
+ String[] baseFields = new String[]{"id", "blogName", "username", "blogId", "url"};
+ String[] allFields = baseFields;
+ if (extraFields != null) {
+ allFields = (String[]) ArrayUtils.addAll(baseFields, extraFields);
+ }
+ Cursor c = db.query(BLOGS_TABLE, allFields, byString, null, null, null, null, limitStr);
+ int numRows = c.getCount();
+ c.moveToFirst();
+ List<Map<String, Object>> blogs = new Vector<>();
+ for (int i = 0; i < numRows; i++) {
+ int id = c.getInt(0);
+ String blogName = c.getString(1);
+ String username = c.getString(2);
+ int blogId = c.getInt(3);
+ String url = c.getString(4);
+ if (id > 0) {
+ Map<String, Object> blogMap = new HashMap<>();
+ blogMap.put("id", id);
+ blogMap.put("blogName", blogName);
+ blogMap.put("username", username);
+ blogMap.put("blogId", blogId);
+ blogMap.put("url", url);
+ int extraFieldsIndex = baseFields.length;
+ if (extraFields != null) {
+ for (int j = 0; j < extraFields.length; ++j) {
+ blogMap.put(extraFields[j], c.getString(extraFieldsIndex + j));
+ }
+ }
+ blogs.add(blogMap);
+ }
+ c.moveToNext();
+ }
+ c.close();
+ Collections.sort(blogs, BlogUtils.BlogNameComparator);
+ return blogs;
+ }
+
+ public List<Map<String, Object>> getVisibleBlogs() {
+ return getBlogsBy("isHidden = 0", null);
+ }
+
+ public int getFirstVisibleBlogId() {
+ return SqlUtils.intForQuery(db, "SELECT id FROM " + BLOGS_TABLE + " WHERE isHidden = 0 LIMIT 1", null);
+ }
+
+ public int getFirstHiddenBlogId() {
+ return SqlUtils.intForQuery(db, "SELECT id FROM " + BLOGS_TABLE + " WHERE isHidden = 1 LIMIT 1", null);
+ }
+
+ public List<Map<String, Object>> getVisibleDotComBlogs() {
+ return getBlogsBy("isHidden = 0 AND dotcomFlag = 1", null);
+ }
+
+ public int getNumVisibleBlogs() {
+ return SqlUtils.intForQuery(db, "SELECT COUNT(*) FROM " + BLOGS_TABLE + " WHERE isHidden = 0", null);
+ }
+
+ public int getNumHiddenBlogs() {
+ return SqlUtils.intForQuery(db, "SELECT COUNT(*) FROM " + BLOGS_TABLE + " WHERE isHidden = 1", null);
+ }
+
+ public int getNumDotComBlogs() {
+ return SqlUtils.intForQuery(db, "SELECT COUNT(*) FROM " + BLOGS_TABLE + " WHERE dotcomFlag = 1", null);
+ }
+
+ public int getNumBlogs() {
+ return SqlUtils.intForQuery(db, "SELECT COUNT(*) FROM " + BLOGS_TABLE, null);
+ }
+
+ // Removes stored DotCom credentials. As of March 2015 only the OAuth token is used
+ private void removeDotComCredentials() {
+ // First clear out the password for all WP.com sites
+ ContentValues dotComValues = new ContentValues();
+ dotComValues.put("password", "");
+ db.update(BLOGS_TABLE, dotComValues, "dotcomFlag=1", null);
+
+ // Next, we'll clear out the credentials stored for Jetpack sites
+ ContentValues jetPackValues = new ContentValues();
+ jetPackValues.put("dotcom_username", "");
+ jetPackValues.put("dotcom_password", "");
+ db.update(BLOGS_TABLE, jetPackValues, null, null);
+
+ // Lastly we'll remove the preference that previously stored the WP.com password
+ if (this.context != null) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this.context);
+ SharedPreferences.Editor editor = settings.edit();
+ editor.remove("wp_pref_wpcom_password");
+ editor.apply();
+ }
+ }
+
+ public List<Map<String, Object>> getAllBlogs() {
+ return getBlogsBy(null, null);
+ }
+
+ public int setAllDotComBlogsVisibility(boolean visible) {
+ ContentValues values = new ContentValues();
+ values.put("isHidden", !visible);
+ return db.update(BLOGS_TABLE, values, "dotcomFlag=1", null);
+ }
+
+ public int setDotComBlogsVisibility(int id, boolean visible) {
+ ContentValues values = new ContentValues();
+ values.put("isHidden", !visible);
+ return db.update(BLOGS_TABLE, values, "dotcomFlag=1 AND id=" + id, null);
+ }
+
+ public boolean isDotComBlogVisible(int blogId) {
+ String[] args = {Integer.toString(blogId)};
+ return SqlUtils.boolForQuery(db, "SELECT 1 FROM " + BLOGS_TABLE +
+ " WHERE isHidden = 0 AND blogId=?", args);
+ }
+
+ public boolean isBlogInDatabase(int blogId, String xmlRpcUrl) {
+ Cursor c = db.query(BLOGS_TABLE, new String[]{"id"}, "blogId=? AND url=?",
+ new String[]{Integer.toString(blogId), xmlRpcUrl}, null, null, null, null);
+ boolean result = c.getCount() > 0;
+ c.close();
+ return result;
+ }
+
+ public boolean isLocalBlogIdInDatabase(int localBlogId) {
+ String[] args = {Integer.toString(localBlogId)};
+ return SqlUtils.boolForQuery(db, "SELECT 1 FROM " + BLOGS_TABLE + " WHERE id=?", args);
+ }
+
+ public boolean saveBlog(Blog blog) {
+ if (blog.getLocalTableBlogId() == -1) {
+ return addBlog(blog);
+ }
+
+ ContentValues values = new ContentValues();
+ values.put("url", blog.getUrl());
+ values.put("homeURL", blog.getHomeURL());
+ values.put("username", blog.getUsername());
+ values.put("password", encryptPassword(blog.getPassword()));
+ values.put("httpuser", blog.getHttpuser());
+ values.put("httppassword", encryptPassword(blog.getHttppassword()));
+ values.put("imagePlacement", blog.getImagePlacement());
+ values.put("centerThumbnail", blog.isFeaturedImageCapable());
+ values.put("fullSizeImage", blog.isFullSizeImage());
+ values.put("maxImageWidth", blog.getMaxImageWidth());
+ values.put("maxImageWidthId", blog.getMaxImageWidthId());
+ values.put("postFormats", blog.getPostFormats());
+ values.put("dotcom_username", blog.getDotcom_username());
+ values.put("dotcom_password", encryptPassword(blog.getDotcom_password()));
+ values.put("api_blogid", blog.getApi_blogid());
+ values.put("api_key", blog.getApi_key());
+ values.put("isScaledImage", blog.isScaledImage());
+ values.put("scaledImgWidth", blog.getScaledImageWidth());
+ values.put("blog_options", blog.getBlogOptions());
+ values.put("isHidden", blog.isHidden());
+ values.put("blogName", blog.getBlogName());
+ values.put("isAdmin", blog.isAdmin());
+ values.put("isHidden", blog.isHidden());
+ values.put("plan_product_id", blog.getPlanID());
+ values.put("plan_product_name_short", blog.getPlanShortName());
+ values.put("capabilities", blog.getCapabilities());
+ if (blog.getWpVersion() != null) {
+ values.put("wpVersion", blog.getWpVersion());
+ } else {
+ values.putNull("wpVersion");
+ }
+ boolean returnValue = db.update(BLOGS_TABLE, values, "id=" + blog.getLocalTableBlogId(),
+ null) > 0;
+ if (blog.isDotcomFlag()) {
+ returnValue = updateWPComCredentials(blog.getUsername(), blog.getPassword());
+ }
+
+ updateCurrentBlog(blog);
+
+ return (returnValue);
+ }
+
+ public boolean updateWPComCredentials(String username, String password) {
+ // update the login for wordpress.com blogs
+ ContentValues userPass = new ContentValues();
+ userPass.put("username", username);
+ userPass.put("password", encryptPassword(password));
+ return db.update(BLOGS_TABLE, userPass, "username=\""
+ + username + "\" AND dotcomFlag=1", null) > 0;
+ }
+
+ public boolean deleteBlog(Context ctx, int id) {
+ int rowsAffected = db.delete(BLOGS_TABLE, "id=?", new String[]{Integer.toString(id)});
+ deleteQuickPressShortcutsForLocalTableBlogId(ctx, id);
+ deleteAllPostsForLocalTableBlogId(id);
+ PeopleTable.deletePeopleForLocalBlogId(id);
+ return (rowsAffected > 0);
+ }
+
+ public boolean deleteWordPressComBlogs(Context ctx) {
+ List<Map<String, Object>> wordPressComBlogs = getBlogsBy("isHidden = 0 AND dotcomFlag = 1", null);
+ for (Map<String, Object> blog : wordPressComBlogs) {
+ int localBlogId = MapUtils.getMapInt(blog, "id");
+ deleteQuickPressShortcutsForLocalTableBlogId(ctx, localBlogId);
+ deleteAllPostsForLocalTableBlogId(localBlogId);
+ PeopleTable.deletePeopleForLocalBlogId(localBlogId);
+ }
+
+ // H4ck alert: We need to delete the Jetpack sites that were added in the initial
+ // WP.com get blogs call. These sites will not have the dotcomFlag set and will
+ // have an empty password.
+ String args = String.format("dotcomFlag=1 OR (dotcomFlag=0 AND password='%s')", encryptPassword(""));
+
+ // Delete blogs
+ int rowsAffected = db.delete(BLOGS_TABLE, args, null);
+ return (rowsAffected > 0);
+ }
+
+ /**
+ * Deletes all the things! Use wisely.
+ */
+ public void dangerouslyDeleteAllContent() {
+ db.delete(BLOGS_TABLE, null, null);
+ db.delete(POSTS_TABLE, null, null);
+ db.delete(MEDIA_TABLE, null, null);
+ db.delete(CATEGORIES_TABLE, null, null);
+ db.delete(CommentTable.COMMENTS_TABLE, null, null);
+ }
+
+ public boolean hasDotOrgBlogForUsernameAndUrl(String username, String url) {
+ if (TextUtils.isEmpty(username) || TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ Cursor c = db.query(BLOGS_TABLE, new String[]{"id"}, "username=? AND url=?", new String[]{username, url}, null,
+ null, null);
+ try {
+ return c.getCount() > 0;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public boolean isCurrentUserAdminOfRemoteBlogId(long remoteBlogId) {
+ String args[] = {String.valueOf(remoteBlogId)};
+ String sql = "SELECT isAdmin FROM " + BLOGS_TABLE + " WHERE blogId=?";
+ return SqlUtils.boolForQuery(db, sql, args);
+ }
+
+ /**
+ * Instantiate a new Blog object from it's local id
+ *
+ * @param localId local blog id
+ * @return a new Blog instance or null if the localId was not found
+ */
+ public Blog instantiateBlogByLocalId(int localId) {
+ String[] fields =
+ new String[]{"url", "blogName", "username", "password", "httpuser", "httppassword", "imagePlacement",
+ "centerThumbnail", "fullSizeImage", "maxImageWidth", "maxImageWidthId",
+ "blogId", "dotcomFlag", "dotcom_username", "dotcom_password", "api_key",
+ "api_blogid", "wpVersion", "postFormats", "isScaledImage",
+ "scaledImgWidth", "homeURL", "blog_options", "isAdmin", "isHidden",
+ "plan_product_id", "plan_product_name_short", "capabilities"};
+ Cursor c = db.query(BLOGS_TABLE, fields, "id=?", new String[]{Integer.toString(localId)}, null, null, null);
+
+ Blog blog = null;
+ if (c.moveToFirst()) {
+ if (c.getString(0) != null) {
+ blog = new Blog();
+ blog.setLocalTableBlogId(localId);
+ blog.setUrl(c.getString(c.getColumnIndex("url"))); // 0
+
+ blog.setBlogName(c.getString(c.getColumnIndex("blogName"))); // 1
+ blog.setUsername(c.getString(c.getColumnIndex("username"))); // 2
+ blog.setPassword(decryptPassword(c.getString(c.getColumnIndex("password")))); // 3
+ if (c.getString(c.getColumnIndex("httpuser")) == null) {
+ blog.setHttpuser("");
+ } else {
+ blog.setHttpuser(c.getString(c.getColumnIndex("httpuser")));
+ }
+ if (c.getString(c.getColumnIndex("httppassword")) == null) {
+ blog.setHttppassword("");
+ } else {
+ blog.setHttppassword(decryptPassword(c.getString(c.getColumnIndex("httppassword"))));
+ }
+ blog.setImagePlacement(c.getString(c.getColumnIndex("imagePlacement")));
+ blog.setFeaturedImageCapable(c.getInt(c.getColumnIndex("centerThumbnail")) > 0);
+ blog.setFullSizeImage(c.getInt(c.getColumnIndex("fullSizeImage")) > 0);
+ blog.setMaxImageWidth(c.getString(c.getColumnIndex("maxImageWidth")));
+ blog.setMaxImageWidthId(c.getInt(c.getColumnIndex("maxImageWidthId")));
+ blog.setRemoteBlogId(c.getInt(c.getColumnIndex("blogId")));
+ blog.setDotcomFlag(c.getInt(c.getColumnIndex("dotcomFlag")) > 0);
+ if (c.getString(c.getColumnIndex("dotcom_username")) != null) {
+ blog.setDotcom_username(c.getString(c.getColumnIndex("dotcom_username")));
+ }
+ if (c.getString(c.getColumnIndex("dotcom_password")) != null) {
+ blog.setDotcom_password(decryptPassword(c.getString(c.getColumnIndex("dotcom_password"))));
+ }
+ if (c.getString(c.getColumnIndex("api_key")) != null) {
+ blog.setApi_key(c.getString(c.getColumnIndex("api_key")));
+ }
+ if (c.getString(c.getColumnIndex("api_blogid")) != null) {
+ blog.setApi_blogid(c.getString(c.getColumnIndex("api_blogid")));
+ }
+ if (c.getString(c.getColumnIndex("wpVersion")) != null) {
+ blog.setWpVersion(c.getString(c.getColumnIndex("wpVersion")));
+ }
+ blog.setPostFormats(c.getString(c.getColumnIndex("postFormats")));
+ blog.setScaledImage(c.getInt(c.getColumnIndex("isScaledImage")) > 0);
+ blog.setScaledImageWidth(c.getInt(c.getColumnIndex("scaledImgWidth")));
+ blog.setHomeURL(c.getString(c.getColumnIndex("homeURL")));
+ if (c.getString(c.getColumnIndex("blog_options")) == null) {
+ blog.setBlogOptions("{}");
+ } else {
+ blog.setBlogOptions(c.getString(c.getColumnIndex("blog_options")));
+ }
+ blog.setAdmin(c.getInt(c.getColumnIndex("isAdmin")) > 0);
+ blog.setHidden(c.getInt(c.getColumnIndex("isHidden")) > 0);
+ blog.setPlanID(c.getLong(c.getColumnIndex("plan_product_id")));
+ blog.setPlanShortName(c.getString(c.getColumnIndex("plan_product_name_short")));
+ blog.setCapabilities(c.getString(c.getColumnIndex("capabilities")));
+ }
+ }
+ c.close();
+ return blog;
+ }
+
+ /*
+ * returns true if the passed blog is wp.com or jetpack-enabled (ie: returns false for
+ * self-hosted blogs that don't use jetpack)
+ */
+ public boolean isRemoteBlogIdDotComOrJetpack(int remoteBlogId) {
+ int localId = getLocalTableBlogIdForRemoteBlogId(remoteBlogId);
+ Blog blog = instantiateBlogByLocalId(localId);
+ return blog != null && (blog.isDotcomFlag() || blog.isJetpackPowered());
+ }
+
+ public Blog getBlogForDotComBlogId(String dotComBlogId) {
+ Cursor c = db.query(BLOGS_TABLE, new String[]{"id"}, "api_blogid=? OR (blogId=? AND dotcomFlag=1)",
+ new String[]{dotComBlogId, dotComBlogId}, null, null, null);
+ Blog blog = null;
+ if (c.moveToFirst()) {
+ blog = instantiateBlogByLocalId(c.getInt(0));
+ }
+ c.close();
+ return blog;
+ }
+
+ public List<String> loadStatsLogin(int id) {
+ Cursor c = db.query(BLOGS_TABLE, new String[]{"dotcom_username",
+ "dotcom_password"}, "id=" + id, null, null, null, null);
+
+ c.moveToFirst();
+
+ List<String> returnVector = new Vector<String>();
+ if (c.getString(0) != null) {
+ returnVector.add(c.getString(0));
+ returnVector.add(decryptPassword(c.getString(1)));
+ } else {
+ returnVector = null;
+ }
+ c.close();
+
+ return returnVector;
+ }
+
+ /*
+ * Jetpack blogs have the "wpcom" blog_id stored in options->api_blogid. This is because self-hosted blogs have both
+ * a blogID (local to their network), and a unique blogID on wpcom.
+ */
+ public int getLocalTableBlogIdForJetpackRemoteID(int remoteBlogId, String xmlRpcUrl) {
+ if (TextUtils.isEmpty(xmlRpcUrl)) {
+ String sql = "SELECT id FROM " + BLOGS_TABLE + " WHERE dotcomFlag=0 AND api_blogid=?";
+ String[] args = {Integer.toString(remoteBlogId)};
+ return SqlUtils.intForQuery(db, sql, args);
+ } else {
+ String sql = "SELECT id FROM " + BLOGS_TABLE + " WHERE dotcomFlag=0 AND api_blogid=? AND url=?";
+ String[] args = {Integer.toString(remoteBlogId), xmlRpcUrl};
+ return SqlUtils.intForQuery(db, sql, args);
+ }
+ }
+
+ public int getLocalTableBlogIdForRemoteBlogId(int remoteBlogId) {
+ int localBlogID = SqlUtils.intForQuery(db, "SELECT id FROM accounts WHERE blogId=?",
+ new String[]{Integer.toString(remoteBlogId)});
+ if (localBlogID == 0) {
+ localBlogID = this.getLocalTableBlogIdForJetpackRemoteID(remoteBlogId, null);
+ }
+ return localBlogID;
+ }
+
+ public int getLocalTableBlogIdForRemoteBlogIdAndXmlRpcUrl(int remoteBlogId, String xmlRpcUrl) {
+ int localBlogID = SqlUtils.intForQuery(db, "SELECT id FROM accounts WHERE blogId=? AND url=?",
+ new String[]{Integer.toString(remoteBlogId), xmlRpcUrl});
+ if (localBlogID==0) {
+ localBlogID = this.getLocalTableBlogIdForJetpackRemoteID(remoteBlogId, xmlRpcUrl);
+ }
+ return localBlogID;
+ }
+
+ public int getRemoteBlogIdForLocalTableBlogId(int localBlogId) {
+ int remoteBlogID = SqlUtils.intForQuery(db, "SELECT blogId FROM accounts WHERE id=?", new String[]{Integer.toString(localBlogId)});
+ if (remoteBlogID<=1) { //Make sure we're not returning a wrong ID for jetpack blog.
+ List<Map<String,Object>> allBlogs = this.getBlogsBy("dotcomFlag=0", new String[]{"api_blogid"});
+ for (Map<String, Object> currentBlog : allBlogs) {
+ if (MapUtils.getMapInt(currentBlog, "id")==localBlogId) {
+ remoteBlogID = MapUtils.getMapInt(currentBlog, "api_blogid");
+ break;
+ }
+ }
+ }
+ return remoteBlogID;
+ }
+
+ public long getPlanIdForLocalTableBlogId(int localBlogId) {
+ return SqlUtils.longForQuery(db,
+ "SELECT plan_product_id FROM accounts WHERE id=?",
+ new String[]{Integer.toString(localBlogId)});
+ }
+ /**
+ * Set the ID of the most recently active blog. This value will persist between application
+ * launches.
+ *
+ * @param id ID of the most recently active blog.
+ */
+ public void updateLastBlogId(int id) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putInt("last_blog_id", id);
+ editor.commit();
+ }
+
+ /**
+ * Delete the ID for the most recently active blog.
+ */
+ public void deleteLastBlogId() {
+ updateLastBlogId(-1);
+ // Clear the last selected activity
+ AppPrefs.resetLastActivityStr();
+ }
+
+ /**
+ * Get the ID of the most recently active blog. -1 is returned if there is no recently active
+ * blog.
+ */
+ public int getLastBlogId() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getInt("last_blog_id", -1);
+ }
+
+ public boolean deletePost(Post post) {
+ int result = db.delete(POSTS_TABLE,
+ "blogID=? AND id=?",
+ new String[]{String.valueOf(post.getLocalTableBlogId()), String.valueOf(post.getLocalTablePostId())});
+
+ return (result == 1);
+ }
+
+ // Deletes all posts for the given blogId
+ public void deleteAllPostsForLocalTableBlogId(int localBlogId) {
+ db.delete(POSTS_TABLE, "blogID=?", new String[]{String.valueOf(localBlogId)});
+ }
+
+ public Object[] arrayListToArray(Object array) {
+ if (array instanceof ArrayList) {
+ return ((ArrayList) array).toArray();
+ }
+ return (Object[]) array;
+ }
+
+ /*
+ * returns true if the post matching the passed local blog ID and remote post ID
+ * has local changes
+ */
+ private boolean postHasLocalChanges(int localBlogId, String remotePostId) {
+ if (TextUtils.isEmpty(remotePostId)) {
+ return false;
+ }
+ String[] args = {String.valueOf(localBlogId), remotePostId};
+ String sql = "SELECT 1 FROM " + POSTS_TABLE + " WHERE blogID=? AND postid=? AND isLocalChange=1";
+ return SqlUtils.boolForQuery(db, sql, args);
+ }
+
+ /**
+ * Saves a list of posts to the db
+ * @param postsList: list of post objects
+ * @param localBlogId: the posts table blog id
+ * @param isPage: boolean to save as pages
+ * @param overwriteLocalChanges boolean which determines whether to overwrite posts with local changes
+ */
+ public void savePosts(List<?> postsList, int localBlogId, boolean isPage, boolean overwriteLocalChanges) {
+ if (postsList != null && postsList.size() != 0) {
+ db.beginTransaction();
+ try {
+ for (Object post : postsList) {
+ ContentValues values = new ContentValues();
+
+ // Sanity checks
+ if (!(post instanceof Map)) {
+ continue;
+ }
+ Map<?, ?> postMap = (Map<?, ?>) post;
+ String postID = MapUtils.getMapStr(postMap, (isPage) ? "page_id" : "postid");
+ if (TextUtils.isEmpty(postID)) {
+ // If we don't have a post or page ID, move on
+ continue;
+ }
+
+ values.put("blogID", localBlogId);
+ values.put("postid", postID);
+ values.put("title", MapUtils.getMapStr(postMap, "title"));
+ Date dateCreated = MapUtils.getMapDate(postMap, "dateCreated");
+ if (dateCreated != null) {
+ values.put("dateCreated", dateCreated.getTime());
+ } else {
+ Date now = new Date();
+ values.put("dateCreated", now.getTime());
+ }
+
+ Date dateCreatedGmt = MapUtils.getMapDate(postMap, "date_created_gmt");
+ if (dateCreatedGmt != null) {
+ values.put("date_created_gmt", dateCreatedGmt.getTime());
+ } else {
+ dateCreatedGmt = new Date((Long) values.get("dateCreated"));
+ values.put("date_created_gmt", dateCreatedGmt.getTime() + (dateCreatedGmt.getTimezoneOffset() * 60000));
+ }
+
+ values.put("description", MapUtils.getMapStr(postMap, "description"));
+ values.put("link", MapUtils.getMapStr(postMap, "link"));
+ values.put("permaLink", MapUtils.getMapStr(postMap, "permaLink"));
+
+ Object[] postCategories = (Object[]) postMap.get("categories");
+ JSONArray jsonCategoriesArray = new JSONArray();
+ if (postCategories != null) {
+ for (Object postCategory : postCategories) {
+ jsonCategoriesArray.put(postCategory.toString());
+ }
+ }
+ values.put("categories", jsonCategoriesArray.toString());
+
+ Object[] custom_fields = (Object[]) postMap.get("custom_fields");
+ JSONArray jsonCustomFieldsArray = new JSONArray();
+ if (custom_fields != null) {
+ for (Object custom_field : custom_fields) {
+ jsonCustomFieldsArray.put(custom_field.toString());
+ // Update geo_long and geo_lat from custom fields
+ if (!(custom_field instanceof Map))
+ continue;
+ Map<?, ?> customField = (Map<?, ?>) custom_field;
+ if (customField.get("key") != null && customField.get("value") != null) {
+ if (customField.get("key").equals("geo_longitude"))
+ values.put("longitude", customField.get("value").toString());
+ if (customField.get("key").equals("geo_latitude"))
+ values.put("latitude", customField.get("value").toString());
+ }
+ }
+ }
+ values.put("custom_fields", jsonCustomFieldsArray.toString());
+
+ values.put("mt_excerpt", MapUtils.getMapStr(postMap, (isPage) ? "excerpt" : "mt_excerpt"));
+ values.put("mt_text_more", MapUtils.getMapStr(postMap, (isPage) ? "text_more" : "mt_text_more"));
+ values.put("mt_allow_comments", MapUtils.getMapInt(postMap, "mt_allow_comments", 0));
+ values.put("mt_allow_pings", MapUtils.getMapInt(postMap, "mt_allow_pings", 0));
+ values.put("wp_slug", MapUtils.getMapStr(postMap, "wp_slug"));
+ values.put("wp_password", MapUtils.getMapStr(postMap, "wp_password"));
+ values.put("wp_author_id", MapUtils.getMapStr(postMap, "wp_author_id"));
+ values.put("wp_author_display_name", MapUtils.getMapStr(postMap, "wp_author_display_name"));
+ values.put("wp_post_thumbnail", MapUtils.getMapInt(postMap, "wp_post_thumbnail"));
+ values.put("post_status", MapUtils.getMapStr(postMap, (isPage) ? "page_status" : "post_status"));
+ values.put("userid", MapUtils.getMapStr(postMap, "userid"));
+
+ if (isPage) {
+ values.put("isPage", true);
+ values.put("wp_page_parent_id", MapUtils.getMapStr(postMap, "wp_page_parent_id"));
+ values.put("wp_page_parent_title", MapUtils.getMapStr(postMap, "wp_page_parent_title"));
+ } else {
+ values.put("mt_keywords", MapUtils.getMapStr(postMap, "mt_keywords"));
+ values.put("wp_post_format", MapUtils.getMapStr(postMap, "wp_post_format"));
+ }
+
+ if (overwriteLocalChanges) {
+ values.put("isLocalChange", false);
+ }
+
+ String whereClause = "blogID=? AND postID=? AND isPage=?";
+ if (!overwriteLocalChanges) {
+ whereClause += " AND NOT isLocalChange=1";
+ }
+
+ String[] args = {String.valueOf(localBlogId), postID, String.valueOf(SqlUtils.boolToSql(isPage))};
+ int updateResult = db.update(POSTS_TABLE, values, whereClause, args);
+
+ // only perform insert if update didn't match any rows, and only then if we're
+ // overwriting local changes or local changes for this post don't exist
+ if (updateResult == 0 && (overwriteLocalChanges || !postHasLocalChanges(localBlogId, postID))) {
+ db.insert(POSTS_TABLE, null, values);
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+ }
+
+ /*
+ * returns list of posts for use in the post list fragment
+ */
+ public PostsListPostList getPostsListPosts(int localBlogId, boolean loadPages) {
+ PostsListPostList listPosts = new PostsListPostList();
+
+ String[] args = {Integer.toString(localBlogId), Integer.toString(loadPages ? 1 : 0)};
+ Cursor c = db.query(POSTS_TABLE, null, "blogID=? AND isPage=?", args, null, null, "localDraft DESC, date_created_gmt DESC");
+ try {
+ while (c.moveToNext()) {
+ listPosts.add(new PostsListPost(getPostFromCursor(c)));
+ }
+ return listPosts;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ private Post getPostFromCursor(Cursor c) {
+ Post post = new Post();
+
+ post.setLocalTableBlogId(c.getInt(c.getColumnIndex("blogID")));
+ post.setLocalTablePostId(c.getLong(c.getColumnIndex("id")));
+ post.setRemotePostId(c.getString(c.getColumnIndex("postid")));
+ post.setTitle(StringUtils.unescapeHTML(c.getString(c.getColumnIndex("title"))));
+ post.setDateCreated(c.getLong(c.getColumnIndex("dateCreated")));
+ post.setDate_created_gmt(c.getLong(c.getColumnIndex("date_created_gmt")));
+ post.setCategories(c.getString(c.getColumnIndex("categories")));
+ post.setCustomFields(c.getString(c.getColumnIndex("custom_fields")));
+ post.setDescription(c.getString(c.getColumnIndex("description")));
+ post.setLink(c.getString(c.getColumnIndex("link")));
+ post.setAllowComments(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("mt_allow_comments"))));
+ post.setAllowPings(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("mt_allow_pings"))));
+ post.setPostExcerpt(c.getString(c.getColumnIndex("mt_excerpt")));
+ post.setKeywords(c.getString(c.getColumnIndex("mt_keywords")));
+ post.setMoreText(c.getString(c.getColumnIndex("mt_text_more")));
+ post.setPermaLink(c.getString(c.getColumnIndex("permaLink")));
+ post.setPostStatus(c.getString(c.getColumnIndex("post_status")));
+ post.setUserId(c.getString(c.getColumnIndex("userid")));
+ post.setAuthorDisplayName(c.getString(c.getColumnIndex("wp_author_display_name")));
+ post.setAuthorId(c.getString(c.getColumnIndex("wp_author_id")));
+ post.setPassword(c.getString(c.getColumnIndex("wp_password")));
+ post.setPostFormat(c.getString(c.getColumnIndex("wp_post_format")));
+ post.setSlug(c.getString(c.getColumnIndex("wp_slug")));
+ post.setMediaPaths(c.getString(c.getColumnIndex("mediaPaths")));
+ post.setFeaturedImageId(c.getInt(c.getColumnIndex("wp_post_thumbnail")));
+
+ int latColumnIndex = c.getColumnIndex("latitude");
+ int lngColumnIndex = c.getColumnIndex("longitude");
+ if (!c.isNull(latColumnIndex) && !c.isNull(lngColumnIndex)) {
+ post.setLocation(c.getDouble(latColumnIndex), c.getDouble(lngColumnIndex));
+ }
+
+ post.setLocalDraft(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("localDraft"))));
+ post.setIsPage(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("isPage"))));
+ post.setPageParentId(c.getString(c.getColumnIndex("wp_page_parent_id")));
+ post.setPageParentTitle(c.getString(c.getColumnIndex("wp_page_parent_title")));
+ post.setLocalChange(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("isLocalChange"))));
+
+ return post;
+ }
+
+ public long savePost(Post post) {
+ long result = -1;
+ if (post != null) {
+ ContentValues values = new ContentValues();
+ values.put("blogID", post.getLocalTableBlogId());
+ values.put("title", post.getTitle());
+ values.put("date_created_gmt", post.getDate_created_gmt());
+ values.put("description", post.getDescription());
+ values.put("mt_text_more", post.getMoreText());
+
+ JSONArray categoriesJsonArray = post.getJSONCategories();
+ if (categoriesJsonArray != null) {
+ values.put("categories", categoriesJsonArray.toString());
+ }
+
+ values.put("localDraft", post.isLocalDraft());
+ values.put("mt_keywords", post.getKeywords());
+ values.put("wp_password", post.getPassword());
+ values.put("post_status", post.getPostStatus());
+ values.put("isPage", post.isPage());
+ values.put("wp_post_format", post.getPostFormat());
+ putPostLocation(post, values);
+ values.put("isLocalChange", post.isLocalChange());
+ values.put("mt_excerpt", post.getPostExcerpt());
+ values.put("wp_post_thumbnail", post.getFeaturedImageId());
+
+ result = db.insert(POSTS_TABLE, null, values);
+
+ if (result >= 0 && post.isLocalDraft()) {
+ post.setLocalTablePostId(result);
+ }
+ }
+
+ return (result);
+ }
+
+ public int updatePost(Post post) {
+ int result = 0;
+ if (post != null) {
+ ContentValues values = new ContentValues();
+ values.put("title", post.getTitle());
+ values.put("date_created_gmt", post.getDate_created_gmt());
+ values.put("description", post.getDescription());
+ values.put("mt_text_more", post.getMoreText());
+ values.put("postid", post.getRemotePostId());
+
+ JSONArray categoriesJsonArray = post.getJSONCategories();
+ if (categoriesJsonArray != null) {
+ values.put("categories", categoriesJsonArray.toString());
+ }
+
+ values.put("localDraft", post.isLocalDraft());
+ values.put("mediaPaths", post.getMediaPaths());
+ values.put("mt_keywords", post.getKeywords());
+ values.put("wp_password", post.getPassword());
+ values.put("post_status", post.getPostStatus());
+ values.put("isPage", post.isPage());
+ values.put("wp_post_format", post.getPostFormat());
+ values.put("isLocalChange", post.isLocalChange());
+ values.put("mt_excerpt", post.getPostExcerpt());
+ values.put("wp_post_thumbnail", post.getFeaturedImageId());
+
+ putPostLocation(post, values);
+
+ result = db.update(POSTS_TABLE, values, "blogID=? AND id=? AND isPage=?",
+ new String[]{
+ String.valueOf(post.getLocalTableBlogId()),
+ String.valueOf(post.getLocalTablePostId()),
+ String.valueOf(SqlUtils.boolToSql(post.isPage()))
+ });
+ }
+
+ return (result);
+ }
+
+ private void putPostLocation(Post post, ContentValues values) {
+ if (post.hasLocation()) {
+ PostLocation location = post.getLocation();
+ values.put("latitude", location.getLatitude());
+ values.put("longitude", location.getLongitude());
+ } else {
+ values.putNull("latitude");
+ values.putNull("longitude");
+ }
+ }
+
+ /*
+ * removes all posts/pages in the passed blog that don't have local changes
+ */
+ public void deleteUploadedPosts(int blogID, boolean isPage) {
+ String[] args = {String.valueOf(blogID), isPage ? "1" : "0"};
+ db.delete(POSTS_TABLE, "blogID=? AND isPage=? AND localDraft=0 AND isLocalChange=0", args);
+ }
+
+ public Post getPostForLocalTablePostId(long localTablePostId) {
+ Cursor c = db.query(POSTS_TABLE, null, "id=?", new String[]{String.valueOf(localTablePostId)}, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ return getPostFromCursor(c);
+ } else {
+ return null;
+ }
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ // Categories
+ public boolean insertCategory(int id, int wp_id, int parent_id, String category_name) {
+ ContentValues values = new ContentValues();
+ values.put("blog_id", id);
+ values.put("wp_id", wp_id);
+ values.put("category_name", category_name.toString());
+ values.put("parent_id", parent_id);
+ boolean returnValue = false;
+ synchronized (this) {
+ returnValue = db.insert(CATEGORIES_TABLE, null, values) > 0;
+ }
+
+ return (returnValue);
+ }
+
+ public List<String> loadCategories(int id) {
+ Cursor c = db.query(CATEGORIES_TABLE, new String[] { "id", "wp_id",
+ "category_name" }, "blog_id=" + Integer.toString(id), null, null, null, null);
+ int numRows = c.getCount();
+ c.moveToFirst();
+ List<String> returnVector = new Vector<String>();
+ for (int i = 0; i < numRows; ++i) {
+ String category_name = c.getString(2);
+ if (category_name != null) {
+ returnVector.add(category_name);
+ }
+ c.moveToNext();
+ }
+ c.close();
+
+ return returnVector;
+ }
+
+ public int getCategoryId(int id, String category) {
+ Cursor c = db.query(CATEGORIES_TABLE, new String[] { "wp_id" },
+ "category_name=? AND blog_id=?", new String[] {category, String.valueOf(id)},
+ null, null, null);
+ if (c.getCount() == 0)
+ return 0;
+ c.moveToFirst();
+ int categoryID = 0;
+ categoryID = c.getInt(0);
+
+ c.close();
+
+ return categoryID;
+ }
+
+ public int getCategoryParentId(int id, String category) {
+ Cursor c = db.query(CATEGORIES_TABLE, new String[] { "parent_id" },
+ "category_name=? AND blog_id=?", new String[] {category, String.valueOf(id)},
+ null, null, null);
+ if (c.getCount() == 0)
+ return -1;
+ c.moveToFirst();
+ int categoryParentID = c.getInt(0);
+
+ c.close();
+
+ return categoryParentID;
+ }
+
+ public void clearCategories(int id) {
+ // clear out the table since we are refreshing the whole enchilada
+ db.delete(CATEGORIES_TABLE, "blog_id=" + id, null);
+
+ }
+
+ public boolean addQuickPressShortcut(int blogId, String name) {
+ ContentValues values = new ContentValues();
+ values.put("accountId", blogId);
+ values.put("name", name);
+ boolean returnValue = false;
+ synchronized (this) {
+ returnValue = db.insert(QUICKPRESS_SHORTCUTS_TABLE, null, values) > 0;
+ }
+
+ return (returnValue);
+ }
+
+ /*
+ * return all QuickPress shortcuts connected with the passed blog
+ *
+ */
+ public List<Map<String, Object>> getQuickPressShortcuts(int blogId) {
+ Cursor c = db.query(QUICKPRESS_SHORTCUTS_TABLE, new String[]{"id",
+ "accountId", "name"}, "accountId = " + blogId, null, null,
+ null, null);
+ String id, name;
+ int numRows = c.getCount();
+ c.moveToFirst();
+ List<Map<String, Object>> blogs = new Vector<Map<String, Object>>();
+ for (int i = 0; i < numRows; i++) {
+ id = c.getString(0);
+ name = c.getString(2);
+ if (id != null) {
+ Map<String, Object> thisHash = new HashMap<String, Object>();
+
+ thisHash.put("id", id);
+ thisHash.put("name", name);
+ blogs.add(thisHash);
+ }
+ c.moveToNext();
+ }
+ c.close();
+
+ return blogs;
+ }
+
+ /*
+ * delete QuickPress home screen shortcuts connected with the passed blog
+ */
+ private void deleteQuickPressShortcutsForLocalTableBlogId(Context ctx, int blogId) {
+ List<Map<String, Object>> shortcuts = getQuickPressShortcuts(blogId);
+ if (shortcuts.size() == 0)
+ return;
+
+ for (int i = 0; i < shortcuts.size(); i++) {
+ Map<String, Object> shortcutHash = shortcuts.get(i);
+
+ Intent shortcutIntent = new Intent(WordPress.getContext(), EditPostActivity.class);
+ shortcutIntent.setAction(Intent.ACTION_MAIN);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ Intent broadcastShortcutIntent = new Intent();
+ broadcastShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT,
+ shortcutIntent);
+ broadcastShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME,
+ shortcutHash.get("name").toString());
+ broadcastShortcutIntent.putExtra("duplicate", false);
+ broadcastShortcutIntent
+ .setAction("com.android.launcher.action.UNINSTALL_SHORTCUT");
+ ctx.sendBroadcast(broadcastShortcutIntent);
+
+ // remove from shortcuts table
+ String shortcutId = shortcutHash.get("id").toString();
+ db.delete(QUICKPRESS_SHORTCUTS_TABLE, "id=?", new String[]{shortcutId});
+ }
+ }
+
+ public static String encryptPassword(String clearText) {
+ try {
+ DESKeySpec keySpec = new DESKeySpec(
+ PASSWORD_SECRET.getBytes("UTF-8"));
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+ SecretKey key = keyFactory.generateSecret(keySpec);
+
+ Cipher cipher = Cipher.getInstance("DES");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ String encrypedPwd = Base64.encodeToString(cipher.doFinal(clearText
+ .getBytes("UTF-8")), Base64.DEFAULT);
+ return encrypedPwd;
+ } catch (Exception e) {
+ }
+ return clearText;
+ }
+
+ public static String decryptPassword(String encryptedPwd) {
+ try {
+ DESKeySpec keySpec = new DESKeySpec(
+ PASSWORD_SECRET.getBytes("UTF-8"));
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+ SecretKey key = keyFactory.generateSecret(keySpec);
+
+ byte[] encryptedWithoutB64 = Base64.decode(encryptedPwd, Base64.DEFAULT);
+ Cipher cipher = Cipher.getInstance("DES");
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ byte[] plainTextPwdBytes = cipher.doFinal(encryptedWithoutB64);
+ return new String(plainTextPwdBytes);
+ } catch (Exception e) {
+ }
+ return encryptedPwd;
+ }
+
+ private void migratePasswords() {
+ Cursor c = db.query(BLOGS_TABLE, new String[] { "id", "password",
+ "httppassword", "dotcom_password" }, null, null, null, null,
+ null);
+ int numRows = c.getCount();
+ c.moveToFirst();
+
+ for (int i = 0; i < numRows; i++) {
+ ContentValues values = new ContentValues();
+
+ if (c.getString(1) != null) {
+ values.put("password", encryptPassword(c.getString(1)));
+ }
+ if (c.getString(2) != null) {
+ values.put("httppassword", encryptPassword(c.getString(2)));
+ }
+ if (c.getString(3) != null) {
+ values.put("dotcom_password", encryptPassword(c.getString(3)));
+ }
+
+ db.update(BLOGS_TABLE, values, "id=" + c.getInt(0), null);
+
+ c.moveToNext();
+ }
+ c.close();
+ }
+
+ public int getUnmoderatedCommentCount(int blogID) {
+ int commentCount = 0;
+
+ Cursor c = db
+ .rawQuery(
+ "select count(*) from comments where blogID=? AND status='hold'",
+ new String[]{String.valueOf(blogID)});
+ int numRows = c.getCount();
+ c.moveToFirst();
+
+ if (numRows > 0) {
+ commentCount = c.getInt(0);
+ }
+
+ c.close();
+
+ return commentCount;
+ }
+
+ public void saveMediaFile(MediaFile mf) {
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_NAME_POST_ID, mf.getPostID());
+ values.put(COLUMN_NAME_FILE_PATH, mf.getFilePath());
+ values.put(COLUMN_NAME_FILE_NAME, mf.getFileName());
+ values.put(COLUMN_NAME_TITLE, mf.getTitle());
+ values.put(COLUMN_NAME_DESCRIPTION, mf.getDescription());
+ values.put(COLUMN_NAME_CAPTION, mf.getCaption());
+ values.put(COLUMN_NAME_HORIZONTAL_ALIGNMENT, mf.getHorizontalAlignment());
+ values.put(COLUMN_NAME_WIDTH, mf.getWidth());
+ values.put(COLUMN_NAME_HEIGHT, mf.getHeight());
+ values.put(COLUMN_NAME_MIME_TYPE, mf.getMimeType());
+ values.put(COLUMN_NAME_FEATURED, mf.isFeatured());
+ values.put(COLUMN_NAME_IS_VIDEO, mf.isVideo());
+ values.put(COLUMN_NAME_IS_FEATURED_IN_POST, mf.isFeaturedInPost());
+ values.put(COLUMN_NAME_FILE_URL, mf.getFileURL());
+ values.put(COLUMN_NAME_THUMBNAIL_URL, mf.getThumbnailURL());
+ values.put(COLUMN_NAME_MEDIA_ID, mf.getMediaId());
+ values.put(COLUMN_NAME_BLOG_ID, mf.getBlogId());
+ values.put(COLUMN_NAME_DATE_CREATED_GMT, mf.getDateCreatedGMT());
+ values.put(COLUMN_NAME_VIDEO_PRESS_SHORTCODE, mf.getVideoPressShortCode());
+ if (mf.getUploadState() != null)
+ values.put(COLUMN_NAME_UPLOAD_STATE, mf.getUploadState());
+ else
+ values.putNull(COLUMN_NAME_UPLOAD_STATE);
+
+ synchronized (this) {
+ int result = 0;
+ boolean isMarkedForDelete = false;
+ if (mf.getMediaId() != null) {
+ Cursor cursor = db.rawQuery("SELECT uploadState FROM " + MEDIA_TABLE + " WHERE mediaId=?",
+ new String[]{StringUtils.notNullStr(mf.getMediaId())});
+ if (cursor != null && cursor.moveToFirst()) {
+ isMarkedForDelete = "delete".equals(cursor.getString(0));
+ cursor.close();
+ }
+
+ if (!isMarkedForDelete)
+ result = db.update(MEDIA_TABLE, values, "blogId=? AND mediaId=?",
+ new String[]{StringUtils.notNullStr(mf.getBlogId()), StringUtils.notNullStr(mf.getMediaId())});
+ }
+
+ if (result == 0 && !isMarkedForDelete) {
+ result = db.update(MEDIA_TABLE, values, "postID=? AND filePath=?",
+ new String[]{String.valueOf(mf.getPostID()), StringUtils.notNullStr(mf.getFilePath())});
+ if (result == 0)
+ db.insert(MEDIA_TABLE, null, values);
+ }
+ }
+
+ }
+
+ /** For a given blogId, get the first media files **/
+ public Cursor getFirstMediaFileForBlog(String blogId) {
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND " +
+ "(uploadState IS NULL OR uploadState IN ('uploaded', 'queued', 'failed', 'uploading')) ORDER BY (uploadState=?) DESC, date_created_gmt DESC LIMIT 1",
+ new String[]{blogId, "uploading"});
+ }
+
+ /** For a given blogId, get all the media files **/
+ public Cursor getMediaFilesForBlog(String blogId) {
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND "
+ + "(uploadState IS NULL OR uploadState IN ('uploaded', 'queued', 'failed', 'uploading')) ORDER BY (uploadState=?) DESC, date_created_gmt DESC", new String[] { blogId, "uploading" });
+ }
+
+ /** For a given blogId, get all the media files with searchTerm **/
+ public Cursor getMediaFilesForBlog(String blogId, String searchTerm) {
+ // Currently on WordPress.com, the media search engine only searches the title.
+ // We'll match this.
+
+ String term = searchTerm.toLowerCase(LanguageUtils.getCurrentDeviceLanguage(WordPress.getContext()));
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND title LIKE ? AND (uploadState IS NULL OR uploadState ='uploaded') ORDER BY (uploadState=?) DESC, date_created_gmt DESC", new String[]{blogId, "%" + term + "%", "uploading"});
+ }
+
+ /** For a given blogId, get the media file with the given media_id **/
+ public Cursor getMediaFile(String blogId, String mediaId) {
+ return db.rawQuery("SELECT * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId=?", new String[]{blogId, mediaId});
+ }
+
+
+ /**
+ * Given a VideoPress id, returns the corresponding remote video URL stored in the DB
+ */
+ public String getMediaUrlByVideoPressId(String blogId, String videoId) {
+ if (TextUtils.isEmpty(blogId) || TextUtils.isEmpty(videoId)) {
+ return "";
+ }
+
+ String shortcode = ShortcodeUtils.getVideoPressShortcodeFromId(videoId);
+
+ String query = "SELECT " + COLUMN_NAME_FILE_URL + " FROM " + MEDIA_TABLE + " WHERE blogId=? AND videoPressShortcode=?";
+ return SqlUtils.stringForQuery(db, query, new String[]{blogId, shortcode});
+ }
+
+ public String getMediaThumbnailUrl(int blogId, long mediaId) {
+ String query = "SELECT " + COLUMN_NAME_THUMBNAIL_URL + " FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId=?";
+ return SqlUtils.stringForQuery(db, query, new String[]{Integer.toString(blogId), Long.toString(mediaId)});
+ }
+
+ public int getMediaCountAll(String blogId) {
+ Cursor cursor = getMediaFilesForBlog(blogId);
+ int count = cursor.getCount();
+ cursor.close();
+ return count;
+ }
+
+ public boolean mediaFileExists(String blogId, String mediaId) {
+ return SqlUtils.boolForQuery(db, "SELECT 1 FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId=?",
+ new String[]{blogId, mediaId});
+ }
+
+ public Cursor getMediaImagesForBlog(String blogId) {
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND "
+ + "(uploadState IS NULL OR uploadState IN ('uploaded', 'queued', 'failed', 'uploading')) AND mimeType LIKE ? ORDER BY (uploadState=?) DESC, date_created_gmt DESC", new String[]{blogId, "image%", "uploading"});
+ }
+
+ /** Ids in the filteredIds will not be selected **/
+ public Cursor getMediaImagesForBlog(String blogId, ArrayList<String> filteredIds) {
+ String mediaIdsStr = "";
+
+ if (filteredIds != null && filteredIds.size() > 0) {
+ mediaIdsStr = "AND mediaId NOT IN (";
+ for (String mediaId : filteredIds) {
+ mediaIdsStr += "'" + mediaId + "',";
+ }
+ mediaIdsStr = mediaIdsStr.subSequence(0, mediaIdsStr.length() - 1) + ")";
+ }
+
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND "
+ + "(uploadState IS NULL OR uploadState IN ('uploaded', 'queued', 'failed', 'uploading')) AND mimeType LIKE ? " + mediaIdsStr + " ORDER BY (uploadState=?) DESC, date_created_gmt DESC", new String[]{blogId, "image%", "uploading"});
+ }
+
+ public int getMediaCountImages(String blogId) {
+ return getMediaImagesForBlog(blogId).getCount();
+ }
+
+ public Cursor getMediaUnattachedForBlog(String blogId) {
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND " +
+ "(uploadState IS NULL OR uploadState IN ('uploaded', 'queued', 'failed', 'uploading')) AND postId=0 ORDER BY (uploadState=?) DESC, date_created_gmt DESC", new String[]{blogId, "uploading"});
+ }
+
+ public int getMediaCountUnattached(String blogId) {
+ return getMediaUnattachedForBlog(blogId).getCount();
+ }
+
+ public Cursor getMediaFilesForBlog(String blogId, long startDate, long endDate) {
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId <> '' AND (uploadState IS NULL OR uploadState ='uploaded') AND (date_created_gmt >= ? AND date_created_gmt <= ?) ", new String[]{blogId, String.valueOf(startDate), String.valueOf(endDate)});
+ }
+
+ public Cursor getMediaFiles(String blogId, ArrayList<String> mediaIds) {
+ if (mediaIds == null || mediaIds.size() == 0)
+ return null;
+
+ String mediaIdsStr = "(";
+ for (String mediaId : mediaIds) {
+ mediaIdsStr += "'" + mediaId + "',";
+ }
+ mediaIdsStr = mediaIdsStr.subSequence(0, mediaIdsStr.length() - 1) + ")";
+
+ return db.rawQuery("SELECT id as _id, * FROM " + MEDIA_TABLE + " WHERE blogId=? AND mediaId IN " + mediaIdsStr, new String[] { blogId });
+ }
+
+ public MediaFile getMediaFile(String src, Post post) {
+ Cursor c = db.query(MEDIA_TABLE, null, "postID=? AND filePath=?",
+ new String[]{String.valueOf(post.getLocalTablePostId()), src}, null, null, null);
+
+ try {
+ if (c.moveToFirst()) {
+ MediaFile mf = new MediaFile();
+ mf.setId(c.getInt(0));
+ mf.setPostID(c.getInt(1));
+ mf.setFilePath(c.getString(2));
+ mf.setFileName(c.getString(3));
+ mf.setTitle(c.getString(4));
+ mf.setDescription(c.getString(5));
+ mf.setCaption(c.getString(6));
+ mf.setHorizontalAlignment(c.getInt(7));
+ mf.setWidth(c.getInt(8));
+ mf.setHeight(c.getInt(9));
+ mf.setMimeType(c.getString(10));
+ mf.setFeatured(c.getInt(11) > 0);
+ mf.setVideo(c.getInt(12) > 0);
+ mf.setFeaturedInPost(c.getInt(13) > 0);
+ mf.setFileURL(c.getString(14));
+ mf.setThumbnailURL(c.getString(15));
+ mf.setMediaId(c.getString(16));
+ mf.setBlogId(c.getString(17));
+ mf.setDateCreatedGMT(c.getLong(18));
+ mf.setUploadState(c.getString(19));
+ mf.setVideoPressShortCode(c.getString(20));
+
+ return mf;
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public void deleteMediaFilesForPost(Post post) {
+ db.delete(MEDIA_TABLE, "blogId='" + post.getLocalTableBlogId() + "' AND postID=" + post.getLocalTablePostId(), null);
+ }
+
+ /** Get the queued media files for upload for a given blogId **/
+ public Cursor getMediaUploadQueue(String blogId) {
+ return db.rawQuery("SELECT * FROM " + MEDIA_TABLE + " WHERE uploadState=? AND blogId=?", new String[] {"queued", blogId});
+ }
+
+ /** Update a media file to a new upload state **/
+ public void updateMediaUploadState(String blogId, String mediaId, MediaUploadState uploadState) {
+ if (blogId == null || blogId.equals("")) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ if (uploadState == null) {
+ values.putNull("uploadState");
+ } else {
+ values.put("uploadState", uploadState.toString());
+ }
+
+ if (mediaId == null) {
+ db.update(MEDIA_TABLE, values, "blogId=? AND (uploadState IS NULL OR uploadState ='uploaded')",
+ new String[]{blogId});
+ } else {
+ db.update(MEDIA_TABLE, values, "blogId=? AND mediaId=?", new String[]{blogId, mediaId});
+ EventBus.getDefault().post(new MediaChanged(blogId, mediaId));
+ }
+ }
+
+ public void updateMediaLocalToRemoteId(String blogId, String localMediaId, String remoteMediaId) {
+ ContentValues values = new ContentValues();
+ values.put("mediaId", remoteMediaId);
+ db.update(MEDIA_TABLE, values, "blogId=? AND mediaId=?", new String[]{blogId, localMediaId});
+ }
+
+ public void updateMediaFile(String blogId, String mediaId, String title, String description, String caption) {
+ if (blogId == null || blogId.equals("")) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+
+ if (title == null || title.equals("")) {
+ values.put("title", "");
+ } else {
+ values.put("title", title);
+ }
+
+ if (title == null || title.equals("")) {
+ values.put("description", "");
+ } else {
+ values.put("description", description);
+ }
+
+ if (caption == null || caption.equals("")) {
+ values.put("caption", "");
+ } else {
+ values.put("caption", caption);
+ }
+
+ db.update(MEDIA_TABLE, values, "blogId = ? AND mediaId=?", new String[] { blogId, mediaId });
+ }
+
+ /**
+ * For a given blogId, set all uploading states to failed.
+ * Useful for cleaning up files stuck in the "uploading" state.
+ **/
+ public void setMediaUploadingToFailed(String blogId) {
+ if (blogId == null || blogId.equals(""))
+ return;
+
+ ContentValues values = new ContentValues();
+ values.put("uploadState", "failed");
+ db.update(MEDIA_TABLE, values, "blogId=? AND uploadState=?", new String[]{blogId, "uploading"});
+ }
+
+ /** For a given blogId, clear the upload states in the upload queue **/
+ public void clearMediaUploaded(String blogId) {
+ if (blogId == null || blogId.equals(""))
+ return;
+
+ ContentValues values = new ContentValues();
+ values.putNull("uploadState");
+ db.update(MEDIA_TABLE, values, "blogId=? AND uploadState=?", new String[]{blogId, "uploaded"});
+ }
+
+ /** Delete a media item from a blog locally **/
+ public void deleteMediaFile(String blogId, String mediaId) {
+ db.delete(MEDIA_TABLE, "blogId=? AND mediaId=?", new String[]{blogId, mediaId});
+ }
+
+ /** Mark media files for deletion without actually deleting them. **/
+ public void setMediaFilesMarkedForDelete(String blogId, Set<String> ids) {
+ // This is for queueing up files to delete on the server
+ for (String id : ids) {
+ updateMediaUploadState(blogId, id, MediaUploadState.DELETE);
+ }
+ }
+
+ /** Mark media files as deleted without actually deleting them **/
+ public void setMediaFilesMarkedForDeleted(String blogId) {
+ // This is for syncing our files to the server:
+ // when we pull from the server, everything that is still 'deleted'
+ // was not downloaded from the server and can be removed via deleteFilesMarkedForDeleted()
+ updateMediaUploadState(blogId, null, MediaUploadState.DELETED);
+ }
+
+ /** Delete files marked as deleted **/
+ public void deleteFilesMarkedForDeleted(String blogId) {
+ db.delete(MEDIA_TABLE, "blogId=? AND uploadState=?", new String[]{blogId, "deleted"});
+ }
+
+ /** Get a media file scheduled for delete for a given blogId **/
+ public Cursor getMediaDeleteQueueItem(String blogId) {
+ return db.rawQuery("SELECT blogId, mediaId FROM " + MEDIA_TABLE + " WHERE uploadState=? AND blogId=? LIMIT 1",
+ new String[]{"delete", blogId});
+ }
+
+ /** Get all media files scheduled for delete for a given blogId **/
+ public Cursor getMediaDeleteQueueItems(String blogId) {
+ return db.rawQuery("SELECT blogId, mediaId FROM " + MEDIA_TABLE + " WHERE uploadState=? AND blogId=?",
+ new String[]{"delete", blogId});
+ }
+
+ public boolean hasMediaDeleteQueueItems(int blogId) {
+ return SqlUtils.boolForQuery(db, "SELECT 1 FROM " + MEDIA_TABLE + " WHERE uploadState=? AND blogId=?",
+ new String[]{"delete", Integer.toString(blogId)});
+ }
+
+ public int getWPCOMBlogID() {
+ int id = -1;
+ Cursor c = db.query(BLOGS_TABLE, new String[] { "id" },
+ "dotcomFlag=1", null, null, null, null);
+ int numRows = c.getCount();
+ c.moveToFirst();
+ if (numRows > 0) {
+ id = c.getInt(0);
+ }
+
+ c.close();
+
+ return id;
+ }
+
+ /*
+ * returns true if any posts in the passed blog have changes which haven't been uploaded yet
+ */
+ public boolean blogHasLocalChanges(int localBlogId, boolean isPage) {
+ String sql = "SELECT 1 FROM " + POSTS_TABLE + " WHERE isLocalChange=1 AND blogID=? AND isPage=?";
+ String[] args = {String.valueOf(localBlogId), isPage ? "1" : "0"};
+ return SqlUtils.boolForQuery(db, sql, args);
+ }
+
+ /*
+ * returns the number of posts/pages in the passed blog that aren't local drafts
+ */
+ public int getUploadedCountInBlog(int localBlogId, boolean isPage) {
+ String sql = "SELECT COUNT(*) FROM " + POSTS_TABLE + " WHERE blogID=? AND isPage=? AND localDraft=0";
+ String[] args = {String.valueOf(localBlogId), isPage ? "1" : "0"};
+ return SqlUtils.intForQuery(db, sql, args);
+ }
+
+ public boolean saveTheme(Theme theme) {
+ boolean returnValue = false;
+
+ ContentValues values = new ContentValues();
+ values.put(Theme.ID, theme.getId());
+ values.put(Theme.AUTHOR, theme.getAuthor());
+ values.put(Theme.SCREENSHOT, theme.getScreenshot());
+ values.put(Theme.AUTHOR_URI, theme.getAuthorURI());
+ values.put(Theme.DEMO_URI, theme.getDemoURI());
+ values.put(Theme.NAME, theme.getName());
+ values.put(Theme.STYLESHEET, theme.getStylesheet());
+ values.put(Theme.PRICE, theme.getPrice());
+ values.put(Theme.BLOG_ID, theme.getBlogId());
+ values.put(Theme.IS_CURRENT, theme.getIsCurrent() ? 1 : 0);
+
+ synchronized (this) {
+ int result = db.update(
+ THEMES_TABLE,
+ values,
+ Theme.ID + "=?",
+ new String[]{theme.getId()});
+ if (result == 0)
+ returnValue = db.insert(THEMES_TABLE, null, values) > 0;
+ }
+
+ return (returnValue);
+ }
+
+ public Cursor getThemesAll(String blogId) {
+ String[] columns = {COLUMN_NAME_ID, Theme.ID, Theme.NAME, Theme.SCREENSHOT, Theme.PRICE, Theme.IS_CURRENT};
+ String[] selection = {blogId};
+
+ return db.query(THEMES_TABLE, columns, Theme.BLOG_ID + "=?", selection, null, null, null);
+ }
+
+ public Cursor getThemesFree(String blogId) {
+ String[] columns = {COLUMN_NAME_ID, Theme.ID, Theme.NAME, Theme.SCREENSHOT, Theme.PRICE, Theme.IS_CURRENT};
+ String[] selection = {blogId, ""};
+
+ return db.query(THEMES_TABLE, columns, Theme.BLOG_ID + "=? AND " + Theme.PRICE + "=?", selection, null, null, null);
+ }
+
+ public Cursor getThemesPremium(String blogId) {
+ String[] columns = {COLUMN_NAME_ID, Theme.ID, Theme.NAME, Theme.SCREENSHOT, Theme.PRICE, Theme.IS_CURRENT};
+ String[] selection = {blogId, ""};
+
+ return db.query(THEMES_TABLE, columns, Theme.BLOG_ID + "=? AND " + Theme.PRICE + "!=?", selection, null, null, null);
+ }
+
+ public String getCurrentThemeId(String blogId) {
+ String[] selection = {blogId, String.valueOf(1)};
+ String currentThemeId;
+ try {
+ currentThemeId = DatabaseUtils.stringForQuery(db, "SELECT " + Theme.ID + " FROM " + THEMES_TABLE + " WHERE " + Theme.BLOG_ID + "=? and " + Theme.IS_CURRENT + "=?", selection);
+ } catch (SQLiteException e) {
+ currentThemeId = "";
+ }
+
+ return currentThemeId;
+ }
+
+ public void setCurrentTheme(String blogId, String id) {
+ // update any old themes that are set to true to false
+ ContentValues values = new ContentValues();
+ values.put(Theme.IS_CURRENT, false);
+ db.update(THEMES_TABLE, values, Theme.BLOG_ID + "=?", new String[] { blogId });
+
+ values = new ContentValues();
+ values.put(Theme.IS_CURRENT, true);
+ db.update(THEMES_TABLE, values, Theme.BLOG_ID + "=? AND " + Theme.ID + "=?", new String[] { blogId, id });
+ }
+
+ public int getThemeCount(String blogId) {
+ return getThemesAll(blogId).getCount();
+ }
+
+ public Cursor getThemes(String blogId, String searchTerm) {
+ String[] columns = {COLUMN_NAME_ID, Theme.ID, Theme.NAME, Theme.SCREENSHOT, Theme.PRICE, Theme.IS_CURRENT};
+ String[] selection = {blogId, "%" + searchTerm + "%"};
+
+ return db.query(THEMES_TABLE, columns, Theme.BLOG_ID + "=? AND " + Theme.NAME + " LIKE ?", selection, null, null, null);
+ }
+
+ public Theme getTheme(String blogId, String themeId) {
+ String[] columns = {COLUMN_NAME_ID, Theme.ID, Theme.AUTHOR, Theme.SCREENSHOT, Theme.AUTHOR_URI, Theme.DEMO_URI, Theme.NAME, Theme.STYLESHEET, Theme.PRICE, Theme.IS_CURRENT};
+ String[] selection = {blogId, themeId};
+ Cursor cursor = db.query(THEMES_TABLE, columns, Theme.BLOG_ID + "=? AND " + Theme.ID + "=?", selection, null, null, null);
+
+ if (cursor.moveToFirst()) {
+ String id = cursor.getString(cursor.getColumnIndex(Theme.ID));
+ String author = cursor.getString(cursor.getColumnIndex(Theme.AUTHOR));
+ String screenshot = cursor.getString(cursor.getColumnIndex(Theme.SCREENSHOT));
+ String authorURI = cursor.getString(cursor.getColumnIndex(Theme.AUTHOR_URI));
+ String demoURI = cursor.getString(cursor.getColumnIndex(Theme.DEMO_URI));
+ String name = cursor.getString(cursor.getColumnIndex(Theme.NAME));
+ String stylesheet = cursor.getString(cursor.getColumnIndex(Theme.STYLESHEET));
+ String price = cursor.getString(cursor.getColumnIndex(Theme.PRICE));
+ boolean isCurrent = cursor.getInt(cursor.getColumnIndex(Theme.IS_CURRENT)) > 0;
+
+ Theme theme = new Theme(id, author, screenshot, authorURI, demoURI, name, stylesheet, price, blogId, isCurrent);
+ cursor.close();
+
+ return theme;
+ } else {
+ cursor.close();
+ return null;
+ }
+ }
+
+ public Theme getCurrentTheme(String blogId) {
+ String currentThemeId = getCurrentThemeId(blogId);
+
+ return getTheme(blogId, currentThemeId);
+ }
+
+ /*
+ * used during development to copy database to SD card so we can access it via DDMS
+ */
+ protected void copyDatabase() {
+ String copyFrom = db.getPath();
+ String copyTo = WordPress.getContext().getExternalFilesDir(null).getAbsolutePath() + "/" + DATABASE_NAME + ".db";
+
+ try {
+ InputStream input = new FileInputStream(copyFrom);
+ OutputStream output = new FileOutputStream(copyTo);
+
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0)
+ output.write(buffer, 0, length);
+
+ output.flush();
+ output.close();
+ input.close();
+ } catch (IOException e) {
+ AppLog.e(T.DB, "failed to copy database", e);
+ }
+ }
+
+ public boolean hasAnyJetpackBlogs() {
+ return SqlUtils.boolForQuery(db, "SELECT 1 FROM " + BLOGS_TABLE + " WHERE api_blogid != 0 LIMIT 1", null);
+ }
+
+ private void updateCurrentBlog(Blog blog) {
+ Blog currentBlog = WordPress.currentBlog;
+ if (currentBlog != null && blog.getLocalTableBlogId() == currentBlog.getLocalTableBlogId()) {
+ WordPress.currentBlog = blog;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/AccountTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/AccountTable.java
new file mode 100644
index 000000000..c66776c36
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/AccountTable.java
@@ -0,0 +1,124 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.SqlUtils;
+
+public class AccountTable {
+ // Warning: the "accounts" table in WordPressDB is actually where blogs are stored.
+ private static final String ACCOUNT_TABLE = "tbl_accounts";
+
+ private static SQLiteDatabase getReadableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+ private static SQLiteDatabase getWritableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+
+ public static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + ACCOUNT_TABLE + " ("
+ + "local_id INTEGER PRIMARY KEY DEFAULT 0,"
+ + "user_name TEXT,"
+ + "user_id INTEGER DEFAULT 0,"
+ + "display_name TEXT,"
+ + "profile_url TEXT,"
+ + "avatar_url TEXT,"
+ + "primary_blog_id INTEGER DEFAULT 0,"
+ + "site_count INTEGER DEFAULT 0,"
+ + "visible_site_count INTEGER DEFAULT 0,"
+ + "access_token TEXT)");
+ }
+
+ public static void migrationAddEmailAddressField(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD email TEXT DEFAULT '';");
+ }
+
+ public static void migrationAddFirstNameLastNameAboutMeFields(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD first_name TEXT DEFAULT '';");
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD last_name TEXT DEFAULT '';");
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD about_me TEXT DEFAULT '';");
+ }
+
+ public static void migrationAddDateFields(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD date TEXT DEFAULT '';");
+ }
+
+ public static void migrationAddAccountSettingsFields(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD new_email TEXT DEFAULT '';");
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD pending_email_change BOOLEAN DEFAULT false;");
+ db.execSQL("ALTER TABLE " + ACCOUNT_TABLE + " ADD web_address TEXT DEFAULT '';");
+ }
+
+ private static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + ACCOUNT_TABLE);
+ }
+
+ public static void save(Account account) {
+ save(account, getWritableDb());
+ }
+
+ public static void save(Account account, SQLiteDatabase database) {
+ ContentValues values = new ContentValues();
+ // we only support one wpcom user at the moment: local_id is always 0
+ values.put("local_id", 0);
+ values.put("user_name", account.getUserName());
+ values.put("user_id", account.getUserId());
+ values.put("display_name", account.getDisplayName());
+ values.put("profile_url", account.getProfileUrl());
+ values.put("avatar_url", account.getAvatarUrl());
+ values.put("primary_blog_id", account.getPrimaryBlogId());
+ values.put("site_count", account.getSiteCount());
+ values.put("visible_site_count", account.getVisibleSiteCount());
+ values.put("access_token", account.getAccessToken());
+ values.put("email", account.getEmail());
+ values.put("first_name", account.getFirstName());
+ values.put("last_name", account.getLastName());
+ values.put("about_me", account.getAboutMe());
+ values.put("date", DateTimeUtils.iso8601FromDate(account.getDateCreated()));
+ values.put("new_email", account.getNewEmail());
+ values.put("pending_email_change", account.getPendingEmailChange());
+ values.put("web_address", account.getWebAddress());
+ database.insertWithOnConflict(ACCOUNT_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public static Account getDefaultAccount() {
+ return getAccountByLocalId(0);
+ }
+
+ private static Account getAccountByLocalId(long localId) {
+ Account account = new Account();
+
+ String[] args = {Long.toString(localId)};
+ Cursor c = getReadableDb().rawQuery("SELECT * FROM " + ACCOUNT_TABLE + " WHERE local_id=?", args);
+
+ try {
+ if (c.moveToFirst()) {
+ account.setUserName(c.getString(c.getColumnIndex("user_name")));
+ account.setUserId(c.getLong(c.getColumnIndex("user_id")));
+ account.setDisplayName(c.getString(c.getColumnIndex("display_name")));
+ account.setProfileUrl(c.getString(c.getColumnIndex("profile_url")));
+ account.setAvatarUrl(c.getString(c.getColumnIndex("avatar_url")));
+ account.setPrimaryBlogId(c.getLong(c.getColumnIndex("primary_blog_id")));
+ account.setSiteCount(c.getInt(c.getColumnIndex("site_count")));
+ account.setVisibleSiteCount(c.getInt(c.getColumnIndex("visible_site_count")));
+ account.setAccessToken(c.getString(c.getColumnIndex("access_token")));
+ account.setEmail(c.getString(c.getColumnIndex("email")));
+ account.setFirstName(c.getString(c.getColumnIndex("first_name")));
+ account.setLastName(c.getString(c.getColumnIndex("last_name")));
+ account.setAboutMe(c.getString(c.getColumnIndex("about_me")));
+ account.setDateCreated(DateTimeUtils.dateFromIso8601(c.getString(c.getColumnIndex("date"))));
+ account.setNewEmail(c.getString(c.getColumnIndex("new_email")));
+ account.setPendingEmailChange(c.getInt(c.getColumnIndex("pending_email_change")) > 0);
+ account.setWebAddress(c.getString(c.getColumnIndex("web_address")));
+ }
+ return account;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/CommentTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/CommentTable.java
new file mode 100644
index 000000000..9545ec10f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/CommentTable.java
@@ -0,0 +1,419 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.SqlUtils;
+import org.wordpress.android.util.StringUtils;
+
+/**
+ * replaces the comments table used in versions prior to 2.6.1, which didn't use a primary key
+ * and missed a few important fields
+ */
+public class CommentTable {
+ public static final String COMMENTS_TABLE = "comments";
+
+ public static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + COMMENTS_TABLE + " ("
+ + " blog_id INTEGER DEFAULT 0,"
+ + " post_id INTEGER DEFAULT 0,"
+ + " comment_id INTEGER DEFAULT 0,"
+ + " comment TEXT,"
+ + " published TEXT,"
+ + " status TEXT,"
+ + " author_name TEXT,"
+ + " author_url TEXT,"
+ + " author_email TEXT,"
+ + " post_title TEXT,"
+ + " profile_image_url TEXT,"
+ + " PRIMARY KEY (blog_id, post_id, comment_id)"
+ + " );");
+ }
+
+ private static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + COMMENTS_TABLE);
+ }
+
+ public static void reset(SQLiteDatabase db) {
+ AppLog.i(AppLog.T.COMMENTS, "resetting comment table");
+ dropTables(db);
+ createTables(db);
+ }
+
+ private static SQLiteDatabase getReadableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+ private static SQLiteDatabase getWritableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+
+ /*
+ * purge comments attached to blogs that no longer exist, and remove older comments
+ * TODO: call after hiding or deleting blogs
+ */
+ private static final int MAX_COMMENTS = 1000;
+ public static int purge(SQLiteDatabase db) {
+ int numDeleted = 0;
+
+ // get rid of comments on blogs that don't exist or are hidden
+ String sql = " blog_id NOT IN (SELECT DISTINCT id FROM " + WordPressDB.BLOGS_TABLE
+ + " WHERE isHidden = 0)";
+ numDeleted += db.delete(COMMENTS_TABLE, sql, null);
+
+ // get rid of older comments if we've reached the max
+ int numExisting = (int)SqlUtils.getRowCount(db, COMMENTS_TABLE);
+ if (numExisting > MAX_COMMENTS) {
+ int numToPurge = numExisting - MAX_COMMENTS;
+ sql = " comment_id IN (SELECT DISTINCT comment_id FROM " + COMMENTS_TABLE
+ + " ORDER BY published LIMIT " + Integer.toString(numToPurge) + ")";
+ numDeleted += db.delete(COMMENTS_TABLE, sql, null);
+ }
+
+ return numDeleted;
+ }
+
+ /**
+ * add a single comment - will update existing comment with same IDs
+ * @param localBlogId - unique id in account table for the blog the comment is from
+ * @param comment - comment object to store
+ */
+ public static void addComment(int localBlogId, final Comment comment) {
+ if (comment == null)
+ return;
+
+ ContentValues values = new ContentValues();
+ values.put("blog_id", localBlogId);
+ values.put("post_id", comment.postID);
+ values.put("comment_id", comment.commentID);
+ values.put("author_name", comment.getAuthorName());
+ values.put("author_url", comment.getAuthorUrl());
+ values.put("comment", SqlUtils.maxSQLiteText(comment.getCommentText()));
+ values.put("status", comment.getStatus());
+ values.put("author_email", comment.getAuthorEmail());
+ values.put("post_title", comment.getPostTitle());
+ values.put("published", comment.getPublished());
+ values.put("profile_image_url", comment.getProfileImageUrl());
+
+ getWritableDb().insertWithOnConflict(COMMENTS_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ /**
+ * retrieve a single comment
+ * @param localBlogId - unique id in account table for the blog the comment is from
+ * @param commentId - commentId of the actual comment
+ * @return Comment if found, null otherwise
+ */
+ public static Comment getComment(int localBlogId, long commentId) {
+ String[] args = {Integer.toString(localBlogId), Long.toString(commentId)};
+ Cursor c = getReadableDb().rawQuery("SELECT * FROM " + COMMENTS_TABLE + " WHERE blog_id=? AND comment_id=?", args);
+ try {
+ if (!c.moveToFirst()) {
+ return null;
+ }
+ return getCommentFromCursor(c);
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ /**
+ * get all comments for a blog
+ * @param localBlogId - unique id in account table for this blog
+ * @return list of comments for this blog
+ */
+ public static CommentList getCommentsForBlog(int localBlogId) {
+ CommentList comments = new CommentList();
+
+ String[] args = {Integer.toString(localBlogId)};
+ Cursor c = getReadableDb().rawQuery(
+ "SELECT * FROM " + COMMENTS_TABLE + " WHERE blog_id=? ORDER BY published DESC", args);
+
+ try {
+ while (c.moveToNext()) {
+ Comment comment = getCommentFromCursor(c);
+ comments.add(comment);
+ }
+
+ return comments;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ /**
+ * get comments for a blog that have a specific status
+ * @param localBlogId - unique id in account table for this blog
+ * @param filter - status to filter comments by
+ * @return list of comments for this blog
+ */
+ public static CommentList getCommentsForBlogWithFilter(int localBlogId, CommentStatus filter) {
+ CommentList comments = new CommentList();
+ Cursor c;
+
+ //aggregating 'all' to include approved and unapproved comments
+ if (CommentStatus.UNKNOWN.equals(filter)){
+ //we need to get the filter values for both XMLrpc and REST api as in the case of a migration where existing
+ // data is present on a device, we still need to be able to filter both values
+ String[] args = {Integer.toString(localBlogId),
+ CommentStatus.toString(CommentStatus.APPROVED),
+ CommentStatus.toString(CommentStatus.UNAPPROVED),
+ CommentStatus.toRESTString(CommentStatus.APPROVED),
+ CommentStatus.toRESTString(CommentStatus.UNAPPROVED)};
+ c = getReadableDb().rawQuery(
+ "SELECT * FROM " + COMMENTS_TABLE + " WHERE blog_id=? AND (status=? OR status=? OR status=? OR status=?) ORDER BY published DESC", args);
+
+ } else {
+ //we need to get the filter values for both XMLrpc and REST api as in the case of a migration where existing
+ // data is present on a device, we still need to be able to filter both values
+ String[] args = {Integer.toString(localBlogId), CommentStatus.toString(filter), CommentStatus.toRESTString(filter)};
+ c = getReadableDb().rawQuery(
+ "SELECT * FROM " + COMMENTS_TABLE + " WHERE blog_id=? AND (status=? OR status=?) ORDER BY published DESC", args);
+ }
+
+ try {
+ while (c.moveToNext()) {
+ Comment comment = getCommentFromCursor(c);
+ comments.add(comment);
+ }
+
+ return comments;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ /**
+ * delete all comments for a blog
+ * @param localBlogId - unique id in account table for this blog
+ * @return number of comments deleted
+ */
+ public static int deleteCommentsForBlog(int localBlogId) {
+ return getWritableDb().delete(COMMENTS_TABLE, "blog_id=?", new String[]{Integer.toString(localBlogId)});
+ }
+
+ /**
+ * delete comments for a blog that match a specific status
+ * @param localBlogId - unique id in account table for this blog
+ * @param filter - status to use to filter the query
+ * @return number of comments deleted
+ */
+ public static int deleteCommentsForBlogWithFilter(int localBlogId, CommentStatus filter) {
+ if (CommentStatus.UNKNOWN.equals(filter)){
+ //we need to get the filter values for both XMLrpc and REST api as in the case of a migration where existing
+ // data is present on a device, we still need to be able to filter both values
+ String[] args = {Integer.toString(localBlogId),
+ CommentStatus.toString(CommentStatus.APPROVED),
+ CommentStatus.toString(CommentStatus.UNAPPROVED),
+ CommentStatus.toRESTString(CommentStatus.APPROVED),
+ CommentStatus.toRESTString(CommentStatus.UNAPPROVED)};
+ return getWritableDb().delete(COMMENTS_TABLE, "blog_id=? AND (status=? OR status=? OR status=? OR status=?)", args);
+
+ } else {
+ //we need to get the filter values for both XMLrpc and REST api as in the case of a migration where existing
+ // data is present on a device, we still need to be able to filter both values
+ String[] args = {Integer.toString(localBlogId), CommentStatus.toString(filter), CommentStatus.toRESTString(filter)};
+ return getWritableDb().delete(COMMENTS_TABLE, "blog_id=? AND (status=? OR status=?)", args);
+ }
+ }
+
+ /**
+ * saves comments for passed blog to local db, overwriting existing ones if necessary
+ * @param localBlogId - unique id in account table for this blog
+ * @param comments - list of comments to save
+ * @return true if saved, false on failure
+ */
+ public static boolean saveComments(int localBlogId, final CommentList comments) {
+ if (comments == null || comments.size() == 0)
+ return false;
+
+ final String sql = " INSERT OR REPLACE INTO " + COMMENTS_TABLE + "("
+ + " blog_id," // 1
+ + " post_id," // 2
+ + " comment_id," // 3
+ + " comment," // 4
+ + " published," // 5
+ + " status," // 6
+ + " author_name," // 7
+ + " author_url," // 8
+ + " author_email," // 9
+ + " post_title," // 10
+ + " profile_image_url" // 11
+ + " ) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)";
+
+ SQLiteDatabase db = getWritableDb();
+ SQLiteStatement stmt = db.compileStatement(sql);
+ db.beginTransaction();
+ try {
+ try {
+ for (Comment comment: comments) {
+ stmt.bindLong ( 1, localBlogId);
+ stmt.bindLong ( 2, comment.postID);
+ stmt.bindLong ( 3, comment.commentID);
+ stmt.bindString( 4, SqlUtils.maxSQLiteText(comment.getCommentText()));
+ stmt.bindString( 5, comment.getPublished());
+ stmt.bindString( 6, comment.getStatus());
+ stmt.bindString( 7, comment.getAuthorName());
+ stmt.bindString( 8, comment.getAuthorUrl());
+ stmt.bindString( 9, comment.getAuthorEmail());
+ stmt.bindString(10, comment.getPostTitle());
+ stmt.bindString(11, comment.getProfileImageUrl());
+ stmt.execute();
+ }
+
+ db.setTransactionSuccessful();
+ return true;
+ } catch (SQLiteException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ }
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /**
+ * updates the passed comment
+ * @param localBlogId - unique id in account table for this blog
+ * @param comment - comment to update
+ */
+ public static void updateComment(int localBlogId, final Comment comment) {
+ // this will replace the existing comment
+ addComment(localBlogId, comment);
+ }
+
+ /**
+ * updates the status for the passed comment
+ * @param localBlogId - unique id in account table for this blog
+ * @param commentId - id of comment (returned by api)
+ * @param newStatus - status to change to
+ */
+ public static void updateCommentStatus(int localBlogId, long commentId, String newStatus) {
+ ContentValues values = new ContentValues();
+ values.put("status", newStatus);
+ String[] args = {Integer.toString(localBlogId),
+ Long.toString(commentId)};
+ getWritableDb().update(COMMENTS_TABLE, values, "blog_id=? AND comment_id=?", args);
+ }
+
+ /**
+ * updates the status for the passed list of comments
+ * @param localBlogId - unique id in account table for this blog
+ * @param comments - list of comments to update
+ * @param newStatus - status to change to
+ */
+ public static void updateCommentsStatus(int localBlogId, final CommentList comments, String newStatus) {
+ if (comments == null || comments.size() == 0)
+ return;
+ getWritableDb().beginTransaction();
+ try {
+ for (Comment comment: comments) {
+ updateCommentStatus(localBlogId, comment.commentID, newStatus);
+ }
+ getWritableDb().setTransactionSuccessful();
+ } finally {
+ getWritableDb().endTransaction();
+ }
+ }
+
+ /**
+ * updates the post title for the passed comment
+ * @param localBlogId - unique id in account table for this blog
+ * @param postTitle - title to update to
+ * @return true if title updated
+ */
+ public static boolean updateCommentPostTitle(int localBlogId, long commentId, String postTitle) {
+ ContentValues values = new ContentValues();
+ values.put("post_title", StringUtils.notNullStr(postTitle));
+ String[] args = {Integer.toString(localBlogId),
+ Long.toString(commentId)};
+ int count = getWritableDb().update(COMMENTS_TABLE, values, "blog_id=? AND comment_id=?", args);
+ return (count > 0);
+ }
+
+ /**
+ * delete a single comment
+ * @param localBlogId - unique id in account table for this blog
+ * @param commentId - commentId of the actual comment
+ * @return true if comment deleted, false otherwise
+ */
+ public static boolean deleteComment(int localBlogId, long commentId) {
+ String[] args = {Integer.toString(localBlogId),
+ Long.toString(commentId)};
+ int count = getWritableDb().delete(COMMENTS_TABLE, "blog_id=? AND comment_id=?", args);
+ return (count > 0);
+ }
+
+ /**
+ * delete a list of comments
+ * @param localBlogId - unique id in account table for this blog
+ * @param comments - list of comments to delete
+ */
+ public static void deleteComments(int localBlogId, final CommentList comments) {
+ if (comments == null || comments.size() == 0)
+ return;
+ getWritableDb().beginTransaction();
+ try {
+ for (Comment comment: comments) {
+ deleteComment(localBlogId, comment.commentID);
+ }
+ getWritableDb().setTransactionSuccessful();
+ } finally {
+ getWritableDb().endTransaction();
+ }
+ }
+
+ /**
+ * returns the number of unmoderated comments for a specific blog
+ * @param localBlogId - unique id in account table for this blog
+ */
+ public static int getUnmoderatedCommentCount(int localBlogId) {
+ String sql = "SELECT COUNT(*) FROM " + COMMENTS_TABLE + " WHERE blog_id=? AND status=?";
+ String[] args = {Integer.toString(localBlogId), "hold"};
+ return SqlUtils.intForQuery(getReadableDb(), sql, args);
+ }
+
+ private static Comment getCommentFromCursor(Cursor c) {
+ final String authorName = c.getString(c.getColumnIndex("author_name"));
+ final String content = c.getString(c.getColumnIndex("comment"));
+ final String published = c.getString(c.getColumnIndex("published"));
+ final String status = c.getString(c.getColumnIndex("status"));
+ final String authorUrl = c.getString(c.getColumnIndex("author_url"));
+ final String authorEmail = c.getString(c.getColumnIndex("author_email"));
+ final String postTitle = c.getString(c.getColumnIndex("post_title"));
+ final String profileImageUrl = c.getString(c.getColumnIndex("profile_image_url"));
+
+ int postId = c.getInt(c.getColumnIndex("post_id"));
+ int commentId = c.getInt(c.getColumnIndex("comment_id"));
+
+ return new Comment(
+ postId,
+ commentId,
+ authorName,
+ published,
+ content,
+ status,
+ postTitle,
+ authorUrl,
+ authorEmail,
+ profileImageUrl);
+ }
+
+
+ /**
+ * Delete big comments (Maximum 512 * 1024 = 524288) (fix #2855)
+ * @return number of deleted comments
+ */
+ public static int deleteBigComments(SQLiteDatabase db) {
+ return db.delete(COMMENTS_TABLE, "LENGTH(comment) >= 524288", null);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java
new file mode 100644
index 000000000..84ac02dba
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/PeopleTable.java
@@ -0,0 +1,354 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.support.annotation.Nullable;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Person;
+import org.wordpress.android.models.Role;
+import org.wordpress.android.ui.people.utils.PeopleUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.SqlUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PeopleTable {
+ private static final String TEAM_TABLE = "people_team";
+ private static final String FOLLOWERS_TABLE = "people_followers";
+ private static final String EMAIL_FOLLOWERS_TABLE = "people_email_followers";
+ private static final String VIEWERS_TABLE = "people_viewers";
+
+ private static SQLiteDatabase getReadableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+ private static SQLiteDatabase getWritableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+
+ public static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TEAM_TABLE + " ("
+ + "person_id INTEGER DEFAULT 0,"
+ + "local_blog_id INTEGER DEFAULT 0,"
+ + "user_name TEXT,"
+ + "display_name TEXT,"
+ + "avatar_url TEXT,"
+ + "role TEXT,"
+ + "PRIMARY KEY (person_id, local_blog_id)"
+ + ");");
+
+ db.execSQL("CREATE TABLE " + FOLLOWERS_TABLE + " ("
+ + "person_id INTEGER DEFAULT 0,"
+ + "local_blog_id INTEGER DEFAULT 0,"
+ + "user_name TEXT,"
+ + "display_name TEXT,"
+ + "avatar_url TEXT,"
+ + "subscribed TEXT,"
+ + "PRIMARY KEY (person_id, local_blog_id)"
+ + ");");
+
+ db.execSQL("CREATE TABLE " + EMAIL_FOLLOWERS_TABLE + " ("
+ + "person_id INTEGER DEFAULT 0,"
+ + "local_blog_id INTEGER DEFAULT 0,"
+ + "display_name TEXT,"
+ + "avatar_url TEXT,"
+ + "subscribed TEXT,"
+ + "PRIMARY KEY (person_id, local_blog_id)"
+ + ");");
+ }
+
+ public static void createViewersTable(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + VIEWERS_TABLE + " ("
+ + "person_id INTEGER DEFAULT 0,"
+ + "local_blog_id INTEGER DEFAULT 0,"
+ + "user_name TEXT,"
+ + "display_name TEXT,"
+ + "avatar_url TEXT,"
+ + "PRIMARY KEY (person_id, local_blog_id)"
+ + ");");
+ }
+
+ private static void dropTables(SQLiteDatabase db) {
+ // People table is not used anymore, each filter now has it's own table
+ db.execSQL("DROP TABLE IF EXISTS people");
+
+ db.execSQL("DROP TABLE IF EXISTS " + TEAM_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + FOLLOWERS_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + EMAIL_FOLLOWERS_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + VIEWERS_TABLE);
+ }
+
+ public static void reset(SQLiteDatabase db) {
+ AppLog.i(AppLog.T.PEOPLE, "resetting people table");
+ dropTables(db);
+ createTables(db);
+ }
+
+ public static void saveUser(Person person) {
+ save(TEAM_TABLE, person, getWritableDb());
+ }
+
+ private static void save(String table, Person person, SQLiteDatabase database) {
+ ContentValues values = new ContentValues();
+ values.put("person_id", person.getPersonID());
+ values.put("local_blog_id", person.getLocalTableBlogId());
+ values.put("display_name", person.getDisplayName());
+ values.put("avatar_url", person.getAvatarUrl());
+
+ switch (table) {
+ case TEAM_TABLE:
+ values.put("user_name", person.getUsername());
+ if (person.getRole() != null) {
+ values.put("role", person.getRole().toString());
+ }
+ break;
+ case FOLLOWERS_TABLE:
+ values.put("user_name", person.getUsername());
+ values.put("subscribed", person.getSubscribed());
+ break;
+ case EMAIL_FOLLOWERS_TABLE:
+ values.put("subscribed", person.getSubscribed());
+ break;
+ case VIEWERS_TABLE:
+ values.put("user_name", person.getUsername());
+ break;
+ }
+
+ database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public static void saveUsers(List<Person> peopleList, int localTableBlogId, boolean isFreshList) {
+ savePeople(TEAM_TABLE, peopleList, localTableBlogId, isFreshList);
+ }
+
+ public static void saveFollowers(List<Person> peopleList, int localTableBlogId, boolean isFreshList) {
+ savePeople(FOLLOWERS_TABLE, peopleList, localTableBlogId, isFreshList);
+ }
+
+ public static void saveEmailFollowers(List<Person> peopleList, int localTableBlogId, boolean isFreshList) {
+ savePeople(EMAIL_FOLLOWERS_TABLE, peopleList, localTableBlogId, isFreshList);
+ }
+
+ public static void saveViewers(List<Person> peopleList, int localTableBlogId, boolean isFreshList) {
+ savePeople(VIEWERS_TABLE, peopleList, localTableBlogId, isFreshList);
+ }
+
+ private static void savePeople(String table, List<Person> peopleList, int localTableBlogId, boolean isFreshList) {
+ getWritableDb().beginTransaction();
+ try {
+ // We have a fresh list, remove the previous list of people in case it was deleted on remote
+ if (isFreshList) {
+ PeopleTable.deletePeople(table, localTableBlogId);
+ }
+
+ for (Person person : peopleList) {
+ PeopleTable.save(table, person, getWritableDb());
+ }
+ getWritableDb().setTransactionSuccessful();
+ } finally {
+ getWritableDb().endTransaction();
+ }
+ }
+
+ public static void deletePeopleForLocalBlogId(int localTableBlogId) {
+ deletePeople(TEAM_TABLE, localTableBlogId);
+ deletePeople(FOLLOWERS_TABLE, localTableBlogId);
+ deletePeople(EMAIL_FOLLOWERS_TABLE, localTableBlogId);
+ deletePeople(VIEWERS_TABLE, localTableBlogId);
+ }
+
+ private static void deletePeople(String table, int localTableBlogId) {
+ String[] args = new String[]{Integer.toString(localTableBlogId)};
+ getWritableDb().delete(table, "local_blog_id=?1", args);
+ }
+
+ /**
+ * In order to avoid syncing issues, this method will be called when People page is created. We only keep
+ * the first page of users, so we don't show an empty screen. When fresh data is received, it'll replace
+ * the existing page.
+ * @param localTableBlogId - the local blog id people will be deleted from
+ */
+ public static void deletePeopleExceptForFirstPage(int localTableBlogId) {
+ int fetchLimit = PeopleUtils.FETCH_LIMIT;
+ String[] tables = {TEAM_TABLE, FOLLOWERS_TABLE, EMAIL_FOLLOWERS_TABLE, VIEWERS_TABLE};
+
+ getWritableDb().beginTransaction();
+ try {
+ for (String table : tables) {
+ int size = getPeopleCountForLocalBlogId(table, localTableBlogId);
+ if (size > fetchLimit) {
+ String where = "local_blog_id=" + localTableBlogId;
+ String[] columns = {"person_id"};
+ String limit = Integer.toString(size - fetchLimit);
+ String orderBy;
+ if (shouldOrderAlphabetically(table)) {
+ orderBy = "lower(display_name) DESC, lower(user_name) DESC";
+ } else {
+ orderBy = "ROWID DESC";
+ }
+ String inQuery = SQLiteQueryBuilder.buildQueryString(false, table, columns, where, null, null,
+ orderBy, limit);
+
+ String[] args = new String[] {Integer.toString(localTableBlogId)};
+ getWritableDb().delete(table, "local_blog_id=?1 AND person_id IN (" + inQuery + ")", args);
+ }
+ }
+ getWritableDb().setTransactionSuccessful();
+ } finally {
+ getWritableDb().endTransaction();
+ }
+ }
+
+ public static int getUsersCountForLocalBlogId(int localTableBlogId) {
+ return getPeopleCountForLocalBlogId(TEAM_TABLE, localTableBlogId);
+ }
+
+ public static int getViewersCountForLocalBlogId(int localTableBlogId) {
+ return getPeopleCountForLocalBlogId(VIEWERS_TABLE, localTableBlogId);
+ }
+
+ private static int getPeopleCountForLocalBlogId(String table, int localTableBlogId) {
+ String[] args = new String[]{Integer.toString(localTableBlogId)};
+ String sql = "SELECT COUNT(*) FROM " + table + " WHERE local_blog_id=?";
+ return SqlUtils.intForQuery(getReadableDb(), sql, args);
+ }
+
+ public static void deletePerson(long personID, int localTableBlogId, Person.PersonType personType) {
+ String table = getTableForPersonType(personType);
+ if (table != null) {
+ deletePerson(table, personID, localTableBlogId);
+ }
+ }
+
+ private static void deletePerson(String table, long personID, int localTableBlogId) {
+ String[] args = new String[]{Long.toString(personID), Integer.toString(localTableBlogId)};
+ getWritableDb().delete(table, "person_id=? AND local_blog_id=?", args);
+ }
+
+ public static List<Person> getUsers(int localTableBlogId) {
+ return PeopleTable.getPeople(TEAM_TABLE, localTableBlogId);
+ }
+
+ public static List<Person> getFollowers(int localTableBlogId) {
+ return PeopleTable.getPeople(FOLLOWERS_TABLE, localTableBlogId);
+ }
+
+ public static List<Person> getEmailFollowers(int localTableBlogId) {
+ return PeopleTable.getPeople(EMAIL_FOLLOWERS_TABLE, localTableBlogId);
+ }
+
+ public static List<Person> getViewers(int localTableBlogId) {
+ return PeopleTable.getPeople(VIEWERS_TABLE, localTableBlogId);
+ }
+
+ private static List<Person> getPeople(String table, int localTableBlogId) {
+ String[] args = {Integer.toString(localTableBlogId)};
+ String orderBy;
+ if (shouldOrderAlphabetically(table)) {
+ orderBy = " ORDER BY lower(display_name), lower(user_name)";
+ } else {
+ // we want the server-side order for followers & viewers
+ orderBy = " ORDER BY ROWID";
+ }
+ Cursor c = getReadableDb().rawQuery("SELECT * FROM " + table + " WHERE local_blog_id=?" + orderBy, args);
+
+ List<Person> people = new ArrayList<>();
+ try {
+ while (c.moveToNext()) {
+ Person person = getPersonFromCursor(c, table, localTableBlogId);
+ people.add(person);
+ }
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ return people;
+ }
+
+ @Nullable
+ public static Person getPerson(long personId, int localTableBlogId, Person.PersonType personType) {
+ String table = getTableForPersonType(personType);
+ if (table != null) {
+ return getPerson(table, personId, localTableBlogId);
+ }
+ return null;
+ }
+
+ public static Person getUser(long personId, int localTableBlogId) {
+ return getPerson(TEAM_TABLE, personId, localTableBlogId);
+ }
+
+ /**
+ * retrieve a person
+ * @param table - sql table the person record is in
+ * @param personId - id of a person in a particular blog
+ * @param localTableBlogId - the local blog id the user belongs to
+ * @return Person if found, null otherwise
+ */
+ private static Person getPerson(String table, long personId, int localTableBlogId) {
+ String[] args = { Long.toString(personId), Integer.toString(localTableBlogId)};
+ Cursor c = getReadableDb().rawQuery("SELECT * FROM " + table +
+ " WHERE person_id=? AND local_blog_id=?", args);
+ try {
+ if (!c.moveToFirst()) {
+ return null;
+ }
+ return getPersonFromCursor(c, table, localTableBlogId);
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ private static Person getPersonFromCursor(Cursor c, String table, int localTableBlogId) {
+ long personId = c.getInt(c.getColumnIndex("person_id"));
+
+ Person person = new Person(personId, localTableBlogId);
+ person.setDisplayName(c.getString(c.getColumnIndex("display_name")));
+ person.setAvatarUrl(c.getString(c.getColumnIndex("avatar_url")));
+ switch (table) {
+ case TEAM_TABLE:
+ person.setUsername(c.getString(c.getColumnIndex("user_name")));
+ String role = c.getString(c.getColumnIndex("role"));
+ person.setRole(Role.fromString(role));
+ person.setPersonType(Person.PersonType.USER);
+ break;
+ case FOLLOWERS_TABLE:
+ person.setUsername(c.getString(c.getColumnIndex("user_name")));
+ person.setSubscribed(c.getString(c.getColumnIndex("subscribed")));
+ person.setPersonType(Person.PersonType.FOLLOWER);
+ break;
+ case EMAIL_FOLLOWERS_TABLE:
+ person.setSubscribed(c.getString(c.getColumnIndex("subscribed")));
+ person.setPersonType(Person.PersonType.EMAIL_FOLLOWER);
+ break;
+ case VIEWERS_TABLE:
+ person.setUsername(c.getString(c.getColumnIndex("user_name")));
+ person.setPersonType(Person.PersonType.VIEWER);
+ break;
+ }
+
+ return person;
+ }
+
+ // order is disabled for followers & viewers for now since the API is not supporting it
+ private static boolean shouldOrderAlphabetically(String table) {
+ return table.equals(TEAM_TABLE);
+ }
+
+ @Nullable
+ private static String getTableForPersonType(Person.PersonType personType) {
+ switch (personType) {
+ case USER:
+ return TEAM_TABLE;
+ case FOLLOWER:
+ return FOLLOWERS_TABLE;
+ case EMAIL_FOLLOWER:
+ return EMAIL_FOLLOWERS_TABLE;
+ case VIEWER:
+ return VIEWERS_TABLE;
+ }
+ return null;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderBlogTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderBlogTable.java
new file mode 100644
index 000000000..591b628c3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderBlogTable.java
@@ -0,0 +1,382 @@
+package org.wordpress.android.datasets;
+
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.text.TextUtils;
+
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderBlogList;
+import org.wordpress.android.models.ReaderRecommendBlogList;
+import org.wordpress.android.models.ReaderRecommendedBlog;
+import org.wordpress.android.models.ReaderUrlList;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.SqlUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.util.Date;
+
+/**
+ * tbl_blog_info contains information about blogs viewed in the reader, and blogs the
+ * user is following. Note that this table is populated from two endpoints:
+ *
+ * 1. sites/{$siteId}
+ * 2. read/following/mine?meta=site,feed
+ *
+ * The first endpoint is called when the user views blog preview, the second is called
+ * to get the full list of blogs the user is following
+ */
+public class ReaderBlogTable {
+
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_blog_info ("
+ + " blog_id INTEGER DEFAULT 0," // will be same as feedId for feeds
+ + " feed_id INTEGER DEFAULT 0," // will be 0 for blogs
+ + " blog_url TEXT NOT NULL COLLATE NOCASE,"
+ + " image_url TEXT,"
+ + " feed_url TEXT,"
+ + " name TEXT,"
+ + " description TEXT,"
+ + " is_private INTEGER DEFAULT 0,"
+ + " is_jetpack INTEGER DEFAULT 0,"
+ + " is_following INTEGER DEFAULT 0,"
+ + " num_followers INTEGER DEFAULT 0,"
+ + " date_updated TEXT,"
+ + " PRIMARY KEY (blog_id)"
+ + ")");
+
+ db.execSQL("CREATE TABLE tbl_recommended_blogs ("
+ + " blog_id INTEGER DEFAULT 0,"
+ + " follow_reco_id INTEGER DEFAULT 0,"
+ + " score INTEGER DEFAULT 0,"
+ + " title TEXT COLLATE NOCASE,"
+ + " blog_url TEXT COLLATE NOCASE,"
+ + " image_url TEXT,"
+ + " reason TEXT,"
+ + " PRIMARY KEY (blog_id)"
+ + ")");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_blog_info");
+ db.execSQL("DROP TABLE IF EXISTS tbl_recommended_blogs");
+ }
+
+ public static ReaderBlog getBlogInfo(long blogId) {
+ if (blogId == 0) {
+ return null;
+ }
+ String[] args = {Long.toString(blogId)};
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_blog_info WHERE blog_id=?", args);
+ try {
+ if (!cursor.moveToFirst()) {
+ return null;
+ }
+ return getBlogInfoFromCursor(cursor);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ public static ReaderBlog getFeedInfo(long feedId) {
+ if (feedId == 0) {
+ return null;
+ }
+ String[] args = {Long.toString(feedId)};
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_blog_info WHERE feed_id=?", args);
+ try {
+ if (!cursor.moveToFirst()) {
+ return null;
+ }
+ return getBlogInfoFromCursor(cursor);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ public static long getFeedIdFromUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return 0;
+ }
+ String[] args = {UrlUtils.normalizeUrl(url)};
+ return SqlUtils.longForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT feed_id FROM tbl_blog_info WHERE feed_url=?",
+ args);
+ }
+
+ private static ReaderBlog getBlogInfoFromCursor(Cursor c) {
+ if (c == null) {
+ return null;
+ }
+
+ ReaderBlog blogInfo = new ReaderBlog();
+ blogInfo.blogId = c.getLong(c.getColumnIndex("blog_id"));
+ blogInfo.feedId = c.getLong(c.getColumnIndex("feed_id"));
+ blogInfo.setUrl(c.getString(c.getColumnIndex("blog_url")));
+ blogInfo.setImageUrl(c.getString(c.getColumnIndex("image_url")));
+ blogInfo.setFeedUrl(c.getString(c.getColumnIndex("feed_url")));
+ blogInfo.setName(c.getString(c.getColumnIndex("name")));
+ blogInfo.setDescription(c.getString(c.getColumnIndex("description")));
+ blogInfo.isPrivate = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_private")));
+ blogInfo.isJetpack = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_jetpack")));
+ blogInfo.isFollowing = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_following")));
+ blogInfo.numSubscribers = c.getInt(c.getColumnIndex("num_followers"));
+
+ return blogInfo;
+ }
+
+ public static void addOrUpdateBlog(ReaderBlog blogInfo) {
+ if (blogInfo == null) {
+ return;
+ }
+ String sql = "INSERT OR REPLACE INTO tbl_blog_info"
+ + " (blog_id, feed_id, blog_url, image_url, feed_url, name, description, is_private, is_jetpack, is_following, num_followers, date_updated)"
+ + " VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)";
+ SQLiteStatement stmt = ReaderDatabase.getWritableDb().compileStatement(sql);
+ try {
+ stmt.bindLong (1, blogInfo.blogId);
+ stmt.bindLong (2, blogInfo.feedId);
+ stmt.bindString(3, blogInfo.getUrl());
+ stmt.bindString(4, blogInfo.getImageUrl());
+ stmt.bindString(5, blogInfo.getFeedUrl());
+ stmt.bindString(6, blogInfo.getName());
+ stmt.bindString(7, blogInfo.getDescription());
+ stmt.bindLong (8, SqlUtils.boolToSql(blogInfo.isPrivate));
+ stmt.bindLong (9, SqlUtils.boolToSql(blogInfo.isJetpack));
+ stmt.bindLong (10, SqlUtils.boolToSql(blogInfo.isFollowing));
+ stmt.bindLong (11, blogInfo.numSubscribers);
+ stmt.bindString(12, DateTimeUtils.iso8601FromDate(new Date()));
+ stmt.execute();
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /*
+ * returns blogInfo for all followed blogs
+ */
+ public static ReaderBlogList getFollowedBlogs() {
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(
+ "SELECT * FROM tbl_blog_info WHERE is_following!=0 ORDER BY name COLLATE NOCASE, blog_url",
+ null);
+ try {
+ ReaderBlogList blogs = new ReaderBlogList();
+ if (c.moveToFirst()) {
+ do {
+ ReaderBlog blogInfo = getBlogInfoFromCursor(c);
+ blogs.add(blogInfo);
+ } while (c.moveToNext());
+ }
+ return blogs;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ /*
+ * set followed blogs from the read/following/mine endpoint
+ */
+ public static void setFollowedBlogs(ReaderBlogList followedBlogs) {
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ try {
+ // first set all existing blogs to not followed
+ db.execSQL("UPDATE tbl_blog_info SET is_following=0");
+
+ // then insert passed ones
+ if (followedBlogs != null) {
+ for (ReaderBlog blog: followedBlogs) {
+ addOrUpdateBlog(blog);
+ }
+ }
+
+ db.setTransactionSuccessful();
+
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /*
+ * return list of URLs of followed blogs
+ */
+ public static ReaderUrlList getFollowedBlogUrls() {
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT DISTINCT blog_url FROM tbl_blog_info WHERE is_following!=0", null);
+ try {
+ ReaderUrlList urls = new ReaderUrlList();
+ if (c.moveToFirst()) {
+ do {
+ urls.add(c.getString(0));
+ } while (c.moveToNext());
+ }
+ return urls;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ /*
+ * sets the follow state for passed blog without creating a record for it if it doesn't exist
+ */
+ public static void setIsFollowedBlogId(long blogId, boolean isFollowed) {
+ ReaderDatabase.getWritableDb().execSQL(
+ "UPDATE tbl_blog_info SET is_following="
+ + SqlUtils.boolToSql(isFollowed)
+ + " WHERE blog_id=?",
+ new String[]{Long.toString(blogId)});
+ }
+
+ public static void setIsFollowedFeedId(long feedId, boolean isFollowed) {
+ ReaderDatabase.getWritableDb().execSQL(
+ "UPDATE tbl_blog_info SET is_following="
+ + SqlUtils.boolToSql(isFollowed)
+ + " WHERE feed_id=?",
+ new String[]{Long.toString(feedId)});
+ }
+
+ public static boolean hasFollowedBlogs() {
+ String sql = "SELECT 1 FROM tbl_blog_info WHERE is_following!=0 LIMIT 1";
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(), sql, null);
+ }
+
+ public static boolean isFollowedBlogUrl(String blogUrl) {
+ if (TextUtils.isEmpty(blogUrl)) {
+ return false;
+ }
+ String sql = "SELECT 1 FROM tbl_blog_info WHERE is_following!=0 AND blog_url=?";
+ String[] args = {UrlUtils.normalizeUrl(blogUrl)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ public static boolean isFollowedBlog(long blogId) {
+ String sql = "SELECT 1 FROM tbl_blog_info WHERE is_following!=0 AND blog_id=?";
+ String[] args = {Long.toString(blogId)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ public static boolean isFollowedFeedUrl(String feedUrl) {
+ if (TextUtils.isEmpty(feedUrl)) {
+ return false;
+ }
+ String sql = "SELECT 1 FROM tbl_blog_info WHERE is_following!=0 AND feed_url=?";
+ String[] args = {UrlUtils.normalizeUrl(feedUrl)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ public static boolean isFollowedFeed(long feedId) {
+ String sql = "SELECT 1 FROM tbl_blog_info WHERE is_following!=0 AND feed_id=?";
+ String[] args = {Long.toString(feedId)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ public static ReaderRecommendBlogList getRecommendedBlogs() {
+ String sql = " SELECT * FROM tbl_recommended_blogs ORDER BY title";
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(sql, null);
+ try {
+ ReaderRecommendBlogList blogs = new ReaderRecommendBlogList();
+ if (c.moveToFirst()) {
+ do {
+ ReaderRecommendedBlog blog = new ReaderRecommendedBlog();
+ blog.blogId = c.getLong(c.getColumnIndex("blog_id"));
+ blog.followRecoId = c.getLong(c.getColumnIndex("follow_reco_id"));
+ blog.score = c.getInt(c.getColumnIndex("score"));
+ blog.setTitle(c.getString(c.getColumnIndex("title")));
+ blog.setBlogUrl(c.getString(c.getColumnIndex("blog_url")));
+ blog.setImageUrl(c.getString(c.getColumnIndex("image_url")));
+ blog.setReason(c.getString(c.getColumnIndex("reason")));
+ blogs.add(blog);
+ } while (c.moveToNext());
+ }
+ return blogs;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static void setRecommendedBlogs(ReaderRecommendBlogList blogs) {
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ SQLiteStatement stmt = db.compileStatement(
+ "INSERT INTO tbl_recommended_blogs"
+ + " (blog_id, follow_reco_id, score, title, blog_url, image_url, reason)"
+ + " VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)");
+ db.beginTransaction();
+ try {
+ try {
+ // first delete all recommended blogs
+ SqlUtils.deleteAllRowsInTable(db, "tbl_recommended_blogs");
+
+ // then insert the passed ones
+ if (blogs != null && blogs.size() > 0) {
+ for (ReaderRecommendedBlog blog : blogs) {
+ stmt.bindLong (1, blog.blogId);
+ stmt.bindLong (2, blog.followRecoId);
+ stmt.bindLong (3, blog.score);
+ stmt.bindString(4, blog.getTitle());
+ stmt.bindString(5, blog.getBlogUrl());
+ stmt.bindString(6, blog.getImageUrl());
+ stmt.bindString(7, blog.getReason());
+ stmt.execute();
+ }
+ }
+ db.setTransactionSuccessful();
+
+ } catch (SQLException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ db.endTransaction();
+ }
+ }
+
+ /*
+ * determine whether the passed blog info should be updated based on when it was last updated
+ */
+ public static boolean isTimeToUpdateBlogInfo(ReaderBlog blogInfo) {
+ int minutes = minutesSinceLastUpdate(blogInfo);
+ if (minutes == NEVER_UPDATED) {
+ return true;
+ }
+ return (minutes >= ReaderConstants.READER_AUTO_UPDATE_DELAY_MINUTES);
+ }
+
+ private static String getBlogInfoLastUpdated(ReaderBlog blogInfo) {
+ if (blogInfo == null) {
+ return "";
+ }
+ if (blogInfo.blogId != 0) {
+ String[] args = {Long.toString(blogInfo.blogId)};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT date_updated FROM tbl_blog_info WHERE blog_id=?",
+ args);
+ } else {
+ String[] args = {Long.toString(blogInfo.feedId)};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT date_updated FROM tbl_blog_info WHERE feed_id=?",
+ args);
+ }
+ }
+
+ private static final int NEVER_UPDATED = -1;
+ private static int minutesSinceLastUpdate(ReaderBlog blogInfo) {
+ if (blogInfo == null) {
+ return 0;
+ }
+
+ String updated = getBlogInfoLastUpdated(blogInfo);
+ if (TextUtils.isEmpty(updated)) {
+ return NEVER_UPDATED;
+ }
+
+ Date dtUpdated = DateTimeUtils.dateFromIso8601(updated);
+ if (dtUpdated == null) {
+ return 0;
+ }
+
+ Date dtNow = new Date();
+ return DateTimeUtils.minutesBetween(dtUpdated, dtNow);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderCommentTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderCommentTable.java
new file mode 100644
index 000000000..dc61bbb3a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderCommentTable.java
@@ -0,0 +1,336 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderCommentList;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.util.SqlUtils;
+
+/**
+ * stores comments on reader posts
+ */
+public class ReaderCommentTable {
+ private static final String COLUMN_NAMES =
+ " blog_id,"
+ + " post_id,"
+ + " comment_id,"
+ + " parent_id,"
+ + " author_name,"
+ + " author_avatar,"
+ + " author_url,"
+ + " author_id,"
+ + " author_blog_id,"
+ + " published,"
+ + " timestamp,"
+ + " status,"
+ + " text,"
+ + " num_likes,"
+ + " is_liked,"
+ + " page_number";
+
+
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_comments ("
+ + " blog_id INTEGER DEFAULT 0,"
+ + " post_id INTEGER DEFAULT 0,"
+ + " comment_id INTEGER DEFAULT 0,"
+ + " parent_id INTEGER DEFAULT 0,"
+ + " author_name TEXT,"
+ + " author_avatar TEXT,"
+ + " author_url TEXT,"
+ + " author_id INTEGER DEFAULT 0,"
+ + " author_blog_id INTEGER DEFAULT 0,"
+ + " published TEXT,"
+ + " timestamp INTEGER DEFAULT 0,"
+ + " status TEXT,"
+ + " text TEXT,"
+ + " num_likes INTEGER DEFAULT 0,"
+ + " is_liked INTEGER DEFAULT 0,"
+ + " page_number INTEGER DEFAULT 0,"
+ + " PRIMARY KEY (blog_id, post_id, comment_id))");
+ db.execSQL("CREATE INDEX idx_page_number ON tbl_comments(page_number)");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_comments");
+ }
+
+ protected static void reset(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+
+ protected static int purge(SQLiteDatabase db) {
+ // purge comments attached to posts that no longer exist
+ int numDeleted = db.delete("tbl_comments", "post_id NOT IN (SELECT DISTINCT post_id FROM tbl_posts)", null);
+
+ // purge all but the first page of comments
+ numDeleted += db.delete("tbl_comments", "page_number != 1", null);
+
+ return numDeleted;
+ }
+
+ public static boolean isEmpty() {
+ return (getNumComments()==0);
+ }
+
+ private static int getNumComments() {
+ long count = SqlUtils.getRowCount(ReaderDatabase.getReadableDb(), "tbl_comments");
+ return (int)count;
+ }
+
+ /*
+ * returns the highest page_number for comments on the passed post
+ */
+ public static int getLastPageNumberForPost(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT MAX(page_number) FROM tbl_comments WHERE blog_id=? AND post_id=?", args);
+ }
+
+ /*
+ * returns the page number for a specific comment
+ */
+ public static int getPageNumberForComment(long blogId, long postId, long commentId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId), Long.toString(commentId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT page_number FROM tbl_comments WHERE blog_id=? AND post_id=? AND comment_id=?", args);
+ }
+
+ /*
+ * removes all comments for the passed post
+ */
+ public static void purgeCommentsForPost(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ ReaderDatabase.getWritableDb().delete("tbl_comments", "blog_id=? AND post_id=?", args);
+ }
+
+ /*
+ * returns the #comments stored locally for this post, which may differ from ReaderPostTable.getNumCommentsOnPost
+ * (which is the #comments the server says exist for this post)
+ */
+ public static int getNumCommentsForPost(ReaderPost post) {
+ if (post == null) {
+ return 0;
+ }
+ return getNumCommentsForPost(post.blogId, post.postId);
+ }
+ private static int getNumCommentsForPost(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(), "SELECT count(*) FROM tbl_comments WHERE blog_id=? AND post_id=?", args);
+ }
+
+ public static ReaderCommentList getCommentsForPost(ReaderPost post) {
+ if (post == null) {
+ return new ReaderCommentList();
+ }
+
+ String[] args = {Long.toString(post.blogId), Long.toString(post.postId)};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_comments WHERE blog_id=? AND post_id=? ORDER BY timestamp", args);
+ try {
+ ReaderCommentList comments = new ReaderCommentList();
+ if (c.moveToFirst()) {
+ do {
+ comments.add(getCommentFromCursor(c));
+ } while (c.moveToNext());
+ }
+ return comments;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static void addOrUpdateComment(ReaderComment comment) {
+ if (comment == null) {
+ return;
+ }
+ ReaderCommentList comments = new ReaderCommentList();
+ comments.add(comment);
+ addOrUpdateComments(comments);
+ }
+
+ public static void addOrUpdateComments(ReaderCommentList comments) {
+ if (comments == null || comments.size() == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ SQLiteStatement stmt = db.compileStatement("INSERT OR REPLACE INTO tbl_comments ("
+ + COLUMN_NAMES
+ + ") VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16)");
+ try {
+ for (ReaderComment comment: comments) {
+ stmt.bindLong (1, comment.blogId);
+ stmt.bindLong (2, comment.postId);
+ stmt.bindLong (3, comment.commentId);
+ stmt.bindLong (4, comment.parentId);
+ stmt.bindString(5, comment.getAuthorName());
+ stmt.bindString(6, comment.getAuthorAvatar());
+ stmt.bindString(7, comment.getAuthorUrl());
+ stmt.bindLong (8, comment.authorId);
+ stmt.bindLong (9, comment.authorBlogId);
+ stmt.bindString(10, comment.getPublished());
+ stmt.bindLong (11, comment.timestamp);
+ stmt.bindString(12, comment.getStatus());
+ stmt.bindString(13, comment.getText());
+ stmt.bindLong (14, comment.numLikes);
+ stmt.bindLong (15, SqlUtils.boolToSql(comment.isLikedByCurrentUser));
+ stmt.bindLong (16, comment.pageNumber);
+
+ stmt.execute();
+ }
+
+ db.setTransactionSuccessful();
+
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ public static ReaderComment getComment(long blogId, long postId, long commentId) {
+ String[] args = new String[] {Long.toString(blogId), Long.toString(postId), Long.toString(commentId)};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(
+ "SELECT * FROM tbl_comments WHERE blog_id=? AND post_id=? AND comment_id=? LIMIT 1", args);
+ try {
+ if (!c.moveToFirst()) {
+ return null;
+ }
+ return getCommentFromCursor(c);
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static void deleteComment(ReaderPost post, long commentId) {
+ if (post == null) {
+ return;
+ }
+ String[] args = {Long.toString(post.blogId), Long.toString(post.postId), Long.toString(commentId)};
+ ReaderDatabase.getWritableDb().delete("tbl_comments", "blog_id=? AND post_id=? AND comment_id=?", args);
+ }
+
+ /*
+ * returns true if any of the passed comments don't already exist
+ * IMPORTANT: assumes passed comments are all for the same post
+ */
+ public static boolean hasNewComments(ReaderCommentList comments) {
+ if (comments == null || comments.size() == 0) {
+ return false;
+ }
+
+ StringBuilder sb = new StringBuilder(
+ "SELECT COUNT(*) FROM tbl_comments WHERE blog_id=? AND post_id=? AND comment_id IN (");
+ boolean isFirst = true;
+ for (ReaderComment comment: comments) {
+ if (isFirst) {
+ isFirst = false;
+ } else {
+ sb.append(",");
+ }
+ sb.append(comment.commentId);
+ }
+ sb.append(")");
+
+ String[] args = {Long.toString(comments.get(0).blogId),
+ Long.toString(comments.get(0).postId)};
+ int numExisting = SqlUtils.intForQuery(ReaderDatabase.getReadableDb(), sb.toString(), args);
+ return numExisting != comments.size();
+ }
+
+ /*
+ * returns the #likes known to exist for this comment
+ */
+ public static int getNumLikesForComment(long blogId, long postId, long commentId) {
+ String[] args = {Long.toString(blogId),
+ Long.toString(postId),
+ Long.toString(commentId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT num_likes FROM tbl_comments WHERE blog_id=? AND post_id=? AND comment_id=?",
+ args);
+ }
+
+ /*
+ * updates both the like count for a comment and whether it's liked by the current user
+ */
+ public static void setLikesForComment(ReaderComment comment, int numLikes, boolean isLikedByCurrentUser) {
+ if (comment == null) {
+ return;
+ }
+
+ String[] args =
+ {Long.toString(comment.blogId),
+ Long.toString(comment.postId),
+ Long.toString(comment.commentId)};
+
+ ContentValues values = new ContentValues();
+ values.put("num_likes", numLikes);
+ values.put("is_liked", SqlUtils.boolToSql(isLikedByCurrentUser));
+
+ ReaderDatabase.getWritableDb().update(
+ "tbl_comments",
+ values,
+ "blog_id=? AND post_id=? AND comment_id=?",
+ args);
+ }
+
+ public static boolean isCommentLikedByCurrentUser(ReaderComment comment) {
+ if (comment == null) {
+ return false;
+ }
+ return isCommentLikedByCurrentUser(comment.blogId, comment.postId, comment.commentId);
+ }
+ public static boolean isCommentLikedByCurrentUser(long blogId, long postId, long commentId) {
+ String[] args = {Long.toString(blogId),
+ Long.toString(postId),
+ Long.toString(commentId)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT is_liked FROM tbl_comments WHERE blog_id=? AND post_id=? and comment_id=?",
+ args);
+ }
+
+ public static boolean commentExists(long blogId, long postId, long commentId) {
+ String[] args = {Long.toString(blogId),
+ Long.toString(postId),
+ Long.toString(commentId)};
+
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT 1 FROM tbl_comments WHERE blog_id=? AND post_id=? AND comment_id=?", args);
+ }
+
+ private static ReaderComment getCommentFromCursor(Cursor c) {
+ if (c == null) {
+ throw new IllegalArgumentException("null comment cursor");
+ }
+
+ ReaderComment comment = new ReaderComment();
+
+ comment.commentId = c.getLong(c.getColumnIndex("comment_id"));
+ comment.blogId = c.getLong(c.getColumnIndex("blog_id"));
+ comment.postId = c.getLong(c.getColumnIndex("post_id"));
+ comment.parentId = c.getLong(c.getColumnIndex("parent_id"));
+
+ comment.setPublished(c.getString(c.getColumnIndex("published")));
+ comment.timestamp = c.getLong(c.getColumnIndex("timestamp"));
+
+ comment.setAuthorAvatar(c.getString(c.getColumnIndex("author_avatar")));
+ comment.setAuthorName(c.getString(c.getColumnIndex("author_name")));
+ comment.setAuthorUrl(c.getString(c.getColumnIndex("author_url")));
+ comment.authorId = c.getLong(c.getColumnIndex("author_id"));
+ comment.authorBlogId = c.getLong(c.getColumnIndex("author_blog_id"));
+
+ comment.setStatus(c.getString(c.getColumnIndex("status")));
+ comment.setText(c.getString(c.getColumnIndex("text")));
+
+ comment.numLikes = c.getInt(c.getColumnIndex("num_likes"));
+ comment.isLikedByCurrentUser = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_liked")));
+ comment.pageNumber = c.getInt(c.getColumnIndex("page_number"));
+
+ return comment;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java
new file mode 100644
index 000000000..9ae4f5521
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java
@@ -0,0 +1,255 @@
+package org.wordpress.android.datasets;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * database for all reader information
+ */
+public class ReaderDatabase extends SQLiteOpenHelper {
+ protected static final String DB_NAME = "wpreader.db";
+ private static final int DB_VERSION = 125;
+
+ /*
+ * version history
+ * 67 - added tbl_blog_info to ReaderBlogTable
+ * 68 - added author_blog_id to ReaderCommentTable
+ * 69 - renamed tbl_blog_urls to tbl_followed_blogs in ReaderBlogTable
+ * 70 - added author_id to ReaderCommentTable and ReaderPostTable
+ * 71 - added blog_id to ReaderUserTable
+ * 72 - removed tbl_followed_blogs from ReaderBlogTable
+ * 73 - added tbl_recommended_blogs to ReaderBlogTable
+ * 74 - added primary_tag to ReaderPostTable
+ * 75 - added secondary_tag to ReaderPostTable
+ * 76 - added feed_id to ReaderBlogTable
+ * 77 - restructured tag tables (ReaderTagTable)
+ * 78 - added tag_type to ReaderPostTable.tbl_post_tags
+ * 79 - added is_likes_enabled and is_sharing_enabled to tbl_posts
+ * 80 - added tbl_comment_likes in ReaderLikeTable, added num_likes to tbl_comments
+ * 81 - added image_url to tbl_blog_info
+ * 82 - added idx_posts_timestamp to tbl_posts
+ * 83 - removed tag_list from tbl_posts
+ * 84 - added tbl_attachments
+ * 85 - removed tbl_attachments, added attachments_json to tbl_posts
+ * 90 - added default values for all INTEGER columns that were missing them (hotfix 3.1.1)
+ * 92 - added default values for all INTEGER columns that were missing them (3.2)
+ * 93 - tbl_posts text is now truncated to a max length (3.3)
+ * 94 - added is_jetpack to tbl_posts (3.4)
+ * 95 - added page_number to tbl_comments (3.4)
+ * 96 - removed tbl_tag_updates, added date_updated to tbl_tags (3.4)
+ * 97 - added short_url to tbl_posts
+ * 98 - added feed_id to tbl_posts
+ * 99 - added feed_url to tbl_blog_info
+ * 100 - changed primary key on tbl_blog_info
+ * 101 - dropped is_reblogged from ReaderPostTable
+ * 102 - changed primary key of tbl_blog_info from blog_id+feed_id to just blog_id
+ * 103 - added discover_json to ReaderPostTable
+ * 104 - added word_count to ReaderPostTable
+ * 105 - added date_updated to ReaderBlogTable
+ * 106 - dropped is_likes_enabled and is_sharing_enabled from tbl_posts
+ * 107 - "Blogs I Follow" renamed to "Followed Sites"
+ * 108 - added "has_gap_marker" to tbl_post_tags
+ * 109 - added "feed_item_id" to tbl_posts
+ * 110 - added xpost_post_id and xpost_blog_id to tbl_posts
+ * 111 - added author_first_name to tbl_posts
+ * 112 - no structural change, just reset db
+ * 113 - added tag_title to tag tables
+ * 114 - renamed tag_name to tag_slug in tag tables
+ * 115 - added ReaderSearchTable
+ * 116 - added tag_display_name to tag tables
+ * 117 - changed tbl_posts.timestamp from INTEGER to REAL
+ * 118 - renamed tbl_search_history to tbl_search_suggestions
+ * 119 - renamed tbl_posts.timestamp to sort_index
+ * 120 - added "format" to tbl_posts
+ * 121 - removed word_count from tbl_posts
+ * 122 - changed tbl_posts primary key to pseudo_id
+ * 123 - changed tbl_posts.published to tbl_posts.date
+ * 124 - returned tbl_posts.published
+ * 125 - added tbl_posts.railcar_json
+ */
+
+ /*
+ * database singleton
+ */
+ private static ReaderDatabase mReaderDb;
+ private final static Object mDbLock = new Object();
+ public static ReaderDatabase getDatabase() {
+ if (mReaderDb == null) {
+ synchronized(mDbLock) {
+ if (mReaderDb == null) {
+ mReaderDb = new ReaderDatabase(WordPress.getContext());
+ // this ensures that onOpen() is called with a writable database (open will fail if app calls getReadableDb() first)
+ mReaderDb.getWritableDatabase();
+ }
+ }
+ }
+ return mReaderDb;
+ }
+
+ public static SQLiteDatabase getReadableDb() {
+ return getDatabase().getReadableDatabase();
+ }
+ public static SQLiteDatabase getWritableDb() {
+ return getDatabase().getWritableDatabase();
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ super.onOpen(db);
+ //copyDatabase(db);
+ }
+
+ /*
+ * resets (clears) the reader database
+ */
+ public static void reset() {
+ // note that we must call getWritableDb() before getDatabase() in case the database
+ // object hasn't been created yet
+ SQLiteDatabase db = getWritableDb();
+ getDatabase().reset(db);
+ }
+
+ public ReaderDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createAllTables(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // for now just reset the db when upgrading, future versions may want to avoid this
+ // and modify table structures, etc., on upgrade while preserving data
+ AppLog.i(T.READER, "Upgrading database from version " + oldVersion + " to version " + newVersion);
+ reset(db);
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // IMPORTANT: do NOT call super() here - doing so throws a SQLiteException
+ AppLog.w(T.READER, "Downgrading database from version " + oldVersion + " to version " + newVersion);
+ reset(db);
+ }
+
+ private void createAllTables(SQLiteDatabase db) {
+ ReaderCommentTable.createTables(db);
+ ReaderLikeTable.createTables(db);
+ ReaderPostTable.createTables(db);
+ ReaderTagTable.createTables(db);
+ ReaderUserTable.createTables(db);
+ ReaderThumbnailTable.createTables(db);
+ ReaderBlogTable.createTables(db);
+ ReaderSearchTable.createTables(db);
+ }
+
+ private void dropAllTables(SQLiteDatabase db) {
+ ReaderCommentTable.dropTables(db);
+ ReaderLikeTable.dropTables(db);
+ ReaderPostTable.dropTables(db);
+ ReaderTagTable.dropTables(db);
+ ReaderUserTable.dropTables(db);
+ ReaderThumbnailTable.dropTables(db);
+ ReaderBlogTable.dropTables(db);
+ ReaderSearchTable.dropTables(db);
+ }
+
+ /*
+ * drop & recreate all tables (essentially clears the db of all data)
+ */
+ private void reset(SQLiteDatabase db) {
+ db.beginTransaction();
+ try {
+ dropAllTables(db);
+ createAllTables(db);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /*
+ * purge older/unattached data - use purgeAsync() to do this in the background
+ */
+ private static void purge() {
+ SQLiteDatabase db = getWritableDb();
+ db.beginTransaction();
+ try {
+ int numPostsDeleted = ReaderPostTable.purge(db);
+
+ // don't bother purging other data unless posts were purged
+ if (numPostsDeleted > 0) {
+ AppLog.i(T.READER, String.format("%d total posts purged", numPostsDeleted));
+
+ // purge unattached comments
+ int numCommentsDeleted = ReaderCommentTable.purge(db);
+ if (numCommentsDeleted > 0) {
+ AppLog.i(T.READER, String.format("%d comments purged", numCommentsDeleted));
+ }
+
+ // purge unattached likes
+ int numLikesDeleted = ReaderLikeTable.purge(db);
+ if (numLikesDeleted > 0) {
+ AppLog.i(T.READER, String.format("%d likes purged", numLikesDeleted));
+ }
+
+ // purge unattached thumbnails
+ int numThumbsPurged = ReaderThumbnailTable.purge(db);
+ if (numThumbsPurged > 0) {
+ AppLog.i(T.READER, String.format("%d thumbnails purged", numThumbsPurged));
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public static void purgeAsync() {
+ new Thread() {
+ @Override
+ public void run() {
+ purge();
+ }
+ }.start();
+ }
+
+ /*
+ * used during development to copy database to external storage so we can access it via DDMS
+ */
+ private void copyDatabase(SQLiteDatabase db) {
+ String copyFrom = db.getPath();
+ String copyTo = WordPress.getContext().getExternalFilesDir(null).getAbsolutePath() + "/" + DB_NAME;
+
+ try {
+ InputStream input = new FileInputStream(copyFrom);
+ OutputStream output = new FileOutputStream(copyTo);
+
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+
+ output.flush();
+ output.close();
+ input.close();
+ } catch (IOException e) {
+ AppLog.e(T.DB, "failed to copy reader database", e);
+ }
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java
new file mode 100644
index 000000000..7144bdaab
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java
@@ -0,0 +1,222 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderUserIdList;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.SqlUtils;
+
+/**
+ * stores likes for Reader posts and comments
+ */
+public class ReaderLikeTable {
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_post_likes ("
+ + " post_id INTEGER DEFAULT 0,"
+ + " blog_id INTEGER DEFAULT 0,"
+ + " user_id INTEGER DEFAULT 0,"
+ + " PRIMARY KEY (blog_id, post_id, user_id))");
+
+ db.execSQL("CREATE TABLE tbl_comment_likes ("
+ + " comment_id INTEGER DEFAULT 0,"
+ + " blog_id INTEGER DEFAULT 0,"
+ + " user_id INTEGER DEFAULT 0,"
+ + " PRIMARY KEY (blog_id, comment_id, user_id))");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_post_likes");
+ db.execSQL("DROP TABLE IF EXISTS tbl_comment_likes");
+ }
+
+ protected static void reset(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+
+ /*
+ * purge likes attached to posts/comments that no longer exist
+ */
+ protected static int purge(SQLiteDatabase db) {
+ int numDeleted = db.delete("tbl_post_likes", "post_id NOT IN (SELECT DISTINCT post_id FROM tbl_posts)", null);
+ numDeleted += db.delete("tbl_comment_likes", "comment_id NOT IN (SELECT DISTINCT comment_id FROM tbl_comments)", null);
+ return numDeleted;
+ }
+
+ /*
+ * returns userIds of users who like the passed post
+ */
+ public static ReaderUserIdList getLikesForPost(ReaderPost post) {
+ ReaderUserIdList userIds = new ReaderUserIdList();
+ if (post == null) {
+ return userIds;
+ }
+
+ String[] args = {Long.toString(post.blogId), Long.toString(post.postId)};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT user_id FROM tbl_post_likes WHERE blog_id=? AND post_id=?", args);
+ try {
+ if (c.moveToFirst()) {
+ do {
+ userIds.add(c.getLong(0));
+ } while (c.moveToNext());
+ }
+
+ return userIds;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static int getNumLikesForPost(ReaderPost post) {
+ if (post == null) {
+ return 0;
+ }
+ String[] args = {Long.toString(post.blogId), Long.toString(post.postId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(), "SELECT count(*) FROM tbl_post_likes WHERE blog_id=? AND post_id=?", args);
+ }
+
+ public static void setCurrentUserLikesPost(ReaderPost post, boolean isLiked) {
+ if (post == null) {
+ return;
+ }
+ long currentUserId = AccountHelper.getDefaultAccount().getUserId();
+ if (isLiked) {
+ ContentValues values = new ContentValues();
+ values.put("blog_id", post.blogId);
+ values.put("post_id", post.postId);
+ values.put("user_id", currentUserId);
+ ReaderDatabase.getWritableDb().insert("tbl_post_likes", null, values);
+ } else {
+ String args[] = {Long.toString(post.blogId), Long.toString(post.postId), Long.toString(currentUserId)};
+ ReaderDatabase.getWritableDb().delete("tbl_post_likes", "blog_id=? AND post_id=? AND user_id=?", args);
+ }
+ }
+
+ public static void setLikesForPost(ReaderPost post, ReaderUserIdList userIds) {
+ if (post == null) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ SQLiteStatement stmt = db.compileStatement("INSERT INTO tbl_post_likes (blog_id, post_id, user_id) VALUES (?1,?2,?3)");
+ try {
+ // first delete all likes for this post
+ String[] args = {Long.toString(post.blogId), Long.toString(post.postId)};
+ db.delete("tbl_post_likes", "blog_id=? AND post_id=?", args);
+
+ // now insert the passed likes
+ if (userIds != null) {
+ stmt.bindLong(1, post.blogId);
+ stmt.bindLong(2, post.postId);
+ for (Long userId: userIds) {
+ stmt.bindLong(3, userId);
+ stmt.execute();
+ }
+ }
+
+ db.setTransactionSuccessful();
+
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+
+ /****
+ * comment likes
+ */
+
+ public static ReaderUserIdList getLikesForComment(ReaderComment comment) {
+ ReaderUserIdList userIds = new ReaderUserIdList();
+ if (comment == null) {
+ return userIds;
+ }
+
+ String[] args = {Long.toString(comment.blogId),
+ Long.toString(comment.commentId)};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(
+ "SELECT user_id FROM tbl_comment_likes WHERE blog_id=? AND comment_id=?", args);
+ try {
+ if (c.moveToFirst()) {
+ do {
+ userIds.add(c.getLong(0));
+ } while (c.moveToNext());
+ }
+
+ return userIds;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static int getNumLikesForComment(ReaderComment comment) {
+ if (comment == null) {
+ return 0;
+ }
+ String[] args = {Long.toString(comment.blogId),
+ Long.toString(comment.commentId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT count(*) FROM tbl_comment_likes WHERE blog_id=? AND comment_id=?", args);
+ }
+
+ public static void setCurrentUserLikesComment(ReaderComment comment, boolean isLiked) {
+ if (comment == null) {
+ return;
+ }
+
+ long currentUserId = AccountHelper.getDefaultAccount().getUserId();
+ if (isLiked) {
+ ContentValues values = new ContentValues();
+ values.put("blog_id", comment.blogId);
+ values.put("comment_id", comment.commentId);
+ values.put("user_id", currentUserId);
+ ReaderDatabase.getWritableDb().insert("tbl_comment_likes", null, values);
+ } else {
+ String args[] = {Long.toString(comment.blogId),
+ Long.toString(comment.commentId),
+ Long.toString(currentUserId)};
+ ReaderDatabase.getWritableDb().delete("tbl_comment_likes",
+ "blog_id=? AND comment_id=? AND user_id=?", args);
+ }
+ }
+
+ public static void setLikesForComment(ReaderComment comment, ReaderUserIdList userIds) {
+ if (comment == null) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ SQLiteStatement stmt = db.compileStatement(
+ "INSERT INTO tbl_comment_likes (blog_id, comment_id, user_id) VALUES (?1,?2,?3)");
+ try {
+ String[] args = {Long.toString(comment.blogId),
+ Long.toString(comment.commentId)};
+ db.delete("tbl_comment_likes", "blog_id=? AND comment_id=?", args);
+
+ if (userIds != null) {
+ stmt.bindLong(1, comment.blogId);
+ stmt.bindLong(2, comment.commentId);
+ for (Long userId: userIds) {
+ stmt.bindLong(3, userId);
+ stmt.execute();
+ }
+ }
+
+ db.setTransactionSuccessful();
+
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+}
+
+
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java
new file mode 100644
index 000000000..af9b06891
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java
@@ -0,0 +1,933 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostIdList;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.CrashlyticsUtils;
+import org.wordpress.android.util.SqlUtils;
+
+/**
+ * tbl_posts contains all reader posts
+ * tbl_post_tags stores the association between posts and tags (posts can exist in more than one tag)
+ *
+ */
+public class ReaderPostTable {
+ private static final String COLUMN_NAMES =
+ "post_id," // 1
+ + "blog_id," // 2
+ + "feed_id," // 3
+ + "feed_item_id," // 4
+ + "pseudo_id," // 5
+ + "author_name," // 6
+ + "author_first_name," // 7
+ + "author_id," // 8
+ + "title," // 9
+ + "text," // 10
+ + "excerpt," // 11
+ + "format," // 12
+ + "url," // 13
+ + "short_url," // 14
+ + "blog_url," // 15
+ + "blog_name," // 16
+ + "featured_image," // 17
+ + "featured_video," // 18
+ + "post_avatar," // 19
+ + "sort_index," // 20
+ + "date," // 21
+ + "published," // 22
+ + "num_replies," // 23
+ + "num_likes," // 24
+ + "is_liked," // 25
+ + "is_followed," // 26
+ + "is_comments_open," // 27
+ + "is_external," // 28
+ + "is_private," // 29
+ + "is_videopress," // 30
+ + "is_jetpack," // 31
+ + "primary_tag," // 32
+ + "secondary_tag," // 33
+ + "attachments_json," // 34
+ + "discover_json," // 35
+ + "xpost_post_id," // 36
+ + "xpost_blog_id," // 37
+ + "railcar_json"; // 38
+
+ // used when querying multiple rows and skipping tbl_posts.text
+ private static final String COLUMN_NAMES_NO_TEXT =
+ "tbl_posts.post_id," // 1
+ + "tbl_posts.blog_id," // 2
+ + "tbl_posts.feed_id," // 3
+ + "tbl_posts.feed_item_id," // 4
+ + "tbl_posts.author_id," // 5
+ + "tbl_posts.pseudo_id," // 6
+ + "tbl_posts.author_name," // 7
+ + "tbl_posts.author_first_name," // 8
+ + "tbl_posts.blog_name," // 9
+ + "tbl_posts.blog_url," // 10
+ + "tbl_posts.excerpt," // 11
+ + "tbl_posts.format," // 12
+ + "tbl_posts.featured_image," // 13
+ + "tbl_posts.featured_video," // 14
+ + "tbl_posts.title," // 15
+ + "tbl_posts.url," // 16
+ + "tbl_posts.short_url," // 17
+ + "tbl_posts.post_avatar," // 18
+ + "tbl_posts.sort_index," // 19
+ + "tbl_posts.date," // 20
+ + "tbl_posts.published," // 21
+ + "tbl_posts.num_replies," // 22
+ + "tbl_posts.num_likes," // 23
+ + "tbl_posts.is_liked," // 24
+ + "tbl_posts.is_followed," // 25
+ + "tbl_posts.is_comments_open," // 26
+ + "tbl_posts.is_external," // 27
+ + "tbl_posts.is_private," // 28
+ + "tbl_posts.is_videopress," // 29
+ + "tbl_posts.is_jetpack," // 30
+ + "tbl_posts.primary_tag," // 31
+ + "tbl_posts.secondary_tag," // 32
+ + "tbl_posts.attachments_json," // 33
+ + "tbl_posts.discover_json," // 34
+ + "tbl_posts.xpost_post_id," // 35
+ + "tbl_posts.xpost_blog_id," // 36
+ + "tbl_posts.railcar_json"; // 37
+
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_posts ("
+ + " post_id INTEGER DEFAULT 0,"
+ + " blog_id INTEGER DEFAULT 0,"
+ + " feed_id INTEGER DEFAULT 0,"
+ + " feed_item_id INTEGER DEFAULT 0,"
+ + " pseudo_id TEXT NOT NULL,"
+ + " author_name TEXT,"
+ + " author_first_name TEXT,"
+ + " author_id INTEGER DEFAULT 0,"
+ + " title TEXT,"
+ + " text TEXT,"
+ + " excerpt TEXT,"
+ + " format TEXT,"
+ + " url TEXT,"
+ + " short_url TEXT,"
+ + " blog_url TEXT,"
+ + " blog_name TEXT,"
+ + " featured_image TEXT,"
+ + " featured_video TEXT,"
+ + " post_avatar TEXT,"
+ + " sort_index REAL DEFAULT 0,"
+ + " date TEXT,"
+ + " published TEXT,"
+ + " num_replies INTEGER DEFAULT 0,"
+ + " num_likes INTEGER DEFAULT 0,"
+ + " is_liked INTEGER DEFAULT 0,"
+ + " is_followed INTEGER DEFAULT 0,"
+ + " is_comments_open INTEGER DEFAULT 0,"
+ + " is_external INTEGER DEFAULT 0,"
+ + " is_private INTEGER DEFAULT 0,"
+ + " is_videopress INTEGER DEFAULT 0,"
+ + " is_jetpack INTEGER DEFAULT 0,"
+ + " primary_tag TEXT,"
+ + " secondary_tag TEXT,"
+ + " attachments_json TEXT,"
+ + " discover_json TEXT,"
+ + " xpost_post_id INTEGER DEFAULT 0,"
+ + " xpost_blog_id INTEGER DEFAULT 0,"
+ + " railcar_json TEXT,"
+ + " PRIMARY KEY (pseudo_id)"
+ + ")");
+
+ db.execSQL("CREATE UNIQUE INDEX idx_posts_post_id_blog_id ON tbl_posts(post_id, blog_id)");
+ db.execSQL("CREATE INDEX idx_posts_sort_index ON tbl_posts(sort_index)");
+
+ db.execSQL("CREATE TABLE tbl_post_tags ("
+ + " post_id INTEGER DEFAULT 0,"
+ + " blog_id INTEGER DEFAULT 0,"
+ + " feed_id INTEGER DEFAULT 0,"
+ + " pseudo_id TEXT NOT NULL,"
+ + " tag_name TEXT NOT NULL COLLATE NOCASE,"
+ + " tag_type INTEGER DEFAULT 0,"
+ + " has_gap_marker INTEGER DEFAULT 0,"
+ + " PRIMARY KEY (pseudo_id, tag_name, tag_type)"
+ + ")");
+
+ db.execSQL("CREATE INDEX idx_post_tags_tag_name ON tbl_post_tags(tag_name)");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_posts");
+ db.execSQL("DROP TABLE IF EXISTS tbl_post_tags");
+ }
+
+ protected static void reset(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+
+ /*
+ * purge table of unattached/older posts - no need to wrap this in a transaction since it's
+ * only called from ReaderDatabase.purge() which already creates a transaction
+ */
+ protected static int purge(SQLiteDatabase db) {
+ // delete posts in tbl_post_tags attached to tags that no longer exist
+ int numDeleted = db.delete("tbl_post_tags", "tag_name NOT IN (SELECT DISTINCT tag_name FROM tbl_tags)", null);
+
+ // delete excess posts on a per-tag basis
+ ReaderTagList tags = ReaderTagTable.getAllTags();
+ for (ReaderTag tag: tags) {
+ numDeleted += purgePostsForTag(db, tag);
+ }
+
+ // delete search results
+ numDeleted += purgeSearchResults(db);
+
+ // delete posts in tbl_posts that no longer exist in tbl_post_tags
+ numDeleted += db.delete("tbl_posts", "pseudo_id NOT IN (SELECT DISTINCT pseudo_id FROM tbl_post_tags)", null);
+
+ return numDeleted;
+ }
+
+ /*
+ * purge excess posts in the passed tag
+ */
+ private static final int MAX_POSTS_PER_TAG = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY;
+ private static int purgePostsForTag(SQLiteDatabase db, ReaderTag tag) {
+ int numPosts = getNumPostsWithTag(tag);
+ if (numPosts <= MAX_POSTS_PER_TAG) {
+ return 0;
+ }
+
+ int numToPurge = numPosts - MAX_POSTS_PER_TAG;
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt()), Integer.toString(numToPurge)};
+ String where = "pseudo_id IN ("
+ + " SELECT tbl_posts.pseudo_id FROM tbl_posts, tbl_post_tags"
+ + " WHERE tbl_posts.pseudo_id = tbl_post_tags.pseudo_id"
+ + " AND tbl_post_tags.tag_name=?"
+ + " AND tbl_post_tags.tag_type=?"
+ + " ORDER BY tbl_posts.sort_index"
+ + " LIMIT ?"
+ + ")";
+ int numDeleted = db.delete("tbl_post_tags", where, args);
+ AppLog.d(AppLog.T.READER, String.format("reader post table > purged %d posts in tag %s", numDeleted, tag.getTagNameForLog()));
+ return numDeleted;
+ }
+
+ /*
+ * purge all posts that were retained from previous searches
+ */
+ private static int purgeSearchResults(SQLiteDatabase db) {
+ String[] args = {Integer.toString(ReaderTagType.SEARCH.toInt())};
+ return db.delete("tbl_post_tags", "tag_type=?", args);
+ }
+
+ public static int getNumPostsInBlog(long blogId) {
+ if (blogId == 0) {
+ return 0;
+ }
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT count(*) FROM tbl_posts WHERE blog_id=?",
+ new String[]{Long.toString(blogId)});
+ }
+
+ public static int getNumPostsInFeed(long feedId) {
+ if (feedId == 0) {
+ return 0;
+ }
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT count(*) FROM tbl_posts WHERE feed_id=?",
+ new String[]{Long.toString(feedId)});
+ }
+
+ public static int getNumPostsWithTag(ReaderTag tag) {
+ if (tag == null) {
+ return 0;
+ }
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT count(*) FROM tbl_post_tags WHERE tag_name=? AND tag_type=?",
+ args);
+ }
+
+ public static void addOrUpdatePost(ReaderPost post) {
+ if (post == null) {
+ return;
+ }
+ ReaderPostList posts = new ReaderPostList();
+ posts.add(post);
+ addOrUpdatePosts(null, posts);
+ }
+
+ public static ReaderPost getPost(long blogId, long postId, boolean excludeTextColumn) {
+
+ String columns = (excludeTextColumn ? COLUMN_NAMES_NO_TEXT : "*");
+ String sql = "SELECT " + columns + " FROM tbl_posts WHERE blog_id=? AND post_id=? LIMIT 1";
+
+ String[] args = new String[] {Long.toString(blogId), Long.toString(postId)};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ try {
+ if (!c.moveToFirst()) {
+ return null;
+ }
+ return getPostFromCursor(c);
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static String getPostTitle(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT title FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ public static String getPostText(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT text FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ public static boolean postExists(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT 1 FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ /*
+ * returns whether any of the passed posts are new or changed - used after posts are retrieved
+ */
+ public static ReaderActions.UpdateResult comparePosts(ReaderPostList posts) {
+ if (posts == null || posts.size() == 0) {
+ return ReaderActions.UpdateResult.UNCHANGED;
+ }
+
+ boolean hasChanges = false;
+ for (ReaderPost post: posts) {
+ ReaderPost existingPost = getPost(post.blogId, post.postId, true);
+ if (existingPost == null) {
+ return ReaderActions.UpdateResult.HAS_NEW;
+ } else if (!hasChanges && !post.isSamePost(existingPost)) {
+ hasChanges = true;
+ }
+ }
+
+ return (hasChanges ? ReaderActions.UpdateResult.CHANGED : ReaderActions.UpdateResult.UNCHANGED);
+ }
+
+ /*
+ * returns true if any posts in the passed list exist in this list
+ */
+ public static boolean hasOverlap(ReaderPostList posts) {
+ for (ReaderPost post: posts) {
+ if (postExists(post.blogId, post.postId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /*
+ * returns the #comments known to exist for this post (ie: #comments the server says this post has), which
+ * may differ from ReaderCommentTable.getNumCommentsForPost (which returns # local comments for this post)
+ */
+ public static int getNumCommentsForPost(ReaderPost post) {
+ if (post == null) {
+ return 0;
+ }
+ String[] args = new String[] {Long.toString(post.blogId), Long.toString(post.postId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT num_replies FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ /*
+ * returns the #likes known to exist for this post (ie: #likes the server says this post has), which
+ * may differ from ReaderPostTable.getNumLikesForPost (which returns # local likes for this post)
+ */
+ public static int getNumLikesForPost(long blogId, long postId) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.intForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT num_likes FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ public static boolean isPostLikedByCurrentUser(ReaderPost post) {
+ return post != null && isPostLikedByCurrentUser(post.blogId, post.postId);
+ }
+ public static boolean isPostLikedByCurrentUser(long blogId, long postId) {
+ String[] args = new String[] {Long.toString(blogId), Long.toString(postId)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT is_liked FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ /*
+ * updates both the like count for a post and whether it's liked by the current user
+ */
+ public static void setLikesForPost(ReaderPost post, int numLikes, boolean isLikedByCurrentUser) {
+ if (post == null) {
+ return;
+ }
+
+ String[] args = {Long.toString(post.blogId), Long.toString(post.postId)};
+
+ ContentValues values = new ContentValues();
+ values.put("num_likes", numLikes);
+ values.put("is_liked", SqlUtils.boolToSql(isLikedByCurrentUser));
+
+ ReaderDatabase.getWritableDb().update(
+ "tbl_posts",
+ values,
+ "blog_id=? AND post_id=?",
+ args);
+ }
+
+
+ public static boolean isPostFollowed(ReaderPost post) {
+ if (post == null) {
+ return false;
+ }
+ String[] args = new String[] {Long.toString(post.blogId), Long.toString(post.postId)};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT is_followed FROM tbl_posts WHERE blog_id=? AND post_id=?",
+ args);
+ }
+
+ public static int deletePostsWithTag(final ReaderTag tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ // first delete posts from tbl_post_tags, and if any were deleted next delete posts in tbl_posts that no longer exist in tbl_post_tags
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ int numDeleted = ReaderDatabase.getWritableDb().delete("tbl_post_tags",
+ "tag_name=? AND tag_type=?",
+ args);
+
+ if (numDeleted > 0)
+ ReaderDatabase.getWritableDb().delete("tbl_posts",
+ "pseudo_id NOT IN (SELECT DISTINCT pseudo_id FROM tbl_post_tags)",
+ null);
+
+ return numDeleted;
+ }
+
+ public static int deletePostsInBlog(long blogId) {
+ String[] args = {Long.toString(blogId)};
+ return ReaderDatabase.getWritableDb().delete("tbl_posts", "blog_id = ?", args);
+ }
+
+ /*
+ * ensure that posts in blogs that are no longer followed don't have their followed status
+ * set to true
+ */
+ public static void updateFollowedStatus() {
+ SQLiteStatement statement = ReaderDatabase.getWritableDb().compileStatement(
+ "UPDATE tbl_posts SET is_followed = 0"
+ + " WHERE is_followed != 0"
+ + " AND blog_id NOT IN (SELECT DISTINCT blog_id FROM tbl_blog_info WHERE is_followed != 0)");
+ try {
+ int count = statement.executeUpdateDelete();
+ if (count > 0) {
+ AppLog.d(AppLog.T.READER, String.format("reader post table > marked %d posts unfollowed", count));
+ }
+ } finally {
+ statement.close();
+ }
+ }
+
+ /*
+ * returns the iso8601 date of the oldest post with the passed tag
+ */
+ public static String getOldestDateWithTag(final ReaderTag tag) {
+ if (tag == null) {
+ return "";
+ }
+
+ String sql = "SELECT tbl_posts.date FROM tbl_posts, tbl_post_tags"
+ + " WHERE tbl_posts.pseudo_id = tbl_post_tags.pseudo_id"
+ + " AND tbl_post_tags.tag_name=? AND tbl_post_tags.tag_type=?"
+ + " ORDER BY date LIMIT 1";
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ /*
+ * returns the iso8601 date of the oldest post in the passed blog
+ */
+ public static String getOldestDateInBlog(long blogId) {
+ String sql = "SELECT date FROM tbl_posts"
+ + " WHERE blog_id = ?"
+ + " ORDER BY date LIMIT 1";
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(), sql, new String[]{Long.toString(blogId)});
+ }
+
+ public static String getOldestDateInFeed(long feedId) {
+ String sql = "SELECT date FROM tbl_posts"
+ + " WHERE feed_id = ?"
+ + " ORDER BY date LIMIT 1";
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(), sql, new String[]{Long.toString(feedId)});
+ }
+
+ public static void removeGapMarkerForTag(final ReaderTag tag) {
+ if (tag == null) return;
+
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ String sql = "UPDATE tbl_post_tags SET has_gap_marker=0 WHERE has_gap_marker!=0 AND tag_name=? AND tag_type=?";
+ ReaderDatabase.getWritableDb().execSQL(sql, args);
+ }
+
+ /*
+ * returns the blogId/postId of the post with the passed tag that has a gap marker, or null if none exists
+ */
+ public static ReaderBlogIdPostId getGapMarkerIdsForTag(final ReaderTag tag) {
+ if (tag == null) {
+ return null;
+ }
+
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ String sql = "SELECT blog_id, post_id FROM tbl_post_tags WHERE has_gap_marker!=0 AND tag_name=? AND tag_type=?";
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ try {
+ if (cursor.moveToFirst()) {
+ long blogId = cursor.getLong(0);
+ long postId = cursor.getLong(1);
+ return new ReaderBlogIdPostId(blogId, postId);
+ } else {
+ return null;
+ }
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ public static void setGapMarkerForTag(long blogId, long postId, ReaderTag tag) {
+ if (tag == null) return;
+
+ String[] args = {
+ Long.toString(blogId),
+ Long.toString(postId),
+ tag.getTagSlug(),
+ Integer.toString(tag.tagType.toInt())
+ };
+ String sql = "UPDATE tbl_post_tags SET has_gap_marker=1 WHERE blog_id=? AND post_id=? AND tag_name=? AND tag_type=?";
+ ReaderDatabase.getWritableDb().execSQL(sql, args);
+ }
+
+ public static String getGapMarkerDateForTag(ReaderTag tag) {
+ ReaderBlogIdPostId ids = getGapMarkerIdsForTag(tag);
+ if (ids == null) {
+ return null;
+ }
+ String[] args = {Long.toString(ids.getBlogId()), Long.toString(ids.getPostId())};
+ String sql = "SELECT date FROM tbl_posts WHERE blog_id=? AND post_id=?";
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ private static long getGapMarkerSortIndexForTag(ReaderTag tag) {
+ ReaderBlogIdPostId ids = getGapMarkerIdsForTag(tag);
+ if (ids == null) {
+ return 0;
+ }
+
+ String[] args = {Long.toString(ids.getBlogId()), Long.toString(ids.getPostId())};
+ String sql = "SELECT sort_index FROM tbl_posts WHERE blog_id=? AND post_id=?";
+ return SqlUtils.longForQuery(ReaderDatabase.getReadableDb(), sql, args);
+ }
+
+ /*
+ * delete posts with the passed tag that come before the one with the gap marker for
+ * this tag - note this may leave some stray posts in tbl_posts, but these will
+ * be cleaned up by the next purge
+ */
+ public static void deletePostsBeforeGapMarkerForTag(ReaderTag tag) {
+ long sortIndex = getGapMarkerSortIndexForTag(tag);
+ if (sortIndex == 0) return;
+
+ String[] args = {Long.toString(sortIndex), tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ String where = "pseudo_id IN (SELECT tbl_posts.pseudo_id FROM tbl_posts, tbl_post_tags"
+ + " WHERE tbl_posts.sort_index < ?"
+ + " AND tbl_posts.pseudo_id = tbl_post_tags.pseudo_id"
+ + " AND tbl_post_tags.tag_name=? AND tbl_post_tags.tag_type=?)";
+ int numDeleted = ReaderDatabase.getWritableDb().delete("tbl_post_tags", where, args);
+ AppLog.d(AppLog.T.READER, "removed " + numDeleted + " posts older than gap marker");
+ }
+
+ public static void setFollowStatusForPostsInBlog(long blogId, boolean isFollowed) {
+ setFollowStatusForPosts(blogId, 0, isFollowed);
+ }
+ public static void setFollowStatusForPostsInFeed(long feedId, boolean isFollowed) {
+ setFollowStatusForPosts(0, feedId, isFollowed);
+ }
+ private static void setFollowStatusForPosts(long blogId, long feedId, boolean isFollowed) {
+ if (blogId == 0 && feedId == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ try {
+ if (blogId != 0) {
+ String sql = "UPDATE tbl_posts SET is_followed=" + SqlUtils.boolToSql(isFollowed)
+ + " WHERE blog_id=?";
+ db.execSQL(sql, new String[]{Long.toString(blogId)});
+ } else {
+ String sql = "UPDATE tbl_posts SET is_followed=" + SqlUtils.boolToSql(isFollowed)
+ + " WHERE feed_id=?";
+ db.execSQL(sql, new String[]{Long.toString(feedId)});
+ }
+
+
+ // if blog/feed is no longer followed, remove its posts tagged with "Followed Sites" in
+ // tbl_post_tags
+ if (!isFollowed) {
+ if (blogId != 0) {
+ db.delete("tbl_post_tags", "blog_id=? AND tag_name=?",
+ new String[]{Long.toString(blogId), ReaderTag.TAG_TITLE_FOLLOWED_SITES});
+ } else {
+ db.delete("tbl_post_tags", "feed_id=? AND tag_name=?",
+ new String[]{Long.toString(feedId), ReaderTag.TAG_TITLE_FOLLOWED_SITES});
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /*
+ * Android's CursorWindow has a max size of 2MB per row which can be exceeded
+ * with a very large text column, causing an IllegalStateException when the
+ * row is read - prevent this by limiting the amount of text that's stored in
+ * the text column - note that this situation very rarely occurs
+ * https://github.com/android/platform_frameworks_base/blob/b77bc869241644a662f7e615b0b00ecb5aee373d/core/res/res/values/config.xml#L1268
+ * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java/android/database/CursorWindow.java#L103
+ */
+ private static final int MAX_TEXT_LEN = (1024 * 1024) / 2;
+ private static String maxText(final ReaderPost post) {
+ if (post.getText().length() <= MAX_TEXT_LEN) {
+ return post.getText();
+ }
+ // if the post has an excerpt (which should always be the case), store it as the full text
+ // with a link to the full article
+ if (post.hasExcerpt()) {
+ AppLog.w(AppLog.T.READER, "reader post table > max text exceeded, storing excerpt");
+ return "<p>" + post.getExcerpt() + "</p>"
+ + String.format("<p style='text-align:center'><a href='%s'>%s</a></p>",
+ post.getUrl(), WordPress.getContext().getString(R.string.reader_label_view_original));
+ } else {
+ AppLog.w(AppLog.T.READER, "reader post table > max text exceeded, storing truncated text");
+ return post.getText().substring(0, MAX_TEXT_LEN);
+ }
+ }
+
+ public static void addOrUpdatePosts(final ReaderTag tag, ReaderPostList posts) {
+ if (posts == null || posts.size() == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ SQLiteStatement stmtPosts = db.compileStatement(
+ "INSERT OR REPLACE INTO tbl_posts ("
+ + COLUMN_NAMES
+ + ") VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29,?30,?31,?32,?33,?34,?35,?36,?37,?38)");
+ SQLiteStatement stmtTags = db.compileStatement(
+ "INSERT OR REPLACE INTO tbl_post_tags (post_id, blog_id, feed_id, pseudo_id, tag_name, tag_type) VALUES (?1,?2,?3,?4,?5,?6)");
+
+ db.beginTransaction();
+ try {
+ // first insert into tbl_posts
+ for (ReaderPost post: posts) {
+ stmtPosts.bindLong (1, post.postId);
+ stmtPosts.bindLong (2, post.blogId);
+ stmtPosts.bindLong (3, post.feedId);
+ stmtPosts.bindLong (4, post.feedItemId);
+ stmtPosts.bindString(5, post.getPseudoId());
+ stmtPosts.bindString(6, post.getAuthorName());
+ stmtPosts.bindString(7, post.getAuthorFirstName());
+ stmtPosts.bindLong (8, post.authorId);
+ stmtPosts.bindString(9, post.getTitle());
+ stmtPosts.bindString(10, maxText(post));
+ stmtPosts.bindString(11, post.getExcerpt());
+ stmtPosts.bindString(12, post.getFormat());
+ stmtPosts.bindString(13, post.getUrl());
+ stmtPosts.bindString(14, post.getShortUrl());
+ stmtPosts.bindString(15, post.getBlogUrl());
+ stmtPosts.bindString(16, post.getBlogName());
+ stmtPosts.bindString(17, post.getFeaturedImage());
+ stmtPosts.bindString(18, post.getFeaturedVideo());
+ stmtPosts.bindString(19, post.getPostAvatar());
+ stmtPosts.bindDouble(20, post.sortIndex);
+ stmtPosts.bindString(21, post.getDate());
+ stmtPosts.bindString(22, post.getPubDate());
+ stmtPosts.bindLong (23, post.numReplies);
+ stmtPosts.bindLong (24, post.numLikes);
+ stmtPosts.bindLong (25, SqlUtils.boolToSql(post.isLikedByCurrentUser));
+ stmtPosts.bindLong (26, SqlUtils.boolToSql(post.isFollowedByCurrentUser));
+ stmtPosts.bindLong (27, SqlUtils.boolToSql(post.isCommentsOpen));
+ stmtPosts.bindLong (28, SqlUtils.boolToSql(post.isExternal));
+ stmtPosts.bindLong (29, SqlUtils.boolToSql(post.isPrivate));
+ stmtPosts.bindLong (30, SqlUtils.boolToSql(post.isVideoPress));
+ stmtPosts.bindLong (31, SqlUtils.boolToSql(post.isJetpack));
+ stmtPosts.bindString(32, post.getPrimaryTag());
+ stmtPosts.bindString(33, post.getSecondaryTag());
+ stmtPosts.bindString(34, post.getAttachmentsJson());
+ stmtPosts.bindString(35, post.getDiscoverJson());
+ stmtPosts.bindLong (36, post.xpostPostId);
+ stmtPosts.bindLong (37, post.xpostBlogId);
+ stmtPosts.bindString(38, post.getRailcarJson());
+ stmtPosts.execute();
+ }
+
+ // now add to tbl_post_tags if a tag was passed
+ if (tag != null) {
+ String tagName = tag.getTagSlug();
+ int tagType = tag.tagType.toInt();
+ for (ReaderPost post: posts) {
+ stmtTags.bindLong (1, post.postId);
+ stmtTags.bindLong (2, post.blogId);
+ stmtTags.bindLong (3, post.feedId);
+ stmtTags.bindString(4, post.getPseudoId());
+ stmtTags.bindString(5, tagName);
+ stmtTags.bindLong (6, tagType);
+ stmtTags.execute();
+ }
+ }
+
+ db.setTransactionSuccessful();
+
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmtPosts);
+ SqlUtils.closeStatement(stmtTags);
+ }
+ }
+
+ public static ReaderPostList getPostsWithTag(ReaderTag tag, int maxPosts, boolean excludeTextColumn) {
+ if (tag == null) {
+ return new ReaderPostList();
+ }
+
+ String columns = (excludeTextColumn ? COLUMN_NAMES_NO_TEXT : "tbl_posts.*");
+ String sql = "SELECT " + columns + " FROM tbl_posts, tbl_post_tags"
+ + " WHERE tbl_posts.pseudo_id = tbl_post_tags.pseudo_id"
+ + " AND tbl_post_tags.tag_name=?"
+ + " AND tbl_post_tags.tag_type=?";
+
+ if (tag.tagType == ReaderTagType.DEFAULT) {
+ // skip posts that are no longer liked if this is "Posts I Like", skip posts that are no
+ // longer followed if this is "Followed Sites"
+ if (tag.isPostsILike()) {
+ sql += " AND tbl_posts.is_liked != 0";
+ } else if (tag.isFollowedSites()) {
+ sql += " AND tbl_posts.is_followed != 0";
+ }
+ }
+
+ sql += " ORDER BY tbl_posts.sort_index DESC";
+
+ if (maxPosts > 0) {
+ sql += " LIMIT " + Integer.toString(maxPosts);
+ }
+
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ try {
+ return getPostListFromCursor(cursor);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ public static ReaderPostList getPostsInBlog(long blogId, int maxPosts, boolean excludeTextColumn) {
+ String columns = (excludeTextColumn ? COLUMN_NAMES_NO_TEXT : "tbl_posts.*");
+ String sql = "SELECT " + columns + " FROM tbl_posts WHERE blog_id = ? ORDER BY tbl_posts.sort_index DESC";
+
+ if (maxPosts > 0) {
+ sql += " LIMIT " + Integer.toString(maxPosts);
+ }
+
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery(sql, new String[]{Long.toString(blogId)});
+ try {
+ return getPostListFromCursor(cursor);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ public static ReaderPostList getPostsInFeed(long feedId, int maxPosts, boolean excludeTextColumn) {
+ String columns = (excludeTextColumn ? COLUMN_NAMES_NO_TEXT : "tbl_posts.*");
+ String sql = "SELECT " + columns + " FROM tbl_posts WHERE feed_id = ? ORDER BY tbl_posts.sort_index DESC";
+
+ if (maxPosts > 0) {
+ sql += " LIMIT " + Integer.toString(maxPosts);
+ }
+
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery(sql, new String[]{Long.toString(feedId)});
+ try {
+ return getPostListFromCursor(cursor);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ /*
+ * same as getPostsWithTag() but only returns the blogId/postId pairs
+ */
+ public static ReaderBlogIdPostIdList getBlogIdPostIdsWithTag(ReaderTag tag, int maxPosts) {
+ ReaderBlogIdPostIdList idList = new ReaderBlogIdPostIdList();
+ if (tag == null) {
+ return idList;
+ }
+
+ String sql = "SELECT tbl_posts.blog_id, tbl_posts.post_id FROM tbl_posts, tbl_post_tags"
+ + " WHERE tbl_posts.pseudo_id = tbl_post_tags.pseudo_id"
+ + " AND tbl_post_tags.tag_name=?"
+ + " AND tbl_post_tags.tag_type=?";
+
+ if (tag.tagType == ReaderTagType.DEFAULT) {
+ if (tag.isPostsILike()) {
+ sql += " AND tbl_posts.is_liked != 0";
+ } else if (tag.isFollowedSites()) {
+ sql += " AND tbl_posts.is_followed != 0";
+ }
+ }
+
+ sql += " ORDER BY tbl_posts.sort_index DESC";
+
+ if (maxPosts > 0) {
+ sql += " LIMIT " + Integer.toString(maxPosts);
+ }
+
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ idList.add(new ReaderBlogIdPostId(cursor.getLong(0), cursor.getLong(1)));
+ } while (cursor.moveToNext());
+ }
+ return idList;
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ /*
+ * same as getPostsInBlog() but only returns the blogId/postId pairs
+ */
+ public static ReaderBlogIdPostIdList getBlogIdPostIdsInBlog(long blogId, int maxPosts) {
+ String sql = "SELECT post_id FROM tbl_posts WHERE blog_id = ? ORDER BY tbl_posts.sort_index DESC";
+
+ if (maxPosts > 0) {
+ sql += " LIMIT " + Integer.toString(maxPosts);
+ }
+
+ Cursor cursor = ReaderDatabase.getReadableDb().rawQuery(sql, new String[]{Long.toString(blogId)});
+ try {
+ ReaderBlogIdPostIdList idList = new ReaderBlogIdPostIdList();
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ idList.add(new ReaderBlogIdPostId(blogId, cursor.getLong(0)));
+ } while (cursor.moveToNext());
+ }
+
+ return idList;
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+
+ private static ReaderPost getPostFromCursor(Cursor c) {
+ if (c == null) {
+ throw new IllegalArgumentException("getPostFromCursor > null cursor");
+ }
+
+ ReaderPost post = new ReaderPost();
+
+ // text column is skipped when retrieving multiple rows
+ int idxText = c.getColumnIndex("text");
+ if (idxText > -1) {
+ post.setText(c.getString(idxText));
+ }
+
+ post.postId = c.getLong(c.getColumnIndex("post_id"));
+ post.blogId = c.getLong(c.getColumnIndex("blog_id"));
+ post.feedId = c.getLong(c.getColumnIndex("feed_id"));
+ post.feedItemId = c.getLong(c.getColumnIndex("feed_item_id"));
+ post.authorId = c.getLong(c.getColumnIndex("author_id"));
+ post.setPseudoId(c.getString(c.getColumnIndex("pseudo_id")));
+
+ post.setAuthorName(c.getString(c.getColumnIndex("author_name")));
+ post.setAuthorFirstName(c.getString(c.getColumnIndex("author_first_name")));
+ post.setBlogName(c.getString(c.getColumnIndex("blog_name")));
+ post.setBlogUrl(c.getString(c.getColumnIndex("blog_url")));
+ post.setExcerpt(c.getString(c.getColumnIndex("excerpt")));
+ post.setFormat(c.getString(c.getColumnIndex("format")));
+ post.setFeaturedImage(c.getString(c.getColumnIndex("featured_image")));
+ post.setFeaturedVideo(c.getString(c.getColumnIndex("featured_video")));
+
+ post.setTitle(c.getString(c.getColumnIndex("title")));
+ post.setUrl(c.getString(c.getColumnIndex("url")));
+ post.setShortUrl(c.getString(c.getColumnIndex("short_url")));
+ post.setPostAvatar(c.getString(c.getColumnIndex("post_avatar")));
+
+ post.sortIndex = c.getDouble(c.getColumnIndex("sort_index"));
+ post.setDate(c.getString(c.getColumnIndex("date")));
+ post.setPubDate(c.getString(c.getColumnIndex("published")));
+
+ post.numReplies = c.getInt(c.getColumnIndex("num_replies"));
+ post.numLikes = c.getInt(c.getColumnIndex("num_likes"));
+
+ post.isLikedByCurrentUser = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_liked")));
+ post.isFollowedByCurrentUser = SqlUtils.sqlToBool(c.getInt( c.getColumnIndex("is_followed")));
+ post.isCommentsOpen = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_comments_open")));
+ post.isExternal = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_external")));
+ post.isPrivate = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_private")));
+ post.isVideoPress = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_videopress")));
+ post.isJetpack = SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("is_jetpack")));
+
+ post.setPrimaryTag(c.getString(c.getColumnIndex("primary_tag")));
+ post.setSecondaryTag(c.getString(c.getColumnIndex("secondary_tag")));
+
+ post.setAttachmentsJson(c.getString(c.getColumnIndex("attachments_json")));
+ post.setDiscoverJson(c.getString(c.getColumnIndex("discover_json")));
+
+ post.xpostPostId = c.getLong(c.getColumnIndex("xpost_post_id"));
+ post.xpostBlogId = c.getLong(c.getColumnIndex("xpost_blog_id"));
+
+ post.setRailcarJson(c.getString(c.getColumnIndex("railcar_json")));
+
+ return post;
+ }
+
+ private static ReaderPostList getPostListFromCursor(Cursor cursor) {
+ ReaderPostList posts = new ReaderPostList();
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ posts.add(getPostFromCursor(cursor));
+ } while (cursor.moveToNext());
+ }
+ } catch (IllegalStateException e) {
+ CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC);
+ AppLog.e(AppLog.T.READER, e);
+ }
+ return posts;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java
new file mode 100644
index 000000000..77c54cf98
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.datasets;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.SqlUtils;
+
+import java.util.Date;
+
+/**
+ * search suggestion table - populated by user's reader search history
+ */
+public class ReaderSearchTable {
+
+ public static final String COL_ID = "_id";
+ public static final String COL_QUERY = "query_string";
+
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_search_suggestions ("
+ + " _id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + " query_string TEXT NOT NULL COLLATE NOCASE,"
+ + " date_used TEXT)");
+ db.execSQL("CREATE UNIQUE INDEX idx_search_suggestions_query ON tbl_search_suggestions(query_string)");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_search_suggestions");
+ }
+
+ /*
+ * adds the passed query string, updating the usage date
+ */
+ public static void addOrUpdateQueryString(@NonNull String query) {
+ String date = DateTimeUtils.iso8601FromDate(new Date());
+
+ SQLiteStatement stmt = ReaderDatabase.getWritableDb().compileStatement(
+ "INSERT OR REPLACE INTO tbl_search_suggestions (query_string, date_used) VALUES (?1,?2)");
+ try {
+ stmt.bindString(1, query);
+ stmt.bindString(2, date);
+ stmt.execute();
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ public static void deleteQueryString(@NonNull String query) {
+ String[]args = new String[]{query};
+ ReaderDatabase.getWritableDb().delete("tbl_search_suggestions", "query_string=?", args);
+ }
+
+ public static void deleteAllQueries() {
+ SqlUtils.deleteAllRowsInTable(ReaderDatabase.getWritableDb(), "tbl_search_suggestions");
+ }
+
+ /**
+ * Returns a cursor containing query strings previously typed by the user
+ * @param filter - filters the list using LIKE syntax (pass null for no filter)
+ * @param max - limit the list to this many items (pass zero for no limit)
+ */
+ public static Cursor getQueryStringCursor(String filter, int max) {
+ String sql;
+ String[] args;
+ if (TextUtils.isEmpty(filter)) {
+ sql = "SELECT * FROM tbl_search_suggestions";
+ args = null;
+ } else {
+ sql = "SELECT * FROM tbl_search_suggestions WHERE query_string LIKE ?";
+ args = new String[]{filter + "%"};
+ }
+
+ sql += " ORDER BY date_used DESC";
+
+ if (max > 0) {
+ sql += " LIMIT " + max;
+ }
+
+ return ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java
new file mode 100644
index 000000000..b9f962c42
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java
@@ -0,0 +1,381 @@
+package org.wordpress.android.datasets;
+
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.text.TextUtils;
+
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.SqlUtils;
+
+import java.util.Date;
+
+/**
+ * tbl_tags stores the list of tags the user subscribed to or has by default
+ * tbl_tags_recommended stores the list of recommended tags returned by the api
+ */
+public class ReaderTagTable {
+
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_tags ("
+ + " tag_slug TEXT COLLATE NOCASE,"
+ + " tag_display_name TEXT COLLATE NOCASE,"
+ + " tag_title TEXT COLLATE NOCASE,"
+ + " tag_type INTEGER DEFAULT 0,"
+ + " endpoint TEXT,"
+ + " date_updated TEXT,"
+ + " PRIMARY KEY (tag_slug, tag_type)"
+ + ")");
+
+ db.execSQL("CREATE TABLE tbl_tags_recommended ("
+ + " tag_slug TEXT COLLATE NOCASE,"
+ + " tag_display_name TEXT COLLATE NOCASE,"
+ + " tag_title TEXT COLLATE NOCASE,"
+ + " tag_type INTEGER DEFAULT 0,"
+ + " endpoint TEXT,"
+ + " PRIMARY KEY (tag_slug, tag_type)"
+ + ")");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_tags");
+ db.execSQL("DROP TABLE IF EXISTS tbl_tags_recommended");
+ }
+
+ /*
+ * returns true if tbl_tags is empty
+ */
+ public static boolean isEmpty() {
+ return (SqlUtils.getRowCount(ReaderDatabase.getReadableDb(), "tbl_tags") == 0);
+ }
+
+ /*
+ * replaces all tags with the passed list
+ */
+ public static void replaceTags(ReaderTagList tags) {
+ if (tags == null || tags.size() == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ try {
+ try {
+ // first delete all existing tags, then insert the passed ones
+ db.execSQL("DELETE FROM tbl_tags");
+ addOrUpdateTags(tags);
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ AppLog.e(T.READER, e);
+ }
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /*
+ * similar to the above but only replaces followed tags
+ */
+ public static void replaceFollowedTags(ReaderTagList tags) {
+ if (tags == null || tags.size() == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ try {
+ try {
+ // first delete all existing followed tags, then insert the passed ones
+ String[] args = {Integer.toString(ReaderTagType.FOLLOWED.toInt())};
+ db.execSQL("DELETE FROM tbl_tags WHERE tag_type=?", args);
+ addOrUpdateTags(tags);
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ AppLog.e(T.READER, e);
+ }
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public static void addOrUpdateTag(ReaderTag tag) {
+ if (tag == null) {
+ return;
+ }
+ ReaderTagList tags = new ReaderTagList();
+ tags.add(tag);
+ addOrUpdateTags(tags);
+ }
+
+ private static void addOrUpdateTags(ReaderTagList tagList) {
+ if (tagList == null || tagList.size() == 0) {
+ return;
+ }
+ SQLiteStatement stmt = null;
+ try {
+ stmt = ReaderDatabase.getWritableDb().compileStatement(
+ "INSERT OR REPLACE INTO tbl_tags (tag_slug, tag_display_name, tag_title, tag_type, endpoint) VALUES (?1,?2,?3,?4,?5)"
+ );
+
+ for (ReaderTag tag: tagList) {
+ stmt.bindString(1, tag.getTagSlug());
+ stmt.bindString(2, tag.getTagDisplayName());
+ stmt.bindString(3, tag.getTagTitle());
+ stmt.bindLong (4, tag.tagType.toInt());
+ stmt.bindString(5, tag.getEndpoint());
+ stmt.execute();
+ }
+
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /*
+ * returns true if the passed tag exists, regardless of type
+ */
+ public static boolean tagExists(ReaderTag tag) {
+ if (tag == null) {
+ return false;
+ }
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT 1 FROM tbl_tags WHERE tag_slug=?1 AND tag_type=?2",
+ args);
+ }
+
+ /*
+ * returns true if the passed tag exists and it has the passed type
+ */
+ private static boolean tagExistsOfType(String tagSlug, ReaderTagType tagType) {
+ if (TextUtils.isEmpty(tagSlug) || tagType == null) {
+ return false;
+ }
+
+ String[] args = {tagSlug, Integer.toString(tagType.toInt())};
+ return SqlUtils.boolForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT 1 FROM tbl_tags WHERE tag_slug=?1 AND tag_type=?2",
+ args);
+ }
+
+ public static boolean isFollowedTagName(String tagSlug) {
+ return tagExistsOfType(tagSlug, ReaderTagType.FOLLOWED);
+ }
+
+ private static ReaderTag getTagFromCursor(Cursor c) {
+ if (c == null) {
+ throw new IllegalArgumentException("null tag cursor");
+ }
+
+ String tagSlug = c.getString(c.getColumnIndex("tag_slug"));
+ String tagDisplayName = c.getString(c.getColumnIndex("tag_display_name"));
+ String tagTitle = c.getString(c.getColumnIndex("tag_title"));
+ String endpoint = c.getString(c.getColumnIndex("endpoint"));
+ ReaderTagType tagType = ReaderTagType.fromInt(c.getInt(c.getColumnIndex("tag_type")));
+
+ return new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, tagType);
+ }
+
+ public static ReaderTag getTag(String tagSlug, ReaderTagType tagType) {
+ if (TextUtils.isEmpty(tagSlug)) {
+ return null;
+ }
+
+ String[] args = {tagSlug, Integer.toString(tagType.toInt())};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_tags WHERE tag_slug=? AND tag_type=? LIMIT 1", args);
+ try {
+ if (!c.moveToFirst()) {
+ return null;
+ }
+ return getTagFromCursor(c);
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static String getEndpointForTag(ReaderTag tag) {
+ if (tag == null) {
+ return null;
+ }
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT endpoint FROM tbl_tags WHERE tag_slug=? AND tag_type=?",
+ args);
+ }
+
+ public static ReaderTagList getDefaultTags() {
+ return getTagsOfType(ReaderTagType.DEFAULT);
+ }
+
+ public static ReaderTagList getFollowedTags() {
+ return getTagsOfType(ReaderTagType.FOLLOWED);
+ }
+
+ public static ReaderTagList getCustomListTags() {
+ return getTagsOfType(ReaderTagType.CUSTOM_LIST);
+ }
+
+ private static ReaderTagList getTagsOfType(ReaderTagType tagType) {
+ String[] args = {Integer.toString(tagType.toInt())};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_tags WHERE tag_type=? ORDER BY tag_slug", args);
+ try {
+ ReaderTagList tagList = new ReaderTagList();
+ if (c.moveToFirst()) {
+ do {
+ tagList.add(getTagFromCursor(c));
+ } while (c.moveToNext());
+ }
+ return tagList;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ static ReaderTagList getAllTags() {
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_tags ORDER BY tag_slug", null);
+ try {
+ ReaderTagList tagList = new ReaderTagList();
+ if (c.moveToFirst()) {
+ do {
+ tagList.add(getTagFromCursor(c));
+ } while (c.moveToNext());
+ }
+ return tagList;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static void deleteTag(ReaderTag tag) {
+ if (tag == null) {
+ return;
+ }
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ ReaderDatabase.getWritableDb().delete("tbl_tags", "tag_slug=? AND tag_type=?", args);
+ }
+
+
+ public static String getTagLastUpdated(ReaderTag tag) {
+ if (tag == null) {
+ return "";
+ }
+ String[] args = {tag.getTagSlug(), Integer.toString(tag.tagType.toInt())};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(),
+ "SELECT date_updated FROM tbl_tags WHERE tag_slug=? AND tag_type=?",
+ args);
+ }
+
+ public static void setTagLastUpdated(ReaderTag tag) {
+ if (tag == null) {
+ return;
+ }
+
+ String date = DateTimeUtils.iso8601FromDate(new Date());
+ String sql = "UPDATE tbl_tags SET date_updated=?1 WHERE tag_slug=?2 AND tag_type=?3";
+ SQLiteStatement stmt = ReaderDatabase.getWritableDb().compileStatement(sql);
+ try {
+ stmt.bindString(1, date);
+ stmt.bindString(2, tag.getTagSlug());
+ stmt.bindLong (3, tag.tagType.toInt());
+ stmt.execute();
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /*
+ * determine whether the passed tag should be auto-updated based on when it was last updated
+ */
+ public static boolean shouldAutoUpdateTag(ReaderTag tag) {
+ int minutes = minutesSinceLastUpdate(tag);
+ if (minutes == NEVER_UPDATED) {
+ return true;
+ }
+ return (minutes >= ReaderConstants.READER_AUTO_UPDATE_DELAY_MINUTES);
+ }
+
+ private static final int NEVER_UPDATED = -1;
+ private static int minutesSinceLastUpdate(ReaderTag tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ String updated = getTagLastUpdated(tag);
+ if (TextUtils.isEmpty(updated)) {
+ return NEVER_UPDATED;
+ }
+
+ Date dtUpdated = DateTimeUtils.dateFromIso8601(updated);
+ if (dtUpdated == null) {
+ return 0;
+ }
+
+ Date dtNow = new Date();
+ return DateTimeUtils.minutesBetween(dtUpdated, dtNow);
+ }
+
+ /**
+ * recommended tags - stored in a separate table from default/subscribed tags, but have the same column names
+ **/
+ public static ReaderTagList getRecommendedTags(boolean excludeSubscribed) {
+ Cursor c;
+ if (excludeSubscribed) {
+ c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_tags_recommended WHERE tag_slug NOT IN (SELECT tag_slug FROM tbl_tags) ORDER BY tag_slug", null);
+ } else {
+ c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_tags_recommended ORDER BY tag_slug", null);
+ }
+ try {
+ ReaderTagList tagList = new ReaderTagList();
+ if (c.moveToFirst()) {
+ do {
+ tagList.add(getTagFromCursor(c));
+ } while (c.moveToNext());
+ }
+ return tagList;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static void setRecommendedTags(ReaderTagList tagList) {
+ if (tagList == null) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ SQLiteStatement stmt = db.compileStatement
+ ("INSERT INTO tbl_tags_recommended (tag_slug, tag_display_name, tag_title, tag_type, endpoint) VALUES (?1,?2,?3,?4,?5)");
+ db.beginTransaction();
+ try {
+ try {
+ // first delete all recommended tags
+ db.execSQL("DELETE FROM tbl_tags_recommended");
+
+ // then insert the passed ones
+ for (ReaderTag tag: tagList) {
+ stmt.bindString(1, tag.getTagSlug());
+ stmt.bindString(2, tag.getTagDisplayName());
+ stmt.bindString(3, tag.getTagTitle());
+ stmt.bindLong (4, tag.tagType.toInt());
+ stmt.bindString(5, tag.getEndpoint());
+ stmt.execute();
+ }
+
+ db.setTransactionSuccessful();
+
+ } catch (SQLException e) {
+ AppLog.e(T.READER, e);
+ }
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ db.endTransaction();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderThumbnailTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderThumbnailTable.java
new file mode 100644
index 000000000..d9fd46307
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderThumbnailTable.java
@@ -0,0 +1,56 @@
+package org.wordpress.android.datasets;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.SqlUtils;
+
+/**
+ * stores thumbnail urls for videos embedded in Reader posts
+ */
+public class ReaderThumbnailTable {
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_thumbnails ("
+ + " full_url TEXT COLLATE NOCASE PRIMARY KEY,"
+ + " thumbnail_url TEXT NOT NULL,"
+ + " post_id INTEGER DEFAULT 0)");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_thumbnails");
+ }
+
+ /*
+ * purge table of thumbnails attached to posts that no longer exist
+ */
+ protected static int purge(SQLiteDatabase db) {
+ return db.delete("tbl_thumbnails", "post_id NOT IN (SELECT DISTINCT post_id FROM tbl_posts)", null);
+ }
+
+ public static void addThumbnail(long postId, String fullUrl, String thumbnailUrl) {
+ if (TextUtils.isEmpty(fullUrl) || TextUtils.isEmpty(thumbnailUrl))
+ return;
+
+ SQLiteStatement stmt = ReaderDatabase.getWritableDb().compileStatement("INSERT OR REPLACE INTO tbl_thumbnails (full_url, thumbnail_url, post_id) VALUES (?1,?2,?3)");
+ try {
+ stmt.bindString(1, fullUrl);
+ stmt.bindString(2, thumbnailUrl);
+ stmt.bindLong (3, postId);
+ stmt.execute();
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ public static String getThumbnailUrl(String fullUrl) {
+ if (TextUtils.isEmpty(fullUrl)) {
+ return null;
+ }
+ return SqlUtils.stringForQuery(
+ ReaderDatabase.getReadableDb(),
+ "SELECT thumbnail_url FROM tbl_thumbnails WHERE full_url=?",
+ new String[]{fullUrl});
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java
new file mode 100644
index 000000000..dcf94704e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java
@@ -0,0 +1,211 @@
+package org.wordpress.android.datasets;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.models.ReaderUser;
+import org.wordpress.android.models.ReaderUserIdList;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.SqlUtils;
+
+import java.util.ArrayList;
+
+/**
+ * stores info about the current user and liking users
+ */
+public class ReaderUserTable {
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_users ("
+ + " user_id INTEGER PRIMARY KEY,"
+ + " blog_id INTEGER DEFAULT 0,"
+ + " user_name TEXT,"
+ + " display_name TEXT COLLATE NOCASE,"
+ + " url TEXT,"
+ + " profile_url TEXT,"
+ + " avatar_url TEXT)");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_users");
+ }
+
+ public static void addOrUpdateUser(ReaderUser user) {
+ if (user==null)
+ return;
+
+ ReaderUserList users = new ReaderUserList();
+ users.add(user);
+ addOrUpdateUsers(users);
+ }
+
+ private static final String COLUMN_NAMES =
+ " user_id," // 1
+ + " blog_id," // 2
+ + " user_name," // 3
+ + " display_name," // 4
+ + " url," // 5
+ + " profile_url," // 6
+ + " avatar_url"; // 7
+
+ public static void addOrUpdateUsers(ReaderUserList users) {
+ if (users==null || users.size()==0)
+ return;
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ SQLiteStatement stmt = db.compileStatement("INSERT OR REPLACE INTO tbl_users (" + COLUMN_NAMES + ") VALUES (?1,?2,?3,?4,?5,?6,?7)");
+ try {
+ for (ReaderUser user: users) {
+ stmt.bindLong (1, user.userId);
+ stmt.bindLong (2, user.blogId);
+ stmt.bindString(3, user.getUserName());
+ stmt.bindString(4, user.getDisplayName());
+ stmt.bindString(5, user.getUrl());
+ stmt.bindString(6, user.getProfileUrl());
+ stmt.bindString(7, user.getAvatarUrl());
+ stmt.execute();
+ }
+
+ db.setTransactionSuccessful();
+
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /*
+ * returns avatar urls for the passed user ids - used by post detail to show avatars for liking users
+ */
+ public static ArrayList<String> getAvatarUrls(ReaderUserIdList userIds, int max, int avatarSz) {
+ ArrayList<String> avatars = new ArrayList<String>();
+ if (userIds==null || userIds.size()==0)
+ return avatars;
+
+ StringBuilder sb = new StringBuilder("SELECT user_id, avatar_url FROM tbl_users WHERE user_id IN (");
+
+ // make sure current user's avatar is returned if the passed list contains them - this is
+ // important since it may not otherwise be returned when a "max" is passed, and we want
+ // the current user to appear first in post detail when they like a post
+ long currentUserId = AccountHelper.getDefaultAccount().getUserId();
+ boolean containsCurrentUser = userIds.contains(currentUserId);
+ if (containsCurrentUser)
+ sb.append(currentUserId);
+
+ int numAdded = (containsCurrentUser ? 1 : 0);
+ for (Long id: userIds) {
+ // skip current user since we added them already
+ if (id!=currentUserId) {
+ if (numAdded > 0)
+ sb.append(",");
+ sb.append(id);
+ numAdded++;
+ if (max > 0 && numAdded >= max)
+ break;
+ }
+ }
+ sb.append(")");
+
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(sb.toString(), null);
+ try {
+ if (c.moveToFirst()) {
+ do {
+ long userId = c.getLong(0);
+ String url = GravatarUtils.fixGravatarUrl(c.getString(1), avatarSz);
+ // add current user to the top
+ if (userId==currentUserId) {
+ avatars.add(0, url);
+ } else {
+ avatars.add(url);
+ }
+ } while (c.moveToNext());
+ }
+ return avatars;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static ReaderUser getCurrentUser() {
+ return getUser(AccountHelper.getDefaultAccount().getUserId());
+ }
+
+ private static ReaderUser getUser(long userId) {
+ String args[] = {Long.toString(userId)};
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery("SELECT * FROM tbl_users WHERE user_id=?", args);
+ try {
+ if (!c.moveToFirst())
+ return null;
+ return getUserFromCursor(c);
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ private static String getAvatarForUser(long userId) {
+ String args[] = {Long.toString(userId)};
+ return SqlUtils.stringForQuery(ReaderDatabase.getReadableDb(), "SELECT avatar_url FROM tbl_users WHERE user_id=?", args);
+ }
+
+ public static ReaderUserList getUsersWhoLikePost(long blogId, long postId, int max) {
+ String[] args = {Long.toString(blogId), Long.toString(postId)};
+ String sql = "SELECT * from tbl_users WHERE user_id IN (SELECT user_id FROM tbl_post_likes WHERE blog_id=? AND post_id=?) ORDER BY display_name";
+ if (max > 0) {
+ sql += " LIMIT " + Integer.toString(max);
+ }
+
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ try {
+ ReaderUserList users = new ReaderUserList();
+ if (c.moveToFirst()) {
+ do {
+ users.add(getUserFromCursor(c));
+ } while (c.moveToNext());
+ }
+ return users;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static ReaderUserList getUsersWhoLikeComment(long blogId, long commentId, int max) {
+ String[] args = {Long.toString(blogId),
+ Long.toString(commentId)};
+ String sql = "SELECT * from tbl_users WHERE user_id IN"
+ + " (SELECT user_id FROM tbl_comment_likes WHERE blog_id=? AND comment_id=?)"
+ + " ORDER BY display_name";
+ if (max > 0) {
+ sql += " LIMIT " + Integer.toString(max);
+ }
+
+ Cursor c = ReaderDatabase.getReadableDb().rawQuery(sql, args);
+ try {
+ ReaderUserList users = new ReaderUserList();
+ if (c.moveToFirst()) {
+ do {
+ users.add(getUserFromCursor(c));
+ } while (c.moveToNext());
+ }
+ return users;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ private static ReaderUser getUserFromCursor(Cursor c) {
+ ReaderUser user = new ReaderUser();
+
+ user.userId = c.getLong(c.getColumnIndex("user_id"));
+ user.blogId = c.getLong(c.getColumnIndex("blog_id"));
+ user.setUserName(c.getString(c.getColumnIndex("user_name")));
+ user.setDisplayName(c.getString(c.getColumnIndex("display_name")));
+ user.setUrl(c.getString(c.getColumnIndex("url")));
+ user.setProfileUrl(c.getString(c.getColumnIndex("profile_url")));
+ user.setAvatarUrl(c.getString(c.getColumnIndex("avatar_url")));
+
+ return user;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/SQLTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/SQLTable.java
new file mode 100644
index 000000000..ba73f9ca3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/SQLTable.java
@@ -0,0 +1,68 @@
+package org.wordpress.android.datasets;
+
+import java.util.Map;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+
+/**
+ * A class to represent an database table.
+ */
+
+public abstract class SQLTable {
+ public abstract String getName();
+
+ protected abstract String getUniqueConstraint();
+
+ protected abstract Map<String, String> getColumnMapping();
+
+ protected static class BaseColumns {
+ protected final static String _ID = "_id";
+ }
+
+ public String toCreateQuery() {
+ String createQuery = "CREATE TABLE IF NOT EXISTS " + getName() + " (";
+
+ Map<String, String> columns = getColumnMapping();
+
+ for (String column : columns.keySet()) {
+ createQuery += column + " " + columns.get(column) + ", ";
+ }
+
+ createQuery += getUniqueConstraint() + ");";
+
+ return createQuery;
+ }
+
+ public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
+
+ public Cursor query(final SQLiteDatabase database, final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) {
+ return database.query(getName(), projection, selection, selectionArgs, null, null, sortOrder);
+ }
+
+ public long insert(final SQLiteDatabase database, final Uri uri, final ContentValues values) {
+ return database.insert(getName(), null, values);
+ }
+
+ public long insert(final SQLiteDatabase database, final ContentValues values) {
+ return insert(database, null, values);
+ }
+
+ public int update(final SQLiteDatabase database, final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) {
+ return database.update(getName(), values, selection, selectionArgs);
+ }
+
+ public int update(final SQLiteDatabase database, final ContentValues values, final String selection, final String[] selectionArgs) {
+ return update(database, null, values, selection, selectionArgs);
+ }
+
+ public int delete(final SQLiteDatabase database, final Uri uri, final String selection, final String[] selectionArgs) {
+ return database.delete(getName(), selection, selectionArgs);
+ }
+
+ public int delete(final SQLiteDatabase database, final String selection, final String[] selectionArgs) {
+ return delete(database, null, selection, selectionArgs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java
new file mode 100644
index 000000000..e872d4274
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java
@@ -0,0 +1,104 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.models.SiteSettingsModel;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class SiteSettingsTable {
+ public static final String CATEGORIES_TABLE_NAME = "site_categories";
+
+ private static final String CREATE_CATEGORIES_TABLE_SQL =
+ "CREATE TABLE IF NOT EXISTS " +
+ CATEGORIES_TABLE_NAME +
+ " (" +
+ CategoryModel.ID_COLUMN_NAME + " INTEGER PRIMARY KEY, " +
+ CategoryModel.NAME_COLUMN_NAME + " TEXT, " +
+ CategoryModel.SLUG_COLUMN_NAME + " TEXT, " +
+ CategoryModel.DESC_COLUMN_NAME + " TEXT, " +
+ CategoryModel.PARENT_ID_COLUMN_NAME + " INTEGER, " +
+ CategoryModel.POST_COUNT_COLUMN_NAME + " INTEGER" +
+ ");";
+
+ public static void createTable(SQLiteDatabase db) {
+ if (db != null) {
+ db.execSQL(SiteSettingsModel.CREATE_SETTINGS_TABLE_SQL);
+ db.execSQL(CREATE_CATEGORIES_TABLE_SQL);
+ }
+ }
+
+ public static Map<Integer, CategoryModel> getAllCategories() {
+ String sqlCommand = sqlSelectAllCategories() + ";";
+ Cursor cursor = WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null);
+
+ if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) return null;
+
+ Map<Integer, CategoryModel> models = new HashMap<>();
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ CategoryModel model = new CategoryModel();
+ model.deserializeFromDatabase(cursor);
+ models.put(model.id, model);
+ cursor.moveToNext();
+ }
+
+ return models;
+ }
+
+ public static Cursor getCategory(long id) {
+ if (id < 0) return null;
+
+ String sqlCommand = sqlSelectAllCategories() + sqlWhere(CategoryModel.ID_COLUMN_NAME, Long.toString(id)) + ";";
+ return WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null);
+ }
+
+ public static Cursor getSettings(long id) {
+ if (id < 0) return null;
+
+ String sqlCommand = sqlSelectAllSettings() + sqlWhere(SiteSettingsModel.ID_COLUMN_NAME, Long.toString(id)) + ";";
+ return WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null);
+ }
+
+ public static void saveCategory(CategoryModel category) {
+ if (category == null) return;
+
+ ContentValues values = category.serializeToDatabase();
+ category.isInLocalTable = WordPress.wpDB.getDatabase().insertWithOnConflict(
+ CATEGORIES_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1;
+ }
+
+ public static void saveCategories(CategoryModel[] categories) {
+ if (categories == null) return;
+
+ for (CategoryModel category : categories) {
+ saveCategory(category);
+ }
+ }
+
+ public static void saveSettings(SiteSettingsModel settings) {
+ if (settings == null) return;
+
+ ContentValues values = settings.serializeToDatabase();
+ settings.isInLocalTable = WordPress.wpDB.getDatabase().insertWithOnConflict(
+ SiteSettingsModel.SETTINGS_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1;
+
+ saveCategories(settings.categories);
+ }
+
+ private static String sqlSelectAllCategories() {
+ return "SELECT * FROM " + CATEGORIES_TABLE_NAME + " ";
+ }
+
+ private static String sqlSelectAllSettings() {
+ return "SELECT * FROM " + SiteSettingsModel.SETTINGS_TABLE_NAME + " ";
+ }
+
+ private static String sqlWhere(String variable, String value) {
+ return "WHERE " + variable + "=\"" + value + "\" ";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/SuggestionTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/SuggestionTable.java
new file mode 100644
index 000000000..27dd698a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/SuggestionTable.java
@@ -0,0 +1,173 @@
+package org.wordpress.android.datasets;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.models.Tag;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.SqlUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SuggestionTable {
+ private static final String SUGGESTIONS_TABLE = "suggestions";
+ private static final String TAXONOMY_TABLE = "taxonomy";
+
+ public static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + SUGGESTIONS_TABLE + " ("
+ + " site_id INTEGER DEFAULT 0,"
+ + " user_login TEXT,"
+ + " display_name TEXT,"
+ + " image_url TEXT,"
+ + " taxonomy TEXT,"
+ + " PRIMARY KEY (user_login)"
+ + " );");
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + TAXONOMY_TABLE + " ("
+ + " site_id INTEGER DEFAULT 0,"
+ + " tag TEXT,"
+ + " PRIMARY KEY (site_id, tag)"
+ + " );");
+ }
+
+ private static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + SUGGESTIONS_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + TAXONOMY_TABLE);
+ }
+
+ public static void reset(SQLiteDatabase db) {
+ AppLog.i(AppLog.T.SUGGESTION, "resetting suggestion tables");
+ dropTables(db);
+ createTables(db);
+ }
+
+ private static SQLiteDatabase getReadableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+ private static SQLiteDatabase getWritableDb() {
+ return WordPress.wpDB.getDatabase();
+ }
+
+ public static void insertSuggestionsForSite(final int siteId, final List<Suggestion> suggestions) {
+ // we want to delete the current suggestions, so that removed users will not show up as a suggestion
+ deleteSuggestionsForSite(siteId);
+
+ if (suggestions != null) {
+ for (Suggestion suggestion : suggestions) {
+ addSuggestion(suggestion);
+ }
+ }
+ }
+ public static void addSuggestion(final Suggestion suggestion) {
+ if (suggestion == null)
+ return;
+
+ ContentValues values = new ContentValues();
+ values.put("site_id", suggestion.siteID);
+ values.put("user_login", suggestion.getUserLogin());
+ values.put("display_name", suggestion.getDisplayName());
+ values.put("image_url", suggestion.getImageUrl());
+ values.put("taxonomy", suggestion.getTaxonomy());
+
+ getWritableDb().insertWithOnConflict(SUGGESTIONS_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public static List<Suggestion> getSuggestionsForSite(int siteId) {
+ List<Suggestion> suggestions = new ArrayList<Suggestion>();
+
+ String[] args = {Integer.toString(siteId)};
+ Cursor c = getReadableDb().rawQuery("SELECT * FROM " + SUGGESTIONS_TABLE + " WHERE site_id=? ORDER BY user_login ASC", args);
+
+ try {
+ if (c.moveToFirst()) {
+ do {
+ Suggestion comment = getSuggestionFromCursor(c);
+ suggestions.add(comment);
+ } while (c.moveToNext());
+ }
+
+ return suggestions;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static int deleteSuggestionsForSite(int siteId) {
+ return getWritableDb().delete(SUGGESTIONS_TABLE, "site_id=?", new String[]{Integer.toString(siteId)});
+ }
+
+ private static Suggestion getSuggestionFromCursor(Cursor c) {
+ final String userLogin = c.getString(c.getColumnIndex("user_login"));
+ final String displayName = c.getString(c.getColumnIndex("display_name"));
+ final String imageUrl = c.getString(c.getColumnIndex("image_url"));
+ final String taxonomy = c.getString(c.getColumnIndex("taxonomy"));
+
+ int siteId = c.getInt(c.getColumnIndex("site_id"));
+
+ return new Suggestion(
+ siteId,
+ userLogin,
+ displayName,
+ imageUrl,
+ taxonomy);
+ }
+
+ public static void insertTagsForSite(final int siteId, final List<Tag> tags) {
+ // we want to delete the current tags, so that removed tags will not show up
+ deleteTagsForSite(siteId);
+
+ if (tags != null) {
+ for (Tag tag : tags) {
+ addTag(tag);
+ }
+ }
+ }
+
+ public static void addTag(final Tag tag) {
+ if (tag == null)
+ return;
+
+ ContentValues values = new ContentValues();
+ values.put("site_id", tag.siteID);
+ values.put("tag", tag.getTag());
+
+ getWritableDb().insertWithOnConflict(TAXONOMY_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public static List<Tag> getTagsForSite(int siteId) {
+ List<Tag> tags = new ArrayList<Tag>();
+
+ String[] args = {Integer.toString(siteId)};
+ Cursor c = getReadableDb().rawQuery("SELECT * FROM " + TAXONOMY_TABLE + " WHERE site_id=? ORDER BY tag ASC", args);
+
+ try {
+ if (c.moveToFirst()) {
+ do {
+ Tag comment = getTagFromCursor(c);
+ tags.add(comment);
+ } while (c.moveToNext());
+ }
+
+ return tags;
+ } finally {
+ SqlUtils.closeCursor(c);
+ }
+ }
+
+ public static int deleteTagsForSite(int siteId) {
+ return getWritableDb().delete(TAXONOMY_TABLE, "site_id=?", new String[]{Integer.toString(siteId)});
+ }
+
+ private static Tag getTagFromCursor(Cursor c) {
+ final String tag = c.getString(c.getColumnIndex("tag"));
+
+ int siteId = c.getInt(c.getColumnIndex("site_id"));
+
+ return new Tag(
+ siteId,
+ tag);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Account.java b/WordPress/src/main/java/org/wordpress/android/models/Account.java
new file mode 100644
index 000000000..80b414b65
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Account.java
@@ -0,0 +1,111 @@
+package org.wordpress.android.models;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.AccountTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.ui.prefs.PrefsEvents;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Class for managing logged in user informations.
+ */
+public class Account extends AccountModel {
+ public void fetchAccountDetails() {
+ if (!hasAccessToken()) {
+ AppLog.e(T.API, "User is not logged in with WordPress.com, ignoring the fetch account details request");
+ return;
+ }
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ updateFromRestResponse(jsonObject);
+ save();
+
+ ReaderUserTable.addOrUpdateUser(ReaderUser.fromJson(jsonObject));
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().get("me", listener, errorListener);
+ }
+
+ public void fetchAccountSettings() {
+ if (!hasAccessToken()) {
+ AppLog.e(T.API, "User is not logged in with WordPress.com, ignoring the fetch account settings request");
+ return;
+ }
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ updateAccountSettingsFromRestResponse(jsonObject);
+ save();
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsFetchSuccess());
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsFetchError(volleyError));
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().get("me/settings", listener, errorListener);
+ }
+
+ public void postAccountSettings(Map<String, String> params) {
+ if (!hasAccessToken()) {
+ AppLog.e(T.API, "User is not logged in with WordPress.com, ignoring the post account settings request");
+ return;
+ }
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ updateAccountSettingsFromRestResponse(jsonObject);
+ save();
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsPostSuccess());
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsPostError(volleyError));
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().post("me/settings", params, null, listener, errorListener);
+ }
+
+ public void signout() {
+ init();
+ save();
+ }
+
+ public void save() {
+ AccountTable.save(this);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java b/WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java
new file mode 100644
index 000000000..f4593931c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java
@@ -0,0 +1,51 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.AccountTable;
+
+/**
+ * The app supports only one WordPress.com account at the moment, so we might use getDefaultAccount() everywhere we
+ * need the account data.
+ */
+public class AccountHelper {
+ private static Account sAccount;
+ private final static Object mLock = new Object();
+
+ public static Account getDefaultAccount() {
+ if (sAccount == null) {
+ // Singleton pattern in concurrent env.
+ synchronized(mLock) {
+ if (sAccount == null) {
+ sAccount = AccountTable.getDefaultAccount();
+ if (sAccount == null) {
+ sAccount = new Account();
+ }
+ }
+ }
+ }
+ return sAccount;
+ }
+
+ public static boolean isSignedIn() {
+ return getDefaultAccount().hasAccessToken() || (WordPress.wpDB.getNumVisibleBlogs() != 0);
+ }
+
+ public static boolean isSignedInWordPressDotCom() {
+ return getDefaultAccount().hasAccessToken();
+ }
+
+ public static boolean isJetPackUser() {
+ return WordPress.wpDB.hasAnyJetpackBlogs();
+ }
+
+ public static String getCurrentUsernameForBlog(Blog blog) {
+ if (!TextUtils.isEmpty(getDefaultAccount().getUserName())) {
+ return getDefaultAccount().getUserName();
+ } else if (blog != null) {
+ return blog.getUsername();
+ }
+ return "";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/AccountModel.java b/WordPress/src/main/java/org/wordpress/android/models/AccountModel.java
new file mode 100644
index 000000000..93f3400ef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/AccountModel.java
@@ -0,0 +1,248 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.Date;
+
+public class AccountModel {
+ // WordPress.com only - data fetched from the REST API endpoint
+ private String mUserName;
+ private long mUserId;
+ private String mDisplayName;
+ private String mProfileUrl;
+ private String mAvatarUrl;
+ private long mPrimaryBlogId;
+ private int mSiteCount;
+ private int mVisibleSiteCount;
+ private String mAccessToken;
+ private String mEmail;
+ private String mFirstName;
+ private String mLastName;
+ private String mAboutMe;
+ private Date mDateCreated;
+ private String mNewEmail;
+ private boolean mPendingEmailChange;
+ private String mWebAddress;
+
+ public AccountModel() {
+ init();
+ }
+
+ public void init() {
+ mUserName = "";
+ mUserId = 0;
+ mDisplayName = "";
+ mProfileUrl = "";
+ mAvatarUrl = "";
+ mPrimaryBlogId = 0;
+ mSiteCount = 0;
+ mVisibleSiteCount = 0;
+ mAccessToken = "";
+ mEmail = "";
+ mFirstName = "";
+ mLastName = "";
+ mAboutMe = "";
+ mDateCreated = new Date();
+ mNewEmail = "";
+ mPendingEmailChange = false;
+ mWebAddress = "";
+ }
+
+ public void updateFromRestResponse(JSONObject json) {
+ mUserId = json.optLong("ID");
+ mUserName = json.optString("username");
+ mDisplayName = json.optString("display_name");
+ mProfileUrl = json.optString("profile_URL");
+ mAvatarUrl = json.optString("avatar_URL");
+ mPrimaryBlogId = json.optLong("primary_blog");
+ mSiteCount = json.optInt("site_count");
+ mVisibleSiteCount = json.optInt("visible_site_count");
+ mEmail = json.optString("email");
+
+ Date date = DateTimeUtils.dateFromIso8601(json.optString("date"));
+ if (date != null) {
+ mDateCreated = date;
+ } else {
+ AppLog.e(AppLog.T.API, "Date could not be found from Account JSON response");
+ }
+ }
+
+ public void updateAccountSettingsFromRestResponse(JSONObject json) {
+ if (json.has(RestParam.FIRST_NAME.getDescription())) mFirstName = json.optString(RestParam.FIRST_NAME.getDescription());
+ if (json.has(RestParam.LAST_NAME.getDescription())) mLastName = json.optString(RestParam.LAST_NAME.getDescription());
+ if (json.has(RestParam.DISPLAY_NAME.getDescription())) mDisplayName = json.optString(RestParam.DISPLAY_NAME.getDescription());
+ if (json.has(RestParam.ABOUT_ME.getDescription())) mAboutMe = json.optString(RestParam.ABOUT_ME.getDescription());
+ if (json.has(RestParam.EMAIL.getDescription())) mEmail = json.optString(RestParam.EMAIL.getDescription());
+ if (json.has(RestParam.NEW_EMAIL.getDescription())) mNewEmail = json.optString(RestParam.NEW_EMAIL.getDescription());
+ if (json.has(RestParam.EMAIL_CHANGE_PENDING.getDescription())) mPendingEmailChange = json.optBoolean(RestParam.EMAIL_CHANGE_PENDING.getDescription());
+ if (json.has(RestParam.PRIMARY_BLOG.getDescription())) mPrimaryBlogId = json.optLong(RestParam.PRIMARY_BLOG.getDescription());
+ if (json.has(RestParam.WEB_ADDRESS.getDescription())) mWebAddress = json.optString(RestParam.WEB_ADDRESS.getDescription());
+ }
+
+ public long getUserId() {
+ return mUserId;
+ }
+
+ public void setUserId(long userId) {
+ mUserId = userId;
+ }
+
+ public void setPrimaryBlogId(long primaryBlogId) {
+ mPrimaryBlogId = primaryBlogId;
+ }
+
+ public long getPrimaryBlogId() {
+ return mPrimaryBlogId;
+ }
+
+ public String getUserName() {
+ return StringUtils.notNullStr(mUserName);
+ }
+
+ public void setUserName(String userName) {
+ mUserName = userName;
+ }
+
+ public String getAccessToken() {
+ return mAccessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ mAccessToken = accessToken;
+ }
+
+ boolean hasAccessToken() {
+ return !TextUtils.isEmpty(getAccessToken());
+ }
+
+ public String getDisplayName() {
+ return StringUtils.notNullStr(mDisplayName);
+ }
+
+ public void setDisplayName(String displayName) {
+ mDisplayName = displayName;
+ }
+
+ public String getProfileUrl() {
+ return StringUtils.notNullStr(mProfileUrl);
+ }
+
+ public void setProfileUrl(String profileUrl) {
+ mProfileUrl = profileUrl;
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(mAvatarUrl);
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ mAvatarUrl = avatarUrl;
+ }
+
+ public int getSiteCount() {
+ return mSiteCount;
+ }
+
+ public void setSiteCount(int siteCount) {
+ mSiteCount = siteCount;
+ }
+
+ public int getVisibleSiteCount() {
+ return mVisibleSiteCount;
+ }
+
+ public void setVisibleSiteCount(int visibleSiteCount) {
+ mVisibleSiteCount = visibleSiteCount;
+ }
+
+ public void setEmail(String email) {
+ mEmail = email;
+ }
+
+ public String getEmail() {
+ return StringUtils.notNullStr(mEmail);
+ }
+
+ public String getFirstName() {
+ return StringUtils.notNullStr(mFirstName);
+ }
+
+ public void setFirstName(String firstName) {
+ mFirstName = firstName;
+ }
+
+ public String getLastName() {
+ return StringUtils.notNullStr(mLastName);
+ }
+
+ public void setLastName(String lastName) {
+ mLastName = lastName;
+ }
+
+ public String getAboutMe() {
+ return StringUtils.notNullStr(mAboutMe);
+ }
+
+ public void setAboutMe(String aboutMe) {
+ mAboutMe = aboutMe;
+ }
+
+ public Date getDateCreated() {
+ return mDateCreated;
+ }
+
+ public void setDateCreated(Date date) {
+ mDateCreated = date;
+ }
+
+ public String getNewEmail() {
+ return StringUtils.notNullStr(mNewEmail);
+ }
+
+ public void setNewEmail(String newEmail) {
+ mNewEmail = newEmail;
+ }
+
+ public boolean getPendingEmailChange() {
+ return mPendingEmailChange;
+ }
+
+ public void setPendingEmailChange(boolean pendingEmailChange) {
+ mPendingEmailChange = pendingEmailChange;
+ }
+
+ public String getWebAddress() {
+ return mWebAddress;
+ }
+
+ public void setWebAddress(String webAddress) {
+ mWebAddress = webAddress;
+ }
+
+ public enum RestParam {
+ FIRST_NAME("first_name"),
+ LAST_NAME("last_name"),
+ DISPLAY_NAME("display_name"),
+ ABOUT_ME("description"),
+ EMAIL("user_email"),
+ NEW_EMAIL("new_user_email"),
+ EMAIL_CHANGE_PENDING("user_email_change_pending"),
+ PRIMARY_BLOG("primary_site_ID"),
+ WEB_ADDRESS("user_URL");
+
+ private String description;
+
+ RestParam(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Blog.java b/WordPress/src/main/java/org/wordpress/android/models/Blog.java
new file mode 100644
index 000000000..31daba99d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Blog.java
@@ -0,0 +1,576 @@
+//Manages data for blog settings
+
+package org.wordpress.android.models;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+public class Blog {
+ private int localTableBlogId;
+ private String url;
+ private String homeURL;
+ private String blogName;
+ private String username;
+ private String password;
+ private String imagePlacement;
+ private boolean featuredImageCapable;
+ private boolean fullSizeImage;
+ private boolean scaledImage;
+ private int scaledImageWidth;
+ private String maxImageWidth;
+ private int maxImageWidthId;
+ private int remoteBlogId;
+ private String dotcom_username;
+ private String dotcom_password;
+ private String api_key;
+ private String api_blogid;
+ private boolean dotcomFlag;
+ private String wpVersion;
+ private String httpuser = "";
+ private String httppassword = "";
+ private String postFormats;
+ private String blogOptions = "{}";
+ private String capabilities;
+ private boolean isAdmin;
+ private boolean isHidden;
+ private long planID;
+ private String planShortName;
+
+ public Blog() {
+ }
+
+ public Blog(int localTableBlogId, String url, String homeURL, String blogName, String username, String password, String imagePlacement, boolean featuredImageCapable, boolean fullSizeImage, boolean scaledImage, int scaledImageWidth, String maxImageWidth, int maxImageWidthId, int remoteBlogId, String dotcom_username, String dotcom_password, String api_key, String api_blogid, boolean dotcomFlag, String wpVersion, String httpuser, String httppassword, String postFormats, String blogOptions, String capabilities, boolean isAdmin, boolean isHidden) {
+ this.localTableBlogId = localTableBlogId;
+ this.url = url;
+ this.homeURL = homeURL;
+ this.blogName = blogName;
+ this.username = username;
+ this.password = password;
+ this.imagePlacement = imagePlacement;
+ this.featuredImageCapable = featuredImageCapable;
+ this.fullSizeImage = fullSizeImage;
+ this.scaledImage = scaledImage;
+ this.scaledImageWidth = scaledImageWidth;
+ this.maxImageWidth = maxImageWidth;
+ this.maxImageWidthId = maxImageWidthId;
+ this.remoteBlogId = remoteBlogId;
+ this.dotcom_username = dotcom_username;
+ this.dotcom_password = dotcom_password;
+ this.api_key = api_key;
+ this.api_blogid = api_blogid;
+ this.dotcomFlag = dotcomFlag;
+ this.wpVersion = wpVersion;
+ this.httpuser = httpuser;
+ this.httppassword = httppassword;
+ this.postFormats = postFormats;
+ this.blogOptions = blogOptions;
+ this.capabilities = capabilities;
+ this.isAdmin = isAdmin;
+ this.isHidden = isHidden;
+ }
+
+ public Blog(String url, String username, String password) {
+ this.url = url;
+ this.username = username;
+ this.password = password;
+ this.localTableBlogId = -1;
+ }
+
+ public int getLocalTableBlogId() {
+ return localTableBlogId;
+ }
+
+ public void setLocalTableBlogId(int id) {
+ this.localTableBlogId = id;
+ }
+
+ public String getNameOrHostUrl() {
+ return (getBlogName() == null || getBlogName().isEmpty()) ? getUri().getHost() : getBlogName();
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public URI getUri() {
+ try {
+ String url = getUrl();
+ if (url == null) {
+ AppLog.e(T.UTILS, "Blog url is null");
+ return null;
+ }
+ return new URI(url);
+ } catch (URISyntaxException e) {
+ AppLog.e(T.UTILS, "Blog url is invalid: " + getUrl());
+ return null;
+ }
+ }
+
+ /**
+ * TODO: When we rewrite this in WPStores, make sure we only have one of this function.
+ * This is used to open the site in the browser. getHomeURL() was not used, probably due to a bug where
+ * it returns an empty or invalid url.
+ * @return site url
+ */
+ public @NonNull String getAlternativeHomeUrl() {
+ String siteURL = null;
+ Gson gson = new Gson();
+ Type type = new TypeToken<Map<?, ?>>() { }.getType();
+ Map<?, ?> blogOptions = gson.fromJson(this.getBlogOptions(), type);
+ if (blogOptions != null) {
+ Map<?, ?> homeURLMap = (Map<?, ?>) blogOptions.get("home_url");
+ if (homeURLMap != null) {
+ siteURL = homeURLMap.get("value").toString();
+ }
+ }
+ // Try to guess the URL of the site if blogOptions is null (blog not added to the app)
+ if (siteURL == null) {
+ siteURL = this.getUrl().replace("/xmlrpc.php", "");
+ }
+ return siteURL;
+ }
+
+ public String getHomeURL() {
+ return homeURL;
+ }
+
+ public void setHomeURL(String homeURL) {
+ this.homeURL = homeURL;
+ }
+
+ public String getBlogName() {
+ return blogName;
+ }
+
+ public void setBlogName(String blogName) {
+ this.blogName = blogName;
+ }
+
+ public String getUsername() {
+ return StringUtils.notNullStr(username);
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return StringUtils.notNullStr(password);
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getImagePlacement() {
+ return imagePlacement;
+ }
+
+ public void setImagePlacement(String imagePlacement) {
+ this.imagePlacement = imagePlacement;
+ }
+
+ public boolean isFeaturedImageCapable() {
+ return featuredImageCapable;
+ }
+
+ public void setFeaturedImageCapable(boolean isCapable) {
+ this.featuredImageCapable = isCapable;
+ }
+
+ public boolean bsetFeaturedImageCapable(boolean isCapable) {
+ if (featuredImageCapable == isCapable) {
+ return false;
+ }
+ setFeaturedImageCapable(isCapable);
+ return true;
+ }
+
+ public boolean isFullSizeImage() {
+ return fullSizeImage;
+ }
+
+ public void setFullSizeImage(boolean fullSizeImage) {
+ this.fullSizeImage = fullSizeImage;
+ }
+
+ public String getMaxImageWidth() {
+ return StringUtils.notNullStr(maxImageWidth);
+ }
+
+ public void setMaxImageWidth(String maxImageWidth) {
+ this.maxImageWidth = maxImageWidth;
+ }
+
+ public int getMaxImageWidthId() {
+ return maxImageWidthId;
+ }
+
+ public void setMaxImageWidthId(int maxImageWidthId) {
+ this.maxImageWidthId = maxImageWidthId;
+ }
+
+ public int getRemoteBlogId() {
+ return remoteBlogId;
+ }
+
+ public void setRemoteBlogId(int blogId) {
+ this.remoteBlogId = blogId;
+ }
+
+ public String getDotcom_username() {
+ return dotcom_username;
+ }
+
+ public void setDotcom_username(String dotcomUsername) {
+ dotcom_username = dotcomUsername;
+ }
+
+ public String getDotcom_password() {
+ return dotcom_password;
+ }
+
+ public void setDotcom_password(String dotcomPassword) {
+ dotcom_password = dotcomPassword;
+ }
+
+ public String getApi_key() {
+ return api_key;
+ }
+
+ public void setApi_key(String apiKey) {
+ api_key = apiKey;
+ }
+
+ public String getApi_blogid() {
+ if (api_blogid == null) {
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions!=null && jsonOptions.has("jetpack_client_id")) {
+ try {
+ String jetpackBlogId = jsonOptions.getJSONObject("jetpack_client_id").getString("value");
+ if (!TextUtils.isEmpty(jetpackBlogId)) {
+ this.setApi_blogid(jetpackBlogId);
+ WordPress.wpDB.saveBlog(this);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load jetpack_client_id from options: " + jsonOptions, e);
+ }
+ }
+ }
+ return api_blogid;
+ }
+
+ public void setApi_blogid(String apiBlogid) {
+ api_blogid = apiBlogid;
+ }
+
+ public boolean isDotcomFlag() {
+ return dotcomFlag;
+ }
+
+ public void setDotcomFlag(boolean dotcomFlag) {
+ this.dotcomFlag = dotcomFlag;
+ }
+
+ public String getWpVersion() {
+ return wpVersion;
+ }
+
+ public void setWpVersion(String wpVersion) {
+ this.wpVersion = wpVersion;
+ }
+
+ public boolean bsetWpVersion(String wpVersion) {
+ if (StringUtils.equals(this.wpVersion, wpVersion)) {
+ return false;
+ }
+ setWpVersion(wpVersion);
+ return true;
+ }
+
+ public String getHttpuser() {
+ return httpuser;
+ }
+
+ public void setHttpuser(String httpuser) {
+ this.httpuser = httpuser;
+ }
+
+ public String getHttppassword() {
+ return httppassword;
+ }
+
+ public void setHttppassword(String httppassword) {
+ this.httppassword = httppassword;
+ }
+
+ public boolean isHidden() {
+ return isHidden;
+ }
+
+ public void setHidden(boolean isHidden) {
+ this.isHidden = isHidden;
+ }
+
+ public String getPostFormats() {
+ return postFormats;
+ }
+
+ public void setPostFormats(String postFormats) {
+ this.postFormats = postFormats;
+ }
+
+ public boolean bsetPostFormats(String postFormats) {
+ if (StringUtils.equals(this.postFormats, postFormats)) {
+ return false;
+ }
+ setPostFormats(postFormats);
+ return true;
+ }
+
+ public boolean isScaledImage() {
+ return scaledImage;
+ }
+
+ public void setScaledImage(boolean scaledImage) {
+ this.scaledImage = scaledImage;
+ }
+
+ public int getScaledImageWidth() {
+ return scaledImageWidth;
+ }
+
+ public void setScaledImageWidth(int scaledImageWidth) {
+ this.scaledImageWidth = scaledImageWidth;
+ }
+
+ public String getBlogOptions() {
+ return blogOptions;
+ }
+
+ public JSONObject getBlogOptionsJSONObject() {
+ String optionsString = getBlogOptions();
+ if (TextUtils.isEmpty(optionsString)) {
+ return null;
+ }
+ try {
+ return new JSONObject(optionsString);
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "invalid blogOptions json", e);
+ }
+ return null;
+ }
+
+ public void setBlogOptions(String blogOptions) {
+ this.blogOptions = blogOptions;
+ JSONObject options = getBlogOptionsJSONObject();
+ if (options == null) {
+ this.blogOptions = "{}";
+ options = getBlogOptionsJSONObject();
+ }
+
+ if (options.has("jetpack_client_id")) {
+ try {
+ String jetpackBlogId = options.getJSONObject("jetpack_client_id").getString("value");
+ if (!TextUtils.isEmpty(jetpackBlogId)) {
+ this.setApi_blogid(jetpackBlogId);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load jetpack_client_id from options: " + blogOptions, e);
+ }
+ }
+ }
+
+ // TODO: it's ugly to compare json strings, we have to normalize both strings before
+ // comparison or compare JSON objects after parsing
+ public boolean bsetBlogOptions(String blogOptions) {
+ if (StringUtils.equals(this.blogOptions, blogOptions)) {
+ return false;
+ }
+ setBlogOptions(blogOptions);
+ return true;
+ }
+
+ public boolean isAdmin() {
+ return isAdmin;
+ }
+
+ public void setAdmin(boolean isAdmin) {
+ this.isAdmin = isAdmin;
+ }
+
+ public boolean bsetAdmin(boolean isAdmin) {
+ if (this.isAdmin == isAdmin) {
+ return false;
+ }
+ setAdmin(isAdmin);
+ return true;
+ }
+
+ public String getAdminUrl() {
+ String adminUrl = null;
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null) {
+ try {
+ adminUrl = jsonOptions.getJSONObject("admin_url").getString("value");
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load admin_url from options: " + jsonOptions, e);
+ }
+ }
+
+ // Try to guess the URL of the dashboard if blogOptions is null (blog not added to the app), or WP version is < 3.6
+ if (TextUtils.isEmpty(adminUrl)) {
+ if (this.getUrl().lastIndexOf("/") != -1) {
+ adminUrl = this.getUrl().substring(0, this.getUrl().lastIndexOf("/")) + "/wp-admin";
+ } else {
+ adminUrl = this.getUrl().replace("xmlrpc.php", "wp-admin");
+ }
+ }
+ return adminUrl;
+ }
+
+ public boolean isPrivate() {
+ if (!isDotcomFlag()) {
+ return false; // only wpcom blogs can be marked private.
+ }
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null && jsonOptions.has("blog_public")) {
+ try {
+ String blogPublicValue = jsonOptions.getJSONObject("blog_public").getString("value");
+ if (!TextUtils.isEmpty(blogPublicValue) && "-1".equals(blogPublicValue)) {
+ return true;
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load blog_public from options: " + jsonOptions, e);
+ }
+ }
+ return false;
+ }
+
+ public boolean isJetpackPowered() {
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null && jsonOptions.has("jetpack_client_id")) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return the Jetpack plugin version code
+ *
+ * @return The Jetpack version string, null for non Jetpack sites, or wpcom sites
+ */
+ public String getJetpackVersion() {
+ String jetpackVersion = null;
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null && jsonOptions.has("jetpack_version")) {
+ try {
+ jetpackVersion = jsonOptions.getJSONObject("jetpack_version").getString("value");
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load jetpack_version from options: " + jsonOptions, e);
+ }
+ }
+ return jetpackVersion;
+ }
+
+ public boolean isPhotonCapable() {
+ return ((isDotcomFlag() && !isPrivate()) || (isJetpackPowered() && !hasValidHTTPAuthCredentials()));
+ }
+
+ public boolean hasValidJetpackCredentials() {
+ return !TextUtils.isEmpty(getDotcom_username()) && !TextUtils.isEmpty(getApi_key());
+ }
+
+ public boolean hasValidHTTPAuthCredentials() {
+ return !TextUtils.isEmpty(getHttppassword()) && !TextUtils.isEmpty(getHttpuser());
+ }
+
+ /**
+ * Get the remote Blog ID stored on the wpcom backend.
+ *
+ * In the app db it's stored in blogId for WP.com, and in api_blogId for Jetpack.
+ *
+ * For WP.com sites this function returns the same value of getRemoteBlogId().
+ *
+ * @return WP.com blogId string, potentially null for Jetpack sites
+ */
+ public String getDotComBlogId() {
+ if (isDotcomFlag()) {
+ return String.valueOf(getRemoteBlogId());
+ } else {
+ String remoteID = getApi_blogid();
+ // Self-hosted blogs edge cases.
+ if (TextUtils.isEmpty(remoteID)) {
+ return null;
+ }
+ try {
+ long parsedBlogID = Long.parseLong(remoteID);
+ // remote blogID is always > 1 for Jetpack blogs
+ if (parsedBlogID < 1) {
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ AppLog.e(T.UTILS, "The remote blog ID stored in options isn't valid: " + remoteID);
+ return null;
+ }
+ return remoteID;
+ }
+ }
+
+ public long getPlanID() {
+ return planID;
+ }
+
+ public void setPlanID(long planID) {
+ this.planID = planID;
+ }
+
+ public String getPlanShortName() {
+ return StringUtils.notNullStr(planShortName);
+ }
+
+ public void setPlanShortName(String name) {
+ this.planShortName = StringUtils.notNullStr(name);
+ }
+
+ public String getCapabilities() {
+ return StringUtils.notNullStr(capabilities);
+ }
+
+ public void setCapabilities(String capabilities) {
+ this.capabilities = capabilities;
+ }
+
+ public boolean hasCapability(Capability capability) {
+ // If a capability is missing it means the user don't have it.
+ if (capabilities.isEmpty() || capability == null) {
+ return false;
+ }
+ try {
+ JSONObject jsonObject = new JSONObject(capabilities);
+ return jsonObject.optBoolean(capability.getLabel());
+ } catch (JSONException e) {
+ AppLog.e(T.PEOPLE, "Capabilities is not a valid json: " + capabilities);
+ return false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java b/WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java
new file mode 100644
index 000000000..755614324
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.models;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * A blog is uniquely identified by the combination of xmlRpcUrl and blogId
+ */
+public class BlogIdentifier {
+ private String mXmlRpcUrl;
+ private int mBlogId;
+
+ public BlogIdentifier(String mXmlRpcUrl, int mBlogId) {
+ this.mXmlRpcUrl = mXmlRpcUrl;
+ this.mBlogId = mBlogId;
+ }
+
+ public String getXmlRpcUrl() {
+ return mXmlRpcUrl;
+ }
+
+ public void setXmlRpcUrl(String mXmlRpcUrl) {
+ this.mXmlRpcUrl = mXmlRpcUrl;
+ }
+
+ public int getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(int mBlogId) {
+ this.mBlogId = mBlogId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) {
+ return false;
+ }
+ if (other == this) { // same instance
+ return true;
+ }
+ if (!(other instanceof BlogIdentifier)) {
+ return false;
+ }
+ BlogIdentifier o = (BlogIdentifier) other;
+ return mXmlRpcUrl.equals(o.getXmlRpcUrl()) && mBlogId == o.getBlogId();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(3739, 50989).append(mBlogId).append(mXmlRpcUrl).toHashCode();
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java b/WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java
new file mode 100644
index 000000000..c458fe67b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.models;
+
+import java.io.Serializable;
+
+/**
+ * Simple POJO to store a pair of ids: a remoteBlogId + a genericId.
+ * Could be used to identify a comment (remoteBlogId + commentId) or a post (remoteBlogId + postId)
+ */
+public class BlogPairId implements Serializable {
+ private long mId;
+ private long mRemoteBlogId;
+
+ public BlogPairId(long remoteBlogId, long id) {
+ mRemoteBlogId = remoteBlogId;
+ mId = id;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public long getRemoteBlogId() {
+ return mRemoteBlogId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Capability.java b/WordPress/src/main/java/org/wordpress/android/models/Capability.java
new file mode 100644
index 000000000..2ae03aeac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Capability.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.models;
+
+/**
+ * Used to decide what can the current user do in a particular blog
+ * A list of capabilities can be found in: https://codex.wordpress.org/Roles_and_Capabilities#Capabilities
+ */
+public enum Capability {
+ LIST_USERS("list_users"), // Check if user can visit People page
+ PROMOTE_USERS("promote_users"), // Check if user can change another user's role
+ REMOVE_USERS("remove_users"); // Check if user can remove another user
+
+ private final String label;
+
+ Capability(String label) {
+ this.label = label;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java b/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java
new file mode 100644
index 000000000..77b08bfae
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java
@@ -0,0 +1,65 @@
+package org.wordpress.android.models;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+/**
+ * Represents WordPress post Category data and handles local database (de)serialization.
+ */
+public class CategoryModel {
+ // Categories table column names
+ public static final String ID_COLUMN_NAME = "ID";
+ public static final String NAME_COLUMN_NAME = "name";
+ public static final String SLUG_COLUMN_NAME = "slug";
+ public static final String DESC_COLUMN_NAME = "description";
+ public static final String PARENT_ID_COLUMN_NAME = "parent";
+ public static final String POST_COUNT_COLUMN_NAME = "post_count";
+
+ public int id;
+ public String name;
+ public String slug;
+ public String description;
+ public int parentId;
+ public int postCount;
+ public boolean isInLocalTable;
+
+ public CategoryModel() {
+ id = -1;
+ name = "";
+ slug = "";
+ description = "";
+ parentId = -1;
+ postCount = 0;
+ isInLocalTable = false;
+ }
+
+ /**
+ * Sets data from a local database {@link Cursor}.
+ */
+ public void deserializeFromDatabase(Cursor cursor) {
+ if (cursor == null) return;
+
+ id = cursor.getInt(cursor.getColumnIndex(ID_COLUMN_NAME));
+ name = cursor.getString(cursor.getColumnIndex(NAME_COLUMN_NAME));
+ slug = cursor.getString(cursor.getColumnIndex(SLUG_COLUMN_NAME));
+ description = cursor.getString(cursor.getColumnIndex(DESC_COLUMN_NAME));
+ parentId = cursor.getInt(cursor.getColumnIndex(PARENT_ID_COLUMN_NAME));
+ postCount = cursor.getInt(cursor.getColumnIndex(POST_COUNT_COLUMN_NAME));
+ isInLocalTable = true;
+ }
+
+ /**
+ * Creates the {@link ContentValues} object to store this category data in a local database.
+ */
+ public ContentValues serializeToDatabase() {
+ ContentValues values = new ContentValues();
+ values.put(ID_COLUMN_NAME, id);
+ values.put(NAME_COLUMN_NAME, name);
+ values.put(SLUG_COLUMN_NAME, slug);
+ values.put(DESC_COLUMN_NAME, description);
+ values.put(PARENT_ID_COLUMN_NAME, parentId);
+ values.put(POST_COUNT_COLUMN_NAME, postCount);
+
+ return values;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java b/WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java
new file mode 100644
index 000000000..cd01d8c79
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java
@@ -0,0 +1,110 @@
+package org.wordpress.android.models;
+
+import android.util.SparseArray;
+import org.wordpress.android.WordPress;
+
+import java.util.*;
+
+public class CategoryNode {
+ private int categoryId;
+ private String name;
+ private int parentId;
+ private int level;
+ SortedMap<String, CategoryNode> children = new TreeMap<String, CategoryNode>(new Comparator<String>() {
+ @Override
+ public int compare(String s, String s2) {
+ return s.compareToIgnoreCase(s2);
+ }
+ });
+
+ public SortedMap<String, CategoryNode> getChildren() {
+ return children;
+ }
+
+ public void setChildren(SortedMap<String, CategoryNode> children) {
+ this.children = children;
+ }
+
+ public CategoryNode(int categoryId, int parentId, String name) {
+ this.categoryId = categoryId;
+ this.parentId = parentId;
+ this.name = name;
+ }
+
+ public int getCategoryId() {
+ return categoryId;
+ }
+
+ public void setCategoryId(int categoryId) {
+ this.categoryId = categoryId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getParentId() {
+ return parentId;
+ }
+
+ public void setParentId(int parentId) {
+ this.parentId = parentId;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public static CategoryNode createCategoryTreeFromDB(int blogId) {
+ CategoryNode rootCategory = new CategoryNode(-1, -1, "");
+ if (WordPress.wpDB == null) {
+ return rootCategory;
+ }
+ List<String> stringCategories = WordPress.wpDB.loadCategories(blogId);
+
+ // First pass instantiate CategoryNode objects
+ SparseArray<CategoryNode> categoryMap = new SparseArray<CategoryNode>();
+ CategoryNode currentRootNode;
+ for (String name : stringCategories) {
+ int categoryId = WordPress.wpDB.getCategoryId(blogId, name);
+ int parentId = WordPress.wpDB.getCategoryParentId(blogId, name);
+ CategoryNode node = new CategoryNode(categoryId, parentId, name);
+ categoryMap.put(categoryId, node);
+ }
+
+ // Second pass associate nodes to form a tree
+ for(int i = 0; i < categoryMap.size(); i++){
+ CategoryNode category = categoryMap.valueAt(i);
+ if (category.getParentId() == 0) { // root node
+ currentRootNode = rootCategory;
+ } else {
+ currentRootNode = categoryMap.get(category.getParentId(), rootCategory);
+ }
+ currentRootNode.children.put(category.getName(), categoryMap.get(category.getCategoryId()));
+ }
+ return rootCategory;
+ }
+
+ private static void preOrderTreeTraversal(CategoryNode node, int level, ArrayList<CategoryNode> returnValue) {
+ if (node == null) {
+ return ;
+ }
+ if (node.parentId != -1) {
+ node.level = level;
+ returnValue.add(node);
+ }
+ for (CategoryNode child : node.getChildren().values()) {
+ preOrderTreeTraversal(child, level + 1, returnValue);
+ }
+ }
+
+ public static ArrayList<CategoryNode> getSortedListOfCategoriesFromRoot(CategoryNode node) {
+ ArrayList<CategoryNode> sortedCategories = new ArrayList<CategoryNode>();
+ preOrderTreeTraversal(node, 0, sortedCategories);
+ return sortedCategories;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Comment.java b/WordPress/src/main/java/org/wordpress/android/models/Comment.java
new file mode 100644
index 000000000..7ec244b53
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Comment.java
@@ -0,0 +1,244 @@
+package org.wordpress.android.models;
+
+import android.content.Context;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class Comment {
+ public long postID;
+ public long commentID;
+
+ private String authorName;
+ private String status;
+ private String comment;
+ private String postTitle;
+ private String authorUrl;
+ private String authorEmail;
+ private String published;
+ private String profileImageUrl;
+
+ public Comment(long postID,
+ long commentID,
+ String authorName,
+ String pubDateGmt,
+ String comment,
+ String status,
+ String postTitle,
+ String authorURL,
+ String authorEmail,
+ String profileImageUrl) {
+ this.postID = postID;
+ this.commentID = commentID;
+ this.authorName = authorName;
+ this.status = status;
+ this.comment = comment;
+ this.postTitle = postTitle;
+ this.authorUrl = authorURL;
+ this.authorEmail = authorEmail;
+ this.profileImageUrl = profileImageUrl;
+ this.published = pubDateGmt;
+ }
+
+ private Comment() {
+ // nop
+ }
+
+ /*
+ * nbradbury 11/14/13 - create a comment from JSON (REST response)
+ * https://developer.wordpress.com/docs/api/1/get/sites/%24site/comments/%24comment_ID/
+ */
+ public static Comment fromJSON(JSONObject json) {
+ if (json == null)
+ return null;
+
+ Comment comment = new Comment();
+ comment.commentID = json.optLong("ID");
+ comment.status = JSONUtils.getString(json, "status");
+ comment.published = JSONUtils.getString(json, "date");
+
+ // note that the content often contains html, and on rare occasions may contain
+ // script blocks that need to be removed (only seen with blogs that use the
+ // sociable plugin)
+ comment.comment = HtmlUtils.stripScript(JSONUtils.getString(json, "content"));
+
+ JSONObject jsonPost = json.optJSONObject("post");
+ if (jsonPost != null) {
+ comment.postID = jsonPost.optLong("ID");
+ // TODO: c.postTitle = ???
+ }
+
+ JSONObject jsonAuthor = json.optJSONObject("author");
+ if (jsonAuthor!=null) {
+ // author names may contain html entities (esp. pingbacks)
+ comment.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
+ comment.authorUrl = JSONUtils.getString(jsonAuthor, "URL");
+
+ // email address will be set to "false" when there isn't an email address
+ comment.authorEmail = JSONUtils.getString(jsonAuthor, "email");
+ if (comment.authorEmail.equals("false"))
+ comment.authorEmail = "";
+
+ comment.profileImageUrl = JSONUtils.getString(jsonAuthor, "avatar_URL");
+ }
+
+ return comment;
+ }
+
+ public String getProfileImageUrl() {
+ return StringUtils.notNullStr(profileImageUrl);
+ }
+ public void setProfileImageUrl(String url) {
+ profileImageUrl = StringUtils.notNullStr(url);
+ }
+ public boolean hasProfileImageUrl() {
+ return !TextUtils.isEmpty(profileImageUrl);
+ }
+
+ public CommentStatus getStatusEnum() {
+ return CommentStatus.fromString(status);
+ }
+
+ public String getStatus() {
+ return StringUtils.notNullStr(status);
+ }
+ public void setStatus(String status) {
+ this.status = StringUtils.notNullStr(status);
+ }
+
+ public String getPublished() {
+ return StringUtils.notNullStr(published);
+ }
+ public void setPublished(String pubDate) {
+ published = StringUtils.notNullStr(pubDate);
+ }
+
+ public boolean hasAuthorName() {
+ return !TextUtils.isEmpty(authorName);
+ }
+ public String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+ public void setAuthorName(String name) {
+ authorName = StringUtils.notNullStr(name);
+ }
+
+ public boolean hasAuthorEmail() {
+ return !TextUtils.isEmpty(authorEmail);
+ }
+ public String getAuthorEmail() {
+ return StringUtils.notNullStr(authorEmail);
+ }
+ public void setAuthorEmail(String email) {
+ authorEmail = StringUtils.notNullStr(email);
+ }
+
+ public boolean hasAuthorUrl() {
+ return !TextUtils.isEmpty(authorUrl);
+ }
+ public String getAuthorUrl() {
+ return StringUtils.notNullStr(authorUrl);
+ }
+ public void setAuthorUrl(String url) {
+ authorUrl = StringUtils.notNullStr(url);
+ }
+
+ public String getCommentText() {
+ return StringUtils.notNullStr(comment);
+ }
+ public void setCommentText(String text) {
+ comment = StringUtils.notNullStr(text);
+ }
+
+ public boolean hasPostTitle() {
+ return !TextUtils.isEmpty(postTitle);
+ }
+ public String getPostTitle() {
+ return StringUtils.notNullStr(postTitle);
+ }
+ public void setPostTitle(String title) {
+ postTitle = StringUtils.notNullStr(title);
+ }
+
+ /****
+ * the following are transient variables whose sole purpose is to cache commonly-used values
+ * for the comment that speeds up accessing them inside adapters
+ ****/
+
+ /*
+ * converts iso8601 published date to an actual java date
+ */
+ private transient java.util.Date dtPublished;
+ public java.util.Date getDatePublished() {
+ if (dtPublished == null)
+ dtPublished = DateTimeUtils.dateFromIso8601(published);
+ return dtPublished;
+ }
+
+ private transient Spanned unescapedCommentWithDrawables;
+ public void setUnescapedCommentWithDrawables(Spanned spanned){
+ unescapedCommentWithDrawables = spanned;
+ }
+ public Spanned getUnescapedCommentTextWithDrawables() {
+ return unescapedCommentWithDrawables;
+ }
+
+ private transient String unescapedPostTitle;
+ public String getUnescapedPostTitle() {
+ if (unescapedPostTitle == null)
+ unescapedPostTitle = StringUtils.unescapeHTML(getPostTitle().trim());
+ return unescapedPostTitle;
+ }
+
+ /*
+ * returns the avatar url as a photon/gravatar url set to the passed size
+ */
+ private transient String avatarForDisplay;
+ public String getAvatarForDisplay(int avatarSize) {
+ if (avatarForDisplay == null) {
+ if (hasProfileImageUrl()) {
+ avatarForDisplay = GravatarUtils.fixGravatarUrl(profileImageUrl, avatarSize);
+ } else if (hasAuthorEmail()) {
+ avatarForDisplay = GravatarUtils.gravatarFromEmail(authorEmail, avatarSize);
+ } else {
+ avatarForDisplay = "";
+ }
+ }
+ return avatarForDisplay;
+ }
+
+ /*
+ * returns the author + post title as "Author Name on Post Title" - used by comment list
+ */
+ private transient String formattedTitle;
+ public String getFormattedTitle() {
+ if (formattedTitle == null) {
+ Context context = WordPress.getContext();
+ final String author = (hasAuthorName() ? getAuthorName() : context.getString(R.string.anonymous));
+ if (hasPostTitle()) {
+ formattedTitle = author
+ + "<font color=" + HtmlUtils.colorResToHtmlColor(context, R.color.grey_darken_10) + ">"
+ + " " + context.getString(R.string.on) + " "
+ + "</font>"
+ + getUnescapedPostTitle();
+ } else {
+ formattedTitle = author;
+ }
+ }
+ return formattedTitle;
+ }
+
+ public boolean willTrashingPermanentlyDelete(){
+ CommentStatus status = getStatusEnum();
+ return CommentStatus.TRASH.equals(status) || CommentStatus.SPAM.equals(status);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CommentList.java b/WordPress/src/main/java/org/wordpress/android/models/CommentList.java
new file mode 100644
index 000000000..6942e3f86
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CommentList.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class CommentList extends ArrayList<Comment> {
+ public int indexOfCommentId(long commentId) {
+ for (int i=0; i < this.size(); i++) {
+ if (commentId==this.get(i).commentID)
+ return i;
+ }
+ return -1;
+ }
+
+ /*
+ * replace comments in this list that match the passed list
+ */
+ public void replaceComments(final CommentList comments) {
+ if (comments == null || comments.size() == 0)
+ return;
+ for (Comment comment: comments) {
+ int index = indexOfCommentId(comment.commentID);
+ if (index > -1)
+ set(index, comment);
+ }
+ }
+
+ /*
+ * delete comments in this list that match the passed list
+ */
+ public void deleteComments(final CommentList comments) {
+ if (comments == null || comments.size() == 0)
+ return;
+ for (Comment comment: comments) {
+ int index = indexOfCommentId(comment.commentID);
+ if (index > -1)
+ remove(index);
+ }
+ }
+
+ /*
+ * returns true if any comments in this list have the passed status
+ */
+ public boolean hasAnyWithStatus(CommentStatus status) {
+ for (Comment comment: this) {
+ if (comment.getStatusEnum().equals(status))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * returns true if any comments in this list do NOT have the passed status
+ */
+ public boolean hasAnyWithoutStatus(CommentStatus status) {
+ for (Comment comment: this) {
+ if (!comment.getStatusEnum().equals(status))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * does passed list contain the same comments as this list?
+ */
+ public boolean isSameList(CommentList comments) {
+ if (comments == null || comments.size() != this.size())
+ return false;
+
+ for (final Comment comment: comments) {
+ int index = this.indexOfCommentId(comment.commentID);
+ if (index == -1)
+ return false;
+ final Comment thisComment = this.get(index);
+ if (!thisComment.getStatus().equals(comment.getStatus()))
+ return false;
+ if (!thisComment.getCommentText().equals(comment.getCommentText()))
+ return false;
+ if (!thisComment.getAuthorName().equals(comment.getAuthorName()))
+ return false;
+ if (!thisComment.getAuthorEmail().equals(comment.getAuthorEmail()))
+ return false;
+ if (!thisComment.getAuthorUrl().equals(comment.getAuthorUrl()))
+ return false;
+
+ }
+
+ return true;
+ }
+
+ public static CommentList fromJSONV1_1(JSONObject object) throws JSONException {
+ CommentList commentList = new CommentList();
+ if (object == null) {
+ return null;
+ } else {
+ JSONArray comments = object.getJSONArray("comments");
+ for (int i=0; i < comments.length(); i++){
+ JSONObject commentJson = comments.getJSONObject(i);
+ commentList.add(Comment.fromJSON(commentJson));
+ }
+ return commentList;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java b/WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java
new file mode 100644
index 000000000..a855e716f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java
@@ -0,0 +1,83 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.StringRes;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+public enum CommentStatus implements FilterCriteria {
+ UNKNOWN(R.string.comment_status_all),
+ UNAPPROVED(R.string.comment_status_unapproved),
+ APPROVED(R.string.comment_status_approved),
+ TRASH(R.string.comment_status_trash),
+ SPAM(R.string.comment_status_spam),
+ DELETE(R.string.comment_status_trash);
+
+ private final int mLabelResId;
+
+ CommentStatus(@StringRes int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ @Override
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+
+ /*
+ * returns the string representation of the passed status, as used by the XMLRPC API
+ */
+ public static String toString(CommentStatus status) {
+ if (status == null){
+ return "";
+ }
+
+ switch (status) {
+ case UNAPPROVED:
+ return "hold";
+ case APPROVED:
+ return "approve";
+ case SPAM:
+ return "spam";
+ case TRASH:
+ return "trash";
+ default:
+ return "";
+ }
+ }
+
+ /*
+ * returns the string representation of the passed status, as used by the REST API
+ */
+ public static String toRESTString(CommentStatus status) {
+ switch (status) {
+ case UNAPPROVED:
+ return "unapproved";
+ case APPROVED:
+ return "approved";
+ case SPAM:
+ return "spam";
+ case TRASH:
+ return "trash";
+ default:
+ return "all";
+ }
+ }
+
+ /*
+ * returns the status associated with the passed strings - handles both XMLRPC and REST
+ */
+ public static CommentStatus fromString(String value) {
+ if (value == null)
+ return CommentStatus.UNKNOWN;
+ if (value.equals("approve") || value.equals("approved"))
+ return CommentStatus.APPROVED;
+ if (value.equals("hold") || value.equals("unapproved"))
+ return CommentStatus.UNAPPROVED;
+ if (value.equals("spam"))
+ return SPAM;
+ if (value.equals("trash"))
+ return TRASH;
+ return CommentStatus.UNKNOWN;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java b/WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java
new file mode 100644
index 000000000..a64445042
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.models;
+
+import java.util.Map;
+
+/**
+ * A Model for parsing the result of wpcom.getFeatures() to retrieve
+ * features for a hosted WordPress.com blog.
+ */
+public class FeatureSet {
+ private int mBlogId;
+
+ private boolean mIsVideopressEnabled = false;
+ // add future features here
+
+ public FeatureSet(int blogId, Map<?,?> map) {
+ setBlogId(blogId);
+
+ if (map.containsKey("videopress_enabled"))
+ setIsVideopressEnabled((Boolean) map.get("videopress_enabled"));
+
+ }
+
+ public boolean isVideopressEnabled() {
+ return mIsVideopressEnabled;
+ }
+
+ public void setIsVideopressEnabled(boolean enabled) {
+ this.mIsVideopressEnabled = enabled;
+ }
+
+ public int getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(int blogId) {
+ this.mBlogId = blogId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java b/WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java
new file mode 100644
index 000000000..db4866752
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java
@@ -0,0 +1,5 @@
+package org.wordpress.android.models;
+
+public interface FilterCriteria {
+ String getLabel();
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java b/WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java
new file mode 100644
index 000000000..5a40b3801
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java
@@ -0,0 +1,15 @@
+package org.wordpress.android.models;
+
+public enum MediaUploadState {
+ QUEUED,
+ UPLOADING,
+ DELETE,
+ DELETED,
+ FAILED,
+ UPLOADED;
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Note.java b/WordPress/src/main/java/org/wordpress/android/models/Note.java
new file mode 100644
index 000000000..7dc1a463b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Note.java
@@ -0,0 +1,590 @@
+/**
+ * Note represents a single WordPress.com notification
+ */
+package org.wordpress.android.models;
+
+import android.text.Html;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.simperium.client.BucketSchema;
+import com.simperium.client.Syncable;
+import com.simperium.util.JSONDiff;
+
+import org.apache.commons.lang.time.DateUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+
+public class Note extends Syncable {
+ private static final String TAG = "NoteModel";
+
+ // Maximum character length for a comment preview
+ static private final int MAX_COMMENT_PREVIEW_LENGTH = 200;
+
+ // Note types
+ public static final String NOTE_FOLLOW_TYPE = "follow";
+ public static final String NOTE_LIKE_TYPE = "like";
+ public static final String NOTE_COMMENT_TYPE = "comment";
+ private static final String NOTE_MATCHER_TYPE = "automattcher";
+ private static final String NOTE_COMMENT_LIKE_TYPE = "comment_like";
+ private static final String NOTE_REBLOG_TYPE = "reblog";
+ private static final String NOTE_UNKNOWN_TYPE = "unknown";
+
+ // JSON action keys
+ private static final String ACTION_KEY_REPLY = "replyto-comment";
+ private static final String ACTION_KEY_APPROVE = "approve-comment";
+ private static final String ACTION_KEY_SPAM = "spam-comment";
+ private static final String ACTION_KEY_LIKE = "like-comment";
+
+ private JSONObject mActions;
+ private JSONObject mNoteJSON;
+ private final String mKey;
+
+ private final Object mSyncLock = new Object();
+ private String mLocalStatus;
+
+ public enum EnabledActions {
+ ACTION_REPLY,
+ ACTION_APPROVE,
+ ACTION_UNAPPROVE,
+ ACTION_SPAM,
+ ACTION_LIKE
+ }
+
+ public enum NoteTimeGroup {
+ GROUP_TODAY,
+ GROUP_YESTERDAY,
+ GROUP_OLDER_TWO_DAYS,
+ GROUP_OLDER_WEEK,
+ GROUP_OLDER_MONTH
+ }
+
+ /**
+ * Create a note using JSON from Simperium
+ */
+ private Note(String key, JSONObject noteJSON) {
+ mKey = key;
+ mNoteJSON = noteJSON;
+ }
+
+ /**
+ * Simperium method @see Diffable
+ */
+ @Override
+ public JSONObject getDiffableValue() {
+ synchronized (mSyncLock) {
+ return JSONDiff.deepCopy(mNoteJSON);
+ }
+ }
+
+ /**
+ * Simperium method for identifying bucket object @see Diffable
+ */
+ @Override
+ public String getSimperiumKey() {
+ return getId();
+ }
+
+ public String getId() {
+ return mKey;
+ }
+
+ public String getType() {
+ return queryJSON("type", NOTE_UNKNOWN_TYPE);
+ }
+
+ private Boolean isType(String type) {
+ return getType().equals(type);
+ }
+
+ public Boolean isCommentType() {
+ synchronized (mSyncLock) {
+ return (isAutomattcherType() && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1) ||
+ isType(NOTE_COMMENT_TYPE);
+ }
+ }
+
+ public Boolean isAutomattcherType() {
+ return isType(NOTE_MATCHER_TYPE);
+ }
+
+ public Boolean isFollowType() {
+ return isType(NOTE_FOLLOW_TYPE);
+ }
+
+ public Boolean isLikeType() {
+ return isType(NOTE_LIKE_TYPE);
+ }
+
+ public Boolean isCommentLikeType() {
+ return isType(NOTE_COMMENT_LIKE_TYPE);
+ }
+
+ public Boolean isReblogType() {
+ return isType(NOTE_REBLOG_TYPE);
+ }
+
+ public Boolean isCommentReplyType() {
+ return isCommentType() && getParentCommentId() > 0;
+ }
+
+ // Returns true if the user has replied to this comment note
+ public Boolean isCommentWithUserReply() {
+ return isCommentType() && !TextUtils.isEmpty(getCommentSubjectNoticon());
+ }
+
+ public Boolean isUserList() {
+ return isLikeType() || isCommentLikeType() || isFollowType() || isReblogType();
+ }
+
+ /*
+ * does user have permission to moderate/reply/spam this comment?
+ */
+ public boolean canModerate() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return enabledActions != null && (enabledActions.contains(EnabledActions.ACTION_APPROVE) || enabledActions.contains(EnabledActions.ACTION_UNAPPROVE));
+ }
+
+ public boolean canMarkAsSpam() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_SPAM));
+ }
+
+ public boolean canReply() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_REPLY));
+ }
+
+ public boolean canTrash() {
+ return canModerate();
+ }
+
+ public boolean canEdit(int localBlogId) {
+ return (localBlogId > 0 && canModerate());
+ }
+
+ public boolean canLike() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_LIKE));
+ }
+
+ private String getLocalStatus() {
+ return StringUtils.notNullStr(mLocalStatus);
+ }
+
+ public void setLocalStatus(String localStatus) {
+ mLocalStatus = localStatus;
+ }
+
+ private JSONObject getSubject() {
+ try {
+ synchronized (mSyncLock) {
+ JSONArray subjectArray = mNoteJSON.getJSONArray("subject");
+ if (subjectArray.length() > 0) {
+ return subjectArray.getJSONObject(0);
+ }
+ }
+ } catch (JSONException e) {
+ return null;
+ }
+
+ return null;
+ }
+
+ private Spannable getFormattedSubject() {
+ return NotificationsUtils.getSpannableContentForRanges(getSubject());
+ }
+
+ public String getTitle() {
+ return queryJSON("title", "");
+ }
+
+ private String getIconURL() {
+ return queryJSON("icon", "");
+ }
+
+ private String getCommentSubject() {
+ synchronized (mSyncLock) {
+ JSONArray subjectArray = mNoteJSON.optJSONArray("subject");
+ if (subjectArray != null) {
+ String commentSubject = JSONUtils.queryJSON(subjectArray, "subject[1].text", "");
+
+ // Trim down the comment preview if the comment text is too large.
+ if (commentSubject != null && commentSubject.length() > MAX_COMMENT_PREVIEW_LENGTH) {
+ commentSubject = commentSubject.substring(0, MAX_COMMENT_PREVIEW_LENGTH - 1);
+ }
+
+ return commentSubject;
+ }
+
+ }
+
+ return "";
+ }
+
+ private String getCommentSubjectNoticon() {
+ JSONArray subjectRanges = queryJSON("subject[0].ranges", new JSONArray());
+ if (subjectRanges != null) {
+ for (int i=0; i < subjectRanges.length(); i++) {
+ try {
+ JSONObject rangeItem = subjectRanges.getJSONObject(i);
+ if (rangeItem.has("type") && rangeItem.optString("type").equals("noticon")) {
+ return rangeItem.optString("value", "");
+ }
+ } catch (JSONException e) {
+ return "";
+ }
+ }
+ }
+
+ return "";
+ }
+
+ public long getCommentReplyId() {
+ return queryJSON("meta.ids.reply_comment", 0);
+ }
+
+ /**
+ * Compare note timestamp to now and return a time grouping
+ */
+ public static NoteTimeGroup getTimeGroupForTimestamp(long timestamp) {
+ Date today = new Date();
+ Date then = new Date(timestamp * 1000);
+
+ if (then.compareTo(DateUtils.addMonths(today, -1)) < 0) {
+ return NoteTimeGroup.GROUP_OLDER_MONTH;
+ } else if (then.compareTo(DateUtils.addWeeks(today, -1)) < 0) {
+ return NoteTimeGroup.GROUP_OLDER_WEEK;
+ } else if (then.compareTo(DateUtils.addDays(today, -2)) < 0
+ || DateUtils.isSameDay(DateUtils.addDays(today, -2), then)) {
+ return NoteTimeGroup.GROUP_OLDER_TWO_DAYS;
+ } else if (DateUtils.isSameDay(DateUtils.addDays(today, -1), then)) {
+ return NoteTimeGroup.GROUP_YESTERDAY;
+ } else {
+ return NoteTimeGroup.GROUP_TODAY;
+ }
+ }
+
+ /**
+ * The inverse of isRead
+ */
+ public Boolean isUnread() {
+ return !isRead();
+ }
+
+ private Boolean isRead() {
+ return queryJSON("read", 0) == 1;
+ }
+
+ public void markAsRead() {
+ try {
+ synchronized (mSyncLock) {
+ mNoteJSON.put("read", 1);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Unable to update note read property", e);
+ return;
+ }
+ save();
+ }
+
+ /**
+ * Get the timestamp provided by the API for the note
+ */
+ public long getTimestamp() {
+ return DateTimeUtils.timestampFromIso8601(queryJSON("timestamp", ""));
+ }
+
+ public JSONArray getBody() {
+ try {
+ synchronized (mSyncLock) {
+ return mNoteJSON.getJSONArray("body");
+ }
+ } catch (JSONException e) {
+ return new JSONArray();
+ }
+ }
+
+ // returns character code for notification font
+ private String getNoticonCharacter() {
+ return queryJSON("noticon", "");
+ }
+
+ private JSONObject getCommentActions() {
+ if (mActions == null) {
+ // Find comment block that matches the root note comment id
+ long commentId = getCommentId();
+ JSONArray bodyArray = getBody();
+ for (int i = 0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject bodyItem = bodyArray.getJSONObject(i);
+ if (bodyItem.has("type") && bodyItem.optString("type").equals("comment")
+ && commentId == JSONUtils.queryJSON(bodyItem, "meta.ids.comment", 0)) {
+ mActions = JSONUtils.queryJSON(bodyItem, "actions", new JSONObject());
+ break;
+ }
+ } catch (JSONException e) {
+ break;
+ }
+ }
+
+ if (mActions == null) {
+ mActions = new JSONObject();
+ }
+ }
+
+ return mActions;
+ }
+
+
+ private void updateJSON(JSONObject json) {
+ synchronized (mSyncLock) {
+ mNoteJSON = json;
+ }
+ }
+
+ /*
+ * returns the actions allowed on this note, assumes it's a comment notification
+ */
+ public EnumSet<EnabledActions> getEnabledActions() {
+ EnumSet<EnabledActions> actions = EnumSet.noneOf(EnabledActions.class);
+ JSONObject jsonActions = getCommentActions();
+ if (jsonActions == null || jsonActions.length() == 0) {
+ return actions;
+ }
+
+ if (jsonActions.has(ACTION_KEY_REPLY)) {
+ actions.add(EnabledActions.ACTION_REPLY);
+ }
+ if (jsonActions.has(ACTION_KEY_APPROVE) && jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
+ actions.add(EnabledActions.ACTION_UNAPPROVE);
+ }
+ if (jsonActions.has(ACTION_KEY_APPROVE) && !jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
+ actions.add(EnabledActions.ACTION_APPROVE);
+ }
+ if (jsonActions.has(ACTION_KEY_SPAM)) {
+ actions.add(EnabledActions.ACTION_SPAM);
+ }
+ if (jsonActions.has(ACTION_KEY_LIKE)) {
+ actions.add(EnabledActions.ACTION_LIKE);
+ }
+
+ return actions;
+ }
+
+ public int getSiteId() {
+ return queryJSON("meta.ids.site", 0);
+ }
+
+ public int getPostId() {
+ return queryJSON("meta.ids.post", 0);
+ }
+
+ public long getCommentId() {
+ return queryJSON("meta.ids.comment", 0);
+ }
+
+ public long getParentCommentId() {
+ return queryJSON("meta.ids.parent_comment", 0);
+ }
+
+ /**
+ * Rudimentary system for pulling an item out of a JSON object hierarchy
+ */
+ private <U> U queryJSON(String query, U defaultObject) {
+ synchronized (mSyncLock) {
+ if (mNoteJSON == null) return defaultObject;
+ return JSONUtils.queryJSON(mNoteJSON, query, defaultObject);
+ }
+ }
+
+ /**
+ * Constructs a new Comment object based off of data in a Note
+ */
+ public Comment buildComment() {
+ return new Comment(
+ getPostId(),
+ getCommentId(),
+ getCommentAuthorName(),
+ DateTimeUtils.iso8601FromTimestamp(getTimestamp()),
+ getCommentText(),
+ CommentStatus.toString(getCommentStatus()),
+ "", // post title is unavailable in note model
+ getCommentAuthorUrl(),
+ "", // user email is unavailable in note model
+ getIconURL()
+ );
+ }
+
+ public String getCommentAuthorName() {
+ JSONArray bodyArray = getBody();
+
+ for (int i=0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject bodyItem = bodyArray.getJSONObject(i);
+ if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
+ return bodyItem.optString("text");
+ }
+ } catch (JSONException e) {
+ return "";
+ }
+ }
+
+ return "";
+ }
+
+ private String getCommentText() {
+ return queryJSON("body[last].text", "");
+ }
+
+ private String getCommentAuthorUrl() {
+ JSONArray bodyArray = getBody();
+
+ for (int i=0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject bodyItem = bodyArray.getJSONObject(i);
+ if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
+ return JSONUtils.queryJSON(bodyItem, "meta.links.home", "");
+ }
+ } catch (JSONException e) {
+ return "";
+ }
+ }
+
+ return "";
+ }
+
+ public CommentStatus getCommentStatus() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+
+ if (enabledActions.contains(EnabledActions.ACTION_UNAPPROVE)) {
+ return CommentStatus.APPROVED;
+ } else if (enabledActions.contains(EnabledActions.ACTION_APPROVE)) {
+ return CommentStatus.UNAPPROVED;
+ }
+
+ return CommentStatus.UNKNOWN;
+ }
+
+ public boolean hasLikedComment() {
+ JSONObject jsonActions = getCommentActions();
+ return !(jsonActions == null || jsonActions.length() == 0) && jsonActions.optBoolean(ACTION_KEY_LIKE);
+ }
+
+ public String getUrl() {
+ return queryJSON("url", "");
+ }
+
+ public JSONArray getHeader() {
+ synchronized (mSyncLock) {
+ return mNoteJSON.optJSONArray("header");
+ }
+ }
+
+ /**
+ * Represents a user replying to a note.
+ */
+ public static class Reply {
+ private final String mContent;
+ private final String mRestPath;
+
+ Reply(String restPath, String content) {
+ mRestPath = restPath;
+ mContent = content;
+ }
+
+ public String getContent() {
+ return mContent;
+ }
+
+ public String getRestPath() {
+ return mRestPath;
+ }
+ }
+
+ public Reply buildReply(String content) {
+ String restPath;
+ if (this.isCommentType()) {
+ restPath = String.format("sites/%d/comments/%d", getSiteId(), getCommentId());
+ } else {
+ restPath = String.format("sites/%d/posts/%d", getSiteId(), getPostId());
+ }
+
+ return new Reply(String.format("%s/replies/new", restPath), content);
+ }
+
+ /**
+ * Simperium Schema
+ */
+ public static class Schema extends BucketSchema<Note> {
+
+ static public final String NAME = "note20";
+ static public final String TIMESTAMP_INDEX = "timestamp";
+ static public final String SUBJECT_INDEX = "subject";
+ static public final String SNIPPET_INDEX = "snippet";
+ static public final String UNREAD_INDEX = "unread";
+ static public final String NOTICON_INDEX = "noticon";
+ static public final String ICON_URL_INDEX = "icon";
+ static public final String IS_UNAPPROVED_INDEX = "unapproved";
+ static public final String COMMENT_SUBJECT_NOTICON = "comment_subject_noticon";
+ static public final String LOCAL_STATUS = "local_status";
+ static public final String TYPE_INDEX = "type";
+
+ private static final Indexer<Note> sNoteIndexer = new Indexer<Note>() {
+
+ @Override
+ public List<Index> index(Note note) {
+ List<Index> indexes = new ArrayList<>();
+ try {
+ indexes.add(new Index(TIMESTAMP_INDEX, note.getTimestamp()));
+ } catch (NumberFormatException e) {
+ // note will not have an indexed timestamp so it will
+ // show up at the end of a query sorting by timestamp
+ android.util.Log.e("WordPress", "Failed to index timestamp", e);
+ }
+
+ indexes.add(new Index(SUBJECT_INDEX, Html.toHtml(note.getFormattedSubject())));
+ indexes.add(new Index(SNIPPET_INDEX, note.getCommentSubject()));
+ indexes.add(new Index(UNREAD_INDEX, note.isUnread()));
+ indexes.add(new Index(NOTICON_INDEX, note.getNoticonCharacter()));
+ indexes.add(new Index(ICON_URL_INDEX, note.getIconURL()));
+ indexes.add(new Index(IS_UNAPPROVED_INDEX, note.getCommentStatus() == CommentStatus.UNAPPROVED));
+ indexes.add(new Index(COMMENT_SUBJECT_NOTICON, note.getCommentSubjectNoticon()));
+ indexes.add(new Index(LOCAL_STATUS, note.getLocalStatus()));
+ indexes.add(new Index(TYPE_INDEX, note.getType()));
+
+ return indexes;
+ }
+
+ };
+
+ public Schema() {
+ addIndex(sNoteIndexer);
+ }
+
+ @Override
+ public String getRemoteName() {
+ return NAME;
+ }
+
+ @Override
+ public Note build(String key, JSONObject properties) {
+ return new Note(key, properties);
+ }
+
+ public void update(Note note, JSONObject properties) {
+ note.updateJSON(properties);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java
new file mode 100644
index 000000000..688c3821a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java
@@ -0,0 +1,115 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+// Maps to notification settings returned from the /me/notifications/settings endpoint on wp.com
+public class NotificationsSettings {
+
+ public static final String KEY_BLOGS = "blogs";
+ public static final String KEY_OTHER = "other";
+ public static final String KEY_DOTCOM = "wpcom";
+ public static final String KEY_DEVICES = "devices";
+
+ public static final String KEY_DEVICE_ID = "device_id";
+ public static final String KEY_BLOG_ID = "blog_id";
+
+ private JSONObject mOtherSettings;
+ private JSONObject mDotcomSettings;
+ private Map<Long, JSONObject> mBlogSettings;
+
+ // The main notification settings channels (displayed at root of NoticationsSettingsFragment)
+ public enum Channel {
+ OTHER,
+ BLOGS,
+ DOTCOM
+ }
+
+ // The notification setting type, used in BLOGS and OTHER channels
+ public enum Type {
+ TIMELINE,
+ EMAIL,
+ DEVICE;
+
+ public String toString() {
+ switch (this) {
+ case TIMELINE:
+ return "timeline";
+ case EMAIL:
+ return "email";
+ case DEVICE:
+ return "device";
+ default:
+ return "";
+ }
+ }
+ }
+
+ public NotificationsSettings(JSONObject json) {
+ updateJson(json);
+ }
+
+ // Parses the json response from /me/notifications/settings endpoint and updates the instance variables
+ public void updateJson(JSONObject json) {
+ mBlogSettings = new HashMap<>();
+
+ mOtherSettings = JSONUtils.queryJSON(json, KEY_OTHER, new JSONObject());
+ mDotcomSettings = JSONUtils.queryJSON(json, KEY_DOTCOM, new JSONObject());
+
+ JSONArray siteSettingsArray = JSONUtils.queryJSON(json, KEY_BLOGS, new JSONArray());
+ for (int i=0; i < siteSettingsArray.length(); i++) {
+ try {
+ JSONObject siteSetting = siteSettingsArray.getJSONObject(i);
+ mBlogSettings.put(siteSetting.optLong(KEY_BLOG_ID), siteSetting);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not parse blog JSON in notification settings");
+ }
+ }
+ }
+
+ // Updates a specific notification setting after a user makes a change
+ public void updateSettingForChannelAndType(Channel channel, Type type, String settingName, boolean newValue, long blogId) {
+ String typeName = type.toString();
+ try {
+ switch (channel) {
+ case BLOGS:
+ JSONObject blogJson = getBlogSettings().get(blogId);
+ if (blogJson != null) {
+ JSONObject blogSetting = JSONUtils.queryJSON(blogJson, typeName, new JSONObject());
+ blogSetting.put(settingName, newValue);
+ blogJson.put(typeName, blogSetting);
+
+ getBlogSettings().put(blogId, blogJson);
+ }
+ break;
+ case OTHER:
+ JSONObject otherSetting = JSONUtils.queryJSON(getOtherSettings(), typeName, new JSONObject());
+ otherSetting.put(settingName, newValue);
+ getOtherSettings().put(typeName, otherSetting);
+ break;
+ case DOTCOM:
+ getDotcomSettings().put(settingName, newValue);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not update notifications settings JSON");
+ }
+ }
+
+ public JSONObject getOtherSettings() {
+ return mOtherSettings;
+ }
+
+ public Map<Long, JSONObject> getBlogSettings() {
+ return mBlogSettings;
+ }
+
+ public JSONObject getDotcomSettings() {
+ return mDotcomSettings;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java b/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
new file mode 100644
index 000000000..4c62fd9c7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
@@ -0,0 +1,24 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.StringRes;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+public enum PeopleListFilter implements FilterCriteria {
+ TEAM(R.string.people_dropdown_item_team),
+ FOLLOWERS(R.string.people_dropdown_item_followers),
+ EMAIL_FOLLOWERS(R.string.people_dropdown_item_email_followers),
+ VIEWERS(R.string.people_dropdown_item_viewers);
+
+ private final int mLabelResId;
+
+ PeopleListFilter(@StringRes int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ @Override
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Person.java b/WordPress/src/main/java/org/wordpress/android/models/Person.java
new file mode 100644
index 000000000..f5c841bb4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Person.java
@@ -0,0 +1,174 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.Nullable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class Person {
+ public enum PersonType { USER, FOLLOWER, EMAIL_FOLLOWER, VIEWER }
+
+ private long personID;
+ private int localTableBlogId;
+ private String displayName;
+ private String avatarUrl;
+ private PersonType personType;
+
+ // Only users have a role
+ private Role role;
+
+ // Users, followers & viewers has a username, email followers don't
+ private String username;
+
+ // Only followers & email followers have a subscribed date
+ private String subscribed;
+
+ public Person(long personID, int localTableBlogId) {
+ this.personID = personID;
+ this.localTableBlogId = localTableBlogId;
+ }
+
+ @Nullable
+ public static Person userFromJSON(JSONObject json, int localTableBlogId) throws JSONException {
+ if (json == null) {
+ return null;
+ }
+
+ // Response parameters are in: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/users/%24user_id/
+ try {
+ long personID = Long.parseLong(json.getString("ID"));
+ Person person = new Person(personID, localTableBlogId);
+ person.setUsername(json.optString("login"));
+ person.setDisplayName(json.optString("name"));
+ person.setAvatarUrl(json.optString("avatar_URL"));
+ person.personType = PersonType.USER;
+ // We don't support multiple roles, so the first role is picked just as it's in Calypso
+ String role = json.getJSONArray("roles").optString(0);
+ person.setRole(Role.fromString(role));
+
+ return person;
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.PEOPLE, "The ID parsed from the JSON couldn't be converted to long: " + e);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public static Person followerFromJSON(JSONObject json, int localTableBlogId, boolean isEmailFollower)
+ throws JSONException {
+ if (json == null) {
+ return null;
+ }
+
+ // Response parameters are in: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/followers/
+ try {
+ long personID = Long.parseLong(json.getString("ID"));
+ Person person = new Person(personID, localTableBlogId);
+ person.setDisplayName(json.optString("label"));
+ person.setUsername(json.optString("login"));
+ person.setAvatarUrl(json.optString("avatar"));
+ person.setSubscribed(json.optString("date_subscribed"));
+ person.personType = isEmailFollower ? PersonType.EMAIL_FOLLOWER : PersonType.FOLLOWER;
+
+ return person;
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.PEOPLE, "The ID parsed from the JSON couldn't be converted to long: " + e);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public static Person viewerFromJSON(JSONObject json, int localTableBlogId) throws JSONException {
+ if (json == null) {
+ return null;
+ }
+
+ // Similar response parameters in:
+ // https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/users/%24user_id/
+ try {
+ long personID = Long.parseLong(json.getString("ID"));
+ Person person = new Person(personID, localTableBlogId);
+ person.setUsername(json.optString("login"));
+ person.setDisplayName(json.optString("name"));
+ person.setAvatarUrl(json.optString("avatar_URL"));
+ person.setPersonType(PersonType.VIEWER);
+
+ return person;
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.PEOPLE, "The ID parsed from the JSON couldn't be converted to long: " + e);
+ }
+
+ return null;
+ }
+
+ public long getPersonID() {
+ return personID;
+ }
+
+ public int getLocalTableBlogId() {
+ return localTableBlogId;
+ }
+
+ public String getUsername() {
+ return StringUtils.notNullStr(username);
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getDisplayName() {
+ return StringUtils.notNullStr(displayName);
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+
+ public void setRole(Role role) {
+ this.role = role;
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(avatarUrl);
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public String getSubscribed() {
+ return StringUtils.notNullStr(subscribed);
+ }
+
+ public void setSubscribed(String subscribed) {
+ this.subscribed = StringUtils.notNullStr(subscribed);
+ }
+
+ /*
+ * converts iso8601 subscribed date to an actual java date
+ */
+ private transient java.util.Date dtSubscribed;
+ public java.util.Date getDateSubscribed() {
+ if (dtSubscribed == null)
+ dtSubscribed = DateTimeUtils.dateFromIso8601(subscribed);
+ return dtSubscribed;
+ }
+
+ public PersonType getPersonType() {
+ return personType;
+ }
+
+ public void setPersonType(PersonType personType) {
+ this.personType = personType;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Post.java b/WordPress/src/main/java/org/wordpress/android/models/Post.java
new file mode 100644
index 000000000..c62e2a5f5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Post.java
@@ -0,0 +1,505 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+
+import java.io.Serializable;
+
+public class Post implements Serializable {
+ // Increment this value if this model changes
+ // See: http://www.javapractices.com/topic/TopicAction.do?Id=45
+ static final long serialVersionUID = 2L;
+
+ public static String QUICK_MEDIA_TYPE_PHOTO = "QuickPhoto";
+
+ private static long FEATURED_IMAGE_INIT_VALUE = -2;
+
+ private long localTablePostId;
+ private int localTableBlogId;
+ private String categories;
+ private String customFields;
+ private long dateCreated;
+ private long dateCreatedGmt;
+ private String description;
+ private String link;
+ private boolean allowComments;
+ private boolean allowPings;
+ private String excerpt;
+ private String keywords;
+ private String moreText;
+ private String permaLink;
+ private String status;
+ private String remotePostId;
+ private String title;
+ private String userId;
+ private String authorDisplayName;
+ private String authorId;
+ private String password;
+ private String postFormat;
+ private String slug;
+
+ private boolean localDraft;
+ private boolean mChangedFromLocalToPublished;
+ private boolean isPage;
+ private String pageParentId;
+ private String pageParentTitle;
+ private boolean isLocalChange;
+ private String mediaPaths;
+ private String quickPostType;
+ private PostLocation mPostLocation;
+
+ private long lastKnownRemoteFeaturedImageId;
+ private long featuredImageId = FEATURED_IMAGE_INIT_VALUE;
+
+ public Post() {
+ }
+
+ public Post(int blogId, boolean isPage) {
+ // creates a new, empty post for the passed in blogId
+ this.localTableBlogId = blogId;
+ this.isPage = isPage;
+ this.localDraft = true;
+ }
+
+ public long getLocalTablePostId() {
+ return localTablePostId;
+ }
+
+ public long getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(long dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ public long getDate_created_gmt() {
+ return dateCreatedGmt;
+ }
+
+ public void setDate_created_gmt(long dateCreatedGmt) {
+ this.dateCreatedGmt = dateCreatedGmt;
+ }
+
+ public void setCategories(String postCategories) {
+ this.categories = postCategories;
+ }
+
+ public void setCustomFields(String customFields) {
+ this.customFields = customFields;
+ }
+
+ public int getLocalTableBlogId() {
+ return localTableBlogId;
+ }
+
+ public void setLocalTableBlogId(int localTableBlogId) {
+ this.localTableBlogId = localTableBlogId;
+ }
+
+ public boolean isLocalDraft() {
+ return localDraft;
+ }
+
+ public void setLocalDraft(boolean localDraft) {
+ this.localDraft = localDraft;
+ }
+
+ public JSONArray getJSONCategories() {
+ JSONArray jArray = null;
+ if (categories == null) {
+ categories = "[]";
+ }
+ try {
+ categories = StringUtils.unescapeHTML(categories);
+ if (TextUtils.isEmpty(categories)) {
+ jArray = new JSONArray();
+ } else {
+ jArray = new JSONArray(categories);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ return jArray;
+ }
+
+ public void setJSONCategories(JSONArray categories) {
+ this.categories = categories.toString();
+ }
+
+ public JSONArray getCustomFields() {
+ if (customFields == null) {
+ return null;
+ }
+ JSONArray jArray = null;
+ try {
+ jArray = new JSONArray(customFields);
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, "No custom fields found for post.");
+ }
+ return jArray;
+ }
+
+ public JSONObject getCustomField(String key) {
+ JSONArray customFieldsJson = getCustomFields();
+ if (customFieldsJson == null) {
+ return null;
+ }
+
+ for (int i = 0; i < customFieldsJson.length(); i++) {
+ try {
+ JSONObject jsonObject = new JSONObject(customFieldsJson.getString(i));
+ String curentKey = jsonObject.getString("key");
+ if (key.equals(curentKey)) {
+ return jsonObject;
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ return null;
+ }
+
+ public void setCustomFields(JSONArray customFields) {
+ this.customFields = customFields.toString();
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(description);
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getLink() {
+ return StringUtils.notNullStr(link);
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public boolean isAllowComments() {
+ return allowComments;
+ }
+
+ public void setAllowComments(boolean mtAllowComments) {
+ allowComments = mtAllowComments;
+ }
+
+ public boolean isAllowPings() {
+ return allowPings;
+ }
+
+ public void setAllowPings(boolean mtAllowPings) {
+ allowPings = mtAllowPings;
+ }
+
+ public String getPostExcerpt() {
+ return StringUtils.notNullStr(excerpt);
+ }
+
+ public void setPostExcerpt(String mtExcerpt) {
+ excerpt = mtExcerpt;
+ }
+
+ public String getKeywords() {
+ return StringUtils.notNullStr(keywords);
+ }
+
+ public void setKeywords(String mtKeywords) {
+ keywords = mtKeywords;
+ }
+
+ public String getMoreText() {
+ return StringUtils.notNullStr(moreText);
+ }
+
+ public void setMoreText(String mtTextMore) {
+ moreText = mtTextMore;
+ }
+
+ public String getPermaLink() {
+ return StringUtils.notNullStr(permaLink);
+ }
+
+ public void setPermaLink(String permaLink) {
+ this.permaLink = permaLink;
+ }
+
+ public String getPostStatus() {
+ return StringUtils.notNullStr(status);
+ }
+
+ public void setPostStatus(String postStatus) {
+ status = postStatus;
+ }
+
+ public PostStatus getStatusEnum() {
+ return PostStatus.fromPost(this);
+ }
+
+ public String getRemotePostId() {
+ return StringUtils.notNullStr(remotePostId);
+ }
+
+ public void setRemotePostId(String postId) {
+ this.remotePostId = postId;
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getUserId() {
+ return StringUtils.notNullStr(userId);
+ }
+
+ public void setUserId(String userid) {
+ this.userId = userid;
+ }
+
+ public String getAuthorDisplayName() {
+ return StringUtils.notNullStr(authorDisplayName);
+ }
+
+ public void setAuthorDisplayName(String wpAuthorDisplayName) {
+ authorDisplayName = wpAuthorDisplayName;
+ }
+
+ public String getAuthorId() {
+ return StringUtils.notNullStr(authorId);
+ }
+
+ public void setAuthorId(String wpAuthorId) {
+ authorId = wpAuthorId;
+ }
+
+ public String getPassword() {
+ return StringUtils.notNullStr(password);
+ }
+
+ public void setPassword(String wpPassword) {
+ password = wpPassword;
+ }
+
+ public String getPostFormat() {
+ return StringUtils.notNullStr(postFormat);
+ }
+
+ public void setPostFormat(String wpPostForm) {
+ postFormat = wpPostForm;
+ }
+
+ public String getSlug() {
+ return StringUtils.notNullStr(slug);
+ }
+
+ public void setSlug(String wpSlug) {
+ slug = wpSlug;
+ }
+
+ public String getMediaPaths() {
+ return StringUtils.notNullStr(mediaPaths);
+ }
+
+ public void setMediaPaths(String mediaPaths) {
+ this.mediaPaths = mediaPaths;
+ }
+
+ public boolean supportsLocation() {
+ // Right now, we only disable for pages.
+ return !isPage();
+ }
+
+ public boolean hasLocation() {
+ return mPostLocation != null && mPostLocation.isValid();
+ }
+
+ public PostLocation getLocation() {
+ return mPostLocation;
+ }
+
+ public void setLocation(PostLocation location) {
+ mPostLocation = location;
+ }
+
+ public void unsetLocation() {
+ mPostLocation = null;
+ }
+
+ public void setLocation(double latitude, double longitude) {
+ try {
+ mPostLocation = new PostLocation(latitude, longitude);
+ } catch (IllegalArgumentException e) {
+ mPostLocation = null;
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ public boolean isPage() {
+ return isPage;
+ }
+
+ public void setIsPage(boolean isPage) {
+ this.isPage = isPage;
+ }
+
+ public String getPageParentId() {
+ return StringUtils.notNullStr(pageParentId);
+ }
+
+ public void setPageParentId(String wp_page_parent_id) {
+ this.pageParentId = wp_page_parent_id;
+ }
+
+ public String getPageParentTitle() {
+ return StringUtils.notNullStr(pageParentTitle);
+ }
+
+ public void setPageParentTitle(String wp_page_parent_title) {
+ this.pageParentTitle = wp_page_parent_title;
+ }
+
+ public boolean isLocalChange() {
+ return isLocalChange;
+ }
+
+ public void setLocalChange(boolean isLocalChange) {
+ this.isLocalChange = isLocalChange;
+ }
+
+ public void setLocalTablePostId(long id) {
+ this.localTablePostId = id;
+ }
+
+ public void setQuickPostType(String type) {
+ this.quickPostType = type;
+ }
+
+ public String getQuickPostType() {
+ return StringUtils.notNullStr(quickPostType);
+ }
+
+ /**
+ * This indicates if the post has changed from a draft to published. This is primarily used
+ * for stats tracking purposes as we want to ensure that we properly track certain things when
+ * the user first publishes a post
+ * @return
+ */
+ public boolean hasChangedFromDraftToPublished() {
+ return mChangedFromLocalToPublished;
+ }
+
+ public void setChangedFromDraftToPublished(boolean changedFromDraftToPublished) {
+ this.mChangedFromLocalToPublished = changedFromDraftToPublished;
+ }
+
+ /**
+ * Checks if this post currently has data differing from another post.
+ *
+ * @param otherPost The post to compare to this post's editable data.
+ * @return True if this post's data differs from otherPost's data, False otherwise.
+ */
+ public boolean hasChanges(Post otherPost) {
+ return otherPost == null || !(StringUtils.equals(title, otherPost.title) &&
+ StringUtils.equals(description, otherPost.description) &&
+ StringUtils.equals(excerpt, otherPost.excerpt) &&
+ StringUtils.equals(keywords, otherPost.keywords) &&
+ StringUtils.equals(categories, otherPost.categories) &&
+ StringUtils.equals(status, otherPost.status) &&
+ StringUtils.equals(password, otherPost.password) &&
+ StringUtils.equals(postFormat, otherPost.postFormat) &&
+ this.dateCreatedGmt == otherPost.dateCreatedGmt &&
+ PostLocation.equals(this.mPostLocation, otherPost.mPostLocation)
+ );
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + localTableBlogId;
+ result = prime * result + (int) (localTablePostId ^ (localTablePostId >>> 32));
+ result = prime * result + (isPage ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this)
+ return true;
+ if (other instanceof Post) {
+ Post otherPost = (Post) other;
+ return (this.localTablePostId == otherPost.localTablePostId &&
+ this.isPage == otherPost.isPage &&
+ this.localTableBlogId == otherPost.localTableBlogId
+ );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the entire post content
+ * Joins description and moreText fields if both are valid
+ * @return post content as String
+ */
+ public String getContent() {
+ String postContent;
+ if (!TextUtils.isEmpty(getMoreText())) {
+ if (isLocalDraft()) {
+ postContent = getDescription() + "\n&lt;!--more--&gt;\n" + getMoreText();
+ } else {
+ postContent = getDescription() + "\n<!--more-->\n" + getMoreText();
+ }
+ } else {
+ postContent = getDescription();
+ }
+
+ return postContent;
+ }
+
+ public boolean isPublished() {
+ return !getRemotePostId().isEmpty();
+ }
+
+ public boolean isPublishable() {
+ return !(getContent().isEmpty() && getPostExcerpt().isEmpty() && getTitle().isEmpty());
+ }
+
+ public boolean hasEmptyContentFields() {
+ return TextUtils.isEmpty(this.getTitle()) && TextUtils.isEmpty(this.getContent());
+ }
+
+ public long getFeaturedImageId() {
+ if (featuredImageId == FEATURED_IMAGE_INIT_VALUE) {
+ return 0;
+ }
+
+ return featuredImageId;
+ }
+
+ public void setFeaturedImageId(long id) {
+ if (featuredImageId == FEATURED_IMAGE_INIT_VALUE) {
+ lastKnownRemoteFeaturedImageId = id;
+ }
+
+ featuredImageId = id;
+ }
+
+ public boolean featuredImageHasChanged() {
+ return (lastKnownRemoteFeaturedImageId != featuredImageId);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostLocation.java b/WordPress/src/main/java/org/wordpress/android/models/PostLocation.java
new file mode 100644
index 000000000..fb48e0bf6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostLocation.java
@@ -0,0 +1,93 @@
+package org.wordpress.android.models;
+
+import java.io.Serializable;
+
+public class PostLocation implements Serializable {
+ static final double INVALID_LATITUDE = 9999;
+ static final double INVALID_LONGITUDE = 9999;
+ static final double MIN_LATITUDE = -90;
+ static final double MAX_LATITUDE = 90;
+ static final double MIN_LONGITUDE = -180;
+ static final double MAX_LONGITUDE = 180;
+
+ private double mLatitude = INVALID_LATITUDE;
+ private double mLongitude = INVALID_LONGITUDE;
+
+ public PostLocation() { }
+
+ public PostLocation(double latitude, double longitude) {
+ setLatitude(latitude);
+ setLongitude(longitude);
+ }
+
+ public boolean isValid() {
+ return isValidLatitude(mLatitude) && isValidLongitude(mLongitude);
+ }
+
+ public double getLatitude() {
+ return mLatitude;
+ }
+
+ public void setLatitude(double latitude) {
+ if (!isValidLatitude(latitude)) {
+ throw new IllegalArgumentException(
+ "Invalid latitude; must be between the range " + MIN_LATITUDE + " and " + MAX_LATITUDE
+ );
+ }
+
+ mLatitude = latitude;
+ }
+
+ public double getLongitude() {
+ return mLongitude;
+ }
+
+ public void setLongitude(double longitude) {
+ if (!isValidLongitude(longitude)) {
+ throw new IllegalArgumentException(
+ "Invalid longitude; must be between the range " + MIN_LONGITUDE + " and " + MAX_LONGITUDE
+ );
+ }
+
+ mLongitude = longitude;
+ }
+
+ private boolean isValidLatitude(double latitude) {
+ return latitude >= MIN_LATITUDE && latitude <= MAX_LATITUDE;
+ }
+
+ private boolean isValidLongitude(double longitude) {
+ return longitude >= MIN_LONGITUDE && longitude <= MAX_LONGITUDE;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ int hashCode = 1;
+
+ hashCode = prime * hashCode + (Double.valueOf(mLatitude).hashCode());
+ hashCode = prime * hashCode + (Double.valueOf(mLongitude).hashCode());
+
+ return hashCode;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (other instanceof PostLocation) {
+ PostLocation otherLocation = (PostLocation) other;
+ return this.mLatitude == otherLocation.mLatitude
+ && this.mLongitude == otherLocation.mLongitude;
+ }
+ return false;
+ }
+
+ public static boolean equals(Object a, Object b) {
+ if (a == b) {
+ return true;
+ } else if (a == null || b == null) {
+ return false;
+ } else {
+ return a.equals(b);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostStatus.java b/WordPress/src/main/java/org/wordpress/android/models/PostStatus.java
new file mode 100644
index 000000000..192a273a4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostStatus.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.models;
+
+import java.util.Date;
+
+public enum PostStatus {
+ UNKNOWN,
+ PUBLISHED,
+ DRAFT,
+ PRIVATE,
+ PENDING,
+ TRASHED,
+ SCHEDULED; //NOTE: Only used locally, WP has a 'future' status but it is not returned from the metaWeblog API
+
+ private synchronized static PostStatus fromStringAndDateGMT(String value, long dateCreatedGMT) {
+ if (value == null) {
+ return PostStatus.UNKNOWN;
+ } else if (value.equals("publish")) {
+ // Check if post is scheduled
+ Date d = new Date();
+ // Subtract 10 seconds from the server GMT date, in case server and device time slightly differ
+ if (dateCreatedGMT - 10000 > d.getTime()) {
+ return SCHEDULED;
+ }
+ return PUBLISHED;
+ } else if (value.equals("draft")) {
+ return PostStatus.DRAFT;
+ } else if (value.equals("private")) {
+ return PostStatus.PRIVATE;
+ } else if (value.equals("pending")) {
+ return PENDING;
+ } else if (value.equals("trash")) {
+ return TRASHED;
+ } else if (value.equals("future")) {
+ return SCHEDULED;
+ } else {
+ return PostStatus.UNKNOWN;
+ }
+ }
+
+ public synchronized static PostStatus fromPost(Post post) {
+ String value = post.getPostStatus();
+ long dateCreatedGMT = post.getDate_created_gmt();
+ return fromStringAndDateGMT(value, dateCreatedGMT);
+ }
+
+ public synchronized static PostStatus fromPostsListPost(PostsListPost post) {
+ String value = post.getOriginalStatus();
+ long dateCreatedGMT = post.getDateCreatedGmt();
+ return fromStringAndDateGMT(value, dateCreatedGMT);
+ }
+
+ public static String toString(PostStatus status) {
+ switch (status) {
+ case PUBLISHED:
+ return "publish";
+ case DRAFT:
+ return "draft";
+ case PRIVATE:
+ return "private";
+ case PENDING:
+ return "pending";
+ case TRASHED:
+ return "trash";
+ case SCHEDULED:
+ return "future";
+ default:
+ return "";
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java b/WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java
new file mode 100644
index 000000000..b9ddd7e26
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java
@@ -0,0 +1,183 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.posts.services.PostUploadService;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.text.BreakIterator;
+import java.util.Date;
+
+/**
+ * Barebones post/page as listed in PostsListFragment
+ */
+public class PostsListPost {
+ private static final int MAX_EXCERPT_LEN = 150;
+
+ private final long postId;
+ private final long blogId;
+ private long dateCreatedGmt;
+ private final long featuredImageId;
+
+ private final String title;
+ private String excerpt;
+ private final String description;
+ private final String status;
+
+ private final boolean isLocalDraft;
+ private final boolean hasLocalChanges;
+ private final boolean isUploading;
+
+ // featuredImageUrl is generated by the adapter on the fly
+ private transient String featuredImageUrl;
+
+ public PostsListPost(Post post) {
+ postId = post.getLocalTablePostId();
+ blogId = post.getLocalTableBlogId();
+ featuredImageId = post.getFeaturedImageId();
+
+ title = post.getTitle();
+ description = post.getDescription();
+ excerpt = post.getPostExcerpt();
+
+ status = post.getPostStatus();
+ isLocalDraft = post.isLocalDraft();
+ hasLocalChanges = post.isLocalChange();
+ isUploading = PostUploadService.isPostUploading(postId);
+
+ setDateCreatedGmt(post.getDate_created_gmt());
+
+ // if the post doesn't have an excerpt, generate one from the description
+ if (!hasExcerpt() && hasDescription()) {
+ excerpt = makeExcerpt(description);
+ }
+ }
+
+ public long getPostId() {
+ return postId;
+ }
+
+ public long getBlogId() {
+ return blogId;
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+ public boolean hasTitle() {
+ return !TextUtils.isEmpty(title);
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(description);
+ }
+ public boolean hasDescription() {
+ return !TextUtils.isEmpty(description);
+ }
+
+ public String getExcerpt() {
+ return StringUtils.notNullStr(excerpt);
+ }
+ public boolean hasExcerpt() {
+ return !TextUtils.isEmpty(excerpt);
+ }
+
+ /*
+ * java's string.trim() doesn't handle non-breaking space chars (#160), which may appear at the
+ * end of post content - work around this by converting them to standard spaces before trimming
+ */
+ private static final String NBSP = String.valueOf((char) 160);
+ private static String trimEx(final String s) {
+ return s.replace(NBSP, " ").trim();
+ }
+
+ private static String makeExcerpt(String description) {
+ if (TextUtils.isEmpty(description)) {
+ return null;
+ }
+
+ String s = HtmlUtils.fastStripHtml(description);
+ if (s.length() < MAX_EXCERPT_LEN) {
+ return trimEx(s);
+ }
+
+ StringBuilder result = new StringBuilder();
+ BreakIterator wordIterator = BreakIterator.getWordInstance();
+ wordIterator.setText(s);
+ int start = wordIterator.first();
+ int end = wordIterator.next();
+ int totalLen = 0;
+ while (end != BreakIterator.DONE) {
+ String word = s.substring(start, end);
+ result.append(word);
+ totalLen += word.length();
+ if (totalLen >= MAX_EXCERPT_LEN) {
+ break;
+ }
+ start = end;
+ end = wordIterator.next();
+ }
+
+ if (totalLen == 0) {
+ return null;
+ }
+ return trimEx(result.toString()) + "...";
+ }
+
+ public long getFeaturedImageId() {
+ return featuredImageId;
+ }
+ public boolean hasFeaturedImageId() {
+ return featuredImageId != 0;
+ }
+
+ public String getFeaturedImageUrl() {
+ return StringUtils.notNullStr(featuredImageUrl);
+ }
+ public void setFeaturedImageUrl(String url) {
+ this.featuredImageUrl = StringUtils.notNullStr(url);
+ }
+ public boolean hasFeaturedImageUrl() {
+ return !TextUtils.isEmpty(featuredImageUrl);
+ }
+
+ public long getDateCreatedGmt() {
+ return dateCreatedGmt;
+ }
+ private void setDateCreatedGmt(long dateCreatedGmt) {
+ this.dateCreatedGmt = dateCreatedGmt;
+ }
+
+ public String getOriginalStatus() {
+ return StringUtils.notNullStr(status);
+ }
+
+ public PostStatus getStatusEnum() {
+ return PostStatus.fromPostsListPost(this);
+ }
+
+ public String getFormattedDate() {
+ if (getStatusEnum() == PostStatus.SCHEDULED) {
+ return DateUtils.formatDateTime(WordPress.getContext(), dateCreatedGmt, DateUtils.FORMAT_ABBREV_ALL);
+ } else {
+ return DateTimeUtils.javaDateToTimeSpan(new Date(dateCreatedGmt), WordPress.getContext());
+ }
+ }
+
+ public boolean isLocalDraft() {
+ return isLocalDraft;
+ }
+
+ public boolean hasLocalChanges() {
+ return hasLocalChanges;
+ }
+
+ public boolean isUploading() {
+ return isUploading;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java b/WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java
new file mode 100644
index 000000000..4e5a20b32
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.models;
+
+import java.util.ArrayList;
+
+public class PostsListPostList extends ArrayList<PostsListPost> {
+
+ public boolean isSameList(PostsListPostList newPostsList) {
+ if (newPostsList == null || this.size() != newPostsList.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < newPostsList.size(); i++) {
+ PostsListPost newPost = newPostsList.get(i);
+ PostsListPost currentPost = this.get(i);
+
+ if (newPost.getPostId() != currentPost.getPostId())
+ return false;
+ if (!newPost.getTitle().equals(currentPost.getTitle()))
+ return false;
+ if (newPost.getDateCreatedGmt() != currentPost.getDateCreatedGmt())
+ return false;
+ if (!newPost.getOriginalStatus().equals(currentPost.getOriginalStatus()))
+ return false;
+ if (newPost.isUploading() != currentPost.isUploading())
+ return false;
+ if (newPost.isLocalDraft() != currentPost.isLocalDraft())
+ return false;
+ if (newPost.hasLocalChanges() != currentPost.hasLocalChanges())
+ return false;
+ if (!newPost.getDescription().equals(currentPost.getDescription()))
+ return false;
+ }
+
+ return true;
+ }
+
+ public int indexOfPost(PostsListPost post) {
+ if (post == null) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).getPostId() == post.getPostId() && this.get(i).getBlogId() == post.getBlogId()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public int indexOfFeaturedMediaId(long mediaId) {
+ if (mediaId == 0) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).getFeaturedImageId() == mediaId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java
new file mode 100644
index 000000000..e5c71a028
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java
@@ -0,0 +1,169 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+public class ReaderBlog {
+ public long blogId;
+ public long feedId;
+
+ public boolean isPrivate;
+ public boolean isJetpack;
+ public boolean isFollowing;
+ public int numSubscribers;
+
+ private String name;
+ private String description;
+ private String url;
+ private String imageUrl;
+ private String feedUrl;
+
+ public static ReaderBlog fromJson(JSONObject json) {
+ ReaderBlog blog = new ReaderBlog();
+ if (json == null) {
+ return blog;
+ }
+
+ // if meta/data/site exists then JSON is for a read/following/mine?meta=site subscription,
+ // if meta/data/feed exists then JSON is for a read/following/mine?meta=feed subscription,
+ // otherwise JSON the response for a single site/$siteId or read/feed/$feedId
+ JSONObject jsonSite = JSONUtils.getJSONChild(json, "meta/data/site");
+ JSONObject jsonFeed = JSONUtils.getJSONChild(json, "meta/data/feed");
+ if (jsonSite != null) {
+ blog.blogId = jsonSite.optLong("ID");
+ blog.setName(JSONUtils.getStringDecoded(jsonSite, "name"));
+ blog.setDescription(JSONUtils.getStringDecoded(jsonSite, "description"));
+ blog.setUrl(JSONUtils.getString(jsonSite, "URL"));
+ blog.isJetpack = JSONUtils.getBool(jsonSite, "jetpack");
+ blog.isPrivate = JSONUtils.getBool(jsonSite, "is_private");
+ blog.isFollowing = JSONUtils.getBool(jsonSite, "is_following");
+ blog.numSubscribers = jsonSite.optInt("subscribers_count");
+ JSONObject jsonIcon = jsonSite.optJSONObject("icon");
+ if (jsonIcon != null) {
+ blog.setImageUrl(JSONUtils.getString(jsonIcon, "img"));
+ }
+ } else if (jsonFeed != null) {
+ blog.feedId = jsonFeed.optLong("feed_ID");
+ blog.setFeedUrl(JSONUtils.getString(jsonFeed, "feed_URL"));
+ blog.setName(JSONUtils.getStringDecoded(jsonFeed, "name"));
+ blog.setUrl(JSONUtils.getString(jsonFeed, "URL"));
+ blog.numSubscribers = jsonFeed.optInt("subscribers_count");
+ // read/following/mine doesn't include is_following for feeds, so assume to be true
+ blog.isFollowing = true;
+ } else {
+ blog.blogId = json.optLong("ID");
+ blog.feedId = json.optLong("feed_ID");
+ blog.setName(JSONUtils.getStringDecoded(json, "name"));
+ blog.setDescription(JSONUtils.getStringDecoded(json, "description"));
+ blog.setUrl(JSONUtils.getString(json, "URL"));
+ blog.setFeedUrl(JSONUtils.getString(json, "feed_URL"));
+ blog.isJetpack = JSONUtils.getBool(json, "jetpack");
+ blog.isPrivate = JSONUtils.getBool(json, "is_private");
+ blog.isFollowing = JSONUtils.getBool(json, "is_following");
+ blog.numSubscribers = json.optInt("subscribers_count");
+ }
+
+ // blogId will be empty for feeds, so set it to the feedId (consistent with /read/ endpoints)
+ if (blog.blogId == 0 && blog.feedId != 0) {
+ blog.blogId = blog.feedId;
+ }
+
+ JSONObject jsonIcon = JSONUtils.getJSONChild(json, "icon");
+ if (jsonIcon != null) {
+ blog.setImageUrl(JSONUtils.getString(jsonIcon, "img"));
+ if (!blog.hasImageUrl()) {
+ blog.setImageUrl(JSONUtils.getString(jsonIcon, "ico"));
+ }
+ }
+
+ return blog;
+ }
+
+ public String getName() {
+ return StringUtils.notNullStr(name);
+ }
+ public void setName(String blogName) {
+ this.name = StringUtils.notNullStr(blogName).trim();
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(description);
+ }
+ public void setDescription(String description) {
+ this.description = StringUtils.notNullStr(description).trim();
+ }
+
+ public String getImageUrl() {
+ return StringUtils.notNullStr(imageUrl);
+ }
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = StringUtils.notNullStr(imageUrl);
+ }
+
+ public String getUrl() {
+ return StringUtils.notNullStr(url);
+ }
+ public void setUrl(String url) {
+ this.url = StringUtils.notNullStr(url);
+ }
+
+ public String getFeedUrl() {
+ return StringUtils.notNullStr(feedUrl);
+ }
+ public void setFeedUrl(String feedUrl) {
+ this.feedUrl = StringUtils.notNullStr(feedUrl);
+ }
+
+ public boolean hasUrl() {
+ return !TextUtils.isEmpty(url);
+ }
+
+ public boolean hasFeedUrl() {
+ return !TextUtils.isEmpty(feedUrl);
+ }
+
+ public boolean hasImageUrl() {
+ return !TextUtils.isEmpty(imageUrl);
+ }
+ public boolean hasName() {
+ return !TextUtils.isEmpty(name);
+ }
+ public boolean hasDescription() {
+ return !TextUtils.isEmpty(description);
+ }
+
+ public boolean isExternal() {
+ return (feedId != 0);
+ }
+
+ /*
+ * returns the mshot url to use for this blog, ex:
+ * http://s.wordpress.com/mshots/v1/http%3A%2F%2Fnickbradbury.com?w=600
+ * note that while mshots support a "h=" parameter, this crops rather than
+ * scales the image to that height
+ * https://github.com/Automattic/mShots
+ */
+ public String getMshotsUrl(int width) {
+ return "http://s.wordpress.com/mshots/v1/"
+ + UrlUtils.urlEncode(getUrl())
+ + String.format("?w=%d", width);
+ }
+
+ public boolean isSameAs(ReaderBlog blogInfo) {
+ return blogInfo != null
+ && this.blogId == blogInfo.blogId
+ && this.feedId == blogInfo.feedId
+ && this.isFollowing == blogInfo.isFollowing
+ && this.isPrivate == blogInfo.isPrivate
+ && this.numSubscribers == blogInfo.numSubscribers
+ && this.getName().equals(blogInfo.getName())
+ && this.getDescription().equals(blogInfo.getDescription())
+ && this.getUrl().equals(blogInfo.getUrl())
+ && this.getFeedUrl().equals(blogInfo.getFeedUrl())
+ && this.getImageUrl().equals(blogInfo.getImageUrl());
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java
new file mode 100644
index 000000000..4351940a7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.NonNull;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ReaderBlogList extends ArrayList<ReaderBlog> {
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ public static ReaderBlogList fromJson(JSONObject json) {
+ ReaderBlogList blogs = new ReaderBlogList();
+ if (json == null) {
+ return blogs;
+ }
+
+ // read/following/mine response
+ JSONArray jsonBlogs = json.optJSONArray("subscriptions");
+ if (jsonBlogs != null) {
+ for (int i = 0; i < jsonBlogs.length(); i++) {
+ ReaderBlog blog = ReaderBlog.fromJson(jsonBlogs.optJSONObject(i));
+ // make sure blog is valid before adding to the list - this can happen if user
+ // added a URL that isn't a feed or a blog since as of 29-May-2014 the API
+ // will let you follow any URL regardless if it's valid
+ if (blog.hasName() || blog.hasDescription() || blog.hasUrl()) {
+ blogs.add(blog);
+ }
+ }
+ }
+
+ return blogs;
+ }
+
+ private int indexOfBlogId(long blogId) {
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).blogId == blogId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public boolean isSameList(ReaderBlogList blogs) {
+ if (blogs == null || blogs.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderBlog blogInfo: blogs) {
+ int index = indexOfBlogId(blogInfo.blogId);
+ if (index == -1) {
+ return false;
+ }
+ ReaderBlog thisInfo = this.get(index);
+ if (!thisInfo.isSameAs(blogInfo)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * returns true if the passed blog list has the same blogs that are in this list - differs
+ * from isSameList() in that isSameList() checks for *any* changes (subscription count, etc.)
+ * whereas this only checks if the passed list has any blogs that are not in this list, or
+ * this list has any blogs that are not in the passed list
+ */
+ public boolean hasSameBlogs(@NonNull ReaderBlogList blogs) {
+ if (blogs.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderBlog blogInfo: blogs) {
+ if (indexOfBlogId(blogInfo.blogId) == -1) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java
new file mode 100644
index 000000000..f0d92cf08
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class ReaderComment {
+ public long commentId;
+ public long blogId;
+ public long postId;
+ public long parentId;
+
+ private String authorName;
+ private String authorAvatar;
+ private String authorUrl;
+ private String status;
+ private String text;
+
+ private String published;
+ public long timestamp;
+
+ public long authorId;
+ public long authorBlogId;
+
+ public int numLikes;
+ public boolean isLikedByCurrentUser;
+
+ public int pageNumber;
+
+ // not stored in db - denotes the indentation level when displaying this comment
+ public transient int level = 0;
+
+ public static ReaderComment fromJson(JSONObject json, long blogId) {
+ if (json == null) {
+ throw new IllegalArgumentException("null json comment");
+ }
+
+ ReaderComment comment = new ReaderComment();
+
+ comment.blogId = blogId;
+ comment.commentId = json.optLong("ID");
+ comment.status = JSONUtils.getString(json, "status");
+
+ // note that content may contain html, adapter needs to handle it
+ comment.text = HtmlUtils.stripScript(JSONUtils.getString(json, "content"));
+
+ comment.published = JSONUtils.getString(json, "date");
+ comment.timestamp = DateTimeUtils.timestampFromIso8601(comment.published);
+
+ JSONObject jsonPost = json.optJSONObject("post");
+ if (jsonPost != null) {
+ comment.postId = jsonPost.optLong("ID");
+ }
+
+ JSONObject jsonAuthor = json.optJSONObject("author");
+ if (jsonAuthor!=null) {
+ // author names may contain html entities (esp. pingbacks)
+ comment.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
+ comment.authorAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL");
+ comment.authorUrl = JSONUtils.getString(jsonAuthor, "URL");
+ comment.authorId = jsonAuthor.optLong("ID");
+ comment.authorBlogId = jsonAuthor.optLong("site_ID");
+ }
+
+ JSONObject jsonParent = json.optJSONObject("parent");
+ if (jsonParent != null) {
+ comment.parentId = jsonParent.optLong("ID");
+ }
+
+ // like info is found under meta/data/likes when meta=likes query param is used
+ JSONObject jsonLikes = JSONUtils.getJSONChild(json, "meta/data/likes");
+ if (jsonLikes != null) {
+ comment.numLikes = jsonLikes.optInt("found");
+ comment.isLikedByCurrentUser = JSONUtils.getBool(jsonLikes, "i_like");
+ }
+
+ return comment;
+ }
+
+ public String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+
+ public void setAuthorName(String authorName) {
+ this.authorName = StringUtils.notNullStr(authorName);
+ }
+
+ public String getAuthorAvatar() {
+ return StringUtils.notNullStr(authorAvatar);
+ }
+ public void setAuthorAvatar(String authorAvatar) {
+ this.authorAvatar = StringUtils.notNullStr(authorAvatar);
+ }
+
+ public String getAuthorUrl() {
+ return StringUtils.notNullStr(authorUrl);
+ }
+ public void setAuthorUrl(String authorUrl) {
+ this.authorUrl = StringUtils.notNullStr(authorUrl);
+ }
+
+ public String getText() {
+ return StringUtils.notNullStr(text);
+ }
+ public void setText(String text) {
+ this.text = StringUtils.notNullStr(text);
+ }
+
+ public String getStatus() {
+ return StringUtils.notNullStr(status);
+ }
+ public void setStatus(String status) {
+ this.status = StringUtils.notNullStr(status);
+ }
+
+ public String getPublished() {
+ return StringUtils.notNullStr(published);
+ }
+ public void setPublished(String published) {
+ this.published = StringUtils.notNullStr(published);
+ }
+
+ public boolean hasAuthorUrl() {
+ return !TextUtils.isEmpty(authorUrl);
+ }
+
+ public boolean hasAuthorBlogId() {
+ return (authorBlogId != 0);
+ }
+
+ public boolean hasAuthorAvatar() {
+ return !TextUtils.isEmpty(authorAvatar);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java
new file mode 100644
index 000000000..7a329d689
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java
@@ -0,0 +1,115 @@
+package org.wordpress.android.models;
+
+import java.util.ArrayList;
+
+public class ReaderCommentList extends ArrayList<ReaderComment> {
+
+ private boolean commentIdExists(long commentId) {
+ return (indexOfCommentId(commentId) > -1);
+ }
+
+ public int indexOfCommentId(long commentId) {
+ for (int i=0; i < this.size(); i++) {
+ if (commentId==this.get(i).commentId)
+ return i;
+ }
+ return -1;
+ }
+
+ /*
+ * does passed list contain the same comments as this list?
+ */
+ public boolean isSameList(ReaderCommentList comments) {
+ if (comments==null || comments.size()!=this.size())
+ return false;
+
+ for (ReaderComment comment: comments) {
+ if (!commentIdExists(comment.commentId))
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean replaceComment(long commentId, ReaderComment newComment) {
+ if (newComment == null) {
+ return false;
+ }
+
+ int index = indexOfCommentId(commentId);
+ if (index == -1) {
+ return false;
+ }
+
+ // make sure the new comment has the same level as the old one
+ newComment.level = this.get(index).level;
+
+ this.set(index, newComment);
+ return true;
+ }
+
+ /*
+ * builds a new list from the passed one with child comments placed under their parents and indent levels applied
+ */
+ public static ReaderCommentList getLevelList(ReaderCommentList thisList) {
+ if (thisList==null)
+ return new ReaderCommentList();
+
+ // first check if there are any child comments - if not, just return the passed list
+ boolean hasChildComments = false;
+ for (ReaderComment comment: thisList) {
+ if (comment.parentId!=0) {
+ hasChildComments = true;
+ break;
+ }
+ }
+ if (!hasChildComments)
+ return thisList;
+
+ ReaderCommentList result = new ReaderCommentList();
+
+ // reset all levels, and add root comments to result
+ for (ReaderComment comment: thisList) {
+ comment.level = 0;
+ if (comment.parentId==0)
+ result.add(comment);
+ }
+
+ // add child comments under their parents
+ boolean done;
+ do {
+ done = true;
+ for (ReaderComment comment: thisList) {
+ // only look at comments that have a parentId but no level assigned yet
+ if (comment.parentId!=0 && comment.level==0) {
+ int parentIndex = result.indexOfCommentId(comment.parentId);
+ if (parentIndex > -1) {
+ comment.level = result.get(parentIndex).level + 1;
+
+ // insert this comment after the last comment of this level that has this parent
+ int commentIndex=parentIndex+1;
+ while (commentIndex < result.size()) {
+ if (result.get(commentIndex).level!=comment.level || result.get(commentIndex).parentId!=comment.parentId)
+ break;
+ commentIndex++;
+ }
+ result.add(commentIndex, comment);
+
+
+ done = false;
+ }
+ }
+ }
+ } while (!done);
+
+ // handle orphans (child comments whose parents weren't found above)
+ for (ReaderComment comment: thisList) {
+ if (comment.level==0 && comment.parentId!=0) {
+ comment.level = 1; // give it a non-zero level so it's indented by ReaderCommentAdapter
+ result.add(comment);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
new file mode 100644
index 000000000..ee96aaa5f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
@@ -0,0 +1,718 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.ui.reader.utils.ImageSizeMap;
+import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.text.BreakIterator;
+import java.util.Iterator;
+
+public class ReaderPost {
+ private String pseudoId;
+ public long postId;
+ public long blogId;
+ public long feedId;
+ public long feedItemId;
+ public long authorId;
+
+ private String title;
+ private String text;
+ private String excerpt;
+ private String authorName;
+ private String authorFirstName;
+ private String blogName;
+ private String blogUrl;
+ private String postAvatar;
+
+ private String primaryTag; // most popular tag on this post based on usage in blog
+ private String secondaryTag; // second most popular tag on this post based on usage in blog
+
+ /*
+ * the "date" field is a generic date which depends on the stream the post is from:
+ * - for tagged posts, this is the date the post was tagged
+ * - for liked posts, this is the date the post was liked
+ * - for other posts, this is the date the post was published
+ * this date is used when requesting older posts from the backend, and is also used
+ * to generate the sortIndex below (which determines how posts are sorted for display)
+ */
+ private String date;
+ private String pubDate;
+ public double sortIndex;
+
+ private String url;
+ private String shortUrl;
+ private String featuredImage;
+ private String featuredVideo;
+
+ public int numReplies; // includes comments, trackbacks & pingbacks
+ public int numLikes;
+
+ public boolean isLikedByCurrentUser;
+ public boolean isFollowedByCurrentUser;
+ public boolean isCommentsOpen;
+ public boolean isExternal;
+ public boolean isPrivate;
+ public boolean isVideoPress;
+ public boolean isJetpack;
+
+ private String attachmentsJson;
+ private String discoverJson;
+ private String format;
+
+ public long xpostPostId;
+ public long xpostBlogId;
+
+ private String railcarJson;
+
+ public static ReaderPost fromJson(JSONObject json) {
+ if (json == null) {
+ throw new IllegalArgumentException("null json post");
+ }
+
+ ReaderPost post = new ReaderPost();
+
+ post.postId = json.optLong("ID");
+ post.blogId = json.optLong("site_ID");
+ post.feedId = json.optLong("feed_ID");
+ post.feedItemId = json.optLong("feed_item_ID");
+
+ if (json.has("pseudo_ID")) {
+ post.pseudoId = JSONUtils.getString(json, "pseudo_ID"); // read/ endpoint
+ } else {
+ post.pseudoId = JSONUtils.getString(json, "global_ID"); // sites/ endpoint
+ }
+
+ // remove HTML from the excerpt
+ post.excerpt = HtmlUtils.fastStripHtml(JSONUtils.getString(json, "excerpt")).trim();
+
+ post.text = JSONUtils.getString(json, "content");
+ post.title = JSONUtils.getStringDecoded(json, "title");
+ post.format = JSONUtils.getString(json, "format");
+ post.url = JSONUtils.getString(json, "URL");
+ post.shortUrl = JSONUtils.getString(json, "short_URL");
+ post.setBlogUrl(JSONUtils.getString(json, "site_URL"));
+
+ post.numLikes = json.optInt("like_count");
+ post.isLikedByCurrentUser = JSONUtils.getBool(json, "i_like");
+ post.isFollowedByCurrentUser = JSONUtils.getBool(json, "is_following");
+ post.isExternal = JSONUtils.getBool(json, "is_external");
+ post.isPrivate = JSONUtils.getBool(json, "site_is_private");
+ post.isJetpack = JSONUtils.getBool(json, "is_jetpack");
+
+ JSONObject jsonDiscussion = json.optJSONObject("discussion");
+ if (jsonDiscussion != null) {
+ post.isCommentsOpen = JSONUtils.getBool(jsonDiscussion, "comments_open");
+ post.numReplies = jsonDiscussion.optInt("comment_count");
+ } else {
+ post.isCommentsOpen = JSONUtils.getBool(json, "comments_open");
+ post.numReplies = json.optInt("comment_count");
+ }
+
+ // parse the author section
+ assignAuthorFromJson(post, json.optJSONObject("author"));
+
+ post.featuredImage = JSONUtils.getString(json, "featured_image");
+ post.blogName = JSONUtils.getStringDecoded(json, "site_name");
+ post.pubDate = JSONUtils.getString(json, "date");
+
+ // a post's date is the liked date for liked posts, tagged date for tag streams, and
+ // published date for all others
+ if (json.has("date_liked")) {
+ post.date = JSONUtils.getString(json, "date_liked");
+ } else if (json.has("tagged_on")) {
+ post.date = JSONUtils.getString(json, "tagged_on");
+ } else {
+ post.date = post.pubDate;
+ }
+
+ // sort index determines how posts are sorted, which is based on the date retrieved above
+ post.sortIndex = DateTimeUtils.timestampFromIso8601(post.date);
+
+ // if the post is untitled, make up a title from the excerpt
+ if (!post.hasTitle() && post.hasExcerpt()) {
+ post.title = extractTitle(post.excerpt, 50);
+ }
+
+ // remove html from title (rare, but does happen)
+ if (post.hasTitle() && post.title.contains("<") && post.title.contains(">")) {
+ post.title = HtmlUtils.stripHtml(post.title);
+ }
+
+ // parse the tags section
+ assignTagsFromJson(post, json.optJSONObject("tags"));
+
+ // parse the attachments
+ JSONObject jsonAttachments = json.optJSONObject("attachments");
+ if (jsonAttachments != null && jsonAttachments.length() > 0) {
+ post.attachmentsJson = jsonAttachments.toString();
+ }
+
+ // site metadata - returned when ?meta=site was added to the request
+ JSONObject jsonSite = JSONUtils.getJSONChild(json, "meta/data/site");
+ if (jsonSite != null) {
+ post.blogId = jsonSite.optInt("ID");
+ post.blogName = JSONUtils.getString(jsonSite, "name");
+ post.setBlogUrl(JSONUtils.getString(jsonSite, "URL"));
+ post.isPrivate = JSONUtils.getBool(jsonSite, "is_private");
+ // TODO: as of 29-Sept-2014, this is broken - endpoint returns false when it should be true
+ post.isJetpack = JSONUtils.getBool(jsonSite, "jetpack");
+ }
+
+ // "discover" posts
+ JSONObject jsonDiscover = json.optJSONObject("discover_metadata");
+ if (jsonDiscover != null) {
+ post.setDiscoverJson(jsonDiscover.toString());
+ }
+
+ // xpost info
+ assignXpostIdsFromJson(post, json.optJSONArray("metadata"));
+
+ // if there's no featured image, check if featured media has been set - this is sometimes
+ // a YouTube or Vimeo video, in which case store it as the featured video so we can treat
+ // it as a video
+ if (!post.hasFeaturedImage()) {
+ JSONObject jsonMedia = json.optJSONObject("featured_media");
+ if (jsonMedia != null && jsonMedia.length() > 0) {
+ String mediaUrl = JSONUtils.getString(jsonMedia, "uri");
+ if (!TextUtils.isEmpty(mediaUrl)) {
+ String type = JSONUtils.getString(jsonMedia, "type");
+ boolean isVideo = (type != null && type.equals("video"));
+ if (isVideo) {
+ post.featuredVideo = mediaUrl;
+ } else {
+ post.featuredImage = mediaUrl;
+ }
+ }
+ }
+ }
+ // if the post still doesn't have a featured image but we have attachment data, check whether
+ // we can find a suitable featured image from the attachments
+ if (!post.hasFeaturedImage() && post.hasAttachments()) {
+ post.featuredImage = new ImageSizeMap(post.attachmentsJson)
+ .getLargestImageUrl(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH);
+ }
+ // if we *still* don't have a featured image but the text contains an IMG tag, check whether
+ // we can find a suitable image from the text
+ if (!post.hasFeaturedImage() && post.hasText() && post.text.contains("<img")) {
+ post.featuredImage = new ReaderImageScanner(post.text, post.isPrivate)
+ .getLargestImage(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH);
+ }
+
+ // "railcar" data - currently used in search streams, used by TrainTracks
+ JSONObject jsonRailcar = json.optJSONObject("railcar");
+ if (jsonRailcar != null) {
+ post.setRailcarJson(jsonRailcar.toString());
+ }
+
+ return post;
+ }
+
+ /*
+ * assigns cross post blog & post IDs from post's metadata section
+ * "metadata": [
+ * {
+ * "id": "21192",
+ * "key": "xpost_origin",
+ * "value": "11326809:18427"
+ * }
+ * ],
+ */
+ private static void assignXpostIdsFromJson(ReaderPost post, JSONArray jsonMetadata) {
+ if (jsonMetadata == null) return;
+
+ for (int i = 0; i < jsonMetadata.length(); i++) {
+ JSONObject jsonMetaItem = jsonMetadata.optJSONObject(i);
+ String metaKey = jsonMetaItem.optString("key");
+ if (!TextUtils.isEmpty(metaKey) && metaKey.equals("xpost_origin")) {
+ String value = jsonMetaItem.optString("value");
+ if (!TextUtils.isEmpty(value) && value.contains(":")) {
+ String[] valuePair = value.split(":");
+ if (valuePair.length == 2) {
+ post.xpostBlogId = StringUtils.stringToLong(valuePair[0]);
+ post.xpostPostId = StringUtils.stringToLong(valuePair[1]);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * assigns author-related info to the passed post from the passed JSON "author" object
+ */
+ private static void assignAuthorFromJson(ReaderPost post, JSONObject jsonAuthor) {
+ if (jsonAuthor == null) return;
+
+ post.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
+ post.authorFirstName = JSONUtils.getStringDecoded(jsonAuthor, "first_name");
+ post.postAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL");
+ post.authorId = jsonAuthor.optLong("ID");
+
+ // site_URL doesn't exist for /sites/ endpoints, so get it from the author
+ if (TextUtils.isEmpty(post.blogUrl)) {
+ post.setBlogUrl(JSONUtils.getString(jsonAuthor, "URL"));
+ }
+ }
+
+ /*
+ * assigns primary/secondary tags to the passed post from the passed JSON "tags" object
+ */
+ private static void assignTagsFromJson(ReaderPost post, JSONObject jsonTags) {
+ if (jsonTags == null) {
+ return;
+ }
+
+ Iterator<String> it = jsonTags.keys();
+ if (!it.hasNext()) {
+ return;
+ }
+
+ // most popular tag & second most popular tag, based on usage count on this blog
+ String mostPopularTag = null;
+ String nextMostPopularTag = null;
+ int popularCount = 0;
+
+ while (it.hasNext()) {
+ JSONObject jsonThisTag = jsonTags.optJSONObject(it.next());
+
+ // if the number of posts on this blog that use this tag is higher than previous,
+ // set this as the most popular tag, and set the second most popular tag to
+ // the current most popular tag
+ int postCount = jsonThisTag.optInt("post_count");
+ if (postCount > popularCount) {
+ nextMostPopularTag = mostPopularTag;
+ mostPopularTag = JSONUtils.getStringDecoded(jsonThisTag, "slug");
+ popularCount = postCount;
+ }
+ }
+
+ // don't set primary tag if one is already set
+ if (!post.hasPrimaryTag()) {
+ post.setPrimaryTag(mostPopularTag);
+ }
+ post.setSecondaryTag(nextMostPopularTag);
+ }
+
+ /*
+ * extracts a title from a post's excerpt - used when the post has no title
+ */
+ private static String extractTitle(final String excerpt, int maxLen) {
+ if (TextUtils.isEmpty(excerpt))
+ return null;
+
+ if (excerpt.length() < maxLen)
+ return excerpt.trim();
+
+ StringBuilder result = new StringBuilder();
+ BreakIterator wordIterator = BreakIterator.getWordInstance();
+ wordIterator.setText(excerpt);
+ int start = wordIterator.first();
+ int end = wordIterator.next();
+ int totalLen = 0;
+ while (end != BreakIterator.DONE) {
+ String word = excerpt.substring(start, end);
+ result.append(word);
+ totalLen += word.length();
+ if (totalLen >= maxLen)
+ break;
+ start = end;
+ end = wordIterator.next();
+ }
+
+ if (totalLen==0)
+ return null;
+ return result.toString().trim() + "...";
+ }
+
+ // --------------------------------------------------------------------------------------------
+
+ public String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+ public void setAuthorName(String name) {
+ this.authorName = StringUtils.notNullStr(name);
+ }
+
+ public String getAuthorFirstName() {
+ return StringUtils.notNullStr(authorFirstName);
+ }
+ public void setAuthorFirstName(String name) {
+ this.authorFirstName = StringUtils.notNullStr(name);
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+ public void setTitle(String title) {
+ this.title = StringUtils.notNullStr(title);
+ }
+
+ public String getText() {
+ return StringUtils.notNullStr(text);
+ }
+ public void setText(String text) {
+ this.text = StringUtils.notNullStr(text);
+ }
+
+ public String getExcerpt() {
+ return StringUtils.notNullStr(excerpt);
+ }
+ public void setExcerpt(String excerpt) {
+ this.excerpt = StringUtils.notNullStr(excerpt);
+ }
+
+ // https://codex.wordpress.org/Post_Formats
+ public String getFormat() {
+ return StringUtils.notNullStr(format);
+ }
+ public void setFormat(String format) {
+ this.format = StringUtils.notNullStr(format);
+ }
+
+ public boolean isGallery() {
+ return format != null && format.equals("gallery");
+ }
+
+
+ public String getUrl() {
+ return StringUtils.notNullStr(url);
+ }
+ public void setUrl(String url) {
+ this.url = StringUtils.notNullStr(url);
+ }
+
+ public String getShortUrl() {
+ return StringUtils.notNullStr(shortUrl);
+ }
+ public void setShortUrl(String url) {
+ this.shortUrl = StringUtils.notNullStr(url);
+ }
+ public boolean hasShortUrl() {
+ return !TextUtils.isEmpty(shortUrl);
+ }
+
+ public String getFeaturedImage() {
+ return StringUtils.notNullStr(featuredImage);
+ }
+ public void setFeaturedImage(String featuredImage) {
+ this.featuredImage = StringUtils.notNullStr(featuredImage);
+ }
+
+ public String getFeaturedVideo() {
+ return StringUtils.notNullStr(featuredVideo);
+ }
+ public void setFeaturedVideo(String featuredVideo) {
+ this.featuredVideo = StringUtils.notNullStr(featuredVideo);
+ }
+
+ public String getBlogName() {
+ return StringUtils.notNullStr(blogName);
+ }
+ public void setBlogName(String blogName) {
+ this.blogName = StringUtils.notNullStr(blogName);
+ }
+
+ public String getBlogUrl() {
+ return StringUtils.notNullStr(blogUrl);
+ }
+ public void setBlogUrl(String blogUrl) {
+ this.blogUrl = StringUtils.notNullStr(blogUrl);
+ }
+
+ public String getPostAvatar() {
+ return StringUtils.notNullStr(postAvatar);
+ }
+ public void setPostAvatar(String postAvatar) {
+ this.postAvatar = StringUtils.notNullStr(postAvatar);
+ }
+
+ public String getPseudoId() {
+ return StringUtils.notNullStr(pseudoId);
+ }
+ public void setPseudoId(String pseudoId) {
+ this.pseudoId = StringUtils.notNullStr(pseudoId);
+ }
+
+ public String getDate() {
+ return StringUtils.notNullStr(date);
+ }
+ public void setDate(String dateStr) {
+ this.date = StringUtils.notNullStr(dateStr);
+ }
+
+ public String getPubDate() {
+ return StringUtils.notNullStr(pubDate);
+ }
+ public void setPubDate(String published) {
+ this.pubDate = StringUtils.notNullStr(published);
+ }
+
+ public String getPrimaryTag() {
+ return StringUtils.notNullStr(primaryTag);
+ }
+ public void setPrimaryTag(String tagName) {
+ // this is a bit of a hack to avoid setting the primary tag to one of the defaults
+ if (!ReaderTag.isDefaultTagTitle(tagName)) {
+ this.primaryTag = StringUtils.notNullStr(tagName);
+ }
+ }
+ boolean hasPrimaryTag() {
+ return !TextUtils.isEmpty(primaryTag);
+ }
+
+ public String getSecondaryTag() {
+ return StringUtils.notNullStr(secondaryTag);
+ }
+ public void setSecondaryTag(String tagName) {
+ if (!ReaderTag.isDefaultTagTitle(tagName)) {
+ this.secondaryTag = StringUtils.notNullStr(tagName);
+ }
+ }
+
+ /*
+ * attachments are stored as the actual JSON to avoid having a separate table for
+ * them, may need to revisit this if/when attachments become more important
+ */
+ public String getAttachmentsJson() {
+ return StringUtils.notNullStr(attachmentsJson);
+ }
+ public void setAttachmentsJson(String json) {
+ attachmentsJson = StringUtils.notNullStr(json);
+ }
+ public boolean hasAttachments() {
+ return !TextUtils.isEmpty(attachmentsJson);
+ }
+
+ /*
+ * "discover" posts also store the actual JSON
+ */
+ public String getDiscoverJson() {
+ return StringUtils.notNullStr(discoverJson);
+ }
+ public void setDiscoverJson(String json) {
+ discoverJson = StringUtils.notNullStr(json);
+ }
+ public boolean isDiscoverPost() {
+ return !TextUtils.isEmpty(discoverJson);
+ }
+
+ private transient ReaderPostDiscoverData discoverData;
+ public ReaderPostDiscoverData getDiscoverData() {
+ if (discoverData == null && !TextUtils.isEmpty(discoverJson)) {
+ try {
+ discoverData = new ReaderPostDiscoverData(new JSONObject(discoverJson));
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ return discoverData;
+ }
+
+ public boolean hasText() {
+ return !TextUtils.isEmpty(text);
+ }
+
+ public boolean hasUrl() {
+ return !TextUtils.isEmpty(url);
+ }
+
+ public boolean hasExcerpt() {
+ return !TextUtils.isEmpty(excerpt);
+ }
+
+ public boolean hasFeaturedImage() {
+ return !TextUtils.isEmpty(featuredImage);
+ }
+
+ public boolean hasFeaturedVideo() {
+ return !TextUtils.isEmpty(featuredVideo);
+ }
+
+ public boolean hasPostAvatar() {
+ return !TextUtils.isEmpty(postAvatar);
+ }
+
+ public boolean hasBlogName() {
+ return !TextUtils.isEmpty(blogName);
+ }
+
+ public boolean hasAuthorName() {
+ return !TextUtils.isEmpty(authorName);
+ }
+
+ public boolean hasAuthorFirstName() {
+ return !TextUtils.isEmpty(authorFirstName);
+ }
+
+ public boolean hasTitle() {
+ return !TextUtils.isEmpty(title);
+ }
+
+ public boolean hasBlogUrl() {
+ return !TextUtils.isEmpty(blogUrl);
+ }
+
+ /*
+ * returns true if this post is from a WordPress blog
+ */
+ public boolean isWP() {
+ return !isExternal;
+ }
+
+ /*
+ * returns true if this is a cross-post
+ */
+ public boolean isXpost() {
+ return xpostBlogId != 0 && xpostPostId != 0;
+ }
+
+ /*
+ * returns true if the passed post appears to be the same as this one - used when posts are
+ * retrieved to determine which ones are new/changed/unchanged
+ */
+ public boolean isSamePost(ReaderPost post) {
+ return post != null
+ && post.blogId == this.blogId
+ && post.postId == this.postId
+ && post.feedId == this.feedId
+ && post.feedItemId == this.feedItemId
+ && post.numLikes == this.numLikes
+ && post.numReplies == this.numReplies
+ && post.isFollowedByCurrentUser == this.isFollowedByCurrentUser
+ && post.isLikedByCurrentUser == this.isLikedByCurrentUser
+ && post.isCommentsOpen == this.isCommentsOpen
+ && post.getTitle().equals(this.getTitle())
+ && post.getExcerpt().equals(this.getExcerpt())
+ && post.getText().equals(this.getText());
+ }
+
+ public boolean hasIds(ReaderBlogIdPostId ids) {
+ return ids != null
+ && ids.getBlogId() == this.blogId
+ && ids.getPostId() == this.postId;
+ }
+
+ /*
+ * liking is enabled for all wp.com and jp posts with the exception of discover posts
+ */
+ public boolean canLikePost() {
+ return (isWP() || isJetpack) && (!isDiscoverPost());
+ }
+
+
+ public String getRailcarJson() {
+ return StringUtils.notNullStr(railcarJson);
+ }
+ public void setRailcarJson(String jsonRailcar) {
+ this.railcarJson = StringUtils.notNullStr(jsonRailcar);
+ }
+ public boolean hasRailcar() {
+ return !TextUtils.isEmpty(railcarJson);
+ }
+
+ /****
+ * the following are transient variables - not stored in the db or returned in the json - whose
+ * sole purpose is to cache commonly-used values for the post that speeds up using them inside
+ * adapters
+ ****/
+
+ /*
+ * returns the featured image url as a photon url set to the passed width/height
+ */
+ private transient String featuredImageForDisplay;
+ public String getFeaturedImageForDisplay(int width, int height) {
+ if (featuredImageForDisplay == null) {
+ if (!hasFeaturedImage()) {
+ featuredImageForDisplay = "";
+ } else {
+ featuredImageForDisplay = ReaderUtils.getResizedImageUrl(featuredImage, width, height, isPrivate);
+ }
+ }
+ return featuredImageForDisplay;
+ }
+
+ /*
+ * returns the avatar url as a photon url set to the passed size
+ */
+ private transient String avatarForDisplay;
+ public String getPostAvatarForDisplay(int size) {
+ if (avatarForDisplay == null) {
+ if (!hasPostAvatar()) {
+ return "";
+ }
+ avatarForDisplay = GravatarUtils.fixGravatarUrl(postAvatar, size);
+ }
+ return avatarForDisplay;
+ }
+
+ /*
+ * returns the blog's blavatar url as a photon url set to the passed size
+ */
+ private transient String blavatarForDisplay;
+ public String getPostBlavatarForDisplay(int size) {
+ if (blavatarForDisplay == null) {
+ if (!hasBlogUrl()) {
+ return "";
+ }
+ blavatarForDisplay = GravatarUtils.blavatarFromUrl(getBlogUrl(), size);
+ }
+ return blavatarForDisplay;
+ }
+
+ /*
+ * converts iso8601 pubDate to a java date for display - this is the date that appears on posts
+ */
+ private transient java.util.Date dtDisplay;
+ public java.util.Date getDisplayDate() {
+ if (dtDisplay == null) {
+ dtDisplay = DateTimeUtils.dateFromIso8601(this.pubDate);
+ }
+ return dtDisplay;
+ }
+
+ /*
+ * determine which tag to display for this post
+ * - no tag if this is a private blog or there is no primary tag for this post
+ * - primary tag, unless it's the same as the currently selected tag
+ * - secondary tag if primary tag is the same as the currently selected tag
+ */
+ private transient String tagForDisplay;
+ public String getTagForDisplay(final String currentTagName) {
+ if (tagForDisplay == null) {
+ if (!isPrivate && hasPrimaryTag()) {
+ if (getPrimaryTag().equalsIgnoreCase(currentTagName)) {
+ tagForDisplay = getSecondaryTag();
+ } else {
+ tagForDisplay = getPrimaryTag();
+ }
+ } else {
+ tagForDisplay = "";
+ }
+ }
+ return tagForDisplay;
+ }
+
+ /*
+ * used when a unique numeric id is required by an adapter (when hasStableIds() = true)
+ */
+ private transient long stableId;
+ public long getStableId() {
+ if (stableId == 0) {
+ stableId = (pseudoId != null ? pseudoId.hashCode() : 0);
+ }
+ return stableId;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java
new file mode 100644
index 000000000..b525708a7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java
@@ -0,0 +1,187 @@
+package org.wordpress.android.models;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+/**
+ * additional data for "discover" posts in the reader - these are posts chosen by
+ * Editorial which highlight other posts or sites - the reader shows an attribution
+ * line for these posts, and when tapped they open the original post - the like
+ * and comment counts come from the original post
+ */
+public class ReaderPostDiscoverData {
+
+ public enum DiscoverType {
+ EDITOR_PICK,
+ SITE_PICK,
+ OTHER
+ }
+
+ private String authorName;
+ private String authorUrl;
+ private String blogName;
+ private String blogUrl;
+ private String avatarUrl;
+ private final String permaLink;
+
+ private long blogId;
+ private long postId;
+
+ private int numLikes;
+ private int numComments;
+
+ private DiscoverType discoverType = DiscoverType.OTHER;
+
+ /*
+ * passed JSONObject is the "discover_metadata" section of a reader post
+ */
+ public ReaderPostDiscoverData(@NonNull JSONObject json) {
+ permaLink = json.optString("permalink");
+
+ JSONObject jsonAttribution = json.optJSONObject("attribution");
+ if (jsonAttribution != null) {
+ authorName = jsonAttribution.optString("author_name");
+ authorUrl = jsonAttribution.optString("author_url");
+ blogName = jsonAttribution.optString("blog_name");
+ blogUrl = jsonAttribution.optString("blog_url");
+ avatarUrl = jsonAttribution.optString("avatar_url");
+ }
+
+ JSONObject jsonWpcomData = json.optJSONObject("featured_post_wpcom_data");
+ if (jsonWpcomData != null) {
+ blogId = jsonWpcomData.optLong("blog_id");
+ postId = jsonWpcomData.optLong("post_id");
+ numLikes = jsonWpcomData.optInt("like_count");
+ numComments = jsonWpcomData.optInt("comment_count");
+ }
+
+ // walk the post formats array until we find one we know we should handle differently
+ // - image-pick, quote-pick, and standard-pick all display as editors picks
+ // - site-pick displays as a site pick
+ // - collection + feature can be ignored because those display the same as normal posts
+ JSONArray jsonPostFormats = json.optJSONArray("discover_fp_post_formats");
+ if (jsonPostFormats != null) {
+ for (int i = 0; i < jsonPostFormats.length(); i++) {
+ String slug = JSONUtils.getString(jsonPostFormats.optJSONObject(i), "slug");
+ if (slug.equals("site-pick")) {
+ discoverType = DiscoverType.SITE_PICK;
+ break;
+ } else if (slug.equals("standard-pick") || slug.equals("image-pick") || slug.equals("quote-pick")) {
+ discoverType = DiscoverType.EDITOR_PICK;
+ break;
+ }
+ }
+ }
+ }
+
+ public long getBlogId() {
+ return blogId;
+ }
+
+ public long getPostId() {
+ return postId;
+ }
+
+ private String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+
+ private String getAuthorUrl() {
+ return StringUtils.notNullStr(authorUrl);
+ }
+
+ public String getBlogName() {
+ return StringUtils.notNullStr(blogName);
+ }
+
+ public String getBlogUrl() {
+ return StringUtils.notNullStr(blogUrl);
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(avatarUrl);
+ }
+
+ public String getPermaLink() {
+ return StringUtils.notNullStr(permaLink);
+ }
+
+ public boolean hasBlogUrl() {
+ return !TextUtils.isEmpty(blogUrl);
+ }
+
+ public boolean hasBlogName() {
+ return !TextUtils.isEmpty(blogName);
+ }
+
+ private boolean hasAuthorName() {
+ return !TextUtils.isEmpty(authorName);
+ }
+
+ public boolean hasPermalink() {
+ return !TextUtils.isEmpty(permaLink);
+ }
+
+ public boolean hasAvatarUrl() {
+ return !TextUtils.isEmpty(avatarUrl);
+ }
+
+ public DiscoverType getDiscoverType() {
+ return discoverType;
+ }
+
+ /*
+ * returns the spanned html for the attribution line
+ */
+ private transient Spanned attributionHtml;
+ public Spanned getAttributionHtml() {
+ if (attributionHtml == null) {
+ String html;
+ String author = "<strong>" + getAuthorName() + "</strong>";
+ String blog = "<strong>" + getBlogName() + "</strong>";
+ Context context = WordPress.getContext();
+
+ switch (getDiscoverType()) {
+ case EDITOR_PICK:
+ if (hasBlogName() && hasAuthorName()) {
+ // "Originally posted by [AuthorName] on [BlogName]"
+ html = String.format(context.getString(R.string.reader_discover_attribution_author_and_blog), author, blog);
+ } else if (hasBlogName()) {
+ // "Originally posted on [BlogName]"
+ html = String.format(context.getString(R.string.reader_discover_attribution_blog), blog);
+ } else if (hasAuthorName()) {
+ // "Originally posted by [AuthorName]"
+ html = String.format(context.getString(R.string.reader_discover_attribution_author), author);
+ } else {
+ return null;
+ }
+ break;
+
+ case SITE_PICK:
+ if (blogId != 0 && hasBlogName()) {
+ // "Visit [BlogName]" - opens blog preview when tapped
+ html = String.format(context.getString(R.string.reader_discover_visit_blog), blog);
+ } else {
+ return null;
+ }
+ break;
+
+ default:
+ return null;
+ }
+
+ attributionHtml = Html.fromHtml(html);
+ }
+ return attributionHtml;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java
new file mode 100644
index 000000000..bcc10ec0f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+
+import java.util.ArrayList;
+
+public class ReaderPostList extends ArrayList<ReaderPost> {
+
+ public static ReaderPostList fromJson(JSONObject json) {
+ if (json == null) {
+ throw new IllegalArgumentException("null json post list");
+ }
+
+ ReaderPostList posts = new ReaderPostList();
+ JSONArray jsonPosts = json.optJSONArray("posts");
+ if (jsonPosts != null) {
+ for (int i = 0; i < jsonPosts.length(); i++) {
+ posts.add(ReaderPost.fromJson(jsonPosts.optJSONObject(i)));
+ }
+ }
+
+ return posts;
+ }
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ public int indexOfPost(ReaderPost post) {
+ if (post == null) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).postId == post.postId) {
+ if (post.isExternal && post.feedId == this.get(i).feedId) {
+ return i;
+ } else if (!post.isExternal && post.blogId == this.get(i).blogId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ public int indexOfIds(ReaderBlogIdPostId ids) {
+ if (ids == null) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).hasIds(ids)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /*
+ * does passed list contain the same posts as this list?
+ */
+ public boolean isSameList(ReaderPostList posts) {
+ if (posts == null || posts.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderPost post: posts) {
+ int index = indexOfPost(post);
+ if (index == -1 || !post.isSamePost(this.get(index))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * returns posts in this list which are in the passed blog
+ */
+ public ReaderPostList getPostsInBlog(long blogId) {
+ ReaderPostList postsInBlog = new ReaderPostList();
+ for (ReaderPost post: this) {
+ if (post.blogId == blogId) {
+ postsInBlog.add(post);
+ }
+ }
+ return postsInBlog;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java
new file mode 100644
index 000000000..2076a543a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java
@@ -0,0 +1,54 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ReaderRecommendBlogList extends ArrayList<ReaderRecommendedBlog> {
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ public static ReaderRecommendBlogList fromJson(JSONObject json) {
+ ReaderRecommendBlogList blogs = new ReaderRecommendBlogList();
+
+ if (json == null) {
+ return blogs;
+ }
+
+ JSONArray jsonBlogs = json.optJSONArray("blogs");
+ if (jsonBlogs != null) {
+ for (int i = 0; i < jsonBlogs.length(); i++)
+ blogs.add(ReaderRecommendedBlog.fromJson(jsonBlogs.optJSONObject(i)));
+ }
+
+ return blogs;
+ }
+
+ private int indexOfBlogId(long blogId) {
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).blogId == blogId)
+ return i;
+ }
+ return -1;
+ }
+
+ public boolean isSameList(ReaderRecommendBlogList blogs) {
+ if (blogs == null || blogs.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderRecommendedBlog blog: blogs) {
+ int index = indexOfBlogId(blog.blogId);
+ if (index == -1 || !this.get(index).isSameAs(blog)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java
new file mode 100644
index 000000000..5f314b2d7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.models;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class ReaderRecommendedBlog {
+ public long blogId;
+ public long followRecoId;
+ public int score;
+
+ private String title;
+ private String blogUrl;
+ private String imageUrl;
+ private String reason;
+
+ /*
+ * populated by response from get/read/recommendations/mine/
+ */
+ public static ReaderRecommendedBlog fromJson(JSONObject json) {
+ if (json == null) {
+ return null;
+ }
+
+ ReaderRecommendedBlog blog = new ReaderRecommendedBlog();
+
+ blog.blogId = json.optLong("blog_id");
+ blog.followRecoId = json.optLong("follow_reco_id");
+ blog.score = json.optInt("score");
+
+ blog.setTitle(JSONUtils.getString(json, "title"));
+ blog.setImageUrl(JSONUtils.getString(json, "image"));
+ blog.setReason(JSONUtils.getStringDecoded(json, "reason"));
+
+ // the "url" field points to an API endpoint, "blog_domain" contains the actual url
+ blog.setBlogUrl(JSONUtils.getString(json, "blog_domain"));
+
+ return blog;
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+ public void setTitle(String title) {
+ this.title = StringUtils.notNullStr(title);
+ }
+
+ public String getReason() {
+ return StringUtils.notNullStr(reason);
+ }
+ public void setReason(String reason) {
+ this.reason = StringUtils.notNullStr(reason);
+ }
+
+ public String getBlogUrl() {
+ return StringUtils.notNullStr(blogUrl);
+ }
+ public void setBlogUrl(String blogUrl) {
+ this.blogUrl = StringUtils.notNullStr(blogUrl);
+ }
+
+ public String getImageUrl() {
+ return StringUtils.notNullStr(imageUrl);
+ }
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = StringUtils.notNullStr(imageUrl);
+ }
+
+ protected boolean isSameAs(ReaderRecommendedBlog blog) {
+ if (blog == null) {
+ return false;
+ }
+ return (blog.blogId == this.blogId
+ && blog.score == this.score
+ && blog.followRecoId == this.followRecoId);
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
new file mode 100644
index 000000000..0fa75a8b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
@@ -0,0 +1,214 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.io.Serializable;
+import java.util.regex.Pattern;
+
+public class ReaderTag implements Serializable, FilterCriteria {
+ private String tagSlug; // tag for API calls
+ private String tagDisplayName; // tag for display, usually the same as the slug
+ private String tagTitle; // title, used for default tags
+ private String endpoint; // endpoint for updating posts with this tag
+ public final ReaderTagType tagType;
+
+ // these are the default tags, which aren't localized in the /read/menu/ response
+ private static final String TAG_TITLE_LIKED = "Posts I Like";
+ private static final String TAG_TITLE_DISCOVER = "Discover";
+ public static final String TAG_TITLE_DEFAULT = TAG_TITLE_DISCOVER;
+ public static final String TAG_TITLE_FOLLOWED_SITES = "Followed Sites";
+
+ public ReaderTag(String slug,
+ String displayName,
+ String title,
+ String endpoint,
+ ReaderTagType tagType) {
+ // we need a slug since it's used to uniquely ID the tag (including setting it as the
+ // primary key in the tag table)
+ if (TextUtils.isEmpty(slug)) {
+ if (!TextUtils.isEmpty(title)) {
+ setTagSlug(ReaderUtils.sanitizeWithDashes(title));
+ } else {
+ setTagSlug(getTagSlugFromEndpoint(endpoint));
+ }
+ } else {
+ setTagSlug(slug);
+ }
+
+ setTagDisplayName(displayName);
+ setTagTitle(title);
+ setEndpoint(endpoint);
+ this.tagType = tagType;
+ }
+
+ public String getEndpoint() {
+ return StringUtils.notNullStr(endpoint);
+ }
+ private void setEndpoint(String endpoint) {
+ this.endpoint = StringUtils.notNullStr(endpoint);
+ }
+
+ public String getTagTitle() {
+ return StringUtils.notNullStr(tagTitle);
+ }
+ private void setTagTitle(String title) {
+ this.tagTitle = StringUtils.notNullStr(title);
+ }
+ private boolean hasTagTitle() {
+ return !TextUtils.isEmpty(tagTitle);
+ }
+
+ public String getTagDisplayName() {
+ return StringUtils.notNullStr(tagDisplayName);
+ }
+ private void setTagDisplayName(String displayName) {
+ this.tagDisplayName = StringUtils.notNullStr(displayName);
+ }
+
+ public String getTagSlug() {
+ return StringUtils.notNullStr(tagSlug);
+ }
+ private void setTagSlug(String slug) {
+ this.tagSlug = StringUtils.notNullStr(slug);
+ }
+
+ /*
+ * returns the tag name for use in the application log - if this is a default tag it returns
+ * the full tag name, otherwise it abbreviates the tag name since exposing followed tags
+ * in the log could be considered a privacy issue
+ */
+ public String getTagNameForLog() {
+ String tagSlug = getTagSlug();
+ if (tagType == ReaderTagType.DEFAULT) {
+ return tagSlug;
+ } else if (tagSlug.length() >= 6) {
+ return tagSlug.substring(0, 3) + "...";
+ } else if (tagSlug.length() >= 4) {
+ return tagSlug.substring(0, 2) + "...";
+ } else if (tagSlug.length() >= 2) {
+ return tagSlug.substring(0, 1) + "...";
+ } else {
+ return "...";
+ }
+ }
+
+ /*
+ * used to ensure a tag name is valid before adding it
+ */
+ private static final Pattern INVALID_CHARS = Pattern.compile("^.*[~#@*+%{}<>\\[\\]|\"\\_].*$");
+ public static boolean isValidTagName(String tagName) {
+ return !TextUtils.isEmpty(tagName)
+ && !INVALID_CHARS.matcher(tagName).matches();
+ }
+
+ /*
+ * extracts the tag slug from a valid read/tags/[tagSlug]/posts endpoint
+ */
+ private static String getTagSlugFromEndpoint(final String endpoint) {
+ if (TextUtils.isEmpty(endpoint))
+ return "";
+
+ // make sure passed endpoint is valid
+ if (!endpoint.endsWith("/posts"))
+ return "";
+ int start = endpoint.indexOf("/read/tags/");
+ if (start == -1)
+ return "";
+
+ // skip "/read/tags/" then find the next "/"
+ start += 11;
+ int end = endpoint.indexOf("/", start);
+ if (end == -1)
+ return "";
+
+ return endpoint.substring(start, end);
+ }
+
+ /*
+ * is the passed string one of the default tags?
+ */
+ public static boolean isDefaultTagTitle(String title) {
+ if (TextUtils.isEmpty(title)) {
+ return false;
+ }
+ return (title.equalsIgnoreCase(TAG_TITLE_FOLLOWED_SITES)
+ || title.equalsIgnoreCase(TAG_TITLE_DISCOVER)
+ || title.equalsIgnoreCase(TAG_TITLE_LIKED));
+ }
+
+ public static boolean isSameTag(ReaderTag tag1, ReaderTag tag2) {
+ if (tag1 == null || tag2 == null) {
+ return false;
+ }
+ return tag1.tagType == tag2.tagType
+ && tag1.getTagSlug().equalsIgnoreCase(tag2.getTagSlug());
+ }
+
+ public boolean isPostsILike() {
+ return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/liked");
+ }
+
+ public boolean isFollowedSites() {
+ return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/following");
+ }
+
+ public boolean isDiscover() {
+ return tagType == ReaderTagType.DEFAULT && getTagSlug().equals(TAG_TITLE_DISCOVER);
+ }
+
+ public boolean isTagTopic() {
+ String endpoint = getEndpoint();
+ return endpoint.toLowerCase().contains("/read/tags/");
+ }
+ public boolean isListTopic() {
+ String endpoint = getEndpoint();
+ return endpoint.toLowerCase().contains("/read/list/");
+ }
+
+ /*
+ * the label is the text displayed in the dropdown filter
+ */
+ @Override
+ public String getLabel() {
+ if (tagType == ReaderTagType.DEFAULT) {
+ return getTagTitle();
+ } else if (isTagDisplayNameAlphaNumeric()) {
+ return getTagDisplayName().toLowerCase();
+ } else if (hasTagTitle()) {
+ return getTagTitle();
+ } else {
+ return getTagDisplayName();
+ }
+ }
+
+ /*
+ * returns true if the tag display name contains only alpha-numeric characters or hyphens
+ */
+ private boolean isTagDisplayNameAlphaNumeric() {
+ if (TextUtils.isEmpty(tagDisplayName)) {
+ return false;
+ }
+
+ for (int i=0; i < tagDisplayName.length(); i++) {
+ char c = tagDisplayName.charAt(i);
+ if (!Character.isLetterOrDigit(c) && c != '-') {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object object){
+ if (object instanceof ReaderTag) {
+ ReaderTag tag = (ReaderTag) object;
+ return (tag.tagType == this.tagType && tag.getLabel().equals(this.getLabel()));
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java
new file mode 100644
index 000000000..87464b722
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.models;
+
+import java.util.ArrayList;
+
+public class ReaderTagList extends ArrayList<ReaderTag> {
+
+ public int indexOfTagName(String tagName) {
+ if (tagName == null || isEmpty()) {
+ return -1;
+ }
+
+ for (int i = 0; i < size(); i++) {
+ if (tagName.equals(this.get(i).getTagSlug())) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private int indexOfTag(ReaderTag tag) {
+ if (tag == null || isEmpty()) {
+ return -1;
+ }
+
+ for (int i = 0; i < this.size(); i++) {
+ if (ReaderTag.isSameTag(tag, this.get(i))) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public boolean isSameList(ReaderTagList otherList) {
+ if (otherList == null || otherList.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderTag otherTag: otherList) {
+ int i = this.indexOfTag(otherTag);
+ if (i == -1) {
+ return false;
+ } else if (!otherTag.getEndpoint().equals(this.get(i).getEndpoint())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * returns a list of tags that are in this list but not in the passed list
+ */
+ public ReaderTagList getDeletions(ReaderTagList otherList) {
+ ReaderTagList deletions = new ReaderTagList();
+ if (otherList == null) {
+ return deletions;
+ }
+
+ for (ReaderTag thisTag: this) {
+ if (otherList.indexOfTag(thisTag) == -1) {
+ deletions.add(thisTag);
+ }
+ }
+
+ return deletions;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
new file mode 100644
index 000000000..5075e81d3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.models;
+
+public enum ReaderTagType {
+ FOLLOWED,
+ DEFAULT,
+ RECOMMENDED,
+ CUSTOM_LIST,
+ SEARCH;
+
+ private static final int INT_DEFAULT = 0;
+ private static final int INT_FOLLOWED = 1;
+ private static final int INT_RECOMMENDED = 2;
+ private static final int INT_CUSTOM_LIST = 3;
+ private static final int INT_SEARCH = 4;
+
+ public static ReaderTagType fromInt(int value) {
+ switch (value) {
+ case INT_RECOMMENDED :
+ return RECOMMENDED;
+ case INT_FOLLOWED :
+ return FOLLOWED;
+ case INT_CUSTOM_LIST:
+ return CUSTOM_LIST;
+ case INT_SEARCH:
+ return SEARCH;
+ default :
+ return DEFAULT;
+ }
+ }
+
+ public int toInt() {
+ switch (this) {
+ case FOLLOWED:
+ return INT_FOLLOWED;
+ case RECOMMENDED:
+ return INT_RECOMMENDED;
+ case CUSTOM_LIST:
+ return INT_CUSTOM_LIST;
+ case SEARCH:
+ return INT_SEARCH;
+ default :
+ return INT_DEFAULT;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java
new file mode 100644
index 000000000..17c97f1eb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java
@@ -0,0 +1,36 @@
+package org.wordpress.android.models;
+
+import org.wordpress.android.util.UrlUtils;
+
+import java.util.HashSet;
+
+/**
+ * URLs are normalized before being added and during comparison to ensure better comparison
+ * of URLs that may be different strings but point to the same URL
+ */
+public class ReaderUrlList extends HashSet<String> {
+ @Override
+ public boolean add(String url) {
+ return super.add(UrlUtils.normalizeUrl(url));
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ if (object instanceof String) {
+ return super.remove(UrlUtils.normalizeUrl((String) object));
+ } else {
+ return super.remove(object);
+ }
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ if (object instanceof String) {
+ return super.contains(UrlUtils.normalizeUrl((String) object));
+ } else {
+ return super.contains(object);
+ }
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java
new file mode 100644
index 000000000..abd905df1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java
@@ -0,0 +1,121 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+public class ReaderUser {
+ public long userId;
+ public long blogId;
+ private String userName;
+ private String displayName;
+ private String url;
+ private String profileUrl;
+ private String avatarUrl;
+
+ public static ReaderUser fromJson(JSONObject json) {
+ ReaderUser user = new ReaderUser();
+ if (json==null)
+ return user;
+
+ user.userId = json.optLong("ID");
+ user.blogId = json.optLong("site_ID");
+
+ user.userName = JSONUtils.getString(json, "username");
+ user.url = JSONUtils.getString(json, "URL"); // <-- this isn't necessarily a wp blog
+ user.profileUrl = JSONUtils.getString(json, "profile_URL");
+ user.avatarUrl = JSONUtils.getString(json, "avatar_URL");
+
+ // "me" api call (current user) has "display_name", others have "name"
+ if (json.has("display_name")) {
+ user.displayName = JSONUtils.getStringDecoded(json, "display_name");
+ } else {
+ user.displayName = JSONUtils.getStringDecoded(json, "name");
+ }
+
+ return user;
+ }
+
+ public String getUserName() {
+ return StringUtils.notNullStr(userName);
+ }
+ public void setUserName(String userName) {
+ this.userName = StringUtils.notNullStr(userName);
+ }
+
+ public String getDisplayName() {
+ return StringUtils.notNullStr(displayName);
+ }
+ public void setDisplayName(String displayName) {
+ this.displayName = StringUtils.notNullStr(displayName);
+ }
+
+ public String getUrl() {
+ return StringUtils.notNullStr(url);
+ }
+ public void setUrl(String url) {
+ this.url = StringUtils.notNullStr(url);
+ }
+
+ public String getProfileUrl() {
+ return StringUtils.notNullStr(profileUrl);
+ }
+ public void setProfileUrl(String profileUrl) {
+ this.profileUrl = StringUtils.notNullStr(profileUrl);
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(avatarUrl);
+ }
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = StringUtils.notNullStr(avatarUrl);
+ }
+
+ public boolean hasUrl() {
+ return !TextUtils.isEmpty(url);
+ }
+
+ public boolean hasAvatarUrl() {
+ return !TextUtils.isEmpty(avatarUrl);
+ }
+
+ public boolean hasBlogId() {
+ return (blogId != 0);
+ }
+
+ /*
+ * not stored - used by ReaderUserAdapter for performance
+ */
+ private transient String urlDomain;
+ public String getUrlDomain() {
+ if (urlDomain == null) {
+ if (hasUrl()) {
+ urlDomain = UrlUtils.getHost(getUrl());
+ } else {
+ urlDomain = "";
+ }
+ }
+ return urlDomain;
+ }
+
+ public boolean isSameUser(ReaderUser user) {
+ if (user == null)
+ return false;
+ if (this.userId != user.userId)
+ return false;
+ if (!this.getAvatarUrl().equals(user.getAvatarUrl()))
+ return false;
+ if (!this.getDisplayName().equals(user.getDisplayName()))
+ return false;
+ if (!this.getUserName().equals(user.getUserName()))
+ return false;
+ if (!this.getUrl().equals(user.getUrl()))
+ return false;
+ if (!this.getProfileUrl().equals(user.getProfileUrl()))
+ return false;
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java
new file mode 100644
index 000000000..8073e1c61
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.models;
+
+import java.util.HashSet;
+
+public class ReaderUserIdList extends HashSet<Long> {
+ /*
+ * returns true if passed list contains the same userIds as this list
+ */
+ public boolean isSameList(ReaderUserIdList compareIds) {
+ if (compareIds==null || compareIds.size()!=this.size())
+ return false;
+ return this.containsAll(compareIds);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java
new file mode 100644
index 000000000..cd77a99d1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ReaderUserList extends ArrayList<ReaderUser> {
+ /*
+ * returns all userIds in this list
+ */
+ public ReaderUserIdList getUserIds() {
+ ReaderUserIdList ids = new ReaderUserIdList();
+ for (ReaderUser user: this)
+ ids.add(user.userId);
+ return ids;
+ }
+
+ public int indexOfUserId(long userId) {
+ for (int i = 0; i < this.size(); i++) {
+ if (userId == this.get(i).userId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /*
+ * passed json is response from getting likes for a post
+ */
+ public static ReaderUserList fromJsonLikes(JSONObject json) {
+ ReaderUserList users = new ReaderUserList();
+ if (json==null)
+ return users;
+
+ JSONArray jsonLikes = json.optJSONArray("likes");
+ if (jsonLikes!=null) {
+ for (int i=0; i < jsonLikes.length(); i++)
+ users.add(ReaderUser.fromJson(jsonLikes.optJSONObject(i)));
+ }
+
+ return users;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Role.java b/WordPress/src/main/java/org/wordpress/android/models/Role.java
new file mode 100644
index 000000000..4266ba561
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Role.java
@@ -0,0 +1,102 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.StringRes;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.CrashlyticsUtils;
+
+public enum Role {
+ ADMIN(R.string.role_admin),
+ EDITOR(R.string.role_editor),
+ AUTHOR(R.string.role_author),
+ CONTRIBUTOR(R.string.role_contributor),
+ FOLLOWER(R.string.role_follower),
+ VIEWER(R.string.role_viewer);
+
+ private final int mLabelResId;
+
+ Role(@StringRes int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ public String toDisplayString() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+
+ public static Role fromString(String role) {
+ switch (role) {
+ case "administrator":
+ return ADMIN;
+ case "editor":
+ return EDITOR;
+ case "author":
+ return AUTHOR;
+ case "contributor":
+ return CONTRIBUTOR;
+ case "follower":
+ return FOLLOWER;
+ case "viewer":
+ return VIEWER;
+ }
+ Exception e = new IllegalArgumentException("All roles must be handled: " + role);
+ CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC, AppLog.T.PEOPLE);
+
+ // All roles should have been handled, but in case an edge case occurs,
+ // using "Contributor" role is the safest option
+ return CONTRIBUTOR;
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case ADMIN:
+ return "administrator";
+ case EDITOR:
+ return "editor";
+ case AUTHOR:
+ return "author";
+ case CONTRIBUTOR:
+ return "contributor";
+ case FOLLOWER:
+ return "follower";
+ case VIEWER:
+ return "viewer";
+ }
+ throw new IllegalArgumentException("All roles must be handled");
+ }
+
+ /**
+ * @return the string representation of the role, as used by the REST API
+ */
+ public String toRESTString() {
+ switch (this) {
+ case ADMIN:
+ return "administrator";
+ case EDITOR:
+ return "editor";
+ case AUTHOR:
+ return "author";
+ case CONTRIBUTOR:
+ return "contributor";
+ case FOLLOWER:
+ return "follower";
+ case VIEWER:
+ // the remote expects "follower" as the role parameter even if the role is "viewer"
+ return "follower";
+ }
+ throw new IllegalArgumentException("All roles must be handled");
+ }
+
+ public static Role[] userRoles() {
+ return new Role[] { ADMIN, EDITOR, AUTHOR, CONTRIBUTOR };
+ }
+
+ public static Role[] inviteRoles(boolean isPrivateSite) {
+ if (isPrivateSite) {
+ return new Role[] { VIEWER, ADMIN, EDITOR, AUTHOR, CONTRIBUTOR };
+ }
+ return new Role[] { FOLLOWER, ADMIN, EDITOR, AUTHOR, CONTRIBUTOR };
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java b/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java
new file mode 100644
index 000000000..16f905329
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java
@@ -0,0 +1,418 @@
+package org.wordpress.android.models;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds blog settings and provides methods to (de)serialize .com and self-hosted network calls.
+ */
+
+public class SiteSettingsModel {
+ public static final int RELATED_POSTS_ENABLED_FLAG = 0x1;
+ public static final int RELATED_POST_HEADER_FLAG = 0x2;
+ public static final int RELATED_POST_IMAGE_FLAG = 0x4;
+
+ // Settings table column names
+ public static final String ID_COLUMN_NAME = "id";
+ public static final String ADDRESS_COLUMN_NAME = "address";
+ public static final String USERNAME_COLUMN_NAME = "username";
+ public static final String PASSWORD_COLUMN_NAME = "password";
+ public static final String TITLE_COLUMN_NAME = "title";
+ public static final String TAGLINE_COLUMN_NAME = "tagline";
+ public static final String LANGUAGE_COLUMN_NAME = "language";
+ public static final String PRIVACY_COLUMN_NAME = "privacy";
+ public static final String LOCATION_COLUMN_NAME = "location";
+ public static final String DEF_CATEGORY_COLUMN_NAME = "defaultCategory";
+ public static final String DEF_POST_FORMAT_COLUMN_NAME = "defaultPostFormat";
+ public static final String CATEGORIES_COLUMN_NAME = "categories";
+ public static final String POST_FORMATS_COLUMN_NAME = "postFormats";
+ public static final String CREDS_VERIFIED_COLUMN_NAME = "credsVerified";
+ public static final String RELATED_POSTS_COLUMN_NAME = "relatedPosts";
+ public static final String ALLOW_COMMENTS_COLUMN_NAME = "allowComments";
+ public static final String SEND_PINGBACKS_COLUMN_NAME = "sendPingbacks";
+ public static final String RECEIVE_PINGBACKS_COLUMN_NAME = "receivePingbacks";
+ public static final String SHOULD_CLOSE_AFTER_COLUMN_NAME = "shouldCloseAfter";
+ public static final String CLOSE_AFTER_COLUMN_NAME = "closeAfter";
+ public static final String SORT_BY_COLUMN_NAME = "sortBy";
+ public static final String SHOULD_THREAD_COLUMN_NAME = "shouldThread";
+ public static final String THREADING_COLUMN_NAME = "threading";
+ public static final String SHOULD_PAGE_COLUMN_NAME = "shouldPage";
+ public static final String PAGING_COLUMN_NAME = "paging";
+ public static final String MANUAL_APPROVAL_COLUMN_NAME = "manualApproval";
+ public static final String IDENTITY_REQUIRED_COLUMN_NAME = "identityRequired";
+ public static final String USER_ACCOUNT_REQUIRED_COLUMN_NAME = "userAccountRequired";
+ public static final String WHITELIST_COLUMN_NAME = "whitelist";
+ public static final String MODERATION_KEYS_COLUMN_NAME = "moderationKeys";
+ public static final String BLACKLIST_KEYS_COLUMN_NAME = "blacklistKeys";
+
+ public static final String SETTINGS_TABLE_NAME = "site_settings";
+ public static final String CREATE_SETTINGS_TABLE_SQL =
+ "CREATE TABLE IF NOT EXISTS " +
+ SETTINGS_TABLE_NAME +
+ " (" +
+ ID_COLUMN_NAME + " INTEGER PRIMARY KEY, " +
+ ADDRESS_COLUMN_NAME + " TEXT, " +
+ USERNAME_COLUMN_NAME + " TEXT, " +
+ PASSWORD_COLUMN_NAME + " TEXT, " +
+ TITLE_COLUMN_NAME + " TEXT, " +
+ TAGLINE_COLUMN_NAME + " TEXT, " +
+ LANGUAGE_COLUMN_NAME + " INTEGER, " +
+ PRIVACY_COLUMN_NAME + " INTEGER, " +
+ LOCATION_COLUMN_NAME + " BOOLEAN, " +
+ DEF_CATEGORY_COLUMN_NAME + " TEXT, " +
+ DEF_POST_FORMAT_COLUMN_NAME + " TEXT, " +
+ CATEGORIES_COLUMN_NAME + " TEXT, " +
+ POST_FORMATS_COLUMN_NAME + " TEXT, " +
+ CREDS_VERIFIED_COLUMN_NAME + " BOOLEAN, " +
+ RELATED_POSTS_COLUMN_NAME + " INTEGER, " +
+ ALLOW_COMMENTS_COLUMN_NAME + " BOOLEAN, " +
+ SEND_PINGBACKS_COLUMN_NAME + " BOOLEAN, " +
+ RECEIVE_PINGBACKS_COLUMN_NAME + " BOOLEAN, " +
+ SHOULD_CLOSE_AFTER_COLUMN_NAME + " BOOLEAN, " +
+ CLOSE_AFTER_COLUMN_NAME + " INTEGER, " +
+ SORT_BY_COLUMN_NAME + " INTEGER, " +
+ SHOULD_THREAD_COLUMN_NAME + " BOOLEAN, " +
+ THREADING_COLUMN_NAME + " INTEGER, " +
+ SHOULD_PAGE_COLUMN_NAME + " BOOLEAN, " +
+ PAGING_COLUMN_NAME + " INTEGER, " +
+ MANUAL_APPROVAL_COLUMN_NAME + " BOOLEAN, " +
+ IDENTITY_REQUIRED_COLUMN_NAME + " BOOLEAN, " +
+ USER_ACCOUNT_REQUIRED_COLUMN_NAME + " BOOLEAN, " +
+ WHITELIST_COLUMN_NAME + " BOOLEAN, " +
+ MODERATION_KEYS_COLUMN_NAME + " TEXT, " +
+ BLACKLIST_KEYS_COLUMN_NAME + " TEXT" +
+ ");";
+
+ public boolean isInLocalTable;
+ public boolean hasVerifiedCredentials;
+ public long localTableId;
+ public String address;
+ public String username;
+ public String password;
+ public String title;
+ public String tagline;
+ public String language;
+ public int languageId;
+ public int privacy;
+ public boolean location;
+ public int defaultCategory;
+ public CategoryModel[] categories;
+ public String defaultPostFormat;
+ public Map<String, String> postFormats;
+ public boolean showRelatedPosts;
+ public boolean showRelatedPostHeader;
+ public boolean showRelatedPostImages;
+ public boolean allowComments;
+ public boolean sendPingbacks;
+ public boolean receivePingbacks;
+ public boolean shouldCloseAfter;
+ public int closeCommentAfter;
+ public int sortCommentsBy;
+ public boolean shouldThreadComments;
+ public int threadingLevels;
+ public boolean shouldPageComments;
+ public int commentsPerPage;
+ public boolean commentApprovalRequired;
+ public boolean commentsRequireIdentity;
+ public boolean commentsRequireUserAccount;
+ public boolean commentAutoApprovalKnownUsers;
+ public int maxLinks;
+ public List<String> holdForModeration;
+ public List<String> blacklist;
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof SiteSettingsModel)) return false;
+ SiteSettingsModel otherModel = (SiteSettingsModel) other;
+
+ return localTableId == otherModel.localTableId &&
+ address.equals(otherModel.address) &&
+ username.equals(otherModel.username) &&
+ password.equals(otherModel.password) &&
+ title.equals(otherModel.title) &&
+ tagline.equals(otherModel.tagline) &&
+ languageId == otherModel.languageId &&
+ privacy == otherModel.privacy &&
+ location == otherModel.location &&
+ defaultPostFormat.equals(otherModel.defaultPostFormat) &&
+ defaultCategory == otherModel.defaultCategory &&
+ showRelatedPosts == otherModel.showRelatedPosts &&
+ showRelatedPostHeader == otherModel.showRelatedPostHeader &&
+ showRelatedPostImages == otherModel.showRelatedPostImages &&
+ allowComments == otherModel.allowComments &&
+ sendPingbacks == otherModel.sendPingbacks &&
+ receivePingbacks == otherModel.receivePingbacks &&
+ closeCommentAfter == otherModel.closeCommentAfter &&
+ sortCommentsBy == otherModel.sortCommentsBy &&
+ threadingLevels == otherModel.threadingLevels &&
+ commentsPerPage == otherModel.commentsPerPage &&
+ commentApprovalRequired == otherModel.commentApprovalRequired &&
+ commentsRequireIdentity == otherModel.commentsRequireIdentity &&
+ commentsRequireUserAccount == otherModel.commentsRequireUserAccount &&
+ commentAutoApprovalKnownUsers == otherModel.commentAutoApprovalKnownUsers &&
+ maxLinks == otherModel.maxLinks &&
+ holdForModeration != null && holdForModeration.equals(otherModel.holdForModeration) &&
+ blacklist != null && blacklist.equals(otherModel.blacklist);
+ }
+
+ /**
+ * Copies data from another {@link SiteSettingsModel}.
+ */
+ public void copyFrom(SiteSettingsModel other) {
+ if (other == null) return;
+
+ isInLocalTable = other.isInLocalTable;
+ hasVerifiedCredentials = other.hasVerifiedCredentials;
+ localTableId = other.localTableId;
+ address = other.address;
+ username = other.username;
+ password = other.password;
+ title = other.title;
+ tagline = other.tagline;
+ language = other.language;
+ languageId = other.languageId;
+ privacy = other.privacy;
+ location = other.location;
+ defaultCategory = other.defaultCategory;
+ categories = other.categories;
+ defaultPostFormat = other.defaultPostFormat;
+ postFormats = other.postFormats;
+ showRelatedPosts = other.showRelatedPosts;
+ showRelatedPostHeader = other.showRelatedPostHeader;
+ showRelatedPostImages = other.showRelatedPostImages;
+ allowComments = other.allowComments;
+ sendPingbacks = other.sendPingbacks;
+ receivePingbacks = other.receivePingbacks;
+ shouldCloseAfter = other.shouldCloseAfter;
+ closeCommentAfter = other.closeCommentAfter;
+ sortCommentsBy = other.sortCommentsBy;
+ shouldThreadComments = other.shouldThreadComments;
+ threadingLevels = other.threadingLevels;
+ shouldPageComments = other.shouldPageComments;
+ commentsPerPage = other.commentsPerPage;
+ commentApprovalRequired = other.commentApprovalRequired;
+ commentsRequireIdentity = other.commentsRequireIdentity;
+ commentsRequireUserAccount = other.commentsRequireUserAccount;
+ commentAutoApprovalKnownUsers = other.commentAutoApprovalKnownUsers;
+ maxLinks = other.maxLinks;
+ if (other.holdForModeration != null) {
+ holdForModeration = new ArrayList<>(other.holdForModeration);
+ }
+ if (other.blacklist != null) {
+ blacklist = new ArrayList<>(other.blacklist);
+ }
+ }
+
+ /**
+ * Sets values from a local database {@link Cursor}.
+ */
+ public void deserializeOptionsDatabaseCursor(Cursor cursor, Map<Integer, CategoryModel> models) {
+ if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) return;
+
+ localTableId = getIntFromCursor(cursor, ID_COLUMN_NAME);
+ address = getStringFromCursor(cursor, ADDRESS_COLUMN_NAME);
+ username = getStringFromCursor(cursor, USERNAME_COLUMN_NAME);
+ password = getStringFromCursor(cursor, PASSWORD_COLUMN_NAME);
+ title = getStringFromCursor(cursor, TITLE_COLUMN_NAME);
+ tagline = getStringFromCursor(cursor, TAGLINE_COLUMN_NAME);
+ languageId = getIntFromCursor(cursor, LANGUAGE_COLUMN_NAME);
+ privacy = getIntFromCursor(cursor, PRIVACY_COLUMN_NAME);
+ defaultCategory = getIntFromCursor(cursor, DEF_CATEGORY_COLUMN_NAME);
+ defaultPostFormat = getStringFromCursor(cursor, DEF_POST_FORMAT_COLUMN_NAME);
+ location = getBooleanFromCursor(cursor, LOCATION_COLUMN_NAME);
+ hasVerifiedCredentials = getBooleanFromCursor(cursor, CREDS_VERIFIED_COLUMN_NAME);
+ allowComments = getBooleanFromCursor(cursor, ALLOW_COMMENTS_COLUMN_NAME);
+ sendPingbacks = getBooleanFromCursor(cursor, SEND_PINGBACKS_COLUMN_NAME);
+ receivePingbacks = getBooleanFromCursor(cursor, RECEIVE_PINGBACKS_COLUMN_NAME);
+ shouldCloseAfter = getBooleanFromCursor(cursor, SHOULD_CLOSE_AFTER_COLUMN_NAME);
+ closeCommentAfter = getIntFromCursor(cursor, CLOSE_AFTER_COLUMN_NAME);
+ sortCommentsBy = getIntFromCursor(cursor, SORT_BY_COLUMN_NAME);
+ shouldThreadComments = getBooleanFromCursor(cursor, SHOULD_THREAD_COLUMN_NAME);
+ threadingLevels = getIntFromCursor(cursor, THREADING_COLUMN_NAME);
+ shouldPageComments = getBooleanFromCursor(cursor, SHOULD_PAGE_COLUMN_NAME);
+ commentsPerPage = getIntFromCursor(cursor, PAGING_COLUMN_NAME);
+ commentApprovalRequired = getBooleanFromCursor(cursor, MANUAL_APPROVAL_COLUMN_NAME);
+ commentsRequireIdentity = getBooleanFromCursor(cursor, IDENTITY_REQUIRED_COLUMN_NAME);
+ commentsRequireUserAccount = getBooleanFromCursor(cursor, USER_ACCOUNT_REQUIRED_COLUMN_NAME);
+ commentAutoApprovalKnownUsers = getBooleanFromCursor(cursor, WHITELIST_COLUMN_NAME);
+
+ String moderationKeys = getStringFromCursor(cursor, MODERATION_KEYS_COLUMN_NAME);
+ String blacklistKeys = getStringFromCursor(cursor, BLACKLIST_KEYS_COLUMN_NAME);
+ holdForModeration = new ArrayList<>();
+ blacklist = new ArrayList<>();
+ if (!TextUtils.isEmpty(moderationKeys)) {
+ Collections.addAll(holdForModeration, moderationKeys.split("\n"));
+ }
+ if (!TextUtils.isEmpty(blacklistKeys)) {
+ Collections.addAll(blacklist, blacklistKeys.split("\n"));
+ }
+
+ setRelatedPostsFlags(Math.max(0, getIntFromCursor(cursor, RELATED_POSTS_COLUMN_NAME)));
+
+ String cachedCategories = getStringFromCursor(cursor, CATEGORIES_COLUMN_NAME);
+ String cachedFormats = getStringFromCursor(cursor, POST_FORMATS_COLUMN_NAME);
+ if (models != null && !TextUtils.isEmpty(cachedCategories)) {
+ String[] split = cachedCategories.split(",");
+ categories = new CategoryModel[split.length];
+ for (int i = 0; i < split.length; ++i) {
+ int catId = Integer.parseInt(split[i]);
+ categories[i] = models.get(catId);
+ }
+ }
+ if (!TextUtils.isEmpty(cachedFormats)) {
+ String[] split = cachedFormats.split(";");
+ postFormats = new HashMap<>();
+ for (String format : split) {
+ String[] kvp = format.split(",");
+ postFormats.put(kvp[0], kvp[1]);
+ }
+ }
+
+ int cachedRelatedPosts = getIntFromCursor(cursor, RELATED_POSTS_COLUMN_NAME);
+ if (cachedRelatedPosts != -1) {
+ setRelatedPostsFlags(cachedRelatedPosts);
+ }
+
+ isInLocalTable = true;
+ }
+
+ /**
+ * Creates the {@link ContentValues} object to store this category data in a local database.
+ */
+ public ContentValues serializeToDatabase() {
+ ContentValues values = new ContentValues();
+ values.put(ID_COLUMN_NAME, localTableId);
+ values.put(ADDRESS_COLUMN_NAME, address);
+ values.put(USERNAME_COLUMN_NAME, username);
+ values.put(PASSWORD_COLUMN_NAME, password);
+ values.put(TITLE_COLUMN_NAME, title);
+ values.put(TAGLINE_COLUMN_NAME, tagline);
+ values.put(PRIVACY_COLUMN_NAME, privacy);
+ values.put(LANGUAGE_COLUMN_NAME, languageId);
+ values.put(LOCATION_COLUMN_NAME, location);
+ values.put(DEF_CATEGORY_COLUMN_NAME, defaultCategory);
+ values.put(CATEGORIES_COLUMN_NAME, categoryIdList(categories));
+ values.put(DEF_POST_FORMAT_COLUMN_NAME, defaultPostFormat);
+ values.put(POST_FORMATS_COLUMN_NAME, postFormatList(postFormats));
+ values.put(CREDS_VERIFIED_COLUMN_NAME, hasVerifiedCredentials);
+ values.put(RELATED_POSTS_COLUMN_NAME, getRelatedPostsFlags());
+ values.put(ALLOW_COMMENTS_COLUMN_NAME, allowComments);
+ values.put(SEND_PINGBACKS_COLUMN_NAME, sendPingbacks);
+ values.put(RECEIVE_PINGBACKS_COLUMN_NAME, receivePingbacks);
+ values.put(SHOULD_CLOSE_AFTER_COLUMN_NAME, shouldCloseAfter);
+ values.put(CLOSE_AFTER_COLUMN_NAME, closeCommentAfter);
+ values.put(SORT_BY_COLUMN_NAME, sortCommentsBy);
+ values.put(SHOULD_THREAD_COLUMN_NAME, shouldThreadComments);
+ values.put(THREADING_COLUMN_NAME, threadingLevels);
+ values.put(SHOULD_PAGE_COLUMN_NAME, shouldPageComments);
+ values.put(PAGING_COLUMN_NAME, commentsPerPage);
+ values.put(MANUAL_APPROVAL_COLUMN_NAME, commentApprovalRequired);
+ values.put(IDENTITY_REQUIRED_COLUMN_NAME, commentsRequireIdentity);
+ values.put(USER_ACCOUNT_REQUIRED_COLUMN_NAME, commentsRequireUserAccount);
+ values.put(WHITELIST_COLUMN_NAME, commentAutoApprovalKnownUsers);
+
+ String moderationKeys = "";
+ if (holdForModeration != null) {
+ for (String key : holdForModeration) {
+ moderationKeys += key + "\n";
+ }
+ }
+ String blacklistKeys = "";
+ if (blacklist != null) {
+ for (String key : blacklist) {
+ blacklistKeys += key + "\n";
+ }
+ }
+ values.put(MODERATION_KEYS_COLUMN_NAME, moderationKeys);
+ values.put(BLACKLIST_KEYS_COLUMN_NAME, blacklistKeys);
+
+ return values;
+ }
+
+ public int getRelatedPostsFlags() {
+ int flags = 0;
+
+ if (showRelatedPosts) flags |= RELATED_POSTS_ENABLED_FLAG;
+ if (showRelatedPostHeader) flags |= RELATED_POST_HEADER_FLAG;
+ if (showRelatedPostImages) flags |= RELATED_POST_IMAGE_FLAG;
+
+ return flags;
+ }
+
+ public void setRelatedPostsFlags(int flags) {
+ showRelatedPosts = (flags & RELATED_POSTS_ENABLED_FLAG) > 0;
+ showRelatedPostHeader = (flags & RELATED_POST_HEADER_FLAG) > 0;
+ showRelatedPostImages = (flags & RELATED_POST_IMAGE_FLAG) > 0;
+ }
+
+ /**
+ * Used to serialize post formats to store in a local database.
+ *
+ * @param formats
+ * map of post formats where the key is the format ID and the value is the format name
+ * @return
+ * a String of semi-colon separated KVP's of Post Formats; Post Format ID -> Post Format Name
+ */
+ private static String postFormatList(Map<String, String> formats) {
+ if (formats == null || formats.size() == 0) return "";
+
+ StringBuilder builder = new StringBuilder();
+ for (String key : formats.keySet()) {
+ builder.append(key).append(",").append(formats.get(key)).append(";");
+ }
+ builder.setLength(builder.length() - 1);
+
+ return builder.toString();
+ }
+
+ /**
+ * Used to serialize categories to store in a local database.
+ *
+ * @param elements
+ * {@link CategoryModel} array to create String ID list from
+ * @return
+ * a String of comma-separated integer Category ID's
+ */
+ private static String categoryIdList(CategoryModel[] elements) {
+ if (elements == null || elements.length == 0) return "";
+
+ StringBuilder builder = new StringBuilder();
+ for (CategoryModel element : elements) {
+ builder.append(String.valueOf(element.id)).append(",");
+ }
+ builder.setLength(builder.length() - 1);
+
+ return builder.toString();
+ }
+
+ /**
+ * Helper method to get an integer value from a given column in a Cursor.
+ */
+ private int getIntFromCursor(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ return columnIndex != -1 ? cursor.getInt(columnIndex) : -1;
+ }
+
+ /**
+ * Helper method to get a String value from a given column in a Cursor.
+ */
+ private String getStringFromCursor(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ return columnIndex != -1 ? cursor.getString(columnIndex) : "";
+ }
+
+ /**
+ * Helper method to get a boolean value (stored as an int) from a given column in a Cursor.
+ */
+ private boolean getBooleanFromCursor(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ return columnIndex != -1 && cursor.getInt(columnIndex) != 0;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Suggestion.java b/WordPress/src/main/java/org/wordpress/android/models/Suggestion.java
new file mode 100644
index 000000000..0d4b1d752
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Suggestion.java
@@ -0,0 +1,71 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Suggestion {
+ private static final String MENTION_TAXONOMY = "mention";
+
+ public long siteID;
+
+ private String userLogin;
+ private String displayName;
+ private String imageUrl;
+ private String taxonomy;
+
+ public Suggestion(long siteID,
+ String userLogin,
+ String displayName,
+ String imageUrl,
+ String taxonomy) {
+ this.siteID = siteID;
+ this.userLogin = userLogin;
+ this.displayName = displayName;
+ this.imageUrl = imageUrl;
+ this.taxonomy = taxonomy;
+ }
+
+ public static Suggestion fromJSON(JSONObject json, long siteID) {
+ if (json == null) {
+ return null;
+ }
+
+ String userLogin = JSONUtils.getString(json, "user_login");
+ String displayName = JSONUtils.getString(json, "display_name");
+ String imageUrl = JSONUtils.getString(json, "image_URL");
+
+ // the api currently doesn't return a taxonomy field but we want to be ready for when it does
+ return new Suggestion(siteID, userLogin, displayName, imageUrl, MENTION_TAXONOMY);
+ }
+
+ public static List<Suggestion> suggestionListFromJSON(JSONArray jsonArray, long siteID) {
+ if (jsonArray == null) {
+ return null;
+ }
+
+ ArrayList<Suggestion> suggestions = new ArrayList<Suggestion>(jsonArray.length());
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ Suggestion suggestion = Suggestion.fromJSON(jsonArray.optJSONObject(i), siteID);
+ suggestions.add(suggestion);
+ }
+
+ return suggestions;
+ }
+
+ public String getUserLogin() {
+ return StringUtils.notNullStr(userLogin);
+ }
+ public String getDisplayName() {
+ return StringUtils.notNullStr(displayName);
+ }
+ public String getImageUrl() {
+ return StringUtils.notNullStr(imageUrl);
+ }
+ public String getTaxonomy() { return StringUtils.notNullStr(taxonomy); }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Tag.java b/WordPress/src/main/java/org/wordpress/android/models/Tag.java
new file mode 100644
index 000000000..87b756854
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Tag.java
@@ -0,0 +1,51 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Tag {
+ public long siteID;
+
+ private String tag;
+
+ public Tag(long siteID,
+ String tag) {
+ this.siteID = siteID;
+ this.tag = tag;
+ }
+
+ public static Tag fromJSON(JSONObject json, long siteID) {
+ if (json == null) {
+ return null;
+ }
+
+ String tag = JSONUtils.getString(json, "name");
+
+ // the api currently doesn't return a taxonomy field but we want to be ready for when it does
+ return new Tag(siteID, tag);
+ }
+
+ public static List<Tag> tagListFromJSON(JSONArray jsonArray, long siteID) {
+ if (jsonArray == null) {
+ return null;
+ }
+
+ ArrayList<Tag> suggestions = new ArrayList<Tag>(jsonArray.length());
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ Tag suggestion = Tag.fromJSON(jsonArray.optJSONObject(i), siteID);
+ suggestions.add(suggestion);
+ }
+
+ return suggestions;
+ }
+
+ public String getTag() {
+ return StringUtils.notNullStr(tag);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Theme.java b/WordPress/src/main/java/org/wordpress/android/models/Theme.java
new file mode 100644
index 000000000..0ef8cc650
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Theme.java
@@ -0,0 +1,183 @@
+package org.wordpress.android.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+
+public class Theme {
+ public static final String ID = "id";
+ public static final String AUTHOR = "author";
+ public static final String SCREENSHOT = "screenshot";
+ public static final String AUTHOR_URI = "author_uri";
+ public static final String DEMO_URI = "demo_uri";
+ public static final String NAME = "name";
+ public static final String STYLESHEET = "stylesheet";
+ public static final String PRICE = "price";
+ public static final String BLOG_ID = "blogId";
+ public static final String IS_CURRENT = "isCurrent";
+
+ public static final String PREVIEW_URL = "preview_url";
+ public static final String COST = "cost";
+ public static final String DISPLAY = "display";
+
+ private String mId;
+ private String mAuthor;
+ private String mScreenshot;
+ private String mAuthorURI;
+ private String mDemoURI;
+ private String mName;
+ private String mStylesheet;
+ private String mPrice;
+ private String mBlogId;
+ private boolean mIsCurrent;
+
+ public static Theme fromJSONV1_1(JSONObject object) throws JSONException {
+ if (object == null) {
+ return null;
+ } else {
+ String id = object.getString(ID);
+ String author = "";
+ String screenshot = object.getString(SCREENSHOT);
+ String authorURI = "";
+ String demoURI = object.getString(PREVIEW_URL);
+ String name = object.getString(NAME);
+ String stylesheet = "";
+ String price;
+ try {
+ JSONObject cost = object.getJSONObject(COST);
+ price = cost.getString(DISPLAY);
+ } catch (JSONException e) {
+ price = "";
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+
+ return new Theme(id, author, screenshot, authorURI, demoURI, name, stylesheet, price, blogId, false);
+ }
+ }
+
+ public static Theme fromJSONV1_2(JSONObject object) throws JSONException {
+ if (object == null) {
+ return null;
+ } else {
+ String id = object.getString(ID);
+ String author = object.getString(AUTHOR);
+ String screenshot = object.getString(SCREENSHOT);
+ String authorURI = object.getString(AUTHOR_URI);
+ String demoURI = object.getString(DEMO_URI);
+ String name = object.getString(NAME);
+ String stylesheet = object.getString(STYLESHEET);
+ String price;
+ try {
+ price = object.getString(PRICE);
+ } catch (JSONException e) {
+ price = "";
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+
+ return new Theme(id, author, screenshot, authorURI, demoURI, name, stylesheet, price, blogId, false);
+ }
+ }
+
+ public Theme(String id, String author, String screenshot, String authorURI, String demoURI, String name, String stylesheet, String price, String blogId, boolean isCurrent) {
+ setId(id);
+ setAuthor(author);
+ setScreenshot(screenshot);
+ setAuthorURI(authorURI);
+ setDemoURI(demoURI);
+ setName(name);
+ setStylesheet(stylesheet);
+ setPrice(price);
+ setBlogId(blogId);
+ setIsCurrent(isCurrent);
+ }
+
+ public void setId(String id) {
+ mId = id;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public void setAuthor(String author) {
+ mAuthor = author;
+ }
+
+ public String getAuthor() {
+ return mAuthor;
+ }
+
+ public String getScreenshot() {
+ return mScreenshot;
+ }
+
+ public void setScreenshot(String mScreenshot) {
+ this.mScreenshot = mScreenshot;
+ }
+
+ public String getAuthorURI() {
+ return mAuthorURI;
+ }
+
+ public void setAuthorURI(String mAuthorURI) {
+ this.mAuthorURI = mAuthorURI;
+ }
+
+ public String getDemoURI() {
+ return mDemoURI;
+ }
+
+ public void setDemoURI(String mDemoURI) {
+ this.mDemoURI = mDemoURI;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String mName) {
+ this.mName = mName;
+ }
+
+ public String getStylesheet() {
+ return mStylesheet;
+ }
+
+ public void setStylesheet(String mStylesheet) {
+ this.mStylesheet = mStylesheet;
+ }
+
+ public String getPrice() {
+ return mPrice;
+ }
+
+ public void setPrice(String mPrice) {
+ this.mPrice = mPrice;
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(String blogId) {
+ mBlogId = blogId;
+ }
+
+ public boolean getIsCurrent() {
+ return mIsCurrent;
+ }
+
+ public void setIsCurrent(boolean isCurrent) {
+ mIsCurrent = isCurrent;
+ }
+
+ public boolean isPremium() {
+ return !mPrice.equals("");
+ }
+
+ public void save() {
+ WordPress.wpDB.saveTheme(this);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java b/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java
new file mode 100644
index 000000000..238f10fc6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.networking;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.NetworkUtils;
+
+import de.greenrobot.event.EventBus;
+
+/*
+ * global network connection change receiver - declared in the manifest to monitor
+ * android.net.conn.CONNECTIVITY_CHANGE
+ */
+public class ConnectionChangeReceiver extends BroadcastReceiver {
+ private static boolean mIsFirstReceive = true;
+ private static boolean mWasConnected = true;
+ private static boolean mIsEnabled = false; // this value must be synchronized with the ConnectionChangeReceiver
+ // state in our AndroidManifest
+
+ public static class ConnectionChangeEvent {
+ private final boolean mIsConnected;
+ public ConnectionChangeEvent(boolean isConnected) {
+ mIsConnected = isConnected;
+ }
+ public boolean isConnected() {
+ return mIsConnected;
+ }
+ }
+
+ /*
+ * note that onReceive occurs when anything about the connection has changed, not just
+ * when the connection has been lost or restated, so it can happen quite often when the
+ * user is on the move. for this reason we only fire the event the first time onReceive
+ * is called, and afterwards only when we know connection availability has changed
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean isConnected = NetworkUtils.isNetworkAvailable(context);
+ if (mIsFirstReceive || isConnected != mWasConnected) {
+ postConnectionChangeEvent(isConnected);
+ }
+ }
+
+ private static void postConnectionChangeEvent(boolean isConnected) {
+ AppLog.i(T.UTILS, "Connection status changed, isConnected=" + isConnected);
+ mWasConnected = isConnected;
+ mIsFirstReceive = false;
+ EventBus.getDefault().post(new ConnectionChangeEvent(isConnected));
+ }
+
+ public static void setEnabled(Context context, boolean enabled) {
+ if (mIsEnabled == enabled) {
+ return;
+ }
+ mIsEnabled = enabled;
+ AppLog.i(T.UTILS, "ConnectionChangeReceiver.setEnabled " + enabled);
+ int flag = (enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+ ComponentName component = new ComponentName(context, ConnectionChangeReceiver.class);
+ context.getPackageManager().setComponentEnabledSetting(component, flag, PackageManager.DONT_KILL_APP);
+ if (mIsEnabled) {
+ postConnectionChangeEvent(NetworkUtils.isNetworkAvailable(context));
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java b/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java
new file mode 100644
index 000000000..0ed52a0a8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java
@@ -0,0 +1,130 @@
+package org.wordpress.android.networking;
+
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.CrashlyticsUtils;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.Interceptor;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class GravatarApi {
+ public static final String API_BASE_URL = "https://api.gravatar.com/v1/";
+
+ public interface GravatarUploadListener {
+ void onSuccess();
+ void onError();
+ }
+
+ private static OkHttpClient createClient(final String restEndpointUrl) {
+ OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
+
+ //// uncomment the following line to add logcat logging
+ //httpClientBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
+
+ // add oAuth token usage
+ httpClientBuilder.addInterceptor(new Interceptor() {
+ @Override
+ public Response intercept(Interceptor.Chain chain) throws IOException {
+ Request original = chain.request();
+
+ String siteId = AuthenticatorRequest.extractSiteIdFromUrl(restEndpointUrl, original.url()
+ .toString());
+ String token = OAuthAuthenticator.getAccessToken(siteId);
+
+ Request.Builder requestBuilder = original.newBuilder()
+ .header("Authorization", "Bearer " + token)
+ .method(original.method(), original.body());
+
+ Request request = requestBuilder.build();
+ return chain.proceed(request);
+ }
+ });
+
+ return httpClientBuilder.build();
+ }
+
+ public static Request prepareGravatarUpload(String email, File file) {
+ return new Request.Builder()
+ .url(API_BASE_URL + "upload-image")
+ .post(new MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart("account", email)
+ .addFormDataPart("filedata", file.getName(), new StreamingRequest(file))
+ .build())
+ .build();
+ }
+
+ public static void uploadGravatar(final File file, final GravatarUploadListener gravatarUploadListener) {
+ Request request = prepareGravatarUpload(AccountHelper.getDefaultAccount().getEmail(), file);
+
+ createClient(API_BASE_URL).newCall(request).enqueue(
+ new Callback() {
+ @Override
+ public void onResponse(Call call, final Response response) throws IOException {
+ if (!response.isSuccessful()) {
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("network_response_code", response.code());
+
+ // response's body can only be read once so, keep it in a local variable
+ String responseBody;
+
+ try {
+ responseBody = response.body().string();
+ } catch (IOException e) {
+ responseBody = "null";
+ }
+ properties.put("network_response_body", responseBody);
+
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_UNSUCCESSFUL,
+ properties);
+ AppLog.w(AppLog.T.API, "Network call unsuccessful trying to upload Gravatar: " +
+ responseBody);
+ }
+
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ if (response.isSuccessful()) {
+ gravatarUploadListener.onSuccess();
+ } else {
+ gravatarUploadListener.onError();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(okhttp3.Call call, final IOException e) {
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("network_exception_class", e != null ? e.getClass().getCanonicalName() : "null");
+ properties.put("network_exception_message", e != null ? e.getMessage() : "null");
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties);
+ CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC,
+ AppLog.T.API, "Network call failure trying to upload Gravatar!");
+ AppLog.w(AppLog.T.API, "Network call failure trying to upload Gravatar!" + (e != null ?
+ e.getMessage() : "null"));
+
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ gravatarUploadListener.onError();
+ }
+ });
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java
new file mode 100644
index 000000000..326ca9064
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.networking;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.StringUtils;
+
+public class OAuthAuthenticator implements Authenticator {
+ public static String getAccessToken(final String siteId) {
+ String token = AccountHelper.getDefaultAccount().getAccessToken();
+
+ if (siteId != null) {
+ // Get the token for a Jetpack site if needed
+ Blog blog = WordPress.wpDB.getBlogForDotComBlogId(siteId);
+
+ if (blog != null) {
+ String jetpackToken = blog.getApi_key();
+
+ // valid OAuth tokens are 64 chars
+ if (jetpackToken != null && jetpackToken.length() == 64 && !blog.isDotcomFlag()) {
+ token = jetpackToken;
+ }
+ }
+ }
+
+ return token;
+ }
+
+ @Override
+ public void authenticate(final AuthenticatorRequest request) {
+ String siteId = request.getSiteId();
+ String token = getAccessToken(siteId);
+ request.sendWithAccessToken(StringUtils.notNullStr(token));
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java
new file mode 100644
index 000000000..5dc68edb6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java
@@ -0,0 +1,12 @@
+package org.wordpress.android.networking;
+
+public class OAuthAuthenticatorFactory {
+ private static OAuthAuthenticatorFactoryAbstract sFactory;
+
+ public static OAuthAuthenticator instantiate() {
+ if (sFactory == null) {
+ sFactory = new OAuthAuthenticatorFactoryDefault();
+ }
+ return sFactory.make();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java
new file mode 100644
index 000000000..85cff768c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java
@@ -0,0 +1,5 @@
+package org.wordpress.android.networking;
+
+public interface OAuthAuthenticatorFactoryAbstract {
+ public OAuthAuthenticator make();
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java
new file mode 100644
index 000000000..4687d1dca
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.networking;
+
+public class OAuthAuthenticatorFactoryDefault implements OAuthAuthenticatorFactoryAbstract {
+ public OAuthAuthenticator make() {
+ return new OAuthAuthenticator();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java b/WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java
new file mode 100644
index 000000000..c6914b889
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java
@@ -0,0 +1,42 @@
+package org.wordpress.android.networking;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.WebViewActivity;
+
+/**
+ * Display details of a SSL cert
+ */
+public class SSLCertsViewActivity extends WebViewActivity {
+ public static final String CERT_DETAILS_KEYS = "CertDetails";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(getResources().getText(R.string.ssl_certificate_details));
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ }
+ }
+
+ @Override
+ protected void loadContent() {
+ Bundle extras = getIntent().getExtras();
+ if (extras != null && extras.containsKey(CERT_DETAILS_KEYS)) {
+ String certDetails = extras.getString(CERT_DETAILS_KEYS);
+ StringBuilder sb = new StringBuilder("<html><body>");
+ sb.append(certDetails);
+ sb.append("</body></html>");
+ mWebView.loadDataWithBaseURL(null, sb.toString(), "text/html", "utf-8", null);
+ }
+ }
+
+ @Override
+ protected void configureWebView() {
+ mWebView.getSettings().setDefaultTextEncodingName("utf-8");
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java b/WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java
new file mode 100644
index 000000000..172530d51
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java
@@ -0,0 +1,267 @@
+package org.wordpress.android.networking;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.http.SslCertificate;
+import android.os.Bundle;
+
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.GenericCallback;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+import javax.security.auth.x500.X500Principal;
+
+public class SelfSignedSSLCertsManager {
+ private static SelfSignedSSLCertsManager sInstance;
+ private File mLocalTrustStoreFile;
+ private KeyStore mLocalKeyStore;
+ // Used to hold the last self-signed certificate chain that doesn't pass trusting
+ private X509Certificate[] mLastFailureChain;
+
+ private SelfSignedSSLCertsManager(Context ctx) throws IOException, GeneralSecurityException {
+ mLocalTrustStoreFile = new File(ctx.getFilesDir(), "self_signed_certs_truststore.bks");
+ createLocalKeyStoreFile();
+ mLocalKeyStore = loadTrustStore(ctx);
+ }
+
+ public static void askForSslTrust(final Context ctx, final GenericCallback<Void> certificateTrusted) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(ctx);
+ alert.setTitle(ctx.getString(R.string.ssl_certificate_error));
+ alert.setMessage(ctx.getString(R.string.ssl_certificate_ask_trust));
+ alert.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ SelfSignedSSLCertsManager selfSignedSSLCertsManager;
+ try {
+ selfSignedSSLCertsManager = SelfSignedSSLCertsManager.getInstance(ctx);
+ X509Certificate[] certificates = selfSignedSSLCertsManager.getLastFailureChain();
+ AppLog.i(T.NUX, "Add the following certificate to our Certificate Manager: " +
+ Arrays.toString(certificates));
+ selfSignedSSLCertsManager.addCertificates(certificates);
+ } catch (GeneralSecurityException e) {
+ AppLog.e(T.API, e);
+ } catch (IOException e) {
+ AppLog.e(T.API, e);
+ }
+ if (certificateTrusted != null) {
+ certificateTrusted.callback(null);
+ }
+ }
+ }
+ );
+ alert.setNeutralButton(R.string.ssl_certificate_details, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ ActivityLauncher.viewSSLCerts(ctx);
+ }
+ });
+ alert.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ });
+ alert.show();
+ }
+
+ public static synchronized SelfSignedSSLCertsManager getInstance(Context ctx)
+ throws IOException, GeneralSecurityException {
+ if (sInstance == null) {
+ sInstance = new SelfSignedSSLCertsManager(ctx);
+ }
+ return sInstance;
+ }
+
+ public void addCertificates(X509Certificate[] certs) throws IOException, GeneralSecurityException {
+ if (certs == null || certs.length == 0) {
+ return;
+ }
+
+ for (X509Certificate cert : certs) {
+ String alias = hashName(cert.getSubjectX500Principal());
+ mLocalKeyStore.setCertificateEntry(alias, cert);
+ }
+ saveTrustStore();
+ // reset the Volley queue Otherwise new certs are not used
+ WordPress.setupVolleyQueue();
+ }
+
+ public void addCertificate(X509Certificate cert) throws IOException, GeneralSecurityException {
+ if (cert == null) {
+ return;
+ }
+
+ String alias = hashName(cert.getSubjectX500Principal());
+ mLocalKeyStore.setCertificateEntry(alias, cert);
+ saveTrustStore();
+ }
+
+ public KeyStore getLocalKeyStore() {
+ return mLocalKeyStore;
+ }
+
+ private KeyStore loadTrustStore(Context ctx) throws IOException, GeneralSecurityException {
+ KeyStore localTrustStore = KeyStore.getInstance("BKS");
+ InputStream in = new FileInputStream(mLocalTrustStoreFile);
+ try {
+ localTrustStore.load(in, BuildConfig.DB_SECRET.toCharArray());
+ } finally {
+ in.close();
+ }
+ return localTrustStore;
+ }
+
+ private void saveTrustStore() throws IOException, GeneralSecurityException {
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(mLocalTrustStoreFile);
+ mLocalKeyStore.store(out, BuildConfig.DB_SECRET.toCharArray());
+ } finally {
+ if (out!=null){
+ try {
+ out.close();
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create an empty trust store file if missing
+ */
+ private void createLocalKeyStoreFile() throws GeneralSecurityException, IOException {
+ if (!mLocalTrustStoreFile.exists()) {
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(mLocalTrustStoreFile);
+ KeyStore localTrustStore = KeyStore.getInstance("BKS");
+ localTrustStore.load(null, BuildConfig.DB_SECRET.toCharArray());
+ localTrustStore.store(out, BuildConfig.DB_SECRET.toCharArray());
+ } finally {
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+ }
+ }
+ }
+
+ public void emptyLocalKeyStoreFile() {
+ if (mLocalTrustStoreFile.exists()) {
+ mLocalTrustStoreFile.delete();
+ }
+ try {
+ createLocalKeyStoreFile();
+ } catch (GeneralSecurityException e) {
+ AppLog.e(T.API, "Cannot create/initialize local Keystore", e);
+ } catch (IOException e) {
+ AppLog.e(T.API, "Cannot create/initialize local Keystore", e);
+ }
+ }
+
+ private static String hashName(X500Principal principal) {
+ try {
+ byte[] digest = MessageDigest.getInstance("MD5").digest(principal.getEncoded());
+ String result = Integer.toString(leInt(digest), 16);
+ if (result.length() > 8) {
+ StringBuilder buff = new StringBuilder();
+ int padding = 8 - result.length();
+ for (int i = 0; i < padding; i++) {
+ buff.append("0");
+ }
+ buff.append(result);
+
+ return buff.toString();
+ }
+
+ return result;
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static int leInt(byte[] bytes) {
+ int offset = 0;
+ return ((bytes[offset++] & 0xff) << 0)
+ | ((bytes[offset++] & 0xff) << 8)
+ | ((bytes[offset++] & 0xff) << 16)
+ | ((bytes[offset] & 0xff) << 24);
+ }
+
+ public X509Certificate[] getLastFailureChain() {
+ return mLastFailureChain;
+ }
+
+ public void setLastFailureChain(X509Certificate[] lastFaiulreChain) {
+ mLastFailureChain = lastFaiulreChain;
+ }
+
+ public String getLastFailureChainDescription() {
+ return (mLastFailureChain == null || mLastFailureChain.length == 0) ? "" : mLastFailureChain[0].toString();
+ }
+
+ public boolean isCertificateTrusted(SslCertificate cert){
+ if (cert==null)
+ return false;
+
+ Bundle bundle = SslCertificate.saveState(cert);
+ X509Certificate x509Certificate;
+ byte[] bytes = bundle.getByteArray("x509-certificate");
+ if (bytes == null) {
+ AppLog.e(T.API, "Cannot load the SSLCertificate bytes from the bundle!");
+ x509Certificate = null;
+ } else {
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ Certificate certX509 = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
+ x509Certificate = (X509Certificate) certX509;
+ } catch (CertificateException e) {
+ AppLog.e(T.API, "Cannot generate the X509Certificate with the bytes provided", e);
+ x509Certificate = null;
+ }
+ }
+
+ return isCertificateTrusted(x509Certificate);
+ }
+
+ public boolean isCertificateTrusted(X509Certificate x509Certificate){
+ if (x509Certificate==null)
+ return false;
+
+ // Now I have an X509Certificate I can pass to an X509TrustManager for validation.
+ try {
+ String certificateAlias = this.getLocalKeyStore().getCertificateAlias(x509Certificate);
+ if(certificateAlias != null ) {
+ AppLog.w(T.API, "Current certificate " + x509Certificate.getSubjectDN().getName() +" is in KeyStore.");
+ return true;
+ }
+ } catch (KeyStoreException e) {
+ AppLog.e(T.API, "Cannot check if the certificate is in KeyStore. Seems that Keystore is not initialized.", e);
+ }
+
+ AppLog.w(T.API, "Current certificate " + x509Certificate.getSubjectDN().getName() +" is NOT in KeyStore.");
+ return false;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java b/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java
new file mode 100644
index 000000000..60c6880fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java
@@ -0,0 +1,41 @@
+package org.wordpress.android.networking;
+
+import java.io.File;
+import java.io.IOException;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.internal.Util;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+public class StreamingRequest extends RequestBody {
+ public static final int CHUNK_SIZE = 2048;
+
+ private final File mFile;
+
+ public StreamingRequest(File file) {
+ mFile = file;
+ }
+
+ @Override
+ public MediaType contentType() {
+ return MediaType.parse("multipart/form-data");
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ Source source = null;
+ try {
+ source = Okio.source(mFile);
+
+ while (source.read(sink.buffer(), CHUNK_SIZE) != -1) {
+ sink.flush();
+ }
+ } finally {
+ Util.closeQuietly(source);
+ }
+ }
+};
+
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java b/WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java
new file mode 100644
index 000000000..b0afaec72
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java
@@ -0,0 +1,287 @@
+package org.wordpress.android.networking;
+
+import android.content.Context;
+import android.util.Base64;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+import com.android.volley.toolbox.HttpStack;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPUrlUtils;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+
+/**
+ * An {@link HttpStack} based on the code of {@link com.android.volley.toolbox.HurlStack} that internally
+ * uses a {@link HttpURLConnection}.
+ *
+ * This implementation of {@link HttpStack} internally initializes {@link SelfSignedSSLCertsManager} in a secondary
+ * thread since initialization could take a few seconds.
+ */
+public class WPDelayedHurlStack implements HttpStack {
+ private static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ private SSLSocketFactory mSslSocketFactory;
+ private final Blog mCurrentBlog;
+ private final Context mCtx;
+ private final Object monitor = new Object();
+
+ public WPDelayedHurlStack(final Context ctx, final Blog currentBlog) {
+ mCurrentBlog = currentBlog;
+ mCtx = ctx;
+
+ // initializes SelfSignedSSLCertsManager in a separate thread.
+ Thread sslContextInitializer = new Thread() {
+ @Override
+ public void run() {
+ try {
+ TrustManager[] trustAllowedCerts = new TrustManager[]{
+ new WPTrustManager(SelfSignedSSLCertsManager.getInstance(ctx).getLocalKeyStore())
+ };
+ SSLContext context = SSLContext.getInstance("SSL");
+ context.init(null, trustAllowedCerts, new SecureRandom());
+ mSslSocketFactory = context.getSocketFactory();
+ } catch (NoSuchAlgorithmException e) {
+ AppLog.e(T.API, e);
+ } catch (KeyManagementException e) {
+ AppLog.e(T.API, e);
+ } catch (GeneralSecurityException e) {
+ AppLog.e(T.API, e);
+ } catch (IOException e) {
+ AppLog.e(T.API, e);
+ }
+ }
+ };
+ sslContextInitializer.start();
+ }
+
+
+ private static boolean hasAuthorizationHeader(Request request) {
+ try {
+ if (request.getHeaders() != null && request.getHeaders().containsKey("Authorization")) {
+ return true;
+ }
+ } catch (AuthFailureError e) {
+ // nope
+ }
+
+ return false;
+ }
+
+ @Override
+ public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ if (request.getUrl() != null) {
+ if (!WPUrlUtils.isWordPressCom(request.getUrl()) && mCurrentBlog != null
+ && mCurrentBlog.hasValidHTTPAuthCredentials()) {
+ String creds = String.format("%s:%s", mCurrentBlog.getHttpuser(), mCurrentBlog.getHttppassword());
+ String auth = "Basic " + Base64.encodeToString(creds.getBytes(), Base64.DEFAULT);
+ additionalHeaders.put("Authorization", auth);
+ }
+
+ /**
+ * Add the Authorization header to access private WP.com files.
+ *
+ * Note: Additional headers have precedence over request headers, so add Authorization only it it's not already
+ * available in the request.
+ *
+ */
+ if (WPUrlUtils.safeToAddWordPressComAuthToken(request.getUrl()) && mCtx != null
+ && AccountHelper.isSignedInWordPressDotCom() && !hasAuthorizationHeader(request)) {
+ additionalHeaders.put("Authorization", "Bearer " + AccountHelper.getDefaultAccount().getAccessToken());
+ }
+ }
+
+ additionalHeaders.put("User-Agent", WordPress.getUserAgent());
+
+ String url = request.getUrl();
+
+ // Ensure that an HTTPS request is made to wpcom when Authorization is set.
+ if (additionalHeaders.containsKey("Authorization") || hasAuthorizationHeader(request)) {
+ url = UrlUtils.makeHttps(url);
+ }
+
+ HashMap<String, String> map = new HashMap<String, String>();
+ map.putAll(request.getHeaders());
+ map.putAll(additionalHeaders);
+
+ URL parsedUrl = new URL(url);
+ HttpURLConnection connection = openConnection(parsedUrl, request);
+ for (String headerName : map.keySet()) {
+ connection.addRequestProperty(headerName, map.get(headerName));
+ }
+ setConnectionParametersForRequest(connection, request);
+ // Initialize HttpResponse with data from the HttpURLConnection.
+ ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+ int responseCode = connection.getResponseCode();
+ if (responseCode == -1) {
+ // -1 is returned by getResponseCode() if the response code could not be retrieved.
+ // Signal to the caller that something was wrong with the connection.
+ throw new IOException("Could not retrieve response code from HttpUrlConnection.");
+ }
+ StatusLine responseStatus = new BasicStatusLine(protocolVersion,
+ connection.getResponseCode(), connection.getResponseMessage());
+ BasicHttpResponse response = new BasicHttpResponse(responseStatus);
+ response.setEntity(entityFromConnection(connection));
+ for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
+ if (header.getKey() != null) {
+ Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
+ response.addHeader(h);
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
+ * @param connection
+ * @return an HttpEntity populated with data from <code>connection</code>.
+ */
+ private static HttpEntity entityFromConnection(HttpURLConnection connection) {
+ BasicHttpEntity entity = new BasicHttpEntity();
+ InputStream inputStream;
+ try {
+ inputStream = connection.getInputStream();
+ } catch (IOException ioe) {
+ inputStream = connection.getErrorStream();
+ }
+ entity.setContent(inputStream);
+ entity.setContentLength(connection.getContentLength());
+ entity.setContentEncoding(connection.getContentEncoding());
+ entity.setContentType(connection.getContentType());
+ return entity;
+ }
+
+ /**
+ * Create an {@link HttpURLConnection} for the specified {@code url}.
+ */
+ protected HttpURLConnection createConnection(URL url) throws IOException {
+ // Check that the custom SslSocketFactory is not null on HTTPS connections
+ if (UrlUtils.isHttps(url) && !WPUrlUtils.isWordPressCom(url)
+ && !WPUrlUtils.isGravatar(url)) {
+ // WordPress.com doesn't need the custom mSslSocketFactory
+ synchronized (monitor) {
+ while (mSslSocketFactory == null) {
+ try {
+ monitor.wait(500);
+ } catch (InterruptedException e) {
+ // we can't do much here.
+ }
+ }
+ }
+ }
+
+ return (HttpURLConnection) url.openConnection();
+ }
+
+ /**
+ * Opens an {@link HttpURLConnection} with parameters.
+ * @param url
+ * @return an open connection
+ * @throws IOException
+ */
+ private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
+ HttpURLConnection connection = createConnection(url);
+
+ int timeoutMs = request.getTimeoutMs();
+ connection.setConnectTimeout(timeoutMs);
+ connection.setReadTimeout(timeoutMs);
+ connection.setUseCaches(false);
+ connection.setDoInput(true);
+
+ // use caller-provided custom SslSocketFactory, if any, for HTTPS
+ if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
+ ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory);
+ }
+
+ return connection;
+ }
+
+ @SuppressWarnings("deprecation")
+ /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection,
+ Request<?> request) throws IOException, AuthFailureError {
+ switch (request.getMethod()) {
+ case Method.DEPRECATED_GET_OR_POST:
+ // This is the deprecated way that needs to be handled for backwards compatibility.
+ // If the request's post body is null, then the assumption is that the request is
+ // GET. Otherwise, it is assumed that the request is a POST.
+ byte[] postBody = request.getPostBody();
+ if (postBody != null) {
+ // Prepare output. There is no need to set Content-Length explicitly,
+ // since this is handled by HttpURLConnection using the size of the prepared
+ // output stream.
+ connection.setDoOutput(true);
+ connection.setRequestMethod("POST");
+ connection.addRequestProperty(HEADER_CONTENT_TYPE,
+ request.getPostBodyContentType());
+ DataOutputStream out = new DataOutputStream(connection.getOutputStream());
+ out.write(postBody);
+ out.close();
+ }
+ break;
+ case Method.GET:
+ // Not necessary to set the request method because connection defaults to GET but
+ // being explicit here.
+ connection.setRequestMethod("GET");
+ break;
+ case Method.DELETE:
+ connection.setRequestMethod("DELETE");
+ break;
+ case Method.POST:
+ connection.setRequestMethod("POST");
+ addBodyIfExists(connection, request);
+ break;
+ case Method.PUT:
+ connection.setRequestMethod("PUT");
+ addBodyIfExists(connection, request);
+ break;
+ default:
+ throw new IllegalStateException("Unknown method type.");
+ }
+ }
+
+ private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
+ throws IOException, AuthFailureError {
+ byte[] body = request.getBody();
+ if (body != null) {
+ connection.setDoOutput(true);
+ connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ DataOutputStream out = new DataOutputStream(connection.getOutputStream());
+ out.write(body);
+ out.close();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java b/WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java
new file mode 100644
index 000000000..8f7bb9104
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java
@@ -0,0 +1,119 @@
+package org.wordpress.android.networking;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class WPTrustManager implements X509TrustManager {
+ private X509TrustManager defaultTrustManager;
+ private X509TrustManager localTrustManager;
+ private X509Certificate[] acceptedIssuers;
+
+ public WPTrustManager(KeyStore localKeyStore) {
+ try {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init((KeyStore) null);
+
+ defaultTrustManager = findX509TrustManager(tmf);
+ if (defaultTrustManager == null) {
+ throw new IllegalStateException("Couldn't find X509TrustManager");
+ }
+
+ localTrustManager = new LocalStoreX509TrustManager(localKeyStore);
+
+ List<X509Certificate> allIssuers = new ArrayList<X509Certificate>();
+ Collections.addAll(allIssuers, defaultTrustManager.getAcceptedIssuers());
+ Collections.addAll(allIssuers, localTrustManager.getAcceptedIssuers());
+ acceptedIssuers = allIssuers.toArray(new X509Certificate[allIssuers.size()]);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ private static X509TrustManager findX509TrustManager(TrustManagerFactory tmf) {
+ TrustManager tms[] = tmf.getTrustManagers();
+ for (int i = 0; i < tms.length; i++) {
+ if (tms[i] instanceof X509TrustManager) {
+ return (X509TrustManager) tms[i];
+ }
+ }
+ return null;
+ }
+
+
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ try {
+ defaultTrustManager.checkClientTrusted(chain, authType);
+ } catch (CertificateException ce) {
+ localTrustManager.checkClientTrusted(chain, authType);
+ }
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ try {
+ defaultTrustManager.checkServerTrusted(chain, authType);
+ } catch (CertificateException ce) {
+ localTrustManager.checkServerTrusted(chain, authType);
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return acceptedIssuers;
+ }
+
+ static class LocalStoreX509TrustManager implements X509TrustManager {
+ private X509TrustManager trustManager;
+
+ LocalStoreX509TrustManager(KeyStore localKeyStore) {
+ try {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(localKeyStore);
+
+ trustManager = findX509TrustManager(tmf);
+ if (trustManager == null) {
+ throw new IllegalStateException("Couldn't find X509TrustManager");
+ }
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ trustManager.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ try {
+ trustManager.checkServerTrusted(chain, authType);
+ } catch (CertificateException e) {
+ AppLog.e(T.API, "Cannot trust the certificate with the local trust manager...", e);
+ try {
+ SelfSignedSSLCertsManager.getInstance(null).setLastFailureChain(chain);
+ } catch (GeneralSecurityException e1) {
+ } catch (IOException e1) {
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return trustManager.getAcceptedIssuers();
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityId.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityId.java
new file mode 100644
index 000000000..23c65c2af
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityId.java
@@ -0,0 +1,57 @@
+package org.wordpress.android.ui;
+
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public enum ActivityId {
+ UNKNOWN("Unknown"),
+ READER("Reader"),
+ NOTIFICATIONS("Notifications"),
+ ME("Me"),
+ MY_SITE("My Site"),
+ POSTS("Post List"),
+ MEDIA("Media Library"),
+ PAGES("Page List"),
+ COMMENTS("Comments"),
+ COMMENT_DETAIL("Comment Detail"),
+ COMMENT_EDITOR("Comment Editor"),
+ SITE_PICKER("Site Picker"),
+ THEMES("Themes"),
+ STATS("Stats"),
+ STATS_VIEW_ALL("Stats View All"),
+ STATS_POST_DETAILS("Stats Post Details"),
+ VIEW_SITE("View Site"),
+ POST_EDITOR("Post Editor"),
+ LOGIN("Login Screen"),
+ HELP_SCREEN("Help Screen");
+
+ private final String mStringValue;
+
+ private ActivityId(final String stringValue) {
+ mStringValue = stringValue;
+ }
+
+ public String toString() {
+ return mStringValue;
+ }
+
+ public static void trackLastActivity(ActivityId activityId) {
+ AppLog.v(T.UTILS, "trackLastActivity, activityId: " + activityId);
+ if (activityId != null) {
+ AppPrefs.setLastActivityStr(activityId.name());
+ }
+ }
+
+ public static ActivityId getActivityIdFromName(String activityString) {
+ if (activityString == null) {
+ return ActivityId.UNKNOWN;
+ }
+ try {
+ return ActivityId.valueOf(activityString);
+ } catch (IllegalArgumentException e) {
+ // default to UNKNOWN in case the activityString is bogus
+ return ActivityId.UNKNOWN;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
new file mode 100644
index 000000000..7ba24cb6a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
@@ -0,0 +1,304 @@
+package org.wordpress.android.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.networking.SSLCertsViewActivity;
+import org.wordpress.android.networking.SelfSignedSSLCertsManager;
+import org.wordpress.android.ui.accounts.HelpActivity;
+import org.wordpress.android.ui.accounts.NewBlogActivity;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.ui.accounts.login.MagicLinkSignInActivity;
+import org.wordpress.android.ui.comments.CommentsActivity;
+import org.wordpress.android.ui.main.SitePickerActivity;
+import org.wordpress.android.ui.media.MediaBrowserActivity;
+import org.wordpress.android.ui.media.WordPressMediaUtils;
+import org.wordpress.android.ui.people.PeopleManagementActivity;
+import org.wordpress.android.ui.plans.PlansActivity;
+import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.ui.posts.PostPreviewActivity;
+import org.wordpress.android.ui.posts.PostsListActivity;
+import org.wordpress.android.ui.prefs.AccountSettingsActivity;
+import org.wordpress.android.ui.prefs.AppSettingsActivity;
+import org.wordpress.android.ui.prefs.BlogPreferencesActivity;
+import org.wordpress.android.ui.prefs.MyProfileActivity;
+import org.wordpress.android.ui.prefs.SiteSettingsInterface;
+import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsActivity;
+import org.wordpress.android.ui.stats.StatsActivity;
+import org.wordpress.android.ui.stats.StatsConstants;
+import org.wordpress.android.ui.stats.StatsSingleItemDetailsActivity;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.themes.ThemeBrowserActivity;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.HelpshiftHelper.Tag;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.passcodelock.AppLockManager;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+public class ActivityLauncher {
+ public static void showSitePickerForResult(Activity activity, int blogLocalTableId) {
+ Intent intent = new Intent(activity, SitePickerActivity.class);
+ intent.putExtra(SitePickerActivity.KEY_LOCAL_ID, blogLocalTableId);
+ ActivityOptionsCompat options = ActivityOptionsCompat.makeCustomAnimation(
+ activity,
+ R.anim.activity_slide_in_from_left,
+ R.anim.do_nothing);
+ ActivityCompat.startActivityForResult(activity, intent, RequestCodes.SITE_PICKER, options.toBundle());
+ }
+
+ public static void viewBlogStats(Context context, int blogLocalTableId) {
+ if (blogLocalTableId == 0) return;
+
+ Intent intent = new Intent(context, StatsActivity.class);
+ intent.putExtra(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, blogLocalTableId);
+ context.startActivity(intent);
+ }
+
+ public static void viewBlogPlans(Context context, int blogLocalTableId) {
+ Intent intent = new Intent(context, PlansActivity.class);
+ intent.putExtra(PlansActivity.ARG_LOCAL_TABLE_BLOG_ID, blogLocalTableId);
+ context.startActivity(intent);
+ }
+
+ public static void viewCurrentBlogPosts(Context context) {
+ Intent intent = new Intent(context, PostsListActivity.class);
+ context.startActivity(intent);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_POSTS);
+ }
+
+ public static void viewCurrentBlogMedia(Context context) {
+ Intent intent = new Intent(context, MediaBrowserActivity.class);
+ context.startActivity(intent);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_MEDIA_LIBRARY);
+ }
+
+ public static void viewCurrentBlogPages(Context context) {
+ Intent intent = new Intent(context, PostsListActivity.class);
+ intent.putExtra(PostsListActivity.EXTRA_VIEW_PAGES, true);
+ context.startActivity(intent);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_PAGES);
+ }
+
+ public static void viewCurrentBlogComments(Context context) {
+ Intent intent = new Intent(context, CommentsActivity.class);
+ context.startActivity(intent);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_COMMENTS);
+ }
+
+ public static void viewCurrentBlogThemes(Context context) {
+ if (ThemeBrowserActivity.isAccessible()) {
+ Intent intent = new Intent(context, ThemeBrowserActivity.class);
+ context.startActivity(intent);
+ }
+ }
+
+ public static void viewCurrentBlogPeople(Context context) {
+ Intent intent = new Intent(context, PeopleManagementActivity.class);
+ context.startActivity(intent);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_PEOPLE_MANAGEMENT);
+ }
+
+ public static void viewBlogSettingsForResult(Activity activity, Blog blog) {
+ if (blog == null) return;
+
+ Intent intent = new Intent(activity, BlogPreferencesActivity.class);
+ intent.putExtra(BlogPreferencesActivity.ARG_LOCAL_BLOG_ID, blog.getLocalTableBlogId());
+ activity.startActivityForResult(intent, RequestCodes.BLOG_SETTINGS);
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.OPENED_BLOG_SETTINGS, blog);
+ }
+
+ public static void viewCurrentSite(Context context, Blog blog) {
+ if (blog == null) {
+ Toast.makeText(context, context.getText(R.string.blog_not_found), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String siteUrl = blog.getAlternativeHomeUrl();
+ Uri uri = Uri.parse(siteUrl);
+
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_VIEW_SITE);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ context.startActivity(intent);
+ AppLockManager.getInstance().setExtendedTimeout();
+ }
+
+ public static void viewBlogAdmin(Context context, Blog blog) {
+ if (blog == null) {
+ Toast.makeText(context, context.getText(R.string.blog_not_found), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String adminUrl = blog.getAdminUrl();
+ Uri uri = Uri.parse(adminUrl);
+
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.OPENED_VIEW_ADMIN, blog);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ context.startActivity(intent);
+ AppLockManager.getInstance().setExtendedTimeout();
+ }
+
+ public static void viewPostPreviewForResult(Activity activity, Post post, boolean isPage) {
+ if (post == null) return;
+
+ Intent intent = new Intent(activity, PostPreviewActivity.class);
+ intent.putExtra(PostPreviewActivity.ARG_LOCAL_POST_ID, post.getLocalTablePostId());
+ intent.putExtra(PostPreviewActivity.ARG_LOCAL_BLOG_ID, post.getLocalTableBlogId());
+ intent.putExtra(PostPreviewActivity.ARG_IS_PAGE, isPage);
+ activity.startActivityForResult(intent, RequestCodes.PREVIEW_POST);
+ }
+
+ public static void addNewBlogPostOrPageForResult(Activity context, Blog blog, boolean isPage) {
+ if (blog == null) return;
+
+ // Create a new post object and assign default settings
+ Post newPost = new Post(blog.getLocalTableBlogId(), isPage);
+ newPost.setCategories("[" + SiteSettingsInterface.getDefaultCategory(context) +"]");
+ newPost.setPostFormat(SiteSettingsInterface.getDefaultFormat(context));
+ WordPress.wpDB.savePost(newPost);
+
+ Intent intent = new Intent(context, EditPostActivity.class);
+ intent.putExtra(EditPostActivity.EXTRA_POSTID, newPost.getLocalTablePostId());
+ intent.putExtra(EditPostActivity.EXTRA_IS_PAGE, isPage);
+ intent.putExtra(EditPostActivity.EXTRA_IS_NEW_POST, true);
+ context.startActivityForResult(intent, RequestCodes.EDIT_POST);
+ }
+
+ public static void editBlogPostOrPageForResult(Activity activity, long postOrPageId, boolean isPage) {
+ Intent intent = new Intent(activity.getApplicationContext(), EditPostActivity.class);
+ intent.putExtra(EditPostActivity.EXTRA_POSTID, postOrPageId);
+ intent.putExtra(EditPostActivity.EXTRA_IS_PAGE, isPage);
+ activity.startActivityForResult(intent, RequestCodes.EDIT_POST);
+ }
+
+ /*
+ * Load the post preview as an authenticated URL so stats aren't bumped
+ */
+ public static void browsePostOrPage(Context context, Blog blog, Post post) {
+ if (blog == null || post == null || TextUtils.isEmpty(post.getPermaLink())) return;
+
+ // always add the preview parameter to avoid bumping stats when viewing posts
+ String url = UrlUtils.appendUrlParameter(post.getPermaLink(), "preview", "true");
+ WPWebViewActivity.openUrlByUsingBlogCredentials(context, blog, post, url);
+ }
+
+ public static void addMedia(Activity activity) {
+ WordPressMediaUtils.launchPictureLibrary(activity);
+ }
+
+ public static void viewMyProfile(Context context) {
+ Intent intent = new Intent(context, MyProfileActivity.class);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_MY_PROFILE);
+ context.startActivity(intent);
+ }
+
+ public static void viewAccountSettings(Context context) {
+ Intent intent = new Intent(context, AccountSettingsActivity.class);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_ACCOUNT_SETTINGS);
+ context.startActivity(intent);
+ }
+
+ public static void viewAppSettings(Activity activity) {
+ Intent intent = new Intent(activity, AppSettingsActivity.class);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_APP_SETTINGS);
+ activity.startActivityForResult(intent, RequestCodes.APP_SETTINGS);
+ }
+
+ public static void viewNotificationsSettings(Activity activity) {
+ Intent intent = new Intent(activity, NotificationsSettingsActivity.class);
+ activity.startActivity(intent);
+ }
+
+ public static void viewHelpAndSupport(Context context, Tag origin) {
+ Intent intent = new Intent(context, HelpActivity.class);
+ intent.putExtra(HelpshiftHelper.ORIGIN_KEY, origin);
+ context.startActivity(intent);
+ }
+
+ public static void viewSSLCerts(Context context) {
+ try {
+ Intent intent = new Intent(context, SSLCertsViewActivity.class);
+ SelfSignedSSLCertsManager selfSignedSSLCertsManager = SelfSignedSSLCertsManager.getInstance(context);
+ String lastFailureChainDescription =
+ selfSignedSSLCertsManager.getLastFailureChainDescription().replaceAll("\n", "<br/>");
+ intent.putExtra(SSLCertsViewActivity.CERT_DETAILS_KEYS, lastFailureChainDescription);
+ context.startActivity(intent);
+ } catch (GeneralSecurityException e) {
+ AppLog.e(AppLog.T.API, e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.API, e);
+ }
+ }
+
+ public static void newBlogForResult(Activity activity) {
+ Intent intent = new Intent(activity, NewBlogActivity.class);
+ intent.putExtra(NewBlogActivity.KEY_START_MODE, NewBlogActivity.CREATE_BLOG);
+ activity.startActivityForResult(intent, RequestCodes.CREATE_BLOG);
+ }
+
+ public static void showSignInForResult(Activity activity) {
+ if (shouldShowMagicLinksLogin(activity)) {
+ Intent intent = new Intent(activity, MagicLinkSignInActivity.class);
+ activity.startActivityForResult(intent, RequestCodes.ADD_ACCOUNT);
+ } else {
+ Intent intent = new Intent(activity, SignInActivity.class);
+ activity.startActivityForResult(intent, RequestCodes.ADD_ACCOUNT);
+ }
+ }
+
+ public static void viewStatsSinglePostDetails(Context context, Post post, boolean isPage) {
+ if (post == null) return;
+
+ int remoteBlogId = WordPress.wpDB.getRemoteBlogIdForLocalTableBlogId(post.getLocalTableBlogId());
+ PostModel postModel = new PostModel(
+ Integer.toString(remoteBlogId),
+ post.getRemotePostId(),
+ post.getTitle(),
+ post.getLink(),
+ isPage ? StatsConstants.ITEM_TYPE_PAGE : StatsConstants.ITEM_TYPE_POST);
+ viewStatsSinglePostDetails(context, postModel);
+ }
+
+ public static void viewStatsSinglePostDetails(Context context, PostModel post) {
+ if (post == null) return;
+
+ Intent statsPostViewIntent = new Intent(context, StatsSingleItemDetailsActivity.class);
+ statsPostViewIntent.putExtra(StatsSingleItemDetailsActivity.ARG_REMOTE_BLOG_ID, post.getBlogID());
+ statsPostViewIntent.putExtra(StatsSingleItemDetailsActivity.ARG_REMOTE_ITEM_ID, post.getItemID());
+ statsPostViewIntent.putExtra(StatsSingleItemDetailsActivity.ARG_REMOTE_ITEM_TYPE, post.getPostType());
+ statsPostViewIntent.putExtra(StatsSingleItemDetailsActivity.ARG_ITEM_TITLE, post.getTitle());
+ statsPostViewIntent.putExtra(StatsSingleItemDetailsActivity.ARG_ITEM_URL, post.getUrl());
+ context.startActivity(statsPostViewIntent);
+ }
+
+ public static void addSelfHostedSiteForResult(Activity activity) {
+ Intent intent = new Intent(activity, SignInActivity.class);
+ intent.putExtra(SignInActivity.EXTRA_START_FRAGMENT, SignInActivity.ADD_SELF_HOSTED_BLOG);
+ activity.startActivityForResult(intent, RequestCodes.ADD_ACCOUNT);
+ }
+
+ public static boolean shouldShowMagicLinksLogin(Activity activity) {
+ boolean isMagicLinksEnabled = false;
+
+ return isMagicLinksEnabled && WPActivityUtils.isEmailClientAvailable(activity);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java
new file mode 100644
index 000000000..bbfe83860
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java
@@ -0,0 +1,230 @@
+package org.wordpress.android.ui;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+public class AddQuickPressShortcutActivity extends ListActivity {
+ static final int ADD_ACCOUNT_REQUEST = 0;
+
+ public List<Map<String, Object>> accounts;
+ public String[] blogNames;
+ public int[] accountIDs;
+ public String[] accountUsers;
+ public String[] blavatars;
+ public List<String> accountNames = new Vector<String>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.add_quickpress_shortcut);
+ setTitle(getResources().getText(R.string.quickpress_window_title));
+
+ displayAccounts();
+ }
+
+ private void displayAccounts() {
+ accounts = WordPress.wpDB.getVisibleBlogs();
+
+ ListView listView = (ListView) findViewById(android.R.id.list);
+
+ ImageView iv = new ImageView(this);
+ iv.setBackgroundDrawable(getResources().getDrawable(R.drawable.list_divider));
+ listView.addFooterView(iv);
+ listView.setVerticalFadingEdgeEnabled(false);
+ listView.setVerticalScrollBarEnabled(true);
+
+ if (accounts.size() > 0) {
+ ScrollView sv = new ScrollView(this);
+ sv.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
+ LinearLayout layout = new LinearLayout(this);
+ layout.setPadding(10, 10, 10, 0);
+ layout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
+
+ layout.setOrientation(LinearLayout.VERTICAL);
+
+ blogNames = new String[accounts.size()];
+ accountIDs = new int[accounts.size()];
+ accountUsers = new String[accounts.size()];
+ blavatars = new String[accounts.size()];
+ int validBlogCtr = 0;
+ for (int i = 0; i < accounts.size(); i++) {
+ Map<String, Object> curHash = accounts.get(i);
+ blogNames[validBlogCtr] = curHash.get("blogName").toString();
+ accountUsers[validBlogCtr] = curHash.get("username").toString();
+ accountIDs[validBlogCtr] = (Integer)curHash.get("id");
+ String url = curHash.get("url").toString();
+ if (url != null) {
+ blavatars[validBlogCtr] = GravatarUtils.blavatarFromUrl(url, 60);
+ } else {
+ blavatars[validBlogCtr] = "";
+ }
+ accountNames.add(validBlogCtr, blogNames[i]);
+ validBlogCtr++;
+ }
+
+ if (validBlogCtr < accounts.size()){
+ accounts = WordPress.wpDB.getVisibleBlogs();
+ }
+
+ setListAdapter(new HomeListAdapter());
+
+ listView.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View row, int position, long id) {
+ AddQuickPressShortcutActivity.this.buildDialog(position);
+ }
+ });
+
+ if(accounts.size() == 1) {
+ AddQuickPressShortcutActivity.this.buildDialog(0);
+ }
+
+ } else {
+ // no account, load new account view
+ Intent i = new Intent(AddQuickPressShortcutActivity.this, SignInActivity.class);
+ startActivityForResult(i, ADD_ACCOUNT_REQUEST);
+ }
+ }
+
+ private void buildDialog(int positionParam) {
+ final int position = positionParam;
+
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(AddQuickPressShortcutActivity.this);
+ dialogBuilder.setTitle(R.string.quickpress_add_alert_title);
+
+ final EditText quickPressShortcutName = new EditText(AddQuickPressShortcutActivity.this);
+ quickPressShortcutName.setText("QP " + StringUtils.unescapeHTML(accountNames.get(position)));
+ dialogBuilder.setView(quickPressShortcutName);
+
+ dialogBuilder.setPositiveButton(R.string.add, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (TextUtils.isEmpty(quickPressShortcutName.getText())) {
+ Toast t = Toast.makeText(AddQuickPressShortcutActivity.this, R.string.quickpress_add_error, Toast.LENGTH_LONG);
+ t.show();
+ } else {
+ Intent shortcutIntent = new Intent(getApplicationContext(), EditPostActivity.class);
+ shortcutIntent.setAction(Intent.ACTION_MAIN);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ shortcutIntent.putExtra(EditPostActivity.EXTRA_QUICKPRESS_BLOG_ID, accountIDs[position]);
+ shortcutIntent.putExtra(EditPostActivity.EXTRA_IS_QUICKPRESS, true);
+
+ Intent addIntent = new Intent();
+ addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, quickPressShortcutName.getText().toString());
+ addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext
+ (AddQuickPressShortcutActivity.this, R.mipmap.app_icon));
+
+ WordPress.wpDB.addQuickPressShortcut(accountIDs[position], quickPressShortcutName.getText().toString());
+
+ if (WordPress.currentBlog == null) {
+ WordPress.currentBlog = WordPress.wpDB.instantiateBlogByLocalId(accountIDs[position]);
+ WordPress.wpDB.updateLastBlogId(accountIDs[position]);
+ }
+
+ addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
+ AddQuickPressShortcutActivity.this.sendBroadcast(addIntent);
+ AddQuickPressShortcutActivity.this.finish();
+ }
+ }
+ });
+ dialogBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ // just let the dialog close
+ public void onClick(DialogInterface dialog, int which) {}
+ });
+
+ dialogBuilder.setCancelable(false);
+ dialogBuilder.create().show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ switch (requestCode) {
+ case ADD_ACCOUNT_REQUEST:
+ if (resultCode == RESULT_OK) {
+ accounts = WordPress.wpDB.getVisibleBlogs();
+ if (accounts.size() > 0) {
+ displayAccounts();
+ break;
+ }
+ }
+ finish();
+ break;
+ }
+ }
+
+ protected class HomeListAdapter extends BaseAdapter {
+ public HomeListAdapter() {
+ }
+
+ public int getCount() {
+ return accounts.size();
+ }
+
+ public Object getItem(int position) {
+ return position;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ RelativeLayout view = (RelativeLayout) convertView;
+ if (view == null) {
+ LayoutInflater inflater = getLayoutInflater();
+ view = (RelativeLayout)inflater.inflate(R.layout.home_row, parent, false);
+ }
+ String username = accountUsers[position];
+ view.setId(Integer.valueOf(accountIDs[position]));
+
+ TextView blogName = (TextView)view.findViewById(R.id.blogName);
+ TextView blogUsername = (TextView)view.findViewById(R.id.blogUser);
+ NetworkImageView blavatar = (NetworkImageView)view.findViewById(R.id.blavatar);
+
+ blogName.setText(
+ StringUtils.unescapeHTML(blogNames[position]));
+ blogUsername.setText(
+ StringUtils.unescapeHTML(username));
+ blavatar.setErrorImageResId(R.drawable.blavatar_placeholder);
+ blavatar.setImageUrl(blavatars[position], WordPress.imageLoader);
+
+ return view;
+
+ }
+
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/AppLogViewerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/AppLogViewerActivity.java
new file mode 100644
index 000000000..89027f7dd
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/AppLogViewerActivity.java
@@ -0,0 +1,164 @@
+package org.wordpress.android.ui;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.ArrayList;
+
+/**
+ * views the activity log (see utils/AppLog.java)
+ */
+public class AppLogViewerActivity extends AppCompatActivity {
+ private static final int ID_SHARE = 1;
+ private static final int ID_COPY_TO_CLIPBOARD = 2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.logviewer_activity);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ final ListView listView = (ListView) findViewById(android.R.id.list);
+ listView.setAdapter(new LogAdapter(this));
+ }
+
+ private class LogAdapter extends BaseAdapter {
+ private final ArrayList<String> mEntries;
+ private final LayoutInflater mInflater;
+
+ private LogAdapter(Context context) {
+ mEntries = AppLog.toHtmlList(context);
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return mEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final LogViewHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.logviewer_listitem, parent, false);
+ holder = new LogViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (LogViewHolder) convertView.getTag();
+ }
+
+ // take the header lines (app version & device name) into account or else the
+ // line numbers shown here won't match the line numbers when the log is shared
+ int lineNum = position - AppLog.HEADER_LINE_COUNT + 1;
+ if (lineNum > 0) {
+ holder.txtLineNumber.setText(String.format("%02d", lineNum));
+ holder.txtLineNumber.setVisibility(View.VISIBLE);
+ } else {
+ holder.txtLineNumber.setVisibility(View.GONE);
+ }
+
+ holder.txtLogEntry.setText(Html.fromHtml(mEntries.get(position)));
+
+ return convertView;
+ }
+
+ private class LogViewHolder {
+ private final TextView txtLineNumber;
+ private final TextView txtLogEntry;
+
+ LogViewHolder(View view) {
+ txtLineNumber = (TextView) view.findViewById(R.id.text_line);
+ txtLogEntry = (TextView) view.findViewById(R.id.text_log);
+ }
+ }
+ }
+
+ private void shareAppLog() {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, AppLog.toPlainText(this));
+ intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " " + getTitle());
+ try {
+ startActivity(Intent.createChooser(intent, getString(R.string.reader_btn_share)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ ToastUtils.showToast(this, R.string.reader_toast_err_share_intent);
+ }
+ }
+
+ private void copyAppLogToClipboard() {
+ try {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(ClipData.newPlainText("AppLog", AppLog.toPlainText(this)));
+ ToastUtils.showToast(this, R.string.logs_copied_to_clipboard);
+ } catch (Exception e) {
+ AppLog.e(T.UTILS, e);
+ ToastUtils.showToast(this, R.string.error_copy_to_clipboard);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ // Copy to clipboard button
+ MenuItem item = menu.add(Menu.NONE, ID_COPY_TO_CLIPBOARD, Menu.NONE, android.R.string.copy);
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ item.setIcon(R.drawable.ic_content_copy_white_24dp);
+ // Share button
+ item = menu.add(Menu.NONE, ID_SHARE, Menu.NONE, R.string.reader_btn_share);
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ item.setIcon(R.drawable.ic_share_white_24dp);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ case ID_SHARE:
+ shareAppLog();
+ return true;
+ case ID_COPY_TO_CLIPBOARD:
+ copyAppLogToClipboard();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/CheckableFrameLayout.java b/WordPress/src/main/java/org/wordpress/android/ui/CheckableFrameLayout.java
new file mode 100644
index 000000000..4c1e84f52
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/CheckableFrameLayout.java
@@ -0,0 +1,61 @@
+package org.wordpress.android.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.CheckBox;
+import android.widget.Checkable;
+import android.widget.FrameLayout;
+
+import org.wordpress.android.R;
+
+public class CheckableFrameLayout extends FrameLayout implements Checkable {
+ private boolean mIsChecked;
+ private OnCheckedChangeListener mOnCheckedChangeListener;
+
+ public interface OnCheckedChangeListener {
+ public void onCheckedChanged(CheckableFrameLayout view, boolean isChecked);
+ }
+
+ public CheckableFrameLayout(Context context) {
+ super(context);
+ }
+
+ public CheckableFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CheckableFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ CheckBox checkbox = (CheckBox) findViewById(R.id.media_grid_item_checkstate);
+ if (checkbox != null) {
+ checkbox.setChecked(checked);
+ }
+
+ if (mIsChecked != checked) {
+ mIsChecked = checked;
+ refreshDrawableState();
+ if (mOnCheckedChangeListener != null) {
+ mOnCheckedChangeListener.onCheckedChanged((CheckableFrameLayout) this.findViewById(
+ R.id.media_grid_frame_layout), checked);
+ }
+ }
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!mIsChecked);
+ }
+
+ public void setOnCheckedChangeListener(OnCheckedChangeListener onCheckChangeListener) {
+ mOnCheckedChangeListener = onCheckChangeListener;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/CustomSpinner.java b/WordPress/src/main/java/org/wordpress/android/ui/CustomSpinner.java
new file mode 100644
index 000000000..8a7f2a132
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/CustomSpinner.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Spinner;
+
+import org.wordpress.android.ui.media.MediaGridFragment.Filter;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.lang.reflect.Field;
+
+public class CustomSpinner extends Spinner {
+ OnItemSelectedListener listener;
+
+ public CustomSpinner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setSelection(int position) {
+ //only ignore if the old selection is custom date since we may want to click on it again
+ if (position == Filter.CUSTOM_DATE.ordinal())
+ ignoreOldSelectionByReflection();
+ super.setSelection(position);
+ }
+
+ public void setOnItemSelectedEvenIfUnchangedListener(
+ OnItemSelectedListener listener) {
+ this.listener = listener;
+ }
+
+ private void ignoreOldSelectionByReflection() {
+ try {
+ Class<?> c = this.getClass().getSuperclass().getSuperclass().getSuperclass();
+ Field reqField = c.getDeclaredField("mOldSelectedPosition");
+ reqField.setAccessible(true);
+ reqField.setInt(this, -1);
+ } catch (Exception e) {
+ AppLog.e(T.MEDIA, e);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java
new file mode 100644
index 000000000..5c8ef4da4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java
@@ -0,0 +1,82 @@
+package org.wordpress.android.ui;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.ToastUtils;
+
+/**
+ * An activity to handle deep linking.
+ *
+ * wordpress://viewpost?blogId={blogId}&postId={postId}
+ *
+ * Redirects users to the reader activity along with IDs passed in the intent
+ */
+public class DeepLinkingIntentReceiverActivity extends AppCompatActivity {
+ private static final int INTENT_WELCOME = 0;
+
+ private String mBlogId;
+ private String mPostId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String action = getIntent().getAction();
+ Uri uri = getIntent().getData();
+
+ // check if this intent is started via custom scheme link
+ if (Intent.ACTION_VIEW.equals(action) && uri != null) {
+ mBlogId = uri.getQueryParameter("blogId");
+ mPostId = uri.getQueryParameter("postId");
+
+ // if user is logged in, show the post right away - otherwise show welcome activity
+ // and then show the post once the user has logged in
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ showPost();
+ } else {
+ Intent intent = new Intent(this, SignInActivity.class);
+ startActivityForResult(intent, INTENT_WELCOME);
+ }
+ } else {
+ finish();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ // show the post if user is returning from successful login
+ if (requestCode == INTENT_WELCOME && resultCode == RESULT_OK)
+ showPost();
+ }
+
+ private void showPost() {
+ if (!TextUtils.isEmpty(mBlogId) && !TextUtils.isEmpty(mPostId)) {
+ try {
+ ReaderActivityLauncher.showReaderPostDetail(this, Long.parseLong(mBlogId), Long.parseLong(mPostId));
+ } catch (NumberFormatException e) {
+ AppLog.e(T.READER, e);
+ }
+ } else {
+ ToastUtils.showToast(this, R.string.error_generic);
+ }
+
+ finish();
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/EmptyViewMessageType.java b/WordPress/src/main/java/org/wordpress/android/ui/EmptyViewMessageType.java
new file mode 100644
index 000000000..2bb541ef7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/EmptyViewMessageType.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.ui;
+
+public enum EmptyViewMessageType {
+ LOADING, NO_CONTENT, NETWORK_ERROR, PERMISSION_ERROR, GENERIC_ERROR, NO_CONTENT_CUSTOM_DATE;
+
+ public static EmptyViewMessageType getEnumFromString(String value) {
+ for (EmptyViewMessageType id : values()) {
+ if (id.name().equals(value)) {
+ return id;
+ }
+ }
+ return NO_CONTENT;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ExpandableHeightGridView.java b/WordPress/src/main/java/org/wordpress/android/ui/ExpandableHeightGridView.java
new file mode 100644
index 000000000..ee758b752
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ExpandableHeightGridView.java
@@ -0,0 +1,59 @@
+package org.wordpress.android.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.GridView;
+
+// based on http://stackoverflow.com/questions/8481844/gridview-height-gets-cut
+
+public class ExpandableHeightGridView extends GridView
+{
+ boolean expanded = false;
+
+ public ExpandableHeightGridView(Context context)
+ {
+ super(context);
+ }
+
+ public ExpandableHeightGridView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ public ExpandableHeightGridView(Context context, AttributeSet attrs,
+ int defStyle)
+ {
+ super(context, attrs, defStyle);
+ }
+
+ public boolean isExpanded()
+ {
+ return expanded;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+ {
+ if (isExpanded())
+ {
+ // Calculate entire height by providing a very large height hint.
+ // View.MEASURED_SIZE_MASK represents the largest height possible.
+ int expandSpec = MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK,
+ MeasureSpec.AT_MOST);
+ super.onMeasure(widthMeasureSpec, expandSpec);
+
+ ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = getMeasuredHeight();
+ }
+ else
+ {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ public void setExpanded(boolean expanded)
+ {
+ this.expanded = expanded;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FadeInNetworkImageView.java b/WordPress/src/main/java/org/wordpress/android/ui/FadeInNetworkImageView.java
new file mode 100644
index 000000000..019493a5a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/FadeInNetworkImageView.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.volley.toolbox.NetworkImageView;
+
+/**
+ * A custom NetworkImageView that does a fade in animation when the bitmap is set
+ * from: https://gist.github.com/benvd/5683818
+ * nbradbury 10-Mar-2015 - replaced previous TransitionDrawable with faster alpha animation
+ */
+
+public class FadeInNetworkImageView extends NetworkImageView {
+ public FadeInNetworkImageView(Context context) {
+ super(context);
+ }
+
+ public FadeInNetworkImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FadeInNetworkImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bm) {
+ super.setImageBitmap(bm);
+
+ if (getContext() == null)
+ return;
+ int duration = getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
+
+ // use faster property animation if device supports it
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(this, View.ALPHA, 0.25f, 1f);
+ alpha.setDuration(duration);
+ alpha.start();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java
new file mode 100644
index 000000000..835f3293e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java
@@ -0,0 +1,574 @@
+package org.wordpress.android.ui;
+
+import android.content.Context;
+import android.support.annotation.MenuRes;
+import android.support.design.widget.AppBarLayout;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.FilterCriteria;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.wordpress.android.widgets.RecyclerItemDecoration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class FilteredRecyclerView extends RelativeLayout {
+
+ private ProgressBar mProgressLoadMore;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private Spinner mSpinner;
+ private boolean mSelectingRememberedFilterOnCreate = false;
+
+ private RecyclerView mRecyclerView;
+ private TextView mEmptyView;
+ private View mCustomEmptyView;
+ private Toolbar mToolbar;
+ private AppBarLayout mAppBarLayout;
+
+ private List<FilterCriteria> mFilterCriteriaOptions;
+ private FilterCriteria mCurrentFilter;
+ private FilterListener mFilterListener;
+ private SpinnerAdapter mSpinnerAdapter;
+ private RecyclerView.Adapter<RecyclerView.ViewHolder> mAdapter;
+ private int mSpinnerTextColor;
+ private int mSpinnerDrawableRight;
+ private AppLog.T mTAG;
+
+ public FilteredRecyclerView(Context context) {
+ super(context);
+ init();
+ }
+
+ public FilteredRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public FilteredRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mSwipeToRefreshHelper.setRefreshing(refreshing);
+ }
+
+ public boolean isRefreshing(){
+ return mSwipeToRefreshHelper.isRefreshing();
+ }
+
+ public void setCurrentFilter(FilterCriteria filter) {
+ mCurrentFilter = filter;
+ int position = mSpinnerAdapter.getIndexOfCriteria(filter);
+ if (position > -1 && position != mSpinner.getSelectedItemPosition()) {
+ mSpinner.setSelection(position);
+ }
+ }
+
+ public FilterCriteria getCurrentFilter() {
+ return mCurrentFilter;
+ }
+
+ public void setFilterListener(FilterListener filterListener){
+ mFilterListener = filterListener;
+ setup(false);
+ }
+
+ public void setAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter){
+ mAdapter = adapter;
+ mRecyclerView.setAdapter(mAdapter);
+ }
+
+ public RecyclerView.Adapter<RecyclerView.ViewHolder> getAdapter(){
+ return mAdapter;
+ }
+
+ public void setSwipeToRefreshEnabled(boolean enable){
+ mSwipeToRefreshHelper.setEnabled(enable);
+ }
+
+ public void setLogT(AppLog.T tag){
+ mTAG = tag;
+ }
+
+ public void setCustomEmptyView(View v){
+ mCustomEmptyView = v;
+ }
+
+ private void init() {
+ inflate(getContext(), R.layout.filtered_list_component, this);
+
+ int spacingHorizontal = 0;
+ int spacingVertical = DisplayUtils.dpToPx(getContext(), 1);
+ mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
+
+ mToolbar = (Toolbar) findViewById(R.id.toolbar_with_spinner);
+ mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
+
+ mEmptyView = (TextView) findViewById(R.id.empty_view);
+
+ // progress bar that appears when loading more items
+ mProgressLoadMore = (ProgressBar) findViewById(R.id.progress_loading);
+ mProgressLoadMore.setVisibility(View.GONE);
+
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(getContext(),
+ (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ if (!NetworkUtils.checkConnection(getContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ return;
+ }
+ if (mFilterListener != null){
+ mFilterListener.onLoadData();
+ }
+ }
+ });
+ }
+ });
+
+
+ if (mSpinner == null) {
+ mSpinner = (Spinner) findViewById(R.id.filter_spinner);
+ }
+
+ }
+
+ private void setup(boolean refresh){
+ List<FilterCriteria> criterias = mFilterListener.onLoadFilterCriteriaOptions(refresh);
+ if (criterias != null){
+ mFilterCriteriaOptions = criterias;
+ }
+ if (criterias == null){
+ mFilterListener.onLoadFilterCriteriaOptionsAsync(new FilterCriteriaAsyncLoaderListener() {
+ @Override
+ public void onFilterCriteriasLoaded(List<FilterCriteria> criteriaList) {
+ if (criteriaList != null) {
+ mFilterCriteriaOptions = new ArrayList<FilterCriteria>();
+ mFilterCriteriaOptions.addAll(criteriaList);
+ initSpinnerAdapter();
+ setCurrentFilter(mFilterListener.onRecallSelection());
+ }
+ }
+ }, refresh);
+ } else {
+ initSpinnerAdapter();
+ setCurrentFilter(mFilterListener.onRecallSelection());
+ }
+ }
+
+ private void initSpinnerAdapter(){
+ mSpinnerAdapter = new SpinnerAdapter(getContext(), mFilterCriteriaOptions);
+
+ mSelectingRememberedFilterOnCreate = true;
+ mSpinner.setAdapter(mSpinnerAdapter);
+ mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (mSelectingRememberedFilterOnCreate) {
+ mSelectingRememberedFilterOnCreate = false;
+ return;
+ }
+
+ FilterCriteria selectedCriteria =
+ (FilterCriteria) mSpinnerAdapter.getItem(position);
+
+ if (mCurrentFilter == selectedCriteria) {
+ AppLog.d(mTAG, "The selected STATUS is already active: " +
+ selectedCriteria.getLabel());
+ return;
+ }
+
+ AppLog.d(mTAG, "NEW STATUS : " + selectedCriteria.getLabel());
+ setCurrentFilter(selectedCriteria);
+ if (mFilterListener != null) {
+ mFilterListener.onFilterSelected(position, selectedCriteria);
+ setRefreshing(true);
+ mFilterListener.onLoadData();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // nop
+ }
+ });
+
+ }
+
+ private boolean hasAdapter() {
+ return (mAdapter != null);
+ }
+
+ public boolean emptyViewIsVisible(){
+ return (mEmptyView != null && mEmptyView.getVisibility() == View.VISIBLE);
+ }
+
+ public void hideEmptyView() {
+ if (mEmptyView != null) {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ }
+
+ public void updateEmptyView(EmptyViewMessageType emptyViewMessageType) {
+ if (mEmptyView == null) return;
+
+ if ((hasAdapter() && mAdapter.getItemCount() == 0) || !hasAdapter()) {
+ if (mFilterListener != null){
+ if (mCustomEmptyView == null){
+ String msg = mFilterListener.onShowEmptyViewMessage(emptyViewMessageType);
+ if (msg == null){
+ msg = getContext().getString(R.string.empty_list_default);
+ }
+ mEmptyView.setText(msg);
+ mEmptyView.setVisibility(View.VISIBLE);
+ }
+ else {
+ mEmptyView.setVisibility(View.GONE);
+ mFilterListener.onShowCustomEmptyView(emptyViewMessageType);
+ }
+ }
+
+ } else {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * show/hide progress bar which appears at the bottom when loading more items
+ */
+ public void showLoadingProgress() {
+ if (mProgressLoadMore != null) {
+ mProgressLoadMore.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void hideLoadingProgress() {
+ if (mProgressLoadMore != null) {
+ mProgressLoadMore.setVisibility(View.GONE);
+ }
+ }
+
+ /*
+ * add a menu to the right side of the toolbar, returns the toolbar menu so the caller
+ * can act upon it
+ */
+ public Menu addToolbarMenu(@MenuRes int menuResId) {
+ mToolbar.inflateMenu(menuResId);
+ return mToolbar.getMenu();
+ }
+
+ public void setToolbarBackgroundColor(int color){
+ mToolbar.setBackgroundColor(color);
+ }
+
+ public void setToolbarSpinnerTextColor(int color){
+ mSpinnerTextColor = color;
+ }
+
+ public void setToolbarSpinnerDrawable(int drawableResId){
+ mSpinnerDrawableRight = drawableResId;
+ }
+
+ public void setToolbarLeftPadding(int paddingLeft){
+ mToolbar.setPadding(paddingLeft,
+ mToolbar.getPaddingTop(),
+ mToolbar.getPaddingRight(),
+ mToolbar.getPaddingBottom());
+ }
+
+ public void setToolbarRightPadding(int paddingRight){
+ mToolbar.setPadding(
+ mToolbar.getPaddingLeft(),
+ mToolbar.getPaddingTop(),
+ paddingRight,
+ mToolbar.getPaddingBottom());
+ }
+
+ public void setToolbarLeftAndRightPadding(int paddingLeft, int paddingRight){
+ mToolbar.setPadding(
+ paddingLeft,
+ mToolbar.getPaddingTop(),
+ paddingRight,
+ mToolbar.getPaddingBottom());
+ }
+
+ public void scrollRecycleViewToPosition(int position) {
+ if (mRecyclerView == null) return;
+
+ mRecyclerView.scrollToPosition(position);
+ }
+
+ public int getCurrentPosition() {
+ if (mRecyclerView != null && mRecyclerView.getLayoutManager() != null) {
+ return ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
+ } else {
+ return -1;
+ }
+ }
+
+ public void smoothScrollToPosition(int position){
+ if (mRecyclerView != null && mRecyclerView.getLayoutManager() != null) {
+ mRecyclerView.getLayoutManager().smoothScrollToPosition(mRecyclerView, null, position);
+ }
+ }
+
+ public void addItemDecoration(RecyclerView.ItemDecoration decor){
+ if (mRecyclerView == null) return;
+
+ mRecyclerView.addItemDecoration(decor);
+ }
+
+ public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
+ if (mRecyclerView != null) {
+ mRecyclerView.addOnScrollListener(listener);
+ }
+ }
+
+ public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
+ if (mRecyclerView != null) {
+ mRecyclerView.removeOnScrollListener(listener);
+ }
+ }
+
+ public void hideToolbar(){
+ mAppBarLayout.setExpanded(false, true);
+ }
+
+ public void showToolbar(){
+ mAppBarLayout.setExpanded(true, true);
+ }
+
+ /*
+ * use this if you need to reload the criterias for this FilteredRecyclerView. The actual data loading goes
+ * through the FilteredRecyclerView lifecycle using its listeners:
+ *
+ * - FilterCriteriaAsyncLoaderListener
+ * and
+ * - FilterListener.onLoadFilterCriteriaOptions
+ * */
+ public void refreshFilterCriteriaOptions(){
+ setup(true);
+ }
+
+ /*
+ * adapter used by the filter spinner
+ */
+ private class SpinnerAdapter extends BaseAdapter {
+ private final List<FilterCriteria> mFilterValues;
+ private final LayoutInflater mInflater;
+
+ SpinnerAdapter(Context context, List<FilterCriteria> filterValues) {
+ super();
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mFilterValues = filterValues;
+ }
+
+ @Override
+ public int getCount() {
+ return (mFilterValues != null ? mFilterValues.size() : 0);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mFilterValues.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view;
+ if (convertView == null) {
+ view = mInflater.inflate(R.layout.filter_spinner_item, parent, false);
+
+ final TextView text = (TextView) view.findViewById(R.id.text);
+ FilterCriteria selectedCriteria = (FilterCriteria)getItem(position);
+ text.setText(selectedCriteria.getLabel());
+ if (mSpinnerTextColor != 0){
+ text.setTextColor(mSpinnerTextColor);
+ }
+
+ if (mSpinnerDrawableRight != 0){
+ text.setCompoundDrawablesWithIntrinsicBounds(0, 0, mSpinnerDrawableRight, 0);
+ text.setCompoundDrawablePadding(getResources().getDimensionPixelSize(R.dimen.margin_medium));
+ text.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+ }
+
+ } else {
+ view = convertView;
+ }
+
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ FilterCriteria selectedCriteria = (FilterCriteria)getItem(position);
+ final TagViewHolder holder;
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.toolbar_spinner_dropdown_item, parent, false);
+ holder = new TagViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (TagViewHolder) convertView.getTag();
+ }
+
+ holder.textView.setText(selectedCriteria.getLabel());
+ return convertView;
+ }
+
+ private class TagViewHolder {
+ private final TextView textView;
+ TagViewHolder(View view) {
+ textView = (TextView) view.findViewById(R.id.text);
+ }
+ }
+
+ public int getIndexOfCriteria(FilterCriteria tm) {
+ if (tm != null && mFilterValues != null){
+ for (int i = 0; i < mFilterValues.size(); i++) {
+ FilterCriteria criteria = mFilterValues.get(i);
+ if (criteria != null && criteria.equals(tm)) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+ }
+
+ /*
+ * returns true if the first item is still visible in the RecyclerView - will return
+ * false if the first item is scrolled out of view, or if the list is empty
+ */
+ public boolean isFirstItemVisible() {
+ if (mRecyclerView == null
+ || mRecyclerView.getLayoutManager() == null) {
+ return false;
+ }
+
+ View child = mRecyclerView.getLayoutManager().getChildAt(0);
+ return (child != null && mRecyclerView.getLayoutManager().getPosition(child) == 0);
+ }
+
+
+ /**
+ * implement this interface to use FilterRecyclerView
+ */
+ public interface FilterListener {
+ /**
+ * Called upon initialization - provide an array of FilterCriterias here. These are the possible criterias
+ * the Spinner is loaded with, and through which the data can be filtered.
+ *
+ * @param refresh "true"if the criterias need be refreshed
+ * @return an array of FilterCriteria to be used on Spinner initialization, or null if going to use the
+ * Async method below
+ */
+ List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh);
+
+ /**
+ * Called upon initialization - you can use this callback to start an asynctask to build an array of
+ * FilterCriterias here. Once the AsyncTask is done, it should call the provided listener
+ * The Spinner is then loaded with such array of FilterCriterias, through which the main data can be filtered.
+ *
+ * @param listener to be called to pass the FilterCriteria array when done
+ * @param refresh "true"if the criterias need be refreshed
+ */
+ void onLoadFilterCriteriaOptionsAsync(FilterCriteriaAsyncLoaderListener listener, boolean refresh);
+
+ /**
+ * Called upon initialization, right after onLoadFilterCriteriaOptions().
+ * Once the criteria options are set up, use this callback to return the latest option selected on the
+ * screen the last time the user visited it, or a default value for the filter Spinner to be initialized with.
+ *
+ * @return
+ */
+ FilterCriteria onRecallSelection();
+
+ /**
+ * When this method is called, you should load data into the FilteredRecyclerView adapter, using the
+ * latest criteria passed to you in a previous onFilterSelected() call.
+ * Within the FilteredRecyclerView lifecycle, this is triggered in three different moments:
+ * 1 - upon initialisation
+ * 2 - each time a screen refresh is requested
+ * 3 - each time the user changes the filter spinner selection
+ */
+ void onLoadData();
+
+ /**
+ * Called each time the user changes the Spinner selection (i.e. changes the criteria on which to filter
+ * the data). You should only take note of the change, and remember it, as a request to load data with
+ * the newly selected filter shall always arrive through onLoadData().
+ * The parameters passed in this callback can be used alternatively as per your convenience.
+ *
+ * @param position of the selected criteria within the array returned by onLoadFilterCriteriaOptions()
+ * @param criteria the actual criteria selected
+ */
+ void onFilterSelected(int position, FilterCriteria criteria);
+
+ /**
+ * Called when there's no data to show.
+ *
+ * @param emptyViewMsgType this will hint you on the reason why no data is being shown, so you can return
+ * a proper message to be displayed to the user
+ * @return the message to be displayed to the user, or null if using a Custom Empty View (see below)
+ */
+ String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType);
+
+ /**
+ * Called when there's no data to show, and only if a custom EmptyView is set (onShowEmptyViewMessage will
+ * be called otherwise).
+ *
+ * @param emptyViewMsgType this will hint you on the reason why no data is being shown, and
+ * also here you should perform any actions on your custom empty view
+ * @return nothing
+ */
+ void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType);
+
+ }
+
+ /**
+ * implement this interface to load filtering options (that is, an array of FilterCriteria) asynchronously
+ */
+ public interface FilterCriteriaAsyncLoaderListener{
+ /**
+ * Will be called during initialization of FilteredRecyclerView once you're ready building the FilterCriteria array
+ *
+ * @param criteriaList the array of FilterCriteria objects you just built
+ */
+ void onFilterCriteriasLoaded(List<FilterCriteria> criteriaList);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/HelpshiftDeepLinkReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/HelpshiftDeepLinkReceiver.java
new file mode 100644
index 000000000..a2c2328a5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/HelpshiftDeepLinkReceiver.java
@@ -0,0 +1,29 @@
+package org.wordpress.android.ui;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.helpshift.support.Support;
+
+public class HelpshiftDeepLinkReceiver extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String action = getIntent().getAction();
+ Uri data = getIntent().getData();
+
+ if (Intent.ACTION_VIEW.equals(action) && data != null) {
+ String faqid = data.getQueryParameter("faqid");
+ String sectionid = data.getQueryParameter("sectionid");
+ if (faqid != null) {
+ Support.showSingleFAQ(this, faqid);
+ } else if (sectionid != null) {
+ Support.showFAQSection(this, sectionid);
+ }
+ }
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java
new file mode 100644
index 000000000..e313a558f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java
@@ -0,0 +1,24 @@
+package org.wordpress.android.ui;
+
+/**
+ * Global intent identifiers
+ */
+public class RequestCodes {
+ public static final int ADD_ACCOUNT = 100;
+ public static final int REAUTHENTICATE = 200;
+ public static final int APP_SETTINGS = 300;
+ public static final int NOTE_DETAIL = 600;
+ public static final int SITE_PICKER = 700;
+ public static final int EDIT_POST = 800;
+ public static final int PREVIEW_POST = 810;
+ public static final int CREATE_BLOG = 900;
+ public static final int BLOG_SETTINGS = 1000;
+
+ // Media
+ public static final int PICTURE_LIBRARY = 2000;
+ public static final int TAKE_PHOTO = 2100;
+ public static final int VIDEO_LIBRARY = 2200;
+ public static final int TAKE_VIDEO = 2300;
+ public static final int CROP_PHOTO = 2400;
+ public static final int PICTURE_LIBRARY_OR_CAPTURE = 2500;
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java
new file mode 100644
index 000000000..4a222426a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java
@@ -0,0 +1,274 @@
+package org.wordpress.android.ui;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.ui.media.MediaBrowserActivity;
+import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.PermissionUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An activity to handle share intents, since there are multiple actions possible.
+ * If there are multiple blogs, it lets the user choose which blog to share to.
+ * It lists what actions that the user can perform and redirects them to the activity,
+ * along with the content passed in the intent
+ */
+public class ShareIntentReceiverActivity extends AppCompatActivity implements OnItemSelectedListener {
+ public static final String SHARE_LAST_USED_BLOG_ID_KEY = "wp-settings-share-last-used-text-blogid";
+ public static final String SHARE_LAST_USED_ADDTO_KEY = "wp-settings-share-last-used-image-addto";
+
+ public static final int ADD_TO_NEW_POST = 0;
+ public static final int ADD_TO_MEDIA_LIBRARY = 1;
+ public static final int SHARE_MEDIA_PERMISSION_REQUEST_CODE = 1;
+
+ private Spinner mBlogSpinner;
+ private Spinner mActionSpinner;
+ private int mAccountIDs[];
+ private TextView mBlogSpinnerTitle;
+ private int mActionIndex;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.share_intent_receiver_dialog);
+
+ mBlogSpinnerTitle = (TextView) findViewById(R.id.blog_spinner_title);
+ mBlogSpinner = (Spinner) findViewById(R.id.blog_spinner);
+ String[] blogNames = getBlogNames();
+ if (blogNames == null) {
+ finishIfNoVisibleBlogs();
+ return;
+ }
+
+ if (blogNames.length == 1) {
+ mBlogSpinner.setVisibility(View.GONE);
+ mBlogSpinnerTitle.setVisibility(View.GONE);
+ } else {
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ R.layout.spinner_menu_dropdown_item, blogNames);
+ mBlogSpinner.setAdapter(adapter);
+ mBlogSpinner.setOnItemSelectedListener(this);
+ }
+
+ // If type is text/plain hide Media Gallery option
+ mActionSpinner = (Spinner) findViewById(R.id.action_spinner);
+ if (isSharingText()) {
+ mActionSpinner.setVisibility(View.GONE);
+ findViewById(R.id.action_spinner_title).setVisibility(View.GONE);
+ // if text/plain and only one blog, then don't show this fragment, share it directly to a new post
+ if (blogNames.length == 1) {
+ startActivityAndFinish(new Intent(this, EditPostActivity.class));
+ }
+ } else {
+ String[] actions = new String[]{getString(R.string.share_action_post), getString(
+ R.string.share_action_media)};
+ ArrayAdapter<String> actionAdapter = new ArrayAdapter<String>(this,
+ R.layout.spinner_menu_dropdown_item, actions);
+ mActionSpinner.setAdapter(actionAdapter);
+ mActionSpinner.setOnItemSelectedListener(this);
+ }
+ loadLastUsed();
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ finish();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // noop
+ }
+
+ private void finishIfNoVisibleBlogs() {
+ // If not signed in, then ask to sign in, else inform the user to set at least one blog
+ // visible
+ if (!AccountHelper.isSignedIn()) {
+ ToastUtils.showToast(getBaseContext(), R.string.no_account, ToastUtils.Duration.LONG);
+ startActivity(new Intent(this, SignInActivity.class));
+ finish();
+ } else {
+ ToastUtils.showToast(getBaseContext(), R.string.cant_share_no_visible_blog, ToastUtils.Duration.LONG);
+ finish();
+ }
+ }
+
+ private int gepPositionByLocalBlogId(long localBlogId) {
+ for (int i = 0; i < mAccountIDs.length; i++) {
+ if (mAccountIDs[i] == localBlogId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void loadLastUsed() {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
+ int localBlogId = settings.getInt(SHARE_LAST_USED_BLOG_ID_KEY, -1);
+ int actionPosition = settings.getInt(SHARE_LAST_USED_ADDTO_KEY, -1);
+ if (localBlogId != -1) {
+ int position = gepPositionByLocalBlogId(localBlogId);
+ if (position != -1) {
+ mBlogSpinner.setSelection(position);
+ }
+ }
+ if (actionPosition >= 0 && actionPosition < mActionSpinner.getCount()) {
+ mActionSpinner.setSelection(actionPosition);
+ }
+ }
+
+ private boolean isSharingText() {
+ return "text/plain".equals(getIntent().getType());
+ }
+
+ private String[] getBlogNames() {
+ String[] extraFields = {"homeURL"};
+ List<Map<String, Object>> accounts = WordPress.wpDB.getBlogsBy("isHidden = 0", extraFields);
+ if (accounts.size() > 0) {
+ final String blogNames[] = new String[accounts.size()];
+ mAccountIDs = new int[accounts.size()];
+ Blog blog;
+ for (int i = 0; i < accounts.size(); i++) {
+ Map<String, Object> account = accounts.get(i);
+ blogNames[i] = BlogUtils.getBlogNameOrHomeURLFromAccountMap(account);
+ mAccountIDs[i] = (Integer) account.get("id");
+ blog = WordPress.wpDB.instantiateBlogByLocalId(mAccountIDs[i]);
+ if (blog == null) {
+ ToastUtils.showToast(this, R.string.blog_not_found, ToastUtils.Duration.SHORT);
+ return null;
+ }
+ }
+ return blogNames;
+ }
+ return null;
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (parent.getId() == R.id.blog_spinner) {
+ if (!selectBlog(mAccountIDs[position])) {
+ ToastUtils.showToast(this, R.string.blog_not_found, ToastUtils.Duration.SHORT);
+ finish();
+ }
+ } else if (parent.getId() == R.id.action_spinner) {
+ mActionIndex = position;
+ }
+ }
+
+ private boolean selectBlog(int blogId) {
+ WordPress.currentBlog = WordPress.wpDB.instantiateBlogByLocalId(blogId);
+ if (WordPress.currentBlog == null || WordPress.currentBlog.isHidden()) {
+ return false;
+ }
+ WordPress.wpDB.updateLastBlogId(WordPress.currentBlog.getLocalTableBlogId());
+ return true;
+ }
+
+ private void startActivityAndFinish(Intent intent) {
+ String action = getIntent().getAction();
+ if (intent != null) {
+ intent.setAction(action);
+ intent.setType(getIntent().getType());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ intent.putExtra(Intent.EXTRA_TEXT, getIntent().getStringExtra(Intent.EXTRA_TEXT));
+ intent.putExtra(Intent.EXTRA_SUBJECT, getIntent().getStringExtra(Intent.EXTRA_SUBJECT));
+
+ if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ ArrayList<Uri> extra = getIntent().getParcelableArrayListExtra((Intent.EXTRA_STREAM));
+ intent.putExtra(Intent.EXTRA_STREAM, extra);
+ } else {
+ Uri extra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ intent.putExtra(Intent.EXTRA_STREAM, extra);
+ }
+ savePreferences();
+ startActivity(intent);
+ finish();
+ }
+ }
+
+ public void onShareClicked(View view) {
+ shareIt();
+ }
+
+ /**
+ * Start the correct activity if permissions are granted
+ *
+ * @return true if the activity has been started, false else.
+ */
+ private boolean shareIt() {
+ Intent intent = null;
+ if (!isSharingText()) {
+ // If we're sharing media, we must check we have Storage permission (needed for media upload).
+ if (!PermissionUtils.checkAndRequestStoragePermission(this, SHARE_MEDIA_PERMISSION_REQUEST_CODE)) {
+ return false;
+ }
+ }
+ if (mActionIndex == ADD_TO_NEW_POST) {
+ // new post
+ intent = new Intent(this, EditPostActivity.class);
+ } else if (mActionIndex == ADD_TO_MEDIA_LIBRARY) {
+ // add to media gallery
+ intent = new Intent(this, MediaBrowserActivity.class);
+ }
+ startActivityAndFinish(intent);
+ return true;
+ }
+
+ private void savePreferences() {
+ // If current blog is not set don't save preferences
+ if (WordPress.currentBlog == null) {
+ return ;
+ }
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
+
+ // Save last used settings
+ editor.putInt(SHARE_LAST_USED_BLOG_ID_KEY, WordPress.currentBlog.getLocalTableBlogId());
+ editor.putInt(SHARE_LAST_USED_ADDTO_KEY, mActionIndex);
+ editor.commit();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
+ @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case SHARE_MEDIA_PERMISSION_REQUEST_CODE:
+ for (int grantResult : grantResults) {
+ if (grantResult == PackageManager.PERMISSION_DENIED) {
+ ToastUtils.showToast(this, getString(R.string.add_media_permission_required));
+ return;
+ }
+ }
+ shareIt();
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/VisualEditorOptionsReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/VisualEditorOptionsReceiver.java
new file mode 100644
index 000000000..24271fa22
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/VisualEditorOptionsReceiver.java
@@ -0,0 +1,46 @@
+package org.wordpress.android.ui;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.ToastUtils;
+
+public class VisualEditorOptionsReceiver extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String action = getIntent().getAction();
+ Uri uri = getIntent().getData();
+
+ if (Intent.ACTION_VIEW.equals(action) && uri != null) {
+ String available = uri.getQueryParameter("available");
+ String enabled = uri.getQueryParameter("enabled");
+ // Note: doesn't allow to deactivate visual editor
+ if ("1".equals(available)) {
+ AppLog.i(T.EDITOR, "Visual Editor is now Available");
+ AppPrefs.setVisualEditorAvailable(true);
+ ToastUtils.showToast(this, R.string.visual_editor_enabled);
+ }
+
+ if ("1".equals(enabled)) {
+ AppLog.i(T.EDITOR, "Visual Editor Enabled");
+ AppPrefs.setVisualEditorEnabled(true);
+ } else if ("0".equals(enabled)) {
+ AppLog.i(T.EDITOR, "Visual Editor Disabled");
+ AppPrefs.setVisualEditorEnabled(false);
+ }
+ }
+
+ Intent intent = new Intent(this, WPLaunchActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.java
new file mode 100644
index 000000000..e60244fab
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/WPLaunchActivity.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.util.ProfilingUtils;
+import org.wordpress.android.util.ToastUtils;
+
+public class WPLaunchActivity extends AppCompatActivity {
+
+ /*
+ * this the main (default) activity, which does nothing more than launch the
+ * previously active activity on startup - note that it's defined in the
+ * manifest to have no UI
+ */
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ProfilingUtils.split("WPLaunchActivity.onCreate");
+
+ if (WordPress.wpDB == null) {
+ ToastUtils.showToast(this, R.string.fatal_db_error, ToastUtils.Duration.LONG);
+ finish();
+ return;
+ }
+
+ Intent intent = new Intent(this, WPMainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java b/WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java
new file mode 100644
index 000000000..0f2de5050
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java
@@ -0,0 +1,261 @@
+package org.wordpress.android.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.NumberPicker;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+import java.lang.reflect.Field;
+
+public class WPNumberPicker extends NumberPicker {
+ private static final String DIVIDER_FIELD = "mSelectionDivider";
+ private static final String INPUT_FIELD = "mInputText";
+ private static final String INDICES_FIELD = "mSelectorIndices";
+ private static final String CUR_OFFSET_FIELD = "mCurrentScrollOffset";
+ private static final String SELECTOR_HEIGHT_FIELD = "mSelectorElementHeight";
+ private static final String INITIAL_OFFSET_FIELD = "mInitialScrollOffset";
+ private static final String CURRENT_OFFSET_FIELD = "mCurrentScrollOffset";
+ private static final String PAINT_FIELD = "mSelectorWheelPaint";
+
+ private static final int DISPLAY_COUNT = 5;
+ private static final int MIDDLE_INDEX = 2;
+
+ private Field mOffsetField;
+ private Field mSelectorHeight;
+ private Field mSelectorIndices;
+ private Field mInitialOffset;
+ private Field mCurrentOffset;
+
+ private EditText mInputView;
+ private Formatter mFormatter;
+ private Paint mPaint;
+ private int[] mDisplayValues;
+
+ public WPNumberPicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mDisplayValues = new int[DISPLAY_COUNT];
+ getFieldsViaReflection();
+ }
+
+ @Override
+ public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ if (child instanceof TextView) {
+ WPPrefUtils.layoutAsNumberPickerPeek((TextView) child);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ updateIntitialOffset();
+ setVerticalFadingEdgeEnabled(false);
+ setHorizontalFadingEdgeEnabled(false);
+ WPPrefUtils.layoutAsNumberPickerSelected(mInputView);
+ mInputView.setVisibility(View.INVISIBLE);
+ }
+
+ @Override
+ public void setValue(int value) {
+ if (value < getMinValue()) value = getMinValue();
+ if (value > getMaxValue()) value = getMaxValue();
+ super.setValue(value);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int[] selectorIndices = getIndices();
+ setIndices(new int[0]);
+ setIndices(selectorIndices);
+
+ // Draw the middle number with a different font
+ setDisplayValues();
+ float elementHeight = getSelectorElementHeight();
+ float x = ((getRight() - getLeft()) / 2.0f);
+ float y = getScrollOffset();
+ Paint paint = mInputView.getPaint();
+ paint.setTextAlign(Paint.Align.CENTER);
+ //noinspection deprecation
+ paint.setColor(getResources().getColor(R.color.blue_medium));
+ int alpha = isEnabled() ? 255 : 96;
+ paint.setAlpha(alpha);
+ mPaint.setAlpha(alpha);
+
+ int offset = getResources().getDimensionPixelSize(R.dimen.margin_medium);
+ // Draw the visible values
+ for (int i = 0; i < DISPLAY_COUNT; ++i) {
+ String scrollSelectorValue;
+ if (mFormatter != null) {
+ scrollSelectorValue = mFormatter.format(mDisplayValues[i]);
+ } else {
+ scrollSelectorValue = String.valueOf(mDisplayValues[i]);
+ }
+ if (i == MIDDLE_INDEX) {
+ canvas.drawText(scrollSelectorValue, x, y - ((paint.descent() + paint.ascent()) / 2) - offset, paint);
+ } else {
+ canvas.drawText(scrollSelectorValue, x, y - ((mPaint.descent() + mPaint.ascent()) / 2) - offset, mPaint);
+ }
+ y += elementHeight;
+ }
+ }
+
+ @Override
+ public void setFormatter(Formatter formatter) {
+ super.setFormatter(formatter);
+ mFormatter = formatter;
+ }
+
+ private void setDisplayValues() {
+ int value = getValue();
+ for (int i = 0; i < DISPLAY_COUNT; ++i) {
+ mDisplayValues[i] = value - MIDDLE_INDEX + i;
+ if (mDisplayValues[i] < getMinValue()) {
+ mDisplayValues[i] = getMaxValue() + (mDisplayValues[i] + 1 - getMinValue());
+ } else if (mDisplayValues[i] > getMaxValue()) {
+ mDisplayValues[i] = getMinValue() + (mDisplayValues[i] - getMaxValue() - 1);
+ }
+ }
+ }
+
+ private void setIndices(int[] indices) {
+ if (mSelectorIndices != null) {
+ try {
+ mSelectorIndices.set(this, indices);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private int[] getIndices() {
+ if (mSelectorIndices != null) {
+ try {
+ return (int[]) mSelectorIndices.get(this);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return null;
+ }
+
+ private int getScrollOffset() {
+ if (mOffsetField != null) {
+ try {
+ return (Integer) mOffsetField.get(this);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return 0;
+ }
+
+ private int getSelectorElementHeight() {
+ if (mSelectorHeight != null) {
+ try {
+ return (Integer) mSelectorHeight.get(this);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return 0;
+ }
+
+ private void updateIntitialOffset() {
+ if (mInitialOffset != null) {
+ try {
+ int offset = (Integer) mInitialOffset.get(this) - getSelectorElementHeight();
+ mInitialOffset.set(this, offset);
+ // Only do this once
+ mInitialOffset = null;
+
+ if (mCurrentOffset != null) {
+ mCurrentOffset.set(this, offset);
+ }
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * From https://www.snip2code.com/Snippet/67740/NumberPicker-with-transparent-selection-
+ */
+ private void removeDividers(Class<?> clazz) {
+ Field selectionDivider = getFieldAndSetAccessible(clazz, DIVIDER_FIELD);
+ if (selectionDivider != null) {
+ try {
+ selectionDivider.set(this, null);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void getTextPaint(Class<?> clazz) {
+ Field paint = getFieldAndSetAccessible(clazz, PAINT_FIELD);
+ if (paint != null) {
+ try {
+ mPaint = (Paint) paint.get(this);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void getInputField(Class<?> clazz) {
+ Field inputField = getFieldAndSetAccessible(clazz, INPUT_FIELD);
+ if (inputField != null) {
+ try {
+ mInputView = ((EditText) inputField.get(this));
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Gets a class field using reflection and makes it accessible.
+ */
+ private Field getFieldAndSetAccessible(Class<?> clazz, String fieldName) {
+ Field field = null;
+ try {
+ field = clazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+
+ return field;
+ }
+
+ private void getFieldsViaReflection() {
+ Class<?> numberPickerClass = null;
+ try {
+ numberPickerClass = Class.forName(NumberPicker.class.getName());
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ if (numberPickerClass == null) return;
+
+ mSelectorHeight = getFieldAndSetAccessible(numberPickerClass, SELECTOR_HEIGHT_FIELD);
+ mOffsetField = getFieldAndSetAccessible(numberPickerClass, CUR_OFFSET_FIELD);
+ mSelectorIndices = getFieldAndSetAccessible(numberPickerClass, INDICES_FIELD);
+ mInitialOffset = getFieldAndSetAccessible(numberPickerClass, INITIAL_OFFSET_FIELD);
+ mCurrentOffset = getFieldAndSetAccessible(numberPickerClass, CURRENT_OFFSET_FIELD);
+
+ getTextPaint(numberPickerClass);
+ getInputField(numberPickerClass);
+ removeDividers(numberPickerClass);
+ setIndices(new int[DISPLAY_COUNT]);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java
new file mode 100644
index 000000000..1e95dc4b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java
@@ -0,0 +1,363 @@
+package org.wordpress.android.ui;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.webkit.WebViewClient;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.URLFilteredWebViewClient;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPMeShortlinks;
+import org.wordpress.android.util.WPUrlUtils;
+import org.wordpress.android.util.WPWebViewClient;
+import org.wordpress.android.util.helpers.WPWebChromeClient;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Type;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Activity for opening external WordPress links in a webview.
+ *
+ * Try to use one of the methods below to open the webview:
+ * - openURL
+ * - openUrlByUsingMainWPCOMCredentials
+ * - openUrlByUsingWPCOMCredentials
+ * - openUrlByUsingBlogCredentials (for self hosted sites)
+ *
+ * If you need to start the activity with delay, start activity with result, or none of the methods above are enough for your needs,
+ * you can start the activity by passing the required parameters, depending on what you need to do.
+ *
+ * 1. Load a simple URL (without any kind of authentication)
+ * - Start the activity with the parameter URL_TO_LOAD set to the URL to load.
+ *
+ * 2. Load a WordPress.com URL
+ * Start the activity with the following parameters:
+ * - URL_TO_LOAD: target URL to load in the webview.
+ * - AUTHENTICATION_URL: The address of the WordPress.com authentication endpoint. Please use WPCOM_LOGIN_URL.
+ * - AUTHENTICATION_USER: username.
+ * - AUTHENTICATION_PASSWD: password.
+ *
+ * 3. Load a WordPress.org URL with authentication
+ * - URL_TO_LOAD: target URL to load in the webview.
+ * - AUTHENTICATION_URL: The address of the authentication endpoint. Please use the value of getBlogLoginUrl()
+ * to retrieve the correct address of the authentication endpoint.
+ * - AUTHENTICATION_USER: username.
+ * - AUTHENTICATION_PASSWD: password.
+ * - LOCAL_BLOG_ID: local id of the blog in the app database. This is required since some blogs could have HTTP Auth,
+ * or self-signed certs in place.
+ * - REFERRER_URL: url to add as an HTTP referrer header, currently only used for non-authed reader posts
+ *
+ */
+public class WPWebViewActivity extends WebViewActivity {
+ public static final String AUTHENTICATION_URL = "authenticated_url";
+ public static final String AUTHENTICATION_USER = "authenticated_user";
+ public static final String AUTHENTICATION_PASSWD = "authenticated_passwd";
+ public static final String URL_TO_LOAD = "url_to_load";
+ public static final String WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php";
+ public static final String LOCAL_BLOG_ID = "local_blog_id";
+ public static final String SHARABLE_URL = "sharable_url";
+ public static final String REFERRER_URL = "referrer_url";
+ public static final String DISABLE_LINKS_ON_PAGE = "DISABLE_LINKS_ON_PAGE";
+
+ private static final String ENCODING_UTF8 = "UTF-8";
+
+ public static void openUrlByUsingWPCOMCredentials(Context context, String url, String user) {
+ openWPCOMURL(context, url, user);
+ }
+
+ // Note: The webview has links disabled!!
+ public static void openUrlByUsingBlogCredentials(Context context, Blog blog, Post post, String url) {
+ if (context == null) {
+ AppLog.e(AppLog.T.UTILS, "Context is null");
+ return;
+ }
+
+ if (blog == null) {
+ AppLog.e(AppLog.T.UTILS, "Blog obj is null");
+ return;
+ }
+
+ if (TextUtils.isEmpty(url)) {
+ AppLog.e(AppLog.T.UTILS, "Empty or null URL");
+ Toast.makeText(context, context.getResources().getText(R.string.invalid_site_url_message),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String authURL = WPWebViewActivity.getBlogLoginUrl(blog);
+ Intent intent = new Intent(context, WPWebViewActivity.class);
+ intent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, blog.getUsername());
+ intent.putExtra(WPWebViewActivity.AUTHENTICATION_PASSWD, blog.getPassword());
+ intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url);
+ intent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, authURL);
+ intent.putExtra(WPWebViewActivity.LOCAL_BLOG_ID, blog.getLocalTableBlogId());
+ intent.putExtra(WPWebViewActivity.DISABLE_LINKS_ON_PAGE, true);
+ if (post != null) {
+ intent.putExtra(WPWebViewActivity.SHARABLE_URL, WPMeShortlinks.getPostShortlink(blog, post));
+ }
+ context.startActivity(intent);
+ }
+
+ public static void openURL(Context context, String url) {
+ openURL(context, url, null);
+ }
+ public static void openURL(Context context, String url, String referrer) {
+ if (context == null) {
+ AppLog.e(AppLog.T.UTILS, "Context is null");
+ return;
+ }
+
+ if (TextUtils.isEmpty(url)) {
+ AppLog.e(AppLog.T.UTILS, "Empty or null URL");
+ Toast.makeText(context, context.getResources().getText(R.string.invalid_site_url_message),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Intent intent = new Intent(context, WPWebViewActivity.class);
+ intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url);
+ if (!TextUtils.isEmpty(referrer)) {
+ intent.putExtra(REFERRER_URL, referrer);
+ }
+ context.startActivity(intent);
+ }
+
+ private static void openWPCOMURL(Context context, String url, String user) {
+ if (context == null) {
+ AppLog.e(AppLog.T.UTILS, "Context is null");
+ return;
+ }
+
+ if (TextUtils.isEmpty(url)) {
+ AppLog.e(AppLog.T.UTILS, "Empty or null URL passed to openUrlByUsingMainWPCOMCredentials");
+ Toast.makeText(context, context.getResources().getText(R.string.invalid_site_url_message),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (TextUtils.isEmpty(user)) {
+ AppLog.e(AppLog.T.UTILS, "Username empty/null");
+ return;
+ }
+
+ Intent intent = new Intent(context, WPWebViewActivity.class);
+ intent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, user);
+ intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url);
+ intent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, WPCOM_LOGIN_URL);
+ context.startActivity(intent);
+ }
+
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ protected void configureWebView() {
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.getSettings().setDomStorageEnabled(true);
+
+ WebViewClient webViewClient;
+ Bundle extras = getIntent().getExtras();
+
+ // Configure the allowed URLs if available
+ ArrayList<String> allowedURL = null;
+ if (extras.getBoolean(DISABLE_LINKS_ON_PAGE, false)) {
+ String addressToLoad = extras.getString(URL_TO_LOAD);
+ String authURL = extras.getString(AUTHENTICATION_URL);
+ allowedURL = new ArrayList<>();
+ if (!TextUtils.isEmpty(addressToLoad)) {
+ allowedURL.add(addressToLoad);
+ }
+ if (!TextUtils.isEmpty(authURL)) {
+ allowedURL.add(authURL);
+ }
+ }
+
+ if (getIntent().hasExtra(LOCAL_BLOG_ID)) {
+ Blog blog = WordPress.getBlog(getIntent().getIntExtra(LOCAL_BLOG_ID, -1));
+ if (blog == null) {
+ AppLog.e(AppLog.T.UTILS, "No valid blog passed to WPWebViewActivity");
+ finish();
+ }
+ webViewClient = new WPWebViewClient(blog, allowedURL);
+ } else {
+ webViewClient = new URLFilteredWebViewClient(allowedURL);
+ }
+
+ mWebView.setWebViewClient(webViewClient);
+ mWebView.setWebChromeClient(new WPWebChromeClient(this, (ProgressBar) findViewById(R.id.progress_bar)));
+ }
+
+ @Override
+ protected void loadContent() {
+ Bundle extras = getIntent().getExtras();
+
+ if (extras == null) {
+ AppLog.e(AppLog.T.UTILS, "No valid parameters passed to WPWebViewActivity");
+ finish();
+ return;
+ }
+
+ String addressToLoad = extras.getString(URL_TO_LOAD);
+ String username = extras.getString(AUTHENTICATION_USER, "");
+ String password = extras.getString(AUTHENTICATION_PASSWD, "");
+ String authURL = extras.getString(AUTHENTICATION_URL);
+
+ if (TextUtils.isEmpty(addressToLoad) || !UrlUtils.isValidUrlAndHostNotNull(addressToLoad)) {
+ AppLog.e(AppLog.T.UTILS, "Empty or null or invalid URL passed to WPWebViewActivity");
+ Toast.makeText(this, getText(R.string.invalid_site_url_message),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ if (TextUtils.isEmpty(authURL) && TextUtils.isEmpty(username) && TextUtils.isEmpty(password)) {
+ // Only the URL to load is passed to this activity. Use the normal un-authenticated
+ // loader, optionally with our referrer header
+ String referrerUrl = extras.getString(REFERRER_URL);
+ if (!TextUtils.isEmpty(referrerUrl)) {
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Referer", referrerUrl);
+ loadUrl(addressToLoad, headers);
+ } else {
+ loadUrl(addressToLoad);
+ }
+ } else {
+ if (TextUtils.isEmpty(authURL) || !UrlUtils.isValidUrlAndHostNotNull(authURL)) {
+ AppLog.e(AppLog.T.UTILS, "Empty or null or invalid auth URL passed to WPWebViewActivity");
+ Toast.makeText(this, getText(R.string.invalid_site_url_message),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ if (TextUtils.isEmpty(username)) {
+ AppLog.e(AppLog.T.UTILS, "Username empty/null");
+ Toast.makeText(this, getText(R.string.incorrect_credentials), Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ loadAuthenticatedUrl(authURL, addressToLoad, username, password);
+ }
+ }
+
+ /**
+ * Login to the WordPress.com and load the specified URL.
+ *
+ */
+ protected void loadAuthenticatedUrl(String authenticationURL, String urlToLoad, String username, String password) {
+ String postData = getAuthenticationPostData(authenticationURL, urlToLoad, username, password,
+ AccountHelper.getDefaultAccount().getAccessToken());
+
+ mWebView.postUrl(authenticationURL, postData.getBytes());
+ }
+
+ public static String getAuthenticationPostData(String authenticationUrl, String urlToLoad, String username, String password, String token) {
+ if (TextUtils.isEmpty(authenticationUrl)) return "";
+
+ try {
+ String postData = String.format("log=%s&pwd=%s&redirect_to=%s",
+ URLEncoder.encode(StringUtils.notNullStr(username), ENCODING_UTF8),
+ URLEncoder.encode(StringUtils.notNullStr(password), ENCODING_UTF8),
+ URLEncoder.encode(StringUtils.notNullStr(urlToLoad), ENCODING_UTF8)
+ );
+
+ // Add token authorization when signing in to WP.com
+ if (WPUrlUtils.safeToAddWordPressComAuthToken(authenticationUrl)
+ && authenticationUrl.contains("wordpress.com/wp-login.php") && !TextUtils.isEmpty(token)) {
+ postData += "&authorization=Bearer " + URLEncoder.encode(token, ENCODING_UTF8);
+ }
+
+ return postData;
+ } catch (UnsupportedEncodingException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ return "";
+ }
+
+ /**
+ * Get the URL of the WordPress login page.
+ *
+ * @return URL of the login page.
+ */
+ public static String getBlogLoginUrl(Blog blog) {
+ String loginURL = null;
+ Gson gson = new Gson();
+ Type type = new TypeToken<Map<?, ?>>() {}.getType();
+ Map<?, ?> blogOptions = gson.fromJson(blog.getBlogOptions(), type);
+ if (blogOptions != null) {
+ Map<?, ?> homeURLMap = (Map<?, ?>) blogOptions.get("login_url");
+ if (homeURLMap != null) {
+ loginURL = homeURLMap.get("value").toString();
+ }
+ }
+ // Try to guess the login URL if blogOptions is null (blog not added to the app), or WP version is < 3.6
+ if (loginURL == null) {
+ if (blog.getUrl().lastIndexOf("/") != -1) {
+ return blog.getUrl().substring(0, blog.getUrl().lastIndexOf("/"))
+ + "/wp-login.php";
+ } else {
+ return blog.getUrl().replace("xmlrpc.php", "wp-login.php");
+ }
+ }
+
+ return loginURL;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.webview, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (mWebView == null) {
+ return false;
+ }
+
+ int itemID = item.getItemId();
+ if (itemID == R.id.menu_refresh) {
+ mWebView.reload();
+ return true;
+ } else if (itemID == R.id.menu_share) {
+ Intent share = new Intent(Intent.ACTION_SEND);
+ share.setType("text/plain");
+ // Use the preferred sharable URL or the default webview URL
+ Bundle extras = getIntent().getExtras();
+ String sharableUrl = extras.getString(SHARABLE_URL, null);
+ if (sharableUrl == null) {
+ sharableUrl = mWebView.getUrl();
+ }
+ share.putExtra(Intent.EXTRA_TEXT, sharableUrl);
+ startActivity(Intent.createChooser(share, getText(R.string.share_link)));
+ return true;
+ } else if (itemID == R.id.menu_browser) {
+ ReaderActivityLauncher.openUrl(this, mWebView.getUrl(), ReaderActivityLauncher.OpenUrlType.EXTERNAL);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java
new file mode 100644
index 000000000..98df30c6e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java
@@ -0,0 +1,160 @@
+
+package org.wordpress.android.ui;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.webkit.WebView;
+
+import org.wordpress.android.R;
+
+import java.util.Map;
+
+/**
+ * Basic activity for displaying a WebView.
+ */
+public abstract class WebViewActivity extends AppCompatActivity {
+ /** Primary webview used to display content. */
+
+ private static final String URL = "url";
+
+ protected WebView mWebView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ supportRequestWindowFeature(Window.FEATURE_PROGRESS);
+
+ super.onCreate(savedInstanceState);
+
+ // clear title text so there's no title until actual web page title can be shown
+ // this is done here rather than in the manifest to automatically handle descendants
+ // such as AuthenticatedWebViewActivity
+ setTitle("");
+
+ configureView();
+
+ // note: do NOT call mWebView.getSettings().setUserAgentString(WordPress.getUserAgent())
+ // here since it causes problems with the browser-sniffing that some sites rely on to
+ // format the page for mobile display
+ mWebView = (WebView) findViewById(R.id.webView);
+ mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
+ configureWebView();
+
+ if (savedInstanceState == null) {
+ loadContent();
+ }
+ }
+
+ /*
+ * load the desired content - only done on initial activity creation (ie: when savedInstanceState
+ * is null) since onSaveInstanceState() and onRestoreInstanceState() will take care of saving
+ * and restoring the correct URL when the activity is recreated - note that descendants should
+ * override this w/o calling super() to load a different URL.
+ */
+ protected void loadContent() {
+ String url = getIntent().getStringExtra(URL);
+ if (url != null) {
+ loadUrl(url);
+ }
+ }
+
+ /*
+ * save the webView state with the bundle so it can be restored
+ */
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ mWebView.saveState(outState);
+ super.onSaveInstanceState(outState);
+ }
+
+ /*
+ * restore the webView state saved above
+ */
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mWebView.restoreState(savedInstanceState);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // Flash video may keep playing if the webView isn't paused here
+ pauseWebView();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ resumeWebView();
+ }
+
+ public void configureView() {
+ setContentView(R.layout.webview);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ if (toolbar != null) {
+ setSupportActionBar(toolbar);
+ }
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ /*
+ * descendants should override this to set a WebViewClient, WebChromeClient, and anything
+ * else necessary to configure the webView prior to navigation
+ */
+ protected void configureWebView() {
+ // noop
+ }
+
+ private void pauseWebView() {
+ if (mWebView != null) {
+ mWebView.onPause();
+ }
+ }
+
+ private void resumeWebView() {
+ if (mWebView != null) {
+ mWebView.onResume();
+ }
+ }
+
+ /**
+ * Load the specified URL in the webview.
+ *
+ * @param url URL to load in the webview.
+ */
+ protected void loadUrl(String url) {
+ mWebView.loadUrl(url);
+ }
+
+ public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
+ mWebView.loadUrl(url, additionalHttpHeaders);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mWebView != null && mWebView.canGoBack())
+ mWebView.goBack();
+ else
+ super.onBackPressed();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java
new file mode 100644
index 000000000..d68fced44
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java
@@ -0,0 +1,306 @@
+package org.wordpress.android.ui.accounts;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.text.method.PasswordTransformationMethod;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import com.android.volley.RequestQueue;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.Volley;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.WPActivityUtils;
+
+/**
+ * A fragment representing a single step in a wizard. The fragment shows a dummy title indicating
+ * the page number, along with some dummy text.
+ */
+public abstract class AbstractFragment extends Fragment {
+ protected static RequestQueue requestQueue;
+ protected static RestClientUtils mRestClientUtils;
+ protected ConnectivityManager mSystemService;
+ protected boolean mPasswordVisible;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSystemService = (ConnectivityManager) getActivity().getApplicationContext().
+ getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (requestQueue == null) {
+ requestQueue = Volley.newRequestQueue(getActivity());
+ }
+ }
+
+ protected RestClientUtils getRestClientUtils() {
+ if (mRestClientUtils == null) {
+ mRestClientUtils = new RestClientUtils(getContext(), requestQueue, null, null);
+ }
+ return mRestClientUtils;
+ }
+
+ protected void startProgress(String message) {
+ }
+
+ protected void updateProgress(String message) {
+ }
+
+ protected void endProgress() {
+ }
+
+ protected abstract void onDoneAction();
+
+ protected abstract boolean isUserDataValid();
+
+ protected boolean onDoneEvent(int actionId, KeyEvent event) {
+ if (didPressEnterKey(actionId, event)) {
+ if (!isUserDataValid()) {
+ return true;
+ }
+
+ // hide keyboard before calling the done action
+ View view = getActivity().getCurrentFocus();
+ if (view != null) WPActivityUtils.hideKeyboard(view);
+
+ // call child action
+ onDoneAction();
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean didPressNextKey(int actionId, KeyEvent event) {
+ return actionId == EditorInfo.IME_ACTION_NEXT || event != null && (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_NAVIGATE_NEXT);
+ }
+
+ protected boolean didPressEnterKey(int actionId, KeyEvent event) {
+ return actionId == EditorInfo.IME_ACTION_DONE || event != null && (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_ENTER);
+ }
+
+ protected void initPasswordVisibilityButton(View rootView, final EditText passwordEditText) {
+ final ImageView passwordVisibility = (ImageView) rootView.findViewById(R.id.password_visibility);
+ if (passwordVisibility == null) {
+ return;
+ }
+ passwordVisibility.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mPasswordVisible = !mPasswordVisible;
+ if (mPasswordVisible) {
+ passwordVisibility.setImageResource(R.drawable.dashicon_eye_open);
+ passwordVisibility.setColorFilter(v.getContext().getResources().getColor(R.color.nux_eye_icon_color_open));
+ passwordEditText.setTransformationMethod(null);
+ } else {
+ passwordVisibility.setImageResource(R.drawable.dashicon_eye_closed);
+ passwordVisibility.setColorFilter(v.getContext().getResources().getColor(R.color.nux_eye_icon_color_closed));
+ passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
+ }
+ passwordEditText.setSelection(passwordEditText.length());
+ }
+ });
+ }
+
+ protected boolean specificShowError(int messageId) {
+ return false;
+ }
+
+ protected void showError(int messageId) {
+ if (!isAdded()) {
+ return;
+ }
+ if (specificShowError(messageId)) {
+ return;
+ }
+ // Failback if it's not a specific error
+ showError(getString(messageId));
+ }
+
+ protected void showError(String message) {
+ if (!isAdded()) {
+ return;
+ }
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ SignInDialogFragment nuxAlert = SignInDialogFragment.newInstance(getString(R.string.error), message,
+ R.drawable.noticon_alert_big, getString(R.string.nux_tap_continue));
+ ft.add(nuxAlert, "alert");
+ ft.commitAllowingStateLoss();
+ }
+
+ protected ErrorType getErrorType(int messageId) {
+ if (messageId == R.string.username_only_lowercase_letters_and_numbers ||
+ messageId == R.string.username_required || messageId == R.string.username_not_allowed ||
+ messageId == R.string.username_must_be_at_least_four_characters ||
+ messageId == R.string.username_contains_invalid_characters ||
+ messageId == R.string.username_must_include_letters || messageId == R.string.username_exists ||
+ messageId == R.string.username_reserved_but_may_be_available ||
+ messageId == R.string.username_invalid) {
+ return ErrorType.USERNAME;
+ } else if (messageId == R.string.password_invalid) {
+ return ErrorType.PASSWORD;
+ } else if (messageId == R.string.email_cant_be_used_to_signup || messageId == R.string.email_invalid ||
+ messageId == R.string.email_not_allowed || messageId == R.string.email_exists ||
+ messageId == R.string.email_reserved) {
+ return ErrorType.EMAIL;
+ } else if (messageId == R.string.blog_name_required || messageId == R.string.blog_name_not_allowed ||
+ messageId == R.string.blog_name_must_be_at_least_four_characters ||
+ messageId == R.string.blog_name_must_be_less_than_sixty_four_characters ||
+ messageId == R.string.blog_name_contains_invalid_characters ||
+ messageId == R.string.blog_name_cant_be_used ||
+ messageId == R.string.blog_name_only_lowercase_letters_and_numbers ||
+ messageId == R.string.blog_name_must_include_letters || messageId == R.string.blog_name_exists ||
+ messageId == R.string.blog_name_reserved ||
+ messageId == R.string.blog_name_reserved_but_may_be_available ||
+ messageId == R.string.blog_name_invalid) {
+ return ErrorType.SITE_URL;
+ } else if (messageId == R.string.blog_title_invalid) {
+ return ErrorType.TITLE;
+ }
+ return ErrorType.UNDEFINED;
+ }
+
+ protected int getErrorMessageForErrorCode(String errorCode) {
+ if (errorCode.equals("username_only_lowercase_letters_and_numbers")) {
+ return R.string.username_only_lowercase_letters_and_numbers;
+ }
+ if (errorCode.equals("username_required")) {
+ return R.string.username_required;
+ }
+ if (errorCode.equals("username_not_allowed")) {
+ return R.string.username_not_allowed;
+ }
+ if (errorCode.equals("email_cant_be_used_to_signup")) {
+ return R.string.email_cant_be_used_to_signup;
+ }
+ if (errorCode.equals("username_must_be_at_least_four_characters")) {
+ return R.string.username_must_be_at_least_four_characters;
+ }
+ if (errorCode.equals("username_contains_invalid_characters")) {
+ return R.string.username_contains_invalid_characters;
+ }
+ if (errorCode.equals("username_must_include_letters")) {
+ return R.string.username_must_include_letters;
+ }
+ if (errorCode.equals("email_invalid")) {
+ return R.string.email_invalid;
+ }
+ if (errorCode.equals("email_not_allowed")) {
+ return R.string.email_not_allowed;
+ }
+ if (errorCode.equals("username_exists")) {
+ return R.string.username_exists;
+ }
+ if (errorCode.equals("email_exists")) {
+ return R.string.email_exists;
+ }
+ if (errorCode.equals("username_reserved_but_may_be_available")) {
+ return R.string.username_reserved_but_may_be_available;
+ }
+ if (errorCode.equals("email_reserved")) {
+ return R.string.email_reserved;
+ }
+ if (errorCode.equals("blog_name_required")) {
+ return R.string.blog_name_required;
+ }
+ if (errorCode.equals("blog_name_not_allowed")) {
+ return R.string.blog_name_not_allowed;
+ }
+ if (errorCode.equals("blog_name_must_be_at_least_four_characters")) {
+ return R.string.blog_name_must_be_at_least_four_characters;
+ }
+ if (errorCode.equals("blog_name_must_be_less_than_sixty_four_characters")) {
+ return R.string.blog_name_must_be_less_than_sixty_four_characters;
+ }
+ if (errorCode.equals("blog_name_contains_invalid_characters")) {
+ return R.string.blog_name_contains_invalid_characters;
+ }
+ if (errorCode.equals("blog_name_cant_be_used")) {
+ return R.string.blog_name_cant_be_used;
+ }
+ if (errorCode.equals("blog_name_only_lowercase_letters_and_numbers")) {
+ return R.string.blog_name_only_lowercase_letters_and_numbers;
+ }
+ if (errorCode.equals("blog_name_must_include_letters")) {
+ return R.string.blog_name_must_include_letters;
+ }
+ if (errorCode.equals("blog_name_exists")) {
+ return R.string.blog_name_exists;
+ }
+ if (errorCode.equals("blog_name_reserved")) {
+ return R.string.blog_name_reserved;
+ }
+ if (errorCode.equals("blog_name_reserved_but_may_be_available")) {
+ return R.string.blog_name_reserved_but_may_be_available;
+ }
+ if (errorCode.equals("password_invalid")) {
+ return R.string.password_invalid;
+ }
+ if (errorCode.equals("blog_name_invalid")) {
+ return R.string.blog_name_invalid;
+ }
+ if (errorCode.equals("blog_title_invalid")) {
+ return R.string.blog_title_invalid;
+ }
+ if (errorCode.equals("username_invalid")) {
+ return R.string.username_invalid;
+ }
+ return 0;
+ }
+
+ protected enum ErrorType {USERNAME, PASSWORD, SITE_URL, EMAIL, TITLE, UNDEFINED}
+
+ public class ErrorListener implements RestRequest.ErrorListener {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ String message = null;
+ int messageId;
+ AppLog.e(T.NUX, error);
+ if (error.networkResponse != null && error.networkResponse.data != null) {
+ AppLog.e(T.NUX, String.format("Error message: %s", new String(error.networkResponse.data)));
+ String jsonString = new String(error.networkResponse.data);
+ try {
+ JSONObject errorObj = new JSONObject(jsonString);
+ messageId = getErrorMessageForErrorCode((String) errorObj.get("error"));
+ if (messageId == 0) {
+ // Not one of our common errors. Show the error message from the server.
+ message = (String) errorObj.get("message");
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.NUX, e);
+ messageId = R.string.error_generic;
+ }
+ } else {
+ if (error.getMessage() != null) {
+ if (error.getMessage().contains("Limit reached")) {
+ messageId = R.string.limit_reached;
+ } else {
+ messageId = R.string.error_generic;
+ }
+ } else {
+ messageId = R.string.error_generic;
+ }
+ }
+ endProgress();
+ if (messageId == 0) {
+ showError(message);
+ } else {
+ showError(messageId);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/BlogUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/BlogUtils.java
new file mode 100644
index 000000000..298e39395
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/BlogUtils.java
@@ -0,0 +1,191 @@
+package org.wordpress.android.ui.accounts;
+
+import android.content.Context;
+import android.text.Editable;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPUrlUtils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class BlogUtils {
+ private static final String DEFAULT_IMAGE_SIZE = "2000";
+
+ public static final int BLOG_ID_INVALID = 0;
+
+ /**
+ * Remove blogs that are not in the list and add others
+ * TODO: it's horribly slow due to datastructures used (List of Map), We should replace
+ * that by a HashSet of a specialized Blog class (that supports comparison)
+ *
+ * @return true if a change has been made (new blog added, old blog updated, blog deleted).
+ */
+ public static boolean syncBlogs(Context context, List<Map<String, Object>> newBlogList, String username) {
+ boolean retValue;
+
+ // Add all blogs from blogList
+ retValue = addBlogs(newBlogList, username);
+
+ // Delete blogs if not in blogList
+ List<Map<String, Object>> allBlogs = WordPress.wpDB.getBlogsBy("dotcomFlag=1", null);
+ Set<String> newBlogURLs = new HashSet<String>();
+ for (Map<String, Object> blog : newBlogList) {
+ newBlogURLs.add(blog.get("xmlrpc").toString() + blog.get("blogid").toString());
+ }
+ for (Map<String, Object> blog : allBlogs) {
+ if (!newBlogURLs.contains(blog.get("url").toString() + blog.get("blogId"))) {
+ WordPress.wpDB.deleteBlog(context, Integer.parseInt(blog.get("id").toString()));
+ StatsTable.deleteStatsForBlog(context, Integer.parseInt(blog.get("id").toString())); // Remove stats data
+ retValue = true;
+ }
+ }
+ return retValue;
+ }
+
+ /**
+ * Add selected blog(s) to the database.
+ *
+ * @return true if a change has been made (new blog added or old blog updated).
+ */
+ public static boolean addBlogs(List<Map<String, Object>> blogList, String username, String password,
+ String httpUsername, String httpPassword) {
+ boolean retValue = false;
+ for (Map<String, Object> blogMap : blogList) {
+ String blogName = StringUtils.unescapeHTML(blogMap.get("blogName").toString());
+ String xmlrpc = blogMap.get("xmlrpc").toString();
+ String homeUrl = blogMap.get("url").toString();
+ String blogId = blogMap.get("blogid").toString();
+ boolean isVisible = true;
+ if (blogMap.containsKey("isVisible")) {
+ isVisible = MapUtils.getMapBool(blogMap, "isVisible");
+ }
+ boolean isAdmin = MapUtils.getMapBool(blogMap, "isAdmin");
+ long planID = 0;
+ if (blogMap.containsKey("planID")) {
+ planID = MapUtils.getMapLong(blogMap, "planID");
+ }
+ String planShortName = MapUtils.getMapStr(blogMap, "plan_product_name_short");
+ String capabilities = MapUtils.getMapStr(blogMap, "capabilities");
+
+ retValue |= addOrUpdateBlog(blogName, xmlrpc, homeUrl, blogId, username, password, httpUsername,
+ httpPassword, isAdmin, isVisible, planID, planShortName, capabilities);
+ }
+ return retValue;
+ }
+
+ /**
+ * Check xmlrpc urls validity
+ *
+ * @param blogList blog list
+ * @return true if there is at least one invalid xmlrpc url
+ */
+ public static boolean isAnyInvalidXmlrpcUrl(List<Map<String, Object>> blogList) {
+ for (Map<String, Object> blogMap : blogList) {
+ String xmlrpc = blogMap.get("xmlrpc").toString();
+ if (!UrlUtils.isValidUrlAndHostNotNull(xmlrpc)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Add a new blog or update a blog name in local DB.
+ *
+ * @return true if a new blog has been added or an old blog has been updated.
+ * Return false if no change has been made.
+ */
+ public static boolean addOrUpdateBlog(String blogName, String xmlRpcUrl, String homeUrl, String blogId,
+ String username, String password, String httpUsername, String httpPassword,
+ boolean isAdmin, boolean isVisible,
+ long planID, String planShortName, String capabilities) {
+ Blog blog;
+ if (!WordPress.wpDB.isBlogInDatabase(Integer.parseInt(blogId), xmlRpcUrl)) {
+ // The blog isn't in the app, so let's create it
+ blog = new Blog(xmlRpcUrl, username, password);
+ blog.setHomeURL(homeUrl);
+ blog.setHttpuser(httpUsername);
+ blog.setHttppassword(httpPassword);
+ blog.setBlogName(blogName);
+ // deprecated
+ blog.setImagePlacement("");
+ blog.setFullSizeImage(false);
+ blog.setMaxImageWidth(DEFAULT_IMAGE_SIZE);
+ // deprecated
+ blog.setMaxImageWidthId(0);
+ blog.setRemoteBlogId(Integer.parseInt(blogId));
+ blog.setDotcomFlag(WPUrlUtils.isWordPressCom(xmlRpcUrl));
+ // assigned later in getOptions call
+ blog.setWpVersion("");
+ blog.setAdmin(isAdmin);
+ blog.setHidden(!isVisible);
+ blog.setPlanID(planID);
+ blog.setPlanShortName(planShortName);
+ blog.setCapabilities(capabilities);
+ WordPress.wpDB.saveBlog(blog);
+ return true;
+ } else {
+ // Update blog name and/or PlanID/PlanShortName
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogIdAndXmlRpcUrl(
+ Integer.parseInt(blogId), xmlRpcUrl);
+ try {
+ boolean blogUpdated = false;
+ blog = WordPress.wpDB.instantiateBlogByLocalId(localTableBlogId);
+ if (!blogName.equals(blog.getBlogName())) {
+ blog.setBlogName(blogName);
+ blogUpdated = true;
+ }
+ if (planID != blog.getPlanID()) {
+ blog.setPlanID(planID);
+ blogUpdated = true;
+ }
+ if (!blog.getPlanShortName().equals(planShortName)) {
+ blog.setPlanShortName(planShortName);
+ blogUpdated = true;
+ }
+ if (blog.getCapabilities() == null || !blog.getCapabilities().equals(capabilities)) {
+ blog.setCapabilities(capabilities);
+ blogUpdated = true;
+ }
+ if (blogUpdated) {
+ WordPress.wpDB.saveBlog(blog);
+ return true;
+ }
+ } catch (Exception e) {
+ AppLog.e(T.NUX, "localTableBlogId: " + localTableBlogId + " not found");
+ }
+ return false;
+ }
+ }
+
+ public static boolean addBlogs(List<Map<String, Object>> userBlogList, String username) {
+ return addBlogs(userBlogList, username, null, null, null);
+ }
+
+ /**
+ * Get a Blog's local Id.
+ *
+ * @param blog The Blog to get its local ID
+ * @return Blog's local id or {@value BlogUtils#BLOG_ID_INVALID} if null
+ */
+ public static int getBlogLocalId(final Blog blog) {
+ return (blog != null ? blog.getLocalTableBlogId() : BLOG_ID_INVALID);
+ }
+
+ public static void convertToLowercase(Editable s) {
+ String lowerCase = s.toString().toLowerCase();
+ if (!lowerCase.equals(s.toString())) {
+ s.replace(0, s.length(), lowerCase);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.java
new file mode 100644
index 000000000..665021994
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.java
@@ -0,0 +1,100 @@
+package org.wordpress.android.ui.accounts;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.AppLogViewerActivity;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.HelpshiftHelper.MetadataKey;
+import org.wordpress.android.util.HelpshiftHelper.Tag;
+import org.wordpress.android.widgets.WPTextView;
+
+public class HelpActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ initHelpshiftLayout();
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setElevation(0); //remove shadow
+ }
+
+ // Init common elements
+ WPTextView version = (WPTextView) findViewById(R.id.nux_help_version);
+ version.setText(getString(R.string.version) + " " + WordPress.versionName);
+
+ WPTextView applogButton = (WPTextView) findViewById(R.id.applog_button);
+ applogButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(v.getContext(), AppLogViewerActivity.class));
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.HELP_SCREEN);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void initHelpshiftLayout() {
+ setContentView(R.layout.help_activity_with_helpshift);
+
+ WPTextView version = (WPTextView) findViewById(R.id.nux_help_version);
+ version.setText(getString(R.string.version) + " " + WordPress.versionName);
+ WPTextView contactUsButton = (WPTextView) findViewById(R.id.contact_us_button);
+ contactUsButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Bundle extras = getIntent().getExtras();
+ Tag origin = Tag.ORIGIN_UNKNOWN;
+ if (extras != null) {
+ // This could be moved to WelcomeFragmentSignIn directly, but better to have all Helpshift
+ // related code at the same place (Note: value can be null).
+ HelpshiftHelper.getInstance().addMetaData(MetadataKey.USER_ENTERED_URL, extras.getString(
+ SignInFragment.ENTERED_URL_KEY));
+ HelpshiftHelper.getInstance().addMetaData(MetadataKey.USER_ENTERED_USERNAME, extras.getString(
+ SignInFragment.ENTERED_USERNAME_KEY));
+ origin = (Tag) extras.get(HelpshiftHelper.ORIGIN_KEY);
+ }
+ HelpshiftHelper.getInstance().showConversation(HelpActivity.this, origin);
+ }
+ });
+
+ WPTextView faqbutton = (WPTextView) findViewById(R.id.faq_button);
+ faqbutton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Bundle extras = getIntent().getExtras();
+ Tag origin = Tag.ORIGIN_UNKNOWN;
+ if (extras != null) {
+ origin = (Tag) extras.get(HelpshiftHelper.ORIGIN_KEY);
+ }
+ HelpshiftHelper.getInstance().showFAQ(HelpActivity.this, origin);
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogActivity.java
new file mode 100644
index 000000000..157844416
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogActivity.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.ui.accounts;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.AppCompatActivity;
+
+import org.wordpress.android.R;
+
+public class NewBlogActivity extends AppCompatActivity {
+ public static final String KEY_START_MODE = "start-mode";
+ public static final int CREATE_BLOG = 1;
+ public static final int CREATE_BLOG_LOGOUT_ON_CANCEL = 2;
+
+ private NewBlogFragment mNewBlogFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.new_blog_activity);
+
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ mNewBlogFragment = (NewBlogFragment) fragmentManager.
+ findFragmentById(R.id.new_blog_fragment);
+ if (getActionMode() == CREATE_BLOG_LOGOUT_ON_CANCEL) {
+ mNewBlogFragment.setSignoutOnCancelMode(true);
+ }
+ }
+
+ private int getActionMode() {
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ return extras.getInt(KEY_START_MODE, CREATE_BLOG);
+ }
+ return CREATE_BLOG;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mNewBlogFragment.isSignoutOnCancelMode()) {
+ mNewBlogFragment.onBackPressed();
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogFragment.java
new file mode 100644
index 000000000..244429129
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewBlogFragment.java
@@ -0,0 +1,326 @@
+package org.wordpress.android.ui.accounts;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.accounts.helpers.CreateUserAndBlog;
+import org.wordpress.android.ui.plans.PlansConstants;
+import org.wordpress.android.util.AlertUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.WPTextView;
+
+public class NewBlogFragment extends AbstractFragment implements TextWatcher {
+ private EditText mSiteUrlTextField;
+ private EditText mSiteTitleTextField;
+ private WPTextView mSignupButton;
+ private WPTextView mProgressTextSignIn;
+ private WPTextView mCancelButton;
+ private RelativeLayout mProgressBarSignIn;
+ private boolean mSignoutOnCancelMode;
+ private boolean mAutoCompleteUrl;
+
+ public NewBlogFragment() {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ checkIfFieldsFilled();
+ }
+
+ public void setSignoutOnCancelMode(boolean mode) {
+ mSignoutOnCancelMode = mode;
+ mCancelButton.setVisibility(View.VISIBLE);
+ }
+
+ public boolean isSignoutOnCancelMode() {
+ return mSignoutOnCancelMode;
+ }
+
+ public void onBackPressed() {
+ signoutAndFinish();
+ }
+
+ protected void onDoneAction() {
+ validateAndCreateUserAndBlog();
+ }
+
+ private void signoutAndFinish() {
+ if (mSignoutOnCancelMode) {
+ WordPress.WordPressComSignOut(getActivity());
+ getActivity().setResult(Activity.RESULT_CANCELED);
+ getActivity().finish();
+ }
+ }
+
+ private boolean fieldsFilled() {
+ return EditTextUtils.getText(mSiteUrlTextField).trim().length() > 0
+ && EditTextUtils.getText(mSiteTitleTextField).trim().length() > 0;
+ }
+
+ protected void startProgress(String message) {
+ mProgressBarSignIn.setVisibility(View.VISIBLE);
+ mProgressTextSignIn.setVisibility(View.VISIBLE);
+ mSignupButton.setVisibility(View.GONE);
+ mProgressBarSignIn.setEnabled(false);
+ mProgressTextSignIn.setText(message);
+ mSiteTitleTextField.setEnabled(false);
+ mSiteUrlTextField.setEnabled(false);
+ }
+
+ protected void updateProgress(String message) {
+ mProgressTextSignIn.setText(message);
+ }
+
+ protected void endProgress() {
+ mProgressBarSignIn.setVisibility(View.GONE);
+ mProgressTextSignIn.setVisibility(View.GONE);
+ mSignupButton.setVisibility(View.VISIBLE);
+ mSiteTitleTextField.setEnabled(true);
+ mSiteUrlTextField.setEnabled(true);
+ }
+
+ private void showSiteUrlError(int messageId) {
+ mSiteUrlTextField.setError(getString(messageId));
+ mSiteUrlTextField.requestFocus();
+ }
+
+ private void showSiteTitleError(int messageId) {
+ mSiteTitleTextField.setError(getString(messageId));
+ mSiteTitleTextField.requestFocus();
+ }
+
+ protected boolean specificShowError(int messageId) {
+ switch (getErrorType(messageId)) {
+ case TITLE:
+ showSiteTitleError(messageId);
+ return true;
+ case SITE_URL:
+ showSiteUrlError(messageId);
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean isUserDataValid() {
+ final String siteTitle = EditTextUtils.getText(mSiteTitleTextField).trim();
+ final String siteUrl = EditTextUtils.getText(mSiteUrlTextField).trim();
+ boolean retValue = true;
+
+ if (siteTitle.equals("")) {
+ mSiteTitleTextField.setError(getString(R.string.required_field));
+ mSiteTitleTextField.requestFocus();
+ retValue = false;
+ }
+
+ if (siteUrl.equals("")) {
+ mSiteUrlTextField.setError(getString(R.string.required_field));
+ mSiteUrlTextField.requestFocus();
+ retValue = false;
+ }
+ return retValue;
+ }
+
+ private String titleToUrl(String siteUrl) {
+ return siteUrl.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
+ }
+
+ private void validateAndCreateUserAndBlog() {
+ if (mSystemService.getActiveNetworkInfo() == null) {
+ AlertUtils.showAlert(getActivity(), R.string.no_network_title, R.string.no_network_message);
+ return;
+ }
+ if (!isUserDataValid()) {
+ return;
+ }
+
+ // prevent double tapping of the "done" btn in keyboard for those clients that don't dismiss the keyboard.
+ // Samsung S4 for example
+ if (View.VISIBLE == mProgressBarSignIn.getVisibility()) {
+ return;
+ }
+
+ startProgress(getString(R.string.validating_site_data));
+
+ final String siteUrl = EditTextUtils.getText(mSiteUrlTextField).trim();
+ final String siteName = EditTextUtils.getText(mSiteTitleTextField).trim();
+ final String language = CreateUserAndBlog.getDeviceLanguage(getActivity());
+
+ CreateUserAndBlog createUserAndBlog = new CreateUserAndBlog("", "", "", siteUrl, siteName, language,
+ getRestClientUtils(), new ErrorListener(), new CreateUserAndBlog.Callback() {
+ @Override
+ public void onStepFinished(CreateUserAndBlog.Step step) {
+ if (getActivity() != null) {
+ updateProgress(getString(R.string.create_new_blog_wpcom));
+ }
+ }
+
+ @Override
+ public void onSuccess(JSONObject createSiteResponse) {
+ if (getActivity() == null) {
+ return;
+ }
+ endProgress();
+ try {
+ JSONObject details = createSiteResponse.getJSONObject("blog_details");
+ String blogName = details.getString("blogname");
+ String xmlRpcUrl = details.getString("xmlrpc");
+ String homeUrl = details.getString("url");
+ String blogId = details.getString("blogid");
+ String username = AccountHelper.getDefaultAccount().getUserName();
+ BlogUtils.addOrUpdateBlog(blogName, xmlRpcUrl, homeUrl, blogId, username, null, null, null,
+ true, true, PlansConstants.DEFAULT_PLAN_ID_FOR_NEW_BLOG, null, null);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.CREATED_SITE);
+ ToastUtils.showToast(getActivity(), R.string.new_blog_wpcom_created);
+ } catch (JSONException e) {
+ AppLog.e(T.NUX, "Invalid JSON response from site/new", e);
+ }
+ getActivity().setResult(Activity.RESULT_OK);
+ getActivity().finish();
+ }
+
+ @Override
+ public void onError(int messageId) {
+ if (getActivity() == null) {
+ return;
+ }
+ endProgress();
+ showError(getString(messageId));
+ }
+ });
+ AppLog.i(T.NUX, "User tries to create a new site, name: " + siteName + ", URL: " + siteUrl);
+ createUserAndBlog.startCreateBlogProcess();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // Inflate the layout containing a title and body text.
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.create_blog_fragment, container, false);
+
+ mSignupButton = (WPTextView) rootView.findViewById(R.id.signup_button);
+ mSignupButton.setOnClickListener(mSignupClickListener);
+ mSignupButton.setEnabled(false);
+
+ mCancelButton = (WPTextView) rootView.findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(mCancelClickListener);
+
+ mProgressTextSignIn = (WPTextView) rootView.findViewById(R.id.nux_sign_in_progress_text);
+ mProgressBarSignIn = (RelativeLayout) rootView.findViewById(R.id.nux_sign_in_progress_bar);
+
+ mSiteUrlTextField = (EditText) rootView.findViewById(R.id.site_url);
+ mSiteUrlTextField.setOnKeyListener(mSiteUrlKeyListener);
+ mSiteUrlTextField.setOnEditorActionListener(mEditorAction);
+ mSiteUrlTextField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ checkIfFieldsFilled();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ BlogUtils.convertToLowercase(s);
+ }
+ });
+
+ mSiteTitleTextField = (EditText) rootView.findViewById(R.id.site_title);
+ mSiteTitleTextField.addTextChangedListener(this);
+ mSiteTitleTextField.addTextChangedListener(mSiteTitleWatcher);
+ mSiteTitleTextField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ mAutoCompleteUrl = EditTextUtils.getText(mSiteTitleTextField)
+ .equals(EditTextUtils.getText(mSiteUrlTextField))
+ || EditTextUtils.isEmpty(mSiteUrlTextField);
+ }
+ }
+ });
+ return rootView;
+ }
+
+ private void checkIfFieldsFilled() {
+ if (fieldsFilled()) {
+ mSignupButton.setEnabled(true);
+ } else {
+ mSignupButton.setEnabled(false);
+ }
+ }
+
+ private final OnClickListener mSignupClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ validateAndCreateUserAndBlog();
+ }
+ };
+
+ private final OnClickListener mCancelClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ signoutAndFinish();
+ }
+ };
+
+ private final TextWatcher mSiteTitleWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // auto fill blog address from title if user hasn't modified url
+ if (mAutoCompleteUrl) {
+ mSiteUrlTextField.setText(titleToUrl(EditTextUtils.getText(mSiteTitleTextField)));
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ };
+
+ private final OnKeyListener mSiteUrlKeyListener = new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ mAutoCompleteUrl = EditTextUtils.isEmpty(mSiteUrlTextField);
+ return false;
+ }
+ };
+
+ private final TextView.OnEditorActionListener mEditorAction = new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ return onDoneEvent(actionId, event);
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewUserFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewUserFragment.java
new file mode 100644
index 000000000..2a1b40dfb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/NewUserFragment.java
@@ -0,0 +1,543 @@
+package org.wordpress.android.ui.accounts;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.json.JSONObject;
+import org.wordpress.android.Constants;
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.ui.accounts.helpers.CreateUserAndBlog;
+import org.wordpress.android.ui.accounts.helpers.FetchBlogListAbstract.Callback;
+import org.wordpress.android.ui.accounts.helpers.FetchBlogListWPCom;
+import org.wordpress.android.ui.accounts.helpers.LoginAbstract;
+import org.wordpress.android.ui.accounts.helpers.LoginWPCom;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask;
+import org.wordpress.android.util.AlertUtils;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.UserEmailUtils;
+import org.wordpress.android.widgets.WPTextView;
+import org.wordpress.emailchecker2.EmailChecker;
+import org.wordpress.persistentedittext.PersistentEditTextHelper;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NewUserFragment extends AbstractFragment implements TextWatcher {
+ public static final int NEW_USER = 1;
+ private EditText mSiteUrlTextField;
+ private EditText mEmailTextField;
+ private EditText mPasswordTextField;
+ private EditText mUsernameTextField;
+ private WPTextView mSignupButton;
+ private WPTextView mProgressTextSignIn;
+ private RelativeLayout mProgressBarSignIn;
+ private boolean mEmailAutoCorrected;
+ private boolean mAutoCompleteUrl;
+ private String mUsername;
+ private String mPassword;
+
+ public static NewUserFragment newInstance() {
+ return new NewUserFragment();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ checkIfFieldsFilled();
+ }
+
+ private boolean fieldsFilled() {
+ return EditTextUtils.getText(mEmailTextField).trim().length() > 0
+ && EditTextUtils.getText(mPasswordTextField).trim().length() > 0
+ && EditTextUtils.getText(mUsernameTextField).trim().length() > 0
+ && EditTextUtils.getText(mSiteUrlTextField).trim().length() > 0;
+ }
+
+ protected void startProgress(String message) {
+ mProgressBarSignIn.setVisibility(View.VISIBLE);
+ mProgressTextSignIn.setVisibility(View.VISIBLE);
+ mSignupButton.setVisibility(View.GONE);
+ mProgressBarSignIn.setEnabled(false);
+ mProgressTextSignIn.setText(message);
+ mEmailTextField.setEnabled(false);
+ mPasswordTextField.setEnabled(false);
+ mUsernameTextField.setEnabled(false);
+ mSiteUrlTextField.setEnabled(false);
+ }
+
+ protected void updateProgress(String message) {
+ mProgressTextSignIn.setText(message);
+ }
+
+ protected void endProgress() {
+ mProgressBarSignIn.setVisibility(View.GONE);
+ mProgressTextSignIn.setVisibility(View.GONE);
+ mSignupButton.setVisibility(View.VISIBLE);
+ mEmailTextField.setEnabled(true);
+ mPasswordTextField.setEnabled(true);
+ mUsernameTextField.setEnabled(true);
+ mSiteUrlTextField.setEnabled(true);
+ }
+
+ protected boolean isUserDataValid() {
+ // try to create the user
+ final String email = EditTextUtils.getText(mEmailTextField).trim();
+ final String password = EditTextUtils.getText(mPasswordTextField).trim();
+ final String username = EditTextUtils.getText(mUsernameTextField).trim();
+ final String siteUrl = EditTextUtils.getText(mSiteUrlTextField).trim();
+ boolean retValue = true;
+
+ if (email.equals("")) {
+ showEmailError(R.string.required_field);
+ retValue = false;
+ }
+
+ final Pattern emailRegExPattern = Patterns.EMAIL_ADDRESS;
+ Matcher matcher = emailRegExPattern.matcher(email);
+ if (!matcher.find() || email.length() > 100) {
+ showEmailError(R.string.invalid_email_message);
+ retValue = false;
+ }
+
+ if (username.equals("")) {
+ showUsernameError(R.string.required_field);
+ retValue = false;
+ }
+
+ if (username.length() < 4) {
+ showUsernameError(R.string.invalid_username_too_short);
+ retValue = false;
+ }
+
+ if (username.length() > 60) {
+ showUsernameError(R.string.invalid_username_too_long);
+ retValue = false;
+ }
+
+ if (username.contains(" ")) {
+ showUsernameError(R.string.invalid_username_no_spaces);
+ retValue = false;
+ }
+
+ if (siteUrl.contains(" ")) {
+ showSiteUrlError(R.string.blog_name_no_spaced_allowed);
+ retValue = false;
+ }
+
+ if (siteUrl.length() < 4) {
+ showSiteUrlError(R.string.blog_name_must_be_at_least_four_characters);
+ retValue = false;
+ }
+
+ if (password.equals("")) {
+ showPasswordError(R.string.required_field);
+ retValue = false;
+ }
+
+ if (password.length() < 4) {
+ showPasswordError(R.string.invalid_password_message);
+ retValue = false;
+ }
+
+ return retValue;
+ }
+
+ protected void onDoneAction() {
+ validateAndCreateUserAndBlog();
+ }
+
+ private final OnClickListener mSignupClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ validateAndCreateUserAndBlog();
+ }
+ };
+
+ private final TextView.OnEditorActionListener mEditorAction = new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ return onDoneEvent(actionId, event);
+ }
+ };
+
+ private String siteUrlToSiteName(String siteUrl) {
+ return siteUrl;
+ }
+
+ protected boolean specificShowError(int messageId) {
+ switch (getErrorType(messageId)) {
+ case USERNAME:
+ showUsernameError(messageId);
+ return true;
+ case PASSWORD:
+ showPasswordError(messageId);
+ return true;
+ case EMAIL:
+ showEmailError(messageId);
+ return true;
+ case SITE_URL:
+ showSiteUrlError(messageId);
+ return true;
+ }
+ return false;
+ }
+
+ private void showPasswordError(int messageId) {
+ mPasswordTextField.setError(getString(messageId));
+ mPasswordTextField.requestFocus();
+ }
+
+ private void showEmailError(int messageId) {
+ mEmailTextField.setError(getString(messageId));
+ mEmailTextField.requestFocus();
+ }
+
+ private void showUsernameError(int messageId) {
+ mUsernameTextField.setError(getString(messageId));
+ mUsernameTextField.requestFocus();
+ }
+
+ private void showSiteUrlError(int messageId) {
+ mSiteUrlTextField.setError(getString(messageId));
+ mSiteUrlTextField.requestFocus();
+ }
+
+ private void validateAndCreateUserAndBlog() {
+ if (mSystemService.getActiveNetworkInfo() == null) {
+ AlertUtils.showAlert(getActivity(), R.string.no_network_title, R.string.no_network_message);
+ return;
+ }
+ if (!isUserDataValid()) {
+ return;
+ }
+
+ // Prevent double tapping of the "done" btn in keyboard for those clients that don't dismiss the keyboard.
+ // Samsung S4 for example
+ if (View.VISIBLE == mProgressBarSignIn.getVisibility()) {
+ return;
+ }
+
+ startProgress(getString(R.string.validating_user_data));
+
+ final String siteUrl = EditTextUtils.getText(mSiteUrlTextField).trim();
+ final String email = EditTextUtils.getText(mEmailTextField).trim();
+ mUsername = EditTextUtils.getText(mUsernameTextField).trim();
+ mPassword = EditTextUtils.getText(mPasswordTextField).trim();
+ final String siteName = siteUrlToSiteName(siteUrl);
+ final String language = CreateUserAndBlog.getDeviceLanguage(getActivity());
+
+ CreateUserAndBlog createUserAndBlog = new CreateUserAndBlog(email, mUsername, mPassword,
+ siteUrl, siteName, language, getRestClientUtils(), new ErrorListener(),
+ new CreateUserAndBlog.Callback() {
+ @Override
+ public void onStepFinished(CreateUserAndBlog.Step step) {
+ if (!isAdded()) {
+ return;
+ }
+ switch (step) {
+ case VALIDATE_USER:
+ updateProgress(getString(R.string.validating_site_data));
+ break;
+ case VALIDATE_SITE:
+ updateProgress(getString(R.string.creating_your_account));
+ break;
+ case CREATE_USER:
+ updateProgress(getString(R.string.creating_your_site));
+ break;
+ case CREATE_SITE:
+ // no messages
+ case AUTHENTICATE_USER:
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onSuccess(JSONObject createSiteResponse) {
+ // User has been created. From this point, all errors should close this screen and display the
+ // sign in screen
+ AnalyticsUtils.refreshMetadata(mUsername, email);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.CREATED_ACCOUNT);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.CREATED_SITE);
+ // Save credentials to smart lock
+ SmartLockHelper smartLockHelper = getSmartLockHelper();
+ if (smartLockHelper != null) {
+ smartLockHelper.saveCredentialsInSmartLock(mUsername, mPassword, mUsername, null);
+ }
+ if (isAdded()) {
+ signInAndFetchBlogListWPCom();
+ }
+ }
+
+ @Override
+ public void onError(int messageId) {
+ endProgress();
+ if (isAdded()) {
+ showError(getString(messageId));
+ }
+ }
+ });
+ AppLog.i(T.NUX, "User tries to create a new account, username: " + mUsername + ", email: " + email
+ + ", site name: " + siteName + ", site URL: " + siteUrl);
+ createUserAndBlog.startCreateUserAndBlogProcess();
+ }
+
+ private void signInAndFetchBlogListWPCom() {
+ updateProgress(getString(R.string.signing_in));
+ LoginWPCom login = new LoginWPCom(mUsername, mPassword, null, false, null);
+ login.execute(new LoginAbstract.Callback() {
+ @Override
+ public void onSuccess() {
+ FetchBlogListWPCom fetchBlogListWPCom = new FetchBlogListWPCom(getActivity());
+ fetchBlogListWPCom.execute(mFetchBlogListCallback);
+ }
+
+ @Override
+ public void onError(int errorMessageId, boolean twoStepCodeRequired, boolean httpAuthRequired,
+ boolean erroneousSslCertificate) {
+ // Should not happen (excepted for a timeout), go back to the sign in screen
+ finishAndShowSignInScreen();
+ }
+ });
+ }
+
+ private void finishCurrentActivity() {
+ if (!isAdded()) {
+ return;
+ }
+ getActivity().setResult(Activity.RESULT_OK);
+ getActivity().finish();
+ PersistentEditTextHelper persistentEditTextHelper = new PersistentEditTextHelper(getActivity());
+ persistentEditTextHelper.clearSavedText(mEmailTextField, null);
+ persistentEditTextHelper.clearSavedText(mUsernameTextField, null);
+ persistentEditTextHelper.clearSavedText(mSiteUrlTextField, null);
+ }
+
+ /**
+ * In case an error happened after the user creation steps, we don't want to show the sign up screen.
+ * Show the sign in screen with username and password prefilled, plus a toast message to explain what happened.
+ *
+ * Note: this should be called only if the user has been created.
+ */
+ private void finishAndShowSignInScreen() {
+ if (!isAdded()) {
+ return;
+ }
+ endProgress();
+ Intent intent = new Intent();
+ intent.putExtra("username", mUsername);
+ intent.putExtra("password", mPassword);
+ getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, intent);
+ try {
+ getFragmentManager().popBackStack();
+ } catch (IllegalStateException e) {
+ // Catch the ISE exception, because we can't check for the fragment state here
+ // finishAndShowSignInScreen will be called in an Network onError callback so we can't guarantee, the
+ // fragment transaction will be executed. In that case the user already is back on the Sign In screen.
+ AppLog.e(T.NUX, e);
+ }
+ ToastUtils.showToast(getActivity(), R.string.signup_succeed_signin_failed, Duration.LONG);
+ }
+
+ protected final Callback mFetchBlogListCallback = new Callback() {
+ @Override
+ public void onSuccess(final List<Map<String, Object>> userBlogList) {
+ if (!isAdded()) {
+ return;
+ }
+ if (userBlogList != null) {
+ BlogUtils.addBlogs(userBlogList, mUsername);
+ }
+
+ // get reader tags so they're available as soon as the Reader is accessed - done for
+ // both wp.com and self-hosted (self-hosted = "logged out" reader) - note that this
+ // uses the application context since the activity is finished immediately below
+ ReaderUpdateService.startService(getActivity().getApplicationContext(),
+ EnumSet.of(UpdateTask.TAGS));
+ finishCurrentActivity();
+ }
+
+ @Override
+ public void onError(final int messageId, final boolean twoStepCodeRequired, final boolean httpAuthRequired,
+ final boolean erroneousSslCertificate, final String clientResponse) {
+ // Should not happen (excepted for a timeout), go back to the sign in screen
+ finishAndShowSignInScreen();
+ }
+ };
+
+ private void autocorrectEmail() {
+ if (mEmailAutoCorrected) {
+ return;
+ }
+ final String email = EditTextUtils.getText(mEmailTextField).trim();
+ String suggest = EmailChecker.suggestDomainCorrection(email);
+ if (suggest.compareTo(email) != 0) {
+ mEmailAutoCorrected = true;
+ mEmailTextField.setText(suggest);
+ mEmailTextField.setSelection(suggest.length());
+ }
+ }
+
+ private void initInfoButton(View rootView) {
+ ImageView infoBUtton = (ImageView) rootView.findViewById(R.id.info_button);
+ infoBUtton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent newAccountIntent = new Intent(getActivity(), HelpActivity.class);
+ startActivity(newAccountIntent);
+ }
+ });
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout containing a title and body text.
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.new_account_user_fragment_screen, container, false);
+
+ WPTextView termsOfServiceTextView = (WPTextView) rootView.findViewById(R.id.l_agree_terms_of_service);
+ termsOfServiceTextView.setText(Html.fromHtml(String.format(getString(R.string.agree_terms_of_service), "<u>",
+ "</u>")));
+ termsOfServiceTextView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Uri uri = Uri.parse(Constants.URL_TOS);
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ }
+ );
+
+ mSignupButton = (WPTextView) rootView.findViewById(R.id.signup_button);
+ mSignupButton.setOnClickListener(mSignupClickListener);
+ mSignupButton.setEnabled(false);
+
+ mProgressTextSignIn = (WPTextView) rootView.findViewById(R.id.nux_sign_in_progress_text);
+ mProgressBarSignIn = (RelativeLayout) rootView.findViewById(R.id.nux_sign_in_progress_bar);
+
+ mEmailTextField = (EditText) rootView.findViewById(R.id.email_address);
+ mEmailTextField.setText(UserEmailUtils.getPrimaryEmail(getActivity()));
+ mEmailTextField.setSelection(EditTextUtils.getText(mEmailTextField).length());
+ mPasswordTextField = (EditText) rootView.findViewById(R.id.password);
+ mUsernameTextField = (EditText) rootView.findViewById(R.id.username);
+ mSiteUrlTextField = (EditText) rootView.findViewById(R.id.site_url);
+
+ mEmailTextField.addTextChangedListener(this);
+ mPasswordTextField.addTextChangedListener(this);
+ mUsernameTextField.addTextChangedListener(this);
+ mSiteUrlTextField.setOnKeyListener(mSiteUrlKeyListener);
+ mSiteUrlTextField.setOnEditorActionListener(mEditorAction);
+
+ mSiteUrlTextField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ checkIfFieldsFilled();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ BlogUtils.convertToLowercase(s);
+ }
+ });
+
+ mUsernameTextField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // auto fill blog address
+ mSiteUrlTextField.setError(null);
+ if (mAutoCompleteUrl) {
+ mSiteUrlTextField.setText(EditTextUtils.getText(mUsernameTextField));
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ BlogUtils.convertToLowercase(s);
+ }
+ });
+ mUsernameTextField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ mAutoCompleteUrl = EditTextUtils.getText(mUsernameTextField)
+ .equals(EditTextUtils.getText(mSiteUrlTextField))
+ || EditTextUtils.isEmpty(mSiteUrlTextField);
+ }
+ }
+ });
+
+ mEmailTextField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus) {
+ autocorrectEmail();
+ }
+ }
+ });
+ initPasswordVisibilityButton(rootView, mPasswordTextField);
+ initInfoButton(rootView);
+ return rootView;
+ }
+
+ private void checkIfFieldsFilled() {
+ if (fieldsFilled()) {
+ mSignupButton.setEnabled(true);
+ } else {
+ mSignupButton.setEnabled(false);
+ }
+ }
+
+ private final OnKeyListener mSiteUrlKeyListener = new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ mAutoCompleteUrl = EditTextUtils.isEmpty(mSiteUrlTextField);
+ return false;
+ }
+ };
+
+ private SmartLockHelper getSmartLockHelper() {
+ if (getActivity() != null && getActivity() instanceof SignInActivity) {
+ return ((SignInActivity) getActivity()).getSmartLockHelper();
+ }
+ return null;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java
new file mode 100644
index 000000000..8f544986b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java
@@ -0,0 +1,179 @@
+package org.wordpress.android.ui.accounts;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Window;
+
+import com.google.android.gms.auth.api.credentials.Credential;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.accounts.SmartLockHelper.Callback;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class SignInActivity extends AppCompatActivity implements ConnectionCallbacks, OnConnectionFailedListener {
+ public static final int SIGN_IN_REQUEST = 1;
+ public static final int REQUEST_CODE = 5000;
+ public static final int ADD_SELF_HOSTED_BLOG = 2;
+ public static final int SHOW_CERT_DETAILS = 4;
+ public static final int SMART_LOCK_SAVE = 5;
+ public static final int SMART_LOCK_READ = 6;
+
+ public static final String EXTRA_START_FRAGMENT = "start-fragment";
+ public static final String EXTRA_JETPACK_SITE_AUTH = "EXTRA_JETPACK_SITE_AUTH";
+ public static final String EXTRA_JETPACK_MESSAGE_AUTH = "EXTRA_JETPACK_MESSAGE_AUTH";
+ public static final String EXTRA_IS_AUTH_ERROR = "EXTRA_IS_AUTH_ERROR";
+ public static final String EXTRA_PREFILL_URL = "EXTRA_PREFILL_URL";
+
+ private SmartLockHelper mSmartLockHelper;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.welcome_activity);
+
+ String action = getIntent().getAction();
+ Uri data = getIntent().getData();
+
+ if (Intent.ACTION_VIEW.equals(action) && data != null) {
+ if (data.getBooleanQueryParameter("selfhosted", false)) {
+ getIntent().putExtra(SignInActivity.EXTRA_START_FRAGMENT, SignInActivity.ADD_SELF_HOSTED_BLOG);
+ if (data.getQueryParameter("url") != null) {
+ getIntent().putExtra(EXTRA_PREFILL_URL, data.getQueryParameter("url"));
+ }
+ }
+ }
+
+ if (savedInstanceState == null) {
+ addSignInFragment();
+ }
+
+ mSmartLockHelper = new SmartLockHelper(this);
+ mSmartLockHelper.initSmartLockForPasswords();
+
+ ActivityId.trackLastActivity(ActivityId.LOGIN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ actionMode(getIntent().getExtras());
+ }
+
+ protected void addSignInFragment() {
+ SignInFragment signInFragment = new SignInFragment();
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ fragmentTransaction.replace(R.id.fragment_container, signInFragment, SignInFragment.TAG);
+ fragmentTransaction.commit();
+ }
+
+ protected void actionMode(Bundle extras) {
+ int actionMode = SIGN_IN_REQUEST;
+ String prefillUrl = "";
+ if (extras != null) {
+ actionMode = extras.getInt(EXTRA_START_FRAGMENT, -1);
+ if (extras.containsKey(EXTRA_JETPACK_SITE_AUTH)) {
+ Blog jetpackBlog = WordPress.getBlog(extras.getInt(EXTRA_JETPACK_SITE_AUTH));
+ if (jetpackBlog != null) {
+ String customMessage = extras.getString(EXTRA_JETPACK_MESSAGE_AUTH, null);
+ getSignInFragment().setBlogAndCustomMessageForJetpackAuth(jetpackBlog, customMessage);
+ }
+ } else if (extras.containsKey(EXTRA_IS_AUTH_ERROR)) {
+ getSignInFragment().showAuthErrorMessage();
+ }
+ prefillUrl = extras.getString(EXTRA_PREFILL_URL, "");
+ }
+ switch (actionMode) {
+ case ADD_SELF_HOSTED_BLOG:
+ getSignInFragment().forceSelfHostedMode(prefillUrl);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == SHOW_CERT_DETAILS) {
+ getSignInFragment().askForSslTrust();
+ } else if (requestCode == SMART_LOCK_SAVE) {
+ if (resultCode == RESULT_OK) {
+ AnalyticsTracker.track(Stat.LOGIN_AUTOFILL_CREDENTIALS_UPDATED);
+ AppLog.d(T.NUX, "Credentials saved");
+ } else {
+ AppLog.d(T.NUX, "Credentials save cancelled");
+ }
+ } else if (requestCode == SMART_LOCK_READ) {
+ if (resultCode == RESULT_OK) {
+ AppLog.d(T.NUX, "Credentials retrieved");
+ Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
+ SignInFragment signInFragment =
+ (SignInFragment) getSupportFragmentManager().findFragmentByTag(SignInFragment.TAG);
+ if (signInFragment != null) {
+ signInFragment.onCredentialRetrieved(credential);
+ }
+ } else {
+ AppLog.e(T.NUX, "Credential read failed");
+ }
+ }
+ }
+
+ public SignInFragment getSignInFragment() {
+ SignInFragment signInFragment = (SignInFragment) getSupportFragmentManager().findFragmentByTag(SignInFragment.TAG);
+ if (signInFragment == null) {
+ return new SignInFragment();
+ } else {
+ return signInFragment;
+ }
+ }
+
+ public SmartLockHelper getSmartLockHelper() {
+ return mSmartLockHelper;
+ }
+
+ @Override
+ public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
+ AppLog.d(T.NUX, "Connection result: " + connectionResult);
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ AppLog.d(T.NUX, "Google API client connected");
+ SignInFragment signInFragment =
+ (SignInFragment) getSupportFragmentManager().findFragmentByTag(SignInFragment.TAG);
+ // Autofill only if signInFragment is there and if it can be autofilled (ie. username and password fields are
+ // empty).
+ if (signInFragment != null && signInFragment.canAutofillUsernameAndPassword()) {
+ mSmartLockHelper.smartLockAutoFill(new Callback() {
+ @Override
+ public void onCredentialRetrieved(Credential credential) {
+ SignInFragment signInFragment =
+ (SignInFragment) getSupportFragmentManager().findFragmentByTag(SignInFragment.TAG);
+ if (signInFragment != null) {
+ signInFragment.onCredentialRetrieved(credential);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ AppLog.d(T.NUX, "Google API client connection suspended");
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInDialogFragment.java
new file mode 100644
index 000000000..6df3b5369
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInDialogFragment.java
@@ -0,0 +1,170 @@
+package org.wordpress.android.ui.accounts;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.AppLogViewerActivity;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.HelpshiftHelper.MetadataKey;
+import org.wordpress.android.util.HelpshiftHelper.Tag;
+import org.wordpress.android.widgets.WPTextView;
+
+public class SignInDialogFragment extends DialogFragment {
+ private static String ARG_TITLE = "title";
+ private static String ARG_DESCRIPTION = "message";
+ private static String ARG_FOOTER = "footer";
+ private static String ARG_IMAGE = "image";
+ private static String ARG_NUMBER_OF_BUTTONS = "number-of-buttons";
+ private static String ARG_FIRST_BUTTON_LABEL = "first-btn-label";
+ private static String ARG_SECOND_BUTTON_LABEL = "second-btn-label";
+ private static String ARG_THIRD_BUTTON_LABEL = "third-btn-label";
+ private static String ARG_SECOND_BUTTON_ACTION = "second-btn-action";
+ private static String ARG_THIRD_BUTTON_ACTION = "third-btn-action";
+ public static String ARG_OPEN_URL_PARAM = "open-url-param";
+
+ private ImageView mImageView;
+ private WPTextView mTitleTextView;
+ private WPTextView mDescriptionTextView;
+ private WPTextView mFooterBottomButton;
+ private WPTextView mFooterCenterButton;
+ private WPTextView mFooterTopButton;
+
+ public static final int ACTION_FINISH = 1;
+ public static final int ACTION_OPEN_URL = 2;
+ public static final int ACTION_OPEN_SUPPORT_CHAT = 3;
+ public static final int ACTION_OPEN_APPLICATION_LOG = 4;
+
+ public SignInDialogFragment() {
+ // Empty constructor required for DialogFragment
+ }
+
+ public static SignInDialogFragment newInstance(String title, String message, int imageSource, String buttonLabel) {
+ return newInstance(title, message, imageSource, 1, buttonLabel, "", "", 0, 0);
+ }
+
+ public static SignInDialogFragment newInstance(String title, String message, int imageSource, int numberOfButtons,
+ String firstButtonLabel, String secondButtonLabel,
+ String thirdButtonLabel, int secondButtonAction,
+ int thirdButtonAction) {
+ SignInDialogFragment adf = new SignInDialogFragment();
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_TITLE, title);
+ bundle.putString(ARG_DESCRIPTION, message);
+ bundle.putInt(ARG_IMAGE, imageSource);
+ bundle.putInt(ARG_NUMBER_OF_BUTTONS, numberOfButtons);
+ bundle.putString(ARG_FIRST_BUTTON_LABEL, firstButtonLabel);
+ bundle.putString(ARG_SECOND_BUTTON_LABEL, secondButtonLabel);
+ bundle.putString(ARG_THIRD_BUTTON_LABEL, thirdButtonLabel);
+ bundle.putInt(ARG_SECOND_BUTTON_ACTION, secondButtonAction);
+ bundle.putInt(ARG_THIRD_BUTTON_ACTION, thirdButtonAction);
+
+ adf.setArguments(bundle);
+ adf.setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme);
+ return adf;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().getWindow().setBackgroundDrawable(getResources().getDrawable(R.color.nux_alert_bg));
+ View v = inflater.inflate(R.layout.signin_dialog_fragment, container, false);
+
+ mImageView = (ImageView) v.findViewById(R.id.nux_dialog_image);
+ mTitleTextView = (WPTextView) v.findViewById(R.id.nux_dialog_title);
+ mDescriptionTextView = (WPTextView) v.findViewById(R.id.nux_dialog_description);
+ mFooterBottomButton = (WPTextView) v.findViewById(R.id.nux_dialog_left_button);
+ mFooterCenterButton = (WPTextView) v.findViewById(R.id.nux_dialog_center_button);
+ mFooterTopButton = (WPTextView) v.findViewById(R.id.nux_dialog_right_button);
+ final Bundle arguments = getArguments();
+
+ mTitleTextView.setText(arguments.getString(ARG_TITLE));
+ mDescriptionTextView.setText(arguments.getString(ARG_DESCRIPTION));
+ mImageView.setImageResource(arguments.getInt(ARG_IMAGE));
+
+ View.OnClickListener clickListenerDismiss = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismissAllowingStateLoss();
+ }
+ };
+
+ View.OnClickListener clickListenerSecondButton = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onClickAction(v, arguments.getInt(ARG_SECOND_BUTTON_ACTION, 0), arguments);
+ }
+ };
+
+ View.OnClickListener clickListenerThirdButton = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onClickAction(v, arguments.getInt(ARG_THIRD_BUTTON_ACTION, 0), arguments);
+ }
+ };
+
+ switch (arguments.getInt(ARG_NUMBER_OF_BUTTONS, 1)) {
+ case 1:
+ // One button: we keep only the centered button
+ mFooterCenterButton.setText(arguments.getString(ARG_FIRST_BUTTON_LABEL));
+ mFooterCenterButton.setOnClickListener(clickListenerDismiss);
+ mFooterBottomButton.setVisibility(View.GONE);
+ mFooterTopButton.setVisibility(View.GONE);
+ break;
+ case 2:
+ // Two buttons: we keep only the left and right buttons
+ mFooterBottomButton.setText(arguments.getString(ARG_FIRST_BUTTON_LABEL));
+ mFooterTopButton.setText(arguments.getString(ARG_SECOND_BUTTON_LABEL));
+ mFooterCenterButton.setVisibility(View.GONE);
+ mFooterTopButton.setOnClickListener(clickListenerSecondButton);
+ break;
+ case 3:
+ mFooterBottomButton.setText(arguments.getString(ARG_FIRST_BUTTON_LABEL));
+ mFooterCenterButton.setText(arguments.getString(ARG_SECOND_BUTTON_LABEL));
+ mFooterCenterButton.setOnClickListener(clickListenerSecondButton);
+ mFooterTopButton.setText(arguments.getString(ARG_THIRD_BUTTON_LABEL));
+ mFooterTopButton.setOnClickListener(clickListenerThirdButton);
+ break;
+ }
+ v.setClickable(true);
+ v.setOnClickListener(clickListenerDismiss);
+ mFooterBottomButton.setOnClickListener(clickListenerDismiss);
+ return v;
+ }
+
+ private void onClickAction(View v, int action, Bundle arguments) {
+ if (!isAdded()) {
+ return;
+ }
+ switch (action) {
+ case ACTION_OPEN_URL:
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(arguments.getString(ARG_OPEN_URL_PARAM)));
+ startActivity(intent);
+ dismissAllowingStateLoss();
+ break;
+ case ACTION_OPEN_SUPPORT_CHAT:
+ HelpshiftHelper.getInstance().addMetaData(MetadataKey.USER_ENTERED_URL, arguments.getString(
+ SignInFragment.ENTERED_URL_KEY));
+ HelpshiftHelper.getInstance().addMetaData(MetadataKey.USER_ENTERED_USERNAME, arguments.getString(
+ SignInFragment.ENTERED_USERNAME_KEY));
+ HelpshiftHelper.getInstance().showConversation(getActivity(), Tag.ORIGIN_LOGIN_SCREEN_ERROR);
+ dismissAllowingStateLoss();
+ break;
+ case ACTION_OPEN_APPLICATION_LOG:
+ startActivity(new Intent(v.getContext(), AppLogViewerActivity.class));
+ dismissAllowingStateLoss();
+ break;
+ default:
+ case ACTION_FINISH:
+ getActivity().finish();
+ break;
+ }
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java
new file mode 100644
index 000000000..584a0b12e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java
@@ -0,0 +1,1010 @@
+package org.wordpress.android.ui.accounts;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.google.android.gms.auth.api.credentials.Credential;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.SelfSignedSSLCertsManager;
+import org.wordpress.android.ui.accounts.helpers.FetchBlogListAbstract.Callback;
+import org.wordpress.android.ui.accounts.helpers.FetchBlogListWPCom;
+import org.wordpress.android.ui.accounts.helpers.FetchBlogListWPOrg;
+import org.wordpress.android.ui.accounts.helpers.LoginAbstract;
+import org.wordpress.android.ui.accounts.helpers.LoginWPCom;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.GenericCallback;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.HelpshiftHelper.Tag;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPUrlUtils;
+import org.wordpress.android.widgets.WPTextView;
+import org.wordpress.emailchecker2.EmailChecker;
+import org.xmlrpc.android.ApiHelper;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SignInFragment extends AbstractFragment implements TextWatcher {
+ public static final String TAG = "sign_in_fragment_tag";
+ private static final String DOT_COM_BASE_URL = "https://wordpress.com";
+ private static final String FORGOT_PASSWORD_RELATIVE_URL = "/wp-login.php?action=lostpassword";
+ private static final int WPCOM_ERRONEOUS_LOGIN_THRESHOLD = 3;
+ private static final String KEY_IS_SELF_HOSTED = "IS_SELF_HOSTED";
+
+ public static final String ENTERED_URL_KEY = "ENTERED_URL_KEY";
+ public static final String ENTERED_USERNAME_KEY = "ENTERED_USERNAME_KEY";
+
+ protected EditText mUsernameEditText;
+ protected EditText mPasswordEditText;
+ protected EditText mUrlEditText;
+ protected EditText mTwoStepEditText;
+
+ protected LinearLayout mBottomButtonsLayout;
+ protected RelativeLayout mUsernameLayout;
+ protected RelativeLayout mPasswordLayout;
+ protected RelativeLayout mProgressBarSignIn;
+ protected RelativeLayout mUrlButtonLayout;
+ protected RelativeLayout mTwoStepLayout;
+ protected LinearLayout mTwoStepFooter;
+
+ protected boolean mSelfHosted;
+ protected boolean mEmailAutoCorrected;
+ protected boolean mShouldSendTwoStepSMS;
+ protected int mErroneousLogInCount;
+ protected String mUsername;
+ protected String mPassword;
+ protected String mTwoStepCode;
+ protected String mHttpUsername;
+ protected String mHttpPassword;
+ protected Blog mJetpackBlog;
+
+ protected WPTextView mSignInButton;
+ protected WPTextView mCreateAccountButton;
+ protected WPTextView mAddSelfHostedButton;
+ protected WPTextView mProgressTextSignIn;
+ protected WPTextView mForgotPassword;
+ protected WPTextView mJetpackAuthLabel;
+ protected ImageView mInfoButton;
+ protected ImageView mInfoButtonSecondary;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mSelfHosted = savedInstanceState.getBoolean(KEY_IS_SELF_HOSTED);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.signin_fragment, container, false);
+ mUrlButtonLayout = (RelativeLayout) rootView.findViewById(R.id.url_button_layout);
+ mTwoStepLayout = (RelativeLayout) rootView.findViewById(R.id.two_factor_layout);
+ mTwoStepFooter = (LinearLayout) rootView.findViewById(R.id.two_step_footer);
+ mUsernameLayout = (RelativeLayout) rootView.findViewById(R.id.nux_username_layout);
+ mUsernameLayout.setOnClickListener(mOnLoginFormClickListener);
+ mPasswordLayout = (RelativeLayout) rootView.findViewById(R.id.nux_password_layout);
+ mPasswordLayout.setOnClickListener(mOnLoginFormClickListener);
+
+ mUsernameEditText = (EditText) rootView.findViewById(R.id.nux_username);
+ mUsernameEditText.addTextChangedListener(this);
+ mUsernameEditText.setOnClickListener(mOnLoginFormClickListener);
+ mPasswordEditText = (EditText) rootView.findViewById(R.id.nux_password);
+ mPasswordEditText.addTextChangedListener(this);
+ mPasswordEditText.setOnClickListener(mOnLoginFormClickListener);
+ mJetpackAuthLabel = (WPTextView) rootView.findViewById(R.id.nux_jetpack_auth_label);
+ mUrlEditText = (EditText) rootView.findViewById(R.id.nux_url);
+ mSignInButton = (WPTextView) rootView.findViewById(R.id.nux_sign_in_button);
+ mSignInButton.setOnClickListener(mSignInClickListener);
+ mProgressBarSignIn = (RelativeLayout) rootView.findViewById(R.id.nux_sign_in_progress_bar);
+ mProgressTextSignIn = (WPTextView) rootView.findViewById(R.id.nux_sign_in_progress_text);
+ mCreateAccountButton = (WPTextView) rootView.findViewById(R.id.nux_create_account_button);
+ mCreateAccountButton.setOnClickListener(mCreateAccountListener);
+ mAddSelfHostedButton = (WPTextView) rootView.findViewById(R.id.nux_add_selfhosted_button);
+ mAddSelfHostedButton.setText(getString(R.string.nux_add_selfhosted_blog));
+ mAddSelfHostedButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleSignInMode();
+ }
+ });
+
+ mForgotPassword = (WPTextView) rootView.findViewById(R.id.forgot_password);
+ mForgotPassword.setOnClickListener(mForgotPasswordListener);
+ mUsernameEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus) {
+ autocorrectUsername();
+ }
+ }
+ });
+
+ mPasswordEditText.setOnEditorActionListener(mEditorAction);
+ mUrlEditText.setOnEditorActionListener(mEditorAction);
+
+ mTwoStepEditText = (EditText) rootView.findViewById(R.id.nux_two_step);
+ mTwoStepEditText.addTextChangedListener(this);
+ mTwoStepEditText.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (keyCode == EditorInfo.IME_ACTION_DONE)) {
+ if (fieldsFilled()) {
+ signIn();
+ }
+ }
+
+ return false;
+ }
+ });
+
+ WPTextView twoStepFooterButton = (WPTextView) rootView.findViewById(R.id.two_step_footer_button);
+ twoStepFooterButton.setText(Html.fromHtml("<u>" + getString(R.string.two_step_footer_button) + "</u>"));
+ twoStepFooterButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ requestSMSTwoStepCode();
+ }
+ });
+
+ mBottomButtonsLayout = (LinearLayout) rootView.findViewById(R.id.nux_bottom_buttons);
+ initPasswordVisibilityButton(rootView, mPasswordEditText);
+ initInfoButtons(rootView);
+ moveBottomButtons();
+
+ if (mSelfHosted) {
+ showSelfHostedSignInForm();
+ }
+ autofillFromBuildConfig();
+ return rootView;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Ensure two-step form is shown if needed
+ if (!TextUtils.isEmpty(mTwoStepEditText.getText()) && mTwoStepLayout.getVisibility() == View.GONE) {
+ setTwoStepAuthVisibility(true);
+ }
+ }
+
+ /**
+ * Hide toggle button "add self hosted / sign in with WordPress.com" and show self hosted URL
+ * edit box
+ */
+ public void forceSelfHostedMode(String prefillUrl) {
+ mUrlButtonLayout.setVisibility(View.VISIBLE);
+ mAddSelfHostedButton.setVisibility(View.GONE);
+ mCreateAccountButton.setVisibility(View.GONE);
+ mUrlEditText.setText(prefillUrl);
+ mSelfHosted = true;
+ }
+
+ protected void toggleSignInMode(){
+ if (mUrlButtonLayout.getVisibility() == View.VISIBLE) {
+ showDotComSignInForm();
+ mSelfHosted = false;
+ } else {
+ showSelfHostedSignInForm();
+ mSelfHosted = true;
+ }
+ }
+
+ protected void showDotComSignInForm(){
+ mUrlButtonLayout.setVisibility(View.GONE);
+ mAddSelfHostedButton.setText(getString(R.string.nux_add_selfhosted_blog));
+ }
+
+ protected void showSelfHostedSignInForm(){
+ mUrlButtonLayout.setVisibility(View.VISIBLE);
+ mAddSelfHostedButton.setText(getString(R.string.nux_oops_not_selfhosted_blog));
+ }
+
+ protected void track(Stat stat, Map<String, Boolean> properties) {
+ AnalyticsTracker.track(stat, properties);
+ }
+
+ protected void finishCurrentActivity(final List<Map<String, Object>> userBlogList) {
+ if (!isAdded()) {
+ return;
+ }
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (userBlogList != null) {
+ getActivity().setResult(Activity.RESULT_OK);
+ getActivity().finish();
+ }
+ }
+ });
+ }
+
+ private void initInfoButtons(View rootView) {
+ OnClickListener infoButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), HelpActivity.class);
+ // Used to pass data to an eventual support service
+ intent.putExtra(ENTERED_URL_KEY, EditTextUtils.getText(mUrlEditText));
+ intent.putExtra(ENTERED_USERNAME_KEY, EditTextUtils.getText(mUsernameEditText));
+ intent.putExtra(HelpshiftHelper.ORIGIN_KEY, Tag.ORIGIN_LOGIN_SCREEN_HELP);
+ startActivity(intent);
+ }
+ };
+ mInfoButton = (ImageView) rootView.findViewById(R.id.info_button);
+ mInfoButtonSecondary = (ImageView) rootView.findViewById(R.id.info_button_secondary);
+ mInfoButton.setOnClickListener(infoButtonListener);
+ mInfoButtonSecondary.setOnClickListener(infoButtonListener);
+ }
+
+ /*
+ * autofill the username and password from BuildConfig/gradle.properties (developer feature,
+ * only enabled for DEBUG releases)
+ */
+ private void autofillFromBuildConfig() {
+ if (!BuildConfig.DEBUG) return;
+
+ String userName = (String) WordPress.getBuildConfigValue(getActivity().getApplication(),
+ "DEBUG_DOTCOM_LOGIN_USERNAME");
+ String password = (String) WordPress.getBuildConfigValue(getActivity().getApplication(),
+ "DEBUG_DOTCOM_LOGIN_PASSWORD");
+ if (!TextUtils.isEmpty(userName)) {
+ mUsernameEditText.setText(userName);
+ AppLog.d(T.NUX, "Autofilled username from build config");
+ }
+ if (!TextUtils.isEmpty(password)) {
+ mPasswordEditText.setText(password);
+ AppLog.d(T.NUX, "Autofilled password from build config");
+ }
+ }
+
+ public boolean canAutofillUsernameAndPassword() {
+ return EditTextUtils.getText(mUsernameEditText).isEmpty()
+ && EditTextUtils.getText(mPasswordEditText).isEmpty()
+ && mUsernameEditText != null
+ && mPasswordEditText != null;
+ }
+
+ public void onCredentialRetrieved(Credential credential) {
+ AppLog.d(T.NUX, "Retrieved username from SmartLock: " + credential.getId());
+ if (isAdded() && canAutofillUsernameAndPassword()) {
+ track(Stat.LOGIN_AUTOFILL_CREDENTIALS_FILLED, null);
+ mUsernameEditText.setText(credential.getId());
+ mPasswordEditText.setText(credential.getPassword());
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ moveBottomButtons();
+ }
+
+ private void setSecondaryButtonVisible(boolean visible) {
+ mInfoButtonSecondary.setVisibility(visible ? View.VISIBLE : View.GONE);
+ mInfoButton.setVisibility(visible ? View.GONE : View.VISIBLE);
+ }
+
+ private void moveBottomButtons() {
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ mBottomButtonsLayout.setOrientation(LinearLayout.HORIZONTAL);
+ if (getResources().getInteger(R.integer.isSW600DP) == 0) {
+ setSecondaryButtonVisible(true);
+ } else {
+ setSecondaryButtonVisible(false);
+ }
+ } else {
+ mBottomButtonsLayout.setOrientation(LinearLayout.VERTICAL);
+ setSecondaryButtonVisible(false);
+ }
+ }
+
+ private final OnClickListener mOnLoginFormClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Don't change layout if we are performing a network operation
+ if (mProgressBarSignIn.getVisibility() == View.VISIBLE) return;
+
+ if (mTwoStepLayout.getVisibility() == View.VISIBLE) {
+ setTwoStepAuthVisibility(false);
+ }
+ }
+ };
+
+ private void autocorrectUsername() {
+ if (mEmailAutoCorrected) {
+ return;
+ }
+ final String email = EditTextUtils.getText(mUsernameEditText).trim();
+ // Check if the username looks like an email address
+ final Pattern emailRegExPattern = Patterns.EMAIL_ADDRESS;
+ Matcher matcher = emailRegExPattern.matcher(email);
+ if (!matcher.find()) {
+ return;
+ }
+ // It looks like an email address, then try to correct it
+ String suggest = EmailChecker.suggestDomainCorrection(email);
+ if (suggest.compareTo(email) != 0) {
+ mEmailAutoCorrected = true;
+ mUsernameEditText.setText(suggest);
+ mUsernameEditText.setSelection(suggest.length());
+ }
+ }
+
+ private boolean isWPComLogin() {
+ String selfHostedUrl = EditTextUtils.getText(mUrlEditText).trim();
+ return !mSelfHosted || TextUtils.isEmpty(selfHostedUrl) || WPUrlUtils.isWordPressCom(selfHostedUrl);
+ }
+
+ private boolean isJetpackAuth() {
+ return mJetpackBlog != null;
+ }
+
+ // Set blog for Jetpack auth
+ public void setBlogAndCustomMessageForJetpackAuth(Blog blog, String customAuthMessage) {
+ mJetpackBlog = blog;
+ if(customAuthMessage != null && mJetpackAuthLabel != null) {
+ mJetpackAuthLabel.setText(customAuthMessage);
+ }
+
+ if (mAddSelfHostedButton != null) {
+ mJetpackAuthLabel.setVisibility(View.VISIBLE);
+ mAddSelfHostedButton.setVisibility(View.GONE);
+ mCreateAccountButton.setVisibility(View.GONE);
+ mUsernameEditText.setText("");
+ }
+ }
+
+ private final View.OnClickListener mCreateAccountListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ createUserFragment();
+ }
+ };
+
+ private void createUserFragment() {
+ FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ NewUserFragment newUserFragment = NewUserFragment.newInstance();
+ newUserFragment.setTargetFragment(this, NewUserFragment.NEW_USER);
+ transaction.setCustomAnimations(R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left,
+ R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right);
+ transaction.replace(R.id.fragment_container, newUserFragment);
+ transaction.addToBackStack(null);
+ transaction.commit();
+ }
+
+ private String getForgotPasswordURL() {
+ String baseUrl = DOT_COM_BASE_URL;
+ if (!isWPComLogin()) {
+ baseUrl = EditTextUtils.getText(mUrlEditText).trim();
+ String lowerCaseBaseUrl = baseUrl.toLowerCase(Locale.getDefault());
+ if (!lowerCaseBaseUrl.startsWith("https://") && !lowerCaseBaseUrl.startsWith("http://")) {
+ baseUrl = "http://" + baseUrl;
+ }
+ }
+ return baseUrl + FORGOT_PASSWORD_RELATIVE_URL;
+ }
+
+ private final View.OnClickListener mForgotPasswordListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String forgotPasswordUrl = getForgotPasswordURL();
+ AppLog.i(T.NUX, "User tapped forgot password link: " + forgotPasswordUrl);
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(forgotPasswordUrl));
+ startActivity(intent);
+ }
+ };
+
+ protected void onDoneAction() {
+ signIn();
+ }
+
+ private final TextView.OnEditorActionListener mEditorAction = new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (mPasswordEditText == v) {
+ if (mSelfHosted) {
+ mUrlEditText.requestFocus();
+ return true;
+ } else {
+ return onDoneEvent(actionId, event);
+ }
+ }
+ return onDoneEvent(actionId, event);
+ }
+ };
+
+ private void refreshAndSelectSite(Map<String, Object> site) {
+ refreshBlogContent(site);
+ WordPress.setCurrentBlog((Integer) site.get("id"));
+ }
+
+ private void setPrimaryBlog(JSONObject jsonObject) {
+ try {
+ String primarySiteId = jsonObject.getString("primary_blog");
+ boolean hideJetpackWithoutCredentials = true;
+ // Look for a visible site that is not a "non active" Jetpack site with this id in the DB
+ // TODO: when we support Jetpack sites by wpcom login, we should change that
+ List<Map<String, Object>> sites = WordPress.wpDB.getBlogsBy("isHidden = 0 AND blogId = " + primarySiteId,
+ null, 1, hideJetpackWithoutCredentials);
+ if (sites != null && !sites.isEmpty()) {
+ refreshAndSelectSite(sites.get(0));
+ } else {
+ // Primary blog not found or hidden (can happen if it's a "non active" Jetpack site)
+ // Select the first visible site if it exists
+ sites = WordPress.wpDB.getBlogsBy("isHidden = 0", null, 1, hideJetpackWithoutCredentials);
+ if (sites != null && !sites.isEmpty()) {
+ refreshAndSelectSite(sites.get(0));
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.NUX, e);
+ }
+ }
+
+ private void trackAnalyticsSignIn() {
+ AnalyticsUtils.refreshMetadata();
+ Map<String, Boolean> properties = new HashMap<String, Boolean>();
+ properties.put("dotcom_user", isWPComLogin());
+ track(Stat.SIGNED_IN, properties);
+ if (!isWPComLogin()) {
+ track(Stat.ADDED_SELF_HOSTED_SITE, null);
+ }
+ }
+
+ private SmartLockHelper getSmartLockHelper() {
+ if (getActivity() != null && getActivity() instanceof SignInActivity) {
+ return ((SignInActivity) getActivity()).getSmartLockHelper();
+ }
+ return null;
+ }
+
+ protected final Callback mFetchBlogListCallback = new Callback() {
+ @Override
+ public void onSuccess(final List<Map<String, Object>> userBlogList) {
+ if (!isAdded()) {
+ return;
+ }
+ if (userBlogList != null) {
+ if (isWPComLogin()) {
+ BlogUtils.addBlogs(userBlogList, mUsername);
+ } else {
+ BlogUtils.addBlogs(userBlogList, mUsername, mPassword, mHttpUsername, mHttpPassword);
+ }
+
+ // refresh the first 5 blogs
+ refreshFirstFiveBlogsContent();
+ }
+
+ trackAnalyticsSignIn();
+
+ // get reader tags so they're available as soon as the Reader is accessed - done for
+ // both wp.com and self-hosted (self-hosted = "logged out" reader) - note that this
+ // uses the application context since the activity is finished immediately below
+ ReaderUpdateService.startService(getActivity().getApplicationContext(),
+ EnumSet.of(UpdateTask.TAGS));
+
+ if (isWPComLogin()) {
+ //Update previous stats widgets
+ StatsWidgetProvider.updateWidgetsOnLogin(getActivity().getApplicationContext());
+
+ // Fire off a synchronous request to get the primary blog
+ WordPress.getRestClientUtils().get("me", new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ // Set primary blog
+ setPrimaryBlog(jsonObject);
+ finishCurrentActivity(userBlogList);
+ String displayName = JSONUtils.getStringDecoded(jsonObject, "display_name");
+ Uri profilePicture = Uri.parse(JSONUtils.getString(jsonObject, "avatar_URL"));
+ SmartLockHelper smartLockHelper = getSmartLockHelper();
+ // mUsername and mPassword are null when the user sign in with a magic link
+ if (smartLockHelper != null && mUsername != null && mPassword != null) {
+ smartLockHelper.saveCredentialsInSmartLock(mUsername, mPassword, displayName,
+ profilePicture);
+ }
+ }
+ }, null);
+ } else {
+ finishCurrentActivity(userBlogList);
+ }
+ }
+
+ @Override
+ public void onError(final int messageId, final boolean twoStepCodeRequired, final boolean httpAuthRequired,
+ final boolean erroneousSslCertificate, final String clientResponse) {
+ if (!isAdded()) {
+ return;
+ }
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (twoStepCodeRequired) {
+ setTwoStepAuthVisibility(true);
+ endProgress();
+ return;
+ }
+
+ if (erroneousSslCertificate) {
+ askForSslTrust();
+ return;
+ }
+ if (httpAuthRequired) {
+ askForHttpAuthCredentials();
+ return;
+ }
+ if (messageId != 0) {
+ signInError(messageId, clientResponse);
+ return;
+ }
+
+ endProgress();
+ }
+ });
+
+ track(Stat.LOGIN_FAILED, null);
+ }
+ };
+
+ public void showAuthErrorMessage() {
+ if (mJetpackAuthLabel != null) {
+ mJetpackAuthLabel.setVisibility(View.VISIBLE);
+ mJetpackAuthLabel.setText(getResources().getString(R.string.auth_required));
+ }
+ }
+
+ private void setTwoStepAuthVisibility(boolean isVisible) {
+ mTwoStepLayout.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ mTwoStepFooter.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ mSignInButton.setText(isVisible ? getString(R.string.verify) : getString(R.string.sign_in));
+ mForgotPassword.setVisibility(isVisible ? View.GONE : View.VISIBLE);
+ mBottomButtonsLayout.setVisibility(isVisible ? View.GONE : View.VISIBLE);
+ mUsernameEditText.setFocusableInTouchMode(!isVisible);
+ mUsernameLayout.setAlpha(isVisible ? 0.6f : 1.0f);
+ mPasswordEditText.setFocusableInTouchMode(!isVisible);
+ mPasswordLayout.setAlpha(isVisible ? 0.6f : 1.0f);
+
+ if (isVisible) {
+ mTwoStepEditText.requestFocus();
+ mTwoStepEditText.setText("");
+ showSoftKeyboard();
+ } else {
+ mTwoStepEditText.setText("");
+ mTwoStepEditText.clearFocus();
+ }
+ }
+
+ private void showSoftKeyboard() {
+ if (isAdded() && !hasHardwareKeyboard()) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ private boolean hasHardwareKeyboard() {
+ return (getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS);
+ }
+
+ private void signInAndFetchBlogListWPCom() {
+ LoginWPCom login = new LoginWPCom(mUsername, mPassword, mTwoStepCode, mShouldSendTwoStepSMS, mJetpackBlog);
+ login.execute(new LoginAbstract.Callback() {
+ @Override
+ public void onSuccess() {
+ configureAccountAfterSuccessfulSignIn();
+ }
+
+ @Override
+ public void onError(int errorMessageId, boolean twoStepCodeRequired, boolean httpAuthRequired, boolean erroneousSslCertificate) {
+ mFetchBlogListCallback.onError(errorMessageId, twoStepCodeRequired, httpAuthRequired, erroneousSslCertificate, "");
+ mShouldSendTwoStepSMS = false;
+ // Delete credentials only if login failed with an incorrect username/password error
+ if (errorMessageId == R.string.username_or_password_incorrect) {
+ SmartLockHelper smartLockHelper = getSmartLockHelper();
+ if (smartLockHelper != null) {
+ smartLockHelper.deleteCredentialsInSmartLock(mUsername, mPassword);
+ }
+ }
+ }
+ });
+ }
+
+ protected void configureAccountAfterSuccessfulSignIn() {
+ mShouldSendTwoStepSMS = false;
+
+ // Finish this activity if we've authenticated to a Jetpack site
+ if (isJetpackAuth() && getActivity() != null) {
+ getActivity().setResult(Activity.RESULT_OK);
+ getActivity().finish();
+ return;
+ }
+
+ FetchBlogListWPCom fetchBlogListWPCom = new FetchBlogListWPCom(getActivity());
+ fetchBlogListWPCom.execute(mFetchBlogListCallback);
+ }
+
+ private void signInAndFetchBlogListWPOrg() {
+ String url = EditTextUtils.getText(mUrlEditText).trim();
+ FetchBlogListWPOrg fetchBlogListWPOrg = new FetchBlogListWPOrg(mUsername, mPassword, url);
+ if (mHttpUsername != null && mHttpPassword != null) {
+ fetchBlogListWPOrg.setHttpCredentials(mHttpUsername, mHttpPassword);
+ }
+ fetchBlogListWPOrg.execute(mFetchBlogListCallback);
+ }
+
+ private boolean checkNetworkConnectivity() {
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ SignInDialogFragment nuxAlert;
+ nuxAlert = SignInDialogFragment.newInstance(getString(R.string.no_network_title),
+ getString(R.string.no_network_message),
+ R.drawable.noticon_alert_big,
+ getString(R.string.cancel));
+ ft.add(nuxAlert, "alert");
+ ft.commitAllowingStateLoss();
+ return false;
+ }
+ return true;
+ }
+
+ protected void signIn() {
+ if (!isUserDataValid()) {
+ return;
+ }
+
+ if (!checkNetworkConnectivity()) {
+ return;
+ }
+
+ mUsername = EditTextUtils.getText(mUsernameEditText).trim();
+ mPassword = EditTextUtils.getText(mPasswordEditText).trim();
+ mTwoStepCode = EditTextUtils.getText(mTwoStepEditText).trim();
+ if (isWPComLogin()) {
+ AppLog.i(T.NUX, "User tries to sign in on WordPress.com with username: " + mUsername);
+ startProgress(getString(R.string.connecting_wpcom));
+ signInAndFetchBlogListWPCom();
+ } else {
+ String selfHostedUrl = EditTextUtils.getText(mUrlEditText).trim();
+ AppLog.i(T.NUX, "User tries to sign in on Self Hosted: " + selfHostedUrl + " with username: " + mUsername);
+ startProgress(getString(R.string.signing_in));
+ signInAndFetchBlogListWPOrg();
+ }
+ }
+
+ private void requestSMSTwoStepCode() {
+ if (!isAdded()) return;
+
+ ToastUtils.showToast(getActivity(), R.string.two_step_sms_sent);
+ mTwoStepEditText.setText("");
+ mShouldSendTwoStepSMS = true;
+
+ signIn();
+ }
+
+ private final OnClickListener mSignInClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ signIn();
+ }
+ };
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (fieldsFilled()) {
+ mSignInButton.setEnabled(true);
+ } else {
+ mSignInButton.setEnabled(false);
+ }
+ mPasswordEditText.setError(null);
+ mUsernameEditText.setError(null);
+ mTwoStepEditText.setError(null);
+ }
+
+ private boolean fieldsFilled() {
+ return EditTextUtils.getText(mUsernameEditText).trim().length() > 0
+ && (mPasswordLayout.getVisibility() == View.GONE || EditTextUtils.getText(mPasswordEditText).trim().length() > 0)
+ && (mTwoStepLayout.getVisibility() == View.GONE || EditTextUtils.getText(mTwoStepEditText).trim().length() > 0);
+ }
+
+ protected boolean isUserDataValid() {
+ final String username = EditTextUtils.getText(mUsernameEditText).trim();
+ final String password = EditTextUtils.getText(mPasswordEditText).trim();
+ boolean retValue = true;
+
+ if (password.equals("")) {
+ mPasswordEditText.setError(getString(R.string.required_field));
+ mPasswordEditText.requestFocus();
+ retValue = false;
+ }
+
+ if (username.equals("")) {
+ mUsernameEditText.setError(getString(R.string.required_field));
+ mUsernameEditText.requestFocus();
+ retValue = false;
+ }
+
+ return retValue;
+ }
+
+ private void showPasswordError(int messageId) {
+ mPasswordEditText.setError(getString(messageId));
+ mPasswordEditText.requestFocus();
+ }
+
+ private void showUsernameError(int messageId) {
+ mUsernameEditText.setError(getString(messageId));
+ mUsernameEditText.requestFocus();
+ }
+
+ private void showUrlError(int messageId) {
+ mUrlEditText.setError(getString(messageId));
+ mUrlEditText.requestFocus();
+ }
+
+ private void showTwoStepCodeError(int messageId) {
+ mTwoStepEditText.setError(getString(messageId));
+ mTwoStepEditText.requestFocus();
+ }
+
+ protected boolean specificShowError(int messageId) {
+ switch (getErrorType(messageId)) {
+ case USERNAME:
+ case PASSWORD:
+ showPasswordError(messageId);
+ showUsernameError(messageId);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected void startProgress(String message) {
+ mProgressBarSignIn.setVisibility(View.VISIBLE);
+ mProgressTextSignIn.setVisibility(View.VISIBLE);
+ mSignInButton.setVisibility(View.GONE);
+ mProgressBarSignIn.setEnabled(false);
+ mProgressTextSignIn.setText(message);
+ mUsernameEditText.setEnabled(false);
+ mPasswordEditText.setEnabled(false);
+ mTwoStepEditText.setEnabled(false);
+ mUrlEditText.setEnabled(false);
+ mAddSelfHostedButton.setEnabled(false);
+ mCreateAccountButton.setEnabled(false);
+ mForgotPassword.setEnabled(false);
+ }
+
+ protected void endProgress() {
+ mProgressBarSignIn.setVisibility(View.GONE);
+ mProgressTextSignIn.setVisibility(View.GONE);
+ mSignInButton.setVisibility(View.VISIBLE);
+ mUsernameEditText.setEnabled(true);
+ mPasswordEditText.setEnabled(true);
+ mTwoStepEditText.setEnabled(true);
+ mUrlEditText.setEnabled(true);
+ mAddSelfHostedButton.setEnabled(true);
+ mCreateAccountButton.setEnabled(true);
+ mForgotPassword.setEnabled(true);
+ }
+
+ public void askForSslTrust() {
+ SelfSignedSSLCertsManager.askForSslTrust(getActivity(), new GenericCallback<Void>() {
+ @Override
+ public void callback(Void aVoid) {
+ // Try to signin again
+ signIn();
+ }
+ });
+ endProgress();
+ }
+
+ private void askForHttpAuthCredentials() {
+ // Prompt for http credentials
+ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
+ alert.setTitle(R.string.http_authorization_required);
+
+ View httpAuth = getActivity().getLayoutInflater().inflate(R.layout.alert_http_auth, null);
+ final EditText usernameEditText = (EditText) httpAuth.findViewById(R.id.http_username);
+ final EditText passwordEditText = (EditText) httpAuth.findViewById(R.id.http_password);
+ alert.setView(httpAuth);
+ alert.setPositiveButton(R.string.sign_in, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mHttpUsername = EditTextUtils.getText(usernameEditText);
+ mHttpPassword = EditTextUtils.getText(passwordEditText);
+ signIn();
+ }
+ });
+
+ alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // Canceled.
+ }
+ });
+
+ alert.show();
+ endProgress();
+ }
+
+ protected void showInvalidUsernameOrPasswordDialog() {
+ // Show a dialog
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ SignInDialogFragment nuxAlert;
+ // create a 3 buttons dialog ("Contact us", "Forget your password?" and "Cancel")
+ nuxAlert = SignInDialogFragment.newInstance(getString(org.wordpress.android.R.string.nux_cannot_log_in),
+ getString(org.wordpress.android.R.string.username_or_password_incorrect),
+ org.wordpress.android.R.drawable.noticon_alert_big, 3, getString(
+ org.wordpress.android.R.string.cancel), getString(
+ org.wordpress.android.R.string.forgot_password), getString(
+ org.wordpress.android.R.string.contact_us), SignInDialogFragment.ACTION_OPEN_URL,
+ SignInDialogFragment.ACTION_OPEN_SUPPORT_CHAT);
+
+ // Put entered url and entered username args, that could help our support team
+ Bundle bundle = nuxAlert.getArguments();
+ bundle.putString(SignInDialogFragment.ARG_OPEN_URL_PARAM, getForgotPasswordURL());
+ bundle.putString(ENTERED_URL_KEY, EditTextUtils.getText(mUrlEditText));
+ bundle.putString(ENTERED_USERNAME_KEY, EditTextUtils.getText(mUsernameEditText));
+ nuxAlert.setArguments(bundle);
+ ft.add(nuxAlert, "alert");
+ ft.commitAllowingStateLoss();
+ }
+
+ protected void handleInvalidUsernameOrPassword(int messageId) {
+ mErroneousLogInCount += 1;
+ if (mErroneousLogInCount >= WPCOM_ERRONEOUS_LOGIN_THRESHOLD) {
+ // Clear previous errors
+ mPasswordEditText.setError(null);
+ mUsernameEditText.setError(null);
+ showInvalidUsernameOrPasswordDialog();
+ } else {
+ showPasswordError(messageId);
+ showUsernameError(messageId);
+ }
+ endProgress();
+ }
+
+ protected void signInError(int messageId, String clientResponse) {
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ SignInDialogFragment nuxAlert;
+ if (messageId == org.wordpress.android.R.string.username_or_password_incorrect) {
+ handleInvalidUsernameOrPassword(messageId);
+ return;
+ } else if (messageId == R.string.invalid_verification_code) {
+ endProgress();
+ showTwoStepCodeError(messageId);
+ return;
+ } else if (messageId == org.wordpress.android.R.string.invalid_site_url_message) {
+ showUrlError(messageId);
+ endProgress();
+ return;
+ } else {
+ AppLog.e(T.NUX, "Server response: " + clientResponse);
+ nuxAlert = SignInDialogFragment.newInstance(getString(org.wordpress.android.R.string.nux_cannot_log_in),
+ getString(messageId), R.drawable.noticon_alert_big, 3,
+ getString(R.string.cancel), getString(R.string.contact_us), getString(R.string.reader_title_applog),
+ SignInDialogFragment.ACTION_OPEN_SUPPORT_CHAT,
+ SignInDialogFragment.ACTION_OPEN_APPLICATION_LOG);
+ }
+ ft.add(nuxAlert, "alert");
+ ft.commitAllowingStateLoss();
+ endProgress();
+ }
+
+ private void refreshBlogContent(Map<String, Object> blogMap) {
+ String blogId = blogMap.get("blogId").toString();
+ String xmlRpcUrl = blogMap.get("url").toString();
+ int intBlogId = StringUtils.stringToInt(blogId, -1);
+ if (intBlogId == -1) {
+ AppLog.e(T.NUX, "Can't refresh blog content - invalid blogId: " + blogId);
+ return;
+ }
+ int blogLocalId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogIdAndXmlRpcUrl(intBlogId, xmlRpcUrl);
+ Blog firstBlog = WordPress.wpDB.instantiateBlogByLocalId(blogLocalId);
+ new ApiHelper.RefreshBlogContentTask(firstBlog, null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, false);
+ }
+
+ /**
+ * Get the first five blogs and call RefreshBlogContentTask. First blog will be autoselected when user login.
+ * Also when a user add a new self hosted blog, userBlogList contains only one element.
+ * We don't want to refresh the whole list because it can be huge and each blog is refreshed when
+ * user selects it.
+ */
+ private void refreshFirstFiveBlogsContent() {
+ List<Map<String, Object>> visibleBlogs = WordPress.wpDB.getBlogsBy("isHidden = 0", null, 5, true);
+ if (visibleBlogs != null && !visibleBlogs.isEmpty()) {
+ int numberOfBlogsBeingRefreshed = Math.min(5, visibleBlogs.size());
+ for (int i = 0; i < numberOfBlogsBeingRefreshed; i++) {
+ Map<String, Object> currentBlog = visibleBlogs.get(i);
+ refreshBlogContent(currentBlog);
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_IS_SELF_HOSTED, mSelfHosted);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Autofill username / password if string fields are set (only usefull after an error in sign up).
+ // This can't be done in onCreateView
+ if (mUsername != null) {
+ mUsernameEditText.setText(mUsername);
+ }
+ if (mPassword != null) {
+ mPasswordEditText.setText(mPassword);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == NewUserFragment.NEW_USER && resultCode == Activity.RESULT_OK) {
+ if (data != null) {
+ // Text views will be populated by username/password if these fields are set
+ mUsername = data.getStringExtra("username");
+ mPassword = data.getStringExtra("password");
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java
new file mode 100644
index 000000000..925b1a3af
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java
@@ -0,0 +1,150 @@
+package org.wordpress.android.ui.accounts;
+
+import android.app.Activity;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentActivity;
+
+import com.google.android.gms.auth.api.Auth;
+import com.google.android.gms.auth.api.credentials.Credential;
+import com.google.android.gms.auth.api.credentials.CredentialRequest;
+import com.google.android.gms.auth.api.credentials.CredentialRequestResult;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.common.api.CommonStatusCodes;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.lang.ref.WeakReference;
+
+public class SmartLockHelper {
+ private GoogleApiClient mCredentialsClient;
+ private WeakReference<FragmentActivity> mActivity;
+
+ public interface Callback {
+ void onCredentialRetrieved(Credential credential);
+ }
+
+ public SmartLockHelper(@NonNull FragmentActivity activity) {
+ if (activity instanceof OnConnectionFailedListener && activity instanceof ConnectionCallbacks) {
+ mActivity = new WeakReference<>(activity);
+ } else {
+ throw new RuntimeException("SmartLockHelper constructor needs an activity that " +
+ "implements OnConnectionFailedListener and ConnectionCallbacks");
+ }
+ }
+
+ private FragmentActivity getActivityAndCheckAvailability() {
+ FragmentActivity activity = mActivity.get();
+ if (activity == null) {
+ return null;
+ }
+ int status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity);
+ if (status == ConnectionResult.SUCCESS) {
+ return activity;
+ }
+ return null;
+ }
+
+ public void initSmartLockForPasswords() {
+ FragmentActivity activity = getActivityAndCheckAvailability();
+ if (activity == null) {
+ return;
+ }
+ mCredentialsClient = new GoogleApiClient.Builder(activity)
+ .addConnectionCallbacks((ConnectionCallbacks) activity)
+ .enableAutoManage(activity, (OnConnectionFailedListener) activity)
+ .addApi(Auth.CREDENTIALS_API)
+ .build();
+ }
+
+ public void smartLockAutoFill(@NonNull final Callback callback) {
+ Activity activity = getActivityAndCheckAvailability();
+ if (activity == null || mCredentialsClient == null || !mCredentialsClient.isConnected()) {
+ return;
+ }
+ CredentialRequest credentialRequest = new CredentialRequest.Builder()
+ .setPasswordLoginSupported(true)
+ .build();
+ Auth.CredentialsApi.request(mCredentialsClient, credentialRequest).setResultCallback(
+ new ResultCallback<CredentialRequestResult>() {
+ @Override
+ public void onResult(@NonNull CredentialRequestResult result) {
+ Status status = result.getStatus();
+ if (status.isSuccess()) {
+ Credential credential = result.getCredential();
+ callback.onCredentialRetrieved(credential);
+ } else {
+ if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
+ try {
+ Activity activity = getActivityAndCheckAvailability();
+ if (activity == null) {
+ return;
+ }
+ // Prompt the user to choose a saved credential
+ status.startResolutionForResult(activity, SignInActivity.SMART_LOCK_READ);
+ } catch (IntentSender.SendIntentException e) {
+ AppLog.d(T.NUX, "SmartLock: Failed to send resolution for credential request");
+ }
+ } else {
+ // The user must create an account or sign in manually.
+ AppLog.d(T.NUX, "SmartLock: Unsuccessful credential request.");
+ }
+ }
+ }
+ });
+ }
+
+
+ public void saveCredentialsInSmartLock(@NonNull final String username, @NonNull final String password,
+ @NonNull final String displayName, @NonNull final Uri profilePicture) {
+ Activity activity = getActivityAndCheckAvailability();
+ if (activity == null || mCredentialsClient == null || !mCredentialsClient.isConnected()) {
+ return;
+ }
+ Credential credential = new Credential.Builder(username).setPassword(password)
+ .setName(displayName).setProfilePictureUri(profilePicture).build();
+ Auth.CredentialsApi.save(mCredentialsClient, credential).setResultCallback(
+ new ResultCallback<Status>() {
+ @Override
+ public void onResult(@NonNull Status status) {
+ if (!status.isSuccess() && status.hasResolution()) {
+ try {
+ Activity activity = getActivityAndCheckAvailability();
+ if (activity == null) {
+ return;
+ }
+ // This prompt the user to resolve the save request
+ status.startResolutionForResult(activity, SignInActivity.SMART_LOCK_SAVE);
+ } catch (IntentSender.SendIntentException e) {
+ // Could not resolve the request
+ }
+ }
+ }
+ });
+ }
+
+ public void deleteCredentialsInSmartLock(@NonNull final String username, @NonNull final String password) {
+ Activity activity = getActivityAndCheckAvailability();
+ if (activity == null || mCredentialsClient == null || !mCredentialsClient.isConnected()) {
+ return;
+ }
+
+ Credential credential = new Credential.Builder(username).setPassword(password).build();
+ Auth.CredentialsApi.delete(mCredentialsClient, credential).setResultCallback(
+ new ResultCallback<Status>() {
+ @Override
+ public void onResult(@NonNull Status status) {
+ AppLog.i(T.NUX, status.isSuccess() ? "SmartLock: credentials deleted for username: " + username
+ : "SmartLock: Credentials not deleted for username: " + username );
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/CreateUserAndBlog.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/CreateUserAndBlog.java
new file mode 100644
index 000000000..f2c6d2de3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/CreateUserAndBlog.java
@@ -0,0 +1,263 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.accounts.AbstractFragment.ErrorListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+public class CreateUserAndBlog {
+ public static final int WORDPRESS_COM_API_BLOG_VISIBILITY_PUBLIC = 1;
+ private String mEmail;
+ private String mUsername;
+ private String mPassword;
+ private String mSiteUrl;
+ private String mSiteName;
+ private String mLanguage;
+ private Callback mCallback;
+ private ErrorListener mErrorListener;
+ private RestClientUtils mRestClient;
+ private ResponseHandler mResponseHandler;
+
+ public CreateUserAndBlog(String email, String username, String password, String siteUrl, String siteName,
+ String language, RestClientUtils restClient,
+ ErrorListener errorListener, Callback callback) {
+ mEmail = email;
+ mUsername = username;
+ mPassword = password;
+ mSiteUrl = siteUrl;
+ mSiteName = siteName;
+ mLanguage = language;
+ mCallback = callback;
+ mErrorListener = errorListener;
+ mRestClient = restClient;
+ mResponseHandler = new ResponseHandler();
+ }
+
+ public static String getDeviceLanguage(Context context) {
+ Resources resources = context.getResources();
+ XmlResourceParser parser = resources.getXml(R.xml.wpcom_languages);
+ Hashtable<String, String> entries = new Hashtable<String, String>();
+ String matchedDeviceLanguage = "en - English";
+ try {
+ int eventType = parser.getEventType();
+ String deviceLanguageCode = LanguageUtils.getPatchedCurrentDeviceLanguage(context);
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("language")) {
+ String currentID = null;
+ boolean currentLangIsDeviceLanguage = false;
+ int i = 0;
+ while (i < parser.getAttributeCount()) {
+ if (parser.getAttributeName(i).equals("id")) {
+ currentID = parser.getAttributeValue(i);
+ }
+ if (parser.getAttributeName(i).equals("code") &&
+ parser.getAttributeValue(i).equalsIgnoreCase(deviceLanguageCode)) {
+ currentLangIsDeviceLanguage = true;
+ }
+ i++;
+ }
+
+ while (eventType != XmlPullParser.END_TAG) {
+ if (eventType == XmlPullParser.TEXT) {
+ entries.put(parser.getText(), currentID);
+ if (currentLangIsDeviceLanguage) {
+ matchedDeviceLanguage = parser.getText();
+ }
+ }
+ eventType = parser.next();
+ }
+ }
+ }
+ eventType = parser.next();
+ }
+ } catch (Exception e) {
+ // do nothing
+ }
+ return matchedDeviceLanguage;
+ }
+
+ public void startCreateUserAndBlogProcess() {
+ validateUser();
+ }
+
+ public void startCreateBlogProcess() {
+ mResponseHandler.setMode(Mode.CREATE_BLOG_ONLY);
+ validateSite();
+ }
+
+ private void validateUser() {
+ String path = "users/new";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("username", mUsername);
+ params.put("password", mPassword);
+ params.put("email", mEmail);
+ params.put("validate", "1");
+ params.put("client_id", BuildConfig.OAUTH_APP_ID);
+ params.put("client_secret", BuildConfig.OAUTH_APP_SECRET);
+ mResponseHandler.setStep(Step.VALIDATE_USER);
+ mRestClient.post(path, params, null, mResponseHandler, mErrorListener);
+ }
+
+ private void validateSite() {
+ String path = "sites/new";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog_name", mSiteUrl);
+ params.put("blog_title", mSiteName);
+ params.put("lang_id", mLanguage);
+ params.put("public", String.valueOf(WORDPRESS_COM_API_BLOG_VISIBILITY_PUBLIC));
+ params.put("validate", "1");
+ params.put("client_id", BuildConfig.OAUTH_APP_ID);
+ params.put("client_secret", BuildConfig.OAUTH_APP_SECRET);
+ mResponseHandler.setStep(Step.VALIDATE_SITE);
+ mRestClient.post(path, params, null, mResponseHandler, mErrorListener);
+ }
+
+ private void createUser() {
+ String path = "users/new";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("username", mUsername);
+ params.put("password", mPassword);
+ params.put("email", mEmail);
+ params.put("validate", "0");
+ params.put("client_id", BuildConfig.OAUTH_APP_ID);
+ params.put("client_secret", BuildConfig.OAUTH_APP_SECRET);
+ mResponseHandler.setStep(Step.CREATE_USER);
+ mRestClient.post(path, params, null, mResponseHandler, mErrorListener);
+ }
+
+ private void authenticateUser() {
+ LoginWPCom login = new LoginWPCom(mUsername, mPassword, null, false, null);
+ login.execute(new LoginAbstract.Callback() {
+ @Override
+ public void onSuccess() {
+ try {
+ mResponseHandler.nextStep(new JSONObject("{\"success\":true}"));
+ } catch (JSONException e) {
+ AppLog.e(T.API, "Could not parse JSON in new user setup");
+ }
+ }
+
+ @Override
+ public void onError(int errorMessageId, boolean twoStepCodeRequired, boolean httpAuthRequired, boolean erroneousSslCertificate) {
+ mErrorListener.onErrorResponse(new VolleyError("Sign in failed."));
+ }
+ });
+
+ mResponseHandler.setStep(Step.AUTHENTICATE_USER);
+ }
+
+ private void createBlog() {
+ String path = "sites/new";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog_name", mSiteUrl);
+ params.put("blog_title", mSiteName);
+ params.put("lang_id", mLanguage);
+ params.put("public", String.valueOf(WORDPRESS_COM_API_BLOG_VISIBILITY_PUBLIC));
+ params.put("validate", "0");
+ params.put("client_id", BuildConfig.OAUTH_APP_ID);
+ params.put("client_secret", BuildConfig.OAUTH_APP_SECRET);
+ mResponseHandler.setStep(Step.CREATE_SITE);
+ WordPress.getRestClientUtils().post(path, params, null, mResponseHandler, mErrorListener);
+ }
+
+ public enum Step {
+ VALIDATE_USER, VALIDATE_SITE, CREATE_USER, AUTHENTICATE_USER, CREATE_SITE
+ }
+
+ private enum Mode {CREATE_USER_AND_BLOG, CREATE_BLOG_ONLY}
+
+ public interface Callback {
+ void onStepFinished(Step step);
+
+ void onSuccess(JSONObject createSiteResponse);
+
+ void onError(int messageId);
+ }
+
+ private class ResponseHandler implements RestRequest.Listener {
+ public ResponseHandler() {
+ super();
+ }
+
+ public void setMode(Mode mode) {
+ mMode = mode;
+ }
+
+ private Step mStep = Step.VALIDATE_USER;
+
+ public void setStep(Step step) {
+ mStep = step;
+ }
+
+ private void nextStep(JSONObject response) {
+ try {
+ if (mStep == Step.AUTHENTICATE_USER) {
+ mCallback.onStepFinished(Step.AUTHENTICATE_USER);
+ createBlog();
+ } else {
+ // steps VALIDATE_USER and VALIDATE_SITE could be run simultaneously in
+ // CREATE_USER_AND_BLOG mode
+ if (response.getBoolean("success")) {
+ switch (mStep) {
+ case VALIDATE_USER:
+ mCallback.onStepFinished(Step.VALIDATE_USER);
+ validateSite();
+ break;
+ case VALIDATE_SITE:
+ mCallback.onStepFinished(Step.VALIDATE_SITE);
+ if (mMode == Mode.CREATE_BLOG_ONLY) {
+ createBlog();
+ } else {
+ createUser();
+ }
+ break;
+ case CREATE_USER:
+ mCallback.onStepFinished(Step.CREATE_USER);
+ authenticateUser();
+ break;
+ case CREATE_SITE:
+ mCallback.onStepFinished(Step.CREATE_SITE);
+ mCallback.onSuccess(response);
+ break;
+ default:
+ break;
+ }
+ } else {
+ mCallback.onError(R.string.error_generic);
+ }
+ }
+ } catch (JSONException e) {
+ mCallback.onError(R.string.error_generic);
+ }
+ }
+
+ private Mode mMode = Mode.CREATE_USER_AND_BLOG;
+
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(T.NUX, String.format("Create Account step %s", mStep.name()));
+ nextStep(response);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListAbstract.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListAbstract.java
new file mode 100644
index 000000000..02e2e68a6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListAbstract.java
@@ -0,0 +1,28 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import java.util.List;
+import java.util.Map;
+
+public abstract class FetchBlogListAbstract {
+ protected String mUsername;
+ protected String mPassword;
+ protected Callback mCallback;
+
+ public interface Callback {
+ void onSuccess(List<Map<String, Object>> userBlogList);
+ void onError(int errorMessageId, boolean twoStepCodeRequired, boolean httpAuthRequired, boolean erroneousSslCertificate,
+ String clientResponse);
+ }
+
+ public FetchBlogListAbstract(String username, String password) {
+ mUsername = username;
+ mPassword = password;
+ }
+
+ public void execute(final Callback callback) {
+ mCallback = callback;
+ fetchBlogList(callback);
+ }
+
+ protected abstract void fetchBlogList(final Callback callback);
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPCom.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPCom.java
new file mode 100644
index 000000000..5cd850199
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPCom.java
@@ -0,0 +1,95 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import android.content.Context;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class FetchBlogListWPCom extends FetchBlogListAbstract {
+
+ private Context mContext;
+
+ public FetchBlogListWPCom(Context context) {
+ super(null, null);
+ mContext = context;
+ }
+
+ protected void fetchBlogList(final Callback callback) {
+ getUsersBlogsRequestREST(callback);
+ }
+
+ private List<Map<String, Object>> convertJSONObjectToSiteList(JSONObject jsonObject, boolean keepJetpackSites) {
+ List<Map<String, Object>> sites = new ArrayList<Map<String, Object>>();
+ JSONArray jsonSites = jsonObject.optJSONArray("sites");
+ if (jsonSites != null) {
+ for (int i = 0; i < jsonSites.length(); i++) {
+ JSONObject jsonSite = jsonSites.optJSONObject(i);
+ Map<String, Object> site = new HashMap<String, Object>();
+ try {
+ // skip if it's a jetpack site and we don't keep them
+ if (jsonSite.getBoolean("jetpack") && !keepJetpackSites) {
+ continue;
+ }
+ site.put("blogName", jsonSite.get("name"));
+ site.put("url", jsonSite.get("URL"));
+ site.put("blogid", jsonSite.get("ID"));
+ site.put("isAdmin", jsonSite.get("user_can_manage"));
+ site.put("isVisible", jsonSite.get("visible"));
+
+ // store capabilities as a json string
+ site.put("capabilities", jsonSite.getString("capabilities"));
+
+ JSONObject plan = jsonSite.getJSONObject("plan");
+ site.put("planID", plan.get("product_id"));
+ site.put("plan_product_name_short", plan.get("product_name_short"));
+
+ JSONObject jsonLinks = JSONUtils.getJSONChild(jsonSite, "meta/links");
+ if (jsonLinks != null) {
+ site.put("xmlrpc", jsonLinks.getString("xmlrpc"));
+ sites.add(site);
+ } else {
+ AppLog.e(T.NUX, "xmlrpc links missing from the me/sites REST response");
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.NUX, e);
+ }
+ }
+ }
+ return sites;
+ }
+
+ private void getUsersBlogsRequestREST(final FetchBlogListAbstract.Callback callback) {
+ WordPress.getRestClientUtils().get("me/sites", RestClientUtils.getRestLocaleParams(mContext), null, new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (response != null) {
+ List<Map<String, Object>> userBlogListReceiver = convertJSONObjectToSiteList(response, true);
+ callback.onSuccess(userBlogListReceiver);
+ } else {
+ callback.onSuccess(null);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ JSONObject errorObject = VolleyUtils.volleyErrorToJSON(volleyError);
+ callback.onError(LoginWPCom.restLoginErrorToMsgId(errorObject), false, false, false, "");
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java
new file mode 100644
index 000000000..11531bb4c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java
@@ -0,0 +1,81 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+
+import org.xmlrpc.android.XMLRPCUtils;
+import org.xmlrpc.android.XMLRPCUtils.XMLRPCUtilsException;
+
+import android.os.AsyncTask;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class FetchBlogListWPOrg extends FetchBlogListAbstract {
+ private String mSelfHostedUrl;
+ private String mHttpUsername;
+ private String mHttpPassword;
+
+ public FetchBlogListWPOrg(String username, String password, String selfHostedUrl) {
+ super(username, password);
+ mSelfHostedUrl = selfHostedUrl;
+ }
+
+ public void setHttpCredentials(String username, String password) {
+ mHttpUsername = username;
+ mHttpPassword = password;
+ }
+
+ public void fetchBlogList(Callback callback) {
+ (new FetchBlogListTask(callback)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public class FetchBlogListTask extends AsyncTask<Void, Void, List<Map<String, Object>>> {
+ private final Callback mCallback;
+ private boolean mHttpAuthRequired;
+ private boolean mErroneousSslCertificate;
+ private int mErrorMsgId;
+ private String mClientResponse = "";
+
+ public FetchBlogListTask(Callback callback) {
+ mCallback = callback;
+ }
+
+ private void trackInvalidInsertedURL(String url){
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("user_inserted_url", url);
+ AnalyticsTracker.track(Stat.LOGIN_INSERTED_INVALID_URL, properties);
+ }
+
+ @Override
+ protected List<Map<String, Object>> doInBackground(Void... notUsed) {
+ try {
+ String xmlrpcUrl = XMLRPCUtils.verifyOrDiscoverXmlRpcUrl(mSelfHostedUrl, mHttpUsername, mHttpPassword);
+
+ // The XML-RPC address is now available. Call wp.getUsersBlogs and load the sites.
+ return XMLRPCUtils.getUserBlogsList(URI.create(xmlrpcUrl), mUsername, mPassword, mHttpUsername,
+ mHttpPassword);
+ } catch (XMLRPCUtilsException hce) {
+ mErrorMsgId = hce.errorMsgId;
+ mHttpAuthRequired = (hce.kind == XMLRPCUtilsException.Kind.HTTP_AUTH_REQUIRED);
+ mErroneousSslCertificate = (hce.kind == XMLRPCUtilsException.Kind.ERRONEOUS_SSL_CERTIFICATE);
+ trackInvalidInsertedURL(hce.failedUrl);
+ return null;
+ }
+ }
+
+ protected void onPostExecute(List<Map<String, Object>> userBlogList) {
+ if (userBlogList == null) {
+ mCallback.onError(mErrorMsgId, false, mHttpAuthRequired, mErroneousSslCertificate, mClientResponse);
+ } else {
+ mCallback.onSuccess(userBlogList);
+ }
+ }
+
+ protected void onCancelled() {
+ mCallback.onError(mErrorMsgId, false, mHttpAuthRequired, mErroneousSslCertificate, mClientResponse);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginAbstract.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginAbstract.java
new file mode 100644
index 000000000..0709843df
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginAbstract.java
@@ -0,0 +1,29 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+public abstract class LoginAbstract {
+ protected String mUsername;
+ protected String mPassword;
+ protected Callback mCallback;
+
+ public interface Callback {
+ void onSuccess();
+ void onError(int errorMessageId, boolean twoStepCodeRequired, boolean httpAuthRequired, boolean erroneousSslCertificate);
+ }
+
+ public LoginAbstract(String username, String password) {
+ mUsername = username;
+ mPassword = password;
+ }
+
+ public void execute(Callback callback) {
+ mCallback = callback;
+ new Thread() {
+ @Override
+ public void run() {
+ login();
+ }
+ }.start();
+ }
+
+ protected abstract void login();
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginWPCom.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginWPCom.java
new file mode 100644
index 000000000..b163362d5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/LoginWPCom.java
@@ -0,0 +1,112 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import android.annotation.SuppressLint;
+
+import com.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.Oauth;
+import com.wordpress.rest.Oauth.ErrorListener;
+import com.wordpress.rest.Oauth.Listener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.VolleyUtils;
+
+public class LoginWPCom extends LoginAbstract {
+
+ private String mTwoStepCode;
+ private boolean mShouldSendTwoStepSMS;
+ private Blog mJetpackBlog;
+
+ public LoginWPCom(String username, String password, String twoStepCode, boolean shouldSendTwoStepSMS, Blog blog) {
+ super(username, password);
+ mTwoStepCode = twoStepCode;
+ mShouldSendTwoStepSMS = shouldSendTwoStepSMS;
+ mJetpackBlog = blog;
+ }
+
+ public static int restLoginErrorToMsgId(JSONObject errorObject) {
+ // Default to generic error message
+ int errorMsgId = org.wordpress.android.R.string.nux_cannot_log_in;
+
+ // Map REST errors to local error codes
+ if (errorObject != null) {
+ try {
+ String error = errorObject.optString("error", "");
+ String errorDescription = errorObject.getString("error_description");
+ if (error.equals("invalid_request")) {
+ if (errorDescription.contains("Incorrect username or password.")) {
+ errorMsgId = org.wordpress.android.R.string.username_or_password_incorrect;
+ }
+ } else if (error.equals("needs_2fa")) {
+ errorMsgId = org.wordpress.android.R.string.account_two_step_auth_enabled;
+ } else if (error.equals("invalid_otp")) {
+ errorMsgId = org.wordpress.android.R.string.invalid_verification_code;
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.NUX, e);
+ }
+ }
+ return errorMsgId;
+ }
+
+ private Request makeOAuthRequest(final String username, final String password, final Listener listener,
+ final ErrorListener errorListener) {
+ Oauth oauth = new Oauth(org.wordpress.android.BuildConfig.OAUTH_APP_ID,
+ org.wordpress.android.BuildConfig.OAUTH_APP_SECRET,
+ org.wordpress.android.BuildConfig.OAUTH_REDIRECT_URI);
+ Request oauthRequest;
+ oauthRequest = oauth.makeRequest(username, password, mTwoStepCode, mShouldSendTwoStepSMS, listener, errorListener);
+ return oauthRequest;
+ }
+
+ protected void login() {
+ // Get OAuth token for the first time and check for errors
+ WordPress.requestQueue.add(makeOAuthRequest(mUsername, mPassword, new Oauth.Listener() {
+ @SuppressLint("CommitPrefEdits")
+ @Override
+ public void onResponse(final Oauth.Token token) {
+ configureAccountOnSuccess(token);
+ }
+ }, new Oauth.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ JSONObject errorObject = VolleyUtils.volleyErrorToJSON(volleyError);
+ int errorMsgId = restLoginErrorToMsgId(errorObject);
+ mCallback.onError(errorMsgId, errorMsgId == R.string.account_two_step_auth_enabled, false, false);
+ }
+ }));
+ }
+
+ private void configureAccountOnSuccess(Oauth.Token token) {
+ if (mJetpackBlog != null) {
+ // Store token in blog object for Jetpack sites
+ mJetpackBlog.setApi_key(token.toString());
+ mJetpackBlog.setDotcom_username(mUsername);
+ WordPress.wpDB.saveBlog(mJetpackBlog);
+ }
+
+ Account account = AccountHelper.getDefaultAccount();
+
+ if (mJetpackBlog == null) {
+ // Store token in global account
+ account.setAccessToken(token.toString());
+ account.setUserName(mUsername);
+ account.save();
+ account.fetchAccountDetails();
+ }
+
+ // Once we have a token, start up Simperium
+ SimperiumUtils.configureSimperium(WordPress.getContext(), token.toString());
+
+ mCallback.onSuccess();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/PluginsCheckerWPOrg.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/PluginsCheckerWPOrg.java
new file mode 100644
index 000000000..ade442b88
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/PluginsCheckerWPOrg.java
@@ -0,0 +1,202 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import android.text.TextUtils;
+import android.webkit.URLUtil;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * This class can test a WordPress installation for plugins/themes that cause problems with WordPress for Android connecting correctly.
+ *
+ * The tool is a black box scanner, it allows remote testing of a WordPress installation.
+ * Find problematic plugins and themes, configuration issues and other glitches that can cause problems with our apps.
+ *
+ */
+public class PluginsCheckerWPOrg {
+ private final static String BB_PLUGINS_LIST_URL = "https://raw.githubusercontent.com/wordpress-mobile/app-blocking-plugins/master/xmlrpc-plugins.json";
+
+ // Do not use the WP-APP user agent. Requests could be blocked if made from our app UA.
+ private final static String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36";
+
+ /** Socket timeout in milliseconds for the requests */
+ private static final int REQUEST_TIMEOUT_MS = 30000;
+
+ String mOriginalURL;
+
+ public PluginsCheckerWPOrg(String url) {
+ mOriginalURL = url;
+ }
+
+ /**
+ * This routine does a black box scanning on the plugis folder of the remote host, and tries to find plugins that cause
+ * problems connecting to the host from one of our mobile apps.
+ */
+ public List<Plugin> checkForPlugins() {
+ String responseHTML = downloadPluginsList();
+ if (TextUtils.isEmpty(responseHTML)) {
+ AppLog.w(AppLog.T.NUX, "Without the list we can't check if the host has some BB plugins installed on it.");
+ return null;
+ }
+
+ JSONArray listOfPlugins;
+ try {
+ listOfPlugins = new JSONArray(responseHTML);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NUX, "Error while parsing the list of plugins returned from the server.", e);
+ return null;
+ }
+
+ // we have the list. Start the process of checking for plugins
+ String baseURL = getBaseURL(mOriginalURL);
+
+ if (!baseURL.contains("/plugins")) {
+ baseURL = baseURL + "/wp-content/plugins/";
+ }
+
+ AppLog.i(AppLog.T.NUX, "The calculated plugins URL is the following: " + baseURL);
+
+ if (!URLUtil.isValidUrl(baseURL)) {
+ AppLog.w(AppLog.T.NUX, "The calculated plugins URL isn't a valid URL. Returning now.");
+ return null;
+ }
+
+ int respCode = openConnection(baseURL);
+ if (respCode != HttpURLConnection.HTTP_OK && respCode != 401 && respCode != 403) {
+ AppLog.w(AppLog.T.NUX, "The request to plugins URL returned with an unexpected HTTP error code. Returning now.");
+ return null;
+ }
+
+ AppLog.i(AppLog.T.NUX, "Start checking the plugins list..");
+ ArrayList<Plugin> listOfBBPlugins = new ArrayList<>();
+ for (int i=0; i<listOfPlugins.length(); i++) {
+ try {
+ JSONObject currentObject = listOfPlugins.getJSONObject(i);
+ String currentPluginURL = baseURL + currentObject.getString("name") + "/";
+ AppLog.i(AppLog.T.NUX, "Testing the following plugin " + currentPluginURL);
+ respCode = openConnection(currentPluginURL);
+ if (respCode != HttpURLConnection.HTTP_NOT_FOUND) {
+ Plugin currentBBPLugin = new Plugin();
+ currentBBPLugin.name = currentObject.getString("name");
+ currentBBPLugin.url = currentObject.getString("url");
+ listOfBBPlugins.add(currentBBPLugin);
+ AppLog.i(AppLog.T.NUX, "Plugin found on the server: " + currentObject.getString("name"));
+ } else {
+ AppLog.i(AppLog.T.NUX, "Plugin NOT found on the server: " + currentObject.getString("name"));
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NUX, "Error while parsing the " + i + " in the list of plugins returned from the server.", e);
+ }
+ }
+
+ return listOfBBPlugins;
+ }
+
+ private String downloadPluginsList() {
+ HttpURLConnection conn = null;
+ String response = "";
+ try {
+ URL url = new URL(BB_PLUGINS_LIST_URL);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setReadTimeout(REQUEST_TIMEOUT_MS);
+ conn.setConnectTimeout(REQUEST_TIMEOUT_MS);
+ conn.setRequestProperty("User-Agent", USER_AGENT);
+ conn.setUseCaches(false);
+ conn.setRequestProperty("Connection", "close");
+ conn.setRequestMethod("GET");
+ BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
+ String inLine;
+ while ((inLine = in.readLine()) != null) {
+ response += inLine;
+ }
+ } catch (Exception e) {
+ AppLog.e(AppLog.T.NUX, "Error while downloading the plugins list from the server...", e);
+ } finally {
+ try {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ } catch (Exception e) {
+ }
+ }
+ return response;
+ }
+
+ private int openConnection(String url) {
+ HttpURLConnection conn = null;
+ try {
+ URL requestURL = new URL(url);
+ conn = (HttpURLConnection) requestURL.openConnection();
+ conn.setReadTimeout(REQUEST_TIMEOUT_MS);
+ conn.setConnectTimeout(REQUEST_TIMEOUT_MS);
+ conn.setRequestProperty("User-Agent", USER_AGENT);
+ conn.setUseCaches(false);
+ conn.setRequestProperty("Connection", "close");
+ conn.setRequestMethod("GET");
+ conn.connect();
+ int respCode = conn.getResponseCode();
+ return respCode;
+ } catch (Exception e) {
+ AppLog.e(AppLog.T.NUX, "Error while checking for the plugins folder on the server.", e);
+ } finally {
+ try {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ } catch (Exception e) {
+ }
+
+ }
+ return 0;
+ }
+
+ private String getBaseURL(String url) {
+ String sanitizedURL = url;
+ try {
+ sanitizedURL = truncateURLAtPrefix(sanitizedURL, "wp-login.php" );
+ sanitizedURL = truncateURLAtPrefix(sanitizedURL, "/wp-admin" );
+ sanitizedURL = truncateURLAtPrefix(sanitizedURL, "/wp-content" );
+ sanitizedURL = truncateURLAtPrefix(sanitizedURL, "/xmlrpc.php" );
+ } catch (IllegalArgumentException e) {
+ AppLog.e(AppLog.T.NUX, "Can't clean the original url: " + sanitizedURL, e);
+ }
+ while (sanitizedURL.endsWith("/")) {
+ sanitizedURL = sanitizedURL.substring(0, sanitizedURL.length() - 1);
+ }
+ return sanitizedURL;
+ }
+
+ private String truncateURLAtPrefix(String url, String prefix) throws IllegalArgumentException {
+ if (!URLUtil.isValidUrl(url)) {
+ throw new IllegalArgumentException("Input URL " + url + " is not valid!");
+ }
+ if (TextUtils.isEmpty(prefix)) {
+ throw new IllegalArgumentException("Input prefix is empty or null");
+ }
+
+ if (url.indexOf(prefix) > 0) {
+ url = url.substring(0, url.indexOf(prefix));
+ }
+
+ if (!URLUtil.isValidUrl(url)) {
+ throw new IllegalArgumentException("The new URL " + url + " is not valid!");
+ }
+
+ return url;
+ }
+
+ public class Plugin {
+ String name;
+ String url;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/UpdateBlogListTask.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/UpdateBlogListTask.java
new file mode 100644
index 000000000..12e143b59
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/UpdateBlogListTask.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.ui.accounts.helpers;
+
+import android.content.Context;
+import android.os.AsyncTask;
+
+import org.wordpress.android.ui.accounts.BlogUtils;
+import org.wordpress.android.ui.accounts.helpers.FetchBlogListAbstract.Callback;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.CoreEvents;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import de.greenrobot.event.EventBus;
+
+public class UpdateBlogListTask extends AsyncTask<Void, Void, List<Map<String, Object>>> {
+ public static final int GET_BLOG_LIST_TIMEOUT = 30000;
+ protected int mErrorMsgId;
+ protected Context mContext;
+ protected boolean mBlogListChanged;
+ protected static List<Map<String, Object>> mUserBlogList;
+
+ public UpdateBlogListTask(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ }
+
+ @Override
+ protected List<Map<String, Object>> doInBackground(Void... args) {
+ final String username = AccountHelper.getDefaultAccount().getUserName();
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ FetchBlogListWPCom fetchBlogList = new FetchBlogListWPCom(mContext);
+ fetchBlogList.execute(new Callback() {
+ @Override
+ public void onSuccess(List<Map<String, Object>> userBlogList) {
+ mUserBlogList = userBlogList;
+ if (mUserBlogList != null) {
+ mBlogListChanged = BlogUtils.syncBlogs(mContext, mUserBlogList, username);
+ }
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onError(int messageId, boolean twoStepCodeRequired, boolean httpAuthRequired, boolean erroneousSslCertificate,
+ String clientResponse) {
+ mErrorMsgId = messageId;
+ countDownLatch.countDown();
+ }
+ });
+ try {
+ countDownLatch.await(GET_BLOG_LIST_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ AppLog.e(T.NUX, e);
+ }
+ return mUserBlogList;
+ }
+
+ public static class GenericUpdateBlogListTask extends UpdateBlogListTask {
+ public GenericUpdateBlogListTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onPostExecute(final List<Map<String, Object>> userBlogList) {
+ if (mBlogListChanged) {
+ EventBus.getDefault().post(new CoreEvents.BlogListChanged());
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkRequestFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkRequestFragment.java
new file mode 100644
index 000000000..47ea7dec4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkRequestFragment.java
@@ -0,0 +1,162 @@
+package org.wordpress.android.ui.accounts.login;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.ui.accounts.HelpActivity;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.widgets.WPTextView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MagicLinkRequestFragment extends Fragment {
+ public static final String EMAIL_KEY = "email";
+ public static final String CLIENT_ID_KEY = "client_id";
+ public static final String CLIENT_SECRET_KEY = "client_secret";
+ public static final String ERROR_KEY = "error";
+
+ public interface OnMagicLinkFragmentInteraction {
+ void onMagicLinkSent();
+ void onEnterPasswordRequested();
+ }
+
+ private static final String ARG_EMAIL_ADDRESS = "arg_email_address";
+
+ private String mEmail;
+ private OnMagicLinkFragmentInteraction mListener;
+ private ProgressDialog mProgressDialog;
+ private TextView mRequestEmailView;
+
+ public static MagicLinkRequestFragment newInstance(String email) {
+ MagicLinkRequestFragment fragment = new MagicLinkRequestFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_EMAIL_ADDRESS, email);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public MagicLinkRequestFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getArguments() != null) {
+ mEmail = getArguments().getString(ARG_EMAIL_ADDRESS);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.magic_link_request_fragment, container, false);
+ WPTextView magicLinkButton = (WPTextView) view.findViewById(R.id.magic_button);
+ magicLinkButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ sendMagicLinkRequest();
+ }
+ });
+
+ mRequestEmailView = (TextView) view.findViewById(R.id.password_layout);
+ mRequestEmailView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mListener.onEnterPasswordRequested();
+ }
+ });
+
+ initInfoButtons(view);
+
+ return view;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof OnMagicLinkFragmentInteraction) {
+ mListener = (OnMagicLinkFragmentInteraction) context;
+ } else {
+ throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mListener = null;
+ }
+
+ private void initInfoButtons(View rootView) {
+ View.OnClickListener infoButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), HelpActivity.class);
+ intent.putExtra(HelpshiftHelper.ORIGIN_KEY, HelpshiftHelper.Tag.ORIGIN_LOGIN_SCREEN_HELP);
+ startActivity(intent);
+ }
+ };
+ ImageView infoButton = (ImageView) rootView.findViewById(R.id.info_button);
+ infoButton.setOnClickListener(infoButtonListener);
+ }
+
+ private void sendMagicLinkRequest() {
+ disableRequestEmailButtonAndShowProgressDialog();
+
+ Map<String, String> params = new HashMap<>();
+ params.put(EMAIL_KEY, mEmail);
+ params.put(CLIENT_ID_KEY, BuildConfig.OAUTH_APP_ID);
+ params.put(CLIENT_SECRET_KEY, BuildConfig.OAUTH_APP_SECRET);
+
+ WordPress.getRestClientUtilsV1_1().sendLoginEmail(params, new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (mListener != null) {
+ mProgressDialog.cancel();
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_MAGIC_LINK_REQUESTED);
+ mListener.onMagicLinkSent();
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ HashMap<String, String> errorProperties = new HashMap<>();
+ errorProperties.put(ERROR_KEY, error.getMessage());
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_MAGIC_LINK_FAILED, errorProperties);
+ Snackbar.make(getView(), R.string.magic_link_unavailable_error_message, Snackbar.LENGTH_SHORT);
+ if (mListener != null) {
+ mListener.onEnterPasswordRequested();
+ }
+ }
+ });
+ }
+
+ private void disableRequestEmailButtonAndShowProgressDialog() {
+ mRequestEmailView.setClickable(false);
+ mProgressDialog = ProgressDialog.show(getActivity(), "", "Requesting log-in email", true, true, new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mRequestEmailView.setClickable(true);
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSentFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSentFragment.java
new file mode 100644
index 000000000..7b2373ab1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSentFragment.java
@@ -0,0 +1,82 @@
+package org.wordpress.android.ui.accounts.login;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.accounts.HelpActivity;
+import org.wordpress.android.util.HelpshiftHelper;
+
+public class MagicLinkSentFragment extends Fragment {
+ public interface OnMagicLinkSentInteraction {
+ void onEnterPasswordRequested();
+ }
+
+ private OnMagicLinkSentInteraction mListener;
+
+ public MagicLinkSentFragment() {
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof OnMagicLinkSentInteraction) {
+ mListener = (OnMagicLinkSentInteraction) context;
+ } else {
+ throw new RuntimeException(context.toString() + " must implement OnMagicLinkSentInteraction");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.magic_link_sent_fragment, container, false);
+
+ TextView enterPasswordView = (TextView) view.findViewById(R.id.password_layout);
+ enterPasswordView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mListener != null) {
+ mListener.onEnterPasswordRequested();
+ }
+ }
+ });
+
+ TextView openEmailView = (TextView) view.findViewById(R.id.open_email_button);
+ openEmailView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ openEmailClient();
+ }
+ });
+
+ initInfoButtons(view);
+
+ return view;
+ }
+
+ private void initInfoButtons(View rootView) {
+ View.OnClickListener infoButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), HelpActivity.class);
+ intent.putExtra(HelpshiftHelper.ORIGIN_KEY, HelpshiftHelper.Tag.ORIGIN_LOGIN_SCREEN_HELP);
+ startActivity(intent);
+ }
+ };
+ ImageView infoButton = (ImageView) rootView.findViewById(R.id.info_button);
+ infoButton.setOnClickListener(infoButtonListener);
+ }
+
+ private void openEmailClient() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_APP_EMAIL);
+ getActivity().startActivity(intent);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInActivity.java
new file mode 100644
index 000000000..d0d56dc6b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInActivity.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.ui.accounts.login;
+
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.accounts.SignInActivity;
+
+public class MagicLinkSignInActivity extends SignInActivity
+ implements MagicLinkRequestFragment.OnMagicLinkFragmentInteraction,
+ MagicLinkSignInFragment.OnMagicLinkRequestInteraction, MagicLinkSentFragment.OnMagicLinkSentInteraction {
+ public static final String MAGIC_LOGIN = "magic-login";
+ public static final String TOKEN_PARAMETER = "token";
+ private ProgressDialog mProgressDialog;
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (hasMagicLinkLoginIntent()) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_MAGIC_LINK_OPENED);
+ attemptLoginWithToken(getIntent().getData());
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ cancelProgressDialog();
+ }
+
+ @Override
+ public MagicLinkSignInFragment getSignInFragment() {
+ MagicLinkSignInFragment signInFragment =
+ (MagicLinkSignInFragment) getSupportFragmentManager().findFragmentByTag(MagicLinkSignInFragment.TAG);
+ if (signInFragment == null) {
+ return new MagicLinkSignInFragment();
+ } else {
+ return signInFragment;
+ }
+ }
+
+ @Override
+ public void onMagicLinkSent() {
+ MagicLinkSentFragment magicLinkSentFragment = new MagicLinkSentFragment();
+ slideInFragment(magicLinkSentFragment);
+ }
+
+ @Override
+ public void onEnterPasswordRequested() {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_MAGIC_LINK_EXITED);
+ getSignInFragment().setShouldShowPassword(true);
+
+ popBackStackToSignInFragment();
+ }
+
+ @Override
+ public void onMagicLinkRequestSuccess(String email) {
+ saveEmailToAccount(email);
+
+ MagicLinkRequestFragment magicLinkRequestFragment = MagicLinkRequestFragment.newInstance(email);
+ slideInFragment(magicLinkRequestFragment);
+ }
+
+ @Override
+ protected void addSignInFragment() {
+ MagicLinkSignInFragment signInFragment = new MagicLinkSignInFragment();
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ fragmentTransaction.replace(R.id.fragment_container, signInFragment, MagicLinkSignInFragment.TAG);
+ fragmentTransaction.commit();
+ }
+
+ private void cancelProgressDialog() {
+ if (mProgressDialog != null && mProgressDialog.isShowing()) {
+ mProgressDialog.cancel();
+ }
+ }
+
+ private boolean hasMagicLinkLoginIntent() {
+ String action = getIntent().getAction();
+ Uri uri = getIntent().getData();
+
+ return Intent.ACTION_VIEW.equals(action) && uri != null && uri.getHost().contains(MAGIC_LOGIN);
+ }
+
+ private void attemptLoginWithToken(Uri uri) {
+ getSignInFragment().setToken(uri.getQueryParameter(TOKEN_PARAMETER));
+ MagicLinkSignInFragment magicLinkSignInFragment = getSignInFragment();
+ slideInFragment(magicLinkSignInFragment, false);
+
+ mProgressDialog = ProgressDialog
+ .show(this, "", getString(R.string.logging_in), true, true, new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ getSignInFragment().setToken("");
+ }
+ });
+ mProgressDialog.show();
+ }
+
+ private void saveEmailToAccount(String email) {
+ Account account = AccountHelper.getDefaultAccount();
+ account.setUserName(email);
+ account.save();
+ }
+
+ private void popBackStackToSignInFragment() {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ while (fragmentManager.getBackStackEntryCount() > 1) {
+ fragmentManager.popBackStackImmediate();
+ }
+
+ getSupportFragmentManager().popBackStack();
+ }
+
+ private void slideInFragment(Fragment fragment) {
+ slideInFragment(fragment, true);
+ }
+
+ private void slideInFragment(Fragment fragment, boolean shouldAddToBackStack) {
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ fragmentTransaction.setCustomAnimations(R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left,
+ R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right);
+ fragmentTransaction.replace(R.id.fragment_container, fragment);
+ if (shouldAddToBackStack) {
+ fragmentTransaction.addToBackStack(null);
+ }
+ fragmentTransaction.commitAllowingStateLoss();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInFragment.java
new file mode 100644
index 000000000..cdc146eed
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/MagicLinkSignInFragment.java
@@ -0,0 +1,264 @@
+package org.wordpress.android.ui.accounts.login;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Patterns;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.google.android.gms.auth.api.credentials.Credential;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.accounts.SignInFragment;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.EditTextUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MagicLinkSignInFragment extends SignInFragment {
+ public static final String REASON_ERROR = "error";
+ public static final String REASON_ERROR_TAKEN = "taken";
+ public static final int MAX_EMAIL_LENGTH = 100;
+ public static final String MAGIC_LINK_PROPERTY = "magic_link";
+
+ public interface OnMagicLinkRequestInteraction {
+ void onMagicLinkRequestSuccess(String email);
+ }
+
+ private OnMagicLinkRequestInteraction mListener;
+ private String mToken = "";
+ private boolean mShouldShowPassword;
+ private boolean mSmartLockEnabled = true;
+
+ public MagicLinkSignInFragment() {
+ super();
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof OnMagicLinkRequestInteraction) {
+ mListener = (OnMagicLinkRequestInteraction) context;
+ } else {
+ throw new RuntimeException(context.toString() + " must implement OnMagicLinkRequestInteraction");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ if (savedInstanceState == null) {
+ configureMagicLinkUI();
+ }
+ mUsernameEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if ((didPressNextKey(actionId, event) || didPressEnterKey(actionId, event)) && !isEnterPasswordMode()) {
+ signIn();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!mToken.isEmpty()) {
+ attemptLoginWithMagicLink();
+ mSmartLockEnabled = false;
+ } else {
+ mSmartLockEnabled = true;
+ }
+ if (mShouldShowPassword) {
+ showPasswordFieldAndFocus();
+ }
+ }
+
+ public void setToken(String token) {
+ mToken = token;
+ }
+
+ public void setShouldShowPassword(boolean shouldShowPassword) {
+ mShouldShowPassword = shouldShowPassword;
+ }
+
+ @Override
+ protected void toggleSignInMode(){
+ if (mUrlButtonLayout.getVisibility() == View.VISIBLE) {
+ configureMagicLinkUI();
+ mSelfHosted = false;
+ } else {
+ showSelfHostedSignInForm();
+ mSelfHosted = true;
+ }
+ }
+
+ @Override
+ protected void signIn() {
+ if (mSelfHosted || isEnterPasswordMode()) {
+ super.signIn();
+ } else {
+ if (isUsernameEmail()) {
+ startProgress(getActivity().getString(R.string.checking_email));
+ requestWPComEmailCheck();
+ } else {
+ showPasswordFieldAndFocus();
+ }
+ }
+ }
+
+ @Override
+ protected void track(AnalyticsTracker.Stat stat, Map<String, Boolean> properties) {
+ if (properties == null) {
+ properties = new HashMap<>();
+ }
+ properties.put(MAGIC_LINK_PROPERTY, true);
+ AnalyticsTracker.track(stat, properties);
+ }
+
+ @Override
+ protected void finishCurrentActivity(final List<Map<String, Object>> userBlogList) {
+ if (!isAdded()) {
+ return;
+ }
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (userBlogList != null) {
+ Intent intent = new Intent(getActivity(), WPMainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(MagicLinkSignInActivity.MAGIC_LOGIN, true);
+
+ getActivity().startActivity(intent);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void showSelfHostedSignInForm() {
+ super.showSelfHostedSignInForm();
+ showPasswordField();
+ }
+
+ private boolean isEnterPasswordMode() {
+ return mPasswordLayout.getVisibility() == View.VISIBLE;
+ }
+
+ private void configureMagicLinkUI() {
+ showDotComSignInForm();
+ mPasswordLayout.setVisibility(View.GONE);
+ mForgotPassword.setVisibility(View.GONE);
+ mSignInButton.setText(getString(R.string.button_next));
+ }
+
+ private void showPasswordFieldAndFocus() {
+ if (isAdded()) {
+ endProgress();
+ showPasswordField();
+ mPasswordEditText.requestFocus();
+ mSignInButton.setText(getString(R.string.sign_in));
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mPasswordEditText, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+
+ private void showPasswordField() {
+ if (isAdded()) {
+ mPasswordLayout.setVisibility(View.VISIBLE);
+ mForgotPassword.setVisibility(View.VISIBLE);
+ if (!mSelfHosted) {
+ mPasswordEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ }
+ mSignInButton.setText(R.string.sign_in);
+ }
+ }
+
+ private void requestWPComEmailCheck() {
+ WordPress.getRestClientUtilsV0().isAvailable(mUsername, new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ try {
+ String errorReason = response.getString(REASON_ERROR);
+ if (errorReason != null && errorReason.equals(REASON_ERROR_TAKEN) && mListener != null) {
+ mListener.onMagicLinkRequestSuccess(mUsername);
+ } else {
+ showPasswordFieldAndFocus();
+ }
+ } catch (JSONException error) {
+ AppLog.e(AppLog.T.MAIN, error);
+ showPasswordFieldAndFocus();
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ showPasswordFieldAndFocus();
+ }
+ });
+ }
+
+ private boolean isUsernameEmail() {
+ mUsername = EditTextUtils.getText(mUsernameEditText).trim();
+ Pattern emailRegExPattern = Patterns.EMAIL_ADDRESS;
+ Matcher matcher = emailRegExPattern.matcher(mUsername);
+
+ return matcher.find() && mUsername.length() <= MAX_EMAIL_LENGTH;
+ }
+
+ public void attemptLoginWithMagicLink() {
+ saveUsernameAndTokenToAccount();
+
+ SimperiumUtils.configureSimperium(WordPress.getContext(), mToken);
+
+ configureAccountAfterSuccessfulSignIn();
+ }
+
+ private void saveUsernameAndTokenToAccount() {
+ Account account = AccountHelper.getDefaultAccount();
+ account.setAccessToken(mToken);
+ account.setUserName(mUsername);
+ account.save();
+ account.fetchAccountDetails();
+ }
+
+ @Override
+ public void onCredentialRetrieved(Credential credential) {
+ super.onCredentialRetrieved(credential);
+ showPasswordField();
+ }
+
+ @Override
+ public boolean canAutofillUsernameAndPassword() {
+ return mSmartLockEnabled && EditTextUtils.getText(mUsernameEditText).isEmpty()
+ && EditTextUtils.getText(mPasswordEditText).isEmpty();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java
new file mode 100644
index 000000000..41178da31
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.ui.comments;
+
+public class CommentActionResult {
+
+ public static final int COMMENT_ID_ON_ERRORS = -1;
+ public static final int COMMENT_ID_UNKNOWN = -2; // This is used primarily for replies, when the commentID isn't known.
+
+ private long mCommentID = COMMENT_ID_UNKNOWN;
+ private final String mMessage;
+
+ public CommentActionResult(long commentID, String message) {
+ mCommentID = commentID;
+ mMessage = message;
+ }
+
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mCommentID != COMMENT_ID_ON_ERRORS; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java
new file mode 100644
index 000000000..acc83e704
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java
@@ -0,0 +1,503 @@
+package org.wordpress.android.ui.comments;
+
+import android.os.Handler;
+import android.text.TextUtils;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.VolleyUtils;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+import org.xmlrpc.android.XMLRPCFault;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * actions related to comments - replies, moderating, etc.
+ * methods below do network calls in the background & update local DB upon success
+ * all methods below MUST be called from UI thread
+ */
+
+public class CommentActions {
+
+ private CommentActions() {
+ throw new AssertionError();
+ }
+
+ /*
+ * listener when a comment action is performed
+ */
+ public interface CommentActionListener {
+ void onActionResult(CommentActionResult result);
+ }
+
+ /*
+ * listener when comments are moderated or deleted
+ */
+ public interface OnCommentsModeratedListener {
+ void onCommentsModerated(final CommentList moderatedComments);
+ }
+
+ /*
+ * used by comment fragments to alert container activity of a change to one or more
+ * comments (moderated, deleted, added, etc.)
+ */
+ public enum ChangeType {EDITED, REPLIED}
+ public interface OnCommentChangeListener {
+ void onCommentChanged(ChangeType changeType);
+ }
+
+ public interface OnCommentActionListener {
+ void onModerateComment(int accountId, Comment comment, CommentStatus newStatus);
+ }
+
+ public interface OnNoteCommentActionListener {
+ void onModerateCommentForNote(Note note, CommentStatus newStatus);
+ }
+
+
+ /**
+ * reply to an individual comment
+ */
+ static void submitReplyToComment(final int accountId,
+ final Comment comment,
+ final String replyText,
+ final CommentActionListener actionListener) {
+ final Blog blog = WordPress.getBlog(accountId);
+ if (blog==null || comment==null || TextUtils.isEmpty(replyText)) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ }
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Map<String, Object> replyHash = new HashMap<>();
+ replyHash.put("comment_parent", Long.toString(comment.commentID));
+ replyHash.put("content", replyText);
+ replyHash.put("author", "");
+ replyHash.put("author_url", "");
+ replyHash.put("author_email", "");
+
+ Object[] params = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ Long.toString(comment.postID),
+ replyHash };
+
+ long newCommentID;
+ String message = null;
+ try {
+ Object newCommentIDObject = client.call(Method.NEW_COMMENT, params);
+ if (newCommentIDObject instanceof Integer) {
+ newCommentID = ((Integer) newCommentIDObject).longValue();
+ } else if (newCommentIDObject instanceof Long) {
+ newCommentID = (Long) newCommentIDObject;
+ } else {
+ AppLog.e(T.COMMENTS, "wp.newComment returned the wrong data type");
+ newCommentID = CommentActionResult.COMMENT_ID_ON_ERRORS;
+ }
+ } catch (XMLRPCFault e) {
+ AppLog.e(T.COMMENTS, "Error while sending the new comment", e);
+ newCommentID = CommentActionResult.COMMENT_ID_ON_ERRORS;
+ message = e.getFaultString();
+ } catch (XMLRPCException | IOException | XmlPullParserException e) {
+ AppLog.e(T.COMMENTS, "Error while sending the new comment", e);
+ newCommentID = CommentActionResult.COMMENT_ID_ON_ERRORS;
+ }
+
+ final CommentActionResult cr = new CommentActionResult(newCommentID, message);
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onActionResult(cr);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * reply to an individual comment that came from a notification - this differs from
+ * submitReplyToComment() in that it enables responding to a reply to a comment this
+ * user made on someone else's blog
+ */
+ public static void submitReplyToCommentNote(final Note note,
+ final String replyText,
+ final CommentActionListener actionListener) {
+ if (note == null || TextUtils.isEmpty(replyText)) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+
+ return;
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_UNKNOWN, null));
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ if (volleyError != null)
+ AppLog.e(T.COMMENTS, volleyError.getMessage(), volleyError);
+ if (actionListener != null) {
+ actionListener.onActionResult(
+ new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, VolleyUtils.messageStringFromVolleyError(volleyError))
+ );
+ }
+ }
+ };
+
+ Note.Reply reply = note.buildReply(replyText);
+ WordPress.getRestClientUtils().replyToComment(reply.getContent(), reply.getRestPath(), listener, errorListener);
+ }
+
+ /**
+ * reply to an individual comment via the WP.com REST API
+ */
+ public static void submitReplyToCommentRestApi(long siteId, long commentId,
+ final String replyText,
+ final CommentActionListener actionListener) {
+ if (TextUtils.isEmpty(replyText)) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ return;
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_UNKNOWN, null));
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ if (volleyError != null)
+ AppLog.e(T.COMMENTS, volleyError.getMessage(), volleyError);
+ if (actionListener != null)
+ actionListener.onActionResult(
+ new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, VolleyUtils.messageStringFromVolleyError(volleyError))
+ );
+ }
+ };
+
+ WordPress.getRestClientUtils().replyToComment(siteId, commentId, replyText, listener, errorListener);
+ }
+
+ /**
+ * Moderate a comment from a WPCOM notification
+ */
+ public static void moderateCommentRestApi(long siteId,
+ final long commentId,
+ CommentStatus newStatus,
+ final CommentActionListener actionListener) {
+
+ WordPress.getRestClientUtils().moderateComment(
+ String.valueOf(siteId),
+ String.valueOf(commentId),
+ CommentStatus.toRESTString(newStatus),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(commentId, null));
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Moderate a comment from a WPCOM notification
+ */
+ public static void moderateCommentForNote(final Note note, CommentStatus newStatus,
+ final CommentActionListener actionListener) {
+ WordPress.getRestClientUtils().moderateComment(
+ String.valueOf(note.getSiteId()),
+ String.valueOf(note.getCommentId()),
+ CommentStatus.toRESTString(newStatus),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(note.getCommentId(), null));
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * change the status of a single comment
+ */
+ static void moderateComment(final int accountId,
+ final Comment comment,
+ final CommentStatus newStatus,
+ final CommentActionListener actionListener) {
+ // deletion is handled separately
+ if (newStatus != null && (newStatus.equals(CommentStatus.TRASH) || newStatus.equals(CommentStatus.DELETE))) {
+ deleteComment(accountId, comment, actionListener, newStatus.equals(CommentStatus.DELETE));
+ return;
+ }
+
+ final Blog blog = WordPress.getBlog(accountId);
+
+ if (blog==null || comment==null || newStatus==null || newStatus==CommentStatus.UNKNOWN) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ final boolean success = ApiHelper.editComment(blog, comment, newStatus);
+
+ if (success) {
+ CommentTable.updateCommentStatus(blog.getLocalTableBlogId(), comment.commentID, CommentStatus
+ .toString(newStatus));
+ }
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onActionResult(new CommentActionResult(comment.commentID, null));
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * change the status of multiple comments
+ * TODO: investigate using system.multiCall to perform a single call to moderate the list
+ */
+ static void moderateComments(final int accountId,
+ final CommentList comments,
+ final CommentStatus newStatus,
+ final OnCommentsModeratedListener actionListener) {
+ // deletion is handled separately
+ if (newStatus != null && (newStatus.equals(CommentStatus.TRASH) || newStatus.equals(CommentStatus.DELETE))) {
+ deleteComments(accountId, comments, actionListener, newStatus.equals(CommentStatus.DELETE));
+ return;
+ }
+
+ final Blog blog = WordPress.getBlog(accountId);
+
+ if (blog==null || comments==null || comments.size() == 0 || newStatus==null || newStatus==CommentStatus.UNKNOWN) {
+ if (actionListener != null)
+ actionListener.onCommentsModerated(new CommentList());
+ return;
+ }
+
+ final CommentList moderatedComments = new CommentList();
+ final String newStatusStr = CommentStatus.toString(newStatus);
+ final int localBlogId = blog.getLocalTableBlogId();
+
+ final Handler handler = new Handler();
+ new Thread() {
+ @Override
+ public void run() {
+ for (Comment comment: comments) {
+ if (ApiHelper.editComment(blog, comment, newStatus)) {
+ comment.setStatus(newStatusStr);
+ moderatedComments.add(comment);
+ }
+ }
+
+ // update status in SQLite of successfully moderated comments
+ CommentTable.updateCommentsStatus(localBlogId, moderatedComments, newStatusStr);
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onCommentsModerated(moderatedComments);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * delete (trash) a single comment
+ */
+ private static void deleteComment(final int accountId,
+ final Comment comment,
+ final CommentActionListener actionListener,
+ final boolean deletePermanently) {
+ final Blog blog = WordPress.getBlog(accountId);
+ if (blog==null || comment==null) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Object[] params = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ comment.commentID,
+ deletePermanently};
+
+ Object result;
+ try {
+ result = client.call(Method.DELETE_COMMENT, params);
+ } catch (final XMLRPCException | XmlPullParserException | IOException e) {
+ AppLog.e(T.COMMENTS, "Error while deleting comment", e);
+ result = null;
+ }
+
+ //update local database
+ final boolean success = (result != null && Boolean.parseBoolean(result.toString()));
+ if (success){
+ if (deletePermanently) {
+ CommentTable.deleteComment(accountId, comment.commentID);
+ }
+ else {
+ // update status in SQLite of successfully moderated comments
+ CommentTable.updateCommentStatus(blog.getLocalTableBlogId(), comment.commentID,
+ CommentStatus.toString(CommentStatus.TRASH));
+ }
+ }
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onActionResult(new CommentActionResult(comment.commentID, null));
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * delete multiple comments
+ */
+ private static void deleteComments(final int accountId,
+ final CommentList comments,
+ final OnCommentsModeratedListener actionListener,
+ final boolean deletePermanently) {
+ final Blog blog = WordPress.getBlog(accountId);
+
+ if (blog==null || comments==null || comments.size() == 0) {
+ if (actionListener != null)
+ actionListener.onCommentsModerated(new CommentList());
+ return;
+ }
+
+ final CommentList deletedComments = new CommentList();
+ final int localBlogId = blog.getLocalTableBlogId();
+ final int remoteBlogId = blog.getRemoteBlogId();
+
+ final Handler handler = new Handler();
+ new Thread() {
+ @Override
+ public void run() {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ for (Comment comment: comments) {
+ Object[] params = {
+ remoteBlogId,
+ blog.getUsername(),
+ blog.getPassword(),
+ comment.commentID,
+ deletePermanently};
+
+ Object result;
+ try {
+ result = client.call(Method.DELETE_COMMENT, params);
+ boolean success = (result != null && Boolean.parseBoolean(result.toString()));
+ if (success)
+ deletedComments.add(comment);
+ } catch (XMLRPCException | XmlPullParserException | IOException e) {
+ AppLog.e(T.COMMENTS, "Error while deleting comment", e);
+ }
+ }
+
+ // remove successfully deleted comments from SQLite
+ if (deletePermanently) {
+ CommentTable.deleteComments(localBlogId, deletedComments);
+ }
+ else {
+ // update status in SQLite of successfully moderated comments
+ CommentTable.updateCommentsStatus(blog.getLocalTableBlogId(), deletedComments,
+ CommentStatus.toString(CommentStatus.TRASH));
+ }
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onCommentsModerated(deletedComments);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java
new file mode 100644
index 000000000..d5e5bd6fb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java
@@ -0,0 +1,486 @@
+package org.wordpress.android.ui.comments;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.text.Spanned;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPHtml;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.HashSet;
+
+class CommentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ interface OnDataLoadedListener {
+ void onDataLoaded(boolean isEmpty);
+ }
+
+ interface OnLoadMoreListener {
+ void onLoadMore();
+ }
+
+ interface OnSelectedItemsChangeListener {
+ void onSelectedItemsChanged();
+ }
+
+ interface OnCommentPressedListener {
+ void onCommentPressed(int position, View view);
+
+ void onCommentLongPressed(int position, View view);
+ }
+
+ private final LayoutInflater mInflater;
+ private final Context mContext;
+
+ private final CommentList mComments = new CommentList();
+ private final HashSet<Long> mSelectedCommentsId = new HashSet<>();
+ private final HashSet<Long> mModeratingCommentsIds = new HashSet<>();
+
+ private final int mStatusColorSpam;
+ private final int mStatusColorUnapproved;
+
+ private final int mLocalBlogId;
+ private final int mAvatarSz;
+ private final String mStatusTextSpam;
+ private final String mStatusTextUnapproved;
+ private final int mSelectedColor;
+ private final int mUnselectedColor;
+
+ private OnDataLoadedListener mOnDataLoadedListener;
+ private OnCommentPressedListener mOnCommentPressedListener;
+ private OnLoadMoreListener mOnLoadMoreListener;
+ private OnSelectedItemsChangeListener mOnSelectedChangeListener;
+
+ private boolean mEnableSelection;
+
+ class CommentHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener, View.OnLongClickListener {
+ private final TextView txtTitle;
+ private final TextView txtComment;
+ private final TextView txtStatus;
+ private final TextView txtDate;
+ private final WPNetworkImageView imgAvatar;
+ private final ImageView imgCheckmark;
+ private final View progressBar;
+ private final ViewGroup containerView;
+
+ public CommentHolder(View view) {
+ super(view);
+ txtTitle = (TextView) view.findViewById(R.id.title);
+ txtComment = (TextView) view.findViewById(R.id.comment);
+ txtStatus = (TextView) view.findViewById(R.id.status);
+ txtDate = (TextView) view.findViewById(R.id.text_date);
+ imgCheckmark = (ImageView) view.findViewById(R.id.image_checkmark);
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.avatar);
+ progressBar = view.findViewById(R.id.moderate_progress);
+ containerView = (ViewGroup) view.findViewById(R.id.layout_container);
+
+ itemView.setOnClickListener(this);
+ itemView.setOnLongClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mOnCommentPressedListener != null) {
+ mOnCommentPressedListener.onCommentPressed(getAdapterPosition(), v);
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ if (mOnCommentPressedListener != null) {
+ mOnCommentPressedListener.onCommentLongPressed(getAdapterPosition(), v);
+ }
+ return true;
+ }
+ }
+
+ CommentAdapter(Context context, int localBlogId) {
+ mInflater = LayoutInflater.from(context);
+ mContext = context;
+
+ mLocalBlogId = localBlogId;
+
+ mStatusColorSpam = ContextCompat.getColor(context, R.color.comment_status_spam);
+ mStatusColorUnapproved = ContextCompat.getColor(context, R.color.comment_status_unapproved);
+
+ mUnselectedColor = ContextCompat.getColor(context, R.color.white);
+ mSelectedColor = ContextCompat.getColor(context, R.color.translucent_grey_lighten_20);
+
+ mStatusTextSpam = context.getResources().getString(R.string.comment_status_spam);
+ mStatusTextUnapproved = context.getResources().getString(R.string.comment_status_unapproved);
+
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium);
+
+ setHasStableIds(true);
+ }
+
+ void setOnDataLoadedListener(OnDataLoadedListener listener) {
+ mOnDataLoadedListener = listener;
+ }
+
+ void setOnLoadMoreListener(OnLoadMoreListener listener) {
+ mOnLoadMoreListener = listener;
+ }
+
+ void setOnCommentPressedListener(OnCommentPressedListener listener) {
+ mOnCommentPressedListener = listener;
+ }
+
+ void setOnSelectedItemsChangeListener(OnSelectedItemsChangeListener listener) {
+ mOnSelectedChangeListener = listener;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.comment_listitem, null);
+ CommentHolder holder = new CommentHolder(view);
+ view.setTag(holder);
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ Comment comment = mComments.get(position);
+ CommentHolder holder = (CommentHolder) viewHolder;
+
+ if (isModeratingCommentId(comment.commentID)) {
+ holder.progressBar.setVisibility(View.VISIBLE);
+ } else {
+ holder.progressBar.setVisibility(View.GONE);
+ }
+
+ holder.txtTitle.setText(Html.fromHtml(comment.getFormattedTitle()));
+ holder.txtComment.setText(comment.getUnescapedCommentTextWithDrawables());
+ holder.txtDate.setText(DateTimeUtils.javaDateToTimeSpan(comment.getDatePublished(), mContext));
+
+ // status is only shown for comments that haven't been approved
+ final boolean showStatus;
+ switch (comment.getStatusEnum()) {
+ case SPAM:
+ showStatus = true;
+ holder.txtStatus.setText(mStatusTextSpam);
+ holder.txtStatus.setTextColor(mStatusColorSpam);
+ break;
+ case UNAPPROVED:
+ showStatus = true;
+ holder.txtStatus.setText(mStatusTextUnapproved);
+ holder.txtStatus.setTextColor(mStatusColorUnapproved);
+ break;
+ default:
+ showStatus = false;
+ break;
+ }
+ holder.txtStatus.setVisibility(showStatus ? View.VISIBLE : View.GONE);
+
+ int checkmarkVisibility;
+ if (mEnableSelection && isItemSelected(position)) {
+ checkmarkVisibility = View.VISIBLE;
+ holder.containerView.setBackgroundColor(mSelectedColor);
+ } else {
+ checkmarkVisibility = View.GONE;
+ holder.imgAvatar.setImageUrl(comment.getAvatarForDisplay(mAvatarSz), WPNetworkImageView.ImageType.AVATAR);
+ holder.containerView.setBackgroundColor(mUnselectedColor);
+ }
+
+ if (holder.imgCheckmark.getVisibility() != checkmarkVisibility) {
+ holder.imgCheckmark.setVisibility(checkmarkVisibility);
+ }
+
+ // comment text needs to be to the left of date/status when the title is a single line and
+ // the status is displayed or else the status may overlap the comment text - note that
+ // getLineCount() will return 0 if the view hasn't been rendered yet, which is why we
+ // check getLineCount() <= 1
+ boolean adjustComment = (showStatus && holder.txtTitle.getLineCount() <= 1);
+ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.txtComment.getLayoutParams();
+ if (adjustComment) {
+ params.addRule(RelativeLayout.LEFT_OF, R.id.layout_date_status);
+ } else {
+ params.addRule(RelativeLayout.LEFT_OF, 0);
+ }
+
+ // request to load more comments when we near the end
+ if (mOnLoadMoreListener != null && position >= getItemCount() - 1
+ && position >= CommentsListFragment.COMMENTS_PER_PAGE - 1) {
+ mOnLoadMoreListener.onLoadMore();
+ }
+ }
+
+ public Comment getItem(int position) {
+ if (isPositionValid(position)) {
+ return mComments.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mComments.get(position).commentID;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mComments.size();
+ }
+
+ private boolean isEmpty() {
+ return getItemCount() == 0;
+ }
+
+ void setEnableSelection(boolean enable) {
+ if (enable == mEnableSelection) return;
+
+ mEnableSelection = enable;
+ if (mEnableSelection) {
+ notifyDataSetChanged();
+ } else {
+ clearSelectedComments();
+ }
+ }
+
+ void clearSelectedComments() {
+ if (mSelectedCommentsId.size() > 0) {
+ mSelectedCommentsId.clear();
+ notifyDataSetChanged();
+ if (mOnSelectedChangeListener != null) {
+ mOnSelectedChangeListener.onSelectedItemsChanged();
+ }
+ }
+ }
+
+ int getSelectedCommentCount() {
+ return mSelectedCommentsId.size();
+ }
+
+ CommentList getSelectedComments() {
+ CommentList comments = new CommentList();
+ if (!mEnableSelection) {
+ return comments;
+ }
+
+ for (Long commentId : mSelectedCommentsId) {
+ int commentIndex = indexOfCommentId(commentId);
+ if (commentIndex > -1) {
+ comments.add(mComments.get(commentIndex));
+ }
+ }
+
+ return comments;
+ }
+
+ private boolean isItemSelected(int position) {
+ Comment comment = getItem(position);
+ return comment != null && mSelectedCommentsId.contains(comment.commentID);
+ }
+
+ void setItemSelected(int position, boolean isSelected, View view) {
+ if (isItemSelected(position) == isSelected) return;
+
+ Comment comment = getItem(position);
+ if (comment == null) return;
+
+ if (isSelected) {
+ mSelectedCommentsId.add(comment.commentID);
+ } else {
+ mSelectedCommentsId.remove(comment.commentID);
+ }
+
+
+ notifyItemChanged(position);
+
+ if (view != null && view.getTag() instanceof CommentHolder) {
+ CommentHolder holder = (CommentHolder) view.getTag();
+ // animate the selection change
+ AniUtils.startAnimation(holder.imgCheckmark, isSelected ? R.anim.cab_select : R.anim.cab_deselect);
+ holder.imgCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE);
+ }
+
+ if (mOnSelectedChangeListener != null) {
+ mOnSelectedChangeListener.onSelectedItemsChanged();
+ }
+ }
+
+ void toggleItemSelected(int position, View view) {
+ setItemSelected(position, !isItemSelected(position), view);
+ }
+
+ public void addModeratingCommentId(long commentId) {
+ mModeratingCommentsIds.add(commentId);
+ int position = indexOfCommentId(commentId);
+ if (position >= 0) {
+ notifyItemChanged(position);
+ }
+ }
+
+ public void removeModeratingCommentId(long commentId) {
+ mModeratingCommentsIds.remove(commentId);
+ int position = indexOfCommentId(commentId);
+ if (position >= 0) {
+ notifyItemChanged(position);
+ }
+ }
+
+ public boolean isModeratingCommentId(long commentId) {
+ return mModeratingCommentsIds.size() > 0
+ && mModeratingCommentsIds.contains(commentId);
+ }
+
+ private int indexOfCommentId(long commentId) {
+ return mComments.indexOfCommentId(commentId);
+ }
+
+ private boolean isPositionValid(int position) {
+ return (position >= 0 && position < mComments.size());
+ }
+
+ void replaceComments(CommentList comments) {
+ mComments.replaceComments(comments);
+ notifyDataSetChanged();
+ }
+
+ void deleteComments(CommentList comments) {
+ mComments.deleteComments(comments);
+ notifyDataSetChanged();
+ if (mOnDataLoadedListener != null) {
+ mOnDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+
+ public void removeComment(Comment comment) {
+ int position = indexOfCommentId(comment.commentID);
+ if (position >= 0) {
+ mComments.remove(position);
+ notifyItemRemoved(position);
+ }
+ }
+
+ /*
+ * clear all comments
+ */
+ void clearComments() {
+ if (mComments != null) {
+ mComments.clear();
+ notifyDataSetChanged();
+ }
+ }
+
+ /*
+ * load comments using an AsyncTask
+ */
+ void loadComments(CommentStatus statusFilter) {
+ if (mIsLoadTaskRunning) {
+ AppLog.w(AppLog.T.COMMENTS, "load comments task already active");
+ } else {
+ new LoadCommentsTask(statusFilter).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ /*
+ * AsyncTask to load comments from SQLite
+ */
+ private boolean mIsLoadTaskRunning = false;
+
+ private class LoadCommentsTask extends AsyncTask<Void, Void, Boolean> {
+ CommentList tmpComments;
+ final CommentStatus mStatusFilter;
+
+ public LoadCommentsTask(CommentStatus statusFilter) {
+ mStatusFilter = statusFilter;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mIsLoadTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsLoadTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ if (mStatusFilter == null) {
+ tmpComments = CommentTable.getCommentsForBlogWithFilter(mLocalBlogId, CommentStatus.UNKNOWN);
+ } else {
+ tmpComments = CommentTable.getCommentsForBlogWithFilter(mLocalBlogId, mStatusFilter);
+ }
+
+ if (mComments.isSameList(tmpComments)) {
+ return false;
+ }
+
+ // pre-calc transient values so they're cached prior to display
+ for (Comment comment : tmpComments) {
+ comment.getDatePublished();
+ comment.getUnescapedPostTitle();
+ comment.getAvatarForDisplay(mAvatarSz);
+ comment.getFormattedTitle();
+
+ String content = StringUtils.notNullStr(comment.getCommentText());
+ //to load images embedded within comments, pass an ImageGetter to WPHtml.fromHtml()
+ Spanned spanned = WPHtml.fromHtml(content, null, null, mContext, null, 0);
+ comment.setUnescapedCommentWithDrawables(spanned);
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ mComments.clear();
+ mComments.addAll(tmpComments);
+ notifyDataSetChanged();
+ }
+
+ if (mOnDataLoadedListener != null) {
+ mOnDataLoadedListener.onDataLoaded(isEmpty());
+ }
+
+ mIsLoadTaskRunning = false;
+ }
+ }
+
+ public HashSet<Long> getSelectedCommentsId() {
+ return mSelectedCommentsId;
+ }
+
+
+ public CommentAdapterState getAdapterState() {
+ return new CommentAdapterState(mSelectedCommentsId, mModeratingCommentsIds);
+ }
+
+ public void setInitialState(CommentAdapterState adapterState) {
+ if (adapterState == null) return;
+
+ if (adapterState.hasSelectedComments()) {
+ mSelectedCommentsId.clear();
+ mSelectedCommentsId.addAll(adapterState.getSelectedComments());
+ setEnableSelection(true);
+ }
+
+ if (adapterState.hasModeratingComments()) {
+ mModeratingCommentsIds.clear();
+ mModeratingCommentsIds.addAll(adapterState.getModeratedCommentsId());
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java
new file mode 100644
index 000000000..6d9ab1530
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.ui.comments;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+import java.util.HashSet;
+
+/**
+ * Used to store state of {@link CommentAdapter}
+ */
+public class CommentAdapterState implements Parcelable {
+ public static final String KEY = "comments_adapter_state";
+
+ private final HashSet<Long> mSelectedComments;
+ private final HashSet<Long> mModeratedCommentsId;
+
+ public CommentAdapterState(@NonNull HashSet<Long> selectedComments, @NonNull HashSet<Long> moderatedCommentsId) {
+ mSelectedComments = selectedComments;
+ mModeratedCommentsId = moderatedCommentsId;
+ }
+
+ public HashSet<Long> getSelectedComments() {
+ return mSelectedComments;
+ }
+
+ public HashSet<Long> getModeratedCommentsId() {
+ return mModeratedCommentsId;
+ }
+
+
+ public boolean hasSelectedComments() {
+ return mSelectedComments != null && mSelectedComments.size() > 0;
+ }
+
+ public boolean hasModeratingComments() {
+ return mModeratedCommentsId != null && mModeratedCommentsId.size() > 0;
+ }
+
+ @SuppressWarnings("unchecked")
+ private CommentAdapterState(Parcel in) {
+ mSelectedComments = (HashSet<Long>) in.readSerializable();
+ mModeratedCommentsId = (HashSet<Long>) in.readSerializable();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeSerializable(mSelectedComments);
+ dest.writeSerializable(mModeratedCommentsId);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator<CommentAdapterState> CREATOR = new Parcelable.Creator<CommentAdapterState>() {
+ @Override
+ public CommentAdapterState createFromParcel(Parcel in) {
+ return new CommentAdapterState(in);
+ }
+
+ @Override
+ public CommentAdapterState[] newArray(int size) {
+ return new CommentAdapterState[size];
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java
new file mode 100644
index 000000000..69e8938e1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java
@@ -0,0 +1,92 @@
+package org.wordpress.android.ui.comments;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import com.simperium.client.BucketObjectMissingException;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+// simple wrapper activity for CommentDetailFragment
+public class CommentDetailActivity extends AppCompatActivity {
+
+ private static final String KEY_COMMENT_DETAIL_LOCAL_TABLE_BLOG_ID = "local_table_blog_id";
+ private static final String KEY_COMMENT_DETAIL_COMMENT_ID = "comment_detail_comment_id";
+ private static final String KEY_COMMENT_DETAIL_NOTE_ID = "comment_detail_note_id";
+ private static final String KEY_COMMENT_DETAIL_IS_REMOTE = "comment_detail_is_remote";
+
+ private static final String TAG_COMMENT_DETAIL_FRAGMENT = "tag_comment_detail_fragment";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.comment_activity_detail);
+
+ setTitle(R.string.comment);
+
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (savedInstanceState == null) {
+ Intent intent = getIntent();
+ CommentDetailFragment commentDetailFragment = null;
+ if (intent.getStringExtra(KEY_COMMENT_DETAIL_NOTE_ID) != null && SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(
+ intent.getStringExtra(KEY_COMMENT_DETAIL_NOTE_ID)
+ );
+
+ if (intent.hasExtra(KEY_COMMENT_DETAIL_IS_REMOTE)) {
+ commentDetailFragment = CommentDetailFragment.newInstanceForRemoteNoteComment(note.getId());
+ } else {
+ commentDetailFragment = CommentDetailFragment.newInstance(note.getId());
+ }
+ } catch (BucketObjectMissingException e) {
+ AppLog.e(AppLog.T.NOTIFS, "CommentDetailActivity was passed an invalid note id.");
+ }
+ } else if (intent.getIntExtra(KEY_COMMENT_DETAIL_LOCAL_TABLE_BLOG_ID, 0) > 0
+ && intent.getLongExtra(KEY_COMMENT_DETAIL_COMMENT_ID, 0) > 0) {
+ commentDetailFragment = CommentDetailFragment.newInstance(
+ intent.getIntExtra(KEY_COMMENT_DETAIL_LOCAL_TABLE_BLOG_ID, 0),
+ intent.getLongExtra(KEY_COMMENT_DETAIL_COMMENT_ID, 0)
+ );
+ }
+
+ if (commentDetailFragment != null) {
+ commentDetailFragment.setRetainInstance(true);
+ getFragmentManager().beginTransaction()
+ .add(R.id.comment_detail_container, commentDetailFragment, TAG_COMMENT_DETAIL_FRAGMENT)
+ .commit();
+ } else {
+ ToastUtils.showToast(this, R.string.error_load_comment);
+ finish();
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.COMMENT_DETAIL);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
new file mode 100644
index 000000000..eafe02723
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
@@ -0,0 +1,1234 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.simperium.client.BucketObjectMissingException;
+import com.wordpress.rest.RestRequest;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.json.JSONObject;
+import org.wordpress.android.Constants;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.SuggestionTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.models.Note.EnabledActions;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.comments.CommentActions.ChangeType;
+import org.wordpress.android.ui.comments.CommentActions.OnCommentActionListener;
+import org.wordpress.android.ui.comments.CommentActions.OnCommentChangeListener;
+import org.wordpress.android.ui.comments.CommentActions.OnNoteCommentActionListener;
+import org.wordpress.android.ui.notifications.NotificationFragment;
+import org.wordpress.android.ui.notifications.NotificationsDetailListFragment;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderAnim;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.suggestion.adapters.SuggestionAdapter;
+import org.wordpress.android.ui.suggestion.service.SuggestionEvents;
+import org.wordpress.android.ui.suggestion.util.SuggestionServiceConnectionManager;
+import org.wordpress.android.ui.suggestion.util.SuggestionUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.VolleyUtils;
+import org.wordpress.android.util.WPLinkMovementMethod;
+import org.wordpress.android.widgets.SuggestionAutoCompleteText;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.EnumSet;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * comment detail displayed from both the notification list and the comment list
+ * prior to this there were separate comment detail screens for each list
+ */
+public class CommentDetailFragment extends Fragment implements NotificationFragment {
+ private static final String KEY_LOCAL_BLOG_ID = "local_blog_id";
+ private static final String KEY_COMMENT_ID = "comment_id";
+ private static final String KEY_NOTE_ID = "note_id";
+ private int mLocalBlogId;
+ private int mRemoteBlogId;
+ private Comment mComment;
+ private Note mNote;
+ private SuggestionAdapter mSuggestionAdapter;
+ private SuggestionServiceConnectionManager mSuggestionServiceConnectionManager;
+ private TextView mTxtStatus;
+ private TextView mTxtContent;
+ private View mSubmitReplyBtn;
+ private SuggestionAutoCompleteText mEditReply;
+ private ViewGroup mLayoutReply;
+ private ViewGroup mLayoutButtons;
+ private View mBtnLikeComment;
+ private ImageView mBtnLikeIcon;
+ private TextView mBtnLikeTextView;
+ private View mBtnModerateComment;
+ private ImageView mBtnModerateIcon;
+ private TextView mBtnModerateTextView;
+ private TextView mBtnSpamComment;
+ private TextView mBtnTrashComment;
+ private String mRestoredReplyText;
+ private String mRestoredNoteId;
+ private boolean mIsUsersBlog = false;
+ private boolean mShouldFocusReplyField;
+ private boolean mShouldLikeInstantly;
+ private boolean mShouldApproveInstantly;
+
+ /*
+ * Used to request a comment from a note using its site and comment ids, rather than build
+ * the comment with the content in the note. See showComment()
+ */
+ private boolean mShouldRequestCommentFromNote = false;
+ private boolean mIsSubmittingReply = false;
+ private NotificationsDetailListFragment mNotificationsDetailListFragment;
+ private OnCommentChangeListener mOnCommentChangeListener;
+ private OnPostClickListener mOnPostClickListener;
+ private OnCommentActionListener mOnCommentActionListener;
+ private OnNoteCommentActionListener mOnNoteCommentActionListener;
+ /*
+ * these determine which actions (moderation, replying, marking as spam) to enable
+ * for this comment - all actions are enabled when opened from the comment list, only
+ * changed when opened from a notification
+ */
+ private EnumSet<EnabledActions> mEnabledActions = EnumSet.allOf(EnabledActions.class);
+
+ /*
+ * used when called from comment list
+ */
+ static CommentDetailFragment newInstance(int localBlogId, long commentId) {
+ CommentDetailFragment fragment = new CommentDetailFragment();
+ fragment.setComment(localBlogId, commentId);
+ return fragment;
+ }
+
+ /*
+ * used when called from notification list for a comment notification
+ */
+ public static CommentDetailFragment newInstance(final String noteId) {
+ CommentDetailFragment fragment = new CommentDetailFragment();
+ fragment.setNoteWithNoteId(noteId);
+ return fragment;
+ }
+
+ /*
+ * used when called from a comment notification 'like' action
+ */
+ public static CommentDetailFragment newInstanceForInstantLike(final String noteId) {
+ CommentDetailFragment fragment = newInstance(noteId);
+ //here tell the fragment to trigger the Like action when ready
+ fragment.setLikeCommentWhenReady();
+ return fragment;
+ }
+
+ /*
+ * used when called from a comment notification 'approve' action
+ */
+ public static CommentDetailFragment newInstanceForInstantApprove(final String noteId) {
+ CommentDetailFragment fragment = newInstance(noteId);
+ //here tell the fragment to trigger the Like action when ready
+ fragment.setApproveCommentWhenReady();
+ return fragment;
+ }
+
+ /*
+ * used when called from notifications to load a comment that doesn't already exist in the note
+ */
+ public static CommentDetailFragment newInstanceForRemoteNoteComment(final String noteId) {
+ CommentDetailFragment fragment = newInstance(noteId);
+ fragment.enableShouldRequestCommentFromNote();
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ if (savedInstanceState.getString(KEY_NOTE_ID) != null) {
+ // The note will be set in onResume() because Simperium will be running there
+ // See WordPress.deferredInit()
+ mRestoredNoteId = savedInstanceState.getString(KEY_NOTE_ID);
+ } else {
+ int localBlogId = savedInstanceState.getInt(KEY_LOCAL_BLOG_ID);
+ long commentId = savedInstanceState.getLong(KEY_COMMENT_ID);
+ setComment(localBlogId, commentId);
+ }
+ }
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (hasComment()) {
+ outState.putInt(KEY_LOCAL_BLOG_ID, getLocalBlogId());
+ outState.putLong(KEY_COMMENT_ID, getCommentId());
+ }
+
+ if (mNote != null) {
+ outState.putString(KEY_NOTE_ID, mNote.getId());
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mSuggestionServiceConnectionManager != null) {
+ mSuggestionServiceConnectionManager.unbindFromService();
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.comment_detail_fragment, container, false);
+
+ mTxtStatus = (TextView) view.findViewById(R.id.text_status);
+ mTxtContent = (TextView) view.findViewById(R.id.text_content);
+
+ mLayoutButtons = (ViewGroup) inflater.inflate(R.layout.comment_action_footer, null, false);
+ mBtnLikeComment = mLayoutButtons.findViewById(R.id.btn_like);
+ mBtnLikeIcon = (ImageView) mLayoutButtons.findViewById(R.id.btn_like_icon);
+ mBtnLikeTextView = (TextView) mLayoutButtons.findViewById(R.id.btn_like_text);
+ mBtnModerateComment = mLayoutButtons.findViewById(R.id.btn_moderate);
+ mBtnModerateIcon = (ImageView) mLayoutButtons.findViewById(R.id.btn_moderate_icon);
+ mBtnModerateTextView = (TextView) mLayoutButtons.findViewById(R.id.btn_moderate_text);
+ mBtnSpamComment = (TextView) mLayoutButtons.findViewById(R.id.text_btn_spam);
+ mBtnTrashComment = (TextView) mLayoutButtons.findViewById(R.id.image_trash_comment);
+
+ setTextDrawable(mBtnSpamComment, R.drawable.ic_action_spam);
+ setTextDrawable(mBtnTrashComment, R.drawable.ic_action_trash);
+
+ mLayoutReply = (ViewGroup) view.findViewById(R.id.layout_comment_box);
+ mEditReply = (SuggestionAutoCompleteText) mLayoutReply.findViewById(R.id.edit_comment);
+ mEditReply.getAutoSaveTextHelper().setUniqueId(String.format(LanguageUtils.getCurrentDeviceLanguage(getActivity()), "%s%d%d",
+ AccountHelper.getCurrentUsernameForBlog(WordPress.getCurrentBlog()),
+ getRemoteBlogId(), getCommentId()));
+
+ mSubmitReplyBtn = mLayoutReply.findViewById(R.id.btn_submit_reply);
+
+ View replyBox = mLayoutReply.findViewById(R.id.reply_box);
+ if (mComment != null &&
+ (mComment.getStatusEnum() == CommentStatus.SPAM ||
+ mComment.getStatusEnum() == CommentStatus.TRASH ||
+ mComment.getStatusEnum() == CommentStatus.DELETE)) {
+ replyBox.setVisibility(View.GONE);
+ } else {
+ replyBox.setVisibility(View.VISIBLE);
+ }
+
+ // hide comment like button until we know it can be enabled in showCommentForNote()
+ mBtnLikeComment.setVisibility(View.GONE);
+
+ // hide moderation buttons until updateModerationButtons() is called
+ mLayoutButtons.setVisibility(View.GONE);
+
+ // this is necessary in order for anchor tags in the comment text to be clickable
+ mTxtContent.setLinksClickable(true);
+ mTxtContent.setMovementMethod(WPLinkMovementMethod.getInstance());
+
+ mEditReply.setHint(R.string.reader_hint_comment_on_comment);
+ mEditReply.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_SEND)
+ submitReply();
+ return false;
+ }
+ });
+
+ if (!TextUtils.isEmpty(mRestoredReplyText)) {
+ mEditReply.setText(mRestoredReplyText);
+ mRestoredReplyText = null;
+ }
+
+ mSubmitReplyBtn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submitReply();
+ }
+ });
+
+ mBtnSpamComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!hasComment()) return;
+
+ if (mComment.getStatusEnum() == CommentStatus.SPAM) {
+ moderateComment(CommentStatus.APPROVED);
+ } else {
+ moderateComment(CommentStatus.SPAM);
+ }
+ }
+ });
+
+ mBtnTrashComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!hasComment()) return;
+
+ if (mComment.willTrashingPermanentlyDelete()) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
+ getActivity());
+ dialogBuilder.setTitle(getResources().getText(R.string.delete));
+ dialogBuilder.setMessage(getResources().getText(R.string.dlg_sure_to_delete_comment));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ moderateComment(CommentStatus.DELETE);
+ }
+ });
+ dialogBuilder.setNegativeButton(
+ getResources().getText(R.string.no),
+ null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+
+ } else {
+ moderateComment(CommentStatus.TRASH);
+ }
+
+ }
+ });
+
+ mBtnLikeComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ likeComment(false);
+ }
+ });
+
+ setupSuggestionServiceAndAdapter();
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.COMMENT_DETAIL);
+
+ // Set the note if we retrieved the noteId from savedInstanceState
+ if (!TextUtils.isEmpty(mRestoredNoteId)) {
+ setNoteWithNoteId(mRestoredNoteId);
+ mRestoredNoteId = null;
+ }
+
+ if (mShouldLikeInstantly) {
+ mShouldLikeInstantly = false;
+ likeComment(true);
+ } else if (mShouldApproveInstantly) {
+ mShouldApproveInstantly = false;
+ performModerateAction();
+ }
+
+ }
+
+ private void setupSuggestionServiceAndAdapter() {
+ if (!isAdded()) return;
+
+ mSuggestionServiceConnectionManager = new SuggestionServiceConnectionManager(getActivity(), mRemoteBlogId);
+ mSuggestionAdapter = SuggestionUtils.setupSuggestions(mRemoteBlogId, getActivity(), mSuggestionServiceConnectionManager);
+ if (mSuggestionAdapter != null) {
+ mEditReply.setAdapter(mSuggestionAdapter);
+ }
+ }
+
+ private void setComment(int localBlogId, long commentId) {
+ setComment(localBlogId, CommentTable.getComment(localBlogId, commentId));
+ }
+
+ private void setComment(int localBlogId, final Comment comment) {
+ mComment = comment;
+ mLocalBlogId = localBlogId;
+
+ // is this comment on one of the user's blogs? it won't be if this was displayed from a
+ // notification about a reply to a comment this user posted on someone else's blog
+ mIsUsersBlog = (comment != null && WordPress.wpDB.isLocalBlogIdInDatabase(mLocalBlogId));
+
+ if (mIsUsersBlog)
+ mRemoteBlogId = WordPress.wpDB.getRemoteBlogIdForLocalTableBlogId(mLocalBlogId);
+
+ if (isAdded())
+ showComment();
+ }
+
+ private void disableShouldFocusReplyField() {
+ mShouldFocusReplyField = false;
+ }
+
+ private void enableShouldRequestCommentFromNote() {
+ mShouldRequestCommentFromNote = true;
+ }
+
+ @Override
+ public Note getNote() {
+ return mNote;
+ }
+
+ @Override
+ public void setNote(Note note) {
+ mNote = note;
+ if (isAdded() && mNote != null) {
+ showComment();
+ }
+ }
+
+ private void setNoteWithNoteId(String noteId) {
+ if (noteId == null) return;
+
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(noteId);
+ setNote(note);
+ setRemoteBlogId(note.getSiteId());
+ } catch (BucketObjectMissingException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation") // TODO: Remove when minSdkVersion >= 23
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (activity instanceof OnCommentChangeListener)
+ mOnCommentChangeListener = (OnCommentChangeListener) activity;
+ if (activity instanceof OnPostClickListener)
+ mOnPostClickListener = (OnPostClickListener) activity;
+ if (activity instanceof OnCommentActionListener)
+ mOnCommentActionListener = (OnCommentActionListener) activity;
+ if (activity instanceof OnNoteCommentActionListener)
+ mOnNoteCommentActionListener = (OnNoteCommentActionListener) activity;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ showComment();
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(SuggestionEvents.SuggestionNameListUpdated event) {
+ // check if the updated suggestions are for the current blog and update the suggestions
+ if (event.mRemoteBlogId != 0 && event.mRemoteBlogId == mRemoteBlogId && mSuggestionAdapter != null) {
+ List<Suggestion> suggestions = SuggestionTable.getSuggestionsForSite(event.mRemoteBlogId);
+ mSuggestionAdapter.setSuggestionList(suggestions);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Reset comment if this is from a notification
+ if (mNote != null) {
+ mComment = null;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == Constants.INTENT_COMMENT_EDITOR && resultCode == Activity.RESULT_OK) {
+ if (mNote == null) {
+ reloadComment();
+ }
+ // tell the host to reload the comment list
+ if (mOnCommentChangeListener != null)
+ mOnCommentChangeListener.onCommentChanged(ChangeType.EDITED);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ menu.clear();
+ inflater.inflate(R.menu.comment_detail, menu);
+ if (!canEdit()) {
+ menu.removeItem(R.id.menu_edit_comment);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == R.id.menu_edit_comment) {
+ editComment();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private boolean hasComment() {
+ return (mComment != null);
+ }
+
+ private long getCommentId() {
+ return (mComment != null ? mComment.commentID : 0);
+ }
+
+ private int getLocalBlogId() {
+ return mLocalBlogId;
+ }
+
+ private int getRemoteBlogId() {
+ return mRemoteBlogId;
+ }
+
+ private void setRemoteBlogId(int remoteBlogId) {
+ mRemoteBlogId = remoteBlogId;
+ }
+
+ /*
+ * reload the current comment from the local database
+ */
+ private void reloadComment() {
+ if (!hasComment())
+ return;
+ Comment updatedComment = CommentTable.getComment(mLocalBlogId, getCommentId());
+ setComment(mLocalBlogId, updatedComment);
+ }
+
+ /*
+ * open the comment for editing
+ */
+ private void editComment() {
+ if (!isAdded() || !hasComment())
+ return;
+ // IMPORTANT: don't use getActivity().startActivityForResult() or else onActivityResult()
+ // won't be called in this fragment
+ // https://code.google.com/p/android/issues/detail?id=15394#c45
+ Intent intent = new Intent(getActivity(), EditCommentActivity.class);
+ intent.putExtra(EditCommentActivity.ARG_LOCAL_BLOG_ID, getLocalBlogId());
+ intent.putExtra(EditCommentActivity.ARG_COMMENT_ID, getCommentId());
+ if (mNote != null) {
+ intent.putExtra(EditCommentActivity.ARG_NOTE_ID, mNote.getId());
+ }
+ startActivityForResult(intent, Constants.INTENT_COMMENT_EDITOR);
+ }
+
+ /*
+ * display the current comment
+ */
+ private void showComment() {
+ if (!isAdded() || getView() == null)
+ return;
+
+ // these two views contain all the other views except the progress bar
+ final ScrollView scrollView = (ScrollView) getView().findViewById(R.id.scroll_view);
+ final View layoutBottom = getView().findViewById(R.id.layout_bottom);
+
+ // hide container views when comment is null (will happen when opened from a notification)
+ if (mComment == null) {
+ scrollView.setVisibility(View.GONE);
+ layoutBottom.setVisibility(View.GONE);
+
+ if (mNote != null && mShouldRequestCommentFromNote) {
+ // If a remote comment was requested, check if we have the comment for display.
+ // Otherwise request the comment via the REST API
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(mNote.getSiteId());
+ if (localTableBlogId > 0) {
+ Comment comment = CommentTable.getComment(localTableBlogId, mNote.getParentCommentId());
+ if (comment != null) {
+ setComment(localTableBlogId, comment);
+ return;
+ }
+ }
+
+ long commentId = mNote.getParentCommentId() > 0 ? mNote.getParentCommentId() : mNote.getCommentId();
+ requestComment(localTableBlogId, mNote.getSiteId(), commentId);
+ } else if (mNote != null) {
+ showCommentForNote(mNote);
+ }
+
+ return;
+ }
+
+ scrollView.setVisibility(View.VISIBLE);
+ layoutBottom.setVisibility(View.VISIBLE);
+
+ // Add action buttons footer
+ if ((mNote == null || mShouldRequestCommentFromNote) && mLayoutButtons.getParent() == null) {
+ ViewGroup commentContentLayout = (ViewGroup) getView().findViewById(R.id.comment_content_container);
+ commentContentLayout.addView(mLayoutButtons);
+ }
+
+ final WPNetworkImageView imgAvatar = (WPNetworkImageView) getView().findViewById(R.id.image_avatar);
+ final TextView txtName = (TextView) getView().findViewById(R.id.text_name);
+ final TextView txtDate = (TextView) getView().findViewById(R.id.text_date);
+
+ txtName.setText(mComment.hasAuthorName() ? HtmlUtils.fastUnescapeHtml(mComment.getAuthorName()) : getString(R.string.anonymous));
+ txtDate.setText(DateTimeUtils.javaDateToTimeSpan(mComment.getDatePublished(), WordPress.getContext()));
+
+ int maxImageSz = getResources().getDimensionPixelSize(R.dimen.reader_comment_max_image_size);
+ CommentUtils.displayHtmlComment(mTxtContent, mComment.getCommentText(), maxImageSz);
+
+ int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_large);
+ if (mComment.hasProfileImageUrl()) {
+ imgAvatar.setImageUrl(GravatarUtils.fixGravatarUrl(mComment.getProfileImageUrl(), avatarSz), WPNetworkImageView.ImageType.AVATAR);
+ } else if (mComment.hasAuthorEmail()) {
+ String avatarUrl = GravatarUtils.gravatarFromEmail(mComment.getAuthorEmail(), avatarSz);
+ imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ imgAvatar.setImageUrl(null, WPNetworkImageView.ImageType.AVATAR);
+ }
+
+ updateStatusViews();
+
+ // navigate to author's blog when avatar or name clicked
+ if (mComment.hasAuthorUrl()) {
+ View.OnClickListener authorListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderActivityLauncher.openUrl(getActivity(), mComment.getAuthorUrl());
+ }
+ };
+ imgAvatar.setOnClickListener(authorListener);
+ txtName.setOnClickListener(authorListener);
+ txtName.setTextColor(ContextCompat.getColor(getActivity(), R.color.reader_hyperlink));
+ } else {
+ txtName.setTextColor(ContextCompat.getColor(getActivity(), R.color.grey_darken_30));
+ }
+
+ showPostTitle(getRemoteBlogId(), mComment.postID);
+
+ // make sure reply box is showing
+ if (mLayoutReply.getVisibility() != View.VISIBLE && canReply()) {
+ AniUtils.animateBottomBar(mLayoutReply, true);
+ if (mEditReply != null && mShouldFocusReplyField) {
+ mEditReply.requestFocus();
+ disableShouldFocusReplyField();
+ }
+ }
+
+ getFragmentManager().invalidateOptionsMenu();
+ }
+
+ /*
+ * displays the passed post title for the current comment, updates stored title if one doesn't exist
+ */
+ private void setPostTitle(TextView txtTitle, String postTitle, boolean isHyperlink) {
+ if (txtTitle == null || !isAdded())
+ return;
+ if (TextUtils.isEmpty(postTitle)) {
+ txtTitle.setText(R.string.untitled);
+ return;
+ }
+
+ // if comment doesn't have a post title, set it to the passed one and save to comment table
+ if (hasComment() && !mComment.hasPostTitle()) {
+ mComment.setPostTitle(postTitle);
+ CommentTable.updateCommentPostTitle(getLocalBlogId(), getCommentId(), postTitle);
+ }
+
+ // display "on [Post Title]..."
+ if (isHyperlink) {
+ String html = getString(R.string.on)
+ + " <font color=" + HtmlUtils.colorResToHtmlColor(getActivity(), R.color.reader_hyperlink) + ">"
+ + postTitle.trim()
+ + "</font>";
+ txtTitle.setText(Html.fromHtml(html));
+ } else {
+ String text = getString(R.string.on) + " " + postTitle.trim();
+ txtTitle.setText(text);
+ }
+ }
+
+ /*
+ * ensure the post associated with this comment is available to the reader and show its
+ * title above the comment
+ */
+ private void showPostTitle(final int blogId, final long postId) {
+ if (!isAdded())
+ return;
+
+ final TextView txtPostTitle = (TextView) getView().findViewById(R.id.text_post_title);
+ boolean postExists = ReaderPostTable.postExists(blogId, postId);
+
+ // the post this comment is on can only be requested if this is a .com blog or a
+ // jetpack-enabled self-hosted blog, and we have valid .com credentials
+ boolean isDotComOrJetpack = WordPress.wpDB.isRemoteBlogIdDotComOrJetpack(mRemoteBlogId);
+ boolean canRequestPost = isDotComOrJetpack && AccountHelper.isSignedInWordPressDotCom();
+
+ final String title;
+ final boolean hasTitle;
+ if (mComment.hasPostTitle()) {
+ // use comment's stored post title if available
+ title = mComment.getPostTitle();
+ hasTitle = true;
+ } else if (postExists) {
+ // use title from post if available
+ title = ReaderPostTable.getPostTitle(blogId, postId);
+ hasTitle = !TextUtils.isEmpty(title);
+ } else {
+ title = null;
+ hasTitle = false;
+ }
+ if (hasTitle) {
+ setPostTitle(txtPostTitle, title, canRequestPost);
+ } else if (canRequestPost) {
+ txtPostTitle.setText(postExists ? R.string.untitled : R.string.loading);
+ }
+
+ // if this is a .com or jetpack blog, tapping the title shows the associated post
+ // in the reader
+ if (canRequestPost) {
+ // first make sure this post is available to the reader, and once it's retrieved set
+ // the title if it wasn't set above
+ if (!postExists) {
+ AppLog.d(T.COMMENTS, "comment detail > retrieving post");
+ ReaderPostActions.requestPost(blogId, postId, new ReaderActions.OnRequestListener() {
+ @Override
+ public void onSuccess() {
+ if (!isAdded()) return;
+
+ // update title if it wasn't set above
+ if (!hasTitle) {
+ String postTitle = ReaderPostTable.getPostTitle(blogId, postId);
+ if (!TextUtils.isEmpty(postTitle)) {
+ setPostTitle(txtPostTitle, postTitle, true);
+ } else {
+ txtPostTitle.setText(R.string.untitled);
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(int statusCode) {
+ }
+ });
+ }
+
+ txtPostTitle.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnPostClickListener != null) {
+ mOnPostClickListener.onPostClicked(getNote(), mRemoteBlogId, (int) mComment.postID);
+ } else {
+ // right now this will happen from notifications
+ AppLog.i(T.COMMENTS, "comment detail > no post click listener");
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), mRemoteBlogId, mComment.postID);
+ }
+ }
+ });
+ }
+ }
+
+ private void trackModerationFromNotification(final CommentStatus newStatus) {
+ switch (newStatus) {
+ case APPROVED:
+ AnalyticsTracker.track(Stat.NOTIFICATION_APPROVED);
+ break;
+ case UNAPPROVED:
+ AnalyticsTracker.track(Stat.NOTIFICATION_UNAPPROVED);
+ break;
+ case SPAM:
+ AnalyticsTracker.track(Stat.NOTIFICATION_FLAGGED_AS_SPAM);
+ break;
+ case TRASH:
+ AnalyticsTracker.track(Stat.NOTIFICATION_TRASHED);
+ break;
+ }
+ }
+
+ /*
+ * approve, disapprove, spam, or trash the current comment
+ */
+ private void moderateComment(final CommentStatus newStatus) {
+ if (!isAdded() || !hasComment())
+ return;
+ if (!NetworkUtils.checkConnection(getActivity()))
+ return;
+
+ // Fire the appropriate listener if we have one
+ if (mNote != null && mOnNoteCommentActionListener != null) {
+ mOnNoteCommentActionListener.onModerateCommentForNote(mNote, newStatus);
+ trackModerationFromNotification(newStatus);
+ return;
+ } else if (mOnCommentActionListener != null) {
+ mOnCommentActionListener.onModerateComment(mLocalBlogId, mComment, newStatus);
+ return;
+ }
+
+ if (mNote == null) return;
+
+ // Basic moderation support, currently only used when this Fragment is in a CommentDetailActivity
+ // Uses WP.com REST API and requires a note object
+ final CommentStatus oldStatus = mComment.getStatusEnum();
+ mComment.setStatus(CommentStatus.toString(newStatus));
+ updateStatusViews();
+ CommentActions.moderateCommentRestApi(mNote.getSiteId(), mComment.commentID, newStatus, new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ if (!isAdded()) return;
+
+ if (result.isSuccess()) {
+ ToastUtils.showToast(getActivity(), R.string.comment_moderated_approved, ToastUtils.Duration.SHORT);
+ } else {
+ mComment.setStatus(CommentStatus.toString(oldStatus));
+ updateStatusViews();
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment);
+ }
+ }
+ });
+ }
+
+ /*
+ * post comment box text as a reply to the current comment
+ */
+ private void submitReply() {
+ if (!hasComment() || !isAdded() || mIsSubmittingReply)
+ return;
+
+ if (!NetworkUtils.checkConnection(getActivity()))
+ return;
+
+ final String replyText = EditTextUtils.getText(mEditReply);
+ if (TextUtils.isEmpty(replyText))
+ return;
+
+ // disable editor, hide soft keyboard, hide submit icon, and show progress spinner while submitting
+ mEditReply.setEnabled(false);
+ EditTextUtils.hideSoftInput(mEditReply);
+ mSubmitReplyBtn.setVisibility(View.GONE);
+ final ProgressBar progress = (ProgressBar) getView().findViewById(R.id.progress_submit_comment);
+ progress.setVisibility(View.VISIBLE);
+
+ CommentActions.CommentActionListener actionListener = new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ mIsSubmittingReply = false;
+ if (result.isSuccess() && mOnCommentChangeListener != null)
+ mOnCommentChangeListener.onCommentChanged(ChangeType.REPLIED);
+ if (isAdded()) {
+ mEditReply.setEnabled(true);
+ mSubmitReplyBtn.setVisibility(View.VISIBLE);
+ progress.setVisibility(View.GONE);
+ updateStatusViews();
+ if (result.isSuccess()) {
+ ToastUtils.showToast(getActivity(), getString(R.string.note_reply_successful));
+ mEditReply.setText(null);
+ mEditReply.getAutoSaveTextHelper().clearSavedText(mEditReply);
+
+ // approve the comment
+ if (mComment != null && mComment.getStatusEnum() != CommentStatus.APPROVED) {
+ moderateComment(CommentStatus.APPROVED);
+ }
+ } else {
+ String errorMessage = TextUtils.isEmpty(result.getMessage()) ? getString(R.string.reply_failed) : result.getMessage();
+ String strUnEscapeHTML = StringEscapeUtils.unescapeHtml(errorMessage);
+ ToastUtils.showToast(getActivity(), strUnEscapeHTML, ToastUtils.Duration.LONG);
+ // refocus editor on failure and show soft keyboard
+ EditTextUtils.showSoftInput(mEditReply);
+ }
+ }
+ }
+ };
+
+ mIsSubmittingReply = true;
+
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_REPLIED_TO);
+ if (mNote != null) {
+ if (mShouldRequestCommentFromNote) {
+ CommentActions.submitReplyToCommentRestApi(mNote.getSiteId(), mComment.commentID, replyText, actionListener);
+ } else {
+ CommentActions.submitReplyToCommentNote(mNote, replyText, actionListener);
+ }
+ } else {
+ CommentActions.submitReplyToComment(mLocalBlogId, mComment, replyText, actionListener);
+ }
+ }
+
+ /*
+ * sets the drawable for moderation buttons
+ */
+ private void setTextDrawable(final TextView view, int resId) {
+ view.setCompoundDrawablesWithIntrinsicBounds(null, ContextCompat.getDrawable(getActivity(), resId), null, null);
+ }
+
+ /*
+ * update the text, drawable & click listener for mBtnModerate based on
+ * the current status of the comment, show mBtnSpam if the comment isn't
+ * already marked as spam, and show the current status of the comment
+ */
+ private void updateStatusViews() {
+ if (!isAdded() || !hasComment())
+ return;
+
+ final int statusTextResId; // string resource id for status text
+ final int statusColor; // color for status text
+
+ switch (mComment.getStatusEnum()) {
+ case APPROVED:
+ statusTextResId = R.string.comment_status_approved;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.notification_status_unapproved_dark);
+ break;
+ case UNAPPROVED:
+ statusTextResId = R.string.comment_status_unapproved;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.notification_status_unapproved_dark);
+ break;
+ case SPAM:
+ statusTextResId = R.string.comment_status_spam;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.comment_status_spam);
+ break;
+ case TRASH:
+ default:
+ statusTextResId = R.string.comment_status_trash;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.comment_status_spam);
+ break;
+ }
+
+ if (mNote != null && canLike()) {
+ mBtnLikeComment.setVisibility(View.VISIBLE);
+
+ toggleLikeButton(mNote.hasLikedComment());
+ } else {
+ mBtnLikeComment.setVisibility(View.GONE);
+ }
+
+ // comment status is only shown if this comment is from one of this user's blogs and the
+ // comment hasn't been approved
+ if (mIsUsersBlog && mComment.getStatusEnum() != CommentStatus.APPROVED) {
+ mTxtStatus.setText(getString(statusTextResId).toUpperCase());
+ mTxtStatus.setTextColor(statusColor);
+ if (mTxtStatus.getVisibility() != View.VISIBLE) {
+ mTxtStatus.clearAnimation();
+ AniUtils.fadeIn(mTxtStatus, AniUtils.Duration.LONG);
+ }
+ } else {
+ mTxtStatus.setVisibility(View.GONE);
+ }
+
+ if (canModerate()) {
+ setModerateButtonForStatus(mComment.getStatusEnum());
+ mBtnModerateComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performModerateAction();
+ }
+ });
+ mBtnModerateComment.setVisibility(View.VISIBLE);
+ } else {
+ mBtnModerateComment.setVisibility(View.GONE);
+ }
+
+ if (canMarkAsSpam()) {
+ mBtnSpamComment.setVisibility(View.VISIBLE);
+ if (mComment.getStatusEnum() == CommentStatus.SPAM) {
+ mBtnSpamComment.setText(R.string.mnu_comment_unspam);
+ } else {
+ mBtnSpamComment.setText(R.string.mnu_comment_spam);
+ }
+ } else {
+ mBtnSpamComment.setVisibility(View.GONE);
+ }
+
+ if (canTrash()) {
+ mBtnTrashComment.setVisibility(View.VISIBLE);
+ if (mComment.getStatusEnum() == CommentStatus.TRASH) {
+ mBtnModerateIcon.setImageResource(R.drawable.ic_action_restore);
+ //mBtnModerateTextView.setTextColor(getActivity().getResources().getColor(R.color.notification_status_unapproved_dark));
+ mBtnModerateTextView.setText(R.string.mnu_comment_untrash);
+ mBtnTrashComment.setText(R.string.mnu_comment_delete_permanently);
+ } else {
+ mBtnTrashComment.setText(R.string.mnu_comment_trash);
+ }
+ } else {
+ mBtnTrashComment.setVisibility(View.GONE);
+ }
+
+ mLayoutButtons.setVisibility(View.VISIBLE);
+ }
+
+ private void performModerateAction(){
+ if (!hasComment() || !isAdded() || !NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ CommentStatus newStatus = CommentStatus.APPROVED;
+ if (mComment.getStatusEnum() == CommentStatus.APPROVED) {
+ newStatus = CommentStatus.UNAPPROVED;
+ }
+
+ mComment.setStatus(newStatus.toString());
+ setModerateButtonForStatus(newStatus);
+ AniUtils.startAnimation(mBtnModerateIcon, R.anim.notifications_button_scale);
+ moderateComment(newStatus);
+ }
+
+ private void setModerateButtonForStatus(CommentStatus status) {
+ if (status == CommentStatus.APPROVED) {
+ mBtnModerateIcon.setImageResource(R.drawable.ic_action_approve_active);
+ mBtnModerateTextView.setText(R.string.comment_status_approved);
+ mBtnModerateTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.notification_status_unapproved_dark));
+ } else {
+ mBtnModerateIcon.setImageResource(R.drawable.ic_action_approve);
+ mBtnModerateTextView.setText(R.string.mnu_comment_approve);
+ mBtnModerateTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.grey));
+ }
+ }
+
+ /*
+ * does user have permission to moderate/reply/spam this comment?
+ */
+ private boolean canModerate() {
+ return mEnabledActions != null && (mEnabledActions.contains(EnabledActions.ACTION_APPROVE) || mEnabledActions.contains(EnabledActions.ACTION_UNAPPROVE));
+ }
+
+ private boolean canMarkAsSpam() {
+ return (mEnabledActions != null && mEnabledActions.contains(EnabledActions.ACTION_SPAM));
+ }
+
+ private boolean canReply() {
+ return (mEnabledActions != null && mEnabledActions.contains(EnabledActions.ACTION_REPLY));
+ }
+
+ private boolean canTrash() {
+ return canModerate();
+ }
+
+ private boolean canEdit() {
+ return (mLocalBlogId > 0 && canModerate());
+ }
+
+ private boolean canLike() {
+ return (!mShouldRequestCommentFromNote && mEnabledActions != null && mEnabledActions.contains(EnabledActions.ACTION_LIKE));
+ }
+
+ /*
+ * display the comment associated with the passed notification
+ */
+ private void showCommentForNote(Note note) {
+ if (getView() == null) return;
+ View view = getView();
+
+ // hide standard comment views, since we'll be adding note blocks instead
+ View commentContent = view.findViewById(R.id.comment_content);
+ if (commentContent != null) {
+ commentContent.setVisibility(View.GONE);
+ }
+
+ View commentText = view.findViewById(R.id.text_content);
+ if (commentText != null) {
+ commentText.setVisibility(View.GONE);
+ }
+
+ // Now we'll add a detail fragment list
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ mNotificationsDetailListFragment = NotificationsDetailListFragment.newInstance(note.getId());
+ mNotificationsDetailListFragment.setFooterView(mLayoutButtons);
+ // Listen for note changes from the detail list fragment, so we can update the status buttons
+ mNotificationsDetailListFragment.setOnNoteChangeListener(new NotificationsDetailListFragment.OnNoteChangeListener() {
+ @Override
+ public void onNoteChanged(Note note) {
+ mNote = note;
+ mComment = mNote.buildComment();
+ updateStatusViews();
+ }
+ });
+ fragmentTransaction.replace(R.id.comment_content_container, mNotificationsDetailListFragment);
+ fragmentTransaction.commitAllowingStateLoss();
+
+ /*
+ * determine which actions to enable for this comment - if the comment is from this user's
+ * blog then all actions will be enabled, but they won't be if it's a reply to a comment
+ * this user made on someone else's blog
+ */
+ mEnabledActions = note.getEnabledActions();
+
+ // Set 'Reply to (Name)' in comment reply EditText if it's a reasonable size
+ if (!TextUtils.isEmpty(mNote.getCommentAuthorName()) && mNote.getCommentAuthorName().length() < 28) {
+ mEditReply.setHint(String.format(getString(R.string.comment_reply_to_user), mNote.getCommentAuthorName()));
+ }
+
+ // note that the local blog id won't be found if the comment is from someone else's blog
+ int localBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(mRemoteBlogId);
+
+ setComment(localBlogId, note.buildComment());
+
+ getFragmentManager().invalidateOptionsMenu();
+ }
+
+ private void setLikeCommentWhenReady() {
+ mShouldLikeInstantly = true;
+ }
+
+ private void setApproveCommentWhenReady() {
+ mShouldApproveInstantly = true;
+ }
+
+ // Like or unlike a comment via the REST API
+ private void likeComment(boolean forceLike) {
+ if (mNote == null) return;
+ if (!isAdded()) return;
+ if (forceLike && mBtnLikeComment.isActivated()) return;
+
+ toggleLikeButton(!mBtnLikeComment.isActivated());
+
+ ReaderAnim.animateLikeButton(mBtnLikeIcon, mBtnLikeComment.isActivated());
+
+ // Bump analytics
+ AnalyticsTracker.track(mBtnLikeComment.isActivated() ? Stat.NOTIFICATION_LIKED : Stat.NOTIFICATION_UNLIKED);
+
+ boolean commentWasUnapproved = false;
+ if (mNotificationsDetailListFragment != null && mComment != null) {
+ // Optimistically set comment to approved when liking an unapproved comment
+ // WP.com will set a comment to approved if it is liked while unapproved
+ if (mBtnLikeComment.isActivated() && mComment.getStatusEnum() == CommentStatus.UNAPPROVED) {
+ mComment.setStatus(CommentStatus.toString(CommentStatus.APPROVED));
+ mNotificationsDetailListFragment.refreshBlocksForCommentStatus(CommentStatus.APPROVED);
+ setModerateButtonForStatus(CommentStatus.APPROVED);
+ commentWasUnapproved = true;
+ }
+ }
+
+ final boolean commentStatusShouldRevert = commentWasUnapproved;
+ WordPress.getRestClientUtils().likeComment(String.valueOf(mNote.getSiteId()),
+ String.valueOf(mNote.getCommentId()),
+ mBtnLikeComment.isActivated(),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (response != null && !response.optBoolean("success")) {
+ if (!isAdded()) return;
+
+ // Failed, so switch the button state back
+ toggleLikeButton(!mBtnLikeComment.isActivated());
+
+ if (commentStatusShouldRevert) {
+ setCommentStatusUnapproved();
+ }
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (!isAdded()) return;
+
+ toggleLikeButton(!mBtnLikeComment.isActivated());
+
+ if (commentStatusShouldRevert) {
+ setCommentStatusUnapproved();
+ }
+ }
+ });
+ }
+
+ private void setCommentStatusUnapproved() {
+ mComment.setStatus(CommentStatus.toString(CommentStatus.UNAPPROVED));
+ mNotificationsDetailListFragment.refreshBlocksForCommentStatus(CommentStatus.UNAPPROVED);
+ setModerateButtonForStatus(CommentStatus.UNAPPROVED);
+ }
+
+ private void toggleLikeButton(boolean isLiked) {
+ if (isLiked) {
+ mBtnLikeTextView.setText(getResources().getString(R.string.mnu_comment_liked));
+ mBtnLikeTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.orange_jazzy));
+ mBtnLikeIcon.setImageDrawable(ContextCompat.getDrawable(getActivity(), R.drawable.ic_action_like_active));
+ mBtnLikeComment.setActivated(true);
+ } else {
+ mBtnLikeTextView.setText(getResources().getString(R.string.reader_label_like));
+ mBtnLikeTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.grey));
+ mBtnLikeIcon.setImageDrawable(ContextCompat.getDrawable(getActivity(), R.drawable.ic_action_like));
+ mBtnLikeComment.setActivated(false);
+ }
+ }
+
+ /*
+ * request a comment - note that this uses the REST API rather than XMLRPC, which means the user must
+ * either be wp.com or have Jetpack, but it's safe to do this since this method is only called when
+ * displayed from a notification (and notifications require wp.com/Jetpack)
+ */
+ private void requestComment(final int localBlogId,
+ final int remoteBlogId,
+ final long commentId) {
+
+ final ProgressBar progress = (isAdded() && getView() != null ?
+ (ProgressBar) getView().findViewById(R.id.progress_loading) : null);
+ if (progress != null) {
+ progress.setVisibility(View.VISIBLE);
+ }
+
+ RestRequest.Listener restListener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (isAdded()) {
+ if (progress != null) {
+ progress.setVisibility(View.GONE);
+ }
+ Comment comment = Comment.fromJSON(jsonObject);
+ if (comment != null) {
+ // save comment to local db if localBlogId is valid
+ if (localBlogId > 0) {
+ CommentTable.addComment(localBlogId, comment);
+ }
+ // now, at long last, show the comment
+ setComment(localBlogId, comment);
+ }
+ }
+ }
+ };
+ RestRequest.ErrorListener restErrListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.COMMENTS, VolleyUtils.errStringFromVolleyError(volleyError), volleyError);
+ if (isAdded()) {
+ if (progress != null) {
+ progress.setVisibility(View.GONE);
+ }
+ ToastUtils.showToast(getActivity(), R.string.reader_toast_err_get_comment, ToastUtils.Duration.LONG);
+ }
+ }
+ };
+
+ final String path = String.format("/sites/%s/comments/%s", remoteBlogId, commentId);
+ WordPress.getRestClientUtils().get(path, restListener, restErrListener);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java
new file mode 100644
index 000000000..b2ec720ed
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+
+import org.wordpress.android.R;
+
+/**
+ * Dialogs related to comment moderation displayed from CommentsActivity and NotificationsActivity
+ */
+class CommentDialogs {
+ public static final int ID_COMMENT_DLG_APPROVING = 100;
+ public static final int ID_COMMENT_DLG_DISAPPROVING = 101;
+ public static final int ID_COMMENT_DLG_SPAMMING = 102;
+ public static final int ID_COMMENT_DLG_TRASHING = 103;
+ public static final int ID_COMMENT_DLG_DELETING = 104;
+
+ private CommentDialogs() {
+ throw new AssertionError();
+ }
+
+ public static Dialog createCommentDialog(Activity activity, int dialogId) {
+ final int resId;
+ switch (dialogId) {
+ case ID_COMMENT_DLG_APPROVING :
+ resId = R.string.dlg_approving_comments;
+ break;
+ case ID_COMMENT_DLG_DISAPPROVING:
+ resId = R.string.dlg_unapproving_comments;
+ break;
+ case ID_COMMENT_DLG_TRASHING:
+ resId = R.string.dlg_trashing_comments;
+ break;
+ case ID_COMMENT_DLG_SPAMMING:
+ resId = R.string.dlg_spamming_comments;
+ break;
+ case ID_COMMENT_DLG_DELETING:
+ resId = R.string.dlg_deleting_comments;
+ break;
+ default :
+ return null;
+ }
+
+ ProgressDialog dialog = new ProgressDialog(activity);
+ dialog.setMessage(activity.getString(resId));
+ dialog.setIndeterminate(true);
+ dialog.setCancelable(false);
+
+ return dialog;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java
new file mode 100644
index 000000000..88a82fd7f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java
@@ -0,0 +1,56 @@
+package org.wordpress.android.ui.comments;
+
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+
+class CommentEvents {
+
+ public static class CommentsBatchModerationFinishedEvent {
+ private final CommentList mComments;
+ private final boolean mIsDeleted;
+
+ public CommentsBatchModerationFinishedEvent(CommentList comments, boolean isDeleted) {
+ mComments = comments;
+ mIsDeleted = isDeleted;
+ }
+
+ public CommentList getComments() {
+ return mComments;
+ }
+
+ public boolean isDeleted() {
+ return mIsDeleted;
+ }
+
+ }
+
+ public static class CommentModerationFinishedEvent {
+ private final boolean mIsSuccess;
+ private final boolean mIsCommentsRefreshRequired;
+ private final long mCommentId;
+ private final CommentStatus mNewStatus;
+
+ public CommentModerationFinishedEvent(boolean isSuccess, boolean isCommentsRefreshRequired, long commentId, CommentStatus newStatus) {
+ mIsSuccess = isSuccess;
+ mIsCommentsRefreshRequired = isCommentsRefreshRequired;
+ mCommentId = commentId;
+ mNewStatus = newStatus;
+ }
+
+ public boolean isSuccess() {
+ return mIsSuccess;
+ }
+
+ public boolean isCommentsRefreshRequired() {
+ return mIsCommentsRefreshRequired;
+ }
+
+ public long getCommentId() {
+ return mCommentId;
+ }
+
+ public CommentStatus getNewStatus() {
+ return mNewStatus;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java
new file mode 100644
index 000000000..d21d5231f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.ui.comments;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.support.v4.content.ContextCompat;
+import android.text.Layout;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.LeadingMarginSpan;
+import android.text.util.Linkify;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.EmoticonsUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.helpers.WPImageGetter;
+
+public class CommentUtils {
+ /*
+ * displays comment text as html, including retrieving images
+ */
+ public static void displayHtmlComment(TextView textView, String content, int maxImageSize) {
+ if (textView == null) {
+ return;
+ }
+
+ if (content == null) {
+ textView.setText(null);
+ return;
+ }
+
+ // skip performance hit of html conversion if content doesn't contain html
+ if (!content.contains("<") && !content.contains("&")) {
+ content = content.trim();
+ textView.setText(content);
+ // make sure unnamed links are clickable
+ if (content.contains("://")) {
+ Linkify.addLinks(textView, Linkify.WEB_URLS);
+ }
+ return;
+ }
+
+ // convert emoticons first (otherwise they'll be downloaded)
+ content = EmoticonsUtils.replaceEmoticonsWithEmoji(content);
+
+ // now convert to HTML with an image getter that enforces a max image size
+ final Spanned html;
+ if (maxImageSize > 0 && content.contains("<img")) {
+ Drawable loading = ContextCompat.getDrawable(textView.getContext(),
+ R.drawable.legacy_dashicon_format_image_big_grey);
+ Drawable failed = ContextCompat.getDrawable(textView.getContext(),
+ R.drawable.noticon_warning_big_grey);
+ html = HtmlUtils.fromHtml(content, new WPImageGetter(textView, maxImageSize, WordPress.imageLoader, loading,
+ failed));
+ } else {
+ html = HtmlUtils.fromHtml(content);
+ }
+
+ // remove extra \n\n added by Html.convert()
+ int start = 0;
+ int end = html.length();
+ while (start < end && Character.isWhitespace(html.charAt(start))) {
+ start++;
+ }
+ while (end > start && Character.isWhitespace(html.charAt(end - 1))) {
+ end--;
+ }
+
+ textView.setText(html.subSequence(start, end));
+ }
+
+ // Assumes all lines after first line will not be indented
+ public static void indentTextViewFirstLine(TextView textView, int textOffsetX) {
+ if (textView == null || textOffsetX < 0) return;
+
+ SpannableString text = new SpannableString(textView.getText());
+ text.setSpan(new TextWrappingLeadingMarginSpan(textOffsetX), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ textView.setText(text);
+ }
+
+ private static class TextWrappingLeadingMarginSpan implements LeadingMarginSpan.LeadingMarginSpan2 {
+ private final int margin;
+ private final int lines;
+
+ public TextWrappingLeadingMarginSpan(int margin) {
+ this.margin = margin;
+ this.lines = 1;
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return first ? margin : 0;
+ }
+
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+
+ }
+
+ @Override
+ public int getLeadingMarginLineCount() {
+ return lines;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java
new file mode 100644
index 000000000..e82645fe6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java
@@ -0,0 +1,314 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.comments.CommentsListFragment.OnCommentSelectedListener;
+import org.wordpress.android.ui.notifications.NotificationFragment;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.reader.ReaderPostDetailFragment;
+import org.wordpress.android.util.AppLog;
+
+import de.greenrobot.event.EventBus;
+
+public class CommentsActivity extends AppCompatActivity
+ implements OnCommentSelectedListener,
+ NotificationFragment.OnPostClickListener,
+ CommentActions.OnCommentActionListener,
+ CommentActions.OnCommentChangeListener {
+ private static final String KEY_SELECTED_COMMENT_ID = "selected_comment_id";
+ static final String KEY_AUTO_REFRESHED = "has_auto_refreshed";
+ static final String KEY_EMPTY_VIEW_MESSAGE = "empty_view_message";
+ private static final String SAVED_COMMENTS_STATUS_TYPE = "saved_comments_status_type";
+ private long mSelectedCommentId;
+ private final CommentList mTrashedComments = new CommentList();
+
+ private CommentStatus mCurrentCommentStatusType = CommentStatus.UNKNOWN;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.comment_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setElevation(0);
+ actionBar.setTitle(R.string.comments);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (getIntent() != null && getIntent().hasExtra(SAVED_COMMENTS_STATUS_TYPE)) {
+ mCurrentCommentStatusType = (CommentStatus) getIntent().getSerializableExtra(SAVED_COMMENTS_STATUS_TYPE);
+ } else {
+ // Read the value from app preferences here. Default to 0 - All
+ mCurrentCommentStatusType = AppPrefs.getCommentsStatusFilter();
+ }
+
+ if (savedInstanceState == null) {
+ CommentsListFragment commentsListFragment = new CommentsListFragment();
+ // initialize comment status filter first time
+ commentsListFragment.setCommentStatusFilter(mCurrentCommentStatusType);
+ getFragmentManager().beginTransaction()
+ .add(R.id.layout_fragment_container, commentsListFragment, getString(R.string
+ .fragment_tag_comment_list))
+ .commitAllowingStateLoss();
+ } else {
+ getIntent().putExtra(KEY_AUTO_REFRESHED, savedInstanceState.getBoolean(KEY_AUTO_REFRESHED));
+ getIntent().putExtra(KEY_EMPTY_VIEW_MESSAGE, savedInstanceState.getString(KEY_EMPTY_VIEW_MESSAGE));
+
+ mSelectedCommentId = savedInstanceState.getLong(KEY_SELECTED_COMMENT_ID);
+ }
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.COMMENTS);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (getFragmentManager().getBackStackEntryCount() > 0) {
+ getFragmentManager().popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ AppLog.d(AppLog.T.COMMENTS, "comment activity new intent");
+ }
+
+
+ private CommentDetailFragment getDetailFragment() {
+ Fragment fragment = getFragmentManager().findFragmentByTag(getString(
+ R.string.fragment_tag_comment_detail));
+ if (fragment == null) {
+ return null;
+ }
+ return (CommentDetailFragment) fragment;
+ }
+
+ private boolean hasDetailFragment() {
+ return (getDetailFragment() != null);
+ }
+
+ private CommentsListFragment getListFragment() {
+ Fragment fragment = getFragmentManager().findFragmentByTag(getString(
+ R.string.fragment_tag_comment_list));
+ if (fragment == null) {
+ return null;
+ }
+ return (CommentsListFragment) fragment;
+ }
+
+ private boolean hasListFragment() {
+ return (getListFragment() != null);
+ }
+
+ private void showReaderFragment(long remoteBlogId, long postId) {
+ FragmentManager fm = getFragmentManager();
+ fm.executePendingTransactions();
+
+ Fragment fragment = ReaderPostDetailFragment.newInstance(remoteBlogId, postId);
+ FragmentTransaction ft = fm.beginTransaction();
+ String tagForFragment = getString(R.string.fragment_tag_reader_post_detail);
+ ft.add(R.id.layout_fragment_container, fragment, tagForFragment)
+ .addToBackStack(tagForFragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ if (hasDetailFragment())
+ ft.hide(getDetailFragment());
+ ft.commit();
+ }
+
+ /*
+ * called from comment list when user taps a comment
+ */
+ @Override
+ public void onCommentSelected(long commentId) {
+ mSelectedCommentId = commentId;
+ FragmentManager fm = getFragmentManager();
+ if (fm == null) return;
+
+ fm.executePendingTransactions();
+ CommentsListFragment listFragment = getListFragment();
+
+ FragmentTransaction ft = fm.beginTransaction();
+ String tagForFragment = getString(R.string.fragment_tag_comment_detail);
+ CommentDetailFragment detailFragment = CommentDetailFragment.newInstance(WordPress.getCurrentLocalTableBlogId(),
+ commentId);
+ ft.add(R.id.layout_fragment_container, detailFragment, tagForFragment).addToBackStack(tagForFragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ if (listFragment != null) {
+ ft.hide(listFragment);
+ }
+ ft.commitAllowingStateLoss();
+ }
+
+ /*
+ * called from comment detail when user taps a link to a post - show the post in a
+ * reader detail fragment
+ */
+ @Override
+ public void onPostClicked(Note note, int remoteBlogId, int postId) {
+ showReaderFragment(remoteBlogId, postId);
+ }
+
+ /*
+ * reload the comment list from existing data
+ */
+ private void reloadCommentList() {
+ CommentsListFragment listFragment = getListFragment();
+ if (listFragment != null)
+ listFragment.loadComments();
+ }
+
+ /*
+ * tell the comment list to get recent comments from server
+ */
+ private void updateCommentList() {
+ CommentsListFragment listFragment = getListFragment();
+ if (listFragment != null) {
+ //listFragment.setRefreshing(true);
+ listFragment.setCommentStatusFilter(mCurrentCommentStatusType);
+ listFragment.updateComments(false);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ // https://code.google.com/p/android/issues/detail?id=19917
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+
+ // retain the id of the highlighted and selected comments
+ if (mSelectedCommentId != 0 && hasDetailFragment()) {
+ outState.putLong(KEY_SELECTED_COMMENT_ID, mSelectedCommentId);
+ }
+
+ if (hasListFragment()) {
+ outState.putBoolean(KEY_AUTO_REFRESHED, getListFragment().mHasAutoRefreshedComments);
+ outState.putString(KEY_EMPTY_VIEW_MESSAGE, getListFragment().getEmptyViewMessage());
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ Dialog dialog = CommentDialogs.createCommentDialog(this, id);
+ if (dialog != null)
+ return dialog;
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ public void onModerateComment(final int accountId, final Comment comment, final CommentStatus newStatus) {
+ FragmentManager fm = getFragmentManager();
+ if (fm.getBackStackEntryCount() > 0) {
+ fm.popBackStack();
+ }
+
+ if (newStatus == CommentStatus.APPROVED || newStatus == CommentStatus.UNAPPROVED) {
+ getListFragment().setCommentIsModerating(comment.commentID, true);
+ getListFragment().updateEmptyView();
+ CommentActions.moderateComment(accountId, comment, newStatus,
+ new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ EventBus.getDefault().post(new CommentEvents.CommentModerationFinishedEvent
+ (result.isSuccess(), true, comment.commentID, newStatus));
+ }
+ });
+ } else if (newStatus == CommentStatus.SPAM || newStatus == CommentStatus.TRASH || newStatus == CommentStatus.DELETE) {
+ mTrashedComments.add(comment);
+ getListFragment().removeComment(comment);
+ getListFragment().setCommentIsModerating(comment.commentID, true);
+ getListFragment().updateEmptyView();
+
+ String message = (newStatus == CommentStatus.TRASH ? getString(R.string.comment_trashed) : newStatus == CommentStatus.SPAM ? getString(R.string.comment_spammed) : getString(R.string.comment_deleted_permanently));
+ View.OnClickListener undoListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTrashedComments.remove(comment);
+ getListFragment().setCommentIsModerating(comment.commentID, false);
+ getListFragment().loadComments();
+ }
+ };
+
+ Snackbar snackbar = Snackbar.make(getListFragment().getView(), message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, undoListener);
+
+ // do the actual moderation once the undo bar has been hidden
+ snackbar.setCallback(new Snackbar.Callback() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+
+ // comment will no longer exist in moderating list if action was undone
+ if (!mTrashedComments.contains(comment)) {
+ return;
+ }
+ mTrashedComments.remove(comment);
+ CommentActions.moderateComment(accountId, comment, newStatus, new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ EventBus.getDefault().post(new CommentEvents.CommentModerationFinishedEvent
+ (result.isSuccess(), true, comment.commentID, newStatus));
+ }
+ });
+ }
+ });
+
+ snackbar.show();
+ }
+ }
+
+ @Override
+ public void onCommentChanged(CommentActions.ChangeType changeType) {
+ switch (changeType) {
+ case EDITED:
+ reloadCommentList();
+ break;
+ case REPLIED:
+ updateCommentList();
+ break;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java
new file mode 100644
index 000000000..1b61519cc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java
@@ -0,0 +1,795 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.FilterCriteria;
+import org.wordpress.android.ui.EmptyViewMessageType;
+import org.wordpress.android.ui.FilteredRecyclerView;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.ErrorType;
+import org.xmlrpc.android.XMLRPCFault;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class CommentsListFragment extends Fragment implements CommentAdapter.OnDataLoadedListener,
+ CommentAdapter.OnLoadMoreListener, CommentAdapter.OnSelectedItemsChangeListener, CommentAdapter.OnCommentPressedListener {
+
+ interface OnCommentSelectedListener {
+ void onCommentSelected(long commentId);
+ }
+
+ private boolean mIsUpdatingComments = false;
+ private boolean mCanLoadMoreComments = true;
+ boolean mHasAutoRefreshedComments = false;
+
+ private final CommentStatus[] commentStatuses = {CommentStatus.UNKNOWN, CommentStatus.UNAPPROVED,
+ CommentStatus.APPROVED, CommentStatus.TRASH, CommentStatus.SPAM};
+
+ private EmptyViewMessageType mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT;
+ private FilteredRecyclerView mFilteredCommentsView;
+ private CommentAdapter mAdapter;
+ private ActionMode mActionMode;
+ private CommentStatus mCommentStatusFilter;
+
+ private UpdateCommentsTask mUpdateCommentsTask;
+
+ public static final int COMMENTS_PER_PAGE = 30;
+
+ private CommentAdapterState mCommentAdapterState;
+
+
+ private boolean hasAdapter() {
+ return (mAdapter != null);
+ }
+
+ private int getSelectedCommentCount() {
+ return getAdapter().getSelectedCommentCount();
+ }
+
+ public void removeComment(Comment comment) {
+ if (hasAdapter() && comment != null) {
+ getAdapter().removeComment(comment);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ Bundle extras = getActivity().getIntent().getExtras();
+ if (extras != null) {
+ mHasAutoRefreshedComments = extras.getBoolean(CommentsActivity.KEY_AUTO_REFRESHED);
+ mEmptyViewMessageType = EmptyViewMessageType.getEnumFromString(extras.getString(
+ CommentsActivity.KEY_EMPTY_VIEW_MESSAGE));
+ } else {
+ mHasAutoRefreshedComments = false;
+ mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT;
+ }
+
+ if (savedInstanceState != null) {
+ mCommentAdapterState = savedInstanceState.getParcelable(CommentAdapterState.KEY);
+ }
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ return;
+ }
+
+ // Restore the empty view's message
+ mFilteredCommentsView.updateEmptyView(mEmptyViewMessageType);
+
+ if (!mHasAutoRefreshedComments) {
+ updateComments(false);
+ mFilteredCommentsView.setRefreshing(true);
+ mHasAutoRefreshedComments = true;
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.comment_list_fragment, container, false);
+
+ mFilteredCommentsView = (FilteredRecyclerView) view.findViewById(R.id.filtered_recycler_view);
+ mFilteredCommentsView.setLogT(AppLog.T.COMMENTS);
+ mFilteredCommentsView.setFilterListener(new FilteredRecyclerView.FilterListener() {
+ @Override
+ public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) {
+ @SuppressWarnings("unchecked")
+ ArrayList<FilterCriteria> criteria = new ArrayList();
+ Collections.addAll(criteria, commentStatuses);
+ return criteria;
+ }
+
+ @Override
+ public void onLoadFilterCriteriaOptionsAsync(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) {
+ }
+
+ @Override
+ public void onLoadData() {
+ updateComments(false);
+ }
+
+ @Override
+ public void onFilterSelected(int position, FilterCriteria criteria) {
+ //trackCommentsAnalytics();
+ AppPrefs.setCommentsStatusFilter((CommentStatus) criteria);
+ mCommentStatusFilter = (CommentStatus) criteria;
+ finishActionMode();
+ }
+
+ @Override
+ public FilterCriteria onRecallSelection() {
+ mCommentStatusFilter = AppPrefs.getCommentsStatusFilter();
+ return mCommentStatusFilter;
+ }
+
+ @Override
+ public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) {
+
+ if (emptyViewMsgType == EmptyViewMessageType.NO_CONTENT) {
+ FilterCriteria filter = mFilteredCommentsView.getCurrentFilter();
+ if (filter == null || CommentStatus.UNKNOWN.equals(filter)) {
+ return getString(R.string.comments_empty_list);
+ } else {
+ switch (mCommentStatusFilter) {
+ case APPROVED:
+ return getString(R.string.comments_empty_list_filtered_approved);
+ case UNAPPROVED:
+ return getString(R.string.comments_empty_list_filtered_pending);
+ case SPAM:
+ return getString(R.string.comments_empty_list_filtered_spam);
+ case TRASH:
+ return getString(R.string.comments_empty_list_filtered_trashed);
+ default:
+ return getString(R.string.comments_empty_list);
+ }
+ }
+
+ } else {
+ int stringId = 0;
+ switch (emptyViewMsgType) {
+ case LOADING:
+ stringId = R.string.comments_fetching;
+ break;
+ case NETWORK_ERROR:
+ stringId = R.string.no_network_message;
+ break;
+ case PERMISSION_ERROR:
+ stringId = R.string.error_refresh_unauthorized_comments;
+ break;
+ case GENERIC_ERROR:
+ stringId = R.string.error_refresh_comments;
+ break;
+ }
+ return getString(stringId);
+ }
+
+ }
+
+ @Override
+ public void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType) {
+ }
+ });
+
+ // the following will change the look and feel of the toolbar to match the current design
+ mFilteredCommentsView.setToolbarBackgroundColor(ContextCompat.getColor(getActivity(), R.color.blue_medium));
+ mFilteredCommentsView.setToolbarSpinnerTextColor(ContextCompat.getColor(getActivity(), R.color.white));
+ mFilteredCommentsView.setToolbarSpinnerDrawable(R.drawable.arrow);
+ mFilteredCommentsView.setToolbarLeftAndRightPadding(
+ getResources().getDimensionPixelSize(R.dimen.margin_filter_spinner),
+ getResources().getDimensionPixelSize(R.dimen.margin_none));
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mFilteredCommentsView.getAdapter() == null) {
+ mFilteredCommentsView.setAdapter(getAdapter());
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments_showing_older));
+ }
+ getAdapter().loadComments(mCommentStatusFilter);
+ }
+ }
+
+ public void setCommentStatusFilter(CommentStatus statusFilter) {
+ mCommentStatusFilter = statusFilter;
+ }
+
+ private void moderateSelectedComments(final CommentStatus newStatus) {
+ if (!NetworkUtils.checkConnection(getActivity())) return;
+
+ final CommentList selectedComments = getAdapter().getSelectedComments();
+ final CommentList updateComments = new CommentList();
+
+ // build list of comments whose status is different than passed
+ for (Comment comment : selectedComments) {
+ if (comment.getStatusEnum() != newStatus) {
+ setCommentIsModerating(comment.commentID, true);
+ updateComments.add(comment);
+ }
+
+ }
+ if (updateComments.size() == 0) return;
+
+ CommentActions.OnCommentsModeratedListener listener = new CommentActions.OnCommentsModeratedListener() {
+ @Override
+ public void onCommentsModerated(final CommentList moderatedComments) {
+ EventBus.getDefault().post(
+ new CommentEvents.CommentsBatchModerationFinishedEvent(moderatedComments, false));
+ }
+ };
+
+ getAdapter().clearSelectedComments();
+ finishActionMode();
+
+ CommentActions.moderateComments(
+ WordPress.getCurrentLocalTableBlogId(),
+ updateComments,
+ newStatus,
+ listener);
+ }
+
+
+ private void confirmDeleteComments() {
+ if (CommentStatus.TRASH.equals(mCommentStatusFilter)) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
+ getActivity());
+ dialogBuilder.setTitle(getResources().getText(R.string.delete));
+ int resId = getAdapter().getSelectedCommentCount() > 1 ? R.string.dlg_sure_to_delete_comments : R.string.dlg_sure_to_delete_comment;
+ dialogBuilder.setMessage(getResources().getText(resId));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deleteSelectedComments(true);
+ }
+ });
+ dialogBuilder.setNegativeButton(
+ getResources().getText(R.string.no),
+ null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+
+ } else {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.dlg_confirm_trash_comments);
+ builder.setTitle(R.string.trash);
+ builder.setCancelable(true);
+ builder.setPositiveButton(R.string.trash_yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ deleteSelectedComments(false);
+ }
+ });
+ builder.setNegativeButton(R.string.trash_no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+ }
+
+ private void deleteSelectedComments(boolean deletePermanently) {
+ if (!NetworkUtils.checkConnection(getActivity())) return;
+
+
+ final CommentList selectedComments = getAdapter().getSelectedComments();
+
+ for (Comment comment : selectedComments) {
+ setCommentIsModerating(comment.commentID, true);
+ }
+
+ final CommentStatus newStatus = deletePermanently ? CommentStatus.DELETE : CommentStatus.TRASH;
+
+
+ CommentActions.OnCommentsModeratedListener listener = new CommentActions.OnCommentsModeratedListener() {
+ @Override
+ public void onCommentsModerated(final CommentList deletedComments) {
+ EventBus.getDefault().post(
+ new CommentEvents.CommentsBatchModerationFinishedEvent(deletedComments, true));
+ }
+ };
+
+ getAdapter().clearSelectedComments();
+ CommentActions.moderateComments(
+ WordPress.getCurrentLocalTableBlogId(), selectedComments, newStatus, listener);
+ }
+
+ void loadComments() {
+ // this is called from CommentsActivity when a comment was changed in the detail view,
+ // and the change will already be in SQLite so simply reload the comment adapter
+ // to show the change
+ getAdapter().loadComments(mCommentStatusFilter);
+ }
+
+ void updateEmptyView() {
+ //this is called from CommentsActivity in the case the last moment for a given type has been changed from that
+ //status, leaving the list empty, so we need to update the empty view. The method inside FilteredRecyclerView
+ //does the handling itself, so we only check for null here.
+ if (mFilteredCommentsView != null) {
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+
+ /*
+ * get latest comments from server, or pass loadMore=true to get comments beyond the
+ * existing ones
+ */
+ void updateComments(boolean loadMore) {
+ if (mIsUpdatingComments) {
+ AppLog.w(AppLog.T.COMMENTS, "update comments task already running");
+ return;
+ } else if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ mFilteredCommentsView.setRefreshing(false);
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments_showing_older));
+ //we're offline, load/refresh whatever we have in our local db
+ getAdapter().loadComments(mCommentStatusFilter);
+ return;
+ }
+
+ //immediately load/refresh whatever we have in our local db as we wait for the API call to get latest results
+ if (!loadMore) {
+ getAdapter().loadComments(mCommentStatusFilter);
+ }
+
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.LOADING);
+
+ mUpdateCommentsTask = new UpdateCommentsTask(loadMore, mCommentStatusFilter);
+ mUpdateCommentsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public void setCommentIsModerating(long commentId, boolean isModerating) {
+ if (!hasAdapter()) return;
+
+ if (isModerating) {
+ getAdapter().addModeratingCommentId(commentId);
+ } else {
+ getAdapter().removeModeratingCommentId(commentId);
+ }
+ }
+
+ public String getEmptyViewMessage() {
+ return mEmptyViewMessageType.name();
+ }
+
+ /*
+ * task to retrieve latest comments from server
+ */
+ private class UpdateCommentsTask extends AsyncTask<Void, Void, CommentList> {
+ ErrorType mErrorType = ErrorType.NO_ERROR;
+ final boolean mIsLoadingMore;
+ final CommentStatus mStatusFilter;
+
+ private UpdateCommentsTask(boolean loadMore, CommentStatus statusFilter) {
+ mIsLoadingMore = loadMore;
+ mStatusFilter = statusFilter;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsUpdatingComments = true;
+ if (mIsLoadingMore) {
+ mFilteredCommentsView.showLoadingProgress();
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsUpdatingComments = false;
+ mUpdateCommentsTask = null;
+ mFilteredCommentsView.setRefreshing(false);
+ }
+
+ @Override
+ protected CommentList doInBackground(Void... args) {
+ if (!isAdded()) {
+ return null;
+ }
+
+ final Blog blog = WordPress.getCurrentBlog();
+ if (blog == null) {
+ mErrorType = ErrorType.INVALID_CURRENT_BLOG;
+ return null;
+ }
+
+ Map<String, Object> hPost = new HashMap<>();
+ if (mIsLoadingMore) {
+ int numExisting = getAdapter().getItemCount();
+ hPost.put("offset", numExisting);
+ hPost.put("number", COMMENTS_PER_PAGE);
+ } else {
+ hPost.put("number", COMMENTS_PER_PAGE);
+ }
+
+ if (mStatusFilter != null) {
+ //if this is UNKNOWN that means show ALL, i.e., do not apply filter
+ if (!mStatusFilter.equals(CommentStatus.UNKNOWN)) {
+ hPost.put("status", CommentStatus.toString(mStatusFilter));
+ }
+ }
+
+ Object[] params = {blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ hPost};
+ try {
+ return ApiHelper.refreshComments(blog, params, new ApiHelper.DatabasePersistCallback() {
+ @Override
+ public void onDataReadyToSave(List list) {
+ int localBlogId = blog.getLocalTableBlogId();
+
+ if (!mIsLoadingMore) { //existing comments should be deleted only if we are not "loading more"
+ CommentTable.deleteCommentsForBlogWithFilter(localBlogId, mStatusFilter);
+ }
+ CommentTable.saveComments(localBlogId, (CommentList) list);
+ }
+ });
+ } catch (XMLRPCFault xmlrpcFault) {
+ mErrorType = ErrorType.UNKNOWN_ERROR;
+ if (xmlrpcFault.getFaultCode() == 401) {
+ mErrorType = ErrorType.UNAUTHORIZED;
+ }
+ } catch (Exception e) {
+ mErrorType = ErrorType.UNKNOWN_ERROR;
+ }
+ return null;
+ }
+
+ protected void onPostExecute(CommentList comments) {
+
+ boolean isRefreshing = mFilteredCommentsView.isRefreshing();
+ mIsUpdatingComments = false;
+ mUpdateCommentsTask = null;
+
+ if (!isAdded()) return;
+
+ if (mIsLoadingMore) {
+ mFilteredCommentsView.hideLoadingProgress();
+ }
+ mFilteredCommentsView.setRefreshing(false);
+
+ if (isCancelled()) return;
+
+ mCanLoadMoreComments = (comments != null && comments.size() > 0);
+
+ // result will be null on error OR if no more comments exists
+ if (comments == null && !getActivity().isFinishing() && mErrorType != ErrorType.NO_ERROR) {
+ switch (mErrorType) {
+ case UNAUTHORIZED:
+ if (!mFilteredCommentsView.emptyViewIsVisible()) {
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_unauthorized_comments));
+ }
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.PERMISSION_ERROR);
+ return;
+ default:
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments));
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.GENERIC_ERROR);
+ return;
+ }
+ }
+
+ if (!getActivity().isFinishing()) {
+ if (comments != null && comments.size() > 0) {
+ getAdapter().loadComments(mStatusFilter);
+ } else {
+ if (isRefreshing) {
+ //if refreshing and no errors, we only want freshest stuff, so clear old data
+ getAdapter().clearComments();
+ }
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+
+ if (hasAdapter()) {
+ outState.putParcelable(CommentAdapterState.KEY, getAdapter().getAdapterState());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ /****
+ * Contextual ActionBar (CAB) routines
+ ***/
+ private void updateActionModeTitle() {
+ if (mActionMode == null)
+ return;
+ int numSelected = getSelectedCommentCount();
+ if (numSelected > 0) {
+ mActionMode.setTitle(Integer.toString(numSelected));
+ } else {
+ mActionMode.setTitle("");
+ }
+ }
+
+ private void finishActionMode() {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ }
+
+ private final class ActionModeCallback implements ActionMode.Callback {
+ @Override
+ public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
+ mActionMode = actionMode;
+ MenuInflater inflater = actionMode.getMenuInflater();
+ inflater.inflate(R.menu.menu_comments_cab, menu);
+ mFilteredCommentsView.setSwipeToRefreshEnabled(false);
+ return true;
+ }
+
+ private void setItemEnabled(Menu menu, int menuId, boolean isEnabled) {
+ final MenuItem item = menu.findItem(menuId);
+ if (item == null || item.isEnabled() == isEnabled)
+ return;
+ item.setEnabled(isEnabled);
+ if (item.getIcon() != null) {
+ // must mutate the drawable to avoid affecting other instances of it
+ Drawable icon = item.getIcon().mutate();
+ icon.setAlpha(isEnabled ? 255 : 128);
+ item.setIcon(icon);
+ }
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
+ final CommentList selectedComments = getAdapter().getSelectedComments();
+
+ boolean hasSelection = (selectedComments.size() > 0);
+ boolean hasApproved = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.APPROVED);
+ boolean hasUnapproved = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.UNAPPROVED);
+ boolean hasSpam = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.SPAM);
+ boolean hasAnyNonSpam = hasSelection && selectedComments.hasAnyWithoutStatus(CommentStatus.SPAM);
+ boolean hasTrash = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.TRASH);
+
+ setItemEnabled(menu, R.id.menu_approve, hasUnapproved || hasSpam || hasTrash);
+ setItemEnabled(menu, R.id.menu_unapprove, hasApproved);
+ setItemEnabled(menu, R.id.menu_spam, hasAnyNonSpam);
+ setItemEnabled(menu, R.id.menu_trash, hasSelection);
+
+ final MenuItem trashItem = menu.findItem(R.id.menu_trash);
+ if (trashItem != null) {
+ if (CommentStatus.TRASH.equals(mCommentStatusFilter)) {
+ trashItem.setTitle(R.string.mnu_comment_delete_permanently);
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
+ int numSelected = getSelectedCommentCount();
+ if (numSelected == 0)
+ return false;
+
+ int i = menuItem.getItemId();
+ if (i == R.id.menu_approve) {
+ moderateSelectedComments(CommentStatus.APPROVED);
+ return true;
+ } else if (i == R.id.menu_unapprove) {
+ moderateSelectedComments(CommentStatus.UNAPPROVED);
+ return true;
+ } else if (i == R.id.menu_spam) {
+ moderateSelectedComments(CommentStatus.SPAM);
+ return true;
+ } else if (i == R.id.menu_trash) {// unlike the other status changes, we ask the user to confirm trashing
+ confirmDeleteComments();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ getAdapter().setEnableSelection(false);
+ mFilteredCommentsView.setSwipeToRefreshEnabled(true);
+ mActionMode = null;
+ }
+ }
+
+ private CommentAdapter getAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new CommentAdapter(getActivity(), WordPress.getCurrentLocalTableBlogId());
+ mAdapter.setInitialState(mCommentAdapterState);
+ mAdapter.setOnCommentPressedListener(this);
+ mAdapter.setOnDataLoadedListener(this);
+ mAdapter.setOnLoadMoreListener(this);
+ mAdapter.setOnSelectedItemsChangeListener(this);
+ }
+
+ return mAdapter;
+ }
+
+
+ // adapter calls this when selected comments have changed (CAB)
+ @Override
+ public void onSelectedItemsChanged() {
+ if (mActionMode != null) {
+ if (getSelectedCommentCount() == 0) {
+ mActionMode.finish();
+ } else {
+ updateActionModeTitle();
+ // must invalidate to ensure onPrepareActionMode is called
+ mActionMode.invalidate();
+ }
+ }
+ }
+
+ @Override
+ public void onCommentPressed(int position, View view) {
+ // if the comment is being moderated ignore the press
+ Comment comment = getAdapter().getItem(position);
+ if (!isCommentSelectable(comment)) {
+ return;
+ }
+
+ if (mActionMode == null) {
+ mFilteredCommentsView.invalidate();
+ if (getActivity() instanceof OnCommentSelectedListener) {
+ ((OnCommentSelectedListener) getActivity()).onCommentSelected(comment.commentID);
+ }
+ } else {
+ getAdapter().toggleItemSelected(position, view);
+ }
+ }
+
+ @Override
+ public void onCommentLongPressed(int position, View view) {
+ // if the comment is being moderated ignore the press
+ Comment comment = getAdapter().getItem(position);
+ if (!isCommentSelectable(comment)) {
+ return;
+ }
+
+ // enable CAB if it's not already enabled
+ if (mActionMode == null) {
+ if (getActivity() instanceof AppCompatActivity) {
+ ((AppCompatActivity) getActivity()).startSupportActionMode(new ActionModeCallback());
+ getAdapter().setEnableSelection(true);
+ getAdapter().setItemSelected(position, true, view);
+ }
+ } else {
+ getAdapter().toggleItemSelected(position, view);
+ }
+ }
+
+ private boolean isCommentSelectable(Comment comment){
+ return comment != null && !getAdapter().isModeratingCommentId(comment.commentID);
+ }
+
+ private boolean shouldRestoreCab() {
+ return hasAdapter() && !getAdapter().getSelectedCommentsId().isEmpty() && mActionMode == null;
+ }
+
+ private void restoreCab() {
+ if (getActivity() instanceof AppCompatActivity) {
+ ((AppCompatActivity) getActivity()).startSupportActionMode(new ActionModeCallback());
+ updateActionModeTitle();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CommentEvents.CommentModerationFinishedEvent event) {
+ if (!isAdded()) return;
+
+ setCommentIsModerating(event.getCommentId(), false);
+
+ if (!event.isSuccess()) {
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment, ToastUtils.Duration.LONG);
+ }
+
+ if (event.isCommentsRefreshRequired() || event.getNewStatus() != mCommentStatusFilter) {
+ loadComments();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CommentEvents.CommentsBatchModerationFinishedEvent moderatedComments) {
+ if (!isAdded()) return;
+
+ if (moderatedComments.getComments().size() > 0) {
+ for (Comment comment : moderatedComments.getComments()) {
+ setCommentIsModerating(comment.commentID, false);
+ }
+
+ if (moderatedComments.isDeleted()) {
+ getAdapter().deleteComments(moderatedComments.getComments());
+ } else {
+ getAdapter().replaceComments(moderatedComments.getComments());
+ }
+
+ loadComments();
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment);
+ }
+ }
+
+
+ // called after comments have been loaded
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ if (!isAdded()) return;
+
+ if (!isEmpty) {
+ // After comments are loaded, we should check if some of them are selected and show CAB if necessary
+ if (shouldRestoreCab()) {
+ restoreCab();
+ }
+
+ // Hide the empty view if there are already some displayed comments
+ mFilteredCommentsView.hideEmptyView();
+ } else if (!mIsUpdatingComments) {
+ // Change LOADING to NO_CONTENT message
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+
+ @Override
+ public void onLoadMore() {
+ if (mCanLoadMoreComments && !mIsUpdatingComments) {
+ updateComments(true);
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java
new file mode 100644
index 000000000..fc784483c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java
@@ -0,0 +1,440 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+
+import com.android.volley.VolleyError;
+import com.simperium.client.BucketObjectMissingException;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.VolleyUtils;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class EditCommentActivity extends AppCompatActivity {
+ static final String ARG_LOCAL_BLOG_ID = "blog_id";
+ static final String ARG_COMMENT_ID = "comment_id";
+ static final String ARG_NOTE_ID = "note_id";
+
+ private static final int ID_DIALOG_SAVING = 0;
+
+ private int mLocalBlogId;
+ private long mCommentId;
+ private Comment mComment;
+ private Note mNote;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.comment_edit_activity);
+ setTitle(getString(R.string.edit_comment));
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ loadComment(getIntent());
+
+ ActivityId.trackLastActivity(ActivityId.COMMENT_EDITOR);
+ }
+
+ private void loadComment(Intent intent) {
+ if (intent == null) {
+ showErrorAndFinish();
+ return;
+ }
+
+ mLocalBlogId = intent.getIntExtra(ARG_LOCAL_BLOG_ID, 0);
+ mCommentId = intent.getLongExtra(ARG_COMMENT_ID, 0);
+ final String noteId = intent.getStringExtra(ARG_NOTE_ID);
+ if (noteId == null) {
+ mComment = CommentTable.getComment(mLocalBlogId, mCommentId);
+ if (mComment == null) {
+ showErrorAndFinish();
+ return;
+ }
+
+ configureViews();
+ } else {
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ mNote = SimperiumUtils.getNotesBucket().get(noteId);
+ requestFullCommentForNote(mNote);
+ } catch (BucketObjectMissingException e) {
+ showErrorAndFinish();
+ }
+ }
+ }
+ }
+
+ private void showErrorAndFinish() {
+ ToastUtils.showToast(this, R.string.error_load_comment);
+ finish();
+ }
+
+ private void configureViews() {
+ final EditText editAuthorName = (EditText) this.findViewById(R.id.author_name);
+ editAuthorName.setText(mComment.getAuthorName());
+
+ final EditText editAuthorEmail = (EditText) this.findViewById(R.id.author_email);
+ editAuthorEmail.setText(mComment.getAuthorEmail());
+
+ final EditText editAuthorUrl = (EditText) this.findViewById(R.id.author_url);
+ editAuthorUrl.setText(mComment.getAuthorUrl());
+
+ // REST API can currently only edit comment content
+ if (mNote != null) {
+ editAuthorName.setVisibility(View.GONE);
+ editAuthorEmail.setVisibility(View.GONE);
+ editAuthorUrl.setVisibility(View.GONE);
+ }
+
+ final EditText editContent = (EditText) this.findViewById(R.id.edit_comment_content);
+ editContent.setText(mComment.getCommentText());
+
+ // show error when comment content is empty
+ editContent.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ boolean hasError = (editContent.getError() != null);
+ boolean hasText = (s != null && s.length() > 0);
+ if (!hasText && !hasError) {
+ editContent.setError(getString(R.string.content_required));
+ } else if (hasText && hasError) {
+ editContent.setError(null);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.edit_comment, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int i = item.getItemId();
+ if (i == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (i == R.id.menu_save_comment) {
+ saveComment();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private String getEditTextStr(int resId) {
+ final EditText edit = (EditText) findViewById(resId);
+ return EditTextUtils.getText(edit);
+ }
+
+ private void saveComment() {
+ // make sure comment content was entered
+ final EditText editContent = (EditText) findViewById(R.id.edit_comment_content);
+ if (EditTextUtils.isEmpty(editContent)) {
+ editContent.setError(getString(R.string.content_required));
+ return;
+ }
+
+ // return immediately if comment hasn't changed
+ if (!isCommentEdited()) {
+ ToastUtils.showToast(this, R.string.toast_comment_unedited);
+ return;
+ }
+
+ // make sure we have an active connection
+ if (!NetworkUtils.checkConnection(this))
+ return;
+
+ if (mNote != null) {
+ // Edit comment via REST API :)
+ showSaveDialog();
+ WordPress.getRestClientUtils().editCommentContent(mNote.getSiteId(),
+ mNote.getCommentId(),
+ EditTextUtils.getText(editContent),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (isFinishing()) return;
+
+ dismissSaveDialog();
+ setResult(RESULT_OK);
+ finish();
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (isFinishing()) return;
+
+ dismissSaveDialog();
+ showEditErrorAlert();
+ }
+ });
+ } else {
+ // Edit comment via XML-RPC :(
+ if (mIsUpdateTaskRunning)
+ AppLog.w(AppLog.T.COMMENTS, "update task already running");
+ new UpdateCommentTask().execute();
+ }
+ }
+
+ /*
+ * returns true if user made any changes to the comment
+ */
+ private boolean isCommentEdited() {
+ if (mComment == null)
+ return false;
+
+ final String authorName = getEditTextStr(R.id.author_name);
+ final String authorEmail = getEditTextStr(R.id.author_email);
+ final String authorUrl = getEditTextStr(R.id.author_url);
+ final String content = getEditTextStr(R.id.edit_comment_content);
+
+ return !(authorName.equals(mComment.getAuthorName())
+ && authorEmail.equals(mComment.getAuthorEmail())
+ && authorUrl.equals(mComment.getAuthorUrl())
+ && content.equals(mComment.getCommentText()));
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ if (id == ID_DIALOG_SAVING) {
+ ProgressDialog savingDialog = new ProgressDialog(this);
+ savingDialog.setMessage(getResources().getText(R.string.saving_changes));
+ savingDialog.setIndeterminate(true);
+ savingDialog.setCancelable(true);
+ return savingDialog;
+ }
+ return super.onCreateDialog(id);
+ }
+
+ private void showSaveDialog() {
+ showDialog(ID_DIALOG_SAVING);
+ }
+
+ private void dismissSaveDialog() {
+ try {
+ dismissDialog(ID_DIALOG_SAVING);
+ } catch (IllegalArgumentException e) {
+ // dialog doesn't exist
+ }
+ }
+
+ /*
+ * AsyncTask to save comment to server
+ */
+ private boolean mIsUpdateTaskRunning = false;
+ private class UpdateCommentTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected void onPreExecute() {
+ mIsUpdateTaskRunning = true;
+ showSaveDialog();
+ }
+ @Override
+ protected void onCancelled() {
+ mIsUpdateTaskRunning = false;
+ dismissSaveDialog();
+ }
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ final Blog blog;
+ blog = WordPress.wpDB.instantiateBlogByLocalId(mLocalBlogId);
+ if (blog == null) {
+ AppLog.e(AppLog.T.COMMENTS, "Invalid local blog id:" + mLocalBlogId);
+ return false;
+ }
+ final String authorName = getEditTextStr(R.id.author_name);
+ final String authorEmail = getEditTextStr(R.id.author_email);
+ final String authorUrl = getEditTextStr(R.id.author_url);
+ final String content = getEditTextStr(R.id.edit_comment_content);
+
+ final Map<String, String> postHash = new HashMap<>();
+
+ // using CommentStatus.toString() rather than getStatus() ensures that the XML-RPC
+ // status value is used - important since comment may have been loaded via the
+ // REST API, which uses different status values
+ postHash.put("status", CommentStatus.toString(mComment.getStatusEnum()));
+ postHash.put("content", content);
+ postHash.put("author", authorName);
+ postHash.put("author_url", authorUrl);
+ postHash.put("author_email", authorEmail);
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Object[] xmlParams = {blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(), Long.toString(
+ mCommentId), postHash};
+
+ try {
+ Object result = client.call(Method.EDIT_COMMENT, xmlParams);
+ boolean isSaved = (result != null && Boolean.parseBoolean(result.toString()));
+ if (isSaved) {
+ mComment.setAuthorEmail(authorEmail);
+ mComment.setAuthorUrl(authorUrl);
+ mComment.setAuthorName(authorName);
+ mComment.setCommentText(content);
+ CommentTable.updateComment(mLocalBlogId, mComment);
+ }
+ return isSaved;
+ } catch (XMLRPCException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ } catch (XmlPullParserException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ }
+ }
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (isFinishing()) return;
+
+ mIsUpdateTaskRunning = false;
+ dismissSaveDialog();
+
+ if (result) {
+ setResult(RESULT_OK);
+ finish();
+ } else {
+ // alert user to error
+ showEditErrorAlert();
+ }
+ }
+ }
+
+ private void showEditErrorAlert() {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(EditCommentActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(R.string.error_edit_comment);
+ dialogBuilder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // just close the dialog
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+
+ // Request a comment via the REST API for a note
+ private void requestFullCommentForNote(Note note) {
+ if (isFinishing()) return;
+ final ProgressBar progress = (ProgressBar)findViewById(R.id.edit_comment_progress);
+ final View editContainer = findViewById(R.id.edit_comment_container);
+
+ if (progress == null || editContainer == null) {
+ return;
+ }
+
+ editContainer.setVisibility(View.GONE);
+ progress.setVisibility(View.VISIBLE);
+
+ RestRequest.Listener restListener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (!isFinishing()) {
+ progress.setVisibility(View.GONE);
+ editContainer.setVisibility(View.VISIBLE);
+ Comment comment = Comment.fromJSON(jsonObject);
+ if (comment != null) {
+ mComment = comment;
+ configureViews();
+ } else {
+ showErrorAndFinish();
+ }
+ }
+ }
+ };
+ RestRequest.ErrorListener restErrListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.COMMENTS, VolleyUtils.errStringFromVolleyError(volleyError), volleyError);
+ if (!isFinishing()) {
+ progress.setVisibility(View.GONE);
+ showErrorAndFinish();
+ }
+ }
+ };
+
+ final String path = String.format("/sites/%s/comments/%s", note.getSiteId(), note.getCommentId());
+ WordPress.getRestClientUtils().get(path, restListener, restErrListener);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (isCommentEdited()) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
+ EditCommentActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.cancel_edit));
+ dialogBuilder.setMessage(getResources().getText(R.string.sure_to_cancel_edit_comment));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ finish();
+ }
+ });
+ dialogBuilder.setNegativeButton(
+ getResources().getText(R.string.no),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // just close the dialog
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java
new file mode 100644
index 000000000..7873492d3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java
@@ -0,0 +1,748 @@
+package org.wordpress.android.ui.main;
+
+import com.android.volley.Cache;
+import com.android.volley.Request;
+import com.github.xizzhu.simpletooltip.ToolTip;
+import com.github.xizzhu.simpletooltip.ToolTipView;
+import com.yalantis.ucrop.UCrop;
+import com.yalantis.ucrop.UCropActivity;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.CursorLoader;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.networking.GravatarApi;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.media.WordPressMediaUtils;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.prefs.PrefsEvents;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HelpshiftHelper.Tag;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.PermissionUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import de.greenrobot.event.EventBus;
+
+public class MeFragment extends Fragment {
+ private static final String IS_DISCONNECTING = "IS_DISCONNECTING";
+ private static final String IS_UPDATING_GRAVATAR = "IS_UPDATING_GRAVATAR";
+ private static final String MEDIA_CAPTURE_PATH = "MEDIA_CAPTURE_PATH";
+
+ private static final int CAMERA_AND_MEDIA_PERMISSION_REQUEST_CODE = 1;
+
+ private ViewGroup mAvatarFrame;
+ private View mProgressBar;
+ private ToolTipView mGravatarToolTipView;
+ private View mAvatarTooltipAnchor;
+ private ViewGroup mAvatarContainer;
+ private WPNetworkImageView mAvatarImageView;
+ private TextView mDisplayNameTextView;
+ private TextView mUsernameTextView;
+ private TextView mLoginLogoutTextView;
+ private View mMyProfileView;
+ private View mAccountSettingsView;
+ private View mNotificationsView;
+ private View mNotificationsDividerView;
+ private ProgressDialog mDisconnectProgressDialog;
+ private String mMediaCapturePath;
+
+ // setUserVisibleHint is not available so we need to manually handle the UserVisibleHint state
+ private boolean mIsUserVisible;
+
+ private boolean mIsUpdatingGravatar;
+
+ public static MeFragment newInstance() {
+ return new MeFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ mMediaCapturePath = savedInstanceState.getString(MEDIA_CAPTURE_PATH);
+ mIsUpdatingGravatar = savedInstanceState.getBoolean(IS_UPDATING_GRAVATAR);
+ }
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+
+ mIsUserVisible = isVisibleToUser;
+
+ if (isResumed()) {
+ showGravatarTooltipIfNeeded();
+ }
+ }
+
+ private void showGravatarTooltipIfNeeded() {
+ if (!isAdded() || !AccountHelper.isSignedInWordPressDotCom() || !AppPrefs.isGravatarChangePromoRequired() ||
+ !mIsUserVisible || mGravatarToolTipView != null) {
+ return;
+ }
+
+ ToolTip toolTip = createGravatarPromoToolTip(getString(R.string.gravatar_tip), ContextCompat.getColor
+ (getActivity(), R.color.color_primary));
+ mGravatarToolTipView = new ToolTipView.Builder(getActivity())
+ .withAnchor(mAvatarTooltipAnchor)
+ .withToolTip(toolTip)
+ .withGravity(Gravity.END)
+ .build();
+ mGravatarToolTipView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_TOOLTIP_TAPPED);
+
+ mGravatarToolTipView.remove();
+ AppPrefs.setGravatarChangePromoRequired(false);
+ }
+ });
+ mGravatarToolTipView.showDelayed(500);
+ }
+
+ private ToolTip createGravatarPromoToolTip(CharSequence text, int backgroundColor) {
+ Resources resources = getResources();
+ int padding = resources.getDimensionPixelSize(R.dimen.tooltip_padding);
+ int textSize = resources.getDimensionPixelSize(R.dimen.tooltip_text_size);
+ int radius = resources.getDimensionPixelSize(R.dimen.tooltip_radius);
+ return new ToolTip.Builder()
+ .withText(text)
+ .withTextColor(Color.WHITE)
+ .withTextSize(textSize)
+ .withBackgroundColor(backgroundColor)
+ .withPadding(padding, padding, padding, padding)
+ .withCornerRadius(radius)
+ .build();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.me_fragment, container, false);
+
+ mAvatarFrame = (ViewGroup) rootView.findViewById(R.id.frame_avatar);
+ mAvatarContainer = (ViewGroup) rootView.findViewById(R.id.avatar_container);
+ mAvatarImageView = (WPNetworkImageView) rootView.findViewById(R.id.me_avatar);
+ mAvatarTooltipAnchor = rootView.findViewById(R.id.avatar_tooltip_anchor);
+ mProgressBar = rootView.findViewById(R.id.avatar_progress);
+ mDisplayNameTextView = (TextView) rootView.findViewById(R.id.me_display_name);
+ mUsernameTextView = (TextView) rootView.findViewById(R.id.me_username);
+ mLoginLogoutTextView = (TextView) rootView.findViewById(R.id.me_login_logout_text_view);
+ mMyProfileView = rootView.findViewById(R.id.row_my_profile);
+ mAccountSettingsView = rootView.findViewById(R.id.row_account_settings);
+ mNotificationsView = rootView.findViewById(R.id.row_notifications);
+ mNotificationsDividerView = rootView.findViewById(R.id.me_notifications_divider);
+
+ addDropShadowToAvatar();
+
+ mAvatarContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_TAPPED);
+
+ // User tapped the Gravatar so dismiss the tooltip
+ if (mGravatarToolTipView != null) {
+ mGravatarToolTipView.remove();
+ }
+ // and no need to promote the feature any more
+ AppPrefs.setGravatarChangePromoRequired(false);
+
+ if (PermissionUtils.checkAndRequestCameraAndStoragePermissions(MeFragment.this,
+ CAMERA_AND_MEDIA_PERMISSION_REQUEST_CODE)) {
+ askForCameraOrGallery();
+ }
+ }
+ });
+ mMyProfileView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewMyProfile(getActivity());
+ }
+ });
+
+ mAccountSettingsView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewAccountSettings(getActivity());
+ }
+ });
+
+ rootView.findViewById(R.id.row_app_settings).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewAppSettings(getActivity());
+ }
+ });
+
+ mNotificationsView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewNotificationsSettings(getActivity());
+ }
+ });
+
+ rootView.findViewById(R.id.row_support).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewHelpAndSupport(getActivity(), Tag.ORIGIN_ME_SCREEN_HELP);
+ }
+ });
+
+ rootView.findViewById(R.id.row_logout).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ signOutWordPressComWithConfirmation();
+ } else {
+ ActivityLauncher.showSignInForResult(getActivity());
+ }
+ }
+ });
+
+ if (savedInstanceState != null) {
+ if (savedInstanceState.getBoolean(IS_DISCONNECTING, false)) {
+ showDisconnectDialog(getActivity());
+ }
+
+ if (savedInstanceState.getBoolean(IS_UPDATING_GRAVATAR, false)) {
+ showGravatarProgressBar(true);
+ }
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mDisconnectProgressDialog != null) {
+ outState.putBoolean(IS_DISCONNECTING, true);
+ }
+
+ if (mMediaCapturePath != null) {
+ outState.putString(MEDIA_CAPTURE_PATH, mMediaCapturePath);
+ }
+
+ outState.putBoolean(IS_UPDATING_GRAVATAR, mIsUpdatingGravatar);
+
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshAccountDetails();
+
+ showGravatarTooltipIfNeeded();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mDisconnectProgressDialog != null) {
+ mDisconnectProgressDialog.dismiss();
+ mDisconnectProgressDialog = null;
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * adds a circular drop shadow to the avatar's parent view (Lollipop+ only)
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void addDropShadowToAvatar() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mAvatarImageView.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setOval(0, 0, view.getWidth(), view.getHeight());
+ }
+ });
+ mAvatarImageView.setElevation(mAvatarImageView.getResources().getDimensionPixelSize(R.dimen.card_elevation));
+ }
+ }
+
+ private void refreshAccountDetails() {
+ // we only want to show user details for WordPress.com users
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ Account defaultAccount = AccountHelper.getDefaultAccount();
+
+ mDisplayNameTextView.setVisibility(View.VISIBLE);
+ mUsernameTextView.setVisibility(View.VISIBLE);
+ mAvatarFrame.setVisibility(View.VISIBLE);
+ mMyProfileView.setVisibility(View.VISIBLE);
+ mNotificationsView.setVisibility(View.VISIBLE);
+ mNotificationsDividerView.setVisibility(View.VISIBLE);
+
+ final String avatarUrl = constructGravatarUrl(AccountHelper.getDefaultAccount());
+ loadAvatar(avatarUrl, null);
+
+ mUsernameTextView.setText("@" + defaultAccount.getUserName());
+ mLoginLogoutTextView.setText(R.string.me_disconnect_from_wordpress_com);
+
+ String displayName = StringUtils.unescapeHTML(defaultAccount.getDisplayName());
+ if (!TextUtils.isEmpty(displayName)) {
+ mDisplayNameTextView.setText(displayName);
+ } else {
+ mDisplayNameTextView.setText(defaultAccount.getUserName());
+ }
+ } else {
+ mDisplayNameTextView.setVisibility(View.GONE);
+ mUsernameTextView.setVisibility(View.GONE);
+ mAvatarFrame.setVisibility(View.GONE);
+ mProgressBar.setVisibility(View.GONE);
+ mMyProfileView.setVisibility(View.GONE);
+ mAccountSettingsView.setVisibility(View.GONE);
+ mNotificationsView.setVisibility(View.GONE);
+ mNotificationsDividerView.setVisibility(View.GONE);
+ mLoginLogoutTextView.setText(R.string.me_connect_to_wordpress_com);
+ }
+ }
+
+ private void showGravatarProgressBar(boolean isUpdating) {
+ mProgressBar.setVisibility(isUpdating ? View.VISIBLE : View.GONE);
+ mIsUpdatingGravatar = isUpdating;
+ }
+
+ private String constructGravatarUrl(Account account) {
+ int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_large);
+ return GravatarUtils.fixGravatarUrl(account.getAvatarUrl(), avatarSz);
+ }
+
+ private void loadAvatar(String avatarUrl, String injectFilePath) {
+ if (injectFilePath != null && !injectFilePath.isEmpty()) {
+ // invalidate the specific gravatar entry from the bitmap cache. It will be updated via the injected
+ // request cache.
+ WordPress.getBitmapCache().removeSimilar(avatarUrl);
+
+ try {
+ // fool the network requests cache by injecting the new image. The Gravatar backend (plus CDNs)
+ // can't be trusted to have updated the image quick enough.
+ injectCache(new File(injectFilePath), avatarUrl);
+ } catch (IOException e) {
+ EventBus.getDefault().post(new GravatarLoadFinished(false));
+ }
+
+ // invalidate the WPNetworkImageView
+ mAvatarImageView.invalidateImage();
+ }
+
+ mAvatarImageView.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR, new WPNetworkImageView
+ .ImageLoadListener() {
+ @Override
+ public void onLoaded() {
+ EventBus.getDefault().post(new GravatarLoadFinished(true));
+ }
+
+ @Override
+ public void onError() {
+ EventBus.getDefault().post(new GravatarLoadFinished(false));
+ }
+ });
+ }
+
+ private void signOutWordPressComWithConfirmation() {
+ String message = String.format(getString(R.string.sign_out_wpcom_confirm), AccountHelper.getDefaultAccount()
+ .getUserName());
+
+ new AlertDialog.Builder(getActivity())
+ .setMessage(message)
+ .setPositiveButton(R.string.signout, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ signOutWordPressCom();
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .setCancelable(true)
+ .create().show();
+ }
+
+ private void signOutWordPressCom() {
+ // note that signing out sends a CoreEvents.UserSignedOutWordPressCom EventBus event,
+ // which will cause the main activity to recreate this fragment
+ (new SignOutWordPressComAsync(getActivity())).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void showDisconnectDialog(Context context) {
+ mDisconnectProgressDialog = ProgressDialog.show(context, null, context.getText(R.string.signing_out), false);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[]
+ grantResults) {
+ switch (requestCode) {
+ case CAMERA_AND_MEDIA_PERMISSION_REQUEST_CODE:
+ if (permissions.length == 0) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_PERMISSIONS_INTERRUPTED);
+ } else {
+ List<String> granted = new ArrayList<>();
+ List<String> denied = new ArrayList<>();
+
+ for (int i = 0; i < grantResults.length; i++) {
+ if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ granted.add(permissions[i]);
+ } else {
+ denied.add(permissions[i]);
+ }
+ }
+
+ if (denied.size() == 0) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_PERMISSIONS_ACCEPTED);
+ askForCameraOrGallery();
+ } else {
+ ToastUtils.showToast(this.getActivity(), getString(R.string
+ .gravatar_camera_and_media_permission_required), ToastUtils.Duration.LONG);
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("permissions granted", granted.size() == 0 ? "[none]" : TextUtils
+ .join(",", granted));
+ properties.put("permissions denied", TextUtils.join(",", denied));
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_PERMISSIONS_DENIED, properties);
+ }
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case RequestCodes.PICTURE_LIBRARY_OR_CAPTURE:
+ if (resultCode == Activity.RESULT_OK) {
+ Uri imageUri;
+
+ if (data == null || data.getData() == null) {
+ // image is from a capture
+ imageUri = Uri.fromFile(new File(mMediaCapturePath));
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_SHOT_NEW);
+ } else {
+ imageUri = data.getData();
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_GALLERY_PICKED);
+ }
+
+ if (imageUri != null) {
+ startCropActivity(imageUri);
+ } else {
+ AppLog.e(AppLog.T.UTILS, "Can't resolve picked or captured image");
+ }
+ }
+ break;
+ case UCrop.REQUEST_CROP:
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_CROPPED);
+
+ if (resultCode == Activity.RESULT_OK) {
+ fetchMedia(UCrop.getOutput(data));
+ } else if (resultCode == UCrop.RESULT_ERROR) {
+ Toast.makeText(getActivity(), getString(R.string.error_cropping_image), Toast.LENGTH_SHORT).show();
+
+ final Throwable cropError = UCrop.getError(data);
+ AppLog.e(AppLog.T.MAIN, "Image cropping failed!", cropError);
+ }
+ break;
+ }
+ }
+
+ private void askForCameraOrGallery() {
+ WordPressMediaUtils
+ .launchPictureLibraryOrCapture(MeFragment.this, BuildConfig.APPLICATION_ID,
+ new WordPressMediaUtils.LaunchCameraCallback() {
+ @Override
+ public void onMediaCapturePathReady(String mediaCapturePath) {
+ mMediaCapturePath = mediaCapturePath;
+ }
+ });
+ }
+
+ private void startCropActivity(Uri uri) {
+ final Context context = getActivity();
+
+ if (context == null) {
+ return;
+ }
+
+ UCrop.Options options = new UCrop.Options();
+ options.setShowCropGrid(false);
+ options.setStatusBarColor(ContextCompat.getColor(context, R.color.status_bar_tint));
+ options.setToolbarColor(ContextCompat.getColor(context, R.color.color_primary));
+ options.setAllowedGestures(UCropActivity.ALL, UCropActivity.ALL, UCropActivity.ALL);
+ options.setHideBottomControls(true);
+
+ UCrop.of(uri, Uri.fromFile(new File(context.getCacheDir(), "cropped_for_gravatar.jpg")))
+ .withAspectRatio(1, 1)
+ .withOptions(options)
+ .start(getActivity(), this);
+ }
+
+ private void fetchMedia(Uri mediaUri) {
+ if (!MediaUtils.isInMediaStore(mediaUri)) {
+ // Create an AsyncTask to download the file
+ new DownloadMediaTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mediaUri);
+ } else {
+ // It is a regular local media file
+ startGravatarUpload(getRealPathFromURI(mediaUri));
+ }
+ }
+
+ private String getRealPathFromURI(Uri uri) {
+ String path;
+ if ("content".equals(uri.getScheme())) {
+ path = getRealPathFromContentURI(uri);
+ } else if ("file".equals(uri.getScheme())) {
+ path = uri.getPath();
+ } else {
+ path = uri.toString();
+ }
+ return path;
+ }
+
+ private String getRealPathFromContentURI(Uri contentUri) {
+ if (contentUri == null)
+ return null;
+
+ String[] proj = { MediaStore.Images.Media.DATA };
+ CursorLoader loader = new CursorLoader(getActivity(), contentUri, proj, null, null, null);
+ Cursor cursor = loader.loadInBackground();
+
+ if (cursor == null)
+ return null;
+
+ int column_index = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
+ if (column_index == -1) {
+ cursor.close();
+ return null;
+ }
+
+ String path;
+ if (cursor.moveToFirst()) {
+ path = cursor.getString(column_index);
+ } else {
+ path = null;
+ }
+
+ cursor.close();
+ return path;
+ }
+
+ private class DownloadMediaTask extends AsyncTask<Uri, Integer, Uri> {
+ @Override
+ protected Uri doInBackground(Uri... uris) {
+ Uri imageUri = uris[0];
+ return MediaUtils.downloadExternalMedia(getActivity(), imageUri);
+ }
+
+ protected void onPostExecute(Uri newUri) {
+ if (getActivity() == null)
+ return;
+
+ if (newUri != null) {
+ String path = getRealPathFromURI(newUri);
+ startGravatarUpload(path);
+ } else {
+ Toast.makeText(getActivity(), getString(R.string.error_downloading_image), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private void startGravatarUpload(final String filePath) {
+ File file = new File(filePath);
+ if (!file.exists()) {
+ Toast.makeText(getActivity(), getString(R.string.error_locating_image), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ showGravatarProgressBar(true);
+
+ GravatarApi.uploadGravatar(file, new GravatarApi.GravatarUploadListener() {
+ @Override
+ public void onSuccess() {
+ EventBus.getDefault().post(new GravatarUploadFinished(filePath, true));
+ }
+
+ @Override
+ public void onError() {
+ EventBus.getDefault().post(new GravatarUploadFinished(filePath, false));
+ }
+ });
+ }
+
+ static public class GravatarUploadFinished {
+ public final String filePath;
+ public final boolean success;
+
+ public GravatarUploadFinished(String filePath, boolean success) {
+ this.filePath = filePath;
+ this.success = success;
+ }
+ }
+
+ public void onEventMainThread(GravatarUploadFinished event) {
+ if (event.success) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOADED);
+ final String avatarUrl = constructGravatarUrl(AccountHelper.getDefaultAccount());
+ loadAvatar(avatarUrl, event.filePath);
+ } else {
+ showGravatarProgressBar(false);
+ Toast.makeText(getActivity(), getString(R.string.error_updating_gravatar), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ static public class GravatarLoadFinished {
+ public final boolean success;
+
+ public GravatarLoadFinished(boolean success) {
+ this.success = success;
+ }
+ }
+
+ public void onEventMainThread(GravatarLoadFinished event) {
+ showGravatarProgressBar(false);
+
+ if (!event.success) {
+ Toast.makeText(getActivity(), getString(R.string.error_refreshing_gravatar), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // injects a fabricated cache entry to the request cache
+ private void injectCache(File file, String avatarUrl) throws IOException {
+ final SimpleDateFormat sdf = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z");
+ final long currentTimeMs = System.currentTimeMillis();
+ final Date currentTime = new Date(currentTimeMs);
+ final long fiveMinutesLaterMs = currentTimeMs + 5 * 60 * 1000;
+ final Date fiveMinutesLater = new Date(fiveMinutesLaterMs);
+
+ Cache.Entry entry = new Cache.Entry();
+
+ entry.data = new byte[(int) file.length()];
+ DataInputStream dis = new DataInputStream(new FileInputStream(file));
+ dis.readFully(entry.data);
+ dis.close();
+
+ entry.etag = null;
+ entry.softTtl = fiveMinutesLaterMs;
+ entry.ttl = fiveMinutesLaterMs;
+ entry.serverDate = currentTimeMs;
+ entry.lastModified = currentTimeMs;
+
+ entry.responseHeaders = new TreeMap<>();
+ entry.responseHeaders.put("Accept-Ranges", "bytes");
+ entry.responseHeaders.put("Access-Control-Allow-Origin", "*");
+ entry.responseHeaders.put("Cache-Control", "max-age=300");
+ entry.responseHeaders.put("Content-Disposition", "inline; filename=\"" + AccountHelper.getDefaultAccount()
+ .getAvatarUrl() + ".jpeg\"");
+ entry.responseHeaders.put("Content-Length", String.valueOf(file.length()));
+ entry.responseHeaders.put("Content-Type", "image/jpeg");
+ entry.responseHeaders.put("Date", sdf.format(currentTime));
+ entry.responseHeaders.put("Expires", sdf.format(fiveMinutesLater));
+ entry.responseHeaders.put("Last-Modified", sdf.format(currentTime));
+ entry.responseHeaders.put("Link", "<" + avatarUrl + ">; rel=\"canonical\"");
+ entry.responseHeaders.put("Server", "injected cache");
+ entry.responseHeaders.put("Source-Age", "0");
+ entry.responseHeaders.put("X-Android-Received-Millis", String.valueOf(currentTimeMs));
+ entry.responseHeaders.put("X-Android-Response-Source", "NETWORK 200");
+ entry.responseHeaders.put("X-Android-Selected-Protocol", "http/1.1");
+ entry.responseHeaders.put("X-Android-Sent-Millis", String.valueOf(currentTimeMs));
+
+ WordPress.requestQueue.getCache().put(Request.Method.GET + ":" + avatarUrl, entry);
+ }
+
+ private class SignOutWordPressComAsync extends AsyncTask<Void, Void, Void> {
+ WeakReference<Context> mWeakContext;
+
+ public SignOutWordPressComAsync(Context context) {
+ mWeakContext = new WeakReference<Context>(context);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ Context context = mWeakContext.get();
+ if (context != null) {
+ showDisconnectDialog(context);
+ }
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ Context context = mWeakContext.get();
+ if (context != null) {
+ WordPress.WordPressComSignOut(context);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ super.onPostExecute(aVoid);
+ if (mDisconnectProgressDialog != null && mDisconnectProgressDialog.isShowing()) {
+ mDisconnectProgressDialog.dismiss();
+ }
+ mDisconnectProgressDialog = null;
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchSuccess event) {
+ refreshAccountDetails();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java
new file mode 100644
index 000000000..7352b4e51
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java
@@ -0,0 +1,449 @@
+package org.wordpress.android.ui.main;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.ScrollView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Capability;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.accounts.BlogUtils;
+import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.ui.themes.ThemeBrowserActivity;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.ServiceUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+import org.wordpress.android.widgets.WPTextView;
+
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import de.greenrobot.event.EventBus;
+
+public class MySiteFragment extends Fragment
+ implements WPMainActivity.OnScrollToTopListener {
+
+ private static final long ALERT_ANIM_OFFSET_MS = 1000L;
+ private static final long ALERT_ANIM_DURATION_MS = 1000L;
+ public static final int HIDE_WP_ADMIN_YEAR = 2015;
+ public static final int HIDE_WP_ADMIN_MONTH = 9;
+ public static final int HIDE_WP_ADMIN_DAY = 7;
+ public static final String HIDE_WP_ADMIN_GMT_TIME_ZONE = "GMT";
+
+ private WPNetworkImageView mBlavatarImageView;
+ private WPTextView mBlogTitleTextView;
+ private WPTextView mBlogSubtitleTextView;
+ private LinearLayout mLookAndFeelHeader;
+ private RelativeLayout mThemesContainer;
+ private RelativeLayout mPeopleView;
+ private RelativeLayout mPlanContainer;
+ private View mConfigurationHeader;
+ private View mSettingsView;
+ private RelativeLayout mAdminView;
+ private View mFabView;
+ private LinearLayout mNoSiteView;
+ private ScrollView mScrollView;
+ private ImageView mNoSiteDrakeImageView;
+ private WPTextView mCurrentPlanNameTextView;
+
+ private int mFabTargetYTranslation;
+ private int mBlavatarSz;
+
+ private int mBlogLocalId = BlogUtils.BLOG_ID_INVALID;
+
+ public static MySiteFragment newInstance() {
+ return new MySiteFragment();
+ }
+
+ public void setBlog(@Nullable final Blog blog) {
+ mBlogLocalId = BlogUtils.getBlogLocalId(blog);
+
+ refreshBlogDetails(blog);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mBlogLocalId = BlogUtils.getBlogLocalId(WordPress.getCurrentBlog());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Blog blog = WordPress.getBlog(mBlogLocalId);
+
+ // Site details may have changed (e.g. via Settings and returning to this Fragment) so update the UI
+ refreshBlogDetails(blog);
+
+ if (ServiceUtils.isServiceRunning(getActivity(), StatsService.class)) {
+ getActivity().stopService(new Intent(getActivity(), StatsService.class));
+ }
+ // redisplay hidden fab after a short delay
+ long delayMs = getResources().getInteger(R.integer.fab_animation_delay);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isAdded()
+ && blog != null
+ && (mFabView.getVisibility() != View.VISIBLE || mFabView.getTranslationY() != 0)) {
+ AniUtils.showFab(mFabView, true);
+ }
+ }
+ }, delayMs);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.my_site_fragment, container, false);
+
+ int fabHeight = getResources().getDimensionPixelSize(android.support.design.R.dimen.design_fab_size_normal);
+ int fabMargin = getResources().getDimensionPixelSize(R.dimen.fab_margin);
+ mFabTargetYTranslation = (fabHeight + fabMargin) * 2;
+ mBlavatarSz = getResources().getDimensionPixelSize(R.dimen.blavatar_sz_small);
+
+ mBlavatarImageView = (WPNetworkImageView) rootView.findViewById(R.id.my_site_blavatar);
+ mBlogTitleTextView = (WPTextView) rootView.findViewById(R.id.my_site_title_label);
+ mBlogSubtitleTextView = (WPTextView) rootView.findViewById(R.id.my_site_subtitle_label);
+ mLookAndFeelHeader = (LinearLayout) rootView.findViewById(R.id.my_site_look_and_feel_header);
+ mThemesContainer = (RelativeLayout) rootView.findViewById(R.id.row_themes);
+ mPeopleView = (RelativeLayout) rootView.findViewById(R.id.row_people);
+ mPlanContainer = (RelativeLayout) rootView.findViewById(R.id.row_plan);
+ mConfigurationHeader = rootView.findViewById(R.id.row_configuration);
+ mSettingsView = rootView.findViewById(R.id.row_settings);
+ mAdminView = (RelativeLayout) rootView.findViewById(R.id.row_admin);
+ mScrollView = (ScrollView) rootView.findViewById(R.id.scroll_view);
+ mNoSiteView = (LinearLayout) rootView.findViewById(R.id.no_site_view);
+ mNoSiteDrakeImageView = (ImageView) rootView.findViewById(R.id.my_site_no_site_view_drake);
+ mFabView = rootView.findViewById(R.id.fab_button);
+ mCurrentPlanNameTextView = (WPTextView) rootView.findViewById(R.id.my_site_current_plan_text_view);
+
+ // hide the FAB the first time the fragment is created in order to animate it in onResume()
+ if (savedInstanceState == null) {
+ mFabView.setVisibility(View.INVISIBLE);
+ }
+
+ mFabView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.addNewBlogPostOrPageForResult(getActivity(), WordPress.getBlog(mBlogLocalId), false);
+ }
+ });
+
+ rootView.findViewById(R.id.switch_site).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showSitePicker();
+ }
+ });
+
+ rootView.findViewById(R.id.row_view_site).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentSite(getActivity(), WordPress.getBlog(mBlogLocalId));
+ }
+ });
+
+ rootView.findViewById(R.id.row_stats).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewBlogStats(getActivity(), mBlogLocalId);
+ }
+ });
+
+ mPlanContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewBlogPlans(getActivity(), mBlogLocalId);
+ }
+ });
+
+ rootView.findViewById(R.id.row_blog_posts).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentBlogPosts(getActivity());
+ }
+ });
+
+ rootView.findViewById(R.id.row_media).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentBlogMedia(getActivity());
+ }
+ });
+
+ rootView.findViewById(R.id.row_pages).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentBlogPages(getActivity());
+ }
+ });
+
+ rootView.findViewById(R.id.row_comments).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentBlogComments(getActivity());
+ }
+ });
+
+ mThemesContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentBlogThemes(getActivity());
+ }
+ });
+
+ mPeopleView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewCurrentBlogPeople(getActivity());
+ }
+ });
+
+ mSettingsView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewBlogSettingsForResult(getActivity(), WordPress.getBlog(mBlogLocalId));
+ }
+ });
+
+ rootView.findViewById(R.id.row_admin).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityLauncher.viewBlogAdmin(getActivity(), WordPress.getBlog(mBlogLocalId));
+ }
+ });
+
+ rootView.findViewById(R.id.my_site_add_site_btn).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ SitePickerActivity.addSite(getActivity());
+ }
+ });
+
+ return rootView;
+ }
+
+ private void showSitePicker() {
+ if (isAdded()) {
+ ActivityLauncher.showSitePickerForResult(getActivity(), mBlogLocalId);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case RequestCodes.SITE_PICKER:
+ // RESULT_OK = site picker changed the current blog
+ if (resultCode == Activity.RESULT_OK) {
+ //reset comments status filter
+ AppPrefs.setCommentsStatusFilter(CommentStatus.UNKNOWN);
+ setBlog(WordPress.getCurrentBlog());
+ }
+ break;
+
+ case RequestCodes.EDIT_POST:
+ // if user returned from adding a post via the FAB and it was saved as a local
+ // draft, briefly animate the background of the "Blog posts" view to give the
+ // user a cue as to where to go to return to that post
+ if (resultCode == Activity.RESULT_OK
+ && getView() != null
+ && data != null
+ && data.getBooleanExtra(EditPostActivity.EXTRA_SAVED_AS_LOCAL_DRAFT, false)) {
+ showAlert(getView().findViewById(R.id.postsGlowBackground));
+ }
+ break;
+
+ case RequestCodes.CREATE_BLOG:
+ // user created a new blog so, use and show that new one
+ setBlog(WordPress.getCurrentBlog());
+ break;
+ }
+ }
+
+ private void showAlert(View view) {
+ if (isAdded() && view != null) {
+ Animation highlightAnimation = new AlphaAnimation(0.0f, 1.0f);
+ highlightAnimation.setInterpolator(new Interpolator() {
+ private float bounce(float t) {
+ return t * t * 24.0f;
+ }
+
+ public float getInterpolation(float t) {
+ t *= 1.1226f;
+ if (t < 0.184f) return bounce(t);
+ else if (t < 0.545f) return bounce(t - 0.40719f);
+ else if (t < 0.7275f) return -bounce(t - 0.6126f) + 1.0f;
+ else return 0.0f;
+ }
+ });
+ highlightAnimation.setStartOffset(ALERT_ANIM_OFFSET_MS);
+ highlightAnimation.setRepeatCount(1);
+ highlightAnimation.setRepeatMode(Animation.RESTART);
+ highlightAnimation.setDuration(ALERT_ANIM_DURATION_MS);
+ view.startAnimation(highlightAnimation);
+ }
+ }
+
+ private void refreshBlogDetails(@Nullable final Blog blog) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (blog == null) {
+ mScrollView.setVisibility(View.GONE);
+ mFabView.setVisibility(View.GONE);
+ mNoSiteView.setVisibility(View.VISIBLE);
+
+ // if the screen height is too short, we can just hide the drake illustration
+ Activity activity = getActivity();
+ boolean drakeVisibility = DisplayUtils.getDisplayPixelHeight(activity) >= 500;
+ if (drakeVisibility) {
+ mNoSiteDrakeImageView.setVisibility(View.VISIBLE);
+ } else {
+ mNoSiteDrakeImageView.setVisibility(View.GONE);
+ }
+
+ return;
+ }
+
+ mScrollView.setVisibility(View.VISIBLE);
+ mNoSiteView.setVisibility(View.GONE);
+
+ toggleAdminVisibility(blog);
+
+ int themesVisibility = ThemeBrowserActivity.isAccessible() ? View.VISIBLE : View.GONE;
+ mLookAndFeelHeader.setVisibility(themesVisibility);
+ mThemesContainer.setVisibility(themesVisibility);
+
+ // show settings for all self-hosted to expose Delete Site
+ boolean isAdminOrSelfHosted = blog.isAdmin() || !blog.isDotcomFlag();
+ boolean canListPeople = blog.hasCapability(Capability.LIST_USERS);
+ mSettingsView.setVisibility(isAdminOrSelfHosted ? View.VISIBLE : View.GONE);
+ mPeopleView.setVisibility(canListPeople ? View.VISIBLE : View.GONE);
+
+ // if either people or settings is visible, configuration header should be visible
+ int settingsVisibility = (isAdminOrSelfHosted || canListPeople) ? View.VISIBLE : View.GONE;
+ mConfigurationHeader.setVisibility(settingsVisibility);
+
+ mBlavatarImageView.setImageUrl(GravatarUtils.blavatarFromUrl(blog.getUrl(), mBlavatarSz), WPNetworkImageView.ImageType.BLAVATAR);
+
+ String blogName = StringUtils.unescapeHTML(blog.getBlogName());
+ String homeURL;
+ if (!TextUtils.isEmpty(blog.getHomeURL())) {
+ homeURL = UrlUtils.removeScheme(blog.getHomeURL());
+ homeURL = StringUtils.removeTrailingSlash(homeURL);
+ } else {
+ homeURL = UrlUtils.getHost(blog.getUrl());
+ }
+ String blogTitle = TextUtils.isEmpty(blogName) ? homeURL : blogName;
+
+ mBlogTitleTextView.setText(blogTitle);
+ mBlogSubtitleTextView.setText(homeURL);
+
+ // Hide the Plan item if the Plans feature is not available for this blog
+ String planShortName = blog.getPlanShortName();
+ if (!TextUtils.isEmpty(planShortName)) {
+ mCurrentPlanNameTextView.setText(planShortName);
+ mPlanContainer.setVisibility(View.VISIBLE);
+ } else {
+ mPlanContainer.setVisibility(View.GONE);
+ }
+ }
+
+ private void toggleAdminVisibility(@Nullable final Blog blog) {
+ if (blog == null) {
+ return;
+ }
+ if (shouldHideWPAdmin(blog)) {
+ mAdminView.setVisibility(View.GONE);
+ } else {
+ mAdminView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private boolean shouldHideWPAdmin(@Nullable final Blog blog) {
+ if (blog == null) {
+ return false;
+ }
+ if (!blog.isDotcomFlag()) {
+ return false;
+ } else {
+ Account account = AccountHelper.getDefaultAccount();
+
+ GregorianCalendar calendar = new GregorianCalendar(HIDE_WP_ADMIN_YEAR, HIDE_WP_ADMIN_MONTH, HIDE_WP_ADMIN_DAY);
+ calendar.setTimeZone(TimeZone.getTimeZone(HIDE_WP_ADMIN_GMT_TIME_ZONE));
+
+ Date dateCreated = account.getDateCreated();
+ return dateCreated != null && dateCreated.after(calendar.getTime());
+ }
+ }
+
+ @Override
+ public void onScrollToTop() {
+ if (isAdded()) {
+ mScrollView.smoothScrollTo(0, 0);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ /*
+ * animate the fab as the users scrolls the "My Site" page in the main activity's ViewPager
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.MainViewPagerScrolled event) {
+ mFabView.setTranslationY(mFabTargetYTranslation * event.mXOffset);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.BlogListChanged event) {
+ if (!isAdded()) {
+ return;
+ }
+
+ refreshBlogDetails(WordPress.getBlog(mBlogLocalId));
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java
new file mode 100644
index 000000000..c40ab4356
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java
@@ -0,0 +1,471 @@
+package org.wordpress.android.ui.main;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.view.ActionMode;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.SearchView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.main.SitePickerAdapter.SiteList;
+import org.wordpress.android.ui.main.SitePickerAdapter.SiteRecord;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.xmlrpc.android.ApiHelper;
+
+import de.greenrobot.event.EventBus;
+
+public class SitePickerActivity extends AppCompatActivity
+ implements SitePickerAdapter.OnSiteClickListener,
+ SitePickerAdapter.OnSelectedCountChangedListener,
+ SearchView.OnQueryTextListener {
+
+ public static final String KEY_LOCAL_ID = "local_id";
+ private static final String KEY_IS_IN_SEARCH_MODE = "is_in_search_mode";
+ private static final String KEY_LAST_SEARCH = "last_search";
+
+ private SitePickerAdapter mAdapter;
+ private RecyclerView mRecycleView;
+ private ActionMode mActionMode;
+ private MenuItem mMenuEdit;
+ private MenuItem mMenuAdd;
+ private MenuItem mMenuSearch;
+ private SearchView mSearchView;
+ private int mCurrentLocalId;
+ private boolean mDidUserSelectSite;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.site_picker_activity);
+ restoreSavedInstanceState(savedInstanceState);
+ setupActionBar();
+ setupRecycleView();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.SITE_PICKER);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(KEY_LOCAL_ID, mCurrentLocalId);
+ outState.putBoolean(KEY_IS_IN_SEARCH_MODE, getAdapter().getIsInSearchMode());
+ outState.putString(KEY_LAST_SEARCH, getAdapter().getLastSearch());
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ if (mDidUserSelectSite) {
+ overridePendingTransition(R.anim.do_nothing, R.anim.activity_slide_out_to_left);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.site_picker, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ mMenuEdit = menu.findItem(R.id.menu_edit);
+ mMenuAdd = menu.findItem(R.id.menu_add);
+ mMenuSearch = menu.findItem(R.id.menu_search);
+
+ updateMenuItemVisibility();
+ setupSearchView();
+
+ return true;
+ }
+
+ private void updateMenuItemVisibility() {
+ if (mMenuAdd == null || mMenuEdit == null || mMenuSearch == null) return;
+
+ if (getAdapter().getIsInSearchMode()) {
+ mMenuEdit.setVisible(false);
+ mMenuAdd.setVisible(false);
+ } else {
+ // don't allow editing visibility unless there are multiple wp.com blogs
+ mMenuEdit.setVisible(WordPress.wpDB.getNumDotComBlogs() > 1);
+ mMenuAdd.setVisible(true);
+ }
+
+ // no point showing search if there aren't multiple blogs
+ mMenuSearch.setVisible(WordPress.wpDB.getNumBlogs() > 1);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (itemId == R.id.menu_edit) {
+ mRecycleView.setItemAnimator(new DefaultItemAnimator());
+ getAdapter().setEnableEditMode(true);
+ startSupportActionMode(new ActionModeCallback());
+ return true;
+ } else if (itemId == R.id.menu_search) {
+ mSearchView.requestFocus();
+ showSoftKeyboard();
+ return true;
+ } else if (itemId == R.id.menu_add) {
+ addSite(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case RequestCodes.ADD_ACCOUNT:
+ case RequestCodes.CREATE_BLOG:
+ if (resultCode != RESULT_CANCELED) {
+ getAdapter().loadSites();
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.BlogListChanged event) {
+ if (!isFinishing()) {
+ getAdapter().loadSites();
+ }
+ }
+
+ private void setupRecycleView() {
+ mRecycleView = (RecyclerView) findViewById(R.id.recycler_view);
+ mRecycleView.setLayoutManager(new LinearLayoutManager(this));
+ mRecycleView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+ mRecycleView.setItemAnimator(null);
+ mRecycleView.setAdapter(getAdapter());
+ }
+
+ private void restoreSavedInstanceState(Bundle savedInstanceState) {
+ boolean isInSearchMode = false;
+ String lastSearch = "";
+
+ if (savedInstanceState != null) {
+ mCurrentLocalId = savedInstanceState.getInt(KEY_LOCAL_ID);
+ isInSearchMode = savedInstanceState.getBoolean(KEY_IS_IN_SEARCH_MODE);
+ lastSearch = savedInstanceState.getString(KEY_LAST_SEARCH);
+ } else if (getIntent() != null) {
+ mCurrentLocalId = getIntent().getIntExtra(KEY_LOCAL_ID, 0);
+ }
+
+ setNewAdapter(lastSearch, isInSearchMode);
+ }
+
+ private void setupActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ private void setIsInSearchModeAndSetNewAdapter(boolean isInSearchMode) {
+ String lastSearch = getAdapter().getLastSearch();
+ setNewAdapter(lastSearch, isInSearchMode);
+ }
+
+ private SitePickerAdapter getAdapter() {
+ if (mAdapter == null) {
+ setNewAdapter("", false);
+ }
+ return mAdapter;
+ }
+
+ private void setNewAdapter(String lastSearch, boolean isInSearchMode) {
+ mAdapter = new SitePickerAdapter(this, mCurrentLocalId, lastSearch, isInSearchMode);
+ mAdapter.setOnSiteClickListener(this);
+ mAdapter.setOnSelectedCountChangedListener(this);
+ }
+
+ private void saveHiddenSites() {
+ WordPress.wpDB.getDatabase().beginTransaction();
+ try {
+ // make all sites visible...
+ WordPress.wpDB.setAllDotComBlogsVisibility(true);
+
+ // ...then update ones marked hidden in the adapter, but don't hide the current site
+ boolean skippedCurrentSite = false;
+ String currentSiteName = null;
+ SiteList hiddenSites = getAdapter().getHiddenSites();
+ for (SiteRecord site : hiddenSites) {
+ if (site.localId == mCurrentLocalId) {
+ skippedCurrentSite = true;
+ currentSiteName = site.getBlogNameOrHomeURL();
+ } else {
+ WordPress.wpDB.setDotComBlogsVisibility(site.localId, false);
+ StatsTable.deleteStatsForBlog(this, site.localId); // Remove stats data for hidden sites
+ }
+ }
+
+ // let user know the current site wasn't hidden
+ if (skippedCurrentSite) {
+ String cantHideCurrentSite = getString(R.string.site_picker_cant_hide_current_site);
+ ToastUtils.showToast(this,
+ String.format(cantHideCurrentSite, currentSiteName),
+ ToastUtils.Duration.LONG);
+ }
+
+ WordPress.wpDB.getDatabase().setTransactionSuccessful();
+ } finally {
+ WordPress.wpDB.getDatabase().endTransaction();
+ }
+ }
+
+ private void updateActionModeTitle() {
+ if (mActionMode != null) {
+ int numSelected = getAdapter().getNumSelected();
+ String cabSelected = getString(R.string.cab_selected);
+ mActionMode.setTitle(String.format(cabSelected, numSelected));
+ }
+ }
+
+ private void setupSearchView() {
+ mSearchView = (SearchView) mMenuSearch.getActionView();
+ mSearchView.setIconifiedByDefault(false);
+ mSearchView.setOnQueryTextListener(this);
+
+ MenuItemCompat.setOnActionExpandListener(mMenuSearch, new MenuItemCompat.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ enableSearchMode();
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ disableSearchMode();
+ return true;
+ }
+ });
+
+ setQueryIfInSearch();
+ }
+
+ private void setQueryIfInSearch() {
+ if (getAdapter().getIsInSearchMode()) {
+ mMenuSearch.expandActionView();
+ mSearchView.setQuery(getAdapter().getLastSearch(), false);
+ }
+ }
+
+ private void enableSearchMode() {
+ setIsInSearchModeAndSetNewAdapter(true);
+ mRecycleView.swapAdapter(getAdapter(), true);
+ updateMenuItemVisibility();
+ }
+
+ private void disableSearchMode() {
+ hideSoftKeyboard();
+ setIsInSearchModeAndSetNewAdapter(false);
+ mRecycleView.swapAdapter(getAdapter(), true);
+ updateMenuItemVisibility();
+ }
+
+ private void hideSoftKeyboard() {
+ if (!hasHardwareKeyboard()) {
+ WPActivityUtils.hideKeyboard(mSearchView);
+ }
+ }
+
+ private void showSoftKeyboard() {
+ if (!hasHardwareKeyboard()) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ private boolean hasHardwareKeyboard() {
+ return (getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS);
+ }
+
+ @Override
+ public void onSelectedCountChanged(int numSelected) {
+ if (mActionMode != null) {
+ updateActionModeTitle();
+ mActionMode.invalidate();
+ }
+ }
+
+ @Override
+ public void onSiteClick(SiteRecord site) {
+ if (mActionMode == null) {
+ hideSoftKeyboard();
+ WordPress.setCurrentBlogAndSetVisible(site.localId);
+ WordPress.wpDB.updateLastBlogId(site.localId);
+ setResult(RESULT_OK);
+ mDidUserSelectSite = true;
+ new ApiHelper.RefreshBlogContentTask(WordPress.getCurrentBlog(), null).executeOnExecutor(
+ AsyncTask.THREAD_POOL_EXECUTOR, false);
+
+ finish();
+ }
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ hideSoftKeyboard();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ getAdapter().setLastSearch(s);
+ getAdapter().searchSites(s);
+ return true;
+ }
+
+ private final class ActionModeCallback implements ActionMode.Callback {
+ private boolean mHasChanges;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
+ mActionMode = actionMode;
+ mHasChanges = false;
+ updateActionModeTitle();
+ actionMode.getMenuInflater().inflate(R.menu.site_picker_action_mode, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
+ MenuItem mnuShow = menu.findItem(R.id.menu_show);
+ mnuShow.setEnabled(getAdapter().getNumHiddenSelected() > 0);
+
+ MenuItem mnuHide = menu.findItem(R.id.menu_hide);
+ mnuHide.setEnabled(getAdapter().getNumVisibleSelected() > 0);
+
+ MenuItem mnuSelectAll = menu.findItem(R.id.menu_select_all);
+ mnuSelectAll.setEnabled(getAdapter().getNumSelected() != getAdapter().getItemCount());
+
+ MenuItem mnuDeselectAll = menu.findItem(R.id.menu_deselect_all);
+ mnuDeselectAll.setEnabled(getAdapter().getNumSelected() > 0);
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.menu_show) {
+ getAdapter().setVisibilityForSelectedSites(true);
+ mHasChanges = true;
+ mActionMode.finish();
+ } else if (itemId == R.id.menu_hide) {
+ getAdapter().setVisibilityForSelectedSites(false);
+ mHasChanges = true;
+ mActionMode.finish();
+ } else if (itemId == R.id.menu_select_all) {
+ getAdapter().selectAll();
+ } else if (itemId == R.id.menu_deselect_all) {
+ getAdapter().deselectAll();
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode actionMode) {
+ if (mHasChanges) {
+ saveHiddenSites();
+ }
+ getAdapter().setEnableEditMode(false);
+ mActionMode = null;
+ }
+ }
+
+ public static void addSite(Activity activity) {
+ // if user is signed into wp.com use the dialog to enable choosing whether to
+ // create a new wp.com blog or add a self-hosted one
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ DialogFragment dialog = new AddSiteDialog();
+ dialog.show(activity.getFragmentManager(), AddSiteDialog.ADD_SITE_DIALOG_TAG);
+ } else {
+ // user isn't signed into wp.com, so simply enable adding self-hosted
+ ActivityLauncher.addSelfHostedSiteForResult(activity);
+ }
+ }
+
+ /*
+ * dialog which appears after user taps "Add site" - enables choosing whether to create
+ * a new wp.com blog or add an existing self-hosted one
+ */
+ public static class AddSiteDialog extends DialogFragment {
+ static final String ADD_SITE_DIALOG_TAG = "add_site_dialog";
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ CharSequence[] items =
+ {getString(R.string.site_picker_create_dotcom),
+ getString(R.string.site_picker_add_self_hosted)};
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.site_picker_add_site);
+ builder.setItems(items, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ ActivityLauncher.newBlogForResult(getActivity());
+ } else {
+ ActivityLauncher.addSelfHostedSiteForResult(getActivity());
+ }
+ }
+ });
+ return builder.create();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java
new file mode 100644
index 000000000..7267d8d5e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java
@@ -0,0 +1,508 @@
+package org.wordpress.android.ui.main;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class SitePickerAdapter extends RecyclerView.Adapter<SitePickerAdapter.SiteViewHolder> {
+
+ interface OnSiteClickListener {
+ void onSiteClick(SiteRecord site);
+ }
+
+ interface OnSelectedCountChangedListener {
+ void onSelectedCountChanged(int numSelected);
+ }
+
+ private final int mTextColorNormal;
+ private final int mTextColorHidden;
+
+ private static int mBlavatarSz;
+
+ private SiteList mSites = new SiteList();
+ private final int mCurrentLocalId;
+
+ private final Drawable mSelectedItemBackground;
+
+ private final LayoutInflater mInflater;
+ private final HashSet<Integer> mSelectedPositions = new HashSet<>();
+
+ private boolean mIsMultiSelectEnabled;
+ private final boolean mIsInSearchMode;
+ private boolean mShowHiddenSites = false;
+ private boolean mShowSelfHostedSites = true;
+ private String mLastSearch;
+ private SiteList mAllSites;
+
+ private OnSiteClickListener mSiteSelectedListener;
+ private OnSelectedCountChangedListener mSelectedCountListener;
+
+ class SiteViewHolder extends RecyclerView.ViewHolder {
+ private final ViewGroup layoutContainer;
+ private final TextView txtTitle;
+ private final TextView txtDomain;
+ private final WPNetworkImageView imgBlavatar;
+ private final View divider;
+ private Boolean isSiteHidden;
+
+ public SiteViewHolder(View view) {
+ super(view);
+ layoutContainer = (ViewGroup) view.findViewById(R.id.layout_container);
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtDomain = (TextView) view.findViewById(R.id.text_domain);
+ imgBlavatar = (WPNetworkImageView) view.findViewById(R.id.image_blavatar);
+ divider = view.findViewById(R.id.divider);
+ isSiteHidden = null;
+
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ int clickedPosition = getAdapterPosition();
+ if (isValidPosition(clickedPosition)) {
+ if (mIsMultiSelectEnabled) {
+ toggleSelection(clickedPosition);
+ } else if (mSiteSelectedListener != null) {
+ mSiteSelectedListener.onSiteClick(getItem(clickedPosition));
+ }
+ } else {
+ AppLog.w(AppLog.T.MAIN, "site picker > invalid clicked position " + clickedPosition);
+ }
+ }
+ });
+ }
+ }
+
+ public SitePickerAdapter(Context context, int currentLocalBlogId, String lastSearch, boolean isInSearchMode) {
+ super();
+
+ setHasStableIds(true);
+
+ mLastSearch = StringUtils.notNullStr(lastSearch);
+ mAllSites = new SiteList();
+ mIsInSearchMode = isInSearchMode;
+ mCurrentLocalId = currentLocalBlogId;
+ mInflater = LayoutInflater.from(context);
+
+ mBlavatarSz = context.getResources().getDimensionPixelSize(R.dimen.blavatar_sz);
+ mTextColorNormal = context.getResources().getColor(R.color.grey_dark);
+ mTextColorHidden = context.getResources().getColor(R.color.grey);
+
+ mSelectedItemBackground = new ColorDrawable(context.getResources().getColor(R.color.translucent_grey_lighten_20));
+
+ loadSites();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSites.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).localId;
+ }
+
+ private SiteRecord getItem(int position) {
+ return mSites.get(position);
+ }
+
+ void setOnSelectedCountChangedListener(OnSelectedCountChangedListener listener) {
+ mSelectedCountListener = listener;
+ }
+
+ public void setOnSiteClickListener(OnSiteClickListener listener) {
+ mSiteSelectedListener = listener;
+ }
+
+ @Override
+ public SiteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = mInflater.inflate(R.layout.site_picker_listitem, parent, false);
+ return new SiteViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(SiteViewHolder holder, int position) {
+ SiteRecord site = getItem(position);
+
+ holder.txtTitle.setText(site.getBlogNameOrHomeURL());
+ holder.txtDomain.setText(site.homeURL);
+ holder.imgBlavatar.setImageUrl(site.blavatarUrl, WPNetworkImageView.ImageType.BLAVATAR);
+
+ if (site.localId == mCurrentLocalId || (mIsMultiSelectEnabled && isItemSelected(position))) {
+ holder.layoutContainer.setBackgroundDrawable(mSelectedItemBackground);
+ } else {
+ holder.layoutContainer.setBackgroundDrawable(null);
+ }
+
+ // different styling for visible/hidden sites
+ if (holder.isSiteHidden == null || holder.isSiteHidden != site.isHidden) {
+ holder.isSiteHidden = site.isHidden;
+ holder.txtTitle.setTextColor(site.isHidden ? mTextColorHidden : mTextColorNormal);
+ holder.txtTitle.setTypeface(holder.txtTitle.getTypeface(), site.isHidden ? Typeface.NORMAL : Typeface.BOLD);
+ holder.imgBlavatar.setAlpha(site.isHidden ? 0.5f : 1f);
+ }
+
+ // hide the divider for the last item
+ boolean isLastItem = (position == getItemCount() - 1);
+ holder.divider.setVisibility(isLastItem ? View.INVISIBLE : View.VISIBLE);
+ }
+
+ public String getLastSearch() {
+ return mLastSearch;
+ }
+
+ public void setLastSearch(String lastSearch) {
+ mLastSearch = lastSearch;
+ }
+
+ public boolean getIsInSearchMode() {
+ return mIsInSearchMode;
+ }
+
+ public void searchSites(String searchText) {
+ mLastSearch = searchText;
+ mSites = filteredSitesByText(mAllSites);
+
+ notifyDataSetChanged();
+ }
+
+ private boolean isValidPosition(int position) {
+ return (position >= 0 && position < mSites.size());
+ }
+
+ /*
+ * called when the user chooses to edit the visibility of wp.com blogs
+ */
+ void setEnableEditMode(boolean enable) {
+ if (mIsMultiSelectEnabled == enable) return;
+
+ if (enable) {
+ mShowHiddenSites = true;
+ mShowSelfHostedSites = false;
+ } else {
+ mShowHiddenSites = false;
+ mShowSelfHostedSites = true;
+ }
+
+ mIsMultiSelectEnabled = enable;
+ mSelectedPositions.clear();
+
+ loadSites();
+ }
+
+ int getNumSelected() {
+ return mSelectedPositions.size();
+ }
+
+ int getNumHiddenSelected() {
+ int numHidden = 0;
+ for (Integer i: mSelectedPositions) {
+ if (mSites.get(i).isHidden) {
+ numHidden++;
+ }
+ }
+ return numHidden;
+ }
+
+ int getNumVisibleSelected() {
+ int numVisible = 0;
+ for (Integer i: mSelectedPositions) {
+ if (!mSites.get(i).isHidden) {
+ numVisible++;
+ }
+ }
+ return numVisible;
+ }
+
+ private void toggleSelection(int position) {
+ setItemSelected(position, !isItemSelected(position));
+ }
+
+ private boolean isItemSelected(int position) {
+ return mSelectedPositions.contains(position);
+ }
+
+ private void setItemSelected(int position, boolean isSelected) {
+ if (isItemSelected(position) == isSelected) {
+ return;
+ }
+
+ if (isSelected) {
+ mSelectedPositions.add(position);
+ } else {
+ mSelectedPositions.remove(position);
+ }
+ notifyItemChanged(position);
+
+ if (mSelectedCountListener != null) {
+ mSelectedCountListener.onSelectedCountChanged(getNumSelected());
+ }
+ }
+
+ void selectAll() {
+ if (mSelectedPositions.size() == mSites.size()) return;
+
+ mSelectedPositions.clear();
+ for (int i = 0; i < mSites.size(); i++) {
+ mSelectedPositions.add(i);
+ }
+ notifyDataSetChanged();
+
+ if (mSelectedCountListener != null) {
+ mSelectedCountListener.onSelectedCountChanged(getNumSelected());
+ }
+ }
+
+ void deselectAll() {
+ if (mSelectedPositions.size() == 0) return;
+
+ mSelectedPositions.clear();
+ notifyDataSetChanged();
+
+ if (mSelectedCountListener != null) {
+ mSelectedCountListener.onSelectedCountChanged(getNumSelected());
+ }
+ }
+
+ private SiteList getSelectedSites() {
+ SiteList sites = new SiteList();
+ if (!mIsMultiSelectEnabled) {
+ return sites;
+ }
+
+ for (Integer position : mSelectedPositions) {
+ if (isValidPosition(position))
+ sites.add(mSites.get(position));
+ }
+
+ return sites;
+ }
+
+ SiteList getHiddenSites() {
+ SiteList hiddenSites = new SiteList();
+ for (SiteRecord site: mSites) {
+ if (site.isHidden) {
+ hiddenSites.add(site);
+ }
+ }
+
+ return hiddenSites;
+ }
+
+ void setVisibilityForSelectedSites(boolean makeVisible) {
+ SiteList sites = getSelectedSites();
+ if (sites != null && sites.size() > 0) {
+ for (SiteRecord site: sites) {
+ int index = mSites.indexOfSite(site);
+ if (index > -1) {
+ mSites.get(index).isHidden = !makeVisible;
+ }
+ }
+ }
+ }
+
+ void loadSites() {
+ if (mIsTaskRunning) {
+ AppLog.w(AppLog.T.UTILS, "site picker > already loading sites");
+ } else {
+ new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ private SiteList filteredSitesByTextIfInSearchMode(SiteList sites) {
+ if (!mIsInSearchMode) {
+ return sites;
+ } else {
+ return filteredSitesByText(sites);
+ }
+ }
+
+ private SiteList filteredSitesByText(SiteList sites) {
+ SiteList filteredSiteList = new SiteList();
+
+ for (int i = 0; i < sites.size(); i++) {
+ SiteRecord record = sites.get(i);
+ String siteNameLowerCase = record.blogName.toLowerCase();
+ String hostNameLowerCase = record.homeURL.toLowerCase();
+
+ if (siteNameLowerCase.contains(mLastSearch.toLowerCase()) || hostNameLowerCase.contains(mLastSearch.toLowerCase())) {
+ filteredSiteList.add(record);
+ }
+ }
+
+ return filteredSiteList;
+ }
+
+ /*
+ * AsyncTask which loads sites from database and populates the adapter
+ */
+ private boolean mIsTaskRunning;
+ private class LoadSitesTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ List<Map<String, Object>> blogs;
+ String[] extraFields = {"isHidden", "dotcomFlag", "homeURL"};
+
+ if (mIsInSearchMode) {
+ blogs = WordPress.wpDB.getBlogsBy(null, extraFields);
+ } else {
+ blogs = getBlogsForCurrentView(extraFields);
+ }
+
+ SiteList sites = new SiteList(blogs);
+
+ // sort by blog/host
+ final long primaryBlogId = AccountHelper.getDefaultAccount().getPrimaryBlogId();
+ Collections.sort(sites, new Comparator<SiteRecord>() {
+ public int compare(SiteRecord site1, SiteRecord site2) {
+ if (primaryBlogId > 0) {
+ if (site1.blogId == primaryBlogId) {
+ return -1;
+ } else if (site2.blogId == primaryBlogId) {
+ return 1;
+ }
+ }
+ return site1.getBlogNameOrHomeURL().compareToIgnoreCase(site2.getBlogNameOrHomeURL());
+ }
+ });
+
+ if (mSites == null || !mSites.isSameList(sites)) {
+ mAllSites = (SiteList) sites.clone();
+ mSites = filteredSitesByTextIfInSearchMode(sites);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void results) {
+ notifyDataSetChanged();
+ mIsTaskRunning = false;
+ }
+
+ private List<Map<String, Object>> getBlogsForCurrentView(String[] extraFields) {
+ if (mShowHiddenSites) {
+ if (mShowSelfHostedSites) {
+ // all self-hosted blogs and all wp.com blogs
+ return WordPress.wpDB.getBlogsBy(null, extraFields);
+ } else {
+ // only wp.com blogs
+ return WordPress.wpDB.getBlogsBy("dotcomFlag=1", extraFields);
+ }
+ } else {
+ if (mShowSelfHostedSites) {
+ // all self-hosted blogs plus visible wp.com blogs
+ return WordPress.wpDB.getBlogsBy("dotcomFlag=0 OR (isHidden=0 AND dotcomFlag=1) ", extraFields);
+ } else {
+ // only visible wp.com blogs
+ return WordPress.wpDB.getBlogsBy("isHidden=0 AND dotcomFlag=1", extraFields);
+ }
+ }
+ }
+ }
+
+ /**
+ * SiteRecord is a simplified version of the full account (blog) record
+ */
+ static class SiteRecord {
+ final int localId;
+ final int blogId;
+ final String blogName;
+ final String homeURL;
+ final String url;
+ final String blavatarUrl;
+ final boolean isDotCom;
+ boolean isHidden;
+
+ SiteRecord(Map<String, Object> account) {
+ localId = MapUtils.getMapInt(account, "id");
+ blogId = MapUtils.getMapInt(account, "blogId");
+ blogName = BlogUtils.getBlogNameOrHomeURLFromAccountMap(account);
+ homeURL = BlogUtils.getHomeURLOrHostNameFromAccountMap(account);
+ url = MapUtils.getMapStr(account, "url");
+ blavatarUrl = GravatarUtils.blavatarFromUrl(url, mBlavatarSz);
+ isDotCom = MapUtils.getMapBool(account, "dotcomFlag");
+ isHidden = MapUtils.getMapBool(account, "isHidden");
+ }
+
+ String getBlogNameOrHomeURL() {
+ if (TextUtils.isEmpty(blogName)) {
+ return homeURL;
+ }
+ return blogName;
+ }
+ }
+
+ static class SiteList extends ArrayList<SiteRecord> {
+ SiteList() { }
+ SiteList(List<Map<String, Object>> accounts) {
+ if (accounts != null) {
+ for (Map<String, Object> account : accounts) {
+ add(new SiteRecord(account));
+ }
+ }
+ }
+
+ boolean isSameList(SiteList sites) {
+ if (sites == null || sites.size() != this.size()) {
+ return false;
+ }
+ int i;
+ for (SiteRecord site: sites) {
+ i = indexOfSite(site);
+ if (i == -1 || this.get(i).isHidden != site.isHidden) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ int indexOfSite(SiteRecord site) {
+ if (site != null && site.blogId > 0) {
+ for (int i = 0; i < size(); i++) {
+ if (site.blogId == this.get(i).blogId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
new file mode 100644
index 000000000..a29e624f1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
@@ -0,0 +1,638 @@
+package org.wordpress.android.ui.main;
+
+import android.animation.ObjectAnimator;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.design.widget.TabLayout;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import com.simperium.client.Bucket;
+import com.simperium.client.BucketObjectMissingException;
+
+import org.wordpress.android.GCMMessageService;
+import org.wordpress.android.GCMRegistrationIntentService;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.networking.ConnectionChangeReceiver;
+import org.wordpress.android.networking.SelfSignedSSLCertsManager;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.accounts.login.MagicLinkSignInActivity;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+import org.wordpress.android.ui.notifications.NotificationsListFragment;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.ui.posts.PromoDialog;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.prefs.AppSettingsFragment;
+import org.wordpress.android.ui.prefs.SiteSettingsFragment;
+import org.wordpress.android.ui.reader.ReaderPostListFragment;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.AuthenticationDialogUtils;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.CoreEvents.MainViewPagerScrolled;
+import org.wordpress.android.util.CoreEvents.UserSignedOutCompletely;
+import org.wordpress.android.util.CoreEvents.UserSignedOutWordPressCom;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ProfilingUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.WPViewPager;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Main activity which hosts sites, reader, me and notifications tabs
+ */
+public class WPMainActivity extends AppCompatActivity implements Bucket.Listener<Note> {
+
+ private WPViewPager mViewPager;
+ private WPMainTabLayout mTabLayout;
+ private WPMainTabAdapter mTabAdapter;
+ private TextView mConnectionBar;
+ private int mAppBarElevation;
+
+ public static final String ARG_OPENED_FROM_PUSH = "opened_from_push";
+
+ /*
+ * tab fragments implement this if their contents can be scrolled, called when user
+ * requests to scroll to the top
+ */
+ public interface OnScrollToTopListener {
+ void onScrollToTop();
+ }
+
+ /*
+ * tab fragments implement this and return true if the fragment handles the back button
+ * and doesn't want the activity to handle it as well
+ */
+ public interface OnActivityBackPressedListener {
+ boolean onActivityBackPressed();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ ProfilingUtils.split("WPMainActivity.onCreate");
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+
+ mViewPager = (WPViewPager) findViewById(R.id.viewpager_main);
+ mViewPager.setOffscreenPageLimit(WPMainTabAdapter.NUM_TABS - 1);
+
+ mTabAdapter = new WPMainTabAdapter(getFragmentManager());
+ mViewPager.setAdapter(mTabAdapter);
+
+ mConnectionBar = (TextView) findViewById(R.id.connection_bar);
+ mConnectionBar.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // slide out the bar on click, then re-check connection after a brief delay
+ AniUtils.animateBottomBar(mConnectionBar, false);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ checkConnection();
+ }
+ }
+ }, 2000);
+ }
+ });
+ mTabLayout = (WPMainTabLayout) findViewById(R.id.tab_layout);
+ mTabLayout.createTabs();
+
+ mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ mViewPager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ // nop
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ //scroll the active fragment's contents to the top when user taps the current tab
+ Fragment fragment = mTabAdapter.getFragment(tab.getPosition());
+ if (fragment instanceof OnScrollToTopListener) {
+ ((OnScrollToTopListener) fragment).onScrollToTop();
+ }
+ }
+ });
+
+ mAppBarElevation = getResources().getDimensionPixelSize(R.dimen.appbar_elevation);
+
+ mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout));
+ mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ AppPrefs.setMainTabIndex(position);
+
+ switch (position) {
+ case WPMainTabAdapter.TAB_MY_SITE:
+ setTabLayoutElevation(mAppBarElevation);
+ break;
+ case WPMainTabAdapter.TAB_READER:
+ setTabLayoutElevation(0);
+ break;
+ case WPMainTabAdapter.TAB_ME:
+ setTabLayoutElevation(mAppBarElevation);
+ break;
+ case WPMainTabAdapter.TAB_NOTIFS:
+ setTabLayoutElevation(mAppBarElevation);
+ new UpdateLastSeenTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ break;
+ }
+
+ trackLastVisibleTab(position, true);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // noop
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ // fire event if the "My Site" page is being scrolled so the fragment can
+ // animate its fab to match
+ if (position == WPMainTabAdapter.TAB_MY_SITE) {
+ EventBus.getDefault().post(new MainViewPagerScrolled(positionOffset));
+ }
+ }
+ });
+
+ if (savedInstanceState == null) {
+ if (AccountHelper.isSignedIn()) {
+ // open note detail if activity called from a push, otherwise return to the tab
+ // that was showing last time
+ boolean openedFromPush = (getIntent() != null && getIntent().getBooleanExtra(ARG_OPENED_FROM_PUSH,
+ false));
+ if (openedFromPush) {
+ getIntent().putExtra(ARG_OPENED_FROM_PUSH, false);
+ launchWithNoteId();
+ } else {
+ int position = AppPrefs.getMainTabIndex();
+ if (mTabAdapter.isValidPosition(position) && position != mViewPager.getCurrentItem()) {
+ mViewPager.setCurrentItem(position);
+ }
+ checkMagicLinkSignIn();
+ }
+ } else {
+ ActivityLauncher.showSignInForResult(this);
+ }
+ }
+ }
+
+ private void setTabLayoutElevation(float newElevation){
+ if (mTabLayout == null) return;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ float oldElevation = mTabLayout.getElevation();
+ if (oldElevation != newElevation) {
+ ObjectAnimator.ofFloat(mTabLayout, "elevation", oldElevation, newElevation)
+ .setDuration(1000L)
+ .start();
+ }
+ }
+ }
+
+ private void showVisualEditorPromoDialogIfNeeded() {
+ if (AppPrefs.isVisualEditorPromoRequired() && AppPrefs.isVisualEditorEnabled()) {
+ DialogFragment newFragment = PromoDialog.newInstance(R.drawable.new_editor_promo_header,
+ R.string.new_editor_promo_title, R.string.new_editor_promo_desc,
+ R.string.new_editor_promo_button_label);
+ newFragment.show(getFragmentManager(), "visual-editor-promo");
+ AppPrefs.setVisualEditorPromoRequired(false);
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ AppLog.i(T.MAIN, "main activity > new intent");
+ if (intent.hasExtra(NotificationsListFragment.NOTE_ID_EXTRA)) {
+ launchWithNoteId();
+ }
+ }
+
+ /*
+ * called when app is launched from a push notification, switches to the notification tab
+ * and opens the desired note detail
+ */
+ private void launchWithNoteId() {
+ if (isFinishing() || getIntent() == null) return;
+
+ // Check for push authorization request
+ if (getIntent().hasExtra(NotificationsUtils.ARG_PUSH_AUTH_TOKEN)) {
+ Bundle extras = getIntent().getExtras();
+ String token = extras.getString(NotificationsUtils.ARG_PUSH_AUTH_TOKEN, "");
+ String title = extras.getString(NotificationsUtils.ARG_PUSH_AUTH_TITLE, "");
+ String message = extras.getString(NotificationsUtils.ARG_PUSH_AUTH_MESSAGE, "");
+ long expires = extras.getLong(NotificationsUtils.ARG_PUSH_AUTH_EXPIRES, 0);
+
+ long now = System.currentTimeMillis() / 1000;
+ if (expires > 0 && now > expires) {
+ // Show a toast if the user took too long to open the notification
+ ToastUtils.showToast(this, R.string.push_auth_expired, ToastUtils.Duration.LONG);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_EXPIRED);
+ } else {
+ NotificationsUtils.showPushAuthAlert(this, token, title, message);
+ }
+ }
+
+ mViewPager.setCurrentItem(WPMainTabAdapter.TAB_NOTIFS);
+
+ if (GCMMessageService.getNotificationsCount() == 1) {
+ String noteId = getIntent().getStringExtra(NotificationsListFragment.NOTE_ID_EXTRA);
+ if (!TextUtils.isEmpty(noteId)) {
+ GCMMessageService.bumpPushNotificationsTappedAnalytics(noteId);
+ boolean doLikeNote = getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_LIKE_EXTRA, false);
+ if (doLikeNote) {
+ NotificationsListFragment.openNoteForLike(this, noteId);
+ } else {
+ boolean doApproveNote = getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_APPROVE_EXTRA, false);
+ if (doApproveNote) {
+ NotificationsListFragment.openNoteForApprove(this, noteId);
+ } else {
+ boolean shouldShowKeyboard = getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA, false);
+ NotificationsListFragment.openNoteForReply(this, noteId, shouldShowKeyboard);
+ }
+ }
+ }
+ } else {
+ // mark all tapped here
+ GCMMessageService.bumpPushNotificationsTappedAllAnalytics();
+ }
+
+ GCMMessageService.removeAllNotifications(this);
+ }
+
+ @Override
+ protected void onPause() {
+ if (SimperiumUtils.getNotesBucket() != null) {
+ SimperiumUtils.getNotesBucket().removeListener(this);
+ }
+
+ super.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Start listening to Simperium Note bucket
+ if (SimperiumUtils.getNotesBucket() != null) {
+ SimperiumUtils.getNotesBucket().addListener(this);
+ }
+ mTabLayout.checkNoteBadge();
+
+ // We need to track the current item on the screen when this activity is resumed.
+ // Ex: Notifications -> notifications detail -> back to notifications
+ trackLastVisibleTab(mViewPager.getCurrentItem(), false);
+
+ checkConnection();
+
+ ProfilingUtils.split("WPMainActivity.onResume");
+ ProfilingUtils.dump();
+ ProfilingUtils.stop();
+ }
+
+ @Override
+ public void onBackPressed() {
+ // let the fragment handle the back button if it implements our OnParentBackPressedListener
+ Fragment fragment = getActiveFragment();
+ if (fragment instanceof OnActivityBackPressedListener) {
+ boolean handled = ((OnActivityBackPressedListener) fragment).onActivityBackPressed();
+ if (handled) {
+ return;
+ }
+ }
+ super.onBackPressed();
+ }
+
+ private Fragment getActiveFragment() {
+ return mTabAdapter.getFragment(mViewPager.getCurrentItem());
+ }
+
+ private void checkMagicLinkSignIn() {
+ if (getIntent() != null) {
+ if (getIntent().getBooleanExtra(MagicLinkSignInActivity.MAGIC_LOGIN, false)) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_MAGIC_LINK_SUCCEEDED);
+ startWithNewAccount();
+ }
+ }
+ }
+
+ private void trackLastVisibleTab(int position, boolean trackAnalytics) {
+ if (position == WPMainTabAdapter.TAB_MY_SITE) {
+ showVisualEditorPromoDialogIfNeeded();
+ }
+ switch (position) {
+ case WPMainTabAdapter.TAB_MY_SITE:
+ ActivityId.trackLastActivity(ActivityId.MY_SITE);
+ if (trackAnalytics) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.MY_SITE_ACCESSED);
+ }
+ break;
+ case WPMainTabAdapter.TAB_READER:
+ ActivityId.trackLastActivity(ActivityId.READER);
+ if (trackAnalytics) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_ACCESSED);
+ }
+ break;
+ case WPMainTabAdapter.TAB_ME:
+ ActivityId.trackLastActivity(ActivityId.ME);
+ if (trackAnalytics) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.ME_ACCESSED);
+ }
+ break;
+ case WPMainTabAdapter.TAB_NOTIFS:
+ ActivityId.trackLastActivity(ActivityId.NOTIFICATIONS);
+ if (trackAnalytics) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATIONS_ACCESSED);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ public void setReaderTabActive() {
+ if (isFinishing() || mTabLayout == null) return;
+
+ mTabLayout.setSelectedTabPosition(WPMainTabAdapter.TAB_READER);
+ }
+
+ /*
+ * re-create the fragment adapter so all its fragments are also re-created - used when
+ * user signs in/out so the fragments reflect the active account
+ */
+ private void resetFragments() {
+ AppLog.i(AppLog.T.MAIN, "main activity > reset fragments");
+
+ // reset the timestamp that determines when followed tags/blogs are updated so they're
+ // updated when the fragment is recreated (necessary after signin/disconnect)
+ ReaderPostListFragment.resetLastUpdateDate();
+
+ // remember the current tab position, then recreate the adapter so new fragments are created
+ int position = mViewPager.getCurrentItem();
+ mTabAdapter = new WPMainTabAdapter(getFragmentManager());
+ mViewPager.setAdapter(mTabAdapter);
+
+ // restore previous position
+ if (mTabAdapter.isValidPosition(position)) {
+ mViewPager.setCurrentItem(position);
+ }
+ }
+
+ private void moderateCommentOnActivityResult(Intent data) {
+ try {
+ if (SimperiumUtils.getNotesBucket() != null) {
+ Note note = SimperiumUtils.getNotesBucket().get(StringUtils.notNullStr(data.getStringExtra
+ (NotificationsListFragment.NOTE_MODERATE_ID_EXTRA)));
+ CommentStatus status = CommentStatus.fromString(data.getStringExtra(
+ NotificationsListFragment.NOTE_MODERATE_STATUS_EXTRA));
+ NotificationsUtils.moderateCommentForNote(note, status, findViewById(R.id.root_view_main));
+ }
+ } catch (BucketObjectMissingException e) {
+ AppLog.e(T.NOTIFS, e);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ switch (requestCode) {
+ case RequestCodes.EDIT_POST:
+ case RequestCodes.CREATE_BLOG:
+ if (resultCode == RESULT_OK) {
+ MySiteFragment mySiteFragment = getMySiteFragment();
+ if (mySiteFragment != null) {
+ mySiteFragment.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ break;
+ case RequestCodes.ADD_ACCOUNT:
+ if (resultCode == RESULT_OK) {
+ // Register for Cloud messaging
+ startWithNewAccount();
+ } else if (!AccountHelper.isSignedIn()) {
+ // can't do anything if user isn't signed in (either to wp.com or self-hosted)
+ finish();
+ }
+ break;
+ case RequestCodes.REAUTHENTICATE:
+ if (resultCode == RESULT_CANCELED) {
+ ActivityLauncher.showSignInForResult(this);
+ } else {
+ // Register for Cloud messaging
+ startService(new Intent(this, GCMRegistrationIntentService.class));
+ }
+ break;
+ case RequestCodes.NOTE_DETAIL:
+ if (resultCode == RESULT_OK && data != null) {
+ moderateCommentOnActivityResult(data);
+ }
+ break;
+ case RequestCodes.SITE_PICKER:
+ if (getMySiteFragment() != null) {
+ getMySiteFragment().onActivityResult(requestCode, resultCode, data);
+ }
+ break;
+ case RequestCodes.BLOG_SETTINGS:
+ if (resultCode == SiteSettingsFragment.RESULT_BLOG_REMOVED) {
+ handleBlogRemoved();
+ }
+ break;
+ case RequestCodes.APP_SETTINGS:
+ if (resultCode == AppSettingsFragment.LANGUAGE_CHANGED) {
+ resetFragments();
+ }
+ break;
+ }
+ }
+
+ private void startWithNewAccount() {
+ startService(new Intent(this, GCMRegistrationIntentService.class));
+ resetFragments();
+ }
+
+ /*
+ * returns the my site fragment from the sites tab
+ */
+ private MySiteFragment getMySiteFragment() {
+ Fragment fragment = mTabAdapter.getFragment(WPMainTabAdapter.TAB_MY_SITE);
+ if (fragment instanceof MySiteFragment) {
+ return (MySiteFragment) fragment;
+ }
+ return null;
+ }
+
+ // Updates `last_seen` notifications flag in Simperium and removes tab indicator
+ private class UpdateLastSeenTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ return SimperiumUtils.updateLastSeenTime();
+ }
+
+ @Override
+ protected void onPostExecute(Boolean lastSeenTimeUpdated) {
+ if (isFinishing()) return;
+
+ if (lastSeenTimeUpdated) {
+ mTabLayout.showNoteBadge(false);
+ }
+ }
+ }
+
+ // Events
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(UserSignedOutWordPressCom event) {
+ resetFragments();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(UserSignedOutCompletely event) {
+ ActivityLauncher.showSignInForResult(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.InvalidCredentialsDetected event) {
+ AuthenticationDialogUtils.showAuthErrorView(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.RestApiUnauthorized event) {
+ AuthenticationDialogUtils.showAuthErrorView(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.TwoFactorAuthenticationDetected event) {
+ AuthenticationDialogUtils.showAuthErrorView(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.InvalidSslCertificateDetected event) {
+ SelfSignedSSLCertsManager.askForSslTrust(this, null);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CoreEvents.LoginLimitDetected event) {
+ ToastUtils.showToast(this, R.string.limit_reached, ToastUtils.Duration.LONG);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(NotificationEvents.NotificationsChanged event) {
+ mTabLayout.checkNoteBadge();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) {
+ updateConnectionBar(event.isConnected());
+ }
+
+ private void checkConnection() {
+ updateConnectionBar(NetworkUtils.isNetworkAvailable(this));
+ }
+
+ private void updateConnectionBar(boolean isConnected) {
+ if (isConnected && mConnectionBar.getVisibility() == View.VISIBLE) {
+ AniUtils.animateBottomBar(mConnectionBar, false);
+ } else if (!isConnected && mConnectionBar.getVisibility() != View.VISIBLE) {
+ AniUtils.animateBottomBar(mConnectionBar, true);
+ }
+ }
+
+ private void handleBlogRemoved() {
+ if (!AccountHelper.isSignedIn()) {
+ ActivityLauncher.showSignInForResult(this);
+ } else {
+ Blog blog = WordPress.getCurrentBlog();
+ MySiteFragment mySiteFragment = getMySiteFragment();
+ if (mySiteFragment != null) {
+ mySiteFragment.setBlog(blog);
+ }
+
+ if (blog != null) {
+ int blogId = blog.getLocalTableBlogId();
+ ActivityLauncher.showSitePickerForResult(this, blogId);
+ }
+ }
+ }
+
+ /*
+ * Simperium Note bucket listeners
+ */
+ @Override
+ public void onNetworkChange(Bucket<Note> noteBucket, Bucket.ChangeType changeType, String s) {
+ if (changeType == Bucket.ChangeType.INSERT || changeType == Bucket.ChangeType.MODIFY) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (isFinishing()) return;
+
+ if (isViewingNotificationsTab()) {
+ new UpdateLastSeenTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ mTabLayout.checkNoteBadge();
+ }
+ }
+ });
+ }
+ }
+
+ private boolean isViewingNotificationsTab() {
+ return mViewPager.getCurrentItem() == WPMainTabAdapter.TAB_NOTIFS;
+ }
+
+ @Override
+ public void onBeforeUpdateObject(Bucket<Note> noteBucket, Note note) {
+ // noop
+ }
+
+ @Override
+ public void onDeleteObject(Bucket<Note> noteBucket, Note note) {
+ // noop
+ }
+
+ @Override
+ public void onSaveObject(Bucket<Note> noteBucket, Note note) {
+ // noop
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabAdapter.java
new file mode 100644
index 000000000..62ce37813
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabAdapter.java
@@ -0,0 +1,91 @@
+package org.wordpress.android.ui.main;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Parcelable;
+import android.support.v13.app.FragmentStatePagerAdapter;
+import android.util.SparseArray;
+import android.view.ViewGroup;
+
+import org.wordpress.android.ui.notifications.NotificationsListFragment;
+import org.wordpress.android.ui.reader.ReaderPostListFragment;
+
+/**
+ * pager adapter containing tab fragments used by WPMainActivity
+ */
+class WPMainTabAdapter extends FragmentStatePagerAdapter {
+
+ static final int NUM_TABS = 4;
+
+ static final int TAB_MY_SITE = 0;
+ static final int TAB_READER = 1;
+ static final int TAB_ME = 2;
+ static final int TAB_NOTIFS = 3;
+
+ private final SparseArray<Fragment> mFragments = new SparseArray<>(NUM_TABS);
+
+ public WPMainTabAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ // work around "Fragement no longer exists for key" Android bug
+ // by catching the IllegalStateException
+ // https://code.google.com/p/android/issues/detail?id=42601
+ try {
+ super.restoreState(state, loader);
+ } catch (IllegalStateException e) {
+ // nop
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_TABS;
+ }
+
+ public boolean isValidPosition(int position) {
+ return (position >= 0 && position < getCount());
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case TAB_MY_SITE:
+ return MySiteFragment.newInstance();
+ case TAB_READER:
+ return ReaderPostListFragment.newInstance();
+ case TAB_ME:
+ return MeFragment.newInstance();
+ case TAB_NOTIFS:
+ return NotificationsListFragment.newInstance();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ Object item = super.instantiateItem(container, position);
+ if (item instanceof Fragment) {
+ mFragments.put(position, (Fragment) item);
+ }
+ return item;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ mFragments.remove(position);
+ super.destroyItem(container, position, object);
+ }
+
+ public Fragment getFragment(int position) {
+ if (isValidPosition(position)) {
+ return mFragments.get(position);
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabLayout.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabLayout.java
new file mode 100644
index 000000000..6e5b61517
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainTabLayout.java
@@ -0,0 +1,121 @@
+package org.wordpress.android.ui.main;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+import android.support.design.widget.TabLayout;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.BounceInterpolator;
+import android.widget.ImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+
+/**
+ * tab layout for main activity
+ */
+public class WPMainTabLayout extends TabLayout {
+
+ private View mNoteBadge;
+
+ public WPMainTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public WPMainTabLayout(Context context) {
+ super(context);
+ }
+
+ public WPMainTabLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void createTabs() {
+ addTab(R.drawable.main_tab_sites, R.string.tabbar_accessibility_label_my_site, false);
+ addTab(R.drawable.main_tab_reader, R.string.reader, false);
+ addTab(R.drawable.main_tab_me, R.string.tabbar_accessibility_label_me, false);
+ addTab(R.drawable.main_tab_notifications, R.string.notifications, true);
+ checkNoteBadge();
+ }
+
+ private void addTab(@DrawableRes int iconId, @StringRes int contentDescriptionId, boolean isNoteTab) {
+ View customView = LayoutInflater.from(getContext()).inflate(R.layout.tab_icon, null);
+
+ ImageView icon = (ImageView) customView.findViewById(R.id.tab_icon);
+ icon.setImageResource(iconId);
+ icon.setContentDescription(getResources().getText(contentDescriptionId));
+
+ // each tab has a badge icon, but we only care about the one on the notifications tab
+ if (isNoteTab) {
+ mNoteBadge = customView.findViewById(R.id.tab_badge);
+ }
+ addTab(newTab().setCustomView(customView));
+ }
+
+ /*
+ * adds or removes the badge on the notifications tab depending on whether there are
+ * unread notifications
+ */
+ void checkNoteBadge() {
+ showNoteBadge(SimperiumUtils.hasUnreadNotes());
+ }
+
+ void showNoteBadge(boolean showBadge) {
+ if (mNoteBadge == null) return;
+
+ boolean isBadged = (mNoteBadge.getVisibility() == View.VISIBLE);
+ if (showBadge == isBadged) {
+ return;
+ }
+
+ float start = showBadge ? 0f : 1f;
+ float end = showBadge ? 1f : 0f;
+
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, start, end);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, start, end);
+ ObjectAnimator animScale = ObjectAnimator.ofPropertyValuesHolder(mNoteBadge, scaleX, scaleY);
+
+ if (showBadge) {
+ animScale.setInterpolator(new BounceInterpolator());
+ animScale.setDuration(getContext().getResources().getInteger(android.R.integer.config_longAnimTime));
+ animScale.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mNoteBadge.setVisibility(View.VISIBLE);
+ }
+ });
+ } else {
+ animScale.setInterpolator(new AccelerateInterpolator());
+ animScale.setDuration(getContext().getResources().getInteger(android.R.integer.config_shortAnimTime));
+ animScale.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mNoteBadge.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ animScale.start();
+ }
+
+ private boolean isValidPosition(int position) {
+ return (position >=0 && position < getTabCount());
+ }
+
+ public void setSelectedTabPosition(int position) {
+ if (!isValidPosition(position) || getSelectedTabPosition() == position) {
+ return;
+ }
+ Tab tab = getTabAt(position);
+ if (tab != null) {
+ tab.select();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java
new file mode 100644
index 000000000..ea8d06f47
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java
@@ -0,0 +1,281 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.v4.content.CursorLoader;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.MediaUploadState;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.media.WordPressMediaUtils.LaunchCameraCallback;
+import org.wordpress.android.ui.media.services.MediaEvents.MediaChanged;
+import org.wordpress.android.ui.media.services.MediaUploadService;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.io.File;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * An invisible fragment in charge of launching the right intents to camera, video, and image library.
+ * Also queues up media for upload and listens to notifications from the upload service.
+ */
+public class MediaAddFragment extends Fragment implements LaunchCameraCallback {
+ private static final String BUNDLE_MEDIA_CAPTURE_PATH = "mediaCapturePath";
+ private String mMediaCapturePath = "";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // This view doesn't really matter as this fragment is invisible
+
+ if (savedInstanceState != null && savedInstanceState.getString(BUNDLE_MEDIA_CAPTURE_PATH) != null)
+ mMediaCapturePath = savedInstanceState.getString(BUNDLE_MEDIA_CAPTURE_PATH);
+
+ return inflater.inflate(R.layout.actionbar_add_media_cell, container, false);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mMediaCapturePath != null && !mMediaCapturePath.equals(""))
+ outState.putString(BUNDLE_MEDIA_CAPTURE_PATH, mMediaCapturePath);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // register context for change in connection status
+ getActivity().registerReceiver(mReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+ }
+
+ @Override
+ public void onStop() {
+ getActivity().unregisterReceiver(mReceiver);
+ super.onStop();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ resumeMediaUploadService();
+ }
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
+ // Coming from zero connection. Re-register upload intent.
+ resumeMediaUploadService();
+ }
+ }
+ };
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (data != null || requestCode == RequestCodes.TAKE_PHOTO ||
+ requestCode == RequestCodes.TAKE_VIDEO) {
+ String path;
+
+ switch (requestCode) {
+ case RequestCodes.PICTURE_LIBRARY:
+ case RequestCodes.VIDEO_LIBRARY:
+ Uri imageUri = data.getData();
+ fetchMedia(imageUri);
+ break;
+ case RequestCodes.TAKE_PHOTO:
+ if (resultCode == Activity.RESULT_OK) {
+ path = mMediaCapturePath;
+ mMediaCapturePath = null;
+ queueFileForUpload(path);
+ }
+ break;
+ case RequestCodes.TAKE_VIDEO:
+ if (resultCode == Activity.RESULT_OK) {
+ path = getRealPathFromURI(MediaUtils.getLastRecordedVideoUri(getActivity()));
+ queueFileForUpload(path);
+ }
+ break;
+ }
+ }
+ }
+
+ private void fetchMedia(Uri mediaUri) {
+ if (!MediaUtils.isInMediaStore(mediaUri)) {
+ // Create an AsyncTask to download the file
+ new DownloadMediaTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mediaUri);
+ } else {
+ // It is a regular local media file
+ String path = getRealPathFromURI(mediaUri);
+ queueFileForUpload(path);
+ }
+ }
+
+ private String getRealPathFromURI(Uri uri) {
+ String path;
+ if ("content".equals(uri.getScheme())) {
+ path = getRealPathFromContentURI(uri);
+ } else if ("file".equals(uri.getScheme())) {
+ path = uri.getPath();
+ } else {
+ path = uri.toString();
+ }
+ return path;
+ }
+
+ private String getRealPathFromContentURI(Uri contentUri) {
+ if (contentUri == null)
+ return null;
+
+ String[] proj = { MediaStore.Images.Media.DATA };
+ CursorLoader loader = new CursorLoader(getActivity(), contentUri, proj, null, null, null);
+ Cursor cursor = loader.loadInBackground();
+
+ if (cursor == null)
+ return null;
+
+ int column_index = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
+ if (column_index == -1) {
+ cursor.close();
+ return null;
+ }
+
+ String path;
+ if (cursor.moveToFirst()) {
+ path = cursor.getString(column_index);
+ } else {
+ path = null;
+ }
+
+ cursor.close();
+ return path;
+ }
+
+ private void queueFileForUpload(String path) {
+ if (path == null || path.equals("")) {
+ Toast.makeText(getActivity(), "Error opening file", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Blog blog = WordPress.getCurrentBlog();
+
+ File file = new File(path);
+ if (!file.exists()) {
+ return;
+ }
+
+ String mimeType = MediaUtils.getMediaFileMimeType(file);
+ String fileName = MediaUtils.getMediaFileName(file, mimeType);
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setBlogId(String.valueOf(blog.getLocalTableBlogId()));
+ mediaFile.setFileName(fileName);
+ mediaFile.setFilePath(path);
+ mediaFile.setUploadState("queued");
+ mediaFile.setDateCreatedGMT(System.currentTimeMillis());
+ mediaFile.setMediaId(String.valueOf(System.currentTimeMillis()));
+ if (mimeType != null && mimeType.startsWith("image")) {
+ // get width and height
+ BitmapFactory.Options bfo = new BitmapFactory.Options();
+ bfo.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, bfo);
+ mediaFile.setWidth(bfo.outWidth);
+ mediaFile.setHeight(bfo.outHeight);
+ }
+
+ if (!TextUtils.isEmpty(mimeType)) {
+ mediaFile.setMimeType(mimeType);
+ }
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ EventBus.getDefault().post(new MediaChanged(String.valueOf(blog.getLocalTableBlogId()), mediaFile.getMediaId()));
+ startMediaUploadService();
+ }
+
+ private void startMediaUploadService() {
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ getActivity().startService(new Intent(getActivity(), MediaUploadService.class));
+ }
+ }
+
+ private void resumeMediaUploadService() {
+ startMediaUploadService();
+ }
+
+ @Override
+ public void onMediaCapturePathReady(String mediaCapturePath) {
+ mMediaCapturePath = mediaCapturePath;
+ }
+
+ public void launchCamera() {
+ WordPressMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID, this);
+ }
+
+ public void launchVideoCamera() {
+ WordPressMediaUtils.launchVideoCamera(this);
+ }
+
+ public void launchVideoLibrary() {
+ WordPressMediaUtils.launchVideoLibrary(this);
+ }
+
+ public void launchPictureLibrary() {
+ WordPressMediaUtils.launchPictureLibrary(this);
+ }
+
+ public void addToQueue(String mediaId) {
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.QUEUED);
+ startMediaUploadService();
+ }
+
+ public void uploadList(List<Uri> uriList) {
+ for (Uri uri : uriList) {
+ fetchMedia(uri);
+ }
+ }
+
+ private class DownloadMediaTask extends AsyncTask<Uri, Integer, Uri> {
+ @Override
+ protected Uri doInBackground(Uri... uris) {
+ Uri imageUri = uris[0];
+ return MediaUtils.downloadExternalMedia(getActivity(), imageUri);
+ }
+
+ protected void onPostExecute(Uri newUri) {
+ if (getActivity() == null)
+ return;
+
+ if (newUri != null) {
+ String path = getRealPathFromURI(newUri);
+ queueFileForUpload(path);
+ }
+ else
+ Toast.makeText(getActivity(), getString(R.string.error_downloading_image), Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java
new file mode 100644
index 000000000..2e85481fb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java
@@ -0,0 +1,583 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.ColorDrawable;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v4.view.MenuItemCompat.OnActionExpandListener;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.SearchView;
+import android.support.v7.widget.SearchView.OnQueryTextListener;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.FeatureSet;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.media.MediaEditFragment.MediaEditFragmentCallback;
+import org.wordpress.android.ui.media.MediaGridFragment.Filter;
+import org.wordpress.android.ui.media.MediaGridFragment.MediaGridListener;
+import org.wordpress.android.ui.media.MediaItemFragment.MediaItemFragmentCallback;
+import org.wordpress.android.ui.media.services.MediaDeleteService;
+import org.wordpress.android.ui.media.services.MediaEvents;
+import org.wordpress.android.util.ActivityUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.PermissionUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.GetFeatures.Callback;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * The main activity in which the user can browse their media.
+ */
+public class MediaBrowserActivity extends AppCompatActivity implements MediaGridListener,
+ MediaItemFragmentCallback, OnQueryTextListener, OnActionExpandListener,
+ MediaEditFragmentCallback {
+ private static final String SAVED_QUERY = "SAVED_QUERY";
+ public static final int MEDIA_PERMISSION_REQUEST_CODE = 1;
+
+ private MediaGridFragment mMediaGridFragment;
+ private MediaItemFragment mMediaItemFragment;
+ private MediaEditFragment mMediaEditFragment;
+ private MediaAddFragment mMediaAddFragment;
+ private PopupWindow mAddMediaPopup;
+
+ private SearchView mSearchView;
+ private MenuItem mSearchMenuItem;
+ private Menu mMenu;
+ private FeatureSet mFeatureSet;
+ private String mQuery;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ // Coming from zero connection. Continue what's pending for delete
+ int blogId = WordPress.getCurrentLocalTableBlogId();
+ if (blogId != -1 && WordPress.wpDB.hasMediaDeleteQueueItems(blogId)) {
+ startMediaDeleteService();
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // This should be removed when #2734 is fixed
+ if (WordPress.getCurrentBlog() == null) {
+ ToastUtils.showToast(this, R.string.blog_not_found, ToastUtils.Duration.SHORT);
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.media_browser_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayShowTitleEnabled(true);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle(R.string.media);
+
+ FragmentManager fm = getFragmentManager();
+ fm.addOnBackStackChangedListener(mOnBackStackChangedListener);
+ FragmentTransaction ft = fm.beginTransaction();
+
+ mMediaAddFragment = (MediaAddFragment) fm.findFragmentById(R.id.mediaAddFragment);
+ mMediaGridFragment = (MediaGridFragment) fm.findFragmentById(R.id.mediaGridFragment);
+
+ mMediaItemFragment = (MediaItemFragment) fm.findFragmentByTag(MediaItemFragment.TAG);
+ if (mMediaItemFragment != null)
+ ft.hide(mMediaGridFragment);
+
+ mMediaEditFragment = (MediaEditFragment) fm.findFragmentByTag(MediaEditFragment.TAG);
+ if (mMediaEditFragment != null && !mMediaEditFragment.isInLayout())
+ ft.hide(mMediaItemFragment);
+
+ ft.commitAllowingStateLoss();
+
+ setupAddMenuPopup();
+
+ String action = getIntent().getAction();
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ // We arrived here from a share action
+ uploadSharedFiles();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ registerReceiver(mReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ unregisterReceiver(mReceiver);
+ super.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(SAVED_QUERY, mQuery);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mQuery = savedInstanceState.getString(SAVED_QUERY);
+ }
+
+ private void uploadSharedFiles() {
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ final List<Uri> multi_stream;
+ if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ multi_stream = intent.getParcelableArrayListExtra((Intent.EXTRA_STREAM));
+ } else {
+ multi_stream = new ArrayList<>();
+ multi_stream.add((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM));
+ }
+ mMediaAddFragment.uploadList(multi_stream);
+
+ // clear the intent's action, so that in case the user rotates, we don't re-upload the same
+ // files
+ getIntent().setAction(null);
+ }
+
+ private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
+ public void onBackStackChanged() {
+ FragmentManager manager = getFragmentManager();
+ MediaGridFragment mediaGridFragment = (MediaGridFragment)manager.findFragmentById(R.id.mediaGridFragment);
+ if (mediaGridFragment.isVisible()) {
+ mediaGridFragment.refreshSpinnerAdapter();
+ }
+ ActivityUtils.hideKeyboard(MediaBrowserActivity.this);
+ }
+ };
+
+ /** Setup the popup that allows you to add new media from camera, video camera or local files **/
+ private void setupAddMenuPopup() {
+ String capturePhoto = getResources().getString(R.string.media_add_popup_capture_photo);
+ String captureVideo = getResources().getString(R.string.media_add_popup_capture_video);
+ String pickPhotoFromGallery = getResources().getString(R.string.select_photo);
+ String pickVideoFromGallery = getResources().getString(R.string.select_video);
+ final ArrayAdapter<String> adapter = new ArrayAdapter<>(MediaBrowserActivity.this,
+ R.layout.actionbar_add_media_cell,
+ new String[] {
+ capturePhoto, captureVideo, pickPhotoFromGallery, pickVideoFromGallery
+ });
+
+ View layoutView = getLayoutInflater().inflate(R.layout.actionbar_add_media, null, false);
+ ListView listView = (ListView) layoutView.findViewById(R.id.actionbar_add_media_listview);
+ listView.setAdapter(adapter);
+ listView.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ adapter.notifyDataSetChanged();
+
+ if (position == 0) {
+ mMediaAddFragment.launchCamera();
+ } else if (position == 1) {
+ mMediaAddFragment.launchVideoCamera();
+ } else if (position == 2) {
+ mMediaAddFragment.launchPictureLibrary();
+ } else if (position == 3) {
+ mMediaAddFragment.launchVideoLibrary();
+ }
+
+ mAddMediaPopup.dismiss();
+ }
+ });
+
+ int width = getResources().getDimensionPixelSize(R.dimen.action_bar_spinner_width);
+
+ mAddMediaPopup = new PopupWindow(layoutView, width, ViewGroup.LayoutParams.WRAP_CONTENT, true);
+ mAddMediaPopup.setBackgroundDrawable(new ColorDrawable());
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ startMediaDeleteService();
+ getFeatureSet();
+ ActivityId.trackLastActivity(ActivityId.MEDIA);
+ }
+
+ /** Get the feature set for a wordpress.com hosted blog **/
+ private void getFeatureSet() {
+ if (WordPress.getCurrentBlog() == null || !WordPress.getCurrentBlog().isDotcomFlag())
+ return;
+
+ ApiHelper.GetFeatures task = new ApiHelper.GetFeatures(new Callback() {
+ @Override
+ public void onResult(FeatureSet featureSet) {
+ mFeatureSet = featureSet;
+ }
+
+ });
+
+ List<Object> apiArgs = new ArrayList<>();
+ apiArgs.add(WordPress.getCurrentBlog());
+ task.execute(apiArgs);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (mSearchMenuItem != null) {
+ String tempQuery = mQuery;
+ MenuItemCompat.collapseActionView(mSearchMenuItem);
+ mQuery = tempQuery;
+ }
+ }
+
+ @Override
+ public void onMediaItemSelected(String mediaId) {
+ String tempQuery = mQuery;
+ if (mSearchView != null) {
+ mSearchView.clearFocus();
+ }
+
+ if (mSearchMenuItem != null) {
+ MenuItemCompat.collapseActionView(mSearchMenuItem);
+ }
+
+ FragmentManager fm = getFragmentManager();
+ if (fm.getBackStackEntryCount() == 0) {
+ FragmentTransaction ft = fm.beginTransaction();
+ ft.hide(mMediaGridFragment);
+ mMediaGridFragment.clearSelectedItems();
+ mMediaItemFragment = MediaItemFragment.newInstance(mediaId);
+ ft.add(R.id.media_browser_container, mMediaItemFragment, MediaItemFragment.TAG);
+ ft.addToBackStack(null);
+ ft.commitAllowingStateLoss();
+ mQuery = tempQuery;
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ mMenu = menu;
+ getMenuInflater().inflate(R.menu.media, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ mSearchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
+ mSearchView.setOnQueryTextListener(this);
+
+ mSearchMenuItem = menu.findItem(R.id.menu_search);
+ MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, this);
+
+ //open search bar if we were searching for something before
+ if (!TextUtils.isEmpty(mQuery) && mMediaGridFragment != null && mMediaGridFragment.isVisible()) {
+ String tempQuery = mQuery; //temporary hold onto query
+ MenuItemCompat.expandActionView(mSearchMenuItem); //this will reset mQuery
+ onQueryTextSubmit(tempQuery);
+ mSearchView.setQuery(mQuery, true);
+ }
+
+ return super.onPrepareOptionsMenu(menu);
+
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
+ @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case MEDIA_PERMISSION_REQUEST_CODE:
+ for (int grantResult : grantResults) {
+ if (grantResult == PackageManager.PERMISSION_DENIED) {
+ ToastUtils.showToast(this, getString(R.string.add_media_permission_required));
+ return;
+ }
+ }
+ showNewMediaMenu();
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int i = item.getItemId();
+ if (i == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (i == R.id.menu_new_media) {
+ if (PermissionUtils.checkAndRequestCameraAndStoragePermissions(this, MEDIA_PERMISSION_REQUEST_CODE)) {
+ showNewMediaMenu();
+ }
+ return true;
+ } else if (i == R.id.menu_search) {
+ mSearchMenuItem = item;
+ MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, this);
+ MenuItemCompat.expandActionView(mSearchMenuItem);
+
+ mSearchView = (SearchView) item.getActionView();
+ mSearchView.setOnQueryTextListener(this);
+
+ // load last saved query
+ if (!TextUtils.isEmpty(mQuery)) {
+ onQueryTextSubmit(mQuery);
+ mSearchView.setQuery(mQuery, true);
+ }
+ return true;
+ } else if (i == R.id.menu_edit_media) {
+ String mediaId = mMediaItemFragment.getMediaId();
+ FragmentManager fm = getFragmentManager();
+
+ if (mMediaEditFragment == null || !mMediaEditFragment.isInLayout()) {
+ // phone layout: hide item details, show and update edit fragment
+ FragmentTransaction ft = fm.beginTransaction();
+
+ if (mMediaItemFragment.isVisible())
+ ft.hide(mMediaItemFragment);
+
+ mMediaEditFragment = MediaEditFragment.newInstance(mediaId);
+ ft.add(R.id.media_browser_container, mMediaEditFragment, MediaEditFragment.TAG);
+ ft.addToBackStack(null);
+ ft.commitAllowingStateLoss();
+ } else {
+ // tablet layout: update edit fragment
+ mMediaEditFragment.loadMedia(mediaId);
+ }
+
+ if (mSearchView != null) {
+ mSearchView.clearFocus();
+ }
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onMediaItemListDownloaded() {
+ if (mMediaItemFragment != null) {
+ mMediaGridFragment.setRefreshing(false);
+ if (mMediaItemFragment.isInLayout()) {
+ mMediaItemFragment.loadDefaultMedia();
+ }
+ }
+ }
+
+ @Override
+ public void onMediaItemListDownloadStart() {
+ mMediaGridFragment.setRefreshing(true);
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ if (mMediaGridFragment != null) {
+ mMediaGridFragment.search(query);
+ }
+ mQuery = query;
+ mSearchView.clearFocus();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (mMediaGridFragment != null) {
+ mMediaGridFragment.search(newText);
+ }
+ mQuery = newText;
+ return true;
+ }
+
+ @Override
+ public void onResume(Fragment fragment) {
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onPause(Fragment fragment) {
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ // currently we don't support searching from within a filter, so hide it
+ if (mMediaGridFragment != null) {
+ mMediaGridFragment.setFilterVisibility(View.GONE);
+ mMediaGridFragment.setFilter(Filter.ALL);
+ }
+
+ // load last search query
+ if (!TextUtils.isEmpty(mQuery))
+ onQueryTextChange(mQuery);
+ mMenu.findItem(R.id.menu_new_media).setVisible(false);
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ if (mMediaGridFragment != null) {
+ mMediaGridFragment.setFilterVisibility(View.VISIBLE);
+ mMediaGridFragment.setFilter(Filter.ALL);
+ }
+ mMenu.findItem(R.id.menu_new_media).setVisible(true);
+ return true;
+ }
+
+ public void onSavedEdit(String mediaId, boolean result) {
+ if (mMediaEditFragment != null && mMediaEditFragment.isVisible() && result) {
+ FragmentManager fm = getFragmentManager();
+ fm.popBackStack();
+
+ // refresh media item details (phone-only)
+ if (mMediaItemFragment != null)
+ mMediaItemFragment.loadMedia(mediaId);
+
+ // refresh grid
+ mMediaGridFragment.refreshMediaFromDB();
+ }
+ }
+
+ private void startMediaDeleteService() {
+ if (NetworkUtils.isNetworkAvailable(this)) {
+ startService(new Intent(this, MediaDeleteService.class));
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ FragmentManager fm = getFragmentManager();
+ if (fm.getBackStackEntryCount() > 0) {
+ fm.popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(MediaEvents.MediaChanged event) {
+ updateOnMediaChanged(event.mLocalBlogId, event.mMediaId);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(MediaEvents.MediaUploadSucceeded event) {
+ updateOnMediaChanged(event.mLocalBlogId, event.mLocalMediaId);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(MediaEvents.MediaUploadFailed event) {
+ ToastUtils.showToast(this, event.mErrorMessage, ToastUtils.Duration.LONG);
+ }
+
+ public void updateOnMediaChanged(String blogId, String mediaId) {
+ if (mediaId == null) {
+ return;
+ }
+
+ // If the media was deleted, remove it from multi select (if it was selected) and hide it from the the detail
+ // view (if it was the one displayed)
+ if (!WordPress.wpDB.mediaFileExists(blogId, mediaId)) {
+ mMediaGridFragment.removeFromMultiSelect(mediaId);
+ if (mMediaEditFragment != null && mMediaEditFragment.isVisible()
+ && mediaId.equals(mMediaEditFragment.getMediaId())) {
+ if (mMediaEditFragment.isInLayout()) {
+ mMediaEditFragment.loadMedia(null);
+ } else {
+ getFragmentManager().popBackStack();
+ }
+ }
+ }
+
+ // Update Grid view
+ mMediaGridFragment.refreshMediaFromDB();
+
+ // Update Spinner views
+ mMediaGridFragment.updateFilterText();
+ mMediaGridFragment.updateSpinnerAdapter();
+ }
+
+ @Override
+ public void onRetryUpload(String mediaId) {
+ mMediaAddFragment.addToQueue(mediaId);
+ }
+
+ public void deleteMedia(final ArrayList<String> ids) {
+ final String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ Set<String> sanitizedIds = new HashSet<>(ids.size());
+
+ // phone layout: pop the item fragment if it's visible
+ getFragmentManager().popBackStack();
+
+ // Make sure there are no media in "uploading"
+ for (String currentID : ids) {
+ if (WordPressMediaUtils.canDeleteMedia(blogId, currentID)) {
+ sanitizedIds.add(currentID);
+ }
+ }
+
+ if (sanitizedIds.size() != ids.size()) {
+ if (ids.size() == 1) {
+ Toast.makeText(this, R.string.wait_until_upload_completes, Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(this, R.string.cannot_delete_multi_media_items, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ // mark items for delete without actually deleting items yet,
+ // and then refresh the grid
+ WordPress.wpDB.setMediaFilesMarkedForDelete(blogId, sanitizedIds);
+ startMediaDeleteService();
+ if (mMediaGridFragment != null) {
+ mMediaGridFragment.clearSelectedItems();
+ mMediaGridFragment.refreshMediaFromDB();
+ }
+ }
+
+ private void showNewMediaMenu() {
+ View view = findViewById(R.id.menu_new_media);
+ if (view != null) {
+ int y_offset = getResources().getDimensionPixelSize(R.dimen.action_bar_spinner_y_offset);
+ int[] loc = new int[2];
+ view.getLocationOnScreen(loc);
+ mAddMediaPopup.showAtLocation(view, Gravity.TOP | Gravity.LEFT, loc[0],
+ loc[1] + view.getHeight() + y_offset);
+ } else {
+ // In case menu button is not on screen (declared showAsAction="ifRoom"), center the popup in the view.
+ View gridView = findViewById(R.id.media_gridview);
+ mAddMediaPopup.showAtLocation(gridView, Gravity.CENTER, 0, 0);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java
new file mode 100644
index 000000000..6b9dbb0e8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java
@@ -0,0 +1,385 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ScrollView;
+import android.widget.Toast;
+
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.ActivityUtils;
+import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback;
+import org.wordpress.android.util.ImageUtils.BitmapWorkerTask;
+import org.wordpress.android.util.MediaUtils;
+import org.xmlrpc.android.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A fragment for editing media on the Media tab
+ */
+public class MediaEditFragment extends Fragment {
+ private static final String ARGS_MEDIA_ID = "media_id";
+ // also appears in the layouts, from the strings.xml
+ public static final String TAG = "MediaEditFragment";
+
+ private NetworkImageView mNetworkImageView;
+ private ImageView mLocalImageView;
+ private EditText mTitleView;
+ private EditText mCaptionView;
+ private EditText mDescriptionView;
+ private Button mSaveButton;
+
+ private MediaEditFragmentCallback mCallback;
+
+ private boolean mIsMediaUpdating = false;
+
+ private String mMediaId;
+ private ScrollView mScrollView;
+ private View mLinearLayout;
+ private ImageLoader mImageLoader;
+
+ public interface MediaEditFragmentCallback {
+ void onResume(Fragment fragment);
+ void onPause(Fragment fragment);
+ void onSavedEdit(String mediaId, boolean result);
+ }
+
+ public static MediaEditFragment newInstance(String mediaId) {
+ MediaEditFragment fragment = new MediaEditFragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARGS_MEDIA_ID, mediaId);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ mImageLoader = MediaImageLoader.getInstance();
+
+ // retain this fragment across configuration changes
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mCallback = (MediaEditFragmentCallback) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement "
+ + MediaEditFragmentCallback.class.getSimpleName());
+ }
+ }
+
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ // set callback to null so we don't accidentally leak the activity instance
+ mCallback = null;
+ }
+
+ private boolean hasCallback() {
+ return (mCallback != null);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (hasCallback()) {
+ mCallback.onResume(this);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (hasCallback()) {
+ mCallback.onPause(this);
+ }
+ }
+
+ public String getMediaId() {
+ if (mMediaId != null) {
+ return mMediaId;
+ } else if (getArguments() != null) {
+ mMediaId = getArguments().getString(ARGS_MEDIA_ID);
+ return mMediaId;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mScrollView = (ScrollView) inflater.inflate(R.layout.media_edit_fragment, container, false);
+
+ mLinearLayout = mScrollView.findViewById(R.id.media_edit_linear_layout);
+ mTitleView = (EditText) mScrollView.findViewById(R.id.media_edit_fragment_title);
+ mCaptionView = (EditText) mScrollView.findViewById(R.id.media_edit_fragment_caption);
+ mDescriptionView = (EditText) mScrollView.findViewById(R.id.media_edit_fragment_description);
+ mLocalImageView = (ImageView) mScrollView.findViewById(R.id.media_edit_fragment_image_local);
+ mNetworkImageView = (NetworkImageView) mScrollView.findViewById(R.id.media_edit_fragment_image_network);
+ mSaveButton = (Button) mScrollView.findViewById(R.id.media_edit_save_button);
+ mSaveButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ editMedia();
+ }
+ });
+
+ disableEditingOnOldVersion();
+
+ loadMedia(getMediaId());
+
+ return mScrollView;
+ }
+
+ private void disableEditingOnOldVersion() {
+ if (WordPressMediaUtils.isWordPressVersionWithMediaEditingCapabilities()) {
+ return;
+ }
+
+ mSaveButton.setEnabled(false);
+ mTitleView.setEnabled(false);
+ mCaptionView.setEnabled(false);
+ mDescriptionView.setEnabled(false);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ }
+
+ public void loadMedia(String mediaId) {
+ mMediaId = mediaId;
+ Blog blog = WordPress.getCurrentBlog();
+
+ if (blog != null && getActivity() != null) {
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+
+ if (mMediaId != null) {
+ Cursor cursor = WordPress.wpDB.getMediaFile(blogId, mMediaId);
+ refreshViews(cursor);
+ cursor.close();
+ } else {
+ refreshViews(null);
+ }
+ }
+ }
+
+ void editMedia() {
+ ActivityUtils.hideKeyboard(getActivity());
+ final String mediaId = this.getMediaId();
+ final String title = mTitleView.getText().toString();
+ final String description = mDescriptionView.getText().toString();
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ final String caption = mCaptionView.getText().toString();
+
+ ApiHelper.EditMediaItemTask task = new ApiHelper.EditMediaItemTask(mediaId, title, description, caption,
+ new ApiHelper.GenericCallback() {
+ @Override
+ public void onSuccess() {
+ String blogId = String.valueOf(currentBlog.getLocalTableBlogId());
+ WordPress.wpDB.updateMediaFile(blogId, mediaId, title, description, caption);
+ if (getActivity() != null) {
+ Toast.makeText(getActivity(), R.string.media_edit_success, Toast.LENGTH_LONG).show();
+ }
+ setMediaUpdating(false);
+ if (hasCallback()) {
+ mCallback.onSavedEdit(mediaId, true);
+ }
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ if (getActivity() != null) {
+ Toast.makeText(getActivity(), R.string.media_edit_failure, Toast.LENGTH_LONG).show();
+ getActivity().invalidateOptionsMenu();
+ }
+ setMediaUpdating(false);
+ if (hasCallback()) {
+ mCallback.onSavedEdit(mediaId, false);
+ }
+ }
+ }
+ );
+
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(currentBlog);
+
+ if (!isMediaUpdating()) {
+ setMediaUpdating(true);
+ task.execute(apiArgs);
+ }
+ }
+
+ private void setMediaUpdating(boolean isUpdating) {
+ mIsMediaUpdating = isUpdating;
+ mSaveButton.setEnabled(!isUpdating);
+
+ if (isUpdating) {
+ mSaveButton.setText(R.string.saving);
+ } else {
+ mSaveButton.setText(R.string.save);
+ }
+ }
+
+ private boolean isMediaUpdating() {
+ return mIsMediaUpdating;
+ }
+
+ private void refreshImageView(Cursor cursor, boolean isLocal) {
+ final String imageUri;
+ if (isLocal) {
+ imageUri = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH));
+ } else {
+ imageUri = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL));
+ }
+ if (MediaUtils.isValidImage(imageUri)) {
+ int width = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH));
+ int height = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT));
+
+ // differentiating between tablet and phone
+ float screenWidth;
+ if (this.isInLayout()) {
+ screenWidth = mLinearLayout.getMeasuredWidth();
+ } else {
+ screenWidth = getActivity().getResources().getDisplayMetrics().widthPixels;
+ }
+ float screenHeight = getActivity().getResources().getDisplayMetrics().heightPixels;
+
+ if (width > screenWidth) {
+ height = (int) (height / (width / screenWidth));
+ } else if (height > screenHeight) {
+ width = (int) (width / (height / screenHeight));
+ }
+
+ if (isLocal) {
+ loadLocalImage(mLocalImageView, imageUri, width, height);
+ mLocalImageView.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, height));
+ } else {
+ mNetworkImageView.setImageUrl(imageUri + "?w=" + screenWidth, mImageLoader);
+ mNetworkImageView.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, height));
+ }
+ } else {
+ mNetworkImageView.setVisibility(View.GONE);
+ mLocalImageView.setVisibility(View.GONE);
+ }
+ }
+
+ private void refreshViews(Cursor cursor) {
+ if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) {
+ mLinearLayout.setVisibility(View.GONE);
+ return;
+ }
+
+ mLinearLayout.setVisibility(View.VISIBLE);
+
+ mScrollView.scrollTo(0, 0);
+
+ String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE));
+ boolean isLocal = MediaUtils.isLocalFile(state);
+ if (isLocal) {
+ mNetworkImageView.setVisibility(View.GONE);
+ mLocalImageView.setVisibility(View.VISIBLE);
+ } else {
+ mNetworkImageView.setVisibility(View.VISIBLE);
+ mLocalImageView.setVisibility(View.GONE);
+ }
+
+ // user can't edit local files
+ mSaveButton.setEnabled(!isLocal);
+ mTitleView.setEnabled(!isLocal);
+ mCaptionView.setEnabled(!isLocal);
+ mDescriptionView.setEnabled(!isLocal);
+
+ mMediaId = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID));
+ mTitleView.setText(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE)));
+ mTitleView.requestFocus();
+ mTitleView.setSelection(mTitleView.getText().length());
+ mCaptionView.setText(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_CAPTION)));
+ mDescriptionView.setText(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DESCRIPTION)));
+
+ refreshImageView(cursor, isLocal);
+ disableEditingOnOldVersion();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (!isInLayout()) {
+ inflater.inflate(R.menu.media_edit, menu);
+ }
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ if (!isInLayout()) {
+ menu.findItem(R.id.menu_new_media).setVisible(false);
+ menu.findItem(R.id.menu_search).setVisible(false);
+
+ if (!WordPressMediaUtils.isWordPressVersionWithMediaEditingCapabilities()) {
+ menu.findItem(R.id.menu_save_media).setVisible(false);
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == R.id.menu_save_media) {
+ item.setActionView(R.layout.progressbar);
+ editMedia();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private synchronized void loadLocalImage(ImageView imageView, String filePath, int width, int height) {
+ if (MediaUtils.isValidImage(filePath)) {
+ imageView.setTag(filePath);
+
+ Bitmap bitmap = WordPress.getBitmapCache().get(filePath);
+ if (bitmap != null) {
+ imageView.setImageBitmap(bitmap);
+ } else {
+ BitmapWorkerTask task = new BitmapWorkerTask(imageView, width, height, new BitmapWorkerCallback() {
+ @Override
+ public void onBitmapReady(String path, ImageView imageView, Bitmap bitmap) {
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ }
+ WordPress.getBitmapCache().put(path, bitmap);
+ }
+ });
+ task.execute(filePath);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java
new file mode 100644
index 000000000..e3168ed45
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java
@@ -0,0 +1,186 @@
+package org.wordpress.android.ui.media;
+
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import com.sothree.slidinguppanel.SlidingUpPanelLayout;
+import com.sothree.slidinguppanel.SlidingUpPanelLayout.PanelSlideListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.helpers.MediaGallery;
+import org.wordpress.android.ui.media.MediaGallerySettingsFragment.MediaGallerySettingsCallback;
+import org.wordpress.android.util.DisplayUtils;
+
+import java.util.ArrayList;
+
+/**
+ * An activity where the user can manage a media gallery
+ */
+public class MediaGalleryActivity extends AppCompatActivity implements MediaGallerySettingsCallback {
+ public static final int REQUEST_CODE = 3000;
+
+ // params for the gallery
+ public static final String PARAMS_MEDIA_GALLERY = "PARAMS_MEDIA_GALLERY";
+
+ // launches media picker in onCreate() if set
+ public static final String PARAMS_LAUNCH_PICKER = "PARAMS_LAUNCH_PICKER";
+
+ // result of the gallery
+ public static final String RESULT_MEDIA_GALLERY = "RESULT_MEDIA_GALLERY";
+
+ private MediaGalleryEditFragment mMediaGalleryEditFragment;
+ private MediaGallerySettingsFragment mMediaGallerySettingsFragment;
+
+ private SlidingUpPanelLayout mSlidingPanelLayout;
+ private boolean mIsPanelCollapsed = true;
+
+ private MediaGallery mMediaGallery;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (WordPress.wpDB == null) {
+ Toast.makeText(this, R.string.fatal_db_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ setTitle(R.string.media_gallery_edit);
+
+ setContentView(R.layout.media_gallery_activity);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ }
+
+ FragmentManager fm = getFragmentManager();
+
+ mMediaGallery = (MediaGallery) getIntent().getSerializableExtra(PARAMS_MEDIA_GALLERY);
+ if (mMediaGallery == null) {
+ mMediaGallery = new MediaGallery();
+ }
+
+ mMediaGalleryEditFragment = (MediaGalleryEditFragment) fm.findFragmentById(R.id.mediaGalleryEditFragment);
+ mMediaGallerySettingsFragment = (MediaGallerySettingsFragment) fm.findFragmentById(
+ R.id.mediaGallerySettingsFragment);
+ if (savedInstanceState == null) {
+ // if not null, the fragments will remember its state
+ mMediaGallerySettingsFragment.setRandom(mMediaGallery.isRandom());
+ mMediaGallerySettingsFragment.setNumColumns(mMediaGallery.getNumColumns());
+ mMediaGallerySettingsFragment.setType(mMediaGallery.getType());
+ mMediaGalleryEditFragment.setMediaIds(mMediaGallery.getIds());
+ }
+
+ mSlidingPanelLayout = (SlidingUpPanelLayout) findViewById(R.id.media_gallery_root);
+ if (mSlidingPanelLayout != null) {
+ // sliding panel layout is on phone only
+
+ mSlidingPanelLayout.setDragView(mMediaGallerySettingsFragment.getDragView());
+ mSlidingPanelLayout.setPanelHeight(DisplayUtils.dpToPx(this, 48));
+ mSlidingPanelLayout.setPanelSlideListener(new PanelSlideListener() {
+ @Override
+ public void onPanelSlide(View panel, float slideOffset) {
+ }
+
+ @Override
+ public void onPanelExpanded(View panel) {
+ mMediaGallerySettingsFragment.onPanelExpanded();
+ mIsPanelCollapsed = false;
+ }
+
+ @Override
+ public void onPanelCollapsed(View panel) {
+ mMediaGallerySettingsFragment.onPanelCollapsed();
+ mIsPanelCollapsed = true;
+ }
+ });
+ }
+
+ if (getIntent().hasExtra(PARAMS_LAUNCH_PICKER)) {
+ handleAddMedia();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.media_gallery, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_add_media) {
+ handleAddMedia();
+ return true;
+ } else if (item.getItemId() == R.id.menu_save) {
+ handleSaveMedia();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == MediaGalleryPickerActivity.REQUEST_CODE) {
+ if (resultCode == RESULT_OK) {
+ ArrayList<String> ids = data.getStringArrayListExtra(MediaGalleryPickerActivity.RESULT_IDS);
+ mMediaGalleryEditFragment.setMediaIds(ids);
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ private void handleAddMedia() {
+ // need to make MediaGalleryAdd into an activity rather than a fragment because I can't add this fragment
+ // on top of the slidingpanel layout (since it needs to be the root layout)
+
+ ArrayList<String> mediaIds = mMediaGalleryEditFragment.getMediaIds();
+
+ Intent intent = new Intent(this, MediaGalleryPickerActivity.class);
+ intent.putExtra(MediaGalleryPickerActivity.PARAM_SELECTED_IDS, mediaIds);
+ startActivityForResult(intent, MediaGalleryPickerActivity.REQUEST_CODE);
+ }
+
+ private void handleSaveMedia() {
+ Intent intent = new Intent();
+ ArrayList<String> ids = mMediaGalleryEditFragment.getMediaIds();
+ boolean isRandom = mMediaGallerySettingsFragment.isRandom();
+ int numColumns = mMediaGallerySettingsFragment.getNumColumns();
+ String type = mMediaGallerySettingsFragment.getType();
+
+ mMediaGallery.setIds(ids);
+ mMediaGallery.setRandom(isRandom);
+ mMediaGallery.setNumColumns(numColumns);
+ mMediaGallery.setType(type);
+
+ intent.putExtra(RESULT_MEDIA_GALLERY, mMediaGallery);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSlidingPanelLayout != null && !mIsPanelCollapsed) {
+ mSlidingPanelLayout.collapsePane();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onReverseClicked() {
+ mMediaGalleryEditFragment.reverseIds();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java
new file mode 100644
index 000000000..93a24ab2f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java
@@ -0,0 +1,140 @@
+package org.wordpress.android.ui.media;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.NetworkImageView;
+import com.mobeta.android.dslv.ResourceDragSortCursorAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.StringUtils;
+
+/**
+ * Adapter for a drag-sort listview where the user can drag media items to sort their order
+ * for a media gallery
+ */
+class MediaGalleryAdapter extends ResourceDragSortCursorAdapter {
+ private ImageLoader mImageLoader;
+
+ public MediaGalleryAdapter(Context context, int layout, Cursor c, boolean autoRequery, ImageLoader imageLoader) {
+ super(context, layout, c, autoRequery);
+ setImageLoader(imageLoader);
+ }
+
+ void setImageLoader(ImageLoader imageLoader) {
+ if (imageLoader != null) {
+ mImageLoader = imageLoader;
+ } else {
+ mImageLoader = WordPress.imageLoader;
+ }
+ }
+
+ private static class GridViewHolder {
+ private final TextView filenameView;
+ private final TextView titleView;
+ private final TextView uploadDateView;
+ private final ImageView imageView;
+ private final TextView fileTypeView;
+ private final TextView dimensionView;
+
+ GridViewHolder(View view) {
+ filenameView = (TextView) view.findViewById(R.id.media_grid_item_filename);
+ titleView = (TextView) view.findViewById(R.id.media_grid_item_name);
+ uploadDateView = (TextView) view.findViewById(R.id.media_grid_item_upload_date);
+ imageView = (ImageView) view.findViewById(R.id.media_grid_item_image);
+ fileTypeView = (TextView) view.findViewById(R.id.media_grid_item_filetype);
+ dimensionView = (TextView) view.findViewById(R.id.media_grid_item_dimension);
+ }
+ }
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final GridViewHolder holder;
+ if (view.getTag() instanceof GridViewHolder) {
+ holder = (GridViewHolder) view.getTag();
+ } else {
+ holder = new GridViewHolder(view);
+ view.setTag(holder);
+ }
+
+ String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE));
+ boolean isLocalFile = MediaUtils.isLocalFile(state);
+
+ // file name
+ String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME));
+ if (holder.filenameView != null) {
+ holder.filenameView.setText(String.format(context.getString(R.string.media_file_name), fileName));
+ }
+
+ // title of media
+ String title = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE));
+ if (title == null || title.equals(""))
+ title = fileName;
+ holder.titleView.setText(title);
+
+ // upload date
+ if (holder.uploadDateView != null) {
+ String date = MediaUtils.getDate(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT)));
+ holder.uploadDateView.setText(String.format(context.getString(R.string.media_uploaded_on), date));
+ }
+
+ // load image
+ if (isLocalFile) {
+ // should not be local file
+ } else {
+ loadNetworkImage(cursor, (NetworkImageView) holder.imageView);
+ }
+
+ // get the file extension from the fileURL
+ String filePath = StringUtils.notNullStr(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH)));
+ if (filePath.isEmpty())
+ filePath = StringUtils.notNullStr(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL)));
+
+ // file type
+ String fileExtension = filePath.replaceAll(".*\\.(\\w+)$", "$1").toUpperCase();
+ if (holder.fileTypeView != null) {
+ holder.fileTypeView.setText(String.format(context.getString(R.string.media_file_type), fileExtension));
+ }
+
+ // dimensions
+ if (holder.dimensionView != null) {
+ if( MediaUtils.isValidImage(filePath)) {
+ int width = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH));
+ int height = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT));
+
+ if (width > 0 && height > 0) {
+ String dimensions = width + "x" + height;
+ holder.dimensionView.setText(String.format(context.getString(R.string.media_dimensions),
+ dimensions));
+ holder.dimensionView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ holder.dimensionView.setVisibility(View.GONE);
+ }
+ }
+
+ }
+
+ private void loadNetworkImage(Cursor cursor, NetworkImageView imageView) {
+ String thumbnailURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL));
+ if (thumbnailURL == null) {
+ imageView.setImageUrl(null, null);
+ return;
+ }
+
+ Uri uri = Uri.parse(thumbnailURL);
+ if (uri != null && MediaUtils.isValidImage(uri.getLastPathSegment())) {
+ imageView.setTag(thumbnailURL);
+ imageView.setImageUrl(thumbnailURL, mImageLoader);
+ } else {
+ imageView.setImageUrl(null, null);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java
new file mode 100644
index 000000000..e99418720
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java
@@ -0,0 +1,191 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Fragment;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.os.Bundle;
+import android.util.SparseIntArray;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+
+import com.mobeta.android.dslv.DragSortListView;
+import com.mobeta.android.dslv.DragSortListView.DropListener;
+import com.mobeta.android.dslv.DragSortListView.RemoveListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Fragment where containing a drag-sort listview where the user can drag items
+ * to change their position in a media gallery
+ */
+public class MediaGalleryEditFragment extends Fragment implements DropListener, RemoveListener {
+ private static final String SAVED_MEDIA_IDS = "SAVED_MEDIA_IDS";
+ private MediaGalleryAdapter mGridAdapter;
+ private ArrayList<String> mIds;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+
+ mIds = new ArrayList<String>();
+ if (savedInstanceState != null) {
+ mIds = savedInstanceState.getStringArrayList(SAVED_MEDIA_IDS);
+ }
+
+ mGridAdapter = new MediaGalleryAdapter(getActivity(), R.layout.media_gallery_item, null, true,
+ MediaImageLoader.getInstance());
+
+ View view = inflater.inflate(R.layout.media_gallery_edit_fragment, container, false);
+
+ DragSortListView gridView = (DragSortListView) view.findViewById(R.id.edit_media_gallery_gridview);
+ gridView.setAdapter(mGridAdapter);
+ gridView.setOnCreateContextMenuListener(this);
+ gridView.setDropListener(this);
+ gridView.setRemoveListener(this);
+ refreshGridView();
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putStringArrayList(SAVED_MEDIA_IDS, mIds);
+ }
+
+ private void refreshGridView() {
+ if (WordPress.getCurrentBlog() == null) {
+ return;
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ Cursor cursor = WordPress.wpDB.getMediaFiles(blogId, mIds);
+ if (cursor == null) {
+ mGridAdapter.changeCursor(null);
+ return;
+ }
+ SparseIntArray positions = mapIdsToCursorPositions(cursor);
+ mGridAdapter.swapCursor(new OrderedCursor(cursor, positions));
+ }
+
+ private SparseIntArray mapIdsToCursorPositions(Cursor cursor) {
+ SparseIntArray positions = new SparseIntArray();
+ int size = mIds.size();
+ for (int i = 0; i < size; i++) {
+ while (cursor.moveToNext()) {
+ String mediaId = cursor.getString(cursor.getColumnIndex("mediaId"));
+ if (mediaId.equals(mIds.get(i))) {
+ positions.put(i, cursor.getPosition());
+ cursor.moveToPosition(-1);
+ break;
+ }
+ }
+ }
+ return positions;
+ }
+
+ public void setMediaIds(ArrayList<String> ids) {
+ mIds = ids;
+ refreshGridView();
+ }
+
+ public ArrayList<String> getMediaIds() {
+ return mIds;
+ }
+
+ public void reverseIds() {
+ Collections.reverse(mIds);
+ refreshGridView();
+ }
+
+ private class OrderedCursor extends CursorWrapper {
+ final int mPos;
+ private final int mCount;
+
+ // a map of custom position to cursor position
+ private final SparseIntArray mPositions;
+
+ /**
+ * A wrapper to allow for a custom order of items in a cursor *
+ */
+ public OrderedCursor(Cursor cursor, SparseIntArray positions) {
+ super(cursor);
+ cursor.moveToPosition(-1);
+ mPos = 0;
+ mCount = cursor.getCount();
+ mPositions = positions;
+ }
+
+ @Override
+ public boolean move(int offset) {
+ return this.moveToPosition(this.mPos + offset);
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return this.moveToPosition(this.mPos + 1);
+ }
+
+ @Override
+ public boolean moveToPrevious() {
+ return this.moveToPosition(this.mPos - 1);
+ }
+
+ @Override
+ public boolean moveToFirst() {
+ return this.moveToPosition(0);
+ }
+
+ @Override
+ public boolean moveToLast() {
+ return this.moveToPosition(this.mCount - 1);
+ }
+
+ @Override
+ public boolean moveToPosition(int position) {
+ return super.moveToPosition(mPositions.get(position));
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Cursor cursor = mGridAdapter.getCursor();
+ if (cursor == null) {
+ return;
+ }
+ cursor.moveToPosition(info.position);
+ String mediaId = cursor.getString(cursor.getColumnIndex("mediaId"));
+
+ menu.add(ContextMenu.NONE, mIds.indexOf(mediaId), ContextMenu.NONE, R.string.delete);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ int index = item.getItemId();
+ mIds.remove(index);
+ refreshGridView();
+ return true;
+ }
+
+ @Override
+ public void drop(int from, int to) {
+ String id = mIds.get(from);
+ mIds.remove(id);
+ mIds.add(to, id);
+ refreshGridView();
+ }
+
+ @Override
+ public void remove(int position) {
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java
new file mode 100644
index 000000000..128ac806a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java
@@ -0,0 +1,275 @@
+package org.wordpress.android.ui.media;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.ToastUtils;
+import org.xmlrpc.android.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An activity where the user can add new images to their media gallery or where the user
+ * can choose a single image to embed into their post.
+ */
+public class MediaGalleryPickerActivity extends AppCompatActivity
+ implements MultiChoiceModeListener, ActionMode.Callback, MediaGridAdapter.MediaGridAdapterCallback,
+ AdapterView.OnItemClickListener {
+ private GridView mGridView;
+ private MediaGridAdapter mGridAdapter;
+ private ActionMode mActionMode;
+
+ private ArrayList<String> mFilteredItems;
+ private boolean mIsSelectOneItem;
+ private boolean mIsRefreshing;
+ private boolean mHasRetrievedAllMedia;
+
+ private static final String STATE_FILTERED_ITEMS = "STATE_FILTERED_ITEMS";
+ private static final String STATE_SELECTED_ITEMS = "STATE_SELECTED_ITEMS";
+ private static final String STATE_IS_SELECT_ONE_ITEM = "STATE_IS_SELECT_ONE_ITEM";
+
+ public static final int REQUEST_CODE = 4000;
+ public static final String PARAM_SELECT_ONE_ITEM = "PARAM_SELECT_ONE_ITEM";
+ private static final String PARAM_FILTERED_IDS = "PARAM_FILTERED_IDS";
+ public static final String PARAM_SELECTED_IDS = "PARAM_SELECTED_IDS";
+ public static final String RESULT_IDS = "RESULT_IDS";
+ public static final String TAG = MediaGalleryPickerActivity.class.getSimpleName();
+
+ private int mOldMediaSyncOffset = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ArrayList<String> selectedItems = new ArrayList<String>();
+ mFilteredItems = getIntent().getStringArrayListExtra(PARAM_FILTERED_IDS);
+ mIsSelectOneItem = getIntent().getBooleanExtra(PARAM_SELECT_ONE_ITEM, false);
+
+ ArrayList<String> prevSelectedItems = getIntent().getStringArrayListExtra(PARAM_SELECTED_IDS);
+ if (prevSelectedItems != null) {
+ selectedItems.addAll(prevSelectedItems);
+ }
+
+ if (savedInstanceState != null) {
+ selectedItems.addAll(savedInstanceState.getStringArrayList(STATE_SELECTED_ITEMS));
+ mFilteredItems = savedInstanceState.getStringArrayList(STATE_FILTERED_ITEMS);
+ mIsSelectOneItem = savedInstanceState.getBoolean(STATE_IS_SELECT_ONE_ITEM, mIsSelectOneItem);
+ }
+
+ setContentView(R.layout.media_gallery_picker_layout);
+ mGridView = (GridView) findViewById(R.id.media_gallery_picker_gridview);
+ mGridView.setMultiChoiceModeListener(this);
+ mGridView.setOnItemClickListener(this);
+ mGridAdapter = new MediaGridAdapter(this, null, 0, MediaImageLoader.getInstance());
+ mGridAdapter.setSelectedItems(selectedItems);
+ mGridAdapter.setCallback(this);
+ mGridView.setAdapter(mGridAdapter);
+ if (mIsSelectOneItem) {
+ setTitle(R.string.select_from_media_library);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ } else {
+ mActionMode = startActionMode(this);
+ mActionMode.setTitle(String.format(getString(R.string.cab_selected),
+ mGridAdapter.getSelectedItems().size()));
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshViews();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putStringArrayList(STATE_SELECTED_ITEMS, mGridAdapter.getSelectedItems());
+ outState.putStringArrayList(STATE_FILTERED_ITEMS, mFilteredItems);
+ outState.putBoolean(STATE_IS_SELECT_ONE_ITEM, mIsSelectOneItem);
+ }
+
+ private void refreshViews() {
+ if (WordPress.getCurrentBlog() == null)
+ return;
+ final String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ Cursor cursor = WordPress.wpDB.getMediaImagesForBlog(blogId, mFilteredItems);
+ if (cursor.getCount() == 0) {
+ refreshMediaFromServer(0);
+ } else {
+ mGridAdapter.swapCursor(cursor);
+ }
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ setResult(RESULT_CANCELED, new Intent());
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (mIsSelectOneItem) {
+ // Single select, just finish the activity once an item is selected
+ mGridAdapter.setItemSelected(position, true);
+ Intent intent = new Intent();
+ intent.putStringArrayListExtra(RESULT_IDS, mGridAdapter.getSelectedItems());
+ setResult(RESULT_OK, intent);
+ finish();
+ } else {
+ mGridAdapter.toggleItemSelected(position);
+ mActionMode.setTitle(String.format(getString(R.string.cab_selected),
+ mGridAdapter.getSelectedItems().size()));
+ }
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ mGridAdapter.setItemSelected(position, checked);
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ Intent intent = new Intent();
+ intent.putStringArrayListExtra(RESULT_IDS, mGridAdapter.getSelectedItems());
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ @Override
+ public void fetchMoreData(int offset) {
+ if (!mHasRetrievedAllMedia) {
+ refreshMediaFromServer(offset);
+ }
+ }
+
+ @Override
+ public void onRetryUpload(String mediaId) {
+ }
+
+ @Override
+ public boolean isInMultiSelect() {
+ return false;
+ }
+
+ private void noMediaFinish() {
+ ToastUtils.showToast(this, R.string.media_empty_list, ToastUtils.Duration.LONG);
+ // Delay activity finish
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ }, 1500);
+ }
+
+ void refreshMediaFromServer(int offset) {
+ if (offset == 0 || !mIsRefreshing) {
+ if (offset == mOldMediaSyncOffset) {
+ // we're pulling the same data again for some reason. Pull from the beginning.
+ offset = 0;
+ }
+ mOldMediaSyncOffset = offset;
+ mIsRefreshing = true;
+ mGridAdapter.setRefreshing(true);
+
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(WordPress.getCurrentBlog());
+
+ ApiHelper.SyncMediaLibraryTask.Callback callback = new ApiHelper.SyncMediaLibraryTask.Callback() {
+ // refersh db from server. If returned count is 0, we've retrieved all the media.
+ // stop retrieving until the user manually refreshes
+
+ @Override
+ public void onSuccess(int count) {
+ MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter();
+ mHasRetrievedAllMedia = (count == 0);
+ adapter.setHasRetrievedAll(mHasRetrievedAllMedia);
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ if (WordPress.wpDB.getMediaCountAll(blogId) == 0 && count == 0) {
+ // There is no media at all
+ noMediaFinish();
+ }
+ mIsRefreshing = false;
+
+ // the activity may be gone by the time this finishes, so check for it
+ if (!isFinishing()) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ //mListener.onMediaItemListDownloaded();
+ mGridAdapter.setRefreshing(false);
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ Cursor cursor = WordPress.wpDB.getMediaImagesForBlog(blogId, mFilteredItems);
+ mGridAdapter.swapCursor(cursor);
+
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ if (errorType != ApiHelper.ErrorType.NO_ERROR) {
+ String message = errorType == ApiHelper.ErrorType.NO_UPLOAD_FILES_CAP
+ ? getString(R.string.media_error_no_permission)
+ : getString(R.string.error_refresh_media);
+ Toast.makeText(MediaGalleryPickerActivity.this, message, Toast.LENGTH_SHORT).show();
+ MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter();
+ mHasRetrievedAllMedia = true;
+ adapter.setHasRetrievedAll(mHasRetrievedAllMedia);
+ }
+
+ // the activity may be cone by the time we get this, so check for it
+ if (!isFinishing()) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mIsRefreshing = false;
+ mGridAdapter.setRefreshing(false);
+ }
+ });
+ }
+
+ }
+ };
+
+ ApiHelper.SyncMediaLibraryTask getMediaTask = new ApiHelper.SyncMediaLibraryTask(offset, MediaGridFragment.Filter.ALL, callback);
+ getMediaTask.execute(apiArgs);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java
new file mode 100644
index 000000000..e17074499
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java
@@ -0,0 +1,370 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.ExpandableHeightGridView;
+
+import java.util.ArrayList;
+
+/**
+ * The fragment containing the settings for the media gallery
+ */
+public class MediaGallerySettingsFragment extends Fragment implements OnCheckedChangeListener {
+ private static final int DEFAULT_THUMBNAIL_COLUMN_COUNT = 3;
+
+ private static final String STATE_NUM_COLUMNS = "STATE_NUM_COLUMNS";
+ private static final String STATE_GALLERY_TYPE_ORD = "GALLERY_TYPE_ORD";
+ private static final String STATE_RANDOM_ORDER = "STATE_RANDOM_ORDER";
+
+ private CheckBox mThumbnailCheckbox;
+ private CheckBox mSquaresCheckbox;
+ private CheckBox mTiledCheckbox;
+ private CheckBox mCirclesCheckbox;
+ private CheckBox mSlideshowCheckbox;
+
+ private GalleryType mType;
+ private int mNumColumns;
+ private boolean mIsRandomOrder;
+
+ private View mNumColumnsContainer;
+ private View mHeader;
+
+ private CheckBox mRandomOrderCheckbox;
+
+ private boolean mAllowCheckChanged;
+
+ private TextView mTitleView;
+
+ private ScrollView mScrollView;
+
+ private CustomGridAdapter mGridAdapter;
+
+ private MediaGallerySettingsCallback mCallback;
+
+
+ private enum GalleryType {
+ DEFAULT(""),
+ TILED("rectangular"),
+ SQUARES("square"),
+ CIRCLES("circle"),
+ SLIDESHOW("slideshow");
+
+ private final String mTag;
+
+ private GalleryType(String tag) {
+ mTag = tag;
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
+ public static GalleryType getTypeFromTag(String tag) {
+ if (tag.equals("rectangular"))
+ return TILED;
+ else if (tag.equals("square"))
+ return SQUARES;
+ else if (tag.equals("circle"))
+ return CIRCLES;
+ else if (tag.equals("slideshow"))
+ return SLIDESHOW;
+ else
+ return DEFAULT;
+ }
+
+ }
+
+ public interface MediaGallerySettingsCallback {
+ public void onReverseClicked();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mCallback = (MediaGallerySettingsCallback) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement MediaGallerySettingsCallback");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mAllowCheckChanged = true;
+ mType = GalleryType.DEFAULT;
+ mNumColumns = DEFAULT_THUMBNAIL_COLUMN_COUNT;
+ mIsRandomOrder = false;
+
+ restoreState(savedInstanceState);
+
+ View view = inflater.inflate(R.layout.media_gallery_settings_fragment, container, false);
+
+ mHeader = view.findViewById(R.id.media_gallery_settings_header);
+ mScrollView = (ScrollView) view.findViewById(R.id.media_gallery_settings_content_container);
+ mTitleView = (TextView) view.findViewById(R.id.media_gallery_settings_title);
+
+ mNumColumnsContainer = view.findViewById(R.id.media_gallery_settings_num_columns_container);
+ int visible = (mType == GalleryType.DEFAULT) ? View.VISIBLE : View.GONE;
+ mNumColumnsContainer.setVisibility(visible);
+
+ ExpandableHeightGridView numColumnsGrid = (ExpandableHeightGridView) view.findViewById(R.id.media_gallery_num_columns_grid);
+ numColumnsGrid.setExpanded(true);
+ ArrayList<String> list = new ArrayList<String>(9);
+ for (int i = 1; i <= 9; i++) {
+ list.add(i + "");
+ }
+
+ mGridAdapter = new CustomGridAdapter(mNumColumns);
+ numColumnsGrid.setAdapter(mGridAdapter);
+
+ mThumbnailCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_thumbnail_grid);
+ mTiledCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_tiled);
+ mSquaresCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_squares);
+ mCirclesCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_circles);
+ mSlideshowCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_slideshow);
+
+ setType(mType.getTag());
+
+ mThumbnailCheckbox.setOnCheckedChangeListener(this);
+ mTiledCheckbox.setOnCheckedChangeListener(this);
+ mSquaresCheckbox.setOnCheckedChangeListener(this);
+ mCirclesCheckbox.setOnCheckedChangeListener(this);
+
+ mSlideshowCheckbox.setOnCheckedChangeListener(this);
+
+ mRandomOrderCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_random_checkbox);
+ mRandomOrderCheckbox.setChecked(mIsRandomOrder);
+ mRandomOrderCheckbox.setOnCheckedChangeListener(this);
+
+ Button reverseButton = (Button) view.findViewById(R.id.media_gallery_settings_reverse_button);
+ reverseButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.onReverseClicked();
+ }
+ });
+
+ return view;
+ }
+
+ private void restoreState(Bundle savedInstanceState) {
+ if (savedInstanceState == null)
+ return;
+
+ mNumColumns = savedInstanceState.getInt(STATE_NUM_COLUMNS);
+ int galleryTypeOrdinal = savedInstanceState.getInt(STATE_GALLERY_TYPE_ORD);
+ mType = GalleryType.values()[galleryTypeOrdinal];
+ mIsRandomOrder = savedInstanceState.getBoolean(STATE_RANDOM_ORDER);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(STATE_NUM_COLUMNS, mNumColumns);
+ outState.putBoolean(STATE_RANDOM_ORDER, mIsRandomOrder);
+ outState.putInt(STATE_GALLERY_TYPE_ORD, mType.ordinal());
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton button, boolean checked) {
+ if (!mAllowCheckChanged)
+ return;
+
+ // the checkboxes for types are mutually exclusive, so when one is set,
+ // the others must be unset. Disable the checkChange listener during this time,
+ // and re-enable it once done.
+ mAllowCheckChanged = false;
+
+ int numColumnsContainerVisible = View.GONE;
+ int i = button.getId();
+ if (i == R.id.media_gallery_type_thumbnail_grid) {
+ numColumnsContainerVisible = View.VISIBLE;
+
+ mType = GalleryType.DEFAULT;
+ mThumbnailCheckbox.setChecked(true);
+ mSquaresCheckbox.setChecked(false);
+ mTiledCheckbox.setChecked(false);
+ mCirclesCheckbox.setChecked(false);
+ mSlideshowCheckbox.setChecked(false);
+ } else if (i == R.id.media_gallery_type_tiled) {
+ mType = GalleryType.TILED;
+ mThumbnailCheckbox.setChecked(false);
+ mTiledCheckbox.setChecked(true);
+ mSquaresCheckbox.setChecked(false);
+ mCirclesCheckbox.setChecked(false);
+ mSlideshowCheckbox.setChecked(false);
+ } else if (i == R.id.media_gallery_type_squares) {
+ mType = GalleryType.SQUARES;
+ mThumbnailCheckbox.setChecked(false);
+ mTiledCheckbox.setChecked(false);
+ mSquaresCheckbox.setChecked(true);
+ mCirclesCheckbox.setChecked(false);
+ mSlideshowCheckbox.setChecked(false);
+ } else if (i == R.id.media_gallery_type_circles) {
+ mType = GalleryType.CIRCLES;
+ mThumbnailCheckbox.setChecked(false);
+ mSquaresCheckbox.setChecked(false);
+ mTiledCheckbox.setChecked(false);
+ mCirclesCheckbox.setChecked(true);
+ mSlideshowCheckbox.setChecked(false);
+ } else if (i == R.id.media_gallery_type_slideshow) {
+ mType = GalleryType.SLIDESHOW;
+ mThumbnailCheckbox.setChecked(false);
+ mSquaresCheckbox.setChecked(false);
+ mTiledCheckbox.setChecked(false);
+ mCirclesCheckbox.setChecked(false);
+ mSlideshowCheckbox.setChecked(true);
+ } else if (i == R.id.media_gallery_random_checkbox) {
+ numColumnsContainerVisible = mNumColumnsContainer.getVisibility();
+ mIsRandomOrder = checked;
+ }
+
+ mNumColumnsContainer.setVisibility(numColumnsContainerVisible);
+
+ mAllowCheckChanged = true;
+ }
+
+ private class CustomGridAdapter extends BaseAdapter implements OnCheckedChangeListener {
+ private boolean mAllowCheckChanged;
+ private final SparseBooleanArray mCheckedPositions;
+
+ public CustomGridAdapter(int selection) {
+ mAllowCheckChanged = true;
+ mCheckedPositions = new SparseBooleanArray(9);
+ setSelection(selection);
+ }
+
+ // when a number of columns is checked, the numbers less than
+ // the one chose are also set to checked on the ui.
+ // e.g. when 3 is checked, 1 and 2 are as well.
+ private void setSelection(int selection) {
+ for (int i = 0; i < 9; i++){
+ if (i + 1 <= selection)
+ mCheckedPositions.put(i, true);
+ else
+ mCheckedPositions.put(i, false);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return 9;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return position;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ CheckBox checkbox = (CheckBox) inflater.inflate(R.layout.media_gallery_column_checkbox, parent, false);
+ checkbox.setChecked(mCheckedPositions.get(position));
+ checkbox.setTag(position);
+ checkbox.setText(String.valueOf(position + 1));
+ checkbox.setOnCheckedChangeListener(this);
+
+ return checkbox;
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton button, boolean checked) {
+ if (mAllowCheckChanged) {
+ mAllowCheckChanged = false;
+
+ int position = (Integer) button.getTag();
+ mNumColumns = position + 1;
+
+ int count = mCheckedPositions.size();
+ for (int i = 0; i < count; i++) {
+ if (i <= position)
+ mCheckedPositions.put(i, true);
+ else
+ mCheckedPositions.put(i, false);
+ }
+ notifyDataSetChanged();
+ mAllowCheckChanged = true;
+ }
+ }
+
+ }
+
+ public View getDragView() {
+ return mHeader;
+ }
+
+ public void onPanelExpanded() {
+ mTitleView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.gallery_arrow_dropdown_open, 0);
+ mScrollView.scrollTo(0, 0);
+ }
+
+ public void onPanelCollapsed() {
+ mTitleView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.gallery_arrow_dropdown_closed, 0);
+ }
+
+ public void setRandom(boolean random) {
+ mIsRandomOrder = random;
+ mRandomOrderCheckbox.setChecked(mIsRandomOrder);
+ }
+
+ public boolean isRandom() {
+ return mIsRandomOrder;
+ }
+
+ public void setType(String type) {
+ mType = GalleryType.getTypeFromTag(type);
+ switch (mType) {
+ case CIRCLES:
+ mCirclesCheckbox.setChecked(true);
+ break;
+ case DEFAULT:
+ mThumbnailCheckbox.setChecked(true);
+ break;
+ case SLIDESHOW:
+ mSlideshowCheckbox.setChecked(true);
+ break;
+ case SQUARES:
+ mSquaresCheckbox.setChecked(true);
+ break;
+ case TILED:
+ mTiledCheckbox.setChecked(true);
+ break;
+ }
+ }
+
+ public String getType() {
+ return mType.getTag();
+ }
+
+ public void setNumColumns(int numColumns) {
+ mNumColumns = numColumns;
+ mGridAdapter.setSelection(numColumns);
+ mGridAdapter.notifyDataSetChanged();
+ }
+
+ public int getNumColumns() {
+ return mNumColumns;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java
new file mode 100644
index 000000000..61192a4a1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java
@@ -0,0 +1,519 @@
+package org.wordpress.android.ui.media;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.CursorAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.ui.CheckableFrameLayout;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback;
+import org.wordpress.android.util.ImageUtils.BitmapWorkerTask;
+import org.wordpress.android.util.MediaUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An adapter for the media gallery listViews.
+ */
+public class MediaGridAdapter extends CursorAdapter {
+ private MediaGridAdapterCallback mCallback;
+ private boolean mHasRetrievedAll;
+ private boolean mIsRefreshing;
+ private int mCursorDataCount;
+ private int mGridItemWidth;
+ private final Map<String, List<BitmapReadyCallback>> mFilePathToCallbackMap;
+ private final Handler mHandler;
+ private final int mLocalImageWidth;
+ private final LayoutInflater mInflater;
+ private ImageLoader mImageLoader;
+ private Context mContext;
+ // Must be an ArrayList (order is important for galleries)
+ private ArrayList<String> mSelectedItems;
+
+ public interface MediaGridAdapterCallback {
+ public void fetchMoreData(int offset);
+ public void onRetryUpload(String mediaId);
+ public boolean isInMultiSelect();
+ }
+
+ interface BitmapReadyCallback {
+ void onBitmapReady(Bitmap bitmap);
+ }
+
+ private static enum ViewTypes {
+ LOCAL, NETWORK, PROGRESS, SPACER
+ }
+
+ public MediaGridAdapter(Context context, Cursor c, int flags, ImageLoader imageLoader) {
+ super(context, c, flags);
+ mContext = context;
+ mSelectedItems = new ArrayList<String>();
+ mLocalImageWidth = context.getResources().getDimensionPixelSize(R.dimen.media_grid_local_image_width);
+ mInflater = LayoutInflater.from(context);
+ mFilePathToCallbackMap = new HashMap<String, List<BitmapReadyCallback>>();
+ mHandler = new Handler();
+ setImageLoader(imageLoader);
+ }
+
+ void setImageLoader(ImageLoader imageLoader) {
+ if (imageLoader != null) {
+ mImageLoader = imageLoader;
+ } else {
+ mImageLoader = WordPress.imageLoader;
+ }
+ }
+
+ public ArrayList<String> getSelectedItems() {
+ return mSelectedItems;
+ }
+
+ private static class GridViewHolder {
+ private final TextView filenameView;
+ private final TextView titleView;
+ private final TextView uploadDateView;
+ private final ImageView imageView;
+ private final TextView fileTypeView;
+ private final TextView dimensionView;
+ private final CheckableFrameLayout frameLayout;
+
+ private final TextView stateTextView;
+ private final ProgressBar progressUpload;
+ private final RelativeLayout uploadStateView;
+
+ GridViewHolder(View view) {
+ filenameView = (TextView) view.findViewById(R.id.media_grid_item_filename);
+ titleView = (TextView) view.findViewById(R.id.media_grid_item_name);
+ uploadDateView = (TextView) view.findViewById(R.id.media_grid_item_upload_date);
+ imageView = (ImageView) view.findViewById(R.id.media_grid_item_image);
+ fileTypeView = (TextView) view.findViewById(R.id.media_grid_item_filetype);
+ dimensionView = (TextView) view.findViewById(R.id.media_grid_item_dimension);
+ frameLayout = (CheckableFrameLayout) view.findViewById(R.id.media_grid_frame_layout);
+
+ stateTextView = (TextView) view.findViewById(R.id.media_grid_item_upload_state);
+ progressUpload = (ProgressBar) view.findViewById(R.id.media_grid_item_upload_progress);
+ uploadStateView = (RelativeLayout) view.findViewById(R.id.media_grid_item_upload_state_container);
+ }
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public void bindView(final View view, Context context, Cursor cursor) {
+ int itemViewType = getItemViewType(cursor.getPosition());
+
+ if (itemViewType == ViewTypes.PROGRESS.ordinal()) {
+ if (mIsRefreshing) {
+ int height = mContext.getResources().getDimensionPixelSize(R.dimen.media_grid_progress_height);
+ view.setLayoutParams(new GridView.LayoutParams(GridView.LayoutParams.MATCH_PARENT, height));
+ view.setVisibility(View.VISIBLE);
+ } else {
+ view.setLayoutParams(new GridView.LayoutParams(0, 0));
+ view.setVisibility(View.GONE);
+ }
+ return;
+ } else if (itemViewType == ViewTypes.SPACER.ordinal()) {
+ CheckableFrameLayout frameLayout = (CheckableFrameLayout) view.findViewById(R.id.media_grid_frame_layout);
+ updateGridWidth(context, frameLayout);
+ view.setVisibility(View.INVISIBLE);
+ return;
+ }
+
+ final GridViewHolder holder;
+ if (view.getTag() instanceof GridViewHolder) {
+ holder = (GridViewHolder) view.getTag();
+ } else {
+ holder = new GridViewHolder(view);
+ view.setTag(holder);
+ }
+
+ final String mediaId = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID));
+
+ String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE));
+ boolean isLocalFile = MediaUtils.isLocalFile(state);
+
+ // file name
+ String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME));
+ if (holder.filenameView != null) {
+ holder.filenameView.setText(fileName);
+ }
+
+ // title of media
+ String title = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE));
+ if (title == null || title.equals(""))
+ title = fileName;
+ holder.titleView.setText(title);
+
+ // upload date
+ if (holder.uploadDateView != null) {
+ String date = MediaUtils.getDate(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT)));
+ holder.uploadDateView.setText(date);
+ }
+
+ // load image
+ if (isLocalFile) {
+ loadLocalImage(cursor, holder.imageView);
+ } else {
+ String thumbUrl = WordPressMediaUtils.getNetworkThumbnailUrl(cursor, mGridItemWidth);
+ WordPressMediaUtils.loadNetworkImage(thumbUrl, (NetworkImageView) holder.imageView, mImageLoader);
+ }
+
+ // get the file extension from the fileURL
+ String mimeType = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MIME_TYPE));
+ String fileExtension = MediaUtils.getExtensionForMimeType(mimeType);
+ fileExtension = fileExtension.toUpperCase();
+ // file type
+ if (DisplayUtils.isXLarge(context) && !TextUtils.isEmpty(fileExtension)) {
+ holder.fileTypeView.setText(String.format(context.getString(R.string.media_file_type), fileExtension));
+ } else {
+ holder.fileTypeView.setText(fileExtension);
+ }
+
+ // dimensions
+ String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL));
+ TextView dimensionView = (TextView) view.findViewById(R.id.media_grid_item_dimension);
+ if (dimensionView != null) {
+ if( MediaUtils.isValidImage(filePath)) {
+ int width = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH));
+ int height = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT));
+
+ if (width > 0 && height > 0) {
+ String dimensions = width + "x" + height;
+ holder.dimensionView.setText(dimensions);
+ holder.dimensionView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ holder.dimensionView.setVisibility(View.GONE);
+ }
+ }
+
+ holder.frameLayout.setTag(mediaId);
+ holder.frameLayout.setChecked(mSelectedItems.contains(mediaId));
+
+ // resizing layout to fit nicely into grid view
+ updateGridWidth(context, holder.frameLayout);
+
+ // show upload state
+ if (holder.stateTextView != null) {
+ if (state != null && state.length() > 0) {
+ // show the progressbar only when the state is uploading
+ if (state.equals("uploading")) {
+ holder.progressUpload.setVisibility(View.VISIBLE);
+ } else {
+ holder.progressUpload.setVisibility(View.GONE);
+ if (state.equals("uploaded")) {
+ holder.stateTextView.setVisibility(View.GONE);
+ }
+ }
+
+ // add onclick to retry failed uploads
+ if (state.equals("failed")) {
+ state = "retry";
+ holder.stateTextView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!inMultiSelect()) {
+ ((TextView) v).setText(R.string.upload_queued);
+ v.setOnClickListener(null);
+ mCallback.onRetryUpload(mediaId);
+ }
+ }
+
+ });
+ }
+
+ holder.stateTextView.setText(state);
+ holder.uploadStateView.setVisibility(View.VISIBLE);
+ } else {
+ holder.uploadStateView.setVisibility(View.GONE);
+ }
+ }
+
+ // if we are near the end, make a call to fetch more
+ int position = cursor.getPosition();
+ if (position == mCursorDataCount - 1 && !mHasRetrievedAll) {
+ if (mCallback != null) {
+ mCallback.fetchMoreData(mCursorDataCount);
+ }
+ }
+ }
+
+ private boolean inMultiSelect() {
+ return mCallback.isInMultiSelect();
+ }
+
+ private synchronized void loadLocalImage(Cursor cursor, final ImageView imageView) {
+ final String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH));
+
+ if (MediaUtils.isValidImage(filePath)) {
+ imageView.setTag(filePath);
+
+ Bitmap bitmap = WordPress.getBitmapCache().get(filePath);
+ if (bitmap != null) {
+ imageView.setImageBitmap(bitmap);
+ } else {
+ imageView.setImageBitmap(null);
+
+ boolean shouldFetch = false;
+
+ List<BitmapReadyCallback> list;
+ if (mFilePathToCallbackMap.containsKey(filePath)) {
+ list = mFilePathToCallbackMap.get(filePath);
+ } else {
+ list = new ArrayList<MediaGridAdapter.BitmapReadyCallback>();
+ shouldFetch = true;
+ mFilePathToCallbackMap.put(filePath, list);
+ }
+ list.add(new BitmapReadyCallback() {
+ @Override
+ public void onBitmapReady(Bitmap bitmap) {
+ if (imageView.getTag() instanceof String && imageView.getTag().equals(filePath))
+ imageView.setImageBitmap(bitmap);
+ }
+ });
+
+
+ if (shouldFetch) {
+ fetchBitmap(filePath);
+ }
+ }
+ } else {
+ // if not image, for now show no image.
+ imageView.setImageBitmap(null);
+ }
+ }
+
+ private void fetchBitmap(final String filePath) {
+ BitmapWorkerTask task = new BitmapWorkerTask(null, mLocalImageWidth, mLocalImageWidth, new BitmapWorkerCallback() {
+ @Override
+ public void onBitmapReady(final String path, ImageView imageView, final Bitmap bitmap) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ List<BitmapReadyCallback> callbacks = mFilePathToCallbackMap.get(path);
+ for (BitmapReadyCallback callback : callbacks) {
+ callback.onBitmapReady(bitmap);
+ }
+
+ WordPress.getBitmapCache().put(path, bitmap);
+ callbacks.clear();
+ mFilePathToCallbackMap.remove(path);
+ }
+ });
+ }
+ });
+ task.execute(filePath);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup root) {
+ int itemViewType = getItemViewType(cursor.getPosition());
+
+ // spacer and progress spinner views
+ if (itemViewType == ViewTypes.PROGRESS.ordinal()) {
+ return mInflater.inflate(R.layout.media_grid_progress, root, false);
+ } else if (itemViewType == ViewTypes.SPACER.ordinal()) {
+ return mInflater.inflate(R.layout.media_grid_item, root, false);
+ }
+
+ View view = mInflater.inflate(R.layout.media_grid_item, root, false);
+ ViewStub imageStub = (ViewStub) view.findViewById(R.id.media_grid_image_stub);
+
+ // We need to use ViewStubs to inflate the image to either:
+ // - a regular ImageView (for local images)
+ // - a FadeInNetworkImageView (for network images)
+ // This is because the NetworkImageView can't load local images.
+ // The other option would be to inflate multiple layouts, but that would lead
+ // to extra near-duplicate xml files that would need to be maintained.
+ if (itemViewType == ViewTypes.LOCAL.ordinal()) {
+ imageStub.setLayoutResource(R.layout.media_grid_image_local);
+ } else {
+ imageStub.setLayoutResource(R.layout.media_grid_image_network);
+ }
+
+ imageStub.inflate();
+
+ view.setTag(new GridViewHolder(view));
+
+ return view;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return ViewTypes.values().length;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Cursor cursor = getCursor();
+ cursor.moveToPosition(position);
+
+ // spacer / progress cells
+ int _id = cursor.getInt(cursor.getColumnIndex("_id"));
+ if (_id < 0) {
+ if (_id == Integer.MIN_VALUE)
+ return ViewTypes.PROGRESS.ordinal();
+ else
+ return ViewTypes.SPACER.ordinal();
+ }
+
+ // regular cells
+ String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE));
+ if (MediaUtils.isLocalFile(state))
+ return ViewTypes.LOCAL.ordinal();
+ else
+ return ViewTypes.NETWORK.ordinal();
+ }
+
+ /** Updates the width of a cell to max out the space available, for phones **/
+ private void updateGridWidth(Context context, View view) {
+ setGridItemWidth();
+ int columnCount = getColumnCount(context);
+
+ if (columnCount > 1) {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mGridItemWidth, mGridItemWidth);
+ view.setLayoutParams(params);
+ }
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ if (newCursor == null) {
+ mCursorDataCount = 0;
+ return super.swapCursor(newCursor);
+ }
+
+ mCursorDataCount = newCursor.getCount();
+
+ // to mimic the infinite the notification's infinite scroll ui
+ // (with a progress spinner on the bottom of the list), we'll need to add
+ // extra cells in the gridview:
+ // - spacer cells as fillers to place the progress spinner on the first cell (_id < 0)
+ // - progress spinner cell (_id = Integer.MIN_VALUE)
+
+ // use a matrix cursor to create the extra rows
+ MatrixCursor matrixCursor = new MatrixCursor(new String[] { "_id" });
+
+ // add spacer cells
+ int columnCount = getColumnCount(mContext);
+ int remainder = newCursor.getCount() % columnCount;
+ if (remainder > 0) {
+ int spaceCount = columnCount - remainder;
+ for (int i = 0; i < spaceCount; i++ ) {
+ int id = i - spaceCount;
+ matrixCursor.addRow(new Object[] {id + ""});
+ }
+ }
+
+ // add progress spinner cell
+ matrixCursor.addRow(new Object[] { Integer.MIN_VALUE });
+
+ // use a merge cursor to place merge the extra rows at the bottom of the newly swapped cursor
+ MergeCursor mergeCursor = new MergeCursor(new Cursor[] { newCursor, matrixCursor });
+ return super.swapCursor(mergeCursor);
+ }
+
+ /** Return the number of columns in the media grid **/
+ private int getColumnCount(Context context) {
+ return context.getResources().getInteger(R.integer.media_grid_num_columns);
+ }
+
+ public void setCallback(MediaGridAdapterCallback callback) {
+ mCallback = callback;
+ }
+
+ public void setHasRetrievedAll(boolean b) {
+ mHasRetrievedAll = b;
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mIsRefreshing = refreshing;
+ notifyDataSetChanged();
+ }
+
+ public int getDataCount() {
+ return mCursorDataCount;
+ }
+
+ private void setGridItemWidth() {
+ int maxWidth = mContext.getResources().getDisplayMetrics().widthPixels;
+ int columnCount = getColumnCount(mContext);
+ if (columnCount > 0) {
+ int dp8 = DisplayUtils.dpToPx(mContext, 8);
+ int padding = (columnCount + 1) * dp8;
+ mGridItemWidth = (maxWidth - padding) / columnCount;
+ }
+ }
+
+ public void clearSelection() {
+ mSelectedItems.clear();
+ }
+
+ public boolean isItemSelected(String mediaId) {
+ return mSelectedItems.contains(mediaId);
+ }
+
+ public void setItemSelected(int position, boolean selected) {
+ Cursor cursor = (Cursor) getItem(position);
+ if (cursor == null) {
+ return;
+ }
+ int columnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID);
+ if (columnIndex != -1) {
+ String mediaId = cursor.getString(columnIndex);
+ setItemSelected(mediaId, selected);
+ }
+ }
+
+ public void setItemSelected(String mediaId, boolean selected) {
+ if (selected) {
+ mSelectedItems.add(mediaId);
+ } else {
+ mSelectedItems.remove(mediaId);
+ }
+ notifyDataSetChanged();
+ }
+
+ public void toggleItemSelected(int position) {
+ Cursor cursor = (Cursor) getItem(position);
+ int columnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID);
+ if (columnIndex != -1) {
+ String mediaId = cursor.getString(columnIndex);
+ if (mSelectedItems.contains(mediaId)) {
+ mSelectedItems.remove(mediaId);
+ } else {
+ mSelectedItems.add(mediaId);
+ }
+ notifyDataSetChanged();
+ }
+ }
+
+ public void setSelectedItems(ArrayList<String> selectedItems) {
+ mSelectedItems = selectedItems;
+ notifyDataSetChanged();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java
new file mode 100644
index 000000000..793a8e647
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java
@@ -0,0 +1,836 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView.RecyclerListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.DatePicker;
+import android.widget.GridView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader.ImageContainer;
+import com.android.volley.toolbox.ImageLoader.ImageListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.CheckableFrameLayout;
+import org.wordpress.android.ui.CustomSpinner;
+import org.wordpress.android.ui.EmptyViewMessageType;
+import org.wordpress.android.ui.media.MediaGridAdapter.MediaGridAdapterCallback;
+import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.SyncMediaLibraryTask.Callback;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+/**
+ * The grid displaying the media items.
+ */
+public class MediaGridFragment extends Fragment
+ implements OnItemClickListener, MediaGridAdapterCallback, RecyclerListener {
+ private static final String BUNDLE_SELECTED_STATES = "BUNDLE_SELECTED_STATES";
+ private static final String BUNDLE_IN_MULTI_SELECT_MODE = "BUNDLE_IN_MULTI_SELECT_MODE";
+ private static final String BUNDLE_SCROLL_POSITION = "BUNDLE_SCROLL_POSITION";
+ private static final String BUNDLE_HAS_RETREIEVED_ALL_MEDIA = "BUNDLE_HAS_RETREIEVED_ALL_MEDIA";
+ private static final String BUNDLE_FILTER = "BUNDLE_FILTER";
+ private static final String BUNDLE_EMPTY_VIEW_MESSAGE = "BUNDLE_EMPTY_VIEW_MESSAGE";
+
+ private static final String BUNDLE_DATE_FILTER_SET = "BUNDLE_DATE_FILTER_SET";
+ private static final String BUNDLE_DATE_FILTER_VISIBLE = "BUNDLE_DATE_FILTER_VISIBLE";
+ private static final String BUNDLE_DATE_FILTER_START_YEAR = "BUNDLE_DATE_FILTER_START_YEAR";
+ private static final String BUNDLE_DATE_FILTER_START_MONTH = "BUNDLE_DATE_FILTER_START_MONTH";
+ private static final String BUNDLE_DATE_FILTER_START_DAY = "BUNDLE_DATE_FILTER_START_DAY";
+ private static final String BUNDLE_DATE_FILTER_END_YEAR = "BUNDLE_DATE_FILTER_END_YEAR";
+ private static final String BUNDLE_DATE_FILTER_END_MONTH = "BUNDLE_DATE_FILTER_END_MONTH";
+ private static final String BUNDLE_DATE_FILTER_END_DAY = "BUNDLE_DATE_FILTER_END_DAY";
+
+ private Filter mFilter = Filter.ALL;
+ private String[] mFiltersText;
+ private GridView mGridView;
+ private MediaGridAdapter mGridAdapter;
+ private MediaGridListener mListener;
+
+ private boolean mIsRefreshing;
+ private boolean mHasRetrievedAllMedia;
+ private boolean mIsMultiSelect;
+ private String mSearchTerm;
+
+ private View mSpinnerContainer;
+ private TextView mResultView;
+ private CustomSpinner mSpinner;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+
+ private LinearLayout mEmptyView;
+ private TextView mEmptyViewTitle;
+ private EmptyViewMessageType mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT;
+
+ private int mOldMediaSyncOffset = 0;
+
+ private boolean mIsDateFilterSet;
+ private boolean mSpinnerHasLaunched;
+
+ private int mStartYear, mStartMonth, mStartDay, mEndYear, mEndMonth, mEndDay;
+ private AlertDialog mDatePickerDialog;
+
+ public interface MediaGridListener {
+ public void onMediaItemListDownloadStart();
+ public void onMediaItemListDownloaded();
+ public void onMediaItemSelected(String mediaId);
+ public void onRetryUpload(String mediaId);
+ }
+
+ public enum Filter {
+ ALL, IMAGES, UNATTACHED, CUSTOM_DATE;
+
+ public static Filter getFilter(int filterPos) {
+ if (filterPos > Filter.values().length)
+ return ALL;
+ else
+ return Filter.values()[filterPos];
+ }
+ }
+
+ private final OnItemSelectedListener mFilterSelectedListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ // need this to stop the bug where onItemSelected is called during initialization, before user input
+ if (!mSpinnerHasLaunched) {
+ return;
+ }
+ if (position == Filter.CUSTOM_DATE.ordinal()) {
+ mIsDateFilterSet = true;
+ }
+ setFilter(Filter.getFilter(position));
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ };
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ mFiltersText = new String[Filter.values().length];
+ mGridAdapter = new MediaGridAdapter(getActivity(), null, 0, MediaImageLoader.getInstance());
+ mGridAdapter.setCallback(this);
+
+ View view = inflater.inflate(R.layout.media_grid_fragment, container);
+
+ mGridView = (GridView) view.findViewById(R.id.media_gridview);
+ mGridView.setOnItemClickListener(this);
+ mGridView.setRecyclerListener(this);
+ mGridView.setMultiChoiceModeListener(new MultiChoiceModeListener());
+ mGridView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL);
+ mGridView.setAdapter(mGridAdapter);
+
+ mEmptyView = (LinearLayout) view.findViewById(R.id.empty_view);
+ mEmptyViewTitle = (TextView) view.findViewById(R.id.empty_view_title);
+
+ mResultView = (TextView) view.findViewById(R.id.media_filter_result_text);
+
+ mSpinner = (CustomSpinner) view.findViewById(R.id.media_filter_spinner);
+ mSpinner.setOnItemSelectedListener(mFilterSelectedListener);
+ mSpinner.setOnItemSelectedEvenIfUnchangedListener(mFilterSelectedListener);
+
+ mSpinnerContainer = view.findViewById(R.id.media_filter_spinner_container);
+ mSpinnerContainer.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isInMultiSelect()) {
+ mSpinnerHasLaunched = true;
+ mSpinner.performClick();
+ }
+ }
+
+ });
+
+ // swipe to refresh setup
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(getActivity(),
+ (CustomSwipeRefreshLayout) view.findViewById(R.id.ptr_layout),
+ new RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!isAdded()) {
+ return;
+ }
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+ refreshMediaFromServer(0, false);
+ }
+ });
+ restoreState(savedInstanceState);
+ setupSpinnerAdapter();
+
+ return view;
+ }
+
+ private void restoreState(Bundle savedInstanceState) {
+ if (savedInstanceState == null)
+ return;
+
+ boolean isInMultiSelectMode = savedInstanceState.getBoolean(BUNDLE_IN_MULTI_SELECT_MODE);
+
+ if (savedInstanceState.containsKey(BUNDLE_SELECTED_STATES)) {
+ ArrayList selectedItems = savedInstanceState.getStringArrayList(BUNDLE_SELECTED_STATES);
+ mGridAdapter.setSelectedItems(selectedItems);
+ if (isInMultiSelectMode) {
+ setFilterSpinnerVisible(mGridAdapter.getSelectedItems().size() == 0);
+ mSwipeToRefreshHelper.setEnabled(false);
+ }
+ }
+
+ mGridView.setSelection(savedInstanceState.getInt(BUNDLE_SCROLL_POSITION, 0));
+ mHasRetrievedAllMedia = savedInstanceState.getBoolean(BUNDLE_HAS_RETREIEVED_ALL_MEDIA, false);
+ mFilter = Filter.getFilter(savedInstanceState.getInt(BUNDLE_FILTER));
+ mEmptyViewMessageType = EmptyViewMessageType.getEnumFromString(savedInstanceState.
+ getString(BUNDLE_EMPTY_VIEW_MESSAGE));
+
+ mIsDateFilterSet = savedInstanceState.getBoolean(BUNDLE_DATE_FILTER_SET, false);
+ mStartDay = savedInstanceState.getInt(BUNDLE_DATE_FILTER_START_DAY);
+ mStartMonth = savedInstanceState.getInt(BUNDLE_DATE_FILTER_START_MONTH);
+ mStartYear = savedInstanceState.getInt(BUNDLE_DATE_FILTER_START_YEAR);
+ mEndDay = savedInstanceState.getInt(BUNDLE_DATE_FILTER_END_DAY);
+ mEndMonth = savedInstanceState.getInt(BUNDLE_DATE_FILTER_END_MONTH);
+ mEndYear = savedInstanceState.getInt(BUNDLE_DATE_FILTER_END_YEAR);
+
+ boolean datePickerShowing = savedInstanceState.getBoolean(BUNDLE_DATE_FILTER_VISIBLE);
+ if (datePickerShowing)
+ showDatePicker();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveState(outState);
+ }
+
+ private void saveState(Bundle outState) {
+ outState.putStringArrayList(BUNDLE_SELECTED_STATES, mGridAdapter.getSelectedItems());
+ outState.putInt(BUNDLE_SCROLL_POSITION, mGridView.getFirstVisiblePosition());
+ outState.putBoolean(BUNDLE_HAS_RETREIEVED_ALL_MEDIA, mHasRetrievedAllMedia);
+ outState.putBoolean(BUNDLE_IN_MULTI_SELECT_MODE, isInMultiSelect());
+ outState.putInt(BUNDLE_FILTER, mFilter.ordinal());
+ outState.putString(BUNDLE_EMPTY_VIEW_MESSAGE, mEmptyViewMessageType.name());
+
+ outState.putBoolean(BUNDLE_DATE_FILTER_SET, mIsDateFilterSet);
+ outState.putBoolean(BUNDLE_DATE_FILTER_VISIBLE, (mDatePickerDialog != null && mDatePickerDialog.isShowing()));
+ outState.putInt(BUNDLE_DATE_FILTER_START_DAY, mStartDay);
+ outState.putInt(BUNDLE_DATE_FILTER_START_MONTH, mStartMonth);
+ outState.putInt(BUNDLE_DATE_FILTER_START_YEAR, mStartYear);
+ outState.putInt(BUNDLE_DATE_FILTER_END_DAY, mEndDay);
+ outState.putInt(BUNDLE_DATE_FILTER_END_MONTH, mEndMonth);
+ outState.putInt(BUNDLE_DATE_FILTER_END_YEAR, mEndYear);
+ }
+
+ private void setupSpinnerAdapter() {
+ if (getActivity() == null || WordPress.getCurrentBlog() == null) {
+ return;
+ }
+
+ updateFilterText();
+
+ Context context = WPActivityUtils.getThemedContext(getActivity());
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, R.layout.spinner_menu_dropdown_item, mFiltersText);
+ mSpinner.setAdapter(adapter);
+ mSpinner.setSelection(mFilter.ordinal());
+ }
+
+ public void refreshSpinnerAdapter() {
+ updateFilterText();
+ updateSpinnerAdapter();
+ setFilter(mFilter);
+ }
+
+ void resetSpinnerAdapter() {
+ setFiltersText(0, 0, 0);
+ updateSpinnerAdapter();
+ }
+
+ void updateFilterText() {
+ if (WordPress.currentBlog == null)
+ return;
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+
+ int countAll = WordPress.wpDB.getMediaCountAll(blogId);
+ int countImages = WordPress.wpDB.getMediaCountImages(blogId);
+ int countUnattached = WordPress.wpDB.getMediaCountUnattached(blogId);
+
+ setFiltersText(countAll, countImages, countUnattached);
+ }
+
+ private void setFiltersText(int countAll, int countImages, int countUnattached) {
+ mFiltersText[0] = getResources().getString(R.string.all) + " (" + countAll + ")";
+ mFiltersText[1] = getResources().getString(R.string.images) + " (" + countImages + ")";
+ mFiltersText[2] = getResources().getString(R.string.unattached) + " (" + countUnattached + ")";
+ mFiltersText[3] = getResources().getString(R.string.custom_date) + "...";
+ }
+
+ void updateSpinnerAdapter() {
+ ArrayAdapter<String> adapter = (ArrayAdapter<String>) mSpinner.getAdapter();
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mListener = (MediaGridListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement MediaGridListener");
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshSpinnerAdapter();
+ refreshMediaFromDB();
+ }
+
+ public void refreshMediaFromDB() {
+ setFilter(mFilter);
+ if (isAdded() && mGridAdapter.getDataCount() == 0) {
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ if (!mHasRetrievedAllMedia) {
+ refreshMediaFromServer(0, true);
+ }
+ } else {
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ }
+ }
+ }
+
+ public void refreshMediaFromServer(int offset, final boolean auto) {
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ setRefreshing(false);
+ return;
+ }
+
+ // do not refresh if custom date filter is shown
+ if (WordPress.getCurrentBlog() == null || mFilter == Filter.CUSTOM_DATE) {
+ setRefreshing(false);
+ return;
+ }
+
+ // do not refresh if in search
+ if (mSearchTerm != null && mSearchTerm.length() > 0) {
+ setRefreshing(false);
+ return;
+ }
+
+ if (offset == 0 || !mIsRefreshing) {
+ if (offset == mOldMediaSyncOffset) {
+ // we're pulling the same data again for some reason. Pull from the beginning.
+ offset = 0;
+ }
+ mOldMediaSyncOffset = offset;
+
+ mIsRefreshing = true;
+ updateEmptyView(EmptyViewMessageType.LOADING);
+ mListener.onMediaItemListDownloadStart();
+ mGridAdapter.setRefreshing(true);
+
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(WordPress.getCurrentBlog());
+
+ Callback callback = new Callback() {
+ // refresh db from server. If returned count is 0, we've retrieved all the media.
+ // stop retrieving until the user manually refreshes
+
+ @Override
+ public void onSuccess(int count) {
+ MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter();
+ mHasRetrievedAllMedia = (count == 0);
+ adapter.setHasRetrievedAll(mHasRetrievedAllMedia);
+
+ mIsRefreshing = false;
+
+ // the activity may be gone by the time this finishes, so check for it
+ if (getActivity() != null && MediaGridFragment.this.isVisible()) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ refreshSpinnerAdapter();
+ updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ if (!auto) {
+ mGridView.setSelection(0);
+ }
+ mListener.onMediaItemListDownloaded();
+ mGridAdapter.setRefreshing(false);
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onFailure(final ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ if (errorType != ApiHelper.ErrorType.NO_ERROR) {
+ if (getActivity() != null) {
+ if (errorType != ApiHelper.ErrorType.NO_UPLOAD_FILES_CAP) {
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_media),
+ Duration.LONG);
+ } else {
+ if (mEmptyView == null || mEmptyView.getVisibility() != View.VISIBLE) {
+ ToastUtils.showToast(getActivity(), getString(
+ R.string.media_error_no_permission));
+ }
+ }
+ }
+ MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter();
+ mHasRetrievedAllMedia = true;
+ adapter.setHasRetrievedAll(mHasRetrievedAllMedia);
+ }
+
+ // the activity may be cone by the time we get this, so check for it
+ if (getActivity() != null && MediaGridFragment.this.isVisible()) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mIsRefreshing = false;
+ mListener.onMediaItemListDownloaded();
+ mGridAdapter.setRefreshing(false);
+ mSwipeToRefreshHelper.setRefreshing(false);
+ if (errorType == ApiHelper.ErrorType.NO_UPLOAD_FILES_CAP) {
+ updateEmptyView(EmptyViewMessageType.PERMISSION_ERROR);
+ } else {
+ updateEmptyView(EmptyViewMessageType.GENERIC_ERROR);
+ }
+ }
+ });
+ }
+ }
+ };
+
+ ApiHelper.SyncMediaLibraryTask getMediaTask = new ApiHelper.SyncMediaLibraryTask(offset, mFilter, callback);
+ getMediaTask.execute(apiArgs);
+ }
+ }
+
+ public void search(String searchTerm) {
+ mSearchTerm = searchTerm;
+ Blog blog = WordPress.getCurrentBlog();
+ if (blog != null) {
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+ Cursor cursor = WordPress.wpDB.getMediaFilesForBlog(blogId, searchTerm);
+ mGridAdapter.changeCursor(cursor);
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Cursor cursor = ((MediaGridAdapter) parent.getAdapter()).getCursor();
+ String mediaId = cursor.getString(cursor.getColumnIndex("mediaId"));
+ mListener.onMediaItemSelected(mediaId);
+ }
+
+ public void setFilterVisibility(int visibility) {
+ if (mSpinner != null) {
+ mSpinner.setVisibility(visibility);
+ }
+ }
+
+ private void updateEmptyView(EmptyViewMessageType emptyViewMessageType) {
+ if (mEmptyView != null) {
+ if (mGridAdapter.getDataCount() == 0) {
+ int stringId = 0;
+
+ switch (emptyViewMessageType) {
+ case LOADING:
+ stringId = R.string.media_fetching;
+ break;
+ case NO_CONTENT:
+ stringId = R.string.media_empty_list;
+ break;
+ case NETWORK_ERROR:
+ // Don't overwrite NO_CONTENT_CUSTOM_DATE message, since refresh is disabled with that filter on
+ if (mEmptyViewMessageType == EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ return;
+ }
+ stringId = R.string.no_network_message;
+ break;
+ case PERMISSION_ERROR:
+ stringId = R.string.media_error_no_permission;
+ break;
+ case GENERIC_ERROR:
+ stringId = R.string.error_refresh_media;
+ break;
+ case NO_CONTENT_CUSTOM_DATE:
+ stringId = R.string.media_empty_list_custom_date;
+ break;
+ }
+
+ mEmptyViewTitle.setText(getText(stringId));
+ mEmptyViewMessageType = emptyViewMessageType;
+ mEmptyView.setVisibility(View.VISIBLE);
+ } else {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void hideEmptyView() {
+ if (mEmptyView != null) {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ }
+
+ public void setFilter(Filter filter) {
+ mFilter = filter;
+ Cursor cursor = filterItems(mFilter);
+ if (filter != Filter.CUSTOM_DATE || cursor == null || cursor.getCount() == 0) {
+ mResultView.setVisibility(View.GONE);
+ }
+ if (cursor != null && cursor.getCount() != 0) {
+ mGridAdapter.swapCursor(cursor);
+ hideEmptyView();
+ } else {
+ // No data to display. Clear the GridView and display a message in the empty view
+ mGridAdapter.changeCursor(null);
+ }
+ if (filter != Filter.CUSTOM_DATE) {
+ // Overwrite the LOADING and NO_CONTENT_CUSTOM_DATE messages
+ if (mEmptyViewMessageType == EmptyViewMessageType.LOADING ||
+ mEmptyViewMessageType == EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE) {
+ updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ } else {
+ updateEmptyView(mEmptyViewMessageType);
+ }
+ } else {
+ updateEmptyView(EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE);
+ }
+ }
+
+ Cursor setDateFilter() {
+ Blog blog = WordPress.getCurrentBlog();
+
+ if (blog == null)
+ return null;
+
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+
+ GregorianCalendar startDate = new GregorianCalendar(mStartYear, mStartMonth, mStartDay);
+ GregorianCalendar endDate = new GregorianCalendar(mEndYear, mEndMonth, mEndDay);
+
+ long one_day = 24 * 60 * 60 * 1000;
+ Cursor cursor = WordPress.wpDB.getMediaFilesForBlog(blogId, startDate.getTimeInMillis(), endDate.getTimeInMillis() + one_day);
+ mGridAdapter.swapCursor(cursor);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ mResultView.setVisibility(View.VISIBLE);
+ hideEmptyView();
+ DateFormat format = DateFormat.getDateInstance();
+ String formattedStart = format.format(startDate.getTime());
+ String formattedEnd = format.format(endDate.getTime());
+ mResultView.setText(String.format(getString(R.string.media_gallery_date_range), formattedStart,
+ formattedEnd));
+ return cursor;
+ } else {
+ updateEmptyView(EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE);
+ }
+ return null;
+ }
+
+ public void clearSelectedItems() {
+ mGridAdapter.clearSelection();
+ }
+
+ private Cursor filterItems(Filter filter) {
+ Blog blog = WordPress.getCurrentBlog();
+
+ if (blog == null)
+ return null;
+
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+
+ switch (filter) {
+ case ALL:
+ return WordPress.wpDB.getMediaFilesForBlog(blogId);
+ case IMAGES:
+ return WordPress.wpDB.getMediaImagesForBlog(blogId);
+ case UNATTACHED:
+ return WordPress.wpDB.getMediaUnattachedForBlog(blogId);
+ case CUSTOM_DATE:
+ // show date picker only when the user clicks on the spinner, not when we are doing syncing
+ if (mIsDateFilterSet) {
+ mIsDateFilterSet = false;
+ showDatePicker();
+ } else {
+ return setDateFilter();
+ }
+ break;
+ }
+ return null;
+ }
+
+ void showDatePicker() {
+ // Inflate your custom layout containing 2 DatePickers
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View customView = inflater.inflate(R.layout.date_range_dialog, null);
+
+ // Define your date pickers
+ final DatePicker dpStartDate = (DatePicker) customView.findViewById(R.id.dpStartDate);
+ final DatePicker dpEndDate = (DatePicker) customView.findViewById(R.id.dpEndDate);
+
+ // Build the dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setView(customView); // Set the view of the dialog to your custom layout
+ builder.setTitle("Select start and end date");
+ builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mStartYear = dpStartDate.getYear();
+ mStartMonth = dpStartDate.getMonth();
+ mStartDay = dpStartDate.getDayOfMonth();
+ mEndYear = dpEndDate.getYear();
+ mEndMonth = dpEndDate.getMonth();
+ mEndDay = dpEndDate.getDayOfMonth();
+ setDateFilter();
+
+ dialog.dismiss();
+ }
+ });
+
+ // Create and show the dialog
+ mDatePickerDialog = builder.create();
+ mDatePickerDialog.show();
+ }
+
+ @Override
+ public void fetchMoreData(int offset) {
+ if (!mHasRetrievedAllMedia) {
+ refreshMediaFromServer(offset, true);
+ }
+ }
+
+ @Override
+ public void onMovedToScrapHeap(View view) {
+ // cancel image fetch requests if the view has been moved to recycler.
+
+ View imageView = view.findViewById(R.id.media_grid_item_image);
+ if (imageView != null) {
+ // this tag is set in the MediaGridAdapter class
+ String tag = (String) imageView.getTag();
+ if (tag != null && tag.startsWith("http")) {
+ // need a listener to cancel request, even if the listener does nothing
+ ImageContainer container = WordPress.imageLoader.get(tag, new ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) { }
+
+ @Override
+ public void onResponse(ImageContainer response, boolean isImmediate) { }
+
+ });
+ container.cancelRequest();
+ }
+ }
+
+ CheckableFrameLayout layout = (CheckableFrameLayout) view.findViewById(R.id.media_grid_frame_layout);
+ if (layout != null) {
+ layout.setOnCheckedChangeListener(null);
+ }
+ }
+
+ public void setFilterSpinnerVisible(boolean visible) {
+ if (visible) {
+ mSpinner.setEnabled(true);
+ mSpinnerContainer.setEnabled(true);
+ mSpinnerContainer.setVisibility(View.VISIBLE);
+ } else {
+ mSpinner.setEnabled(false);
+ mSpinnerContainer.setEnabled(false);
+ mSpinnerContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onRetryUpload(String mediaId) {
+ mListener.onRetryUpload(mediaId);
+ }
+
+ public boolean hasRetrievedAllMediaFromServer() {
+ return mHasRetrievedAllMedia;
+ }
+
+ /*
+ * called by activity when blog is changed
+ */
+ protected void reset() {
+ mGridAdapter.clearSelection();
+ mGridView.setSelection(0);
+ mGridView.requestFocusFromTouch();
+ mGridView.setSelection(0);
+ mGridAdapter.setImageLoader(MediaImageLoader.getInstance());
+ mGridAdapter.changeCursor(null);
+ resetSpinnerAdapter();
+ mHasRetrievedAllMedia = false;
+ }
+
+ public void removeFromMultiSelect(String mediaId) {
+ if (isInMultiSelect() && mGridAdapter.isItemSelected(mediaId)) {
+ mGridAdapter.setItemSelected(mediaId, false);
+ setFilterSpinnerVisible(mGridAdapter.getSelectedItems().size() == 0);
+ }
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mSwipeToRefreshHelper.setRefreshing(refreshing);
+ }
+
+ public void setSwipeToRefreshEnabled(boolean enabled) {
+ mSwipeToRefreshHelper.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean isInMultiSelect() {
+ return mIsMultiSelect;
+ }
+
+ public class MultiChoiceModeListener implements GridView.MultiChoiceModeListener {
+ private MenuItem mNewPostButton;
+ private MenuItem mNewGalleryButton;
+
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ int selectCount = mGridAdapter.getSelectedItems().size();
+ mode.setTitle(String.format(getString(R.string.cab_selected), selectCount));
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.media_multiselect, menu);
+ mNewPostButton = menu.findItem(R.id.media_multiselect_actionbar_post);
+ mNewGalleryButton = menu.findItem(R.id.media_multiselect_actionbar_gallery);
+ setSwipeToRefreshEnabled(false);
+ mIsMultiSelect = true;
+ updateActionButtons(selectCount);
+ return true;
+ }
+
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ int i = item.getItemId();
+ if (i == R.id.media_multiselect_actionbar_post) {
+ handleNewPost();
+ return true;
+ } else if (i == R.id.media_multiselect_actionbar_gallery) {
+ handleMultiSelectPost();
+ return true;
+ } else if (i == R.id.media_multiselect_actionbar_trash) {
+ handleMultiSelectDelete();
+ return true;
+ }
+ return true;
+ }
+
+ public void onDestroyActionMode(ActionMode mode) {
+ mGridAdapter.clearSelection();
+ setSwipeToRefreshEnabled(true);
+ mIsMultiSelect = false;
+ setFilterSpinnerVisible(mGridAdapter.getSelectedItems().size() == 0);
+ }
+
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ mGridAdapter.setItemSelected(position, checked);
+ int selectCount = mGridAdapter.getSelectedItems().size();
+ setFilterSpinnerVisible(selectCount == 0);
+ mode.setTitle(String.format(getString(R.string.cab_selected), selectCount));
+ updateActionButtons(selectCount);
+ }
+
+ private void updateActionButtons(int selectCount) {
+ switch (selectCount) {
+ case 1:
+ mNewPostButton.setVisible(true);
+ mNewGalleryButton.setVisible(false);
+ break;
+ default:
+ mNewPostButton.setVisible(false);
+ mNewGalleryButton.setVisible(true);
+ break;
+ }
+ }
+
+ private void handleNewPost() {
+ if (!isAdded()) {
+ return;
+ }
+ ArrayList<String> ids = mGridAdapter.getSelectedItems();
+ Intent i = new Intent(getActivity(), EditPostActivity.class);
+ i.setAction(EditPostActivity.NEW_MEDIA_POST);
+ i.putExtra(EditPostActivity.NEW_MEDIA_POST_EXTRA, ids.iterator().next());
+ startActivity(i);
+ }
+
+ private void handleMultiSelectDelete() {
+ if (!isAdded()) {
+ return;
+ }
+ Builder builder = new AlertDialog.Builder(getActivity()).setMessage(R.string.confirm_delete_multi_media)
+ .setCancelable(true).setPositiveButton(
+ R.string.delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (getActivity() instanceof MediaBrowserActivity) {
+ ((MediaBrowserActivity) getActivity()).deleteMedia(
+ mGridAdapter.getSelectedItems());
+ }
+ refreshSpinnerAdapter();
+ }
+ }).setNegativeButton(R.string.cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private void handleMultiSelectPost() {
+ if (!isAdded()) {
+ return;
+ }
+ Intent i = new Intent(getActivity(), EditPostActivity.class);
+ i.setAction(EditPostActivity.NEW_MEDIA_GALLERY);
+ i.putStringArrayListExtra(EditPostActivity.NEW_MEDIA_GALLERY_EXTRA_IDS,
+ mGridAdapter.getSelectedItems());
+ startActivity(i);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java
new file mode 100644
index 000000000..d484d2fa5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java
@@ -0,0 +1,42 @@
+package org.wordpress.android.ui.media;
+
+import android.content.Context;
+
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.Volley;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.VolleyUtils;
+
+/**
+ * provides the ImageLoader and backing RequestQueue for media image requests - necessary because
+ * images in protected blogs need to be authenticated, which requires a separate RequestQueue
+ */
+class MediaImageLoader {
+ private MediaImageLoader() {
+ throw new AssertionError();
+ }
+
+ static ImageLoader getInstance() {
+ return getInstance(WordPress.getCurrentBlog());
+ }
+
+ static ImageLoader getInstance(Blog blog) {
+ if (blog != null && VolleyUtils.isCustomHTTPClientStackNeeded(blog)) {
+ // use ImageLoader with authenticating request queue for protected blogs
+ AppLog.d(AppLog.T.MEDIA, "using custom imageLoader");
+ Context context = WordPress.getContext();
+ RequestQueue authRequestQueue = Volley.newRequestQueue(context, VolleyUtils.getHTTPClientStack(context, blog));
+ ImageLoader imageLoader = new ImageLoader(authRequestQueue, WordPress.getBitmapCache());
+ imageLoader.setBatchedResponseDelay(0);
+ return imageLoader;
+ } else {
+ // use default ImageLoader for all others
+ AppLog.d(AppLog.T.MEDIA, "using default imageLoader");
+ return WordPress.imageLoader;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java
new file mode 100644
index 000000000..53f5be0eb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java
@@ -0,0 +1,380 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Fragment;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback;
+import org.wordpress.android.util.ImageUtils.BitmapWorkerTask;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.SqlUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+
+/**
+ * A fragment display a media item's details.
+ */
+public class MediaItemFragment extends Fragment {
+ private static final String ARGS_MEDIA_ID = "media_id";
+
+ public static final String TAG = MediaItemFragment.class.getName();
+
+ private WPNetworkImageView mImageView;
+ private TextView mCaptionView;
+ private TextView mDescriptionView;
+ private TextView mDateView;
+ private TextView mFileNameView;
+ private TextView mFileTypeView;
+ private MediaItemFragmentCallback mCallback;
+
+ private boolean mIsLocal;
+ private String mImageUri;
+
+ public interface MediaItemFragmentCallback {
+ void onResume(Fragment fragment);
+ void onPause(Fragment fragment);
+ }
+
+ public static MediaItemFragment newInstance(String mediaId) {
+ MediaItemFragment fragment = new MediaItemFragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARGS_MEDIA_ID, mediaId);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mCallback = (MediaItemFragmentCallback) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement MediaItemFragmentCallback");
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mCallback.onResume(this);
+ loadMedia(getMediaId());
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mCallback.onPause(this);
+ }
+
+ public String getMediaId() {
+ if (getArguments() != null) {
+ return getArguments().getString(ARGS_MEDIA_ID);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.media_listitem_details, container, false);
+
+ mCaptionView = (TextView) view.findViewById(R.id.media_listitem_details_caption);
+ mDescriptionView = (TextView) view.findViewById(R.id.media_listitem_details_description);
+ mDateView = (TextView) view.findViewById(R.id.media_listitem_details_date);
+ mFileNameView = (TextView) view.findViewById(R.id.media_listitem_details_file_name);
+ mFileTypeView = (TextView) view.findViewById(R.id.media_listitem_details_file_type);
+ mImageView = (WPNetworkImageView) view.findViewById(R.id.media_listitem_details_image);
+
+ return view;
+ }
+
+ /** Loads the first media item for the current blog from the database **/
+ public void loadDefaultMedia() {
+ loadMedia(null);
+ }
+
+ public void loadMedia(String mediaId) {
+ Blog blog = WordPress.getCurrentBlog();
+
+ if (blog != null) {
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+
+ Cursor cursor = null;
+ try {
+ // if the id is null, get the first media item in the database
+ if (mediaId == null) {
+ cursor = WordPress.wpDB.getFirstMediaFileForBlog(blogId);
+ } else {
+ cursor = WordPress.wpDB.getMediaFile(blogId, mediaId);
+ }
+ refreshViews(cursor);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+ }
+ }
+
+ private void refreshViews(Cursor cursor) {
+ if (!isAdded() || !cursor.moveToFirst()) {
+ return;
+ }
+
+ // check whether or not to show the edit button
+ String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE));
+ mIsLocal = MediaUtils.isLocalFile(state);
+ if (mIsLocal && getActivity() != null) {
+ getActivity().invalidateOptionsMenu();
+ }
+
+ String caption = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_CAPTION));
+ if (TextUtils.isEmpty(caption)) {
+ mCaptionView.setVisibility(View.GONE);
+ } else {
+ mCaptionView.setText(caption);
+ mCaptionView.setVisibility(View.VISIBLE);
+ }
+
+ String desc = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DESCRIPTION));
+ if (TextUtils.isEmpty(desc)) {
+ mDescriptionView.setVisibility(View.GONE);
+ } else {
+ mDescriptionView.setText(desc);
+ mDescriptionView.setVisibility(View.VISIBLE);
+ }
+
+ String date = MediaUtils.getDate(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT)));
+ mDateView.setText(date);
+ TextView txtDateLabel = (TextView) getView().findViewById(R.id.media_listitem_details_date_label);
+ txtDateLabel.setText(
+ mIsLocal ? R.string.media_details_label_date_added : R.string.media_details_label_date_uploaded);
+
+ String fileURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL));
+ String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME));
+ mImageUri = TextUtils.isEmpty(fileURL)
+ ? cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH))
+ : fileURL;
+ boolean isValidImage = MediaUtils.isValidImage(mImageUri);
+
+ mFileNameView.setText(fileName);
+
+ float mediaWidth = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH));
+ float mediaHeight = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT));
+
+ // image and dimensions
+ if (isValidImage) {
+ int screenWidth = DisplayUtils.getDisplayPixelWidth(getActivity());
+ int screenHeight = DisplayUtils.getDisplayPixelHeight(getActivity());
+
+ // determine size for display
+ int imageWidth;
+ int imageHeight;
+ boolean isFullWidth;
+ if (mediaWidth == 0 || mediaHeight == 0) {
+ imageWidth = screenWidth;
+ imageHeight = screenHeight / 2;
+ isFullWidth = true;
+ } else if (mediaWidth > mediaHeight) {
+ float ratio = mediaHeight / mediaWidth;
+ imageWidth = Math.min(screenWidth, (int) mediaWidth);
+ imageHeight = (int) (imageWidth * ratio);
+ isFullWidth = (imageWidth == screenWidth);
+ } else {
+ float ratio = mediaWidth / mediaHeight;
+ imageHeight = Math.min(screenHeight / 2, (int) mediaHeight);
+ imageWidth = (int) (imageHeight * ratio);
+ isFullWidth = false;
+ }
+
+ // set the imageView's parent height to match the image so it takes up space while
+ // the image is loading
+ FrameLayout frameView = (FrameLayout) getView().findViewById(R.id.layout_image_frame);
+ frameView.setLayoutParams(
+ new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, imageHeight));
+
+ // add padding to the frame if the image isn't full-width
+ if (!isFullWidth) {
+ int hpadding = getResources().getDimensionPixelSize(R.dimen.content_margin);
+ int vpadding = getResources().getDimensionPixelSize(R.dimen.margin_extra_large);
+ frameView.setPadding(hpadding, vpadding, hpadding, vpadding);
+ }
+
+ if (mIsLocal) {
+ final String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH));
+ loadLocalImage(mImageView, filePath, imageWidth, imageHeight);
+ } else {
+ // Allow non-private wp.com and Jetpack blogs to use photon to get a higher res thumbnail
+ String thumbnailURL;
+ if (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isPhotonCapable()){
+ thumbnailURL = StringUtils.getPhotonUrl(mImageUri, imageWidth);
+ } else {
+ thumbnailURL = UrlUtils.removeQuery(mImageUri) + "?w=" + imageWidth;
+ }
+ mImageView.setImageUrl(thumbnailURL, WPNetworkImageView.ImageType.PHOTO);
+ }
+ } else {
+ // not an image so show placeholder icon
+ int placeholderResId = WordPressMediaUtils.getPlaceholder(mImageUri);
+ mImageView.setDefaultImageResId(placeholderResId);
+ mImageView.showDefaultImage();
+ }
+
+ // show dimens & file ext together
+ String dimens =
+ (mediaWidth > 0 && mediaHeight > 0) ? (int) mediaWidth + " x " + (int) mediaHeight : null;
+ String fileExt =
+ TextUtils.isEmpty(fileURL) ? null : fileURL.replaceAll(".*\\.(\\w+)$", "$1").toUpperCase();
+ boolean hasDimens = !TextUtils.isEmpty(dimens);
+ boolean hasExt = !TextUtils.isEmpty(fileExt);
+ if (hasDimens & hasExt) {
+ mFileTypeView.setText(fileExt + ", " + dimens);
+ mFileTypeView.setVisibility(View.VISIBLE);
+ } else if (hasExt) {
+ mFileTypeView.setText(fileExt);
+ mFileTypeView.setVisibility(View.VISIBLE);
+ } else {
+ mFileTypeView.setVisibility(View.GONE);
+ }
+
+ // enable fullscreen photo for non-local
+ if (!mIsLocal && isValidImage) {
+ mImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Blog blog = WordPress.getCurrentBlog();
+ boolean isPrivate = blog != null && blog.isPrivate();
+ EnumSet<PhotoViewerOption> imageOptions = EnumSet.noneOf(PhotoViewerOption.class);
+ if (isPrivate) {
+ imageOptions.add(PhotoViewerOption.IS_PRIVATE_IMAGE);
+ }
+ ReaderActivityLauncher.showReaderPhotoViewer(
+ v.getContext(), mImageUri, imageOptions);
+ }
+ });
+ }
+ }
+
+ private synchronized void loadLocalImage(ImageView imageView, String filePath, int width, int height) {
+ if (MediaUtils.isValidImage(filePath)) {
+ imageView.setTag(filePath);
+
+ Bitmap bitmap = WordPress.getBitmapCache().get(filePath);
+ if (bitmap != null) {
+ imageView.setImageBitmap(bitmap);
+ } else {
+ BitmapWorkerTask task = new BitmapWorkerTask(imageView, width, height, new BitmapWorkerCallback() {
+ @Override
+ public void onBitmapReady(String path, ImageView imageView, Bitmap bitmap) {
+ imageView.setImageBitmap(bitmap);
+ WordPress.getBitmapCache().put(path, bitmap);
+ }
+ });
+ task.execute(filePath);
+ }
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.media_details, menu);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.menu_new_media).setVisible(false);
+ menu.findItem(R.id.menu_search).setVisible(false);
+
+ menu.findItem(R.id.menu_edit_media).setVisible(
+ !mIsLocal && WordPressMediaUtils.isWordPressVersionWithMediaEditingCapabilities());
+
+ menu.findItem(R.id.menu_copy_media_url).setVisible(!mIsLocal && !TextUtils.isEmpty(mImageUri));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemId = item.getItemId();
+
+ if (itemId == R.id.menu_delete) {
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ boolean canDeleteMedia = WordPressMediaUtils.canDeleteMedia(blogId, getMediaId());
+ if (!canDeleteMedia) {
+ Toast.makeText(getActivity(), R.string.wait_until_upload_completes, Toast.LENGTH_LONG).show();
+ return true;
+ }
+
+ Builder builder = new AlertDialog.Builder(getActivity()).setMessage(R.string.confirm_delete_media)
+ .setCancelable(true).setPositiveButton(
+ R.string.delete, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ArrayList<String> ids = new ArrayList<>(1);
+ ids.add(getMediaId());
+ if (getActivity() instanceof MediaBrowserActivity) {
+ ((MediaBrowserActivity) getActivity()).deleteMedia(ids);
+ }
+ }
+ }).setNegativeButton(R.string.cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ return true;
+ } else if (itemId == R.id.menu_copy_media_url) {
+ copyUrlToClipboard();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void copyUrlToClipboard() {
+ try {
+ ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(ClipData.newPlainText(mImageUri, mImageUri));
+ ToastUtils.showToast(getActivity(), R.string.media_details_copy_url_toast);
+ } catch (Exception e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ ToastUtils.showToast(getActivity(), R.string.error_copy_to_clipboard);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java
new file mode 100644
index 000000000..ea4664f67
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java
@@ -0,0 +1,538 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.TabLayout;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.Surface;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.widget.LinearLayout;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.widgets.WPViewPager;
+import org.wordpress.mediapicker.MediaItem;
+import org.wordpress.mediapicker.MediaPickerFragment;
+import org.wordpress.mediapicker.source.MediaSource;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Allows users to select a variety of videos and images from any source.
+ *
+ * Title can be set either by defining a string resource, R.string.media_picker_title, or passing
+ * a String extra in the {@link android.content.Intent} with the key ACTIVITY_TITLE_KEY.
+ *
+ * Accepts Image and Video sources as arguments and displays each in a tab.
+ * - Use DEVICE_IMAGE_MEDIA_SOURCES_KEY with a {@link java.util.List} of {@link org.wordpress.mediapicker.source.MediaSource}'s to pass image sources via the Intent
+ * - Use DEVICE_VIDEO_MEDIA_SOURCES_KEY with a {@link java.util.List} of {@link org.wordpress.mediapicker.source.MediaSource}'s to pass video sources via the Intent
+ */
+
+public class MediaPickerActivity extends AppCompatActivity
+ implements MediaPickerFragment.OnMediaSelected {
+ /**
+ * Request code for the {@link android.content.Intent} to start media selection.
+ */
+ public static final int ACTIVITY_REQUEST_CODE_MEDIA_SELECTION = 6000;
+ /**
+ * Result code signaling that media has been selected.
+ */
+ public static final int ACTIVITY_RESULT_CODE_MEDIA_SELECTED = 6001;
+ /**
+ * Result code signaling that a gallery should be created with the results.
+ */
+ public static final int ACTIVITY_RESULT_CODE_GALLERY_CREATED = 6002;
+
+ /**
+ * Pass a {@link String} with this key in the {@link android.content.Intent} to set the title.
+ */
+ public static final String ACTIVITY_TITLE_KEY = "activity-title";
+ /**
+ * Pass an {@link java.util.ArrayList} of {@link org.wordpress.mediapicker.source.MediaSource}'s
+ * in the {@link android.content.Intent} to set image sources for selection.
+ */
+ public static final String DEVICE_IMAGE_MEDIA_SOURCES_KEY = "device-image-media-sources";
+ /**
+ * Pass an {@link java.util.ArrayList} of {@link org.wordpress.mediapicker.source.MediaSource}'s
+ * in the {@link android.content.Intent} to set video sources for selection.
+ */
+ public static final String DEVICE_VIDEO_MEDIA_SOURCES_KEY = "device- video=media-sources";
+ public static final String BLOG_IMAGE_MEDIA_SOURCES_KEY = "blog-image-media-sources";
+ public static final String BLOG_VIDEO_MEDIA_SOURCES_KEY = "blog-video-media-sources";
+ /**
+ * Key to extract the {@link java.util.ArrayList} of {@link org.wordpress.mediapicker.MediaItem}'s
+ * that were selected by the user.
+ */
+ public static final String SELECTED_CONTENT_RESULTS_KEY = "selected-content";
+
+ private static final String CAPTURE_PATH_KEY = "capture-path";
+
+ private static final long TAB_ANIMATION_DURATION_MS = 250L;
+
+ private MediaPickerAdapter mMediaPickerAdapter;
+ private ArrayList<MediaSource>[] mMediaSources;
+ private TabLayout mTabLayout;
+ private WPViewPager mViewPager;
+ private ActionMode mActionMode;
+ private String mCapturePath;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ lockRotation();
+ addMediaSources();
+ setTitle();
+ initializeContentView();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.media_picker, menu);
+
+ return true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putString(CAPTURE_PATH_KEY, mCapturePath);
+ }
+
+ @Override
+ public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+
+ if (savedInstanceState.containsKey(CAPTURE_PATH_KEY)) {
+ mCapturePath = savedInstanceState.getString(CAPTURE_PATH_KEY);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ } else if (item.getItemId() == R.id.capture_image) {
+ WordPressMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID,
+ new WordPressMediaUtils.LaunchCameraCallback() {
+ @Override
+ public void onMediaCapturePathReady(String mediaCapturePath) {
+ mCapturePath = mediaCapturePath;
+ }
+ });
+ return true;
+ } else if (item.getItemId() == R.id.capture_video) {
+ WordPressMediaUtils.launchVideoCamera(this);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case RequestCodes.TAKE_PHOTO:
+ File file = new File(mCapturePath);
+ Uri imageUri = Uri.fromFile(file);
+
+ if (file.exists() && MediaUtils.isValidImage(imageUri.toString())) {
+ // Notify MediaStore of new content
+ sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, imageUri));
+
+ MediaItem newImage = new MediaItem();
+ newImage.setSource(imageUri);
+ newImage.setPreviewSource(imageUri);
+ ArrayList<MediaItem> imageResult = new ArrayList<>();
+ imageResult.add(newImage);
+ finishWithResults(imageResult, ACTIVITY_RESULT_CODE_MEDIA_SELECTED);
+ }
+ break;
+ case RequestCodes.TAKE_VIDEO:
+ Uri videoUri = data != null ? data.getData() : null;
+
+ if (videoUri != null) {
+ // Notify MediaStore of new content
+ sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, videoUri));
+
+ MediaItem newVideo = new MediaItem();
+ newVideo.setSource(videoUri);
+ newVideo.setPreviewSource(videoUri);
+ ArrayList<MediaItem> videoResult = new ArrayList<>();
+ videoResult.add(newVideo);
+ finishWithResults(videoResult, ACTIVITY_RESULT_CODE_MEDIA_SELECTED);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ } else {
+ finishWithResults(null, ACTIVITY_RESULT_CODE_MEDIA_SELECTED);
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ super.onActionModeStarted(mode);
+
+ mViewPager.setPagingEnabled(false);
+ mActionMode = mode;
+
+ animateTabGone();
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ super.onActionModeFinished(mode);
+
+ mViewPager.setPagingEnabled(true);
+ mActionMode = null;
+
+ animateTabAppear();
+ }
+
+ /*
+ OnMediaSelected interface
+ */
+
+ @Override
+ public void onMediaSelectionStarted() {
+ }
+
+ @Override
+ public void onMediaSelected(MediaItem mediaContent, boolean selected) {
+ }
+
+ @Override
+ public void onMediaSelectionConfirmed(ArrayList<MediaItem> mediaContent) {
+ if (mediaContent != null) {
+ finishWithResults(mediaContent, ACTIVITY_RESULT_CODE_MEDIA_SELECTED);
+ } else {
+ finish();
+ }
+ }
+
+ @Override
+ public void onMediaSelectionCancelled() {
+ }
+
+ @Override
+ public boolean onMenuItemSelected(MenuItem menuItem, ArrayList<MediaItem> selectedContent) {
+ if (menuItem.getItemId() == R.id.menu_media_content_selection_gallery) {
+ finishWithResults(selectedContent, ACTIVITY_RESULT_CODE_GALLERY_CREATED);
+ }
+
+ return false;
+ }
+
+ @Override
+ public ImageLoader.ImageCache getImageCache() {
+ return WordPress.getBitmapCache();
+ }
+
+ /**
+ * Finishes the activity after the user has confirmed media selection.
+ *
+ * @param results
+ * list of selected media items
+ */
+ private void finishWithResults(ArrayList<MediaItem> results, int resultCode) {
+ Intent result = new Intent();
+ result.putParcelableArrayListExtra(SELECTED_CONTENT_RESULTS_KEY, results);
+ setResult(resultCode, result);
+ finish();
+ }
+
+ /**
+ * Helper method; sets title to R.string.media_picker_title unless intent defines one
+ */
+ private void setTitle() {
+ final Intent intent = getIntent();
+
+ if (intent != null && intent.hasExtra(ACTIVITY_TITLE_KEY)) {
+ String activityTitle = intent.getStringExtra(ACTIVITY_TITLE_KEY);
+ setTitle(activityTitle);
+ } else {
+ setTitle(getString(R.string.media_picker_title));
+ }
+ }
+
+ /**
+ * Helper method; gathers {@link org.wordpress.mediapicker.source.MediaSource}'s from intent
+ */
+ private void addMediaSources() {
+ final Intent intent = getIntent();
+
+ if (intent != null) {
+ mMediaSources = new ArrayList[4];
+
+ List<MediaSource> mediaSources = intent.getParcelableArrayListExtra(DEVICE_IMAGE_MEDIA_SOURCES_KEY);
+ if (mediaSources != null) {
+ mMediaSources[0] = new ArrayList<>();
+ mMediaSources[0].addAll(mediaSources);
+ }
+
+ mediaSources = intent.getParcelableArrayListExtra(DEVICE_VIDEO_MEDIA_SOURCES_KEY);
+ if (mediaSources != null) {
+ mMediaSources[1] = new ArrayList<>();
+ mMediaSources[1].addAll(mediaSources);
+ }
+
+ mediaSources = intent.getParcelableArrayListExtra(BLOG_IMAGE_MEDIA_SOURCES_KEY);
+ if (mediaSources != null) {
+ mMediaSources[2] = new ArrayList<>();
+ mMediaSources[2].addAll(mediaSources);
+ }
+
+ mediaSources = intent.getParcelableArrayListExtra(BLOG_VIDEO_MEDIA_SOURCES_KEY);
+ if (mediaSources != null) {
+ mMediaSources[3] = new ArrayList<>();
+ mMediaSources[3].addAll(mediaSources);
+ }
+ }
+ }
+
+ /**
+ * Helper method; locks device orientation to its current state while media is being selected
+ */
+ private void lockRotation() {
+ switch (getWindowManager().getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_0:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ break;
+ case Surface.ROTATION_90:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ break;
+ case Surface.ROTATION_180:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
+ break;
+ case Surface.ROTATION_270:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ break;
+ }
+ }
+
+ /**
+ * Helper method; sets up the tab bar, media adapter, and ViewPager for displaying media content
+ */
+ private void initializeContentView() {
+ setContentView(R.layout.media_picker_activity);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayShowHomeEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.show();
+ }
+
+ mMediaPickerAdapter = new MediaPickerAdapter(getFragmentManager());
+ mTabLayout = (TabLayout) findViewById(R.id.tab_layout);
+ mViewPager = (WPViewPager) findViewById(R.id.media_picker_pager);
+
+ if (mViewPager != null) {
+ mViewPager.setPagingEnabled(true);
+
+ mMediaPickerAdapter.addTab(mMediaSources[0] != null ? mMediaSources[0] :
+ new ArrayList<MediaSource>(),
+ getString(R.string.tab_title_device_images),
+ getString(R.string.loading_images),
+ getString(R.string.error_loading_images),
+ getString(R.string.no_device_images));
+ mMediaPickerAdapter.addTab(mMediaSources[1] != null ? mMediaSources[1] :
+ new ArrayList<MediaSource>(),
+ getString(R.string.tab_title_device_videos),
+ getString(R.string.loading_videos),
+ getString(R.string.error_loading_videos),
+ getString(R.string.no_device_videos));
+ mMediaPickerAdapter.addTab(mMediaSources[2] != null ? mMediaSources[2] :
+ new ArrayList<MediaSource>(),
+ getString(R.string.tab_title_site_images),
+ getString(R.string.loading_blog_images),
+ getString(R.string.error_loading_blog_images),
+ getString(R.string.no_blog_images));
+ mMediaPickerAdapter.addTab(mMediaSources[3] != null ? mMediaSources[3] :
+ new ArrayList<MediaSource>(),
+ getString(R.string.tab_title_site_videos),
+ getString(R.string.loading_blog_videos),
+ getString(R.string.error_loading_blog_videos),
+ getString(R.string.no_blog_videos));
+
+ mViewPager.setAdapter(mMediaPickerAdapter);
+
+ if (mTabLayout != null) {
+ int normalColor = getResources().getColor(R.color.blue_light);
+ int selectedColor = getResources().getColor(R.color.white);
+ mTabLayout.setTabTextColors(normalColor, selectedColor);
+ mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
+ mTabLayout.setupWithViewPager(mViewPager);
+ }
+ }
+ }
+
+ /**
+ * Helper method; animates the tab bar and ViewPager in when ActionMode ends
+ */
+ private void animateTabAppear() {
+ TranslateAnimation tabAppearAnimation = new TranslateAnimation(0, 0, -mTabLayout.getHeight(), 0);
+ TranslateAnimation pagerAppearAnimation = new TranslateAnimation(0, 0, -mTabLayout.getHeight(), 0);
+
+ tabAppearAnimation.setDuration(TAB_ANIMATION_DURATION_MS);
+ pagerAppearAnimation.setDuration(TAB_ANIMATION_DURATION_MS);
+ pagerAppearAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mViewPager.getWidth(), mViewPager.getHeight() - mTabLayout.getHeight());
+ mViewPager.setLayoutParams(params);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+
+ mTabLayout.setVisibility(View.VISIBLE);
+ mViewPager.startAnimation(pagerAppearAnimation);
+ mTabLayout.startAnimation(tabAppearAnimation);
+ }
+
+ /**
+ * Helper method; animates the tab bar and ViewPager out when ActionMode begins
+ */
+ private void animateTabGone() {
+ TranslateAnimation tabGoneAnimation = new TranslateAnimation(0, 0, 0, -mTabLayout.getHeight());
+ TranslateAnimation pagerGoneAnimation = new TranslateAnimation(0, 0, 0, -mTabLayout.getHeight());
+ tabGoneAnimation.setDuration(TAB_ANIMATION_DURATION_MS);
+ pagerGoneAnimation.setDuration(TAB_ANIMATION_DURATION_MS);
+ tabGoneAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mTabLayout.setVisibility(View.GONE);
+ mTabLayout.clearAnimation();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+ pagerGoneAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ LinearLayout.LayoutParams newParams = new LinearLayout.LayoutParams(mViewPager.getWidth(), mViewPager.getHeight() + mTabLayout.getHeight());
+ mViewPager.setLayoutParams(newParams);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mViewPager.clearAnimation();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+
+ mTabLayout.startAnimation(tabGoneAnimation);
+ mViewPager.startAnimation(pagerGoneAnimation);
+ }
+
+ /**
+ * Shows {@link org.wordpress.mediapicker.MediaPickerFragment}'s in a tabbed layout.
+ */
+ public class MediaPickerAdapter extends FragmentPagerAdapter {
+ private class MediaPicker {
+ public String loadingText;
+ public String errorText;
+ public String emptyText;
+ public String pickerTitle;
+ public ArrayList<MediaSource> mediaSources;
+
+ public MediaPicker(String name, String loading, String error, String empty, ArrayList<MediaSource> sources) {
+ loadingText = loading;
+ errorText = error;
+ emptyText = empty;
+ pickerTitle = name;
+ mediaSources = sources;
+ }
+ }
+
+ private final List<MediaPicker> mMediaPickers;
+
+ private MediaPickerAdapter(FragmentManager fragmentManager) {
+ super(fragmentManager);
+
+ mMediaPickers = new ArrayList<>();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if (position < mMediaPickers.size()) {
+ MediaPicker mediaPicker = mMediaPickers.get(position);
+ MediaPickerFragment fragment = new MediaPickerFragment();
+ fragment.setLoadingText(mediaPicker.loadingText);
+ fragment.setErrorText(mediaPicker.errorText);
+ fragment.setEmptyText(mediaPicker.emptyText);
+ fragment.setActionModeMenu(R.menu.menu_media_picker_action_mode);
+ fragment.setMediaSources(mediaPicker.mediaSources);
+
+ return fragment;
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return mMediaPickers.size();
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mMediaPickers.get(position).pickerTitle;
+ }
+
+ public void addTab(ArrayList<MediaSource> mediaSources, String tabName, String loading, String error, String empty) {
+ mMediaPickers.add(new MediaPicker(tabName, loading, error, empty, mediaSources));
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java
new file mode 100644
index 000000000..26e61ae12
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java
@@ -0,0 +1,264 @@
+package org.wordpress.android.ui.media;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Parcel;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.models.Blog;
+import org.wordpress.mediapicker.MediaItem;
+import org.wordpress.mediapicker.source.MediaSource;
+import org.wordpress.mediapicker.MediaUtils.LimitedBackgroundOperation;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaSourceWPImages implements MediaSource {
+ private final List<MediaItem> mVerifiedItems = new ArrayList<>();
+ private final List<MediaItem> mMediaItems = new ArrayList<>();
+
+ private OnMediaChange mListener;
+
+ public MediaSourceWPImages() {
+ }
+
+ @Override
+ public void gather(Context context) {
+ mMediaItems.clear();
+
+ Blog blog = WordPress.getCurrentBlog();
+
+ if (blog != null) {
+ Cursor imageCursor = WordPressMediaUtils.getWordPressMediaImages(String.valueOf(blog.getLocalTableBlogId()));
+
+ if (imageCursor != null) {
+ addWordPressImagesFromCursor(imageCursor);
+ imageCursor.close();
+ } else if (mListener != null){
+ mListener.onMediaLoaded(false);
+ }
+ } else if (mListener != null){
+ mListener.onMediaLoaded(false);
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ mMediaItems.clear();
+ }
+
+ @Override
+ public void setListener(OnMediaChange listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public int getCount() {
+ return mVerifiedItems.size();
+ }
+
+ @Override
+ public MediaItem getMedia(int position) {
+ return mVerifiedItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) {
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.media_item_wp_image, parent, false);
+ }
+
+ if (convertView != null) {
+ MediaItem mediaItem = mVerifiedItems.get(position);
+ Uri imageSource = mediaItem.getPreviewSource();
+ ImageView imageView = (ImageView) convertView.findViewById(R.id.wp_image_view_background);
+ if (imageView != null) {
+ if (imageSource != null) {
+ Bitmap imageBitmap = null;
+ if (cache != null) {
+ imageBitmap = cache.getBitmap(imageSource.toString());
+ }
+
+ if (imageBitmap == null) {
+ imageView.setImageDrawable(placeholderDrawable(convertView.getContext()));
+ WordPressMediaUtils.BackgroundDownloadWebImage bgDownload =
+ new WordPressMediaUtils.BackgroundDownloadWebImage(imageView);
+ imageView.setTag(bgDownload);
+ bgDownload.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mediaItem.getPreviewSource());
+ } else {
+ imageView.setImageBitmap(imageBitmap);
+ }
+ } else {
+ imageView.setTag(null);
+ imageView.setImageResource(R.color.grey_darken_10);
+ }
+ }
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) {
+ return !selected;
+ }
+
+ private Drawable placeholderDrawable(Context context) {
+ if (context != null && context.getResources() != null) {
+ return context.getResources().getDrawable(R.drawable.media_item_placeholder);
+ }
+
+ return null;
+ }
+
+ private void addWordPressImagesFromCursor(Cursor cursor) {
+ if (cursor.moveToFirst()) {
+ do {
+ int attachmentIdColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID);
+ int fileUrlColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL);
+ int filePathColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH);
+ int thumbnailColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL);
+
+ String id = "";
+ if (attachmentIdColumnIndex != -1) {
+ id = String.valueOf(cursor.getInt(attachmentIdColumnIndex));
+ }
+ MediaItem newContent = new MediaItem();
+ newContent.setTag(id);
+ newContent.setTitle("");
+
+ if (fileUrlColumnIndex != -1) {
+ String fileUrl = cursor.getString(fileUrlColumnIndex);
+
+ if (fileUrl != null) {
+ newContent.setSource(Uri.parse(fileUrl));
+ newContent.setPreviewSource(Uri.parse(fileUrl));
+ } else if (filePathColumnIndex != -1) {
+ String filePath = cursor.getString(filePathColumnIndex);
+
+ if (filePath != null) {
+ newContent.setSource(Uri.parse(filePath));
+ newContent.setPreviewSource(Uri.parse(filePath));
+ }
+ }
+ }
+
+ if (thumbnailColumnIndex != -1) {
+ String preview = cursor.getString(thumbnailColumnIndex);
+
+ if (preview != null) {
+ newContent.setPreviewSource(Uri.parse(preview));
+ }
+ }
+
+ mMediaItems.add(newContent);
+ } while (cursor.moveToNext());
+
+ removeDeletedEntries();
+ } else if (mListener != null) {
+ mListener.onMediaLoaded(true);
+ }
+ }
+
+ private void removeDeletedEntries() {
+ final List<MediaItem> existingItems = new ArrayList<>(mMediaItems);
+ final List<MediaItem> failedItems = new ArrayList<>();
+
+ for (final MediaItem mediaItem : existingItems) {
+ LimitedBackgroundOperation<MediaItem, Void, MediaItem> backgroundCheck =
+ new LimitedBackgroundOperation<MediaItem, Void, MediaItem>() {
+ private int responseCode;
+
+ @Override
+ protected MediaItem performBackgroundOperation(MediaItem[] params) {
+ MediaItem mediaItem = params[0];
+ try {
+ URL mediaUrl = new URL(mediaItem.getSource().toString());
+ HttpURLConnection connection = (HttpURLConnection) mediaUrl.openConnection();
+ connection.setRequestMethod("GET");
+ connection.connect();
+ responseCode = connection.getResponseCode();
+ } catch (IOException ioException) {
+ Log.e("", "Error reading from " + mediaItem.getSource() + "\nexception:" + ioException);
+
+ return null;
+ }
+
+ return mediaItem;
+ }
+
+ @Override
+ public void performPostExecute(MediaItem result) {
+ if (mListener != null && result != null) {
+ if (responseCode == 200) {
+ mVerifiedItems.add(result);
+ List<MediaItem> resultList = new ArrayList<>();
+ resultList.add(result);
+
+ // Only signal newly loaded data every 3 images
+ if ((existingItems.size() - mVerifiedItems.size()) % 3 == 0) {
+ mListener.onMediaAdded(MediaSourceWPImages.this, resultList);
+ }
+ } else {
+ failedItems.add(result);
+ }
+
+ // Notify of all media loaded if all have been processed
+ if ((failedItems.size() + mVerifiedItems.size()) == existingItems.size()) {
+ mListener.onMediaLoaded(true);
+ }
+ }
+ }
+
+ @Override
+ public void startExecution(Object params) {
+ if (!(params instanceof MediaItem)) {
+ throw new IllegalArgumentException("Params must be of type MediaItem");
+ }
+ executeOnExecutor(THREAD_POOL_EXECUTOR, (MediaItem) params);
+ }
+ };
+ backgroundCheck.executeWithLimit(mediaItem);
+ }
+ }
+
+ /**
+ * {@link android.os.Parcelable} interface
+ */
+
+ public static final Creator<MediaSourceWPImages> CREATOR =
+ new Creator<MediaSourceWPImages>() {
+ public MediaSourceWPImages createFromParcel(Parcel in) {
+ return new MediaSourceWPImages();
+ }
+
+ public MediaSourceWPImages[] newArray(int size) {
+ return new MediaSourceWPImages[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java
new file mode 100644
index 000000000..d87dad566
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java
@@ -0,0 +1,209 @@
+package org.wordpress.android.ui.media;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.mediapicker.MediaItem;
+import org.wordpress.mediapicker.source.MediaSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaSourceWPVideos implements MediaSource {
+ private static final String VIDEO_PRESS_HOST = "https://videos.files.wordpress.com/";
+ private static final String VIDEO_PRESS_THUMBNAIL_APPEND = "_hd.thumbnail.jpg";
+
+ private OnMediaChange mListener;
+ private List<MediaItem> mMediaItems = new ArrayList<>();
+
+ public MediaSourceWPVideos() {
+ }
+
+ @Override
+ public void gather(Context context) {
+ Blog blog = WordPress.getCurrentBlog();
+
+ if (blog != null) {
+ Cursor videoCursor = WordPressMediaUtils.getWordPressMediaVideos(String.valueOf(blog.getLocalTableBlogId()));
+
+ if (videoCursor != null) {
+ addWordPressVideosFromCursor(videoCursor);
+ videoCursor.close();
+ } else if (mListener != null){
+ mListener.onMediaLoaded(false);
+ }
+ } else if (mListener != null){
+ mListener.onMediaLoaded(false);
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ mMediaItems.clear();
+ }
+
+ @Override
+ public void setListener(OnMediaChange listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public int getCount() {
+ return mMediaItems.size();
+ }
+
+ @Override
+ public MediaItem getMedia(int position) {
+ return mMediaItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) {
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.media_item_wp_video, parent, false);
+ }
+
+ if (convertView != null) {
+ MediaItem mediaItem = mMediaItems.get(position);
+ Uri imageSource = mediaItem.getPreviewSource();
+ ImageView imageView = (ImageView) convertView.findViewById(R.id.wp_video_view_background);
+ if (imageView != null) {
+ if (imageSource != null) {
+ Bitmap imageBitmap = null;
+ if (cache != null) {
+ imageBitmap = cache.getBitmap(imageSource.toString());
+ }
+
+ imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ if (imageBitmap == null) {
+ imageView.setImageDrawable(placeholderDrawable(convertView.getContext()));
+ WordPressMediaUtils.BackgroundDownloadWebImage bgDownload =
+ new WordPressMediaUtils.BackgroundDownloadWebImage(imageView);
+ imageView.setTag(bgDownload);
+ bgDownload.execute(mediaItem.getPreviewSource());
+ } else {
+ org.wordpress.mediapicker.MediaUtils.fadeInImage(imageView, imageBitmap);
+ }
+ } else {
+ imageView.setTag(null);
+ imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ imageView.setImageResource(R.drawable.video_thumbnail);
+ }
+ }
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) {
+ return !selected;
+ }
+
+ private Drawable placeholderDrawable(Context context) {
+ if (context != null && context.getResources() != null) {
+ return context.getResources().getDrawable(R.drawable.media_item_placeholder);
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper method; removes unnecessary characters from videoPressShortcode cursor value
+ *
+ * @param cursorEntry
+ * the cursor value for the videoPressShortcode key
+ * @return
+ * the VideoPress code
+ */
+ private String extractVideoPressCode(String cursorEntry) {
+ cursorEntry = cursorEntry.replace("[wpvideo ", "");
+ cursorEntry = cursorEntry.substring(0, cursorEntry.length() - 1);
+
+ return cursorEntry;
+ }
+
+ private void addWordPressVideosFromCursor(Cursor cursor) {
+ if (cursor.moveToFirst()) {
+ do {
+ int attachmentIdColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID);
+ int fileUrlColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL);
+ int fileNameColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME);
+ int videoPressColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_VIDEO_PRESS_SHORTCODE);
+
+ String id = "";
+ if (attachmentIdColumnIndex != -1) {
+ id = String.valueOf(cursor.getInt(attachmentIdColumnIndex));
+ }
+ MediaItem newContent = new MediaItem();
+ newContent.setTag(id);
+ newContent.setTitle("");
+
+ if (fileUrlColumnIndex != -1) {
+ String fileUrl = cursor.getString(fileUrlColumnIndex);
+
+ if (fileUrl != null && MediaUtils.isVideo(fileUrl)) {
+ newContent.setSource(Uri.parse(fileUrl));
+ } else {
+ continue;
+ }
+ }
+
+ if (videoPressColumnIndex != -1 && fileNameColumnIndex != -1) {
+ String videoPressCode = cursor.getString(videoPressColumnIndex);
+ String fileName = cursor.getString(fileNameColumnIndex);
+
+ if (videoPressCode != null && !videoPressCode.isEmpty() && fileName != null && !fileName.isEmpty()) {
+ videoPressCode = extractVideoPressCode(videoPressCode);
+ newContent.setPreviewSource(VIDEO_PRESS_HOST + videoPressCode + "/" + fileName + VIDEO_PRESS_THUMBNAIL_APPEND);
+ }
+ }
+
+ mMediaItems.add(newContent);
+ } while (cursor.moveToNext());
+ }
+
+ if (mListener != null) {
+ mListener.onMediaLoaded(true);
+ }
+ }
+
+ /**
+ * {@link android.os.Parcelable} interface
+ */
+
+ public static final Creator<MediaSourceWPVideos> CREATOR =
+ new Creator<MediaSourceWPVideos>() {
+ public MediaSourceWPVideos createFromParcel(Parcel in) {
+ return new MediaSourceWPVideos();
+ }
+
+ public MediaSourceWPVideos[] newArray(int size) {
+ return new MediaSourceWPVideos[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java
new file mode 100644
index 000000000..16c2c5ccc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java
@@ -0,0 +1,379 @@
+package org.wordpress.android.ui.media;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.support.v4.content.FileProvider;
+import android.widget.ImageView;
+
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DeviceUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.helpers.Version;
+import org.wordpress.passcodelock.AppLockManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+import static org.wordpress.mediapicker.MediaUtils.fadeInImage;
+
+public class WordPressMediaUtils {
+ public interface LaunchCameraCallback {
+ void onMediaCapturePathReady(String mediaCapturePath);
+ }
+
+ private static void showSDCardRequiredDialog(Context context) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
+ dialogBuilder.setTitle(context.getResources().getText(R.string.sdcard_title));
+ dialogBuilder.setMessage(context.getResources().getText(R.string.sdcard_message));
+ dialogBuilder.setPositiveButton(context.getString(R.string.ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.dismiss();
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+
+ public static void launchVideoLibrary(Activity activity) {
+ AppLockManager.getInstance().setExtendedTimeout();
+ activity.startActivityForResult(prepareVideoLibraryIntent(activity),
+ RequestCodes.VIDEO_LIBRARY);
+ }
+
+ public static void launchVideoLibrary(Fragment fragment) {
+ if (!fragment.isAdded()) {
+ return;
+ }
+ AppLockManager.getInstance().setExtendedTimeout();
+ fragment.startActivityForResult(prepareVideoLibraryIntent(fragment.getActivity()),
+ RequestCodes.VIDEO_LIBRARY);
+ }
+
+
+ public static Intent prepareVideoLibraryIntent(Context context) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("video/*");
+ return Intent.createChooser(intent, context.getString(R.string.pick_video));
+ }
+
+ public static void launchVideoCamera(Activity activity) {
+ AppLockManager.getInstance().setExtendedTimeout();
+ activity.startActivityForResult(prepareVideoCameraIntent(), RequestCodes.TAKE_VIDEO);
+ }
+
+ public static void launchVideoCamera(Fragment fragment) {
+ if (!fragment.isAdded()) {
+ return;
+ }
+ AppLockManager.getInstance().setExtendedTimeout();
+ fragment.startActivityForResult(prepareVideoCameraIntent(), RequestCodes.TAKE_VIDEO);
+ }
+
+ private static Intent prepareVideoCameraIntent() {
+ return new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+ }
+
+ public static void launchPictureLibrary(Activity activity) {
+ AppLockManager.getInstance().setExtendedTimeout();
+ activity.startActivityForResult(preparePictureLibraryIntent(activity.getString(R.string.pick_photo)),
+ RequestCodes.PICTURE_LIBRARY);
+ }
+
+ public static void launchPictureLibrary(Fragment fragment) {
+ if (!fragment.isAdded()) {
+ return;
+ }
+ AppLockManager.getInstance().setExtendedTimeout();
+ fragment.startActivityForResult(preparePictureLibraryIntent(fragment.getActivity()
+ .getString(R.string.pick_photo)), RequestCodes.PICTURE_LIBRARY);
+ }
+
+ private static Intent preparePictureLibraryIntent(String title) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ return Intent.createChooser(intent, title);
+ }
+
+ private static Intent prepareGalleryIntent(String title) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/*");
+ return Intent.createChooser(intent, title);
+ }
+
+ public static void launchCamera(Activity activity, String applicationId, LaunchCameraCallback callback) {
+ Intent intent = preparelaunchCamera(activity, applicationId, callback);
+ if (intent != null) {
+ AppLockManager.getInstance().setExtendedTimeout();
+ activity.startActivityForResult(intent, RequestCodes.TAKE_PHOTO);
+ }
+ }
+
+ public static void launchCamera(Fragment fragment, String applicationId, LaunchCameraCallback callback) {
+ if (!fragment.isAdded()) {
+ return;
+ }
+ Intent intent = preparelaunchCamera(fragment.getActivity(), applicationId, callback);
+ if (intent != null) {
+ AppLockManager.getInstance().setExtendedTimeout();
+ fragment.startActivityForResult(intent, RequestCodes.TAKE_PHOTO);
+ }
+ }
+
+ private static Intent preparelaunchCamera(Context context, String applicationId, LaunchCameraCallback callback) {
+ String state = android.os.Environment.getExternalStorageState();
+ if (!state.equals(android.os.Environment.MEDIA_MOUNTED)) {
+ showSDCardRequiredDialog(context);
+ return null;
+ } else {
+ return getLaunchCameraIntent(context, applicationId, callback);
+ }
+ }
+
+ private static Intent getLaunchCameraIntent(Context context, String applicationId, LaunchCameraCallback callback) {
+ File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
+
+ String mediaCapturePath = path + File.separator + "Camera" + File.separator + "wp-" + System
+ .currentTimeMillis() + ".jpg";
+ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(context,
+ applicationId + ".provider", new File(mediaCapturePath)));
+
+ if (callback != null) {
+ callback.onMediaCapturePathReady(mediaCapturePath);
+ }
+
+ // make sure the directory we plan to store the recording in exists
+ File directory = new File(mediaCapturePath).getParentFile();
+ if (!directory.exists() && !directory.mkdirs()) {
+ try {
+ throw new IOException("Path to file could not be created.");
+ } catch (IOException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ return intent;
+ }
+
+ public static void launchPictureLibraryOrCapture(Fragment fragment, String applicationId, LaunchCameraCallback
+ callback) {
+ if (!fragment.isAdded()) {
+ return;
+ }
+ AppLockManager.getInstance().setExtendedTimeout();
+ fragment.startActivityForResult(makePickOrCaptureIntent(fragment.getActivity(), applicationId, callback),
+ RequestCodes.PICTURE_LIBRARY_OR_CAPTURE);
+ }
+
+ private static Intent makePickOrCaptureIntent(Context context, String applicationId, LaunchCameraCallback callback) {
+ Intent pickPhotoIntent = prepareGalleryIntent(context.getString(R.string.capture_or_pick_photo));
+
+ if (DeviceUtils.getInstance().hasCamera(context)) {
+ Intent cameraIntent = getLaunchCameraIntent(context, applicationId, callback);
+ pickPhotoIntent.putExtra(
+ Intent.EXTRA_INITIAL_INTENTS,
+ new Intent[]{ cameraIntent });
+ }
+
+ return pickPhotoIntent;
+ }
+
+ public static int getPlaceholder(String url) {
+ if (MediaUtils.isValidImage(url)) {
+ return R.drawable.media_image_placeholder;
+ } else if (MediaUtils.isDocument(url)) {
+ return R.drawable.media_document;
+ } else if (MediaUtils.isPowerpoint(url)) {
+ return R.drawable.media_powerpoint;
+ } else if (MediaUtils.isSpreadsheet(url)) {
+ return R.drawable.media_spreadsheet;
+ } else if (MediaUtils.isVideo(url)) {
+ return org.wordpress.android.editor.R.drawable.media_movieclip;
+ } else if (MediaUtils.isAudio(url)) {
+ return R.drawable.media_audio;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * This is a workaround for WP3.4.2 that deletes the media from the server when editing media properties
+ * within the app. See: https://github.com/wordpress-mobile/WordPress-Android/issues/204
+ */
+ public static boolean isWordPressVersionWithMediaEditingCapabilities() {
+ if (WordPress.currentBlog == null) {
+ return false;
+ }
+
+ if (WordPress.currentBlog.getWpVersion() == null) {
+ return true;
+ }
+
+ if (WordPress.currentBlog.isDotcomFlag()) {
+ return true;
+ }
+
+ Version minVersion;
+ Version currentVersion;
+ try {
+ minVersion = new Version("3.5.2");
+ currentVersion = new Version(WordPress.currentBlog.getWpVersion());
+
+ if (currentVersion.compareTo(minVersion) == -1) {
+ return false;
+ }
+ } catch (IllegalArgumentException e) {
+ AppLog.e(T.POSTS, e);
+ }
+
+ return true;
+ }
+
+ public static boolean canDeleteMedia(String blogId, String mediaID) {
+ Cursor cursor = WordPress.wpDB.getMediaFile(blogId, mediaID);
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return false;
+ }
+ String state = cursor.getString(cursor.getColumnIndex("uploadState"));
+ cursor.close();
+ return state == null || !state.equals("uploading");
+ }
+
+ public static class BackgroundDownloadWebImage extends AsyncTask<Uri, String, Bitmap> {
+ WeakReference<ImageView> mReference;
+
+ public BackgroundDownloadWebImage(ImageView resultStore) {
+ mReference = new WeakReference<>(resultStore);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Uri... params) {
+ try {
+ String uri = params[0].toString();
+ Bitmap bitmap = WordPress.getBitmapCache().getBitmap(uri);
+
+ if (bitmap == null) {
+ URL url = new URL(uri);
+ bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
+ WordPress.getBitmapCache().put(uri, bitmap);
+ }
+
+ return bitmap;
+ }
+ catch(IOException notFoundException) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ ImageView imageView = mReference.get();
+
+ if (imageView != null) {
+ if (imageView.getTag() == this) {
+ imageView.setImageBitmap(result);
+ fadeInImage(imageView, result);
+ }
+ }
+ }
+ }
+
+ public static Cursor getWordPressMediaImages(String blogId) {
+ return WordPress.wpDB.getMediaImagesForBlog(blogId);
+ }
+
+ public static Cursor getWordPressMediaVideos(String blogId) {
+ return WordPress.wpDB.getMediaFilesForBlog(blogId);
+ }
+
+ /**
+ * Given a media file cursor, returns the thumbnail network URL. Will use photon if available, using the specified
+ * width.
+ * @param cursor the media file cursor
+ * @param width width to use for photon request (if applicable)
+ */
+ public static String getNetworkThumbnailUrl(Cursor cursor, int width) {
+ String thumbnailURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL));
+
+ // Allow non-private wp.com and Jetpack blogs to use photon to get a higher res thumbnail
+ if ((WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isPhotonCapable())) {
+ String imageURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL));
+ if (imageURL != null) {
+ thumbnailURL = PhotonUtils.getPhotonImageUrl(imageURL, width, 0);
+ }
+ }
+
+ return thumbnailURL;
+ }
+
+ /**
+ * Loads the given network image URL into the {@link NetworkImageView}, using the default {@link ImageLoader}.
+ */
+ public static void loadNetworkImage(String imageUrl, NetworkImageView imageView) {
+ loadNetworkImage(imageUrl, imageView, WordPress.imageLoader);
+ }
+
+ /**
+ * Loads the given network image URL into the {@link NetworkImageView}.
+ */
+ public static void loadNetworkImage(String imageUrl, NetworkImageView imageView, ImageLoader imageLoader) {
+ if (imageUrl != null) {
+ Uri uri = Uri.parse(imageUrl);
+ String filepath = uri.getLastPathSegment();
+
+ int placeholderResId = WordPressMediaUtils.getPlaceholder(filepath);
+ imageView.setErrorImageResId(placeholderResId);
+
+ // no default image while downloading
+ imageView.setDefaultImageResId(0);
+
+ if (MediaUtils.isValidImage(filepath)) {
+ imageView.setTag(imageUrl);
+ imageView.setImageUrl(imageUrl, imageLoader);
+ } else {
+ imageView.setImageResource(placeholderResId);
+ }
+ } else {
+ imageView.setImageResource(0);
+ }
+ }
+
+ /**
+ * Returns a poster (thumbnail) URL given a VideoPress video URL
+ * @param videoUrl the remote URL to the VideoPress video
+ */
+ public static String getVideoPressVideoPosterFromURL(String videoUrl) {
+ String posterUrl = "";
+
+ if (videoUrl != null) {
+ int filetypeLocation = videoUrl.lastIndexOf(".");
+ if (filetypeLocation > 0) {
+ posterUrl = videoUrl.substring(0, filetypeLocation) + "_std.original.jpg";
+ }
+ }
+
+ return posterUrl;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java
new file mode 100644
index 000000000..af0497946
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java
@@ -0,0 +1,121 @@
+package org.wordpress.android.ui.media.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.IBinder;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.MediaUploadState;
+import org.xmlrpc.android.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A service for deleting media files from the media browser.
+ * Only one file is deleted at a time.
+ */
+public class MediaDeleteService extends Service {
+ // time to wait before trying to delete the next file
+ private static final int DELETE_WAIT_TIME = 1000;
+
+ private Context mContext;
+ private Handler mHandler = new Handler();
+ private boolean mDeleteInProgress;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mContext = this.getApplicationContext();
+ mDeleteInProgress = false;
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ mHandler.post(mFetchQueueTask);
+ }
+
+ private Runnable mFetchQueueTask = new Runnable() {
+ @Override
+ public void run() {
+ Cursor cursor = getQueueItem();
+ try {
+ if ((cursor == null || cursor.getCount() == 0 || mContext == null) && !mDeleteInProgress) {
+ MediaDeleteService.this.stopSelf();
+ return;
+ } else {
+ if (mDeleteInProgress) {
+ mHandler.postDelayed(this, DELETE_WAIT_TIME);
+ } else {
+ deleteMediaFile(cursor);
+ }
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ }
+ };
+
+ private Cursor getQueueItem() {
+ if (WordPress.getCurrentBlog() == null)
+ return null;
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ return WordPress.wpDB.getMediaDeleteQueueItem(blogId);
+ }
+
+ private void deleteMediaFile(Cursor cursor) {
+ if (!cursor.moveToFirst())
+ return;
+
+ mDeleteInProgress = true;
+
+ final String blogId = cursor.getString((cursor.getColumnIndex("blogId")));
+ final String mediaId = cursor.getString(cursor.getColumnIndex("mediaId"));
+
+ ApiHelper.DeleteMediaTask task = new ApiHelper.DeleteMediaTask(mediaId,
+ new ApiHelper.GenericCallback() {
+ @Override
+ public void onSuccess() {
+ // only delete them once we get an ok from the server
+ if (WordPress.getCurrentBlog() != null && mediaId != null) {
+ WordPress.wpDB.deleteMediaFile(blogId, mediaId);
+ }
+
+ mDeleteInProgress = false;
+ mHandler.post(mFetchQueueTask);
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ // Ideally we would do handle the 401 (unauthorized) and 404 (not found) errors,
+ // but the XMLRPCExceptions don't seem to give messages when they are thrown.
+
+ // Instead we'll just set them as "deleted" so they don't show up in the delete queue.
+ // Otherwise the service will continuously try to delete an item they can't delete.
+
+ WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.DELETED);
+
+ mDeleteInProgress = false;
+ mHandler.post(mFetchQueueTask);
+ }
+ });
+
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(WordPress.getCurrentBlog());
+ task.execute(apiArgs) ;
+
+ mHandler.post(mFetchQueueTask);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java
new file mode 100644
index 000000000..e9ae8d9a7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java
@@ -0,0 +1,51 @@
+package org.wordpress.android.ui.media.services;
+
+public class MediaEvents {
+ public static class MediaUploadSucceeded {
+ public final String mLocalBlogId;
+ public final String mLocalMediaId;
+ public final String mRemoteMediaId;
+ public final String mRemoteMediaUrl;
+ public final String mSecondaryRemoteMediaId;
+ MediaUploadSucceeded(String localBlogId, String localMediaId, String remoteMediaId, String remoteMediaUrl,
+ String secondaryRemoteMediaId) {
+ mLocalBlogId = localBlogId;
+ mLocalMediaId = localMediaId;
+ mRemoteMediaId = remoteMediaId;
+ mRemoteMediaUrl = remoteMediaUrl;
+ mSecondaryRemoteMediaId = secondaryRemoteMediaId;
+ }
+ }
+
+ public static class MediaUploadFailed {
+ public final String mLocalMediaId;
+ public final String mErrorMessage;
+ public final boolean mIsGenericMessage;
+ MediaUploadFailed(String localMediaId, String errorMessage, boolean isGenericMessage) {
+ mLocalMediaId = localMediaId;
+ mErrorMessage = errorMessage;
+ mIsGenericMessage = isGenericMessage;
+ }
+ MediaUploadFailed(String localMediaId, String errorMessage) {
+ this(localMediaId, errorMessage, false);
+ }
+ }
+
+ public static class MediaUploadProgress {
+ public final String mLocalMediaId;
+ public final float mProgress;
+ MediaUploadProgress(String localMediaId, float progress) {
+ mLocalMediaId = localMediaId;
+ mProgress = progress;
+ }
+ }
+
+ public static class MediaChanged {
+ public final String mLocalBlogId;
+ public final String mMediaId;
+ public MediaChanged(String localBlogId, String mediaId) {
+ mLocalBlogId = localBlogId;
+ mMediaId = mediaId;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java
new file mode 100644
index 000000000..11d0ccae5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java
@@ -0,0 +1,247 @@
+package org.wordpress.android.ui.media.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.IBinder;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.models.MediaUploadState;
+import org.wordpress.android.ui.media.services.MediaEvents.MediaChanged;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.CrashlyticsUtils;
+import org.wordpress.android.util.CrashlyticsUtils.ExceptionType;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.ErrorType;
+import org.xmlrpc.android.ApiHelper.GetMediaItemTask;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * A service for uploading media files from the media browser.
+ * Only one file is uploaded at a time.
+ */
+public class MediaUploadService extends Service {
+ // time to wait before trying to upload the next file
+ private static final int UPLOAD_WAIT_TIME = 1000;
+
+ private static MediaUploadService mInstance;
+
+ private Context mContext;
+ private Handler mHandler = new Handler();
+
+ private boolean mUploadInProgress;
+ private ApiHelper.UploadMediaTask mCurrentUploadMediaTask;
+ private String mCurrentUploadMediaId;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mInstance = this;
+
+ mContext = this.getApplicationContext();
+ mUploadInProgress = false;
+
+ cancelOldUploads();
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ mHandler.post(mFetchQueueTask);
+ }
+
+ public static MediaUploadService getInstance() {
+ return mInstance;
+ }
+
+ public void processQueue() {
+ mHandler.post(mFetchQueueTask);
+ }
+
+ /**
+ * Returns whether the service has any media uploads in progress or queued.
+ */
+ public boolean hasUploads() {
+ if (mUploadInProgress) {
+ return true;
+ } else {
+ Cursor queueCursor = getQueue();
+ return (queueCursor == null || queueCursor.getCount() > 0);
+ }
+ }
+
+ /**
+ * Cancel the upload with the given id, whether it's currently uploading or queued.
+ * @param mediaId the id of the media item
+ * @param delete whether to delete the item from the queue or mark it as failed so it can be retried later
+ */
+ public void cancelUpload(String mediaId, boolean delete) {
+ if (mediaId.equals(mCurrentUploadMediaId)) {
+ // The media item is currently uploading - abort the upload process
+ mCurrentUploadMediaTask.cancel(true);
+ mUploadInProgress = false;
+ } else {
+ // Remove the media item from the upload queue
+ if (WordPress.getCurrentBlog() != null) {
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ if (delete) {
+ WordPress.wpDB.deleteMediaFile(blogId, mediaId);
+ } else {
+ WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.FAILED);
+ }
+ }
+ }
+ }
+
+ private Runnable mFetchQueueTask = new Runnable() {
+ @Override
+ public void run() {
+ Cursor cursor = getQueue();
+ try {
+ if ((cursor == null || cursor.getCount() == 0 || mContext == null) && !mUploadInProgress) {
+ MediaUploadService.this.stopSelf();
+ return;
+ } else {
+ if (mUploadInProgress) {
+ mHandler.postDelayed(this, UPLOAD_WAIT_TIME);
+ } else {
+ uploadMediaFile(cursor);
+ }
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ }
+ };
+
+ private void cancelOldUploads() {
+ // There should be no media files with an upload state of 'uploading' at the start of this service.
+ // Since we won't be able to receive notifications for these, set them to 'failed'.
+
+ if (WordPress.getCurrentBlog() != null) {
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ WordPress.wpDB.setMediaUploadingToFailed(blogId);
+ }
+ }
+
+ private Cursor getQueue() {
+ if (WordPress.getCurrentBlog() == null)
+ return null;
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ return WordPress.wpDB.getMediaUploadQueue(blogId);
+ }
+
+ private void uploadMediaFile(Cursor cursor) {
+ if (!cursor.moveToFirst())
+ return;
+
+ mUploadInProgress = true;
+
+ final String blogIdStr = cursor.getString((cursor.getColumnIndex(WordPressDB.COLUMN_NAME_BLOG_ID)));
+ final String mediaId = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID));
+ String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME));
+ String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH));
+ String mimeType = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MIME_TYPE));
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setBlogId(blogIdStr);
+ mediaFile.setFileName(fileName);
+ mediaFile.setFilePath(filePath);
+ mediaFile.setMimeType(mimeType);
+
+ mCurrentUploadMediaId = mediaId;
+
+ mCurrentUploadMediaTask = new ApiHelper.UploadMediaTask(mContext, mediaFile,
+ new ApiHelper.UploadMediaTask.Callback() {
+ @Override
+ public void onSuccess(String remoteId, String remoteUrl, String secondaryId) {
+ // once the file has been uploaded, update the local database entry (swap the id with the remote id)
+ // and download the new one
+ WordPress.wpDB.updateMediaLocalToRemoteId(blogIdStr, mediaId, remoteId);
+ EventBus.getDefault().post(new MediaEvents.MediaUploadSucceeded(blogIdStr, mediaId,
+ remoteId, remoteUrl, secondaryId));
+ fetchMediaFile(remoteId);
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ WordPress.wpDB.updateMediaUploadState(blogIdStr, mediaId, MediaUploadState.FAILED);
+ mUploadInProgress = false;
+ mCurrentUploadMediaId = "";
+
+ MediaEvents.MediaUploadFailed event;
+ if (errorMessage == null) {
+ event = new MediaEvents.MediaUploadFailed(mediaId, getString(R.string.upload_failed), true);
+ } else {
+ event = new MediaEvents.MediaUploadFailed(mediaId, errorMessage);
+ }
+
+ EventBus.getDefault().post(event);
+ mHandler.post(mFetchQueueTask);
+
+ // Only log the error if it's not caused by the network (internal inconsistency)
+ if (errorType != ErrorType.NETWORK_XMLRPC) {
+ CrashlyticsUtils.logException(throwable, ExceptionType.SPECIFIC, T.MEDIA, errorMessage);
+ }
+ }
+
+ @Override
+ public void onProgressUpdate(float progress) {
+ EventBus.getDefault().post(new MediaEvents.MediaUploadProgress(mediaId, progress));
+ }
+ });
+
+ WordPress.wpDB.updateMediaUploadState(blogIdStr, mediaId, MediaUploadState.UPLOADING);
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(WordPress.getCurrentBlog());
+ mCurrentUploadMediaTask.execute(apiArgs);
+ mHandler.post(mFetchQueueTask);
+ }
+
+ private void fetchMediaFile(final String id) {
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(WordPress.getCurrentBlog());
+ GetMediaItemTask task = new GetMediaItemTask(Integer.valueOf(id),
+ new ApiHelper.GetMediaItemTask.Callback() {
+ @Override
+ public void onSuccess(MediaFile mediaFile) {
+ String blogId = mediaFile.getBlogId();
+ String mediaId = mediaFile.getMediaId();
+ WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.UPLOADED);
+ mUploadInProgress = false;
+ mCurrentUploadMediaId = "";
+ mHandler.post(mFetchQueueTask);
+ EventBus.getDefault().post(new MediaChanged(blogId, mediaId));
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ mUploadInProgress = false;
+ mCurrentUploadMediaId = "";
+ mHandler.post(mFetchQueueTask);
+ // Only log the error if it's not caused by the network (internal inconsistency)
+ if (errorType != ErrorType.NETWORK_XMLRPC) {
+ CrashlyticsUtils.logException(throwable, ExceptionType.SPECIFIC, T.MEDIA, errorMessage);
+ }
+ }
+ });
+ task.execute(apiArgs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationDismissBroadcastReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationDismissBroadcastReceiver.java
new file mode 100644
index 000000000..a4b33b540
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationDismissBroadcastReceiver.java
@@ -0,0 +1,28 @@
+package org.wordpress.android.ui.notifications;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.app.NotificationManagerCompat;
+
+import org.wordpress.android.GCMMessageService;
+
+/*
+ * Clears the notification map when a user dismisses a notification
+ */
+public class NotificationDismissBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int notificationId = intent.getIntExtra("notificationId", 0);
+ if (notificationId == GCMMessageService.GROUP_NOTIFICATION_ID) {
+ GCMMessageService.clearNotifications();
+ } else {
+ GCMMessageService.removeNotification(notificationId);
+ // Dismiss the grouped notification if a user dismisses all notifications from a wear device
+ if (!GCMMessageService.hasNotifications()) {
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ notificationManager.cancel(GCMMessageService.GROUP_NOTIFICATION_ID);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationEvents.java
new file mode 100644
index 000000000..c5f4cfed0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationEvents.java
@@ -0,0 +1,33 @@
+package org.wordpress.android.ui.notifications;
+
+public class NotificationEvents {
+ public static class SimperiumNotAuthorized {}
+ public static class NotificationsChanged {}
+ public static class NoteModerationFailed {}
+ public static class NoteModerationStatusChanged {
+ boolean mIsModerating;
+ String mNoteId;
+ public NoteModerationStatusChanged(String noteId, boolean isModerating) {
+ mNoteId = noteId;
+ mIsModerating = isModerating;
+ }
+ }
+ public static class NoteVisibilityChanged {
+ boolean mIsHidden;
+ String mNoteId;
+ public NoteVisibilityChanged(String noteId, boolean isHidden) {
+ mNoteId = noteId;
+ mIsHidden = isHidden;
+ }
+ }
+ public static class NotificationsSettingsStatusChanged {
+ String mMessage;
+ public NotificationsSettingsStatusChanged(String message) {
+ mMessage = message;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationFragment.java
new file mode 100644
index 000000000..6690fd077
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationFragment.java
@@ -0,0 +1,19 @@
+/**
+ * Provides a list view and list adapter to display a note. It will have a header view to show
+ * the avatar and other details for the post.
+ *
+ * More specialized note adapters will need to be made to provide the correct views for the type
+ * of note/note template it has.
+ */
+package org.wordpress.android.ui.notifications;
+
+import org.wordpress.android.models.Note;
+
+public interface NotificationFragment {
+ public static interface OnPostClickListener {
+ public void onPostClicked(Note note, int remoteBlogId, int postId);
+ }
+
+ public Note getNote();
+ public void setNote(Note note);
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java
new file mode 100644
index 000000000..408194613
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java
@@ -0,0 +1,225 @@
+package org.wordpress.android.ui.notifications;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.WindowManager;
+
+import com.simperium.client.BucketObjectMissingException;
+
+import org.wordpress.android.GCMMessageService;
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.comments.CommentActions;
+import org.wordpress.android.ui.comments.CommentDetailFragment;
+import org.wordpress.android.ui.notifications.blocks.NoteBlockRangeType;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderPostDetailFragment;
+import org.wordpress.android.ui.stats.StatsAbstractFragment;
+import org.wordpress.android.ui.stats.StatsActivity;
+import org.wordpress.android.ui.stats.StatsTimeframe;
+import org.wordpress.android.ui.stats.StatsViewAllActivity;
+import org.wordpress.android.ui.stats.StatsViewType;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsDetailActivity extends AppCompatActivity implements
+ CommentActions.OnNoteCommentActionListener {
+ private static final String ARG_TITLE = "activityTitle";
+ private static final String DOMAIN_WPCOM = "wordpress.com";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AppLog.i(AppLog.T.NOTIFS, "Creating NotificationsDetailActivity");
+
+ setContentView(R.layout.notifications_detail_activity);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (savedInstanceState == null) {
+ String noteId = getIntent().getStringExtra(NotificationsListFragment.NOTE_ID_EXTRA);
+ if (noteId == null) {
+ showErrorToastAndFinish();
+ return;
+ }
+
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(noteId);
+
+ Map<String, String> properties = new HashMap<>();
+ properties.put("notification_type", note.getType());
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS, properties);
+
+ Fragment detailFragment = getDetailFragmentForNote(note);
+ getFragmentManager().beginTransaction()
+ .add(R.id.notifications_detail_container, detailFragment)
+ .commitAllowingStateLoss();
+
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setTitle(note.getTitle());
+ }
+
+ // mark the note as read if it's unread
+ if (note.isUnread()) {
+ // mark as read which syncs with simperium
+ note.markAsRead();
+ EventBus.getDefault().post(new NotificationEvents.NotificationsChanged());
+ }
+ } catch (BucketObjectMissingException e) {
+ showErrorToastAndFinish();
+ return;
+ }
+ }
+
+ GCMMessageService.removeNotificationWithNoteIdFromSystemBar(this, noteId);//clearNotifications();
+
+ } else if (savedInstanceState.containsKey(ARG_TITLE) && getSupportActionBar() != null) {
+ getSupportActionBar().setTitle(StringUtils.notNullStr(savedInstanceState.getString(ARG_TITLE)));
+ }
+
+ // Hide the keyboard, unless we arrived here from the 'Reply' action in a push notification
+ if (!getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA, false)) {
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ }
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ if (getSupportActionBar() != null && getSupportActionBar().getTitle() != null) {
+ outState.putString(ARG_TITLE, getSupportActionBar().getTitle().toString());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private void showErrorToastAndFinish() {
+ AppLog.e(AppLog.T.NOTIFS, "Note could not be found.");
+ ToastUtils.showToast(this, R.string.error_notification_open);
+ finish();
+ }
+
+ /**
+ * Tries to pick the correct fragment detail type for a given note
+ * Defaults to NotificationDetailListFragment
+ */
+ private Fragment getDetailFragmentForNote(Note note) {
+ if (note == null)
+ return null;
+
+ Fragment fragment;
+ if (note.isCommentType()) {
+ // show comment detail for comment notifications
+ boolean isInstantLike = getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_LIKE_EXTRA, false);
+ boolean isInstantApprove = getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_APPROVE_EXTRA, false);
+ fragment = isInstantLike ?
+ CommentDetailFragment.newInstanceForInstantLike(note.getId()) :
+ isInstantApprove ?
+ CommentDetailFragment.newInstanceForInstantApprove(note.getId()) :
+ CommentDetailFragment.newInstance(note.getId());
+ } else if (note.isAutomattcherType()) {
+ // show reader post detail for automattchers about posts - note that comment
+ // automattchers are handled by note.isCommentType() above
+ boolean isPost = (note.getSiteId() != 0 && note.getPostId() != 0 && note.getCommentId() == 0);
+ if (isPost) {
+ fragment = ReaderPostDetailFragment.newInstance(note.getSiteId(), note.getPostId());
+ } else {
+ fragment = NotificationsDetailListFragment.newInstance(note.getId());
+ }
+ } else {
+ fragment = NotificationsDetailListFragment.newInstance(note.getId());
+ }
+
+ return fragment;
+ }
+
+ public void showBlogPreviewActivity(long siteId) {
+ if (isFinishing()) return;
+
+ ReaderActivityLauncher.showReaderBlogPreview(this, siteId);
+ }
+
+ public void showPostActivity(long siteId, long postId) {
+ if (isFinishing()) return;
+
+ ReaderActivityLauncher.showReaderPostDetail(this, siteId, postId);
+ }
+
+ public void showStatsActivityForSite(int localTableSiteId, NoteBlockRangeType rangeType) {
+ if (isFinishing()) return;
+
+ if (rangeType == NoteBlockRangeType.FOLLOW) {
+ Intent intent = new Intent(this, StatsViewAllActivity.class);
+ intent.putExtra(StatsAbstractFragment.ARGS_VIEW_TYPE, StatsViewType.FOLLOWERS);
+ intent.putExtra(StatsAbstractFragment.ARGS_TIMEFRAME, StatsTimeframe.DAY);
+ intent.putExtra(StatsAbstractFragment.ARGS_SELECTED_DATE, "");
+ intent.putExtra(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, localTableSiteId);
+ intent.putExtra(StatsViewAllActivity.ARG_STATS_VIEW_ALL_TITLE, getString(R.string.stats_view_followers));
+ startActivity(intent);
+ } else {
+ ActivityLauncher.viewBlogStats(this, localTableSiteId);
+ }
+ }
+
+ public void showWebViewActivityForUrl(String url) {
+ if (isFinishing() || url == null) return;
+
+ if (url.contains(DOMAIN_WPCOM)) {
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(this, url, AccountHelper.getDefaultAccount().getUserName());
+ } else {
+ WPWebViewActivity.openURL(this, url);
+ }
+ }
+
+ public void showReaderPostLikeUsers(long blogId, long postId) {
+ if (isFinishing()) return;
+
+ ReaderActivityLauncher.showReaderLikingUsers(this, blogId, postId);
+ }
+
+ public void showReaderCommentsList(long siteId, long postId, long commentId) {
+ if (isFinishing()) return;
+
+ ReaderActivityLauncher.showReaderComments(this, siteId, postId, commentId);
+ }
+
+ @Override
+ public void onModerateCommentForNote(Note note, CommentStatus newStatus) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(NotificationsListFragment.NOTE_MODERATE_ID_EXTRA, note.getId());
+ resultIntent.putExtra(NotificationsListFragment.NOTE_MODERATE_STATUS_EXTRA, CommentStatus.toRESTString(newStatus));
+
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.java
new file mode 100644
index 000000000..638072914
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.java
@@ -0,0 +1,512 @@
+/**
+ * One fragment to rule them all (Notes, that is)
+ */
+package org.wordpress.android.ui.notifications;
+
+import android.app.ListFragment;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.simperium.client.Bucket;
+import com.simperium.client.BucketObjectMissingException;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.notifications.adapters.NoteBlockAdapter;
+import org.wordpress.android.ui.notifications.blocks.BlockType;
+import org.wordpress.android.ui.notifications.blocks.CommentUserNoteBlock;
+import org.wordpress.android.ui.notifications.blocks.FooterNoteBlock;
+import org.wordpress.android.ui.notifications.blocks.HeaderNoteBlock;
+import org.wordpress.android.ui.notifications.blocks.NoteBlock;
+import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan;
+import org.wordpress.android.ui.notifications.blocks.UserNoteBlock;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.reader.services.ReaderCommentService;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.widgets.WPNetworkImageView.ImageType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsDetailListFragment extends ListFragment implements NotificationFragment, Bucket.Listener<Note> {
+ private static final String KEY_NOTE_ID = "noteId";
+ private static final String KEY_LIST_POSITION = "listPosition";
+
+ private int mRestoredListPosition;
+
+ public interface OnNoteChangeListener {
+ void onNoteChanged(Note note);
+ }
+
+ private Note mNote;
+ private LinearLayout mRootLayout;
+ private ViewGroup mFooterView;
+
+ private String mRestoredNoteId;
+ private int mBackgroundColor;
+ private int mCommentListPosition = ListView.INVALID_POSITION;
+ private boolean mIsUnread;
+
+ private CommentUserNoteBlock.OnCommentStatusChangeListener mOnCommentStatusChangeListener;
+ private OnNoteChangeListener mOnNoteChangeListener;
+ private NoteBlockAdapter mNoteBlockAdapter;
+
+ public NotificationsDetailListFragment() {
+ }
+
+ public static NotificationsDetailListFragment newInstance(final String noteId) {
+ NotificationsDetailListFragment fragment = new NotificationsDetailListFragment();
+ fragment.setNoteWithNoteId(noteId);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NOTE_ID)) {
+ // The note will be set in onResume() because Simperium will be running there
+ // See WordPress.deferredInit()
+ mRestoredNoteId = savedInstanceState.getString(KEY_NOTE_ID);
+ mRestoredListPosition = savedInstanceState.getInt(KEY_LIST_POSITION, 0);
+ }
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.notifications_fragment_detail_list, container, false);
+ mRootLayout = (LinearLayout)view.findViewById(R.id.notifications_list_root);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle bundle) {
+ super.onActivityCreated(bundle);
+
+ mBackgroundColor = getResources().getColor(R.color.white);
+
+ ListView listView = getListView();
+ listView.setDivider(null);
+ listView.setDividerHeight(0);
+ listView.setHeaderDividersEnabled(false);
+
+ if (mFooterView != null) {
+ listView.addFooterView(mFooterView);
+ }
+
+ reloadNoteBlocks();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Start listening to bucket change events
+ if (SimperiumUtils.getNotesBucket() != null) {
+ SimperiumUtils.getNotesBucket().addListener(this);
+ }
+
+ // Set the note if we retrieved the noteId from savedInstanceState
+ if (!TextUtils.isEmpty(mRestoredNoteId)) {
+ setNoteWithNoteId(mRestoredNoteId);
+ reloadNoteBlocks();
+ mRestoredNoteId = null;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ // Remove the simperium bucket listener
+ if (SimperiumUtils.getNotesBucket() != null) {
+ SimperiumUtils.getNotesBucket().removeListener(this);
+ }
+
+ // Stop the reader comment service if it is running
+ ReaderCommentService.stopService(getActivity());
+
+ super.onPause();
+ }
+
+ @Override
+ public Note getNote() {
+ return mNote;
+ }
+
+ @Override
+ public void setNote(Note note) {
+ mNote = note;
+ }
+
+ private void setNoteWithNoteId(String noteId) {
+ if (noteId == null) return;
+
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(noteId);
+ mIsUnread = note.isUnread();
+ setNote(note);
+ } catch (BucketObjectMissingException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mNote != null) {
+ outState.putString(KEY_NOTE_ID, mNote.getId());
+ outState.putInt(KEY_LIST_POSITION, getListView().getFirstVisiblePosition());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ public void setOnNoteChangeListener(OnNoteChangeListener listener) {
+ mOnNoteChangeListener = listener;
+ }
+
+ private void reloadNoteBlocks() {
+ new LoadNoteBlocksTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public void setFooterView(ViewGroup footerView) {
+ mFooterView = footerView;
+ }
+
+ private final NoteBlock.OnNoteBlockTextClickListener mOnNoteBlockTextClickListener = new NoteBlock.OnNoteBlockTextClickListener() {
+ @Override
+ public void onNoteBlockTextClicked(NoteBlockClickableSpan clickedSpan) {
+ if (!isAdded() || !(getActivity() instanceof NotificationsDetailActivity)) return;
+
+ NotificationsUtils.handleNoteBlockSpanClick((NotificationsDetailActivity) getActivity(), clickedSpan);
+ }
+
+ @Override
+ public void showDetailForNoteIds() {
+ if (!isAdded() || mNote == null || !(getActivity() instanceof NotificationsDetailActivity)) {
+ return;
+ }
+
+ NotificationsDetailActivity detailActivity = (NotificationsDetailActivity)getActivity();
+ if (mNote.isCommentReplyType() || (!mNote.isCommentType() && mNote.getCommentId() > 0)) {
+ long commentId = mNote.isCommentReplyType() ? mNote.getParentCommentId() : mNote.getCommentId();
+
+ // show comments list if it exists in the reader
+ if (ReaderUtils.postAndCommentExists(mNote.getSiteId(), mNote.getPostId(), commentId)) {
+ detailActivity.showReaderCommentsList(mNote.getSiteId(), mNote.getPostId(), commentId);
+ } else {
+ detailActivity.showWebViewActivityForUrl(mNote.getUrl());
+ }
+ } else if (mNote.isFollowType()) {
+ detailActivity.showBlogPreviewActivity(mNote.getSiteId());
+ } else {
+ // otherwise, load the post in the Reader
+ detailActivity.showPostActivity(mNote.getSiteId(), mNote.getPostId());
+ }
+ }
+
+ @Override
+ public void showReaderPostComments() {
+ if (!isAdded() || mNote == null || mNote.getCommentId() == 0) return;
+
+ ReaderActivityLauncher.showReaderComments(getActivity(), mNote.getSiteId(), mNote.getPostId(), mNote.getCommentId());
+ }
+
+ @Override
+ public void showSitePreview(long siteId, String siteUrl) {
+ if (!isAdded() || mNote == null || !(getActivity() instanceof NotificationsDetailActivity)) {
+ return;
+ }
+
+ NotificationsDetailActivity detailActivity = (NotificationsDetailActivity)getActivity();
+ if (siteId != 0) {
+ detailActivity.showBlogPreviewActivity(siteId);
+ } else if (!TextUtils.isEmpty(siteUrl)) {
+ detailActivity.showWebViewActivityForUrl(siteUrl);
+ }
+ }
+ };
+
+ private final UserNoteBlock.OnGravatarClickedListener mOnGravatarClickedListener = new UserNoteBlock.OnGravatarClickedListener() {
+ @Override
+ public void onGravatarClicked(long siteId, long userId, String siteUrl) {
+ if (!isAdded() || !(getActivity() instanceof NotificationsDetailActivity)) return;
+
+ NotificationsDetailActivity detailActivity = (NotificationsDetailActivity)getActivity();
+ if (siteId == 0 && !TextUtils.isEmpty(siteUrl)) {
+ detailActivity.showWebViewActivityForUrl(siteUrl);
+ } else if (siteId != 0) {
+ detailActivity.showBlogPreviewActivity(siteId);
+ }
+ }
+ };
+
+ private boolean hasNoteBlockAdapter() {
+ return mNoteBlockAdapter != null;
+ }
+
+
+ // Loop through the 'body' items in this note, and create blocks for each.
+ private class LoadNoteBlocksTask extends AsyncTask<Void, Boolean, List<NoteBlock>> {
+
+ private boolean mIsBadgeView;
+
+ @Override
+ protected List<NoteBlock> doInBackground(Void... params) {
+ if (mNote == null) return null;
+
+ requestReaderContentForNote();
+
+ JSONArray bodyArray = mNote.getBody();
+ final List<NoteBlock> noteList = new ArrayList<>();
+
+ // Add the note header if one was provided
+ if (mNote.getHeader() != null) {
+ ImageType imageType = mNote.isFollowType() ? ImageType.BLAVATAR : ImageType.AVATAR;
+ HeaderNoteBlock headerNoteBlock = new HeaderNoteBlock(
+ getActivity(),
+ mNote.getHeader(),
+ imageType,
+ mOnNoteBlockTextClickListener,
+ mOnGravatarClickedListener
+ );
+
+ headerNoteBlock.setIsComment(mNote.isCommentType());
+ noteList.add(headerNoteBlock);
+ }
+
+ if (bodyArray != null && bodyArray.length() > 0) {
+ for (int i=0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject noteObject = bodyArray.getJSONObject(i);
+ // Determine NoteBlock type and add it to the array
+ NoteBlock noteBlock;
+ String noteBlockTypeString = JSONUtils.queryJSON(noteObject, "type", "");
+
+ if (BlockType.fromString(noteBlockTypeString) == BlockType.USER) {
+ if (mNote.isCommentType()) {
+ // Set comment position so we can target it later
+ // See refreshBlocksForCommentStatus()
+ mCommentListPosition = i + noteList.size();
+
+ // We'll snag the next body array item for comment user blocks
+ if (i + 1 < bodyArray.length()) {
+ JSONObject commentTextBlock = bodyArray.getJSONObject(i + 1);
+ noteObject.put("comment_text", commentTextBlock);
+ i++;
+ }
+
+ // Add timestamp to block for display
+ noteObject.put("timestamp", mNote.getTimestamp());
+
+ noteBlock = new CommentUserNoteBlock(
+ getActivity(),
+ noteObject,
+ mOnNoteBlockTextClickListener,
+ mOnGravatarClickedListener
+ );
+
+ // Set listener for comment status changes, so we can update bg and text colors
+ CommentUserNoteBlock commentUserNoteBlock = (CommentUserNoteBlock)noteBlock;
+ mOnCommentStatusChangeListener = commentUserNoteBlock.getOnCommentChangeListener();
+ commentUserNoteBlock.setCommentStatus(mNote.getCommentStatus());
+ commentUserNoteBlock.configureResources(getActivity());
+ } else {
+ noteBlock = new UserNoteBlock(
+ getActivity(),
+ noteObject,
+ mOnNoteBlockTextClickListener,
+ mOnGravatarClickedListener
+ );
+ }
+ } else if (isFooterBlock(noteObject)) {
+ noteBlock = new FooterNoteBlock(noteObject, mOnNoteBlockTextClickListener);
+ ((FooterNoteBlock)noteBlock).setClickableSpan(
+ JSONUtils.queryJSON(noteObject, "ranges[last]", new JSONObject()),
+ mNote.getType()
+ );
+ } else {
+ noteBlock = new NoteBlock(noteObject, mOnNoteBlockTextClickListener);
+ }
+
+ // Badge notifications apply different colors and formatting
+ if (isAdded() && noteBlock.containsBadgeMediaType()) {
+ mIsBadgeView = true;
+ mBackgroundColor = getActivity().getResources().getColor(R.color.transparent);
+ }
+
+ if (mIsBadgeView) {
+ noteBlock.setIsBadge();
+ }
+
+ noteList.add(noteBlock);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Invalid note data, could not parse.");
+ }
+ }
+ }
+
+ return noteList;
+ }
+
+ @Override
+ protected void onPostExecute(List<NoteBlock> noteList) {
+ if (!isAdded() || noteList == null) return;
+
+ if (mIsBadgeView) {
+ mRootLayout.setGravity(Gravity.CENTER_VERTICAL);
+ }
+
+ if (!hasNoteBlockAdapter()) {
+ mNoteBlockAdapter = new NoteBlockAdapter(getActivity(), noteList, mBackgroundColor);
+ setListAdapter(mNoteBlockAdapter);
+ } else {
+ mNoteBlockAdapter.setNoteList(noteList);
+ }
+
+ if (mRestoredListPosition > 0) {
+ getListView().setSelectionFromTop(mRestoredListPosition, 0);
+ mRestoredListPosition = 0;
+ }
+ }
+ }
+
+ private boolean isFooterBlock(JSONObject blockObject) {
+ if (mNote == null || blockObject == null) return false;
+
+ if (mNote.isCommentType()) {
+ // Check if this is a comment notification that has been replied to
+ // The block will not have a type, and its id will match the comment reply id in the Note.
+ return (JSONUtils.queryJSON(blockObject, "type", null) == null &&
+ mNote.getCommentReplyId() == JSONUtils.queryJSON(blockObject, "ranges[1].id", 0));
+ } else if (mNote.isFollowType() || mNote.isLikeType() ||
+ mNote.isCommentLikeType() || mNote.isReblogType()) {
+ // User list notifications have a footer if they have 10 or more users in the body
+ // The last block will not have a type, so we can use that to determine if it is the footer
+ return JSONUtils.queryJSON(blockObject, "type", null) == null;
+ }
+
+ return false;
+ }
+
+ public void refreshBlocksForCommentStatus(CommentStatus newStatus) {
+ if (mOnCommentStatusChangeListener != null) {
+ mOnCommentStatusChangeListener.onCommentStatusChanged(newStatus);
+ ListView listView = getListView();
+ if (listView == null || mCommentListPosition == ListView.INVALID_POSITION) {
+ return;
+ }
+
+ // Redraw the comment row if it is visible so that the background and text colors update
+ // See: http://stackoverflow.com/questions/4075975/redraw-a-single-row-in-a-listview/9987616#9987616
+ int firstPosition = listView.getFirstVisiblePosition();
+ int endPosition = listView.getLastVisiblePosition();
+ for (int i = firstPosition; i < endPosition; i++) {
+ if (mCommentListPosition == i) {
+ View view = listView.getChildAt(i - firstPosition);
+ listView.getAdapter().getView(i, view, listView);
+ break;
+ }
+ }
+ }
+ }
+
+ // Requests Reader content for certain notification types
+ private void requestReaderContentForNote() {
+ if (mNote == null || !isAdded()) return;
+
+ // Request the reader post so that loading reader activities will work.
+ if (mNote.isUserList() && !ReaderPostTable.postExists(mNote.getSiteId(), mNote.getPostId())) {
+ ReaderPostActions.requestPost(mNote.getSiteId(), mNote.getPostId(), null);
+ }
+
+ // Request reader comments until we retrieve the comment for this note
+ if ((mNote.isCommentLikeType() || mNote.isCommentReplyType() || mNote.isCommentWithUserReply()) &&
+ !ReaderCommentTable.commentExists(mNote.getSiteId(), mNote.getPostId(), mNote.getCommentId())) {
+ ReaderCommentService.startServiceForComment(getActivity(), mNote.getSiteId(), mNote.getPostId(), mNote.getCommentId());
+ }
+ }
+
+ // Simperium bucket listener
+ @Override
+ public void onBeforeUpdateObject(Bucket<Note> noteBucket, Note note) {
+ // noop
+ }
+
+ @Override
+ public void onDeleteObject(Bucket<Note> noteBucket, Note note) {
+ // noop
+ }
+
+ @Override
+ public void onNetworkChange(Bucket<Note> noteBucket, Bucket.ChangeType changeType, String noteId) {
+ // We're not interested in INDEX events here
+ if (changeType == Bucket.ChangeType.INDEX) return;
+
+ // Refresh content if we receive a change for the Note
+ if (mNote != null && mNote.getId().equals(noteId)) {
+ // If the note was removed, pop the back stack to return to the notes list
+ if (changeType == Bucket.ChangeType.REMOVE) {
+ getFragmentManager().popBackStack();
+ return;
+ }
+
+ try {
+ mNote = noteBucket.get(noteId);
+
+ // Don't refresh if the note was just marked as read
+ if (!mNote.isUnread() && mIsUnread) {
+ mIsUnread = false;
+ return;
+ }
+
+ // Mark note as read since we are looking at it already
+ if (mNote.isUnread()) {
+ mNote.markAsRead();
+ EventBus.getDefault().post(new NotificationEvents.NotificationsChanged());
+ }
+
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ reloadNoteBlocks();
+ if (mOnNoteChangeListener != null) {
+ mOnNoteChangeListener.onNoteChanged(mNote);
+ }
+ }
+ });
+ }
+ } catch (BucketObjectMissingException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Couldn't load note after receiving change.");
+ }
+ }
+ }
+
+ @Override
+ public void onSaveObject(Bucket<Note> noteBucket, Note note) {
+ // noop
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java
new file mode 100644
index 000000000..8ad48256c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java
@@ -0,0 +1,542 @@
+package org.wordpress.android.ui.notifications;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.design.widget.AppBarLayout;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import com.simperium.client.Bucket;
+import com.simperium.client.BucketObjectMissingException;
+
+import org.wordpress.android.GCMMessageService;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.notifications.adapters.NotesAdapter;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsListFragment extends Fragment
+ implements Bucket.Listener<Note>,
+ WPMainActivity.OnScrollToTopListener, RadioGroup.OnCheckedChangeListener {
+ public static final String NOTE_ID_EXTRA = "noteId";
+ public static final String NOTE_INSTANT_REPLY_EXTRA = "instantReply";
+ public static final String NOTE_INSTANT_LIKE_EXTRA = "instantLike";
+ public static final String NOTE_INSTANT_APPROVE_EXTRA = "instantApprove";
+ public static final String NOTE_MODERATE_ID_EXTRA = "moderateNoteId";
+ public static final String NOTE_MODERATE_STATUS_EXTRA = "moderateNoteStatus";
+
+ private static final String KEY_LIST_SCROLL_POSITION = "scrollPosition";
+
+ private NotesAdapter mNotesAdapter;
+ private LinearLayoutManager mLinearLayoutManager;
+ private RecyclerView mRecyclerView;
+ private ViewGroup mEmptyView;
+ private View mFilterView;
+ private RadioGroup mFilterRadioGroup;
+ private View mFilterDivider;
+
+ private int mRestoredScrollPosition;
+
+ private Bucket<Note> mBucket;
+
+ public static NotificationsListFragment newInstance() {
+ return new NotificationsListFragment();
+ }
+
+ /**
+ * For responding to tapping of notes
+ */
+ public interface OnNoteClickListener {
+ void onClickNote(String noteId);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.notifications_fragment_notes_list, container, false);
+
+ mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view_notes);
+
+ mFilterRadioGroup = (RadioGroup)view.findViewById(R.id.notifications_radio_group);
+ mFilterRadioGroup.setOnCheckedChangeListener(this);
+ mFilterDivider = view.findViewById(R.id.notifications_filter_divider);
+ mEmptyView = (ViewGroup) view.findViewById(R.id.empty_view);
+ mFilterView = view.findViewById(R.id.notifications_filter);
+
+ mLinearLayoutManager = new LinearLayoutManager(getActivity());
+ mRecyclerView.setLayoutManager(mLinearLayoutManager);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ setRestoredListPosition(savedInstanceState.getInt(KEY_LIST_SCROLL_POSITION, RecyclerView.NO_POSITION));
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ configureBucketAndAdapter();
+ refreshNotes();
+
+ // start listening to bucket change events
+ if (mBucket != null) {
+ mBucket.addListener(this);
+ }
+
+ // Removes app notifications from the system bar
+ new Thread(new Runnable() {
+ public void run() {
+ GCMMessageService.removeAllNotifications(getActivity());
+ }
+ }).start();
+
+ if (SimperiumUtils.isUserAuthorized()) {
+ SimperiumUtils.startBuckets();
+ AppLog.i(AppLog.T.NOTIFS, "Starting Simperium buckets");
+ }
+ }
+
+ @Override
+ public void onPause() {
+ // unregister the listener
+ if (mBucket != null) {
+ mBucket.removeListener(this);
+ }
+ super.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ // Close Simperium cursor
+ if (mNotesAdapter != null) {
+ mNotesAdapter.closeCursor();
+ }
+
+ super.onDestroy();
+ }
+
+ // Sets up the notes bucket and list adapter
+ private void configureBucketAndAdapter() {
+ mBucket = SimperiumUtils.getNotesBucket();
+ if (mBucket != null) {
+ if (mNotesAdapter == null) {
+ mNotesAdapter = new NotesAdapter(getActivity(), mBucket);
+ mNotesAdapter.setOnNoteClickListener(mOnNoteClickListener);
+ }
+
+ if (mRecyclerView.getAdapter() == null) {
+ mRecyclerView.setAdapter(mNotesAdapter);
+ }
+ } else {
+ if (!AccountHelper.isSignedInWordPressDotCom()) {
+ // let user know that notifications require a wp.com account and enable sign-in
+ showEmptyView(R.string.notifications_account_required, 0, R.string.sign_in);
+ mFilterRadioGroup.setVisibility(View.GONE);
+ } else {
+ // failed for some other reason
+ showEmptyView(R.string.error_refresh_notifications);
+ }
+ }
+ }
+
+ private final OnNoteClickListener mOnNoteClickListener = new OnNoteClickListener() {
+ @Override
+ public void onClickNote(String noteId) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (TextUtils.isEmpty(noteId)) return;
+
+ // open the latest version of this note just in case it has changed - this can
+ // happen if the note was tapped from the list fragment after it was updated
+ // by another fragment (such as NotificationCommentLikeFragment)
+ openNoteForReply(getActivity(), noteId, false);
+ }
+ };
+
+ private static Intent getOpenNoteIntent(Activity activity,
+ String noteId) {
+ Intent detailIntent = new Intent(activity, NotificationsDetailActivity.class);
+ detailIntent.putExtra(NOTE_ID_EXTRA, noteId);
+ return detailIntent;
+ }
+
+ /**
+ * Open a note fragment based on the type of note
+ */
+ public static void openNoteForReply(Activity activity,
+ String noteId,
+ boolean shouldShowKeyboard) {
+ if (noteId == null || activity == null) {
+ return;
+ }
+
+ if (activity.isFinishing()) {
+ return;
+ }
+
+ Intent detailIntent = getOpenNoteIntent(activity, noteId);
+ detailIntent.putExtra(NOTE_INSTANT_REPLY_EXTRA, shouldShowKeyboard);
+ activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL);
+ }
+
+ /**
+ * Open a note fragment based on the type of note, signaling to issue a like action immediately
+ */
+ public static void openNoteForLike(Activity activity,
+ String noteId) {
+ if (noteId == null || activity == null) {
+ return;
+ }
+
+ if (activity.isFinishing()) {
+ return;
+ }
+
+ Intent detailIntent = getOpenNoteIntent(activity, noteId);
+ detailIntent.putExtra(NOTE_INSTANT_LIKE_EXTRA, true);
+ activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL);
+ }
+
+ /**
+ * Open a note fragment based on the type of note, signaling to issue a moderate:approve action immediately
+ */
+ public static void openNoteForApprove(Activity activity,
+ String noteId) {
+ if (noteId == null || activity == null) {
+ return;
+ }
+
+ if (activity.isFinishing()) {
+ return;
+ }
+
+ Intent detailIntent = getOpenNoteIntent(activity, noteId);
+ detailIntent.putExtra(NOTE_INSTANT_APPROVE_EXTRA, true);
+ activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL);
+ }
+
+ private void setNoteIsHidden(String noteId, boolean isHidden) {
+ if (mNotesAdapter == null) return;
+
+ if (isHidden) {
+ mNotesAdapter.addHiddenNoteId(noteId);
+ } else {
+ // Scroll the row into view if it isn't visible so the animation can be seen
+ int notePosition = mNotesAdapter.getPositionForNote(noteId);
+ if (notePosition != RecyclerView.NO_POSITION &&
+ mLinearLayoutManager.findFirstCompletelyVisibleItemPosition() > notePosition) {
+ mLinearLayoutManager.scrollToPosition(notePosition);
+ }
+
+ mNotesAdapter.removeHiddenNoteId(noteId);
+ }
+ }
+
+ private void setNoteIsModerating(String noteId, boolean isModerating) {
+ if (mNotesAdapter == null) return;
+
+ if (isModerating) {
+ mNotesAdapter.addModeratingNoteId(noteId);
+ } else {
+ mNotesAdapter.removeModeratingNoteId(noteId);
+ }
+ }
+
+ private void showEmptyView(@StringRes int titleResId) {
+ showEmptyView(titleResId, 0, 0);
+ }
+
+ private void showEmptyView(@StringRes int titleResId, @StringRes int descriptionResId, @StringRes int buttonResId) {
+ if (isAdded() && mEmptyView != null) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ mFilterDivider.setVisibility(View.GONE);
+ setFilterViewScrollable(false);
+ ((TextView) mEmptyView.findViewById(R.id.text_empty)).setText(titleResId);
+
+ TextView descriptionTextView = (TextView) mEmptyView.findViewById(R.id.text_empty_description);
+ if (descriptionResId > 0) {
+ descriptionTextView.setText(descriptionResId);
+ } else {
+ descriptionTextView.setVisibility(View.GONE);
+ }
+
+ TextView btnAction = (TextView)mEmptyView.findViewById(R.id.button_empty_action);
+ if (buttonResId > 0) {
+ btnAction.setText(buttonResId);
+ btnAction.setVisibility(View.VISIBLE);
+ } else {
+ btnAction.setVisibility(View.GONE);
+ }
+
+ btnAction.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performActionForActiveFilter();
+ }
+ });
+ }
+ }
+
+ private void setFilterViewScrollable(boolean isScrollable) {
+ if (mFilterView != null && mFilterView.getLayoutParams() instanceof AppBarLayout.LayoutParams) {
+ AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) mFilterView.getLayoutParams();
+ if (isScrollable) {
+ params.setScrollFlags(
+ AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL|
+ AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
+ );
+ } else {
+ params.setScrollFlags(0);
+ }
+ }
+ }
+
+ private void hideEmptyView() {
+ if (isAdded() && mEmptyView != null) {
+ setFilterViewScrollable(true);
+ mEmptyView.setVisibility(View.GONE);
+ mFilterDivider.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void refreshNotes() {
+ if (!isAdded() || mNotesAdapter == null) {
+ return;
+ }
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Filter the list according to the RadioGroup selection
+ int checkedId = mFilterRadioGroup.getCheckedRadioButtonId();
+ if (checkedId == R.id.notifications_filter_all) {
+ mNotesAdapter.queryNotes();
+ } else if (checkedId == R.id.notifications_filter_unread) {
+ mNotesAdapter.queryNotes(Note.Schema.UNREAD_INDEX, 1);
+ } else if (checkedId == R.id.notifications_filter_comments) {
+ mNotesAdapter.queryNotes(Note.Schema.TYPE_INDEX, Note.NOTE_COMMENT_TYPE);
+ } else if (checkedId == R.id.notifications_filter_follows) {
+ mNotesAdapter.queryNotes(Note.Schema.TYPE_INDEX, Note.NOTE_FOLLOW_TYPE);
+ } else if (checkedId == R.id.notifications_filter_likes) {
+ mNotesAdapter.queryNotes(Note.Schema.TYPE_INDEX, Note.NOTE_LIKE_TYPE);
+ } else {
+ mNotesAdapter.queryNotes();
+ }
+
+ restoreListScrollPosition();
+ if (mNotesAdapter.getCount() > 0) {
+ hideEmptyView();
+ } else {
+ showEmptyViewForCurrentFilter();
+ }
+ }
+ });
+ }
+
+ // Show different empty list message and action button based on the active filter
+ private void showEmptyViewForCurrentFilter() {
+ if (!AccountHelper.isSignedInWordPressDotCom()) return;
+
+ int i = mFilterRadioGroup.getCheckedRadioButtonId();
+ if (i == R.id.notifications_filter_all) {
+ showEmptyView(
+ R.string.notifications_empty_all,
+ R.string.notifications_empty_action_all,
+ R.string.notifications_empty_view_reader
+ );
+ } else if (i == R.id.notifications_filter_unread) {// User might not have a blog, if so just show the title
+ if (WordPress.getCurrentBlog() == null) {
+ showEmptyView(R.string.notifications_empty_unread);
+ } else {
+ showEmptyView(
+ R.string.notifications_empty_unread,
+ R.string.notifications_empty_action_unread,
+ R.string.new_post
+ );
+ }
+ } else if (i == R.id.notifications_filter_comments) {
+ showEmptyView(
+ R.string.notifications_empty_comments,
+ R.string.notifications_empty_action_comments,
+ R.string.notifications_empty_view_reader
+ );
+ } else if (i == R.id.notifications_filter_follows) {
+ showEmptyView(
+ R.string.notifications_empty_followers,
+ R.string.notifications_empty_action_followers_likes,
+ R.string.notifications_empty_view_reader
+ );
+ } else if (i == R.id.notifications_filter_likes) {
+ showEmptyView(
+ R.string.notifications_empty_likes,
+ R.string.notifications_empty_action_followers_likes,
+ R.string.notifications_empty_view_reader
+ );
+ } else {
+ showEmptyView(R.string.notifications_empty_list);
+ }
+ }
+
+ private void performActionForActiveFilter() {
+ if (mFilterRadioGroup == null || !isAdded()) return;
+
+ if (!AccountHelper.isSignedInWordPressDotCom()) {
+ ActivityLauncher.showSignInForResult(getActivity());
+ return;
+ }
+
+ int i = mFilterRadioGroup.getCheckedRadioButtonId();
+ if (i == R.id.notifications_filter_unread) {// Create a new post
+ ActivityLauncher.addNewBlogPostOrPageForResult(getActivity(), WordPress.getCurrentBlog(), false);
+ } else {// Switch to Reader tab
+ if (getActivity() instanceof WPMainActivity) {
+ ((WPMainActivity) getActivity()).setReaderTabActive();
+ }
+ }
+ }
+
+ private void restoreListScrollPosition() {
+ if (isAdded() && mRecyclerView != null && mRestoredScrollPosition != RecyclerView.NO_POSITION
+ && mRestoredScrollPosition < mNotesAdapter.getCount()) {
+ // Restore scroll position in list
+ mLinearLayoutManager.scrollToPosition(mRestoredScrollPosition);
+ mRestoredScrollPosition = RecyclerView.NO_POSITION;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+
+ // Save list view scroll position
+ outState.putInt(KEY_LIST_SCROLL_POSITION, getScrollPosition());
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private int getScrollPosition() {
+ if (!isAdded() || mRecyclerView == null) {
+ return RecyclerView.NO_POSITION;
+ }
+
+ return mLinearLayoutManager.findFirstVisibleItemPosition();
+ }
+
+ private void setRestoredListPosition(int listPosition) {
+ mRestoredScrollPosition = listPosition;
+ }
+
+ // Notification filter methods
+ @Override
+ public void onCheckedChanged(RadioGroup radioGroup, int checkedId) {
+ refreshNotes();
+ }
+
+ /**
+ * Simperium bucket listener methods
+ */
+ @Override
+ public void onSaveObject(Bucket<Note> bucket, final Note object) {
+ refreshNotes();
+ }
+
+ @Override
+ public void onDeleteObject(Bucket<Note> bucket, final Note object) {
+ refreshNotes();
+ }
+
+ @Override
+ public void onNetworkChange(Bucket<Note> bucket, final Bucket.ChangeType type, final String key) {
+ // Reset the note's local status when a remote change is received
+ if (type == Bucket.ChangeType.MODIFY) {
+ try {
+ Note note = bucket.get(key);
+ if (note.isCommentType()) {
+ note.setLocalStatus(null);
+ note.save();
+ }
+ } catch (BucketObjectMissingException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not create note after receiving change.");
+ }
+ }
+
+ refreshNotes();
+ }
+
+ @Override
+ public void onBeforeUpdateObject(Bucket<Note> noteBucket, Note note) {
+ //noop
+ }
+
+ @Override
+ public void onScrollToTop() {
+ if (isAdded() && getScrollPosition() > 0) {
+ mLinearLayoutManager.smoothScrollToPosition(mRecyclerView, null, 0);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().registerSticky(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(NotificationEvents.NoteModerationStatusChanged event) {
+ setNoteIsModerating(event.mNoteId, event.mIsModerating);
+
+ EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteModerationStatusChanged.class);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(NotificationEvents.NoteVisibilityChanged event) {
+ setNoteIsHidden(event.mNoteId, event.mIsHidden);
+
+ EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteVisibilityChanged.class);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(NotificationEvents.NoteModerationFailed event) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment, Duration.LONG);
+ }
+
+ EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteModerationFailed.class);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/ShareAndDismissNotificationReceiver.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/ShareAndDismissNotificationReceiver.java
new file mode 100644
index 000000000..bbdad8b76
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/ShareAndDismissNotificationReceiver.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.ui.notifications;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.wordpress.android.util.SystemServiceFactory;
+
+public class ShareAndDismissNotificationReceiver extends BroadcastReceiver {
+ public static final String NOTIFICATION_ID_KEY = "NOTIFICATION_ID_KEY";
+
+ public void onReceive(Context context, Intent receivedIntent) {
+ // Cancel (dismiss) the notification
+ int notificationId = receivedIntent.getIntExtra(NOTIFICATION_ID_KEY, 0);
+ NotificationManager notificationManager = (NotificationManager) SystemServiceFactory.get(context,
+ Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(notificationId);
+
+ // Close system notification tray
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+ // Start the Share action
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, receivedIntent.getStringExtra(Intent.EXTRA_TEXT));
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, receivedIntent.getStringExtra(Intent.EXTRA_SUBJECT));
+ shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(shareIntent);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/CursorRecyclerViewAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/CursorRecyclerViewAdapter.java
new file mode 100644
index 000000000..82993869b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/CursorRecyclerViewAdapter.java
@@ -0,0 +1,114 @@
+package org.wordpress.android.ui.notifications.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.support.v7.widget.RecyclerView;
+
+// Credit: https://gist.github.com/skyfishjy/443b7448f59be978bc59
+
+public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
+ private Cursor mCursor;
+ private boolean mDataValid;
+ private int mRowIdColumn;
+ private DataSetObserver mDataSetObserver;
+
+ public CursorRecyclerViewAdapter(Context context, Cursor cursor) {
+ mCursor = cursor;
+ mDataValid = cursor != null;
+ mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
+ mDataSetObserver = new NotifyingDataSetObserver();
+ if (mCursor != null) {
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ }
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mDataValid && mCursor != null) {
+ return mCursor.getCount();
+ }
+ return 0;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
+ return mCursor.getLong(mRowIdColumn);
+ }
+ return 0;
+ }
+
+ public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
+
+ @Override
+ public void onBindViewHolder(VH viewHolder, int position) {
+ if (!mDataValid) {
+ throw new IllegalStateException("this should only be called when the cursor is valid");
+ }
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+ onBindViewHolder(viewHolder, mCursor);
+ }
+
+ /**
+ * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
+ * closed.
+ */
+ public void changeCursor(Cursor cursor) {
+ Cursor old = swapCursor(cursor);
+ if (old != null) {
+ old.close();
+ }
+ }
+
+ /**
+ * Swap in a new Cursor, returning the old Cursor. Unlike
+ * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
+ * closed.
+ */
+ public Cursor swapCursor(Cursor newCursor) {
+ if (newCursor == mCursor) {
+ return null;
+ }
+ final Cursor oldCursor = mCursor;
+ if (oldCursor != null && mDataSetObserver != null) {
+ oldCursor.unregisterDataSetObserver(mDataSetObserver);
+ }
+ mCursor = newCursor;
+ if (mCursor != null) {
+ if (mDataSetObserver != null) {
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ }
+ mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
+ mDataValid = true;
+ notifyDataSetChanged();
+ } else {
+ mRowIdColumn = -1;
+ mDataValid = false;
+ notifyDataSetChanged();
+ }
+ return oldCursor;
+ }
+
+ private class NotifyingDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ mDataValid = false;
+ notifyDataSetChanged();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteBlockAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteBlockAdapter.java
new file mode 100644
index 000000000..0d19c796b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteBlockAdapter.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.notifications.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.notifications.blocks.NoteBlock;
+
+import java.util.List;
+
+public class NoteBlockAdapter extends ArrayAdapter<NoteBlock> {
+
+ private final LayoutInflater mLayoutInflater;
+ private final int mBackgroundColor;
+
+ private List<NoteBlock> mNoteBlockList;
+
+ public NoteBlockAdapter(Context context, List<NoteBlock> noteBlocks, int backgroundColor) {
+ super(context, 0, noteBlocks);
+
+ mNoteBlockList = noteBlocks;
+ mLayoutInflater = LayoutInflater.from(context);
+ mBackgroundColor = backgroundColor;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ return mNoteBlockList == null ? 0 : mNoteBlockList.size();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ NoteBlock noteBlock = mNoteBlockList.get(position);
+
+ // Check the tag for this recycled view, if it matches we can reuse it
+ if (convertView == null || noteBlock.getBlockType() != convertView.getTag(R.id.note_block_tag_id)) {
+ convertView = mLayoutInflater.inflate(noteBlock.getLayoutResourceId(), parent, false);
+ convertView.setTag(noteBlock.getViewHolder(convertView));
+ }
+
+ // Update the block type for this view
+ convertView.setTag(R.id.note_block_tag_id, noteBlock.getBlockType());
+
+ noteBlock.setBackgroundColor(mBackgroundColor);
+
+ return noteBlock.configureView(convertView);
+ }
+
+ public void setNoteList(List<NoteBlock> noteList) {
+ if (noteList == null) return;
+
+ mNoteBlockList = noteList;
+ notifyDataSetChanged();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.java
new file mode 100644
index 000000000..e4ab2f6b4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.java
@@ -0,0 +1,309 @@
+package org.wordpress.android.ui.notifications.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.simperium.client.Bucket;
+import com.simperium.client.Query;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.comments.CommentUtils;
+import org.wordpress.android.ui.notifications.NotificationsListFragment;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.SqlUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.widgets.NoticonTextView;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NotesAdapter extends CursorRecyclerViewAdapter<NotesAdapter.NoteViewHolder> {
+
+ private final int mAvatarSz;
+ private final Bucket<Note> mNotesBucket;
+ private final int mColorRead;
+ private final int mColorUnread;
+ private final int mTextIndentSize;
+ private final List<String> mHiddenNoteIds = new ArrayList<>();
+ private final List<String> mModeratingNoteIds = new ArrayList<>();
+
+ private Query mQuery;
+
+ private NotificationsListFragment.OnNoteClickListener mOnNoteClickListener;
+
+ public NotesAdapter(Context context, Bucket<Note> bucket) {
+ super(context, null);
+
+ setHasStableIds(true);
+
+ mNotesBucket = bucket;
+ // build a query that sorts by timestamp descending
+ mQuery = new Query();
+
+ mAvatarSz = (int) context.getResources().getDimension(R.dimen.notifications_avatar_sz);
+ mColorRead = context.getResources().getColor(R.color.white);
+ mColorUnread = context.getResources().getColor(R.color.grey_light);
+ mTextIndentSize = context.getResources().getDimensionPixelSize(R.dimen.notifications_text_indent_sz);
+ }
+
+ public void closeCursor() {
+ Cursor cursor = getCursor();
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ private Query getQueryDefaults() {
+ return mNotesBucket.query()
+ .include(
+ Note.Schema.TIMESTAMP_INDEX,
+ Note.Schema.SUBJECT_INDEX,
+ Note.Schema.SNIPPET_INDEX,
+ Note.Schema.UNREAD_INDEX,
+ Note.Schema.ICON_URL_INDEX,
+ Note.Schema.NOTICON_INDEX,
+ Note.Schema.IS_UNAPPROVED_INDEX,
+ Note.Schema.COMMENT_SUBJECT_NOTICON,
+ Note.Schema.LOCAL_STATUS)
+ .order(Note.Schema.TIMESTAMP_INDEX, Query.SortType.DESCENDING);
+ }
+
+ public void queryNotes() {
+ mQuery = getQueryDefaults();
+ changeCursor(mQuery.execute());
+ }
+
+ public void queryNotes(String columnName, Object value) {
+ mQuery = getQueryDefaults();
+ mQuery.where(columnName, Query.ComparisonType.EQUAL_TO, value);
+ changeCursor(mQuery.execute());
+ }
+
+ public void addHiddenNoteId(String noteId) {
+ mHiddenNoteIds.add(noteId);
+ notifyDataSetChanged();
+ }
+
+ public void removeHiddenNoteId(String noteId) {
+ mHiddenNoteIds.remove(noteId);
+ notifyDataSetChanged();
+ }
+
+ public void addModeratingNoteId(String noteId) {
+ mModeratingNoteIds.add(noteId);
+ notifyDataSetChanged();
+ }
+
+ public void removeModeratingNoteId(String noteId) {
+ mModeratingNoteIds.remove(noteId);
+ notifyDataSetChanged();
+ }
+
+ private String getStringForColumnName(Cursor cursor, String columnName) {
+ if (columnName == null || cursor == null || cursor.getColumnIndex(columnName) == -1) {
+ return "";
+ }
+
+ return StringUtils.notNullStr(cursor.getString(cursor.getColumnIndex(columnName)));
+ }
+
+ private int getIntForColumnName(Cursor cursor, String columnName) {
+ if (columnName == null || cursor == null || cursor.getColumnIndex(columnName) == -1) {
+ return -1;
+ }
+
+ return cursor.getInt(cursor.getColumnIndex(columnName));
+ }
+
+ private long getLongForColumnName(Cursor cursor, String columnName) {
+ if (columnName == null || cursor == null || cursor.getColumnIndex(columnName) == -1) {
+ return -1;
+ }
+
+ return cursor.getLong(cursor.getColumnIndex(columnName));
+ }
+
+ public int getCount() {
+ if (getCursor() != null) {
+ return getCursor().getCount();
+ }
+
+ return 0;
+ }
+
+ @Override
+ public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.notifications_list_item, parent, false);
+
+ return new NoteViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(NoteViewHolder noteViewHolder, Cursor cursor) {
+ final Bucket.ObjectCursor<Note> objectCursor = (Bucket.ObjectCursor<Note>) cursor;
+ noteViewHolder.itemView.setTag(objectCursor.getSimperiumKey());
+
+ // Display group header
+ Note.NoteTimeGroup timeGroup = Note.getTimeGroupForTimestamp(getLongForColumnName(objectCursor, Note.Schema.TIMESTAMP_INDEX));
+
+ Note.NoteTimeGroup previousTimeGroup = null;
+ if (objectCursor.getPosition() > 0 && objectCursor.moveToPrevious()) {
+ previousTimeGroup = Note.getTimeGroupForTimestamp(getLongForColumnName(objectCursor, Note.Schema.TIMESTAMP_INDEX));
+ objectCursor.moveToNext();
+ }
+
+ if (previousTimeGroup != null && previousTimeGroup == timeGroup) {
+ noteViewHolder.headerView.setVisibility(View.GONE);
+ } else {
+ if (timeGroup == Note.NoteTimeGroup.GROUP_TODAY) {
+ noteViewHolder.headerText.setText(R.string.stats_timeframe_today);
+ } else if (timeGroup == Note.NoteTimeGroup.GROUP_YESTERDAY) {
+ noteViewHolder.headerText.setText(R.string.stats_timeframe_yesterday);
+ } else if (timeGroup == Note.NoteTimeGroup.GROUP_OLDER_TWO_DAYS) {
+ noteViewHolder.headerText.setText(R.string.older_two_days);
+ } else if (timeGroup == Note.NoteTimeGroup.GROUP_OLDER_WEEK) {
+ noteViewHolder.headerText.setText(R.string.older_last_week);
+ } else {
+ noteViewHolder.headerText.setText(R.string.older_month);
+ }
+
+ noteViewHolder.headerView.setVisibility(View.VISIBLE);
+ }
+
+ if (mHiddenNoteIds.size() > 0 && mHiddenNoteIds.contains(objectCursor.getSimperiumKey())) {
+ noteViewHolder.contentView.setVisibility(View.GONE);
+ noteViewHolder.headerView.setVisibility(View.GONE);
+ } else {
+ noteViewHolder.contentView.setVisibility(View.VISIBLE);
+ }
+
+ CommentStatus commentStatus = CommentStatus.UNKNOWN;
+ if (SqlUtils.sqlToBool(getIntForColumnName(objectCursor, Note.Schema.IS_UNAPPROVED_INDEX))) {
+ commentStatus = CommentStatus.UNAPPROVED;
+ }
+
+ String localStatus = getStringForColumnName(objectCursor, Note.Schema.LOCAL_STATUS);
+ if (!TextUtils.isEmpty(localStatus)) {
+ commentStatus = CommentStatus.fromString(localStatus);
+ }
+
+ if (mModeratingNoteIds.size() > 0 && mModeratingNoteIds.contains(objectCursor.getSimperiumKey())) {
+ noteViewHolder.progressBar.setVisibility(View.VISIBLE);
+ } else {
+ noteViewHolder.progressBar.setVisibility(View.GONE);
+ }
+
+ // Subject is stored in db as html to preserve text formatting
+ String noteSubjectHtml = getStringForColumnName(objectCursor, Note.Schema.SUBJECT_INDEX).trim();
+ CharSequence noteSubjectSpanned = Html.fromHtml(noteSubjectHtml);
+ // Trim the '\n\n' added by Html.fromHtml()
+ noteSubjectSpanned = noteSubjectSpanned.subSequence(0, TextUtils.getTrimmedLength(noteSubjectSpanned));
+ noteViewHolder.txtSubject.setText(noteSubjectSpanned);
+
+ String noteSubjectNoticon = getStringForColumnName(objectCursor, Note.Schema.COMMENT_SUBJECT_NOTICON);
+ if (!TextUtils.isEmpty(noteSubjectNoticon)) {
+ CommentUtils.indentTextViewFirstLine(noteViewHolder.txtSubject, mTextIndentSize);
+ noteViewHolder.txtSubjectNoticon.setText(noteSubjectNoticon);
+ noteViewHolder.txtSubjectNoticon.setVisibility(View.VISIBLE);
+ } else {
+ noteViewHolder.txtSubjectNoticon.setVisibility(View.GONE);
+ }
+
+ String noteSnippet = getStringForColumnName(objectCursor, Note.Schema.SNIPPET_INDEX);
+ if (!TextUtils.isEmpty(noteSnippet)) {
+ noteViewHolder.txtSubject.setMaxLines(2);
+ noteViewHolder.txtDetail.setText(noteSnippet);
+ noteViewHolder.txtDetail.setVisibility(View.VISIBLE);
+ } else {
+ noteViewHolder.txtSubject.setMaxLines(3);
+ noteViewHolder.txtDetail.setVisibility(View.GONE);
+ }
+
+ String avatarUrl = GravatarUtils.fixGravatarUrl(getStringForColumnName(objectCursor, Note.Schema.ICON_URL_INDEX), mAvatarSz);
+ noteViewHolder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+
+ boolean isUnread = SqlUtils.sqlToBool(getIntForColumnName(objectCursor, Note.Schema.UNREAD_INDEX));
+
+ String noticonCharacter = getStringForColumnName(objectCursor, Note.Schema.NOTICON_INDEX);
+ noteViewHolder.noteIcon.setText(noticonCharacter);
+ if (commentStatus == CommentStatus.UNAPPROVED) {
+ noteViewHolder.noteIcon.setBackgroundResource(R.drawable.shape_oval_orange);
+ } else if (isUnread) {
+ noteViewHolder.noteIcon.setBackgroundResource(R.drawable.shape_oval_blue_white_stroke);
+ } else {
+ noteViewHolder.noteIcon.setBackgroundResource(R.drawable.shape_oval_grey);
+ }
+
+ if (isUnread) {
+ noteViewHolder.itemView.setBackgroundColor(mColorUnread);
+ } else {
+ noteViewHolder.itemView.setBackgroundColor(mColorRead);
+ }
+ }
+
+ public int getPositionForNote(String noteId) {
+ Bucket.ObjectCursor<Note> cursor = (Bucket.ObjectCursor<Note>) getCursor();
+ if (cursor != null) {
+ for (int i = 0; i < cursor.getCount(); i++) {
+ cursor.moveToPosition(i);
+ String noteKey = cursor.getSimperiumKey();
+ if (noteKey != null && noteKey.equals(noteId)) {
+ return i;
+ }
+ }
+ }
+
+ return RecyclerView.NO_POSITION;
+ }
+
+ public void setOnNoteClickListener(NotificationsListFragment.OnNoteClickListener mNoteClickListener) {
+ mOnNoteClickListener = mNoteClickListener;
+ }
+
+ class NoteViewHolder extends RecyclerView.ViewHolder {
+ private final View headerView;
+ private final View contentView;
+ private final TextView headerText;
+
+ private final TextView txtSubject;
+ private final TextView txtSubjectNoticon;
+ private final TextView txtDetail;
+ private final WPNetworkImageView imgAvatar;
+ private final NoticonTextView noteIcon;
+ private final View progressBar;
+
+ public NoteViewHolder(View view) {
+ super(view);
+ headerView = view.findViewById(R.id.time_header);
+ contentView = view.findViewById(R.id.note_content_container);
+ headerText = (TextView)view.findViewById(R.id.header_date_text);
+ txtSubject = (TextView) view.findViewById(R.id.note_subject);
+ txtSubjectNoticon = (TextView) view.findViewById(R.id.note_subject_noticon);
+ txtDetail = (TextView) view.findViewById(R.id.note_detail);
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.note_avatar);
+ noteIcon = (NoticonTextView) view.findViewById(R.id.note_icon);
+ progressBar = view.findViewById(R.id.moderate_progress);
+
+ itemView.setOnClickListener(mOnClickListener);
+ }
+ }
+
+ private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnNoteClickListener != null && v.getTag() instanceof String) {
+ mOnNoteClickListener.onClickNote((String)v.getTag());
+ }
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/BlockType.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/BlockType.java
new file mode 100644
index 000000000..688979255
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/BlockType.java
@@ -0,0 +1,34 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.text.TextUtils;
+
+/** BlockTypes that we know about
+ * Unknown blocks will still be displayed using the rules for BASIC blocks
+ */
+public enum BlockType {
+ UNKNOWN,
+ BASIC,
+ USER,
+ USER_HEADER,
+ USER_COMMENT,
+ FOOTER;
+
+ public static BlockType fromString(String blockType) {
+ if (TextUtils.isEmpty(blockType)) return UNKNOWN;
+
+ switch (blockType) {
+ case "basic":
+ return BASIC;
+ case "user":
+ return USER;
+ case "user_header":
+ return USER_HEADER;
+ case "user_comment":
+ return USER_COMMENT;
+ case "footer":
+ return FOOTER;
+ default:
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java
new file mode 100644
index 000000000..8f8513482
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java
@@ -0,0 +1,232 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.content.Context;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+// A user block with slightly different formatting for display in a comment detail
+public class CommentUserNoteBlock extends UserNoteBlock {
+
+ private CommentStatus mCommentStatus = CommentStatus.UNKNOWN;
+ private int mNormalBackgroundColor;
+ private int mNormalTextColor;
+ private int mAgoTextColor;
+ private int mUnapprovedTextColor;
+ private int mIndentedLeftPadding;
+
+ private boolean mStatusChanged;
+
+ public interface OnCommentStatusChangeListener {
+ public void onCommentStatusChanged(CommentStatus newStatus);
+ }
+
+ public CommentUserNoteBlock(Context context, JSONObject noteObject,
+ OnNoteBlockTextClickListener onNoteBlockTextClickListener,
+ OnGravatarClickedListener onGravatarClickedListener) {
+ super(context, noteObject, onNoteBlockTextClickListener, onGravatarClickedListener);
+
+ if (context != null) {
+ setAvatarSize(context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small));
+ }
+ }
+
+ @Override
+ public BlockType getBlockType() {
+ return BlockType.USER_COMMENT;
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.note_block_comment_user;
+ }
+
+ @Override
+ public View configureView(View view) {
+ final CommentUserNoteBlockHolder noteBlockHolder = (CommentUserNoteBlockHolder)view.getTag();
+
+ noteBlockHolder.nameTextView.setText(Html.fromHtml("<strong>" + getNoteText().toString() + "</strong>"));
+ noteBlockHolder.agoTextView.setText(DateTimeUtils.timeSpanFromTimestamp(getTimestamp(),
+ WordPress.getContext()));
+ if (!TextUtils.isEmpty(getMetaHomeTitle()) || !TextUtils.isEmpty(getMetaSiteUrl())) {
+ noteBlockHolder.bulletTextView.setVisibility(View.VISIBLE);
+ noteBlockHolder.siteTextView.setVisibility(View.VISIBLE);
+ if (!TextUtils.isEmpty(getMetaHomeTitle())) {
+ noteBlockHolder.siteTextView.setText(getMetaHomeTitle());
+ } else {
+ noteBlockHolder.siteTextView.setText(getMetaSiteUrl().replace("http://", "").replace("https://", ""));
+ }
+ } else {
+ noteBlockHolder.bulletTextView.setVisibility(View.GONE);
+ noteBlockHolder.siteTextView.setVisibility(View.GONE);
+ }
+
+ if (hasImageMediaItem()) {
+ String imageUrl = GravatarUtils.fixGravatarUrl(getNoteMediaItem().optString("url", ""), getAvatarSize());
+ noteBlockHolder.avatarImageView.setImageUrl(imageUrl, WPNetworkImageView.ImageType.AVATAR);
+ if (!TextUtils.isEmpty(getUserUrl())) {
+ noteBlockHolder.avatarImageView.setOnTouchListener(mOnGravatarTouchListener);
+ } else {
+ noteBlockHolder.avatarImageView.setOnTouchListener(null);
+ }
+ } else {
+ noteBlockHolder.avatarImageView.showDefaultGravatarImage();
+ noteBlockHolder.avatarImageView.setOnTouchListener(null);
+ }
+
+ noteBlockHolder.commentTextView.setText(
+ NotificationsUtils.getSpannableContentForRanges(
+ getNoteData().optJSONObject("comment_text"),
+ noteBlockHolder.commentTextView,
+ getOnNoteBlockTextClickListener(),
+ false)
+ );
+
+ // Change display based on comment status and type:
+ // 1. Comment replies are indented and have a 'pipe' background
+ // 2. Unapproved comments have different background and text color
+ int paddingLeft = view.getPaddingLeft();
+ int paddingTop = view.getPaddingTop();
+ int paddingRight = view.getPaddingRight();
+ int paddingBottom = view.getPaddingBottom();
+ if (mCommentStatus == CommentStatus.UNAPPROVED) {
+ if (hasCommentNestingLevel()) {
+ paddingLeft = mIndentedLeftPadding;
+ view.setBackgroundResource(R.drawable.comment_reply_unapproved_background);
+ } else {
+ view.setBackgroundResource(R.drawable.comment_unapproved_background);
+ }
+
+ noteBlockHolder.dividerView.setVisibility(View.INVISIBLE);
+
+ noteBlockHolder.agoTextView.setTextColor(mUnapprovedTextColor);
+ noteBlockHolder.bulletTextView.setTextColor(mUnapprovedTextColor);
+ noteBlockHolder.siteTextView.setTextColor(mUnapprovedTextColor);
+ noteBlockHolder.nameTextView.setTextColor(mUnapprovedTextColor);
+ noteBlockHolder.commentTextView.setTextColor(mUnapprovedTextColor);
+ } else {
+ if (hasCommentNestingLevel()) {
+ paddingLeft = mIndentedLeftPadding;
+ view.setBackgroundResource(R.drawable.comment_reply_background);
+ noteBlockHolder.dividerView.setVisibility(View.INVISIBLE);
+ } else {
+ view.setBackgroundColor(mNormalBackgroundColor);
+ noteBlockHolder.dividerView.setVisibility(View.VISIBLE);
+ }
+
+ noteBlockHolder.agoTextView.setTextColor(mAgoTextColor);
+ noteBlockHolder.bulletTextView.setTextColor(mAgoTextColor);
+ noteBlockHolder.siteTextView.setTextColor(mAgoTextColor);
+ noteBlockHolder.nameTextView.setTextColor(mNormalTextColor);
+ noteBlockHolder.commentTextView.setTextColor(mNormalTextColor);
+ }
+
+ view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+
+ // If status was changed, fade in the view
+ if (mStatusChanged) {
+ mStatusChanged = false;
+ view.setAlpha(0.4f);
+ view.animate().alpha(1.0f).start();
+ }
+
+ return view;
+ }
+
+ private long getTimestamp() {
+ return getNoteData().optInt("timestamp", 0);
+ }
+
+ private boolean hasCommentNestingLevel() {
+ try {
+ JSONObject commentTextObject = getNoteData().getJSONObject("comment_text");
+ return commentTextObject.optInt("nest_level", 0) > 0;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public Object getViewHolder(View view) {
+ return new CommentUserNoteBlockHolder(view);
+ }
+
+ private class CommentUserNoteBlockHolder {
+ private final WPNetworkImageView avatarImageView;
+ private final TextView nameTextView;
+ private final TextView agoTextView;
+ private final TextView bulletTextView;
+ private final TextView siteTextView;
+ private final TextView commentTextView;
+ private final View dividerView;
+
+ public CommentUserNoteBlockHolder(View view) {
+ nameTextView = (TextView)view.findViewById(R.id.user_name);
+ agoTextView = (TextView)view.findViewById(R.id.user_comment_ago);
+ agoTextView.setVisibility(View.VISIBLE);
+ bulletTextView = (TextView)view.findViewById(R.id.user_comment_bullet);
+ siteTextView = (TextView)view.findViewById(R.id.user_comment_site);
+ commentTextView = (TextView)view.findViewById(R.id.user_comment);
+ commentTextView.setMovementMethod(new NoteBlockLinkMovementMethod());
+ avatarImageView = (WPNetworkImageView)view.findViewById(R.id.user_avatar);
+ dividerView = view.findViewById(R.id.divider_view);
+
+ siteTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (getOnNoteBlockTextClickListener() != null) {
+ getOnNoteBlockTextClickListener().showSitePreview(getMetaSiteId(), getMetaSiteUrl());
+ }
+ }
+ });
+
+ // show all comments on this post when user clicks the comment text
+ commentTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (getOnNoteBlockTextClickListener() != null) {
+ getOnNoteBlockTextClickListener().showReaderPostComments();
+ }
+ }
+ });
+ }
+ }
+
+ public void configureResources(Context context) {
+ if (context == null) return;
+
+ mNormalTextColor = context.getResources().getColor(R.color.grey_dark);
+ mNormalBackgroundColor = context.getResources().getColor(R.color.white);
+ mAgoTextColor = context.getResources().getColor(R.color.grey);
+ mUnapprovedTextColor = context.getResources().getColor(R.color.notification_status_unapproved_dark);
+ // Double margin_extra_large for increased indent in comment replies
+ mIndentedLeftPadding = context.getResources().getDimensionPixelSize(R.dimen.margin_extra_large) * 2;
+ }
+
+ private final OnCommentStatusChangeListener mOnCommentChangedListener = new OnCommentStatusChangeListener() {
+ @Override
+ public void onCommentStatusChanged(CommentStatus newStatus) {
+ mCommentStatus = newStatus;
+ mStatusChanged = true;
+ }
+ };
+
+ public void setCommentStatus(CommentStatus status) {
+ mCommentStatus = status;
+ }
+
+ public OnCommentStatusChangeListener getOnCommentChangeListener() {
+ return mOnCommentChangedListener;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/FooterNoteBlock.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/FooterNoteBlock.java
new file mode 100644
index 000000000..b4664643f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/FooterNoteBlock.java
@@ -0,0 +1,116 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+public class FooterNoteBlock extends NoteBlock {
+ private NoteBlockClickableSpan mClickableSpan;
+
+ public FooterNoteBlock(JSONObject noteObject, OnNoteBlockTextClickListener onNoteBlockTextClickListener) {
+ super(noteObject, onNoteBlockTextClickListener);
+ }
+
+ public void setClickableSpan(JSONObject rangeObject, String noteType) {
+ if (rangeObject == null) return;
+
+ mClickableSpan = new NoteBlockClickableSpan(
+ WordPress.getContext(),
+ rangeObject,
+ false,
+ true
+ );
+
+ mClickableSpan.setCustomType(noteType);
+ }
+
+ @Override
+ public BlockType getBlockType() {
+ return BlockType.FOOTER;
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.note_block_footer;
+ }
+
+ @Override
+ public View configureView(final View view) {
+ final FooterNoteBlockHolder noteBlockHolder = (FooterNoteBlockHolder)view.getTag();
+
+ // Note text
+ if (!TextUtils.isEmpty(getNoteText())) {
+ noteBlockHolder.getTextView().setText(getNoteText());
+ noteBlockHolder.getTextView().setVisibility(View.VISIBLE);
+ } else {
+ noteBlockHolder.getTextView().setVisibility(View.GONE);
+ }
+
+ String noticonGlyph = getNoticonGlyph();
+ if (!TextUtils.isEmpty(noticonGlyph)) {
+ noteBlockHolder.getNoticonView().setVisibility(View.VISIBLE);
+ noteBlockHolder.getNoticonView().setText(noticonGlyph);
+ } else {
+ noteBlockHolder.getNoticonView().setVisibility(View.GONE);
+ }
+
+ return view;
+ }
+
+ private String getNoticonGlyph() {
+ if (getNoteData() == null) return "";
+
+ return JSONUtils.queryJSON(getNoteData(), "ranges[first].value", "");
+ }
+
+ @Override
+ public Spannable getNoteText() {
+ return NotificationsUtils.getSpannableContentForRanges(getNoteData(), null,
+ getOnNoteBlockTextClickListener(), true);
+ }
+
+ public Object getViewHolder(View view) {
+ return new FooterNoteBlockHolder(view);
+ }
+
+ class FooterNoteBlockHolder {
+ private final View mFooterView;
+ private final TextView mTextView;
+ private final TextView mNoticonView;
+
+ FooterNoteBlockHolder(View view) {
+ mFooterView = view.findViewById(R.id.note_footer);
+ mFooterView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onRangeClick();
+ }
+ });
+ mTextView = (TextView) view.findViewById(R.id.note_footer_text);
+ mNoticonView = (TextView) view.findViewById(R.id.note_footer_noticon);
+ }
+
+ public TextView getTextView() {
+ return mTextView;
+ }
+ public TextView getNoticonView() {
+ return mNoticonView;
+ }
+ }
+
+ public void onRangeClick() {
+ if (mClickableSpan == null || getOnNoteBlockTextClickListener() == null) {
+ return;
+ }
+
+ getOnNoteBlockTextClickListener().onNoteBlockTextClicked(mClickableSpan);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.java
new file mode 100644
index 000000000..34985a5de
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.java
@@ -0,0 +1,164 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.content.Context;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+// Note header, displayed at top of detail view
+public class HeaderNoteBlock extends NoteBlock {
+
+ private final JSONArray mHeaderArray;
+
+ private final UserNoteBlock.OnGravatarClickedListener mGravatarClickedListener;
+ private Boolean mIsComment;
+ private int mAvatarSize;
+
+ private WPNetworkImageView.ImageType mImageType;
+
+ public HeaderNoteBlock(Context context, JSONArray headerArray, WPNetworkImageView.ImageType imageType,
+ OnNoteBlockTextClickListener onNoteBlockTextClickListener,
+ UserNoteBlock.OnGravatarClickedListener onGravatarClickedListener) {
+ super(new JSONObject(), onNoteBlockTextClickListener);
+
+ mHeaderArray = headerArray;
+ mImageType = imageType;
+ mGravatarClickedListener = onGravatarClickedListener;
+
+ if (context != null) {
+ mAvatarSize = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small);
+ }
+ }
+
+ @Override
+ public BlockType getBlockType() {
+ return BlockType.USER_HEADER;
+ }
+
+ public int getLayoutResourceId() {
+ return R.layout.note_block_header;
+ }
+
+ @Override
+ public View configureView(View view) {
+ final NoteHeaderBlockHolder noteBlockHolder = (NoteHeaderBlockHolder)view.getTag();
+
+ Spannable spannable = NotificationsUtils.getSpannableContentForRanges(mHeaderArray.optJSONObject(0));
+ noteBlockHolder.nameTextView.setText(spannable);
+
+ noteBlockHolder.avatarImageView.setImageUrl(getAvatarUrl(), mImageType);
+ if (!TextUtils.isEmpty(getUserUrl())) {
+ noteBlockHolder.avatarImageView.setOnTouchListener(mOnGravatarTouchListener);
+ } else {
+ noteBlockHolder.avatarImageView.setOnTouchListener(null);
+ }
+
+ noteBlockHolder.snippetTextView.setText(getSnippet());
+
+ if (mIsComment) {
+ View footerView = view.findViewById(R.id.header_footer);
+ View footerCommentView = view.findViewById(R.id.header_footer_comment);
+ footerView.setVisibility(View.GONE);
+ footerCommentView.setVisibility(View.VISIBLE);
+ }
+
+ return view;
+ }
+
+ private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (getOnNoteBlockTextClickListener() != null) {
+ getOnNoteBlockTextClickListener().showDetailForNoteIds();
+ }
+ }
+ };
+
+ private String getUserName() {
+ return JSONUtils.queryJSON(mHeaderArray, "[0].text", "");
+ }
+
+ private String getAvatarUrl() {
+ return GravatarUtils.fixGravatarUrl(JSONUtils.queryJSON(mHeaderArray, "[0].media[0].url", ""), mAvatarSize);
+ }
+
+ private String getUserUrl() {
+ return JSONUtils.queryJSON(mHeaderArray, "[0].ranges[0].url", "");
+ }
+
+ private String getSnippet() {
+ return JSONUtils.queryJSON(mHeaderArray, "[1].text", "");
+ }
+
+ @Override
+ public Object getViewHolder(View view) {
+ return new NoteHeaderBlockHolder(view);
+ }
+
+ public void setIsComment(Boolean isComment) {
+ mIsComment = isComment;
+ }
+
+ private class NoteHeaderBlockHolder {
+ private final TextView nameTextView;
+ private final TextView snippetTextView;
+ private final WPNetworkImageView avatarImageView;
+
+ public NoteHeaderBlockHolder(View view) {
+ View rootView = view.findViewById(R.id.header_root_view);
+ rootView.setOnClickListener(mOnClickListener);
+ nameTextView = (TextView)view.findViewById(R.id.header_user);
+ snippetTextView = (TextView)view.findViewById(R.id.header_snippet);
+ avatarImageView = (WPNetworkImageView)view.findViewById(R.id.header_avatar);
+ }
+ }
+
+ private final View.OnTouchListener mOnGravatarTouchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+
+ int animationDuration = 150;
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ v.animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .alpha(0.5f)
+ .setDuration(animationDuration)
+ .setInterpolator(new DecelerateInterpolator());
+ } else if (event.getActionMasked() == MotionEvent.ACTION_UP
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ v.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .alpha(1.0f)
+ .setDuration(animationDuration)
+ .setInterpolator(new DecelerateInterpolator());
+
+ if (event.getActionMasked() == MotionEvent.ACTION_UP && mGravatarClickedListener != null) {
+ // Fire the listener, which will load the site preview for the user's site
+ // In the future we can use this to load a 'profile view' (currently in R&D)
+ long siteId = Long.valueOf(JSONUtils.queryJSON(mHeaderArray, "[0].ranges[0].site_id", 0));
+ long userId = Long.valueOf(JSONUtils.queryJSON(mHeaderArray, "[0].ranges[0].id", 0));
+ String siteUrl = getUserUrl();
+ if (siteId > 0 && userId > 0) {
+ mGravatarClickedListener.onGravatarClicked(siteId, userId, siteUrl);
+ }
+ }
+ }
+
+ return true;
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.java
new file mode 100644
index 000000000..d1f56a419
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.java
@@ -0,0 +1,275 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.widgets.WPTextView;
+
+/**
+ * A block of data displayed in a notification.
+ * This basic block can support a media item (image/video) and/or text.
+ */
+public class NoteBlock {
+
+ private static final String PROPERTY_MEDIA_TYPE = "type";
+ private static final String PROPERTY_MEDIA_URL = "url";
+
+ private final JSONObject mNoteData;
+ private final OnNoteBlockTextClickListener mOnNoteBlockTextClickListener;
+ private JSONObject mMediaItem;
+ private boolean mIsBadge;
+ private boolean mHasAnimatedBadge;
+ private int mBackgroundColor;
+
+ public interface OnNoteBlockTextClickListener {
+ void onNoteBlockTextClicked(NoteBlockClickableSpan clickedSpan);
+ void showDetailForNoteIds();
+ void showReaderPostComments();
+ void showSitePreview(long siteId, String siteUrl);
+ }
+
+ public NoteBlock(JSONObject noteObject, OnNoteBlockTextClickListener onNoteBlockTextClickListener) {
+ mNoteData = noteObject;
+ mOnNoteBlockTextClickListener = onNoteBlockTextClickListener;
+ }
+
+ OnNoteBlockTextClickListener getOnNoteBlockTextClickListener() {
+ return mOnNoteBlockTextClickListener;
+ }
+
+ public BlockType getBlockType() {
+ return BlockType.BASIC;
+ }
+
+ JSONObject getNoteData() {
+ return mNoteData;
+ }
+
+ public Spannable getNoteText() {
+ return NotificationsUtils.getSpannableContentForRanges(mNoteData, null,
+ mOnNoteBlockTextClickListener, false);
+ }
+
+ public String getMetaHomeTitle() {
+ return JSONUtils.queryJSON(mNoteData, "meta.titles.home", "");
+ }
+
+ public long getMetaSiteId() {
+ return JSONUtils.queryJSON(mNoteData, "meta.ids.site", -1);
+ }
+
+ public String getMetaSiteUrl() {
+ return JSONUtils.queryJSON(mNoteData, "meta.links.home", "");
+ }
+
+ JSONObject getNoteMediaItem() {
+ if (mMediaItem == null) {
+ mMediaItem = JSONUtils.queryJSON(mNoteData, "media[0]", new JSONObject());
+ }
+
+ return mMediaItem;
+ }
+
+ public void setIsBadge() {
+ mIsBadge = true;
+ }
+
+ public void setBackgroundColor(int backgroundColor) {
+ mBackgroundColor = backgroundColor;
+ }
+
+ public int getLayoutResourceId() {
+ return R.layout.note_block_basic;
+ }
+
+ private boolean hasMediaArray() {
+ return mNoteData.has("media");
+ }
+
+ boolean hasImageMediaItem() {
+ String mediaType = getNoteMediaItem().optString(PROPERTY_MEDIA_TYPE, "");
+ return hasMediaArray() &&
+ (mediaType.startsWith("image") || mediaType.equals("badge")) &&
+ getNoteMediaItem().has(PROPERTY_MEDIA_URL);
+ }
+
+ boolean hasVideoMediaItem() {
+ return hasMediaArray() &&
+ getNoteMediaItem().optString(PROPERTY_MEDIA_TYPE, "").startsWith("video") &&
+ getNoteMediaItem().has(PROPERTY_MEDIA_URL);
+ }
+
+ public boolean containsBadgeMediaType() {
+ try {
+ JSONArray mediaArray = mNoteData.getJSONArray("media");
+ for (int i=0; i < mediaArray.length(); i++) {
+ JSONObject mediaObject = mediaArray.getJSONObject(i);
+ if (mediaObject.optString(PROPERTY_MEDIA_TYPE, "").equals("badge")) {
+ return true;
+ }
+ }
+ } catch (JSONException e) {
+ return false;
+ }
+
+ return false;
+ }
+
+ public View configureView(final View view) {
+ final BasicNoteBlockHolder noteBlockHolder = (BasicNoteBlockHolder)view.getTag();
+
+ // Note image
+ if (hasImageMediaItem()) {
+ // Request image, and animate it when loaded
+ noteBlockHolder.getImageView().setVisibility(View.VISIBLE);
+ WordPress.imageLoader.get(getNoteMediaItem().optString("url", ""), new ImageLoader.ImageListener() {
+ @Override
+ public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
+ if (!mHasAnimatedBadge && response.getBitmap() != null && view.getContext() != null) {
+ mHasAnimatedBadge = true;
+ noteBlockHolder.getImageView().setImageBitmap(response.getBitmap());
+ Animation pop = AnimationUtils.loadAnimation(view.getContext(), R.anim.pop);
+ noteBlockHolder.getImageView().startAnimation(pop);
+ noteBlockHolder.getImageView().setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ noteBlockHolder.hideImageView();
+ }
+ });
+ } else {
+ noteBlockHolder.hideImageView();
+ }
+
+ // Note video
+ if (hasVideoMediaItem()) {
+ noteBlockHolder.getVideoView().setVideoURI(Uri.parse(getNoteMediaItem().optString("url", "")));
+ noteBlockHolder.getVideoView().setVisibility(View.VISIBLE);
+ } else {
+ noteBlockHolder.hideVideoView();
+ }
+
+ // Note text
+ if (!TextUtils.isEmpty(getNoteText())) {
+ if (mIsBadge) {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT);
+ params.gravity = Gravity.CENTER_HORIZONTAL;
+ noteBlockHolder.getTextView().setLayoutParams(params);
+ noteBlockHolder.getTextView().setGravity(Gravity.CENTER_HORIZONTAL);
+ noteBlockHolder.getTextView().setPadding(0, DisplayUtils.dpToPx(view.getContext(), 8), 0, 0);
+ } else {
+ noteBlockHolder.getTextView().setGravity(Gravity.NO_GRAVITY);
+ noteBlockHolder.getTextView().setPadding(0, 0, 0, 0);
+ }
+ noteBlockHolder.getTextView().setText(getNoteText());
+ noteBlockHolder.getTextView().setVisibility(View.VISIBLE);
+ } else {
+ noteBlockHolder.getTextView().setVisibility(View.GONE);
+ }
+
+ view.setBackgroundColor(mBackgroundColor);
+
+ return view;
+ }
+
+ public Object getViewHolder(View view) {
+ return new BasicNoteBlockHolder(view);
+ }
+
+ static class BasicNoteBlockHolder {
+ private final LinearLayout mRootLayout;
+ private final WPTextView mTextView;
+
+ private ImageView mImageView;
+ private VideoView mVideoView;
+
+ BasicNoteBlockHolder(View view) {
+ mRootLayout = (LinearLayout)view;
+ mTextView = (WPTextView) view.findViewById(R.id.note_text);
+ mTextView.setMovementMethod(new NoteBlockLinkMovementMethod());
+ }
+
+ public WPTextView getTextView() {
+ return mTextView;
+ }
+
+ public ImageView getImageView() {
+ if (mImageView == null) {
+ mImageView = new ImageView(mRootLayout.getContext());
+ int imageSize = DisplayUtils.dpToPx(mRootLayout.getContext(), 180);
+ int imagePadding = mRootLayout.getContext().getResources().getDimensionPixelSize(R.dimen.margin_large);
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(imageSize, imageSize);
+ layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
+ mImageView.setLayoutParams(layoutParams);
+ mImageView.setPadding(0, imagePadding, 0, 0);
+ mRootLayout.addView(mImageView, 0);
+ }
+
+ return mImageView;
+ }
+
+ public VideoView getVideoView() {
+ if (mVideoView == null) {
+ mVideoView = new VideoView(mRootLayout.getContext());
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ DisplayUtils.dpToPx(mRootLayout.getContext(), 220));
+ mVideoView.setLayoutParams(layoutParams);
+ mRootLayout.addView(mVideoView, 0);
+
+ // Attach a mediaController if we are displaying a video.
+ final MediaController mediaController = new MediaController(mRootLayout.getContext());
+ mediaController.setMediaPlayer(mVideoView);
+
+ mVideoView.setMediaController(mediaController);
+ mediaController.requestFocus();
+ mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ // Show the media controls when the video is ready to be played.
+ mediaController.show(0);
+ }
+ });
+ }
+
+ return mVideoView;
+ }
+
+ public void hideImageView() {
+ if (mImageView != null) {
+ mImageView.setVisibility(View.GONE);
+ }
+ }
+
+ public void hideVideoView() {
+ if (mVideoView != null) {
+ mVideoView.setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockClickableSpan.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockClickableSpan.java
new file mode 100644
index 000000000..b9d514c8e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockClickableSpan.java
@@ -0,0 +1,150 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.support.annotation.NonNull;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.ClickableSpan;
+import android.view.View;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+/**
+ * A clickable span that includes extra ids/urls
+ * Maps to a 'range' in a WordPress.com note object
+ */
+public class NoteBlockClickableSpan extends ClickableSpan {
+ private long mId;
+ private long mSiteId;
+ private long mPostId;
+ private NoteBlockRangeType mRangeType;
+ private String mUrl;
+ private int[] mIndices;
+ private boolean mPressed;
+ private boolean mShouldLink;
+ private boolean mIsFooter;
+
+ private int mTextColor;
+ private int mBackgroundColor;
+ private int mLinkColor;
+ private int mLightTextColor;
+
+ private final JSONObject mBlockData;
+
+ public NoteBlockClickableSpan(Context context, JSONObject blockData, boolean shouldLink, boolean isFooter) {
+ mBlockData = blockData;
+ mShouldLink = shouldLink;
+ mIsFooter = isFooter;
+
+ // Text/background colors
+ mTextColor = context.getResources().getColor(R.color.grey_dark);
+ mBackgroundColor = context.getResources().getColor(R.color.pressed_wordpress);
+ mLinkColor = context.getResources().getColor(R.color.blue_medium);
+ mLightTextColor = context.getResources().getColor(R.color.grey);
+
+ processRangeData();
+ }
+
+
+ private void processRangeData() {
+ if (mBlockData != null) {
+ mId = JSONUtils.queryJSON(mBlockData, "id", 0);
+ mSiteId = JSONUtils.queryJSON(mBlockData, "site_id", 0);
+ mPostId = JSONUtils.queryJSON(mBlockData, "post_id", 0);
+ mRangeType = NoteBlockRangeType.fromString(JSONUtils.queryJSON(mBlockData, "type", ""));
+ mUrl = JSONUtils.queryJSON(mBlockData, "url", "");
+ mIndices = NotificationsUtils.getIndicesForRange(mBlockData);
+
+ mShouldLink = shouldLinkRangeType();
+
+ // Apply grey color to some types
+ if (mIsFooter || getRangeType() == NoteBlockRangeType.BLOCKQUOTE || getRangeType() == NoteBlockRangeType.POST) {
+ mTextColor = mLightTextColor;
+ }
+ }
+ }
+
+ // Don't link certain range types, or unknown ones, unless we have a URL
+ private boolean shouldLinkRangeType() {
+ return mShouldLink &&
+ mRangeType != NoteBlockRangeType.BLOCKQUOTE &&
+ mRangeType != NoteBlockRangeType.MATCH &&
+ (mRangeType != NoteBlockRangeType.UNKNOWN || !TextUtils.isEmpty(mUrl));
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ // Set background color
+ textPaint.bgColor = mShouldLink && mPressed && !isBlockquoteType() ?
+ mBackgroundColor : Color.TRANSPARENT;
+ textPaint.setColor(mShouldLink && !mIsFooter ? mLinkColor : mTextColor);
+ // No underlines
+ textPaint.setUnderlineText(mIsFooter);
+ }
+
+ private boolean isBlockquoteType() {
+ return getRangeType() == NoteBlockRangeType.BLOCKQUOTE;
+ }
+
+ // return the desired style for this id type
+ public int getSpanStyle() {
+ if (mIsFooter) {
+ return Typeface.BOLD;
+ }
+
+ switch (getRangeType()) {
+ case USER:
+ case MATCH:
+ return Typeface.BOLD;
+ case SITE:
+ case POST:
+ case COMMENT:
+ case BLOCKQUOTE:
+ return Typeface.ITALIC;
+ default:
+ return Typeface.NORMAL;
+ }
+ }
+
+ @Override
+ public void onClick(View widget) {
+ // noop
+ }
+
+ public NoteBlockRangeType getRangeType() {
+ return mRangeType;
+ }
+
+ public int[] getIndices() {
+ return mIndices;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public long getSiteId() {
+ return mSiteId;
+ }
+
+ public long getPostId() {
+ return mPostId;
+ }
+
+ public void setPressed(boolean isPressed) {
+ this.mPressed = isPressed;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public void setCustomType(String type) {
+ mRangeType = NoteBlockRangeType.fromString(type);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockLinkMovementMethod.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockLinkMovementMethod.java
new file mode 100644
index 000000000..8b53ab6ff
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockLinkMovementMethod.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * Allows links to be highlighted when tapped on note blocks.
+ * See: http://stackoverflow.com/a/20905824/309558
+ */
+class NoteBlockLinkMovementMethod extends LinkMovementMethod {
+
+ private NoteBlockClickableSpan mPressedSpan;
+
+ @Override
+ public boolean onTouchEvent(@NonNull TextView textView, @NonNull Spannable spannable, @NonNull MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mPressedSpan = getPressedSpan(textView, spannable, event);
+ if (mPressedSpan != null) {
+ mPressedSpan.setPressed(true);
+ Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan),
+ spannable.getSpanEnd(mPressedSpan));
+ }
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ NoteBlockClickableSpan touchedSpan = getPressedSpan(textView, spannable, event);
+ if (mPressedSpan != null && touchedSpan != mPressedSpan) {
+ mPressedSpan.setPressed(false);
+ mPressedSpan = null;
+ Selection.removeSelection(spannable);
+ }
+ } else {
+ if (mPressedSpan != null) {
+ mPressedSpan.setPressed(false);
+ super.onTouchEvent(textView, spannable, event);
+ }
+ mPressedSpan = null;
+ Selection.removeSelection(spannable);
+ }
+ return true;
+ }
+
+ private NoteBlockClickableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) {
+
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= textView.getTotalPaddingLeft();
+ y -= textView.getTotalPaddingTop();
+
+ x += textView.getScrollX();
+ y += textView.getScrollY();
+
+ Layout layout = textView.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ NoteBlockClickableSpan[] link = spannable.getSpans(off, off, NoteBlockClickableSpan.class);
+ NoteBlockClickableSpan touchedSpan = null;
+ if (link.length > 0) {
+ touchedSpan = link[0];
+ }
+
+ return touchedSpan;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockRangeType.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockRangeType.java
new file mode 100644
index 000000000..6018de45a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlockRangeType.java
@@ -0,0 +1,49 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.text.TextUtils;
+
+/**
+ * Known NoteBlock Range types
+ */
+public enum NoteBlockRangeType {
+ POST,
+ SITE,
+ COMMENT,
+ USER,
+ STAT,
+ BLOCKQUOTE,
+ FOLLOW,
+ NOTICON,
+ LIKE,
+ MATCH,
+ UNKNOWN;
+
+ public static NoteBlockRangeType fromString(String value) {
+ if (TextUtils.isEmpty(value)) return UNKNOWN;
+
+ switch (value) {
+ case "post":
+ return POST;
+ case "site":
+ return SITE;
+ case "comment":
+ return COMMENT;
+ case "user":
+ return USER;
+ case "stat":
+ return STAT;
+ case "blockquote":
+ return BLOCKQUOTE;
+ case "follow":
+ return FOLLOW;
+ case "noticon":
+ return NOTICON;
+ case "like":
+ return LIKE;
+ case "match":
+ return MATCH;
+ default:
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/UserNoteBlock.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/UserNoteBlock.java
new file mode 100644
index 000000000..5cdefeec8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/UserNoteBlock.java
@@ -0,0 +1,198 @@
+package org.wordpress.android.ui.notifications.blocks;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.TextView;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+/**
+ * A block that displays information about a User (such as a user that liked a post)
+ */
+public class UserNoteBlock extends NoteBlock {
+ private final OnGravatarClickedListener mGravatarClickedListener;
+
+ private int mAvatarSz;
+
+ public interface OnGravatarClickedListener {
+ // userId is currently unused, but will be handy once a profile view is added to the app
+ public void onGravatarClicked(long siteId, long userId, String siteUrl);
+ }
+
+ public UserNoteBlock(
+ Context context,
+ JSONObject noteObject,
+ OnNoteBlockTextClickListener onNoteBlockTextClickListener,
+ OnGravatarClickedListener onGravatarClickedListener) {
+ super(noteObject, onNoteBlockTextClickListener);
+ if (context != null) {
+ setAvatarSize(context.getResources().getDimensionPixelSize(R.dimen.notifications_avatar_sz));
+ }
+ mGravatarClickedListener = onGravatarClickedListener;
+ }
+
+ void setAvatarSize(int size) {
+ mAvatarSz = size;
+ }
+
+ int getAvatarSize() {
+ return mAvatarSz;
+ }
+
+ @Override
+ public BlockType getBlockType() {
+ return BlockType.USER;
+ }
+
+ @Override
+ public int getLayoutResourceId() {
+ return R.layout.note_block_user;
+ }
+
+ @Override
+ public View configureView(View view) {
+ final UserActionNoteBlockHolder noteBlockHolder = (UserActionNoteBlockHolder)view.getTag();
+ noteBlockHolder.nameTextView.setText(getNoteText().toString());
+
+
+ String linkedText = null;
+ if (hasUserUrlAndTitle()) {
+ linkedText = getUserBlogTitle();
+ } else if (hasUserUrl()) {
+ linkedText = getUserUrl();
+ }
+
+ if (!TextUtils.isEmpty(linkedText)) {
+ noteBlockHolder.urlTextView.setText(linkedText);
+ noteBlockHolder.urlTextView.setVisibility(View.VISIBLE);
+ } else {
+ noteBlockHolder.urlTextView.setVisibility(View.GONE);
+ }
+
+ if (hasUserBlogTagline()) {
+ noteBlockHolder.taglineTextView.setText(getUserBlogTagline());
+ noteBlockHolder.taglineTextView.setVisibility(View.VISIBLE);
+ } else {
+ noteBlockHolder.taglineTextView.setVisibility(View.GONE);
+ }
+
+ if (hasImageMediaItem()) {
+ String imageUrl = GravatarUtils.fixGravatarUrl(getNoteMediaItem().optString("url", ""), getAvatarSize());
+ noteBlockHolder.avatarImageView.setImageUrl(imageUrl, WPNetworkImageView.ImageType.AVATAR);
+ if (!TextUtils.isEmpty(getUserUrl())) {
+ noteBlockHolder.avatarImageView.setOnTouchListener(mOnGravatarTouchListener);
+ noteBlockHolder.rootView.setEnabled(true);
+ noteBlockHolder.rootView.setOnClickListener(mOnClickListener);
+ } else {
+ noteBlockHolder.avatarImageView.setOnTouchListener(null);
+ noteBlockHolder.rootView.setEnabled(false);
+ noteBlockHolder.rootView.setOnClickListener(null);
+ }
+ } else {
+ noteBlockHolder.avatarImageView.showDefaultGravatarImage();
+ noteBlockHolder.avatarImageView.setOnTouchListener(null);
+ }
+
+ return view;
+ }
+
+ private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showBlogPreview();
+ }
+ };
+
+ @Override
+ public Object getViewHolder(View view) {
+ return new UserActionNoteBlockHolder(view);
+ }
+
+ private class UserActionNoteBlockHolder {
+ private final View rootView;
+ private final TextView nameTextView;
+ private final TextView urlTextView;
+ private final TextView taglineTextView;
+ private final WPNetworkImageView avatarImageView;
+
+ public UserActionNoteBlockHolder(View view) {
+ rootView = view.findViewById(R.id.user_block_root_view);
+ nameTextView = (TextView)view.findViewById(R.id.user_name);
+ urlTextView = (TextView)view.findViewById(R.id.user_blog_url);
+ taglineTextView = (TextView)view.findViewById(R.id.user_blog_tagline);
+ avatarImageView = (WPNetworkImageView)view.findViewById(R.id.user_avatar);
+ }
+ }
+
+ String getUserUrl() {
+ return JSONUtils.queryJSON(getNoteData(), "meta.links.home", "");
+ }
+
+ private String getUserBlogTitle() {
+ return JSONUtils.queryJSON(getNoteData(), "meta.titles.home", "");
+ }
+
+ private String getUserBlogTagline() {
+ return JSONUtils.queryJSON(getNoteData(), "meta.titles.tagline", "");
+ }
+
+ private boolean hasUserUrl() {
+ return !TextUtils.isEmpty(getUserUrl());
+ }
+
+ private boolean hasUserUrlAndTitle() {
+ return hasUserUrl() && !TextUtils.isEmpty(getUserBlogTitle());
+ }
+
+ private boolean hasUserBlogTagline() {
+ return !TextUtils.isEmpty(getUserBlogTagline());
+ }
+
+ final View.OnTouchListener mOnGravatarTouchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+
+ int animationDuration = 150;
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ v.animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .alpha(0.5f)
+ .setDuration(animationDuration)
+ .setInterpolator(new DecelerateInterpolator());
+ } else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ v.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .alpha(1.0f)
+ .setDuration(animationDuration)
+ .setInterpolator(new DecelerateInterpolator());
+
+ if (event.getActionMasked() == MotionEvent.ACTION_UP && mGravatarClickedListener != null) {
+ // Fire the listener, which will load the site preview for the user's site
+ // In the future we can use this to load a 'profile view' (currently in R&D)
+ showBlogPreview();
+ }
+ }
+
+ return true;
+ }
+ };
+
+ private void showBlogPreview() {
+ long siteId = Long.valueOf(JSONUtils.queryJSON(getNoteData(), "meta.ids.site", 0));
+ long userId = Long.valueOf(JSONUtils.queryJSON(getNoteData(), "meta.ids.user", 0));
+ String siteUrl = getUserUrl();
+ if (mGravatarClickedListener != null) {
+ mGravatarClickedListener.onGravatarClicked(siteId, userId, siteUrl);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java
new file mode 100644
index 000000000..56d6caa19
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java
@@ -0,0 +1,540 @@
+package org.wordpress.android.ui.notifications.utils;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.design.widget.Snackbar;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.AlignmentSpan;
+import android.text.style.ImageSpan;
+import android.text.style.StyleSpan;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.comments.CommentActionResult;
+import org.wordpress.android.ui.comments.CommentActions;
+import org.wordpress.android.ui.notifications.NotificationEvents.NoteModerationFailed;
+import org.wordpress.android.ui.notifications.NotificationEvents.NoteModerationStatusChanged;
+import org.wordpress.android.ui.notifications.NotificationEvents.NoteVisibilityChanged;
+import org.wordpress.android.ui.notifications.NotificationsDetailActivity;
+import org.wordpress.android.ui.notifications.blocks.NoteBlock;
+import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DeviceUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.helpers.WPImageGetter;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsUtils {
+ public static final String ARG_PUSH_AUTH_TOKEN = "arg_push_auth_token";
+ public static final String ARG_PUSH_AUTH_TITLE = "arg_push_auth_title";
+ public static final String ARG_PUSH_AUTH_MESSAGE = "arg_push_auth_message";
+ public static final String ARG_PUSH_AUTH_EXPIRES = "arg_push_auth_expires";
+
+ public static final String WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS = "wp_pref_notification_settings";
+ public static final String WPCOM_PUSH_DEVICE_UUID = "wp_pref_notifications_uuid";
+ public static final String WPCOM_PUSH_DEVICE_TOKEN = "wp_pref_notifications_token";
+
+ public static final String WPCOM_PUSH_DEVICE_SERVER_ID = "wp_pref_notifications_server_id";
+ private static final String PUSH_AUTH_ENDPOINT = "me/two-step/push-authentication";
+
+ private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
+ private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
+
+ private static final String WPCOM_SETTINGS_ENDPOINT = "/me/notifications/settings/";
+
+ private static boolean mSnackbarDidUndo;
+
+ public static void getPushNotificationSettings(Context context, RestRequest.Listener listener,
+ RestRequest.ErrorListener errorListener) {
+ if (!AccountHelper.isSignedInWordPressDotCom()) {
+ return;
+ }
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ String deviceID = settings.getString(WPCOM_PUSH_DEVICE_SERVER_ID, null);
+ String settingsEndpoint = WPCOM_SETTINGS_ENDPOINT;
+ if (!TextUtils.isEmpty(deviceID)) {
+ settingsEndpoint += "?device_id=" + deviceID;
+ }
+ WordPress.getRestClientUtilsV1_1().get(settingsEndpoint, listener, errorListener);
+ }
+
+ public static void registerDeviceForPushNotifications(final Context ctx, String token) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ String uuid = settings.getString(WPCOM_PUSH_DEVICE_UUID, null);
+ if (uuid == null)
+ return;
+
+ String deviceName = DeviceUtils.getInstance().getDeviceName(ctx);
+ Map<String, String> contentStruct = new HashMap<>();
+ contentStruct.put("device_token", token);
+ contentStruct.put("device_family", "android");
+ contentStruct.put("device_name", deviceName);
+ contentStruct.put("device_model", Build.MANUFACTURER + " " + Build.MODEL);
+ contentStruct.put("app_version", WordPress.versionName);
+ contentStruct.put("os_version", Build.VERSION.RELEASE);
+ contentStruct.put("device_uuid", uuid);
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.d(T.NOTIFS, "Register token action succeeded");
+ try {
+ String deviceID = jsonObject.getString("ID");
+ if (deviceID==null) {
+ AppLog.e(T.NOTIFS, "Server response is missing of the device_id. Registration skipped!!");
+ return;
+ }
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(WPCOM_PUSH_DEVICE_SERVER_ID, deviceID);
+ editor.apply();
+ AppLog.d(T.NOTIFS, "Server response OK. The device_id: " + deviceID);
+ } catch (JSONException e1) {
+ AppLog.e(T.NOTIFS, "Server response is NOT ok, registration skipped.", e1);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.NOTIFS, "Register token action failed", volleyError);
+ }
+ };
+
+ WordPress.getRestClientUtils().post("/devices/new", contentStruct, null, listener, errorListener);
+ }
+
+ public static void unregisterDevicePushNotifications(final Context ctx) {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.d(T.NOTIFS, "Unregister token action succeeded");
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(ctx).edit();
+ editor.remove(WPCOM_PUSH_DEVICE_SERVER_ID);
+ editor.remove(WPCOM_PUSH_DEVICE_UUID);
+ editor.remove(WPCOM_PUSH_DEVICE_TOKEN);
+ editor.apply();
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.NOTIFS, "Unregister token action failed", volleyError);
+ }
+ };
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ String deviceID = settings.getString(WPCOM_PUSH_DEVICE_SERVER_ID, null );
+ if (TextUtils.isEmpty(deviceID)) {
+ return;
+ }
+ WordPress.getRestClientUtils().post("/devices/" + deviceID + "/delete", listener, errorListener);
+ }
+
+ public static Spannable getSpannableContentForRanges(JSONObject subject) {
+ return getSpannableContentForRanges(subject, null, null, false);
+ }
+
+ /**
+ * Returns a spannable with formatted content based on WP.com note content 'range' data
+ * @param blockObject the JSON data
+ * @param textView the TextView that will display the spannnable
+ * @param onNoteBlockTextClickListener - click listener for ClickableSpans in the spannable
+ * @param isFooter - Set if spannable should apply special formatting
+ * @return Spannable string with formatted content
+ */
+ public static Spannable getSpannableContentForRanges(JSONObject blockObject, TextView textView,
+ final NoteBlock.OnNoteBlockTextClickListener onNoteBlockTextClickListener,
+ boolean isFooter) {
+ if (blockObject == null) {
+ return new SpannableStringBuilder();
+ }
+
+ String text = blockObject.optString("text", "");
+ SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
+
+ boolean shouldLink = onNoteBlockTextClickListener != null;
+
+ // Add ImageSpans for note media
+ addImageSpansForBlockMedia(textView, blockObject, spannableStringBuilder);
+
+ // Process Ranges to add links and text formatting
+ JSONArray rangesArray = blockObject.optJSONArray("ranges");
+ if (rangesArray != null) {
+ for (int i = 0; i < rangesArray.length(); i++) {
+ JSONObject rangeObject = rangesArray.optJSONObject(i);
+ if (rangeObject == null) {
+ continue;
+ }
+
+ NoteBlockClickableSpan clickableSpan = new NoteBlockClickableSpan(WordPress.getContext(), rangeObject,
+ shouldLink, isFooter) {
+ @Override
+ public void onClick(View widget) {
+ if (onNoteBlockTextClickListener != null) {
+ onNoteBlockTextClickListener.onNoteBlockTextClicked(this);
+ }
+ }
+ };
+
+ int[] indices = clickableSpan.getIndices();
+ if (indices.length == 2 && indices[0] <= spannableStringBuilder.length() &&
+ indices[1] <= spannableStringBuilder.length()) {
+ spannableStringBuilder.setSpan(clickableSpan, indices[0], indices[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ // Add additional styling if the range wants it
+ if (clickableSpan.getSpanStyle() != Typeface.NORMAL) {
+ StyleSpan styleSpan = new StyleSpan(clickableSpan.getSpanStyle());
+ spannableStringBuilder.setSpan(styleSpan, indices[0], indices[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+ }
+ }
+
+ return spannableStringBuilder;
+ }
+
+ public static int[] getIndicesForRange(JSONObject rangeObject) {
+ int[] indices = new int[]{0,0};
+ if (rangeObject == null) {
+ return indices;
+ }
+
+ JSONArray indicesArray = rangeObject.optJSONArray("indices");
+ if (indicesArray != null && indicesArray.length() >= 2) {
+ indices[0] = indicesArray.optInt(0);
+ indices[1] = indicesArray.optInt(1);
+ }
+
+ return indices;
+ }
+
+ /**
+ * Adds ImageSpans to the passed SpannableStringBuilder
+ */
+ private static void addImageSpansForBlockMedia(TextView textView, JSONObject subject, SpannableStringBuilder spannableStringBuilder) {
+ if (textView == null || subject == null || spannableStringBuilder == null) return;
+
+ Context context = textView.getContext();
+ JSONArray mediaArray = subject.optJSONArray("media");
+ if (context == null || mediaArray == null) {
+ return;
+ }
+
+ Drawable loading = context.getResources().getDrawable(
+ org.wordpress.android.editor.R.drawable.legacy_dashicon_format_image_big_grey);
+ Drawable failed = context.getResources().getDrawable(R.drawable.noticon_warning_big_grey);
+ // Note: notifications_max_image_size seems to be the max size an ImageSpan can handle,
+ // otherwise it would load blank white
+ WPImageGetter imageGetter = new WPImageGetter(
+ textView,
+ context.getResources().getDimensionPixelSize(R.dimen.notifications_max_image_size),
+ WordPress.imageLoader,
+ loading,
+ failed
+ );
+
+ int indexAdjustment = 0;
+ String imagePlaceholder;
+ for (int i = 0; i < mediaArray.length(); i++) {
+ JSONObject mediaObject = mediaArray.optJSONObject(i);
+ if (mediaObject == null) {
+ continue;
+ }
+
+ final Drawable remoteDrawable = imageGetter.getDrawable(mediaObject.optString("url", ""));
+ ImageSpan noteImageSpan = new ImageSpan(remoteDrawable, mediaObject.optString("url", ""));
+ int startIndex = JSONUtils.queryJSON(mediaObject, "indices[0]", -1);
+ int endIndex = JSONUtils.queryJSON(mediaObject, "indices[1]", -1);
+ if (startIndex >= 0) {
+ startIndex += indexAdjustment;
+ endIndex += indexAdjustment;
+
+ if (startIndex > spannableStringBuilder.length()) {
+ continue;
+ }
+
+ // If we have a range, it means there is alt text that should be removed
+ if (endIndex > startIndex && endIndex <= spannableStringBuilder.length()) {
+ spannableStringBuilder.replace(startIndex, endIndex, "");
+ }
+
+ // We need an empty space to insert the ImageSpan into
+ imagePlaceholder = " ";
+
+ // Move the image to a new line if needed
+ int previousCharIndex = (startIndex > 0) ? startIndex - 1 : 0;
+ if (!spannableHasCharacterAtIndex(spannableStringBuilder, '\n', previousCharIndex)
+ || spannableStringBuilder.getSpans(startIndex, startIndex, ImageSpan.class).length > 0) {
+ imagePlaceholder = "\n ";
+ }
+
+ int spanIndex = startIndex + imagePlaceholder.length() - 1;
+
+ // Add a newline after the image if needed
+ if (!spannableHasCharacterAtIndex(spannableStringBuilder, '\n', startIndex)
+ && !spannableHasCharacterAtIndex(spannableStringBuilder, '\r', startIndex)) {
+ imagePlaceholder += "\n";
+ }
+
+ spannableStringBuilder.insert(startIndex, imagePlaceholder);
+
+ // Add the image span
+ spannableStringBuilder.setSpan(
+ noteImageSpan,
+ spanIndex,
+ spanIndex + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ // Add an AlignmentSpan to center the image
+ spannableStringBuilder.setSpan(
+ new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
+ spanIndex,
+ spanIndex + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ indexAdjustment += imagePlaceholder.length();
+ }
+ }
+ }
+
+ public static void handleNoteBlockSpanClick(NotificationsDetailActivity activity, NoteBlockClickableSpan clickedSpan) {
+ switch (clickedSpan.getRangeType()) {
+ case SITE:
+ // Show blog preview
+ activity.showBlogPreviewActivity(clickedSpan.getId());
+ break;
+ case USER:
+ // Show blog preview
+ activity.showBlogPreviewActivity(clickedSpan.getSiteId());
+ break;
+ case POST:
+ // Show post detail
+ activity.showPostActivity(clickedSpan.getSiteId(), clickedSpan.getId());
+ break;
+ case COMMENT:
+ // Load the comment in the reader list if it exists, otherwise show a webview
+ if (ReaderUtils.postAndCommentExists(clickedSpan.getSiteId(), clickedSpan.getPostId(), clickedSpan.getId())) {
+ activity.showReaderCommentsList(clickedSpan.getSiteId(), clickedSpan.getPostId(), clickedSpan.getId());
+ } else {
+ activity.showWebViewActivityForUrl(clickedSpan.getUrl());
+ }
+ break;
+ case STAT:
+ case FOLLOW:
+ // We can open native stats if the site is a wpcom or Jetpack + stored in the app locally.
+ // Note that for Jetpack sites we need the options already synced. That happens when the user
+ // selects the site in the sites picker. So adding it to the app doesn't always populate options.
+
+ // Do not load Jetpack shadow sites here. They've empty options and Stats can't be loaded for them.
+ Blog blog = WordPress.wpDB.getBlogForDotComBlogId(
+ String.valueOf(clickedSpan.getSiteId())
+ );
+ // Make sure blog is not null, and it's either JP or dotcom. Better safe than sorry.
+ if (blog == null || blog.getLocalTableBlogId() <= 0 || (!blog.isDotcomFlag() && !blog.isJetpackPowered())) {
+ activity.showWebViewActivityForUrl(clickedSpan.getUrl());
+ break;
+ }
+ activity.showStatsActivityForSite(blog.getLocalTableBlogId(), clickedSpan.getRangeType());
+ break;
+ case LIKE:
+ if (ReaderPostTable.postExists(clickedSpan.getSiteId(), clickedSpan.getId())) {
+ activity.showReaderPostLikeUsers(clickedSpan.getSiteId(), clickedSpan.getId());
+ } else {
+ activity.showPostActivity(clickedSpan.getSiteId(), clickedSpan.getId());
+ }
+ break;
+ default:
+ // We don't know what type of id this is, let's see if it has a URL and push a webview
+ if (!TextUtils.isEmpty(clickedSpan.getUrl())) {
+ activity.showWebViewActivityForUrl(clickedSpan.getUrl());
+ }
+ }
+ }
+
+ public static boolean spannableHasCharacterAtIndex(Spannable spannable, char character, int index) {
+ return spannable != null && index < spannable.length() && spannable.charAt(index) == character;
+ }
+
+
+ public static void showPushAuthAlert(Context context, final String token, String title, String message) {
+ if (context == null ||
+ TextUtils.isEmpty(token) ||
+ TextUtils.isEmpty(title) ||
+ TextUtils.isEmpty(message)) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(title).setMessage(message);
+
+ builder.setPositiveButton(R.string.mnu_comment_approve, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // ping the push auth endpoint with the token, wp.com will take care of the rest!
+ Map<String, String> tokenMap = new HashMap<>();
+ tokenMap.put("action", "authorize_login");
+ tokenMap.put("push_token", token);
+ WordPress.getRestClientUtilsV1_1().post(PUSH_AUTH_ENDPOINT, tokenMap, null, null,
+ new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_FAILED);
+ }
+ });
+
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_APPROVED);
+ }
+ });
+
+ builder.setNegativeButton(R.string.ignore, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.PUSH_AUTHENTICATION_IGNORED);
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private static void showUndoBarForNote(final Note note,
+ final CommentStatus status,
+ final View parentView) {
+ Resources resources = parentView.getContext().getResources();
+ String message = (status == CommentStatus.TRASH ? resources.getString(R.string.comment_trashed) : resources.getString(R.string.comment_spammed));
+ View.OnClickListener undoListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSnackbarDidUndo = true;
+ EventBus.getDefault().postSticky(new NoteVisibilityChanged(note.getId(), false));
+ }
+ };
+
+ mSnackbarDidUndo = false;
+ Snackbar snackbar = Snackbar.make(parentView, message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, undoListener);
+
+ // Deleted notifications in Simperium never come back, so we won't
+ // make the request until the undo bar fades away
+ snackbar.setCallback(new Snackbar.Callback() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+ if (mSnackbarDidUndo) {
+ return;
+ }
+ CommentActions.moderateCommentForNote(note, status,
+ new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ if (!result.isSuccess()) {
+ EventBus.getDefault().postSticky(new NoteVisibilityChanged(note.getId(), false));
+ EventBus.getDefault().postSticky(new NoteModerationFailed());
+ }
+ }
+ });
+ }
+ });
+
+ snackbar.show();
+ }
+
+ /**
+ * Moderate a comment from a WPCOM notification.
+ * Broadcast EventBus events on update/success/failure and show an undo bar if new status is Trash or Spam
+ */
+ public static void moderateCommentForNote(final Note note, final CommentStatus newStatus, final View parentView) {
+ if (newStatus == CommentStatus.APPROVED || newStatus == CommentStatus.UNAPPROVED) {
+ note.setLocalStatus(CommentStatus.toRESTString(newStatus));
+ note.save();
+ EventBus.getDefault().postSticky(new NoteModerationStatusChanged(note.getId(), true));
+ CommentActions.moderateCommentForNote(note, newStatus,
+ new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ EventBus.getDefault().postSticky(new NoteModerationStatusChanged(note.getId(), false));
+ if (!result.isSuccess()) {
+ note.setLocalStatus(null);
+ note.save();
+ EventBus.getDefault().postSticky(new NoteModerationFailed());
+ }
+ }
+ });
+ } else if (newStatus == CommentStatus.TRASH || newStatus == CommentStatus.SPAM) {
+ // Post as sticky, so that NotificationsListFragment can pick it up after it's created
+ EventBus.getDefault().postSticky(new NoteVisibilityChanged(note.getId(), true));
+ // Show undo bar for trash or spam actions
+ showUndoBarForNote(note, newStatus, parentView);
+ }
+ }
+
+ // Checks if global notifications toggle is enabled in the Android app settings
+ // See: https://code.google.com/p/android/issues/detail?id=38482#c15
+ @SuppressWarnings("unchecked")
+ @TargetApi(19)
+ public static boolean isNotificationsEnabled(Context context) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ ApplicationInfo appInfo = context.getApplicationInfo();
+ String pkg = context.getApplicationContext().getPackageName();
+ int uid = appInfo.uid;
+
+ Class appOpsClass;
+ try {
+ appOpsClass = Class.forName(AppOpsManager.class.getName());
+
+ Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);
+
+ Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
+ int value = (int) opPostNotificationValue.get(Integer.class);
+
+ return ((int) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);
+ } catch (ClassNotFoundException | NoSuchFieldException | NoSuchMethodException |
+ IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Default to assuming notifications are enabled
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/SimperiumUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/SimperiumUtils.java
new file mode 100644
index 000000000..0fea3a6a5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/SimperiumUtils.java
@@ -0,0 +1,184 @@
+/**
+ * Simperium integration with WordPress.com
+ * Currently used with Notifications
+ */
+package org.wordpress.android.ui.notifications.utils;
+
+import android.content.Context;
+
+import com.simperium.Simperium;
+import com.simperium.client.Bucket;
+import com.simperium.client.BucketNameInvalid;
+import com.simperium.client.BucketObject;
+import com.simperium.client.BucketObjectMissingException;
+import com.simperium.client.Query;
+import com.simperium.client.User;
+
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+
+import de.greenrobot.event.EventBus;
+
+public class SimperiumUtils {
+ private static final String NOTE_TIMESTAMP = "timestamp";
+ private static final String META_BUCKET_NAME = "meta";
+ private static final String META_LAST_SEEN = "last_seen";
+
+ private static Simperium mSimperium;
+ private static Bucket<Note> mNotesBucket;
+ private static Bucket<BucketObject> mMetaBucket;
+
+ public static Bucket<Note> getNotesBucket() {
+ return mNotesBucket;
+ }
+
+ public static Bucket<BucketObject> getMetaBucket() {
+ return mMetaBucket;
+ }
+
+ public static synchronized Simperium configureSimperium(final Context context, String token) {
+ // Create a new instance of Simperium if it doesn't exist yet.
+ // In any case, authorize the user.
+ if (mSimperium == null) {
+ mSimperium = Simperium.newClient(BuildConfig.SIMPERIUM_APP_NAME,
+ BuildConfig.SIMPERIUM_APP_SECRET, context);
+
+ try {
+ mNotesBucket = mSimperium.bucket(new Note.Schema());
+ mMetaBucket = mSimperium.bucket(META_BUCKET_NAME);
+
+ mSimperium.setUserStatusChangeListener(new User.StatusChangeListener() {
+
+ @Override
+ public void onUserStatusChange(User.Status status) {
+ switch (status) {
+ case AUTHORIZED:
+ startBuckets();
+ break;
+ case NOT_AUTHORIZED:
+ mNotesBucket.stop();
+ mMetaBucket.stop();
+ EventBus.getDefault().post(new NotificationEvents.SimperiumNotAuthorized());
+ break;
+ default:
+ AppLog.d(AppLog.T.SIMPERIUM, "User not authorized yet");
+ break;
+ }
+ }
+
+ });
+
+ } catch (BucketNameInvalid e) {
+ AppLog.e(AppLog.T.SIMPERIUM, e.getMessage());
+ }
+ }
+
+ authorizeUser(mSimperium, token);
+
+ return mSimperium;
+ }
+
+ private static void authorizeUser(Simperium simperium, String token) {
+ User user = simperium.getUser();
+
+ String tokenFormat = "WPCC/%s/%s";
+ String wpccToken = String.format(tokenFormat, BuildConfig.SIMPERIUM_APP_SECRET, StringUtils.notNullStr(token));
+
+ user.setAccessToken(wpccToken);
+
+ // we'll assume the user is AUTHORIZED, and catch NOT_AUTHORIZED if something goes wrong.
+ user.setStatus(User.Status.AUTHORIZED);
+ }
+
+ public static boolean isUserAuthorized() {
+ return mSimperium != null &&
+ mSimperium.getUser() != null &&
+ mSimperium.getUser().getStatus() == User.Status.AUTHORIZED;
+ }
+
+ public static boolean isUserNotAuthorized() {
+ return mSimperium != null &&
+ mSimperium.getUser() != null &&
+ mSimperium.getUser().getStatus() == User.Status.NOT_AUTHORIZED;
+ }
+
+ public static void startBuckets() {
+ if (mNotesBucket != null) {
+ mNotesBucket.start();
+ }
+
+ if (mMetaBucket != null) {
+ mMetaBucket.start();
+ }
+ }
+
+ public static void resetBucketsAndDeauthorize() {
+ if (mNotesBucket != null) {
+ mNotesBucket.reset();
+ mNotesBucket = null;
+ }
+ if (mMetaBucket != null) {
+ mMetaBucket.reset();
+ mMetaBucket = null;
+ }
+
+ // Reset user status
+ if (mSimperium != null) {
+ mSimperium.getUser().setStatus(User.Status.UNKNOWN);
+ mSimperium = null;
+ }
+ }
+
+ // Returns true if we have unread notes with a timestamp greater than last_seen timestamp in the meta bucket
+ public static boolean hasUnreadNotes() {
+ if (getNotesBucket() == null || getMetaBucket() == null) return false;
+
+ try {
+ BucketObject meta = getMetaBucket().get(META_BUCKET_NAME);
+ if (meta != null && meta.getProperty(META_LAST_SEEN) instanceof Integer) {
+ Integer lastSeenTimestamp = (Integer)meta.getProperty(META_LAST_SEEN);
+
+ Query<Note> query = new Query<>(getNotesBucket());
+ query.where(Note.Schema.UNREAD_INDEX, Query.ComparisonType.EQUAL_TO, true);
+ query.where(Note.Schema.TIMESTAMP_INDEX, Query.ComparisonType.GREATER_THAN, lastSeenTimestamp);
+ return query.execute().getCount() > 0;
+ }
+ } catch (BucketObjectMissingException e) {
+ return false;
+ }
+
+ return false;
+ }
+
+ // Updates the 'last_seen' field in the meta bucket with the latest note's timestamp
+ public static boolean updateLastSeenTime() {
+ if (getNotesBucket() == null || getMetaBucket() == null) return false;
+
+ Query<Note> query = new Query<>(getNotesBucket());
+ query.order(NOTE_TIMESTAMP, Query.SortType.DESCENDING);
+ query.limit(1);
+
+ Bucket.ObjectCursor<Note> cursor = query.execute();
+ if (cursor.moveToFirst()) {
+ long latestNoteTimestamp = cursor.getObject().getTimestamp();
+ try {
+ BucketObject meta = getMetaBucket().get(META_BUCKET_NAME);
+ if (meta.getProperty(META_LAST_SEEN) instanceof Integer) {
+ int lastSeen = (int)meta.getProperty(META_LAST_SEEN);
+ if (lastSeen != latestNoteTimestamp) {
+ meta.setProperty(META_LAST_SEEN, latestNoteTimestamp);
+ meta.save();
+ return true;
+ }
+ }
+ } catch (BucketObjectMissingException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Meta bucket not found.");
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java
new file mode 100644
index 000000000..32e8d2341
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteFragment.java
@@ -0,0 +1,667 @@
+package org.wordpress.android.ui.people;
+
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Role;
+import org.wordpress.android.ui.people.utils.PeopleUtils;
+import org.wordpress.android.ui.people.utils.PeopleUtils.ValidateUsernameCallback.ValidationResult;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.MultiUsernameEditText;
+import org.wordpress.passcodelock.AppLockManager;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PeopleInviteFragment extends Fragment implements
+ RoleSelectDialogFragment.OnRoleSelectListener,
+ PeopleManagementActivity.InvitationSender {
+
+ private static final String FLAG_SUCCESS = "SUCCESS";
+
+ private static final String ARG_BLOGID = "ARG_BLOGID";
+
+ private static final int MAX_NUMBER_OF_INVITEES = 10;
+ private static final String[] USERNAME_DELIMITERS = {" ", ","};
+ private final Map<String, ViewGroup> mUsernameButtons = new LinkedHashMap<>();
+ private final HashMap<String, String> mUsernameResults = new HashMap<>();
+ private final Map<String, View> mUsernameErrorViews = new Hashtable<>();
+ private ViewGroup mUsernamesContainer;
+ private MultiUsernameEditText mUsernameEditText;
+ private TextView mRoleTextView;
+ private EditText mCustomMessageEditText;
+
+ private Role mRole;
+ private String mCustomMessage = "";
+ private boolean mInviteOperationInProgress = false;
+
+ public static PeopleInviteFragment newInstance(String dotComBlogId) {
+ PeopleInviteFragment peopleInviteFragment = new PeopleInviteFragment();
+
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_BLOGID, dotComBlogId);
+
+ peopleInviteFragment.setArguments(bundle);
+ return peopleInviteFragment;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.people_invite, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ menu.getItem(0).setEnabled(!mInviteOperationInProgress); // here pass the index of send menu item
+ super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // retain this fragment across configuration changes
+ // WARNING: use setRetainInstance wisely. In this case we need this to be able to get the
+ // results of network connections in the same fragment if going through a configuration change
+ // (for example, device rotation occurs). Given the simplicity of this particular use case
+ // (the fragment state keeps only a couple of EditText components and the SAVE button, it is
+ // OK to use it here.
+ setRetainInstance(true);
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
+ return inflater.inflate(R.layout.people_invite_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mUsernamesContainer = (ViewGroup) view.findViewById(R.id.usernames);
+ mUsernamesContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ EditTextUtils.showSoftInput(mUsernameEditText);
+ }
+ });
+
+ Role role = mRole;
+ if (role == null) {
+ role = getDefaultRole();
+ }
+
+ mUsernameEditText = (MultiUsernameEditText) view.findViewById(R.id.invite_usernames);
+
+ //handle key preses from hardware keyboard
+ mUsernameEditText.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View view, int i, KeyEvent keyEvent) {
+ return keyEvent.getKeyCode() == KeyEvent.KEYCODE_DEL
+ && keyEvent.getAction() == KeyEvent.ACTION_DOWN
+ && removeLastEnteredUsername();
+ }
+ });
+
+ mUsernameEditText.setOnBackspacePressedListener(new MultiUsernameEditText.OnBackspacePressedListener() {
+ @Override
+ public boolean onBackspacePressed() {
+ return removeLastEnteredUsername();
+ }
+ });
+
+ mUsernameEditText.addTextChangedListener(new TextWatcher() {
+ private boolean shouldIgnoreChanges = false;
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (shouldIgnoreChanges) { //used to avoid double call after calling setText from this method
+ return;
+ }
+
+ shouldIgnoreChanges = true;
+ if (mUsernameButtons.size() >= MAX_NUMBER_OF_INVITEES && !TextUtils.isEmpty(s)) {
+ resetEditTextContent(mUsernameEditText);
+ } else if (endsWithDelimiter(mUsernameEditText.getText().toString())) {
+ addUsername(mUsernameEditText, null);
+ }
+ shouldIgnoreChanges = false;
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ mUsernameEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || (event != null && event.getKeyCode() == KeyEvent
+ .KEYCODE_ENTER)) {
+ addUsername(mUsernameEditText, null);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+
+ mUsernameEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus && mUsernameEditText.getText().toString().length() > 0) {
+ addUsername(mUsernameEditText, null);
+ }
+ }
+ });
+
+
+ if (mUsernameButtons.size() > 0) {
+ ArrayList<String> usernames = new ArrayList<>(mUsernameButtons.keySet());
+ populateUsernameButtons(usernames);
+ }
+
+
+ view.findViewById(R.id.role_container).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ RoleSelectDialogFragment.show(PeopleInviteFragment.this, 0, isPrivateSite());
+ }
+ });
+
+ mRoleTextView = (TextView) view.findViewById(R.id.role);
+ setRole(role);
+ ImageView imgRoleInfo = (ImageView) view.findViewById(R.id.imgRoleInfo);
+ imgRoleInfo.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Uri uri = Uri.parse(getString(R.string.role_info_url));
+ AppLockManager.getInstance().setExtendedTimeout();
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ });
+
+ final int MAX_CHARS = getResources().getInteger(R.integer.invite_message_char_limit);
+ final TextView remainingCharsTextView = (TextView) view.findViewById(R.id.message_remaining);
+
+ mCustomMessageEditText = (EditText) view.findViewById(R.id.message);
+ mCustomMessageEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ mCustomMessage = mCustomMessageEditText.getText().toString();
+ updateRemainingCharsView(remainingCharsTextView, mCustomMessage, MAX_CHARS);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ updateRemainingCharsView(remainingCharsTextView, mCustomMessage, MAX_CHARS);
+ }
+
+ private boolean endsWithDelimiter(String string) {
+ if (TextUtils.isEmpty(string)) {
+ return false;
+ }
+
+ for (String usernameDelimiter : USERNAME_DELIMITERS) {
+ if (string.endsWith(usernameDelimiter)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private String removeDelimiterFromUsername(String username) {
+ if (TextUtils.isEmpty(username)) {
+ return username;
+ }
+
+ String trimmedUsername = username.trim();
+
+ for (String usernameDelimiter : USERNAME_DELIMITERS) {
+ if (trimmedUsername.endsWith(usernameDelimiter)) {
+ return trimmedUsername.substring(0, trimmedUsername.length() - usernameDelimiter.length());
+ }
+ }
+
+ return trimmedUsername;
+ }
+
+ private void resetEditTextContent(EditText editText) {
+ if (editText != null) {
+ editText.setText("");
+ }
+ }
+
+ private Role getDefaultRole() {
+ Role[] inviteRoles = Role.inviteRoles(isPrivateSite());
+ return inviteRoles[0];
+ }
+
+ private void updateRemainingCharsView(TextView remainingCharsTextView, String currentString, int limit) {
+ remainingCharsTextView.setText(StringUtils.getQuantityString(getActivity(),
+ R.string.invite_message_remaining_zero,
+ R.string.invite_message_remaining_one,
+ R.string.invite_message_remaining_other, limit - (currentString == null ? 0 : currentString.length())));
+ }
+
+ private void populateUsernameButtons(Collection<String> usernames) {
+ if (usernames != null && usernames.size() > 0) {
+
+ for (String username : usernames) {
+ mUsernameButtons.put(username, buttonizeUsername(username));
+ }
+
+ validateAndStyleUsername(usernames, null);
+ }
+ }
+
+ private ViewGroup buttonizeUsername(final String username) {
+ if (!isAdded()) {
+ return null;
+ }
+
+ final ViewGroup usernameButton = (ViewGroup) LayoutInflater.from(getActivity()).inflate(R.layout
+ .invite_username_button, null);
+ final TextView usernameTextView = (TextView) usernameButton.findViewById(R.id.username);
+ usernameTextView.setText(username);
+
+ mUsernamesContainer.addView(usernameButton, mUsernamesContainer.getChildCount() - 1);
+
+ final ImageButton delete = (ImageButton) usernameButton.findViewById(R.id.username_delete);
+ delete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ removeUsername(username);
+ }
+ });
+
+ return usernameButton;
+ }
+
+ private void addUsername(EditText editText, ValidationEndListener validationEndListener) {
+ String username = removeDelimiterFromUsername(editText.getText().toString());
+ resetEditTextContent(editText);
+
+ if (username.isEmpty() || mUsernameButtons.keySet().contains(username)) {
+ if (validationEndListener != null) {
+ validationEndListener.onValidationEnd();
+ }
+ return;
+ }
+
+ final ViewGroup usernameButton = buttonizeUsername(username);
+
+ mUsernameButtons.put(username, usernameButton);
+
+ validateAndStyleUsername(Collections.singletonList(username), validationEndListener);
+ }
+
+ private void removeUsername(String username) {
+ final ViewGroup usernamesView = (ViewGroup) getView().findViewById(R.id.usernames);
+
+ ViewGroup removedButton = mUsernameButtons.remove(username);
+ mUsernameResults.remove(username);
+ usernamesView.removeView(removedButton);
+
+ updateUsernameError(username, null);
+ }
+
+ private boolean isUserInInvitees(String username) {
+ return mUsernameButtons.get(username) != null;
+ }
+
+ /**
+ * Deletes the last entered username.
+ *
+ * @return true if the username was deleted
+ */
+ private boolean removeLastEnteredUsername() {
+ if (!TextUtils.isEmpty(mUsernameEditText.getText())) {
+ return false;
+ }
+
+ //try and remove the last entered username
+ List<String> list = new ArrayList<>(mUsernameButtons.keySet());
+ if (!list.isEmpty()) {
+ String username = list.get(list.size() - 1);
+ removeUsername(username);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onRoleSelected(Role newRole) {
+ setRole(newRole);
+
+ if (!mUsernameButtons.keySet().isEmpty()) {
+ // clear the username results list and let the 'validate' routine do the updates
+ mUsernameResults.clear();
+
+ validateAndStyleUsername(mUsernameButtons.keySet(), null);
+ }
+ }
+
+ private void setRole(Role newRole) {
+ mRole = newRole;
+ mRoleTextView.setText(newRole.toDisplayString());
+ }
+
+ private void validateAndStyleUsername(Collection<String> usernames, final ValidationEndListener validationEndListener) {
+ List<String> usernamesToCheck = new ArrayList<>();
+
+ for (String username : usernames) {
+ if (mUsernameResults.containsKey(username)) {
+ String resultMessage = mUsernameResults.get(username);
+ styleButton(username, resultMessage);
+ updateUsernameError(username, resultMessage);
+ } else {
+ styleButton(username, null);
+ updateUsernameError(username, null);
+
+ usernamesToCheck.add(username);
+ }
+ }
+
+ if (usernamesToCheck.size() > 0) {
+
+ String dotComBlogId = getArguments().getString(ARG_BLOGID);
+ PeopleUtils.validateUsernames(usernamesToCheck, mRole, dotComBlogId, new PeopleUtils.ValidateUsernameCallback() {
+ @Override
+ public void onUsernameValidation(String username, ValidationResult validationResult) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!isUserInInvitees(username)) {
+ //user is removed from invitees before validation
+ return;
+ }
+
+ final String usernameResultString = getValidationErrorString(username, validationResult);
+ mUsernameResults.put(username, usernameResultString);
+
+ styleButton(username, usernameResultString);
+ updateUsernameError(username, usernameResultString);
+ }
+
+ @Override
+ public void onValidationFinished() {
+ if (validationEndListener != null) {
+ validationEndListener.onValidationEnd();
+ }
+ }
+
+ @Override
+ public void onError() {
+ // properly style the button
+ }
+ });
+ } else {
+ if (validationEndListener != null) {
+ validationEndListener.onValidationEnd();
+ }
+ }
+ }
+
+ private void styleButton(String username, @Nullable String validationResultMessage) {
+ if (!isAdded()) {
+ return;
+ }
+
+ TextView textView = (TextView) mUsernameButtons.get(username).findViewById(R.id.username);
+ textView.setTextColor(ContextCompat.getColor(getActivity(),
+ validationResultMessage == null ? R.color.grey_dark :
+ (validationResultMessage.equals(FLAG_SUCCESS) ? R.color.blue_wordpress : R.color.alert_red)));
+ }
+
+ private
+ @Nullable
+ String getValidationErrorString(String username, ValidationResult validationResult) {
+ switch (validationResult) {
+ case USER_NOT_FOUND:
+ return getString(R.string.invite_username_not_found, username);
+ case ALREADY_MEMBER:
+ return getString(R.string.invite_already_a_member, username);
+ case ALREADY_FOLLOWING:
+ return getString(R.string.invite_already_following, username);
+ case BLOCKED_INVITES:
+ return getString(R.string.invite_user_blocked_invites, username);
+ case INVALID_EMAIL:
+ return getString(R.string.invite_invalid_email, username);
+ case USER_FOUND:
+ return FLAG_SUCCESS;
+ }
+
+ return null;
+ }
+
+ private void updateUsernameError(String username, @Nullable String usernameResult) {
+ if (!isAdded()) {
+ return;
+ }
+
+ TextView usernameErrorTextView;
+ if (mUsernameErrorViews.containsKey(username)) {
+ usernameErrorTextView = (TextView) mUsernameErrorViews.get(username);
+
+ if (usernameResult == null || usernameResult.equals(FLAG_SUCCESS)) {
+ // no error so we need to remove the existing error view
+ ((ViewGroup) usernameErrorTextView.getParent()).removeView(usernameErrorTextView);
+ mUsernameErrorViews.remove(username);
+ return;
+ }
+ } else {
+ if (usernameResult == null || usernameResult.equals(FLAG_SUCCESS)) {
+ // no error so no need to create a new error view
+ return;
+ }
+
+ usernameErrorTextView = (TextView) LayoutInflater.from(getActivity())
+ .inflate(R.layout.people_invite_error_view, null);
+
+ final ViewGroup usernameErrorsContainer = (ViewGroup) getView()
+ .findViewById(R.id.username_errors_container);
+ usernameErrorsContainer.addView(usernameErrorTextView);
+
+ mUsernameErrorViews.put(username, usernameErrorTextView);
+ }
+ usernameErrorTextView.setText(usernameResult);
+ }
+
+ private void clearUsernames(Collection<String> usernames) {
+ for (String username : usernames) {
+ removeUsername(username);
+ }
+
+ if (mUsernameButtons.size() == 0) {
+ setRole(getDefaultRole());
+ resetEditTextContent(mCustomMessageEditText);
+ }
+ }
+
+ @Override
+ public void send() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ enableSendButton(true);
+ return;
+ }
+
+ enableSendButton(false);
+
+ if (mUsernameEditText.getText().toString().length() > 0) {
+ addUsername(mUsernameEditText, new ValidationEndListener() {
+ @Override
+ public void onValidationEnd() {
+ if (!checkAndSend()) {
+ //re-enable SEND button if validation failed
+ enableSendButton(true);
+ }
+ }
+ });
+ } else {
+ if (!checkAndSend()) {
+ //re-enable SEND button if validation failed
+ enableSendButton(true);
+ }
+ }
+ }
+
+ /*
+ * returns true if send is attempted, false if validation failed
+ * */
+ private boolean checkAndSend() {
+ if (!isAdded()) {
+ return false;
+ }
+
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ return false;
+ }
+
+ if (mUsernameButtons.size() == 0) {
+ ToastUtils.showToast(getActivity(), R.string.invite_error_no_usernames);
+ return false;
+ }
+
+ int invalidCount = 0;
+ for (String usernameResultString : mUsernameResults.values()) {
+ if (!usernameResultString.equals(FLAG_SUCCESS)) {
+ invalidCount++;
+ }
+ }
+
+ if (invalidCount > 0) {
+ ToastUtils.showToast(getActivity(), StringUtils.getQuantityString(getActivity(), 0,
+ R.string.invite_error_invalid_usernames_one,
+ R.string.invite_error_invalid_usernames_multiple, invalidCount));
+ return false;
+ }
+
+ //set the "SEND" option disabled
+ enableSendButton(false);
+
+ String dotComBlogId = getArguments().getString(ARG_BLOGID);
+ PeopleUtils.sendInvitations(new ArrayList<>(mUsernameButtons.keySet()), mRole, mCustomMessage, dotComBlogId,
+ new PeopleUtils.InvitationsSendCallback() {
+ @Override
+ public void onSent(List<String> succeededUsernames, Map<String, String> failedUsernameErrors) {
+ if (!isAdded()) {
+ return;
+ }
+
+ clearUsernames(succeededUsernames);
+
+ if (failedUsernameErrors.size() != 0) {
+ clearUsernames(failedUsernameErrors.keySet());
+
+ for (Map.Entry<String, String> error : failedUsernameErrors.entrySet()) {
+ final String username = error.getKey();
+ final String errorMessage = error.getValue();
+ mUsernameResults.put(username, getString(R.string.invite_error_for_username,
+ username, errorMessage));
+ }
+
+ populateUsernameButtons(failedUsernameErrors.keySet());
+
+ ToastUtils.showToast(getActivity(), succeededUsernames.isEmpty()
+ ? R.string.invite_error_sending : R.string.invite_error_some_failed);
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.invite_sent, ToastUtils.Duration.LONG);
+ }
+
+ //set the "SEND" option enabled again
+ enableSendButton(true);
+ }
+
+ @Override
+ public void onError() {
+ if (!isAdded()) {
+ return;
+ }
+
+ ToastUtils.showToast(getActivity(), R.string.invite_error_sending);
+
+ //set the "SEND" option enabled again
+ enableSendButton(true);
+
+ }
+ });
+
+ return true;
+ }
+
+ private void enableSendButton(boolean enable) {
+ mInviteOperationInProgress = !enable;
+ if (getActivity() != null) {
+ getActivity().invalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ //we need to remove focus listener when view is destroyed (ex. orientation change) to prevent mUsernameEditText
+ //content from being converted to username
+ if (mUsernameEditText != null) {
+ mUsernameEditText.setOnFocusChangeListener(null);
+ }
+ }
+
+ private boolean isPrivateSite() {
+ String dotComBlogId = getArguments().getString(ARG_BLOGID);
+ Blog blog = WordPress.wpDB.getBlogForDotComBlogId(dotComBlogId);
+ return blog != null && blog.isPrivate();
+ }
+
+ public interface ValidationEndListener {
+ void onValidationEnd();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java
new file mode 100644
index 000000000..ca878b3cf
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleListFragment.java
@@ -0,0 +1,432 @@
+package org.wordpress.android.ui.people;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.PeopleTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.FilterCriteria;
+import org.wordpress.android.models.PeopleListFilter;
+import org.wordpress.android.models.Person;
+import org.wordpress.android.ui.EmptyViewMessageType;
+import org.wordpress.android.ui.FilteredRecyclerView;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class PeopleListFragment extends Fragment {
+ private static final String ARG_LOCAL_TABLE_BLOG_ID = "local_table_blog_id";
+
+ private int mLocalTableBlogID;
+ private OnPersonSelectedListener mOnPersonSelectedListener;
+ private OnFetchPeopleListener mOnFetchPeopleListener;
+
+ private FilteredRecyclerView mFilteredRecyclerView;
+ private PeopleListFilter mPeopleListFilter;
+
+ public static PeopleListFragment newInstance(int localTableBlogID) {
+ PeopleListFragment peopleListFragment = new PeopleListFragment();
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARG_LOCAL_TABLE_BLOG_ID, localTableBlogID);
+ peopleListFragment.setArguments(bundle);
+ return peopleListFragment;
+ }
+
+ public void setOnPersonSelectedListener(OnPersonSelectedListener listener) {
+ mOnPersonSelectedListener = listener;
+ }
+
+ public void setOnFetchPeopleListener(OnFetchPeopleListener listener) {
+ mOnFetchPeopleListener = listener;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mOnPersonSelectedListener = null;
+ mOnFetchPeopleListener = null;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.people_list, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
+
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.people_list_fragment, container, false);
+
+ mLocalTableBlogID = getArguments().getInt(ARG_LOCAL_TABLE_BLOG_ID);
+ final Blog blog = WordPress.getBlog(mLocalTableBlogID);
+ final boolean isPrivate = blog != null && blog.isPrivate();
+
+ mFilteredRecyclerView = (FilteredRecyclerView) rootView.findViewById(R.id.filtered_recycler_view);
+ mFilteredRecyclerView.addItemDecoration(new PeopleItemDecoration(getActivity(), R.drawable.people_list_divider));
+ mFilteredRecyclerView.setLogT(AppLog.T.PEOPLE);
+ mFilteredRecyclerView.setSwipeToRefreshEnabled(false);
+
+ // the following will change the look and feel of the toolbar to match the current design
+ mFilteredRecyclerView.setToolbarBackgroundColor(ContextCompat.getColor(getActivity(), R.color.blue_medium));
+ mFilteredRecyclerView.setToolbarSpinnerTextColor(ContextCompat.getColor(getActivity(), R.color.white));
+ mFilteredRecyclerView.setToolbarSpinnerDrawable(R.drawable.arrow);
+ mFilteredRecyclerView.setToolbarLeftAndRightPadding(
+ getResources().getDimensionPixelSize(R.dimen.margin_filter_spinner),
+ getResources().getDimensionPixelSize(R.dimen.margin_none));
+
+ mFilteredRecyclerView.setFilterListener(new FilteredRecyclerView.FilterListener() {
+ @Override
+ public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) {
+ ArrayList<FilterCriteria> list = new ArrayList<>();
+ Collections.addAll(list, PeopleListFilter.values());
+ // Only a private blog can have viewers
+ if (!isPrivate) {
+ list.remove(PeopleListFilter.VIEWERS);
+ }
+ return list;
+ }
+
+ @Override
+ public void onLoadFilterCriteriaOptionsAsync(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) {
+ // no-op
+ }
+
+ @Override
+ public FilterCriteria onRecallSelection() {
+ mPeopleListFilter = AppPrefs.getPeopleListFilter();
+
+ // if viewers is not available for this blog, set the filter to TEAM
+ if (mPeopleListFilter == PeopleListFilter.VIEWERS && !isPrivate) {
+ mPeopleListFilter = PeopleListFilter.TEAM;
+ AppPrefs.setPeopleListFilter(mPeopleListFilter);
+ }
+ return mPeopleListFilter;
+ }
+
+ @Override
+ public void onLoadData() {
+ updatePeople(false);
+ }
+
+ @Override
+ public void onFilterSelected(int position, FilterCriteria criteria) {
+ mPeopleListFilter = (PeopleListFilter) criteria;
+ AppPrefs.setPeopleListFilter(mPeopleListFilter);
+ }
+
+ @Override
+ public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) {
+ int stringId = 0;
+ switch (emptyViewMsgType) {
+ case LOADING:
+ stringId = R.string.people_fetching;
+ break;
+ case NETWORK_ERROR:
+ stringId = R.string.no_network_message;
+ break;
+ case NO_CONTENT:
+ switch (mPeopleListFilter) {
+ case TEAM:
+ stringId = R.string.people_empty_list_filtered_users;
+ break;
+ case FOLLOWERS:
+ stringId = R.string.people_empty_list_filtered_followers;
+ break;
+ case EMAIL_FOLLOWERS:
+ stringId = R.string.people_empty_list_filtered_email_followers;
+ break;
+ case VIEWERS:
+ stringId = R.string.people_empty_list_filtered_viewers;
+ break;
+ }
+ break;
+ case GENERIC_ERROR:
+ switch (mPeopleListFilter) {
+ case TEAM:
+ stringId = R.string.error_fetch_users_list;
+ break;
+ case FOLLOWERS:
+ stringId = R.string.error_fetch_followers_list;
+ break;
+ case EMAIL_FOLLOWERS:
+ stringId = R.string.error_fetch_email_followers_list;
+ break;
+ case VIEWERS:
+ stringId = R.string.error_fetch_viewers_list;
+ break;
+ }
+ break;
+ }
+ return getString(stringId);
+ }
+
+ @Override
+ public void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType) {
+
+ }
+ });
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ updatePeople(false);
+ }
+
+ private void updatePeople(boolean loadMore) {
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ mFilteredRecyclerView.setRefreshing(false);
+ return;
+ }
+
+ if (mOnFetchPeopleListener != null) {
+ if (loadMore) {
+ boolean isFetching = mOnFetchPeopleListener.onFetchMorePeople(mPeopleListFilter);
+ if (isFetching) {
+ mFilteredRecyclerView.showLoadingProgress();
+ }
+ } else {
+ boolean isFetching = mOnFetchPeopleListener.onFetchFirstPage(mPeopleListFilter);
+ if (isFetching) {
+ mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.LOADING);
+ } else {
+ mFilteredRecyclerView.hideEmptyView();
+ mFilteredRecyclerView.setRefreshing(false);
+ }
+ refreshPeopleList(isFetching);
+ }
+ }
+ }
+
+ public void refreshPeopleList(boolean isFetching) {
+ if (!isAdded()) return;
+
+ List<Person> peopleList;
+ switch (mPeopleListFilter) {
+ case TEAM:
+ peopleList = PeopleTable.getUsers(mLocalTableBlogID);
+ break;
+ case FOLLOWERS:
+ peopleList = PeopleTable.getFollowers(mLocalTableBlogID);
+ break;
+ case EMAIL_FOLLOWERS:
+ peopleList = PeopleTable.getEmailFollowers(mLocalTableBlogID);
+ break;
+ case VIEWERS:
+ peopleList = PeopleTable.getViewers(mLocalTableBlogID);
+ break;
+ default:
+ peopleList = new ArrayList<>();
+ break;
+ }
+ PeopleAdapter peopleAdapter = (PeopleAdapter) mFilteredRecyclerView.getAdapter();
+ if (peopleAdapter == null) {
+ peopleAdapter = new PeopleAdapter(getActivity(), peopleList);
+ mFilteredRecyclerView.setAdapter(peopleAdapter);
+ } else {
+ peopleAdapter.setPeopleList(peopleList);
+ }
+
+ if (!peopleList.isEmpty()) {
+ // if the list is not empty, don't show any message
+ mFilteredRecyclerView.hideEmptyView();
+ } else if (!isFetching) {
+ // if we are not fetching and list is empty, show no content message
+ mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+
+ public void fetchingRequestFinished(PeopleListFilter filter, boolean isFirstPage, boolean isSuccessful) {
+ if (mPeopleListFilter == filter) {
+ if (isFirstPage) {
+ mFilteredRecyclerView.setRefreshing(false);
+ if (!isSuccessful) {
+ mFilteredRecyclerView.updateEmptyView(EmptyViewMessageType.GENERIC_ERROR);
+ }
+ } else {
+ mFilteredRecyclerView.hideLoadingProgress();
+ }
+ }
+ }
+
+ // Container Activity must implement this interface
+ public interface OnPersonSelectedListener {
+ void onPersonSelected(Person person);
+ }
+
+ public interface OnFetchPeopleListener {
+ boolean onFetchFirstPage(PeopleListFilter filter);
+
+ boolean onFetchMorePeople(PeopleListFilter filter);
+ }
+
+ public class PeopleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ private final LayoutInflater mInflater;
+ private List<Person> mPeopleList;
+ private int mAvatarSz;
+
+ public PeopleAdapter(Context context, List<Person> peopleList) {
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.people_avatar_sz);
+ mInflater = LayoutInflater.from(context);
+ mPeopleList = peopleList;
+ setHasStableIds(true);
+ }
+
+ public void setPeopleList(List<Person> peopleList) {
+ mPeopleList = peopleList;
+ notifyDataSetChanged();
+ }
+
+ public Person getPerson(int position) {
+ if (mPeopleList == null) {
+ return null;
+ }
+ return mPeopleList.get(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mPeopleList == null) {
+ return 0;
+ }
+ return mPeopleList.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ Person person = getPerson(position);
+ if (person == null) {
+ return -1;
+ }
+ return person.getPersonID();
+ }
+
+ @Override
+ public PeopleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.people_list_row, parent, false);
+
+ return new PeopleViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ PeopleViewHolder peopleViewHolder = (PeopleViewHolder) holder;
+ final Person person = getPerson(position);
+
+ if (person != null) {
+ String avatarUrl = GravatarUtils.fixGravatarUrl(person.getAvatarUrl(), mAvatarSz);
+ peopleViewHolder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ peopleViewHolder.txtDisplayName.setText(StringUtils.unescapeHTML(person.getDisplayName()));
+ if (person.getRole() != null) {
+ peopleViewHolder.txtRole.setVisibility(View.VISIBLE);
+ peopleViewHolder.txtRole.setText(StringUtils.capitalize(person.getRole().toDisplayString()));
+ } else {
+ peopleViewHolder.txtRole.setVisibility(View.GONE);
+ }
+ if (!person.getUsername().isEmpty()) {
+ peopleViewHolder.txtUsername.setVisibility(View.VISIBLE);
+ peopleViewHolder.txtUsername.setText(String.format("@%s", person.getUsername()));
+ } else {
+ peopleViewHolder.txtUsername.setVisibility(View.GONE);
+ }
+ if (person.getPersonType() == Person.PersonType.USER
+ || person.getPersonType() == Person.PersonType.VIEWER) {
+ peopleViewHolder.txtSubscribed.setVisibility(View.GONE);
+ } else {
+ peopleViewHolder.txtSubscribed.setVisibility(View.VISIBLE);
+ String dateSubscribed = SimpleDateFormat.getDateInstance().format(person.getDateSubscribed());
+ String dateText = getString(R.string.follower_subscribed_since, dateSubscribed);
+ peopleViewHolder.txtSubscribed.setText(dateText);
+ }
+ }
+
+ // end of list is reached
+ if (position == getItemCount() - 1) {
+ updatePeople(true);
+ }
+ }
+
+ public class PeopleViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ private final WPNetworkImageView imgAvatar;
+ private final TextView txtDisplayName;
+ private final TextView txtUsername;
+ private final TextView txtRole;
+ private final TextView txtSubscribed;
+
+ public PeopleViewHolder(View view) {
+ super(view);
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.person_avatar);
+ txtDisplayName = (TextView) view.findViewById(R.id.person_display_name);
+ txtUsername = (TextView) view.findViewById(R.id.person_username);
+ txtRole = (TextView) view.findViewById(R.id.person_role);
+ txtSubscribed = (TextView) view.findViewById(R.id.follower_subscribed_date);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mOnPersonSelectedListener != null) {
+ Person person = getPerson(getAdapterPosition());
+ mOnPersonSelectedListener.onPersonSelected(person);
+ }
+ }
+ }
+ }
+
+ // Taken from http://stackoverflow.com/a/27037230
+ private class PeopleItemDecoration extends RecyclerView.ItemDecoration {
+ private Drawable mDivider;
+
+ // use a custom drawable
+ public PeopleItemDecoration(Context context, int resId) {
+ mDivider = ContextCompat.getDrawable(context, resId);
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ int left = parent.getPaddingLeft();
+ int right = parent.getWidth() - parent.getPaddingRight();
+
+ int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+ int top = child.getBottom() + params.bottomMargin;
+ int bottom = top + mDivider.getIntrinsicHeight();
+
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java
new file mode 100644
index 000000000..3c0e43c70
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java
@@ -0,0 +1,664 @@
+package org.wordpress.android.ui.people;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.PeopleTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.PeopleListFilter;
+import org.wordpress.android.models.Person;
+import org.wordpress.android.ui.people.utils.PeopleUtils;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+
+public class PeopleManagementActivity extends AppCompatActivity
+ implements PeopleListFragment.OnPersonSelectedListener, PeopleListFragment.OnFetchPeopleListener {
+ private static final String KEY_PEOPLE_LIST_FRAGMENT = "people-list-fragment";
+ private static final String KEY_PERSON_DETAIL_FRAGMENT = "person-detail-fragment";
+ private static final String KEY_PEOPLE_INVITE_FRAGMENT = "people-invite-fragment";
+ private static final String KEY_TITLE = "page-title";
+
+ private static final String KEY_USERS_END_OF_LIST_REACHED = "users-end-of-list-reached";
+ private static final String KEY_FOLLOWERS_END_OF_LIST_REACHED = "followers-end-of-list-reached";
+ private static final String KEY_EMAIL_FOLLOWERS_END_OF_LIST_REACHED = "email-followers-end-of-list-reached";
+ private static final String KEY_VIEWERS_END_OF_LIST_REACHED = "viewers-end-of-list-reached";
+
+ private static final String KEY_USERS_FETCH_REQUEST_IN_PROGRESS = "users-fetch-request-in-progress";
+ private static final String KEY_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS = "followers-fetch-request-in-progress";
+ private static final String KEY_EMAIL_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS = "email-followers-fetch-request-in-progress";
+ private static final String KEY_VIEWERS_FETCH_REQUEST_IN_PROGRESS = "viewers-fetch-request-in-progress";
+
+ private static final String KEY_HAS_REFRESHED_USERS = "has-refreshed-users";
+ private static final String KEY_HAS_REFRESHED_FOLLOWERS = "has-refreshed-followers";
+ private static final String KEY_HAS_REFRESHED_EMAIL_FOLLOWERS = "has-refreshed-email-followers";
+ private static final String KEY_HAS_REFRESHED_VIEWERS = "has-refreshed-viewers";
+
+ private static final String KEY_FOLLOWERS_LAST_FETCHED_PAGE = "followers-last-fetched-page";
+ private static final String KEY_EMAIL_FOLLOWERS_LAST_FETCHED_PAGE = "email-followers-last-fetched-page";
+
+ // End of list reached variables will be true when there is no more data to fetch
+ private boolean mUsersEndOfListReached;
+ private boolean mFollowersEndOfListReached;
+ private boolean mEmailFollowersEndOfListReached;
+ private boolean mViewersEndOfListReached;
+
+ // We only allow the lists to be refreshed once to avoid syncing and jumping animation issues
+ private boolean mHasRefreshedUsers;
+ private boolean mHasRefreshedFollowers;
+ private boolean mHasRefreshedEmailFollowers;
+ private boolean mHasRefreshedViewers;
+
+ // If we are currently making a request for a certain filter
+ private boolean mUsersFetchRequestInProgress;
+ private boolean mFollowersFetchRequestInProgress;
+ private boolean mEmailFollowersFetchRequestInProgress;
+ private boolean mViewersFetchRequestInProgress;
+
+ // Keep track of the last page we received from remote
+ private int mFollowersLastFetchedPage;
+ private int mEmailFollowersLastFetchedPage;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.people_management_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setElevation(0);
+ }
+
+ Blog blog = WordPress.getCurrentBlog();
+ if (blog == null) {
+ ToastUtils.showToast(this, R.string.blog_not_found);
+ finish();
+ return;
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+
+ if (savedInstanceState == null) {
+ // only delete cached people if there is a connection
+ if (NetworkUtils.isNetworkAvailable(this)) {
+ PeopleTable.deletePeopleExceptForFirstPage(blog.getLocalTableBlogId());
+ }
+
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.people);
+ }
+
+ PeopleListFragment peopleListFragment = PeopleListFragment.newInstance(blog.getLocalTableBlogId());
+ peopleListFragment.setOnPersonSelectedListener(this);
+ peopleListFragment.setOnFetchPeopleListener(this);
+
+ mUsersEndOfListReached = false;
+ mFollowersEndOfListReached = false;
+ mEmailFollowersEndOfListReached = false;
+ mViewersEndOfListReached = false;
+
+ mHasRefreshedUsers = false;
+ mHasRefreshedFollowers = false;
+ mHasRefreshedEmailFollowers = false;
+ mHasRefreshedViewers = false;
+
+ mUsersFetchRequestInProgress = false;
+ mFollowersFetchRequestInProgress = false;
+ mEmailFollowersFetchRequestInProgress = false;
+ mViewersFetchRequestInProgress = false;
+ mFollowersLastFetchedPage = 0;
+ mEmailFollowersLastFetchedPage = 0;
+
+
+ fragmentManager.beginTransaction()
+ .add(R.id.fragment_container, peopleListFragment, KEY_PEOPLE_LIST_FRAGMENT)
+ .commit();
+ } else {
+ mUsersEndOfListReached = savedInstanceState.getBoolean(KEY_USERS_END_OF_LIST_REACHED);
+ mFollowersEndOfListReached = savedInstanceState.getBoolean(KEY_FOLLOWERS_END_OF_LIST_REACHED);
+ mEmailFollowersEndOfListReached = savedInstanceState.getBoolean(KEY_EMAIL_FOLLOWERS_END_OF_LIST_REACHED);
+ mViewersEndOfListReached = savedInstanceState.getBoolean(KEY_VIEWERS_END_OF_LIST_REACHED);
+
+ mHasRefreshedUsers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_USERS);
+ mHasRefreshedFollowers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_FOLLOWERS);
+ mHasRefreshedEmailFollowers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_EMAIL_FOLLOWERS);
+ mHasRefreshedViewers = savedInstanceState.getBoolean(KEY_HAS_REFRESHED_VIEWERS);
+
+ mUsersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_USERS_FETCH_REQUEST_IN_PROGRESS);
+ mFollowersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS);
+ mEmailFollowersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_EMAIL_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS);
+ mViewersFetchRequestInProgress = savedInstanceState.getBoolean(KEY_VIEWERS_FETCH_REQUEST_IN_PROGRESS);
+
+ mFollowersLastFetchedPage = savedInstanceState.getInt(KEY_FOLLOWERS_LAST_FETCHED_PAGE);
+ mEmailFollowersLastFetchedPage = savedInstanceState.getInt(KEY_EMAIL_FOLLOWERS_LAST_FETCHED_PAGE);
+
+ CharSequence title = savedInstanceState.getCharSequence(KEY_TITLE);
+ if (actionBar != null && title != null) {
+ actionBar.setTitle(title);
+ }
+
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ peopleListFragment.setOnPersonSelectedListener(this);
+ peopleListFragment.setOnFetchPeopleListener(this);
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState){
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_USERS_END_OF_LIST_REACHED, mUsersEndOfListReached);
+ outState.putBoolean(KEY_FOLLOWERS_END_OF_LIST_REACHED, mFollowersEndOfListReached);
+ outState.putBoolean(KEY_EMAIL_FOLLOWERS_END_OF_LIST_REACHED, mEmailFollowersEndOfListReached);
+ outState.putBoolean(KEY_VIEWERS_END_OF_LIST_REACHED, mViewersEndOfListReached);
+
+ outState.putBoolean(KEY_HAS_REFRESHED_USERS, mHasRefreshedUsers);
+ outState.putBoolean(KEY_HAS_REFRESHED_FOLLOWERS, mHasRefreshedFollowers);
+ outState.putBoolean(KEY_HAS_REFRESHED_EMAIL_FOLLOWERS, mHasRefreshedEmailFollowers);
+ outState.putBoolean(KEY_HAS_REFRESHED_VIEWERS, mHasRefreshedViewers);
+
+ outState.putBoolean(KEY_USERS_FETCH_REQUEST_IN_PROGRESS, mUsersFetchRequestInProgress);
+ outState.putBoolean(KEY_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS, mFollowersFetchRequestInProgress);
+ outState.putBoolean(KEY_EMAIL_FOLLOWERS_FETCH_REQUEST_IN_PROGRESS, mEmailFollowersFetchRequestInProgress);
+ outState.putBoolean(KEY_VIEWERS_FETCH_REQUEST_IN_PROGRESS, mViewersFetchRequestInProgress);
+
+ outState.putInt(KEY_FOLLOWERS_LAST_FETCHED_PAGE, mFollowersLastFetchedPage);
+ outState.putInt(KEY_EMAIL_FOLLOWERS_LAST_FETCHED_PAGE, mEmailFollowersLastFetchedPage);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ outState.putCharSequence(KEY_TITLE, actionBar.getTitle());
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!navigateBackToPeopleListFragment()) {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (item.getItemId() == R.id.remove_person) {
+ confirmRemovePerson();
+ return true;
+ } else if (item.getItemId() == R.id.invite) {
+ FragmentManager fragmentManager = getFragmentManager();
+ Fragment peopleInviteFragment = fragmentManager.findFragmentByTag(KEY_PERSON_DETAIL_FRAGMENT);
+
+ if (peopleInviteFragment == null) {
+ Blog blog = WordPress.getCurrentBlog();
+ peopleInviteFragment = PeopleInviteFragment.newInstance(blog.getDotComBlogId());
+ }
+ if (!peopleInviteFragment.isAdded()) {
+ FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ fragmentTransaction.replace(R.id.fragment_container, peopleInviteFragment, KEY_PEOPLE_INVITE_FRAGMENT);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ }
+ } else if (item.getItemId() == R.id.send_invitation) {
+ FragmentManager fragmentManager = getFragmentManager();
+ Fragment peopleInviteFragment = fragmentManager.findFragmentByTag(KEY_PEOPLE_INVITE_FRAGMENT);
+ if (peopleInviteFragment != null) {
+ ((InvitationSender) peopleInviteFragment).send();
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private boolean fetchUsersList(String dotComBlogId, final int localTableBlogId, final int offset) {
+ if (mUsersEndOfListReached || mUsersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) {
+ return false;
+ }
+
+ mUsersFetchRequestInProgress = true;
+
+ PeopleUtils.fetchUsers(dotComBlogId, localTableBlogId, offset, new PeopleUtils.FetchUsersCallback() {
+ @Override
+ public void onSuccess(List<Person> peopleList, boolean isEndOfList) {
+ boolean isFreshList = (offset == 0);
+ mHasRefreshedUsers = true;
+ mUsersEndOfListReached = isEndOfList;
+ PeopleTable.saveUsers(peopleList, localTableBlogId, isFreshList);
+
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.TEAM, isFreshList, true);
+ }
+
+ refreshOnScreenFragmentDetails();
+ mUsersFetchRequestInProgress = false;
+ }
+
+ @Override
+ public void onError() {
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ boolean isFirstPage = offset == 0;
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.TEAM, isFirstPage, false);
+ }
+ mUsersFetchRequestInProgress = false;
+ ToastUtils.showToast(PeopleManagementActivity.this,
+ R.string.error_fetch_users_list,
+ ToastUtils.Duration.SHORT);
+ }
+ });
+
+ return true;
+ }
+
+ private boolean fetchFollowersList(String dotComBlogId, final int localTableBlogId, final int page) {
+ if (mFollowersEndOfListReached || mFollowersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) {
+ return false;
+ }
+
+ mFollowersFetchRequestInProgress = true;
+
+ PeopleUtils.fetchFollowers(dotComBlogId, localTableBlogId, page, new PeopleUtils.FetchFollowersCallback() {
+ @Override
+ public void onSuccess(List<Person> peopleList, int pageFetched, boolean isEndOfList) {
+ boolean isFreshList = (page == 1);
+ mHasRefreshedFollowers = true;
+ mFollowersLastFetchedPage = pageFetched;
+ mFollowersEndOfListReached = isEndOfList;
+ PeopleTable.saveFollowers(peopleList, localTableBlogId, isFreshList);
+
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.FOLLOWERS, isFreshList, true);
+ }
+
+ refreshOnScreenFragmentDetails();
+ mFollowersFetchRequestInProgress = false;
+ }
+
+ @Override
+ public void onError() {
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ boolean isFirstPage = page == 1;
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.FOLLOWERS, isFirstPage, false);
+ }
+ mFollowersFetchRequestInProgress = false;
+ ToastUtils.showToast(PeopleManagementActivity.this,
+ R.string.error_fetch_followers_list,
+ ToastUtils.Duration.SHORT);
+ }
+ });
+
+ return true;
+ }
+
+ private boolean fetchEmailFollowersList(String dotComBlogId, final int localTableBlogId, final int page) {
+ if (mEmailFollowersEndOfListReached || mEmailFollowersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) {
+ return false;
+ }
+
+ mEmailFollowersFetchRequestInProgress = true;
+
+ PeopleUtils.fetchEmailFollowers(dotComBlogId, localTableBlogId, page, new PeopleUtils.FetchFollowersCallback() {
+ @Override
+ public void onSuccess(List<Person> peopleList, int pageFetched, boolean isEndOfList) {
+ boolean isFreshList = (page == 1);
+ mHasRefreshedEmailFollowers = true;
+ mEmailFollowersLastFetchedPage = pageFetched;
+ mEmailFollowersEndOfListReached = isEndOfList;
+ PeopleTable.saveEmailFollowers(peopleList, localTableBlogId, isFreshList);
+
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.EMAIL_FOLLOWERS, isFreshList, true);
+ }
+
+ refreshOnScreenFragmentDetails();
+ mEmailFollowersFetchRequestInProgress = false;
+ }
+
+ @Override
+ public void onError() {
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ boolean isFirstPage = page == 1;
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.EMAIL_FOLLOWERS, isFirstPage, false);
+ }
+ mEmailFollowersFetchRequestInProgress = false;
+ ToastUtils.showToast(PeopleManagementActivity.this,
+ R.string.error_fetch_email_followers_list,
+ ToastUtils.Duration.SHORT);
+ }
+ });
+
+ return true;
+ }
+
+ private boolean fetchViewersList(String dotComBlogId, final int localTableBlogId, final int offset) {
+ if (mViewersEndOfListReached || mViewersFetchRequestInProgress || !NetworkUtils.checkConnection(this)) {
+ return false;
+ }
+
+ mViewersFetchRequestInProgress = true;
+
+ PeopleUtils.fetchViewers(dotComBlogId, localTableBlogId, offset, new PeopleUtils.FetchViewersCallback() {
+ @Override
+ public void onSuccess(List<Person> peopleList, boolean isEndOfList) {
+ boolean isFreshList = (offset == 0);
+ mHasRefreshedViewers = true;
+ mViewersEndOfListReached = isEndOfList;
+ PeopleTable.saveViewers(peopleList, localTableBlogId, isFreshList);
+
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.VIEWERS, isFreshList, true);
+ }
+
+ refreshOnScreenFragmentDetails();
+ mViewersFetchRequestInProgress = false;
+ }
+
+ @Override
+ public void onError() {
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ boolean isFirstPage = offset == 0;
+ peopleListFragment.fetchingRequestFinished(PeopleListFilter.VIEWERS, isFirstPage, false);
+ }
+ mViewersFetchRequestInProgress = false;
+ ToastUtils.showToast(PeopleManagementActivity.this,
+ R.string.error_fetch_viewers_list,
+ ToastUtils.Duration.SHORT);
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public void onPersonSelected(Person person) {
+ PersonDetailFragment personDetailFragment = getDetailFragment();
+
+ long personID = person.getPersonID();
+ int localTableBlogID = person.getLocalTableBlogId();
+
+ if (personDetailFragment == null) {
+ personDetailFragment = PersonDetailFragment.newInstance(personID, localTableBlogID, person.getPersonType());
+ } else {
+ personDetailFragment.setPersonDetails(personID, localTableBlogID);
+ }
+ if (!personDetailFragment.isAdded()) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.OPENED_PERSON);
+ FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ fragmentTransaction.replace(R.id.fragment_container, personDetailFragment, KEY_PERSON_DETAIL_FRAGMENT);
+ fragmentTransaction.addToBackStack(null);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle("");
+ }
+
+ fragmentTransaction.commit();
+ }
+ }
+
+ public void onEventMainThread(RoleChangeDialogFragment.RoleChangeEvent event) {
+ if(!NetworkUtils.checkConnection(this)) {
+ return;
+ }
+
+ final Person person = PeopleTable.getUser(event.personID, event.localTableBlogId);
+ if (person == null || event.newRole == null || person.getRole() == event.newRole) {
+ return;
+ }
+
+ String blogId = WordPress.getCurrentRemoteBlogId();
+ if (blogId == null) {
+ return;
+ }
+
+ final PersonDetailFragment personDetailFragment = getDetailFragment();
+ if (personDetailFragment != null) {
+ // optimistically update the role
+ personDetailFragment.changeRole(event.newRole);
+ }
+
+ PeopleUtils.updateRole(blogId, person.getPersonID(), event.newRole, event.localTableBlogId,
+ new PeopleUtils.UpdateUserCallback() {
+ @Override
+ public void onSuccess(Person person) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.PERSON_UPDATED);
+ PeopleTable.saveUser(person);
+ refreshOnScreenFragmentDetails();
+ }
+
+ @Override
+ public void onError() {
+ // change the role back to it's original value
+ if (personDetailFragment != null) {
+ personDetailFragment.refreshPersonDetails();
+ }
+ ToastUtils.showToast(PeopleManagementActivity.this,
+ R.string.error_update_role,
+ ToastUtils.Duration.LONG);
+ }
+ });
+ }
+
+ private void confirmRemovePerson() {
+ Person person = getCurrentPerson();
+ if (person == null) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Calypso_AlertDialog);
+ builder.setTitle(getString(R.string.person_remove_confirmation_title, person.getDisplayName()));
+ if (person.getPersonType() == Person.PersonType.USER) {
+ builder.setMessage(getString(R.string.user_remove_confirmation_message, person.getDisplayName()));
+ } else if(person.getPersonType() == Person.PersonType.VIEWER) {
+ builder.setMessage(R.string.viewer_remove_confirmation_message);
+ } else {
+ builder.setMessage(R.string.follower_remove_confirmation_message);
+ }
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.remove, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ removeSelectedPerson();
+ }
+ });
+ builder.show();
+ }
+
+ private void removeSelectedPerson() {
+ if(!NetworkUtils.checkConnection(this)) {
+ return;
+ }
+
+ Person person = getCurrentPerson();
+ if (person == null) {
+ return;
+ }
+ String blogId = WordPress.getCurrentRemoteBlogId();
+ if (blogId == null) {
+ return;
+ }
+
+ final Person.PersonType personType = person.getPersonType();
+ final String displayName = person.getDisplayName();
+
+ PeopleUtils.RemovePersonCallback callback = new PeopleUtils.RemovePersonCallback() {
+ @Override
+ public void onSuccess(long personID, int localTableBlogId) {
+ if (personType == Person.PersonType.USER) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.PERSON_REMOVED);
+ }
+
+ // remove the person from db, navigate back to list fragment and refresh it
+ PeopleTable.deletePerson(personID, localTableBlogId, personType);
+
+ String message = getString(R.string.person_removed, displayName);
+ ToastUtils.showToast(PeopleManagementActivity.this, message, ToastUtils.Duration.LONG);
+
+ navigateBackToPeopleListFragment();
+ refreshPeopleListFragment();
+ }
+
+ @Override
+ public void onError() {
+ int errorMessageRes;
+ switch (personType) {
+ case USER:
+ errorMessageRes = R.string.error_remove_user;
+ break;
+ case VIEWER:
+ errorMessageRes = R.string.error_remove_viewer;
+ break;
+ default:
+ errorMessageRes = R.string.error_remove_follower;
+ break;
+ }
+ ToastUtils.showToast(PeopleManagementActivity.this,
+ errorMessageRes,
+ ToastUtils.Duration.LONG);
+ }
+ };
+
+ if (personType == Person.PersonType.FOLLOWER || personType == Person.PersonType.EMAIL_FOLLOWER) {
+ PeopleUtils.removeFollower(blogId, person.getPersonID(), person.getLocalTableBlogId(),
+ personType, callback);
+ } else if(personType == Person.PersonType.VIEWER) {
+ PeopleUtils.removeViewer(blogId, person.getPersonID(), person.getLocalTableBlogId(), callback);
+ } else {
+ PeopleUtils.removeUser(blogId, person.getPersonID(), person.getLocalTableBlogId(), callback);
+ }
+ }
+
+ // This helper method is used after a successful network request
+ private void refreshOnScreenFragmentDetails() {
+ refreshPeopleListFragment();
+ refreshDetailFragment();
+ }
+
+ private void refreshPeopleListFragment() {
+ PeopleListFragment peopleListFragment = getListFragment();
+ if (peopleListFragment != null) {
+ peopleListFragment.refreshPeopleList(false);
+ }
+ }
+
+ private void refreshDetailFragment() {
+ PersonDetailFragment personDetailFragment = getDetailFragment();
+ if (personDetailFragment != null) {
+ personDetailFragment.refreshPersonDetails();
+ }
+ }
+
+ private boolean navigateBackToPeopleListFragment() {
+ FragmentManager fragmentManager = getFragmentManager();
+ if (fragmentManager.getBackStackEntryCount() > 0) {
+ fragmentManager.popBackStack();
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.people);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private Person getCurrentPerson() {
+ PersonDetailFragment personDetailFragment = getDetailFragment();
+
+ if (personDetailFragment == null) {
+ return null;
+ }
+
+ return personDetailFragment.loadPerson();
+ }
+
+ @Override
+ public boolean onFetchFirstPage(PeopleListFilter filter) {
+ Blog blog = WordPress.getCurrentBlog();
+ if (filter == PeopleListFilter.TEAM && !mHasRefreshedUsers) {
+ return fetchUsersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 0);
+ } else if (filter == PeopleListFilter.FOLLOWERS && !mHasRefreshedFollowers) {
+ return fetchFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 1);
+ } else if (filter == PeopleListFilter.EMAIL_FOLLOWERS && !mHasRefreshedEmailFollowers) {
+ return fetchEmailFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 1);
+ } else if (filter == PeopleListFilter.VIEWERS && !mHasRefreshedViewers) {
+ return fetchViewersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), 0);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onFetchMorePeople(PeopleListFilter filter) {
+ if (filter == PeopleListFilter.TEAM && !mUsersEndOfListReached) {
+ Blog blog = WordPress.getCurrentBlog();
+ int count = PeopleTable.getUsersCountForLocalBlogId(blog.getLocalTableBlogId());
+ return fetchUsersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), count);
+ } else if (filter == PeopleListFilter.FOLLOWERS && !mFollowersEndOfListReached) {
+ Blog blog = WordPress.getCurrentBlog();
+ int pageToFetch = mFollowersLastFetchedPage + 1;
+ return fetchFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), pageToFetch);
+ } else if (filter == PeopleListFilter.EMAIL_FOLLOWERS && !mEmailFollowersEndOfListReached) {
+ Blog blog = WordPress.getCurrentBlog();
+ int pageToFetch = mEmailFollowersLastFetchedPage + 1;
+ return fetchEmailFollowersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), pageToFetch);
+ } else if (filter == PeopleListFilter.VIEWERS && !mViewersEndOfListReached) {
+ Blog blog = WordPress.getCurrentBlog();
+ int count = PeopleTable.getViewersCountForLocalBlogId(blog.getLocalTableBlogId());
+ return fetchViewersList(blog.getDotComBlogId(), blog.getLocalTableBlogId(), count);
+ }
+ return false;
+ }
+
+ private PeopleListFragment getListFragment() {
+ return (PeopleListFragment) getFragmentManager().findFragmentByTag(KEY_PEOPLE_LIST_FRAGMENT);
+ }
+
+ private PersonDetailFragment getDetailFragment() {
+ return (PersonDetailFragment) getFragmentManager().findFragmentByTag(KEY_PERSON_DETAIL_FRAGMENT);
+ }
+
+ public interface InvitationSender {
+ void send();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java
new file mode 100644
index 000000000..0687e23a4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PersonDetailFragment.java
@@ -0,0 +1,209 @@
+package org.wordpress.android.ui.people;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.PeopleTable;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Capability;
+import org.wordpress.android.models.Person;
+import org.wordpress.android.models.Role;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.text.SimpleDateFormat;
+
+public class PersonDetailFragment extends Fragment {
+ private static String ARG_PERSON_ID = "person_id";
+ private static String ARG_LOCAL_TABLE_BLOG_ID = "local_table_blog_id";
+ private static String ARG_PERSON_TYPE = "person_type";
+
+ private long mPersonID;
+ private int mLocalTableBlogID;
+ private Person.PersonType mPersonType;
+
+ private WPNetworkImageView mAvatarImageView;
+ private TextView mDisplayNameTextView;
+ private TextView mUsernameTextView;
+ private LinearLayout mRoleContainer;
+ private TextView mRoleTextView;
+ private LinearLayout mSubscribedDateContainer;
+ private TextView mSubscribedDateTitleView;
+ private TextView mSubscribedDateTextView;
+
+ public static PersonDetailFragment newInstance(long personID, int localTableBlogID, Person.PersonType personType) {
+ PersonDetailFragment personDetailFragment = new PersonDetailFragment();
+ Bundle bundle = new Bundle();
+ bundle.putLong(ARG_PERSON_ID, personID);
+ bundle.putInt(ARG_LOCAL_TABLE_BLOG_ID, localTableBlogID);
+ bundle.putSerializable(ARG_PERSON_TYPE, personType);
+ personDetailFragment.setArguments(bundle);
+ return personDetailFragment;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.person_detail, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.person_detail_fragment, container, false);
+
+ mPersonID = getArguments().getLong(ARG_PERSON_ID);
+ mLocalTableBlogID = getArguments().getInt(ARG_LOCAL_TABLE_BLOG_ID);
+ mPersonType = (Person.PersonType) getArguments().getSerializable(ARG_PERSON_TYPE);
+
+ mAvatarImageView = (WPNetworkImageView) rootView.findViewById(R.id.person_avatar);
+ mDisplayNameTextView = (TextView) rootView.findViewById(R.id.person_display_name);
+ mUsernameTextView = (TextView) rootView.findViewById(R.id.person_username);
+ mRoleContainer = (LinearLayout) rootView.findViewById(R.id.person_role_container);
+ mRoleTextView = (TextView) rootView.findViewById(R.id.person_role);
+ mSubscribedDateContainer = (LinearLayout) rootView.findViewById(R.id.subscribed_date_container);
+ mSubscribedDateTitleView = (TextView) rootView.findViewById(R.id.subscribed_date_title);
+ mSubscribedDateTextView = (TextView) rootView.findViewById(R.id.subscribed_date_text);
+
+ Account account = AccountHelper.getDefaultAccount();
+ boolean isCurrentUser = account.getUserId() == mPersonID;
+ Blog blog = WordPress.getBlog(mLocalTableBlogID);
+ if (!isCurrentUser && blog != null && blog.hasCapability(Capability.REMOVE_USERS)) {
+ setHasOptionsMenu(true);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refreshPersonDetails();
+ }
+
+ public void refreshPersonDetails() {
+ if (!isAdded()) return;
+
+ Person person = loadPerson();
+ if (person != null) {
+ int avatarSz = getResources().getDimensionPixelSize(R.dimen.people_avatar_sz);
+ String avatarUrl = GravatarUtils.fixGravatarUrl(person.getAvatarUrl(), avatarSz);
+
+ mAvatarImageView.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ mDisplayNameTextView.setText(StringUtils.unescapeHTML(person.getDisplayName()));
+ if (person.getRole() != null) {
+ mRoleTextView.setText(StringUtils.capitalize(person.getRole().toDisplayString()));
+ }
+
+ if (!TextUtils.isEmpty(person.getUsername())) {
+ mUsernameTextView.setText(String.format("@%s", person.getUsername()));
+ }
+
+ if (mPersonType == Person.PersonType.USER) {
+ mRoleContainer.setVisibility(View.VISIBLE);
+ setupRoleContainerForCapability();
+ } else {
+ mRoleContainer.setVisibility(View.GONE);
+ }
+
+ if (mPersonType == Person.PersonType.USER || mPersonType == Person.PersonType.VIEWER) {
+ mSubscribedDateContainer.setVisibility(View.GONE);
+ } else {
+ mSubscribedDateContainer.setVisibility(View.VISIBLE);
+ if (mPersonType == Person.PersonType.FOLLOWER) {
+ mSubscribedDateTitleView.setText(R.string.title_follower);
+ } else if (mPersonType == Person.PersonType.EMAIL_FOLLOWER) {
+ mSubscribedDateTitleView.setText(R.string.title_email_follower);
+ }
+ String dateSubscribed = SimpleDateFormat.getDateInstance().format(person.getDateSubscribed());
+ String dateText = getString(R.string.follower_subscribed_since, dateSubscribed);
+ mSubscribedDateTextView.setText(dateText);
+ }
+
+ // Adds extra padding to display name for email followers to make it vertically centered
+ int padding = mPersonType == Person.PersonType.EMAIL_FOLLOWER
+ ? (int) getResources().getDimension(R.dimen.margin_small) : 0;
+ changeDisplayNameTopPadding(padding);
+ } else {
+ AppLog.w(AppLog.T.PEOPLE, "Person returned null from DB for personID: " + mPersonID
+ + " & localTableBlogID: " + mLocalTableBlogID);
+ }
+ }
+
+ public void setPersonDetails(long personID, int localTableBlogID) {
+ mPersonID = personID;
+ mLocalTableBlogID = localTableBlogID;
+ refreshPersonDetails();
+ }
+
+ // Checks current user's capabilities to decide whether she can change the role or not
+ private void setupRoleContainerForCapability() {
+ Blog blog = WordPress.getBlog(mLocalTableBlogID);
+ Account account = AccountHelper.getDefaultAccount();
+ boolean isCurrentUser = account.getUserId() == mPersonID;
+ boolean canChangeRole = (blog != null) && !isCurrentUser && blog.hasCapability(Capability.PROMOTE_USERS);
+ if (canChangeRole) {
+ mRoleContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showRoleChangeDialog();
+ }
+ });
+ } else {
+ // Remove the selectableItemBackground if the user can't be edited
+ clearRoleContainerBackground();
+ // Change transparency to give a visual cue to the user that it's disabled
+ mRoleContainer.setAlpha(0.5f);
+ }
+ }
+
+ private void showRoleChangeDialog() {
+ Person person = loadPerson();
+ if (person == null || person.getRole() == null) {
+ return;
+ }
+
+ RoleChangeDialogFragment dialog = RoleChangeDialogFragment.newInstance(person.getPersonID(),
+ person.getLocalTableBlogId(), person.getRole());
+ dialog.show(getFragmentManager(), null);
+ }
+
+ // used to optimistically update the role
+ public void changeRole(Role newRole) {
+ mRoleTextView.setText(newRole.toDisplayString());
+ }
+
+ @SuppressWarnings("deprecation")
+ private void clearRoleContainerBackground() {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ mRoleContainer.setBackgroundDrawable(null);
+ } else {
+ mRoleContainer.setBackground(null);
+ }
+ }
+
+ private void changeDisplayNameTopPadding(int newPadding) {
+ if (mDisplayNameTextView == null) {
+ return;
+ }
+ mDisplayNameTextView.setPadding(0, newPadding, 0 , 0);
+ }
+
+ public Person loadPerson() {
+ return PeopleTable.getPerson(mPersonID, mLocalTableBlogID, mPersonType);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java
new file mode 100644
index 000000000..438231fe3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleChangeDialogFragment.java
@@ -0,0 +1,148 @@
+package org.wordpress.android.ui.people;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Role;
+
+import de.greenrobot.event.EventBus;
+
+public class RoleChangeDialogFragment extends DialogFragment {
+ private static final String PERSON_ID_TAG = "person_id";
+ private static final String PERSON_LOCAL_TABLE_BLOG_ID_TAG = "local_table_blog_id";
+ private static final String ROLE_TAG = "role";
+
+ private RoleListAdapter mRoleListAdapter;
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Role role = mRoleListAdapter.getSelectedRole();
+ outState.putSerializable(ROLE_TAG, role);
+ }
+
+ public static RoleChangeDialogFragment newInstance(long personID, int localTableBlogId, Role role) {
+ RoleChangeDialogFragment roleChangeDialogFragment = new RoleChangeDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putLong(PERSON_ID_TAG, personID);
+ args.putInt(PERSON_LOCAL_TABLE_BLOG_ID_TAG, localTableBlogId);
+ if (role != null) {
+ args.putSerializable(ROLE_TAG, role);
+ }
+
+ roleChangeDialogFragment.setArguments(args);
+ return roleChangeDialogFragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ builder.setTitle(R.string.role);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Role role = mRoleListAdapter.getSelectedRole();
+ Bundle args = getArguments();
+ if (args != null) {
+ long personID = args.getLong(PERSON_ID_TAG);
+ int localTableBlogId = args.getInt(PERSON_LOCAL_TABLE_BLOG_ID_TAG);
+ EventBus.getDefault().post(new RoleChangeEvent(personID, localTableBlogId, role));
+ }
+ }
+ });
+
+ if (mRoleListAdapter == null) {
+ final Role[] userRoles = Role.userRoles();
+ mRoleListAdapter = new RoleListAdapter(getActivity(), R.layout.role_list_row, userRoles);
+ }
+ if (savedInstanceState != null) {
+ Role savedRole = (Role) savedInstanceState.getSerializable(ROLE_TAG);
+ mRoleListAdapter.setSelectedRole(savedRole);
+ } else {
+ Bundle args = getArguments();
+ if (args != null) {
+ Role role = (Role) args.getSerializable(ROLE_TAG);
+ mRoleListAdapter.setSelectedRole(role);
+ }
+ }
+ builder.setAdapter(mRoleListAdapter, null);
+
+ return builder.create();
+ }
+
+ private class RoleListAdapter extends ArrayAdapter<Role> {
+ private Role mSelectedRole;
+
+ public RoleListAdapter(Context context, int resource, Role[] objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = View.inflate(getContext(), R.layout.role_list_row, null);
+ }
+
+ final RadioButton radioButton = (RadioButton) convertView.findViewById(R.id.radio);
+ TextView mainText = (TextView) convertView.findViewById(R.id.role_label);
+ Role role = getItem(position);
+ mainText.setText(role.toDisplayString());
+
+ if (radioButton != null) {
+ radioButton.setChecked(role == mSelectedRole);
+ radioButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeSelection(position);
+ }
+ });
+ }
+
+ convertView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeSelection(position);
+ }
+ });
+
+ return convertView;
+ }
+
+ private void changeSelection(int position) {
+ mSelectedRole = getItem(position);
+ notifyDataSetChanged();
+ }
+
+ public Role getSelectedRole() {
+ return mSelectedRole;
+ }
+
+ public void setSelectedRole(Role role) {
+ mSelectedRole = role;
+ }
+ }
+
+ public static class RoleChangeEvent {
+ public final long personID;
+ public final int localTableBlogId;
+ public final Role newRole;
+
+ public RoleChangeEvent(long personID, int localTableBlogId, Role newRole) {
+ this.personID = personID;
+ this.localTableBlogId = localTableBlogId;
+ this.newRole = newRole;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java
new file mode 100644
index 000000000..9c39fc559
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/RoleSelectDialogFragment.java
@@ -0,0 +1,66 @@
+package org.wordpress.android.ui.people;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Role;
+
+public class RoleSelectDialogFragment extends DialogFragment {
+ private static final String IS_PRIVATE_TAG = "is_private";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ boolean isPrivateSite = getArguments().getBoolean(IS_PRIVATE_TAG);
+ final Role[] roles = Role.inviteRoles(isPrivateSite);
+ final String[] stringRoles = new String[roles.length];
+ for (int i = 0; i < roles.length; i++) {
+ stringRoles[i] = roles[i].toDisplayString();
+ }
+
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ builder.setTitle(R.string.role);
+ builder.setItems(stringRoles, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (getTargetFragment() instanceof OnRoleSelectListener) {
+ ((OnRoleSelectListener) getTargetFragment()).onRoleSelected(roles[which]);
+ } else if (getActivity() instanceof OnRoleSelectListener) {
+ ((OnRoleSelectListener) getActivity()).onRoleSelected(roles[which]);
+ }
+ }
+ });
+
+ return builder.create();
+ }
+
+ public static <T extends Fragment & OnRoleSelectListener> void show(T parentFragment, int requestCode,
+ boolean isPrivateSite) {
+ RoleSelectDialogFragment roleChangeDialogFragment = new RoleSelectDialogFragment();
+ Bundle args = new Bundle();
+ args.putBoolean(IS_PRIVATE_TAG, isPrivateSite);
+ roleChangeDialogFragment.setArguments(args);
+ roleChangeDialogFragment.setTargetFragment(parentFragment, requestCode);
+ roleChangeDialogFragment.show(parentFragment.getFragmentManager(), null);
+ }
+
+ public static <T extends Activity & OnRoleSelectListener> void show(T parentActivity) {
+ RoleSelectDialogFragment roleChangeDialogFragment = new RoleSelectDialogFragment();
+ roleChangeDialogFragment.show(parentActivity.getFragmentManager(), null);
+ }
+
+ // Container Activity must implement this interface
+ public interface OnRoleSelectListener {
+ void onRoleSelected(Role newRole);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java
new file mode 100644
index 000000000..5c01c6ddf
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/people/utils/PeopleUtils.java
@@ -0,0 +1,527 @@
+package org.wordpress.android.ui.people.utils;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Person;
+import org.wordpress.android.models.Role;
+import org.wordpress.android.ui.people.utils.PeopleUtils.ValidateUsernameCallback.ValidationResult;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PeopleUtils {
+ // We limit followers we display to 1000 to avoid API performance issues
+ public static int FOLLOWER_PAGE_LIMIT = 50;
+ public static int FETCH_LIMIT = 20;
+
+ public static void fetchUsers(final String blogId, final int localTableBlogId, final int offset,
+ final FetchUsersCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ try {
+ JSONArray jsonArray = jsonObject.getJSONArray("users");
+ List<Person> people = peopleListFromJSON(jsonArray, localTableBlogId, Person.PersonType.USER);
+ int numberOfUsers = jsonObject.optInt("found");
+ boolean isEndOfList = (people.size() + offset) >= numberOfUsers;
+ callback.onSuccess(people, isEndOfList);
+ }
+ catch (JSONException e) {
+ AppLog.e(T.API, "JSON exception occurred while parsing the response for sites/%s/users: " + e);
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ Map<String, String> params = new HashMap<>();
+ params.put("number", Integer.toString(PeopleUtils.FETCH_LIMIT));
+ params.put("offset", Integer.toString(offset));
+ params.put("order_by", "display_name");
+ params.put("order", "ASC");
+ String path = String.format("sites/%s/users", blogId);
+ WordPress.getRestClientUtilsV1_1().get(path, params, null, listener, errorListener);
+ }
+
+ public static void fetchFollowers(final String blogId, final int localTableBlogId, final int page,
+ final FetchFollowersCallback callback) {
+ fetchFollowers(blogId, localTableBlogId, page, callback, false);
+ }
+
+ public static void fetchEmailFollowers(final String blogId, final int localTableBlogId, final int page,
+ final FetchFollowersCallback callback) {
+ fetchFollowers(blogId, localTableBlogId, page, callback, true);
+ }
+
+ private static void fetchFollowers(final String blogId, final int localTableBlogId, final int page,
+ final FetchFollowersCallback callback, final boolean isEmailFollower) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ try {
+ JSONArray jsonArray = jsonObject.getJSONArray("subscribers");
+ Person.PersonType personType = isEmailFollower ?
+ Person.PersonType.EMAIL_FOLLOWER : Person.PersonType.FOLLOWER;
+ List<Person> people = peopleListFromJSON(jsonArray, localTableBlogId, personType);
+ int pageFetched = jsonObject.optInt("page");
+ int numberOfPages = jsonObject.optInt("pages");
+ boolean isEndOfList = page >= numberOfPages || page >= FOLLOWER_PAGE_LIMIT;
+ callback.onSuccess(people, pageFetched, isEndOfList);
+ }
+ catch (JSONException e) {
+ AppLog.e(T.API, "JSON exception occurred while parsing the response for " +
+ "sites/%s/stats/followers: " + e);
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ Map<String, String> params = new HashMap<>();
+ params.put("max", Integer.toString(FETCH_LIMIT));
+ params.put("page", Integer.toString(page));
+ params.put("type", isEmailFollower ? "email" : "wp_com");
+ String path = String.format("sites/%s/stats/followers", blogId);
+ WordPress.getRestClientUtilsV1_1().get(path, params, null, listener, errorListener);
+ }
+
+ public static void fetchViewers(final String blogId, final int localTableBlogId, final int offset,
+ final FetchViewersCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ try {
+ JSONArray jsonArray = jsonObject.getJSONArray("viewers");
+ List<Person> people = peopleListFromJSON(jsonArray, localTableBlogId, Person.PersonType.VIEWER);
+ int numberOfUsers = jsonObject.optInt("found");
+ boolean isEndOfList = (people.size() + offset) >= numberOfUsers;
+ callback.onSuccess(people, isEndOfList);
+ }
+ catch (JSONException e) {
+ AppLog.e(T.API, "JSON exception occurred while parsing the response for " +
+ "sites/%s/viewers: " + e);
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ int page = (offset / FETCH_LIMIT) + 1;
+ Map<String, String> params = new HashMap<>();
+ params.put("number", Integer.toString(FETCH_LIMIT));
+ params.put("page", Integer.toString(page));
+ String path = String.format("sites/%s/viewers", blogId);
+ WordPress.getRestClientUtilsV1_1().get(path, params, null, listener, errorListener);
+ }
+
+ public static void updateRole(final String blogId, long personID, Role newRole, final int localTableBlogId,
+ final UpdateUserCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ try {
+ Person person = Person.userFromJSON(jsonObject, localTableBlogId);
+ if (person != null) {
+ callback.onSuccess(person);
+ } else {
+ AppLog.e(T.API, "Couldn't map jsonObject + " + jsonObject + " to person model.");
+ callback.onError();
+ }
+ } catch (JSONException e) {
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ Map<String, String> params = new HashMap<>();
+ params.put("roles", newRole.toRESTString());
+ String path = String.format("sites/%s/users/%d", blogId, personID);
+ WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener);
+ }
+
+ public static void removeUser(String blogId, final long personID, final int localTableBlogId,
+ final RemovePersonCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ // check if the call was successful
+ boolean success = jsonObject.optBoolean("success");
+ if (success) {
+ callback.onSuccess(personID, localTableBlogId);
+ } else {
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ String path = String.format("sites/%s/users/%d/delete", blogId, personID);
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ }
+
+ public static void removeFollower(String blogId, final long personID, final int localTableBlogId,
+ Person.PersonType personType, final RemovePersonCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ // check if the call was successful
+ boolean success = jsonObject.optBoolean("deleted");
+ if (success) {
+ callback.onSuccess(personID, localTableBlogId);
+ } else {
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ String path;
+ if (personType == Person.PersonType.EMAIL_FOLLOWER) {
+ path = String.format("sites/%s/email-followers/%d/delete", blogId, personID);
+ } else {
+ path = String.format("sites/%s/followers/%d/delete", blogId, personID);
+ }
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ }
+
+ public static void removeViewer(String blogId, final long personID, final int localTableBlogId,
+ final RemovePersonCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ // check if the call was successful
+ boolean success = jsonObject.optBoolean("deleted");
+ if (success) {
+ callback.onSuccess(personID, localTableBlogId);
+ } else {
+ callback.onError();
+ }
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ String path = String.format("sites/%s/viewers/%d/delete", blogId, personID);
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ }
+
+ private static List<Person> peopleListFromJSON(JSONArray jsonArray, int localTableBlogId,
+ Person.PersonType personType) throws JSONException {
+ if (jsonArray == null) {
+ return null;
+ }
+
+ ArrayList<Person> peopleList = new ArrayList<>(jsonArray.length());
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ Person person;
+ if (personType == Person.PersonType.USER) {
+ person = Person.userFromJSON(jsonArray.optJSONObject(i), localTableBlogId);
+ } else if (personType == Person.PersonType.VIEWER) {
+ person = Person.viewerFromJSON(jsonArray.optJSONObject(i), localTableBlogId);
+ } else {
+ boolean isEmailFollower = (personType == Person.PersonType.EMAIL_FOLLOWER);
+ person = Person.followerFromJSON(jsonArray.optJSONObject(i), localTableBlogId, isEmailFollower);
+ }
+ if (person != null) {
+ peopleList.add(person);
+ }
+ }
+
+ return peopleList;
+ }
+
+ public interface FetchUsersCallback extends Callback {
+ void onSuccess(List<Person> peopleList, boolean isEndOfList);
+ }
+
+ public interface FetchFollowersCallback extends Callback {
+ void onSuccess(List<Person> peopleList, int pageFetched, boolean isEndOfList);
+ }
+
+ public interface FetchViewersCallback extends Callback {
+ void onSuccess(List<Person> peopleList, boolean isEndOfList);
+ }
+
+ public interface RemovePersonCallback extends Callback {
+ void onSuccess(long personID, int localTableBlogId);
+ }
+
+ public interface UpdateUserCallback extends Callback {
+ void onSuccess(Person person);
+ }
+
+ public interface Callback {
+ void onError();
+ }
+
+ public static void validateUsernames(final List<String> usernames, Role role, String dotComBlogId, final
+ ValidateUsernameCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null && callback != null) {
+ JSONObject errors = jsonObject.optJSONObject("errors");
+
+ int errorredUsernameCount = 0;
+
+ if (errors != null) {
+ for (String username : usernames) {
+ JSONObject userError = errors.optJSONObject(username);
+
+ if (userError == null) {
+ continue;
+ }
+
+ errorredUsernameCount++;
+
+ switch (userError.optString("code")) {
+ case "invalid_input":
+ switch (userError.optString("message")) {
+ case "User not found":
+ callback.onUsernameValidation(username, ValidationResult.USER_NOT_FOUND);
+ continue;
+ case "Invalid email":
+ callback.onUsernameValidation(username, ValidationResult.INVALID_EMAIL);
+ continue;
+ }
+ break;
+ case "invalid_input_has_role":
+ callback.onUsernameValidation(username, ValidationResult.ALREADY_MEMBER);
+ continue;
+ case "invalid_input_following":
+ callback.onUsernameValidation(username, ValidationResult.ALREADY_FOLLOWING);
+ continue;
+ case "invalid_user_blocked_invites":
+ callback.onUsernameValidation(username, ValidationResult.BLOCKED_INVITES);
+ continue;
+ }
+
+ callback.onError();
+ callback.onValidationFinished();
+ return;
+ }
+ }
+
+ JSONArray succeededUsernames = jsonObject.optJSONArray("success");
+ if (succeededUsernames == null) {
+ callback.onError();
+ callback.onValidationFinished();
+ return;
+ }
+
+ int succeededUsernameCount = 0;
+
+ for (int i = 0; i < succeededUsernames.length(); i++) {
+ String username = succeededUsernames.optString(i);
+ if (usernames.contains(username)) {
+ succeededUsernameCount++;
+ callback.onUsernameValidation(username, ValidationResult.USER_FOUND);
+ }
+ }
+
+ if (errorredUsernameCount + succeededUsernameCount != usernames.size()) {
+ callback.onError();
+ callback.onValidationFinished();
+ }
+
+ callback.onValidationFinished();
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ String path = String.format("sites/%s/invites/validate", dotComBlogId);
+ Map<String, String> params = new HashMap<>();
+ for (String username : usernames) {
+ params.put("invitees[" + username + "]", username); // specify an array key so to make the map key unique
+ }
+ params.put("role", role.toRESTString());
+ WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener);
+ }
+
+ public interface ValidateUsernameCallback {
+ enum ValidationResult {
+ USER_NOT_FOUND,
+ ALREADY_MEMBER,
+ ALREADY_FOLLOWING,
+ BLOCKED_INVITES,
+ INVALID_EMAIL,
+ USER_FOUND
+ }
+
+ void onUsernameValidation(String username, ValidationResult validationResult);
+ void onValidationFinished();
+ void onError();
+ }
+
+ public static void sendInvitations(final List<String> usernames, Role role, String message, String dotComBlogId,
+ final InvitationsSendCallback callback) {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (callback == null) {
+ return;
+ }
+
+ if (jsonObject == null) {
+ callback.onError();
+ return;
+ }
+
+ Map<String, String> failedUsernames = new LinkedHashMap<>();
+
+ JSONObject errors = jsonObject.optJSONObject("errors");
+ if (errors != null) {
+ for (String username : usernames) {
+ JSONObject userError = errors.optJSONObject(username);
+
+ if (userError != null) {
+ failedUsernames.put(username, userError.optString("message"));
+ }
+ }
+ }
+
+ List<String> succeededUsernames = new ArrayList<>();
+ JSONArray succeededUsernamesJson = jsonObject.optJSONArray("sent");
+ if (succeededUsernamesJson == null) {
+ callback.onError();
+ return;
+ }
+
+ for (int i = 0; i < succeededUsernamesJson.length(); i++) {
+ String username = succeededUsernamesJson.optString(i);
+ if (usernames.contains(username)) {
+ succeededUsernames.add(username);
+ }
+ }
+
+ if (failedUsernames.size() + succeededUsernames.size() != usernames.size()) {
+ callback.onError();
+ }
+
+ callback.onSent(succeededUsernames, failedUsernames);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.API, volleyError);
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+ };
+
+ String path = String.format("sites/%s/invites/new", dotComBlogId);
+ Map<String, String> params = new HashMap<>();
+ for (String username : usernames) {
+ params.put("invitees[" + username + "]", username); // specify an array key so to make the map key unique
+ }
+ params.put("role", role.toRESTString());
+ params.put("message", message);
+ WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener);
+ }
+
+ public interface InvitationsSendCallback {
+ void onSent(List<String> succeededUsernames, Map<String, String> failedUsernameErrors);
+ void onError();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanEvents.java
new file mode 100644
index 000000000..2ae00a4e1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanEvents.java
@@ -0,0 +1,30 @@
+package org.wordpress.android.ui.plans;
+
+import android.support.annotation.NonNull;
+
+import org.wordpress.android.ui.plans.models.Plan;
+
+import java.util.List;
+
+/**
+ * Plan-related EventBus event classes
+ */
+class PlanEvents {
+
+ public static class PlansUpdated {
+ private final List<Plan> mPlans;
+ private final int mLocalBlogId;
+ public PlansUpdated(int localBlogId, @NonNull List<Plan> plans) {
+ mLocalBlogId = localBlogId;
+ mPlans = plans;
+ }
+ public int getLocalBlogId() {
+ return mLocalBlogId;
+ }
+ public List<Plan> getPlans() {
+ return mPlans;
+ }
+ }
+
+ public static class PlansUpdateFailed { }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java
new file mode 100644
index 000000000..e22ee623c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java
@@ -0,0 +1,171 @@
+package org.wordpress.android.ui.plans;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.plans.models.Feature;
+import org.wordpress.android.ui.plans.models.Plan;
+import org.wordpress.android.ui.plans.models.PlanFeaturesHighlightSection;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.HtmlUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class PlanFragment extends Fragment {
+ private static final String SITE_PLAN = "SITE_PLAN";
+
+ private ViewGroup mPlanContainerView;
+ private Plan mPlanDetails;
+
+ public static PlanFragment newInstance(Plan sitePlan) {
+ PlanFragment fragment = new PlanFragment();
+ fragment.setSitePlan(sitePlan);
+ AppLog.d(AppLog.T.PLANS, "PlanFragment newInstance");
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(SITE_PLAN)) {
+ Serializable serial = savedInstanceState.getSerializable(SITE_PLAN);
+ if (serial instanceof Plan) {
+ setSitePlan((Plan) serial);
+ }
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.plan_fragment, container, false);
+ mPlanContainerView = (LinearLayout) rootView.findViewById(R.id.plan_container);
+ return rootView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ showPlans();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putSerializable(SITE_PLAN, mPlanDetails);
+ super.onSaveInstanceState(outState);
+ }
+
+ private void showPlans() {
+ if (!isAdded()) return;
+
+ if (mPlanDetails == null) {
+ // TODO This should never happen - Fix this. Close the activity?
+ AppLog.w(AppLog.T.PLANS, "empty plan data in fragment");
+ return;
+ }
+
+ int iconSize = getActivity().getResources().getDimensionPixelSize(R.dimen.plan_icon_size);
+ NetworkImageView imgPlan = (NetworkImageView) getView().findViewById(R.id.image_plan_icon);
+ String iconUrl = PlansUtils.getIconUrlForPlan(mPlanDetails, iconSize);
+ if (!TextUtils.isEmpty(iconUrl)) {
+ imgPlan.setImageUrl(iconUrl, WordPress.imageLoader);
+ imgPlan.setVisibility(View.VISIBLE);
+ } else {
+ imgPlan.setVisibility(View.GONE);
+ }
+
+ // show product short name in bold, ex: "WordPress.com <b>Premium</b>"
+ TextView txtProductName = (TextView) getView().findViewById(R.id.text_product_name);
+ String productShortName = mPlanDetails.getProductNameShort();
+ String productName = mPlanDetails.getProductName().replace(productShortName,
+ "<b>" + productShortName + "</b>");
+ txtProductName.setText(Html.fromHtml(productName));
+
+ TextView txtTagLine = (TextView) getView().findViewById(R.id.text_tagline);
+ txtTagLine.setText(HtmlUtils.fastUnescapeHtml(mPlanDetails.getTagline()));
+
+ addFeaturesToHighlight();
+ }
+
+ private void addFeaturesToHighlight() {
+ HashMap<String, Feature> globalFeatures = PlansUtils.getFeatures();
+ if (globalFeatures == null) {
+ AppLog.w(AppLog.T.PLANS, "no global features");
+ return;
+ }
+
+ ArrayList<PlanFeaturesHighlightSection> sectionsToHighlight = mPlanDetails.getFeaturesHighlightSections();
+ if (sectionsToHighlight == null) {
+ AppLog.w(AppLog.T.PLANS, "no sections to highlight");
+ return;
+ }
+
+ for (PlanFeaturesHighlightSection section : sectionsToHighlight) {
+ // add section title if it's not empty
+ addSectionTitle(section.getTitle());
+ // add features to highlight in this section
+ ArrayList<String> featuresToHighlight = section.getFeatures();
+ for (String featureSlug : featuresToHighlight) {
+ addFeature(globalFeatures.get(featureSlug));
+ }
+ }
+ }
+
+ private void addSectionTitle(String title) {
+ if (TextUtils.isEmpty(title)) return;
+
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ ViewGroup view = (ViewGroup) inflater.inflate(R.layout.plan_section_title, mPlanContainerView, false);
+
+ TextView txtTitle = (TextView) view.findViewById(R.id.text_section_title);
+ txtTitle.setText(HtmlUtils.fastUnescapeHtml(title));
+
+ mPlanContainerView.addView(view);
+ }
+
+ private void addFeature(Feature feature) {
+ if (feature == null) return;
+
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ ViewGroup view = (ViewGroup) inflater.inflate(R.layout.plan_feature_item, mPlanContainerView, false);
+
+ TextView txtTitle = (TextView) view.findViewById(R.id.text_feature_title);
+ TextView txtDescription = (TextView) view.findViewById(R.id.text_feature_description);
+ String title = HtmlUtils.fastUnescapeHtml(feature.getTitleForPlan(mPlanDetails.getProductID()));
+ String description = HtmlUtils.fastUnescapeHtml(feature.getDescriptionForPlan(mPlanDetails.getProductID()));
+ txtTitle.setText(title);
+ txtDescription.setText(description);
+
+ // TODO: right now icon is always empty, so we show noticon_publish as a placeholder
+ NetworkImageView imgIcon = (NetworkImageView) view.findViewById(R.id.image_icon);
+ String iconUrl = feature.getIconForPlan(mPlanDetails.getProductID());
+ if (!TextUtils.isEmpty(iconUrl)) {
+ imgIcon.setImageUrl(iconUrl, WordPress.imageLoader);
+ } else {
+ imgIcon.setDefaultImageResId(R.drawable.noticon_publish);
+ }
+
+ mPlanContainerView.addView(view);
+ }
+
+ private void setSitePlan(@NonNull Plan sitePlan) {
+ mPlanDetails = sitePlan;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseActivity.java
new file mode 100644
index 000000000..866fdc50e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseActivity.java
@@ -0,0 +1,243 @@
+package org.wordpress.android.ui.plans;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IdRes;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.widgets.WPViewPager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * post-purchase "on-boarding" for future use after user purchases premium or business plan
+ */
+public class PlanPostPurchaseActivity extends AppCompatActivity {
+
+ static final int PAGE_NUMBER_INTRO = 0;
+ static final int PAGE_NUMBER_CUSTOMIZE = 1;
+ static final int PAGE_NUMBER_VIDEO = 2;
+ static final int PAGE_NUMBER_THEMES = 3; // business only
+
+ private static final String ARG_IS_BUSINESS_PLAN = "is_business_plan";
+
+ private ViewPager mViewPager;
+ private PageAdapter mPageAdapter;
+ private TextView mTxtSkip;
+ private TextView mTxtNext;
+ private ViewGroup mIndicatorContainerView;
+
+ private int mPrevPageNumber = 0;
+ private boolean mIsBusinessPlan;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.plan_post_purchase_activity);
+
+ if (savedInstanceState != null) {
+ mIsBusinessPlan = savedInstanceState.getBoolean(ARG_IS_BUSINESS_PLAN, false);
+ } else {
+ mIsBusinessPlan = getIntent().getBooleanExtra(ARG_IS_BUSINESS_PLAN, false);
+ }
+
+ mTxtSkip = (TextView) findViewById(R.id.text_skip);
+ mTxtNext = (TextView) findViewById(R.id.text_next);
+ mIndicatorContainerView = (ViewGroup) findViewById(R.id.layout_indicator_container);
+
+ mViewPager = (WPViewPager) findViewById(R.id.viewpager);
+ mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ if (position != mPrevPageNumber) {
+ updateIndicator(position);
+ updateIndicator(mPrevPageNumber);
+ }
+ updateButtons();
+ mPrevPageNumber = position;
+ }
+ });
+ mViewPager.setAdapter(getPageAdapter());
+
+ mTxtSkip.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ mTxtNext.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ gotoNextPage();
+ }
+ });
+
+ int numPages = getNumPages();
+ for (int i = 0; i < numPages; i++) {
+ getIndicator(i).setOnClickListener(mIndicatorClickListener);
+ }
+ getIndicator(PAGE_NUMBER_THEMES).setVisibility(numPages > 3 ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(ARG_IS_BUSINESS_PLAN, mIsBusinessPlan);
+ }
+
+ /*
+ * last pages is themes, which should only appear when user has purchased business plan
+ */
+ private int getNumPages() {
+ return mIsBusinessPlan ? 4 : 3;
+ }
+
+ private PageAdapter getPageAdapter() {
+ if (mPageAdapter == null) {
+ List<Fragment> fragments = new ArrayList<>();
+ for (int i = 0; i < getNumPages(); i++) {
+ fragments.add(PlanPostPurchaseFragment.newInstance(i));
+ }
+
+ FragmentManager fm = getFragmentManager();
+ mPageAdapter = new PageAdapter(fm, fragments);
+ }
+ return mPageAdapter;
+ }
+
+ private int getCurrentPage() {
+ return mViewPager.getCurrentItem();
+ }
+
+ private boolean isLastPage() {
+ return getCurrentPage() == getNumPages() - 1;
+ }
+
+ private void gotoNextPage() {
+ if (isLastPage()) {
+ finish();
+ } else {
+ gotoPage(getCurrentPage() + 1);
+ }
+ }
+
+ private void gotoPage(int pageNumber) {
+ mViewPager.setCurrentItem(pageNumber, true);
+ }
+
+ private void updateButtons() {
+ if (isLastPage()) {
+ mTxtNext.setText(R.string.button_done);
+ if (mTxtSkip.getVisibility() == View.VISIBLE) {
+ AniUtils.fadeOut(mTxtSkip, AniUtils.Duration.MEDIUM);
+ }
+ } else {
+ mTxtNext.setText(R.string.button_next);
+ if (mTxtSkip.getVisibility() != View.VISIBLE) {
+ AniUtils.fadeIn(mTxtSkip, AniUtils.Duration.MEDIUM);
+ }
+ }
+ }
+
+ private ImageView getIndicator(int pageNumber) {
+ @IdRes int resId;
+ switch (pageNumber) {
+ case 0:
+ resId = R.id.image_indicator_1;
+ break;
+ case 1:
+ resId = R.id.image_indicator_2;
+ break;
+ case 2:
+ resId = R.id.image_indicator_3;
+ break;
+ case 3:
+ resId = R.id.image_indicator_4;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid indicator page number");
+ }
+ return (ImageView) mIndicatorContainerView.findViewById(resId);
+ }
+
+ private void updateIndicator(int pageNumber) {
+ boolean isSelected = (pageNumber == getCurrentPage());
+ final ImageView indicator = getIndicator(pageNumber);
+ final @DrawableRes int backgroundRes =
+ isSelected ? R.drawable.indicator_circle_selected : R.drawable.indicator_circle_unselected;
+
+ if (isSelected) {
+ // scale it out, change the background, then scale it back in
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 0.25f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 0.25f);
+ ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(indicator, scaleX, scaleY);
+ anim.setDuration(150);
+ anim.setInterpolator(new AccelerateInterpolator());
+ anim.setRepeatCount(1);
+ anim.setRepeatMode(ValueAnimator.REVERSE);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ indicator.setBackgroundResource(backgroundRes);
+ }
+ });
+ anim.start();
+ } else {
+ indicator.setBackgroundResource(backgroundRes);
+ }
+ }
+
+ private final View.OnClickListener mIndicatorClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.image_indicator_1) {
+ gotoPage(0);
+ } else if (id == R.id.image_indicator_2) {
+ gotoPage(1);
+ } else if (id == R.id.image_indicator_3) {
+ gotoPage(2);
+ } else if (id == R.id.image_indicator_4) {
+ gotoPage(3);
+ }
+ }
+ };
+
+ private class PageAdapter extends FragmentPagerAdapter {
+ private final List<Fragment> mFragments;
+
+ PageAdapter(FragmentManager fm, List<Fragment> fragments) {
+ super(fm);
+ mFragments = fragments;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return mFragments.get(position);
+ }
+
+ @Override
+ public int getCount() {
+ return mFragments.size();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseFragment.java
new file mode 100644
index 000000000..1cc61fec6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanPostPurchaseFragment.java
@@ -0,0 +1,145 @@
+package org.wordpress.android.ui.plans;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.support.percent.PercentLayoutHelper;
+import android.support.percent.PercentRelativeLayout;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.themes.ThemeWebActivity;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+
+/**
+ * single page within the post-purchase activity's ViewPager
+ */
+public class PlanPostPurchaseFragment extends Fragment {
+
+ private static final String ARG_PAGE_NUMBER = "page_number";
+ private int mPageNumber;
+
+ static PlanPostPurchaseFragment newInstance(int pageNumber) {
+ PlanPostPurchaseFragment fragment = new PlanPostPurchaseFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_PAGE_NUMBER, pageNumber);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void setArguments(Bundle args) {
+ super.setArguments(args);
+ mPageNumber = args.getInt(ARG_PAGE_NUMBER);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(ARG_PAGE_NUMBER, mPageNumber);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mPageNumber = savedInstanceState.getInt(ARG_PAGE_NUMBER);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.plan_post_purchase_fragment, container, false);
+
+ ImageView image = (ImageView) rootView.findViewById(R.id.image);
+ TextView txtTitle = (TextView) rootView.findViewById(R.id.text_title);
+ TextView txtDescription = (TextView) rootView.findViewById(R.id.text_description);
+ Button button = (Button) rootView.findViewById(R.id.button);
+
+ // reduce margin of image on landscape phones so content doesn't extend beyond the screen
+ if (DisplayUtils.isLandscape(getActivity()) && !DisplayUtils.isXLarge(getActivity())) {
+ PercentRelativeLayout.LayoutParams layoutParams = (PercentRelativeLayout.LayoutParams) image.getLayoutParams();
+ PercentLayoutHelper.PercentLayoutInfo percentLayoutInfo = layoutParams.getPercentLayoutInfo();
+ percentLayoutInfo.topMarginPercent = 15 * 0.01f; // 15%
+ percentLayoutInfo.bottomMarginPercent = 5 * 0.01f; // 5%
+ image.setLayoutParams(layoutParams);
+ }
+
+ int titleResId;
+ int textResId;
+ int buttonResId;
+ int imageResId;
+ switch (mPageNumber) {
+ case PlanPostPurchaseActivity.PAGE_NUMBER_INTRO:
+ titleResId = R.string.plans_post_purchase_title_intro;
+ textResId = R.string.plans_post_purchase_text_intro;
+ buttonResId = 0;
+ imageResId = R.drawable.plans_business_active;
+ break;
+ case PlanPostPurchaseActivity.PAGE_NUMBER_CUSTOMIZE:
+ titleResId = R.string.plans_post_purchase_title_customize;
+ textResId = R.string.plans_post_purchase_text_customize;
+ buttonResId = R.string.plans_post_purchase_button_customize;
+ imageResId = R.drawable.plans_customize;
+ break;
+ case PlanPostPurchaseActivity.PAGE_NUMBER_VIDEO:
+ titleResId = R.string.plans_post_purchase_title_video;
+ textResId = R.string.plans_post_purchase_text_video;
+ buttonResId = R.string.plans_post_purchase_button_video;
+ imageResId = R.drawable.plans_video_upload;
+ break;
+ case PlanPostPurchaseActivity.PAGE_NUMBER_THEMES:
+ titleResId = R.string.plans_post_purchase_title_themes;
+ textResId = R.string.plans_post_purchase_text_themes;
+ buttonResId = R.string.plans_post_purchase_button_themes;
+ imageResId = R.drawable.plans_premium_themes;
+ break;
+ default:
+ AppLog.w(AppLog.T.PLANS, "invalid plans post-purchase page");
+ throw new IllegalArgumentException("invalid plans post-purchase page");
+ }
+
+ txtTitle.setText(titleResId);
+ txtDescription.setText(textResId);
+ image.setImageResource(imageResId);
+
+ if (buttonResId != 0) {
+ button.setVisibility(View.VISIBLE);
+ button.setText(buttonResId);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handleButtonClick();
+ }
+ });
+ } else {
+ button.setVisibility(View.GONE);
+ }
+
+ return rootView;
+ }
+
+ private void handleButtonClick() {
+ switch (mPageNumber) {
+ case PlanPostPurchaseActivity.PAGE_NUMBER_CUSTOMIZE:
+ ThemeWebActivity.openCurrentTheme(getActivity(), ThemeWebActivity.ThemeWebActivityType.PREVIEW);
+ break;
+ case PlanPostPurchaseActivity.PAGE_NUMBER_THEMES:
+ ActivityLauncher.viewCurrentBlogThemes(getActivity());
+ break;
+ case PlanPostPurchaseActivity.PAGE_NUMBER_VIDEO:
+ ActivityLauncher.addNewBlogPostOrPageForResult(getActivity(), WordPress.currentBlog, false);
+ break;
+ }
+
+ // the user launched another activity, so we close this one
+ getActivity().finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanUpdateService.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanUpdateService.java
new file mode 100644
index 000000000..317a38810
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanUpdateService.java
@@ -0,0 +1,159 @@
+package org.wordpress.android.ui.plans;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.plans.models.Plan;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which updates both global plans and available plans for a specific site
+ */
+
+public class PlanUpdateService extends Service {
+
+ private static final String ARG_LOCAL_BLOG_ID = "local_blog_id";
+ private int mNumActiveRequests;
+ private int mLocalBlogId;
+ private final List<Plan> mSitePlans = new ArrayList<>();
+
+ public static void startService(Context context, int localTableBlogId) {
+ Intent intent = new Intent(context, PlanUpdateService.class);
+ intent.putExtra(ARG_LOCAL_BLOG_ID, localTableBlogId);
+ context.startService(intent);
+ }
+
+ public static void stopService(Context context) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, PlanUpdateService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.PLANS, "plan update service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.PLANS, "plan update service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mLocalBlogId = intent.getIntExtra(ARG_LOCAL_BLOG_ID, 0);
+
+ mNumActiveRequests = 2;
+ downloadPlanFeatures();
+ downloadAvailablePlansForSite();
+
+ return START_NOT_STICKY;
+ }
+
+ /*
+ * called when any plan data has been successfully updated
+ */
+ private void requestCompleted() {
+ // send event once all requests have successfully completed
+ mNumActiveRequests--;
+ if (mNumActiveRequests == 0) {
+ EventBus.getDefault().post(new PlanEvents.PlansUpdated(mLocalBlogId, mSitePlans));
+ }
+ }
+
+ /*
+ * called when updating any plan data fails
+ */
+ private void requestFailed() {
+ EventBus.getDefault().post(new PlanEvents.PlansUpdateFailed());
+ }
+
+ /*
+ * download features for the global plans
+ */
+ private void downloadPlanFeatures() {
+ WordPress.getRestClientUtilsV1_2().get("plans/features/", RestClientUtils.getRestLocaleParams(PlanUpdateService.this), null, new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (response != null) {
+ AppLog.d(AppLog.T.PLANS, response.toString());
+ // Store the response into App Prefs
+ AppPrefs.setGlobalPlansFeatures(response.toString());
+ requestCompleted();
+ } else {
+ AppLog.w(AppLog.T.PLANS, "Unexpected empty response from server when downloading Features");
+ requestFailed();
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.PLANS, "Error Loading Plans/Features", volleyError);
+ requestFailed();
+ }
+ });
+ }
+
+ /*
+ * download plans for the specific site
+ */
+ private void downloadAvailablePlansForSite() {
+ int remoteBlogId = WordPress.wpDB.getRemoteBlogIdForLocalTableBlogId(mLocalBlogId);
+ WordPress.getRestClientUtilsV1_2().get("sites/" + remoteBlogId + "/plans", RestClientUtils.getRestLocaleParams(PlanUpdateService.this), null, new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (response == null) {
+ AppLog.w(AppLog.T.PLANS, "Unexpected empty response from server");
+ requestFailed();
+ return;
+ }
+
+ AppLog.d(AppLog.T.PLANS, response.toString());
+ mSitePlans.clear();
+
+ try {
+ JSONArray plansArray = response.getJSONArray("originalResponse");
+ for (int i=0; i < plansArray.length(); i ++) {
+ JSONObject currentPlanJSON = plansArray.getJSONObject(i);
+ Plan currentPlan = new Plan(currentPlanJSON);
+ mSitePlans.add(currentPlan);
+ }
+ requestCompleted();
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.PLANS, "Can't parse the plans list returned from the server", e);
+ requestFailed();
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.UTILS, "Error downloading site plans", volleyError);
+ requestFailed();
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java
new file mode 100644
index 000000000..db4ef6493
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java
@@ -0,0 +1,312 @@
+package org.wordpress.android.ui.plans;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.TargetApi;
+import android.graphics.Point;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.design.widget.TabLayout;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.plans.adapters.PlansPagerAdapter;
+import org.wordpress.android.ui.plans.models.Plan;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPViewPager;
+
+import java.io.Serializable;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+public class PlansActivity extends AppCompatActivity {
+
+ public static final String ARG_LOCAL_TABLE_BLOG_ID = "ARG_LOCAL_TABLE_BLOG_ID";
+ private static final String ARG_LOCAL_AVAILABLE_PLANS = "ARG_LOCAL_AVAILABLE_PLANS";
+
+ private int mLocalBlogID = -1;
+ private Plan[] mAvailablePlans;
+
+ private WPViewPager mViewPager;
+ private PlansPagerAdapter mPageAdapter;
+ private TabLayout mTabLayout;
+ private ViewGroup mManageBar;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.plans_activity);
+
+ if (savedInstanceState != null) {
+ mLocalBlogID = savedInstanceState.getInt(ARG_LOCAL_TABLE_BLOG_ID);
+ Serializable serializable = savedInstanceState.getSerializable(ARG_LOCAL_AVAILABLE_PLANS);
+ if (serializable instanceof Plan[]) {
+ mAvailablePlans = (Plan[]) serializable;
+ }
+ } else if (getIntent() != null) {
+ mLocalBlogID = getIntent().getIntExtra(ARG_LOCAL_TABLE_BLOG_ID, -1);
+ }
+
+ if (WordPress.getBlog(mLocalBlogID) == null) {
+ AppLog.e(AppLog.T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.plans_loading_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ mViewPager = (WPViewPager) findViewById(R.id.viewpager);
+ mTabLayout = (TabLayout) findViewById(R.id.tab_layout);
+ mManageBar = (ViewGroup) findViewById(R.id.frame_manage);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ // Shadow removed on Activities with a tab toolbar
+ actionBar.setTitle(getString(R.string.plans));
+ actionBar.setElevation(0.0f);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ // Download plans if not already available
+ if (mAvailablePlans == null) {
+ if (!NetworkUtils.checkConnection(this)) {
+ finish();
+ return;
+ }
+ showProgress();
+ PlanUpdateService.startService(this, mLocalBlogID);
+ } else {
+ setupPlansUI();
+ }
+
+ // navigate to the "manage plans" page for this blog when the user clicks the manage bar
+ mManageBar.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Blog blog = WordPress.getBlog(mLocalBlogID);
+ if (blog == null) return;
+ String domain = UrlUtils.getHost(blog.getUrl());
+ String managePlansUrl = "https://wordpress.com/plans/" + domain;
+ ReaderActivityLauncher.openUrl(view.getContext(), managePlansUrl, OpenUrlType.EXTERNAL);
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ PlanUpdateService.stopService(this);
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ EventBus.getDefault().unregister(this);
+ }
+
+ private void setupPlansUI() {
+ if (mAvailablePlans == null || mAvailablePlans.length == 0) {
+ // This should never be called with empty plans.
+ Toast.makeText(PlansActivity.this, R.string.plans_loading_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ hideProgress();
+
+ mViewPager.setAdapter(getPageAdapter());
+
+ int normalColor = ContextCompat.getColor(this, R.color.blue_light);
+ int selectedColor = ContextCompat.getColor(this, R.color.white);
+ mTabLayout.setTabTextColors(normalColor, selectedColor);
+ mTabLayout.setupWithViewPager(mViewPager);
+
+ // tabMode is set to scrollable in layout, set to fixed if there's enough space to show them all
+ mTabLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mTabLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+
+ if (mTabLayout.getChildCount() > 0) {
+ int tabLayoutWidth = 0;
+ LinearLayout tabFirstChild = (LinearLayout) mTabLayout.getChildAt(0);
+ for (int i = 0; i < mTabLayout.getTabCount(); i++) {
+ LinearLayout tabView = (LinearLayout) (tabFirstChild.getChildAt(i));
+ tabLayoutWidth += (tabView.getMeasuredWidth() + tabView.getPaddingLeft() + tabView.getPaddingRight());
+ }
+
+ int displayWidth = DisplayUtils.getDisplayPixelWidth(PlansActivity.this);
+ if (tabLayoutWidth < displayWidth) {
+ mTabLayout.setTabMode(TabLayout.MODE_FIXED);
+ mTabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
+ }
+ }
+ }
+ });
+
+ if (mViewPager.getVisibility() != View.VISIBLE) {
+ // use a circular reveal on API 21+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ revealViewPager();
+ } else {
+ mViewPager.setVisibility(View.VISIBLE);
+ mTabLayout.setVisibility(View.VISIBLE);
+ showManageBar();
+ }
+ }
+ }
+
+ private void showManageBar() {
+ if (mManageBar.getVisibility() != View.VISIBLE) {
+ AniUtils.animateBottomBar(mManageBar, true);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void revealViewPager() {
+ mViewPager.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mViewPager.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+
+ Point pt = DisplayUtils.getDisplayPixelSize(PlansActivity.this);
+ float startRadius = 0f;
+ float endRadius = (float) Math.hypot(pt.x, pt.y);
+ int centerX = pt.x / 2;
+ int centerY = pt.y / 2;
+
+ Animator anim = ViewAnimationUtils.createCircularReveal(mViewPager, centerX, centerY, startRadius, endRadius);
+ anim.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+ anim.setInterpolator(new AccelerateInterpolator());
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ showManageBar();
+ }
+ });
+
+ mViewPager.setVisibility(View.VISIBLE);
+ mTabLayout.setVisibility(View.VISIBLE);
+
+ anim.start();
+ }
+ });
+ }
+
+ private void hideProgress() {
+ final ProgressBar progress = (ProgressBar) findViewById(R.id.progress_loading_plans);
+ progress.setVisibility(View.GONE);
+ }
+
+ private void showProgress() {
+ final ProgressBar progress = (ProgressBar) findViewById(R.id.progress_loading_plans);
+ progress.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ outState.putSerializable(ARG_LOCAL_AVAILABLE_PLANS, mAvailablePlans);
+ super.onSaveInstanceState(outState);
+ }
+
+ private PlansPagerAdapter getPageAdapter() {
+ if (mPageAdapter == null) {
+ mPageAdapter = new PlansPagerAdapter(getFragmentManager(), mAvailablePlans);
+ }
+ return mPageAdapter;
+ }
+
+ /*
+ * move the ViewPager to the plan for the current blog
+ */
+ private void selectCurrentPlan() {
+ int position = -1;
+ for (Plan currentSitePlan : mAvailablePlans) {
+ if (currentSitePlan.isCurrentPlan()) {
+ position = getPageAdapter().getPositionOfPlan(currentSitePlan.getProductID());
+ break;
+ }
+ }
+ if (getPageAdapter().isValidPosition(position)) {
+ mViewPager.setCurrentItem(position);
+ }
+ }
+
+ /*
+ * called by the service when plan data is successfully updated
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PlanEvents.PlansUpdated event) {
+ // make sure the update is for this blog
+ if (event.getLocalBlogId() != this.mLocalBlogID) {
+ AppLog.w(AppLog.T.PLANS, "plans updated for different blog");
+ return;
+ }
+
+ List<Plan> plans = event.getPlans();
+ mAvailablePlans = new Plan[plans.size()];
+ plans.toArray(mAvailablePlans);
+
+ setupPlansUI();
+ selectCurrentPlan();
+ }
+
+ /*
+ * called by the service when plan data fails to update
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PlanEvents.PlansUpdateFailed event) {
+ Toast.makeText(PlansActivity.this, R.string.plans_loading_error, Toast.LENGTH_LONG).show();
+ finish();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansConstants.java
new file mode 100644
index 000000000..203bb6eba
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansConstants.java
@@ -0,0 +1,17 @@
+package org.wordpress.android.ui.plans;
+
+
+public class PlansConstants {
+ // This constant is used to set a plan id on newly created blog. Note that a refresh of the blog
+ // is started immediately after, so even if we decide to offer premium for all on new blogs
+ // the app will be in sync after a while.
+ public static final long DEFAULT_PLAN_ID_FOR_NEW_BLOG = 1L;
+
+ public static final long FREE_PLAN_ID = 1L;
+ public static final long PREMIUM_PLAN_ID = 1003L;
+ public static final long BUSINESS_PLAN_ID = 1008L;
+
+ public static final long JETPACK_FREE_PLAN_ID = 2002L;
+ public static final long JETPACK_PREMIUM_PLAN_ID = 2000L;
+ public static final long JETPACK_BUSINESS_PLAN_ID = 2001L;
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java
new file mode 100644
index 000000000..ed40d9fd6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java
@@ -0,0 +1,75 @@
+package org.wordpress.android.ui.plans;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.plans.models.Feature;
+import org.wordpress.android.ui.plans.models.Plan;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.PhotonUtils;
+
+import java.util.HashMap;
+
+public class PlansUtils {
+
+ @Nullable
+ public static HashMap<String, Feature> getFeatures() {
+ String featuresString = AppPrefs.getGlobalPlansFeatures();
+ if (TextUtils.isEmpty(featuresString)) {
+ return null;
+ }
+
+ HashMap<String, Feature> features = new HashMap<>();
+ try {
+ JSONObject featuresJSONObject = new JSONObject(featuresString);
+ JSONArray featuresArray = featuresJSONObject.getJSONArray("originalResponse");
+ for (int i=0; i < featuresArray.length(); i ++) {
+ JSONObject currentFeatureJSON = featuresArray.getJSONObject(i);
+ Feature currentFeature = new Feature(currentFeatureJSON);
+ features.put(currentFeature.getProductSlug(), currentFeature);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.PLANS, "Can't parse the features list returned from the server", e);
+ return null;
+ }
+
+ return features;
+ }
+
+ /**
+ * Returns the url of the image to display for the passed plan
+ *
+ * @param plan - The plan
+ * @param iconSize - desired size of the returned image
+ * @return string containing photon-ized url for the plan icon
+ */
+ public static String getIconUrlForPlan(Plan plan, int iconSize) {
+ if (plan == null || !plan.hasIconUrl()) {
+ return null;
+ }
+ return PhotonUtils.getPhotonImageUrl(plan.getIconUrl(), iconSize, iconSize);
+ }
+
+ /**
+ * Weather the plan ID is a free plan.
+ *
+ * @param planID - The plan ID
+ * @return boolean - true if the current blog is on a free plan.
+ */
+ private static boolean isFreePlan(long planID) {
+ return planID == PlansConstants.JETPACK_FREE_PLAN_ID || planID == PlansConstants.FREE_PLAN_ID;
+ }
+
+ /**
+ * Removes stored plan data - for testing purposes
+ */
+ @SuppressWarnings("unused")
+ public static void clearPlanData() {
+ AppPrefs.setGlobalPlansFeatures(null);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/adapters/PlansPagerAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/adapters/PlansPagerAdapter.java
new file mode 100644
index 000000000..fdcc5686d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/adapters/PlansPagerAdapter.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.plans.adapters;
+
+import android.app.FragmentManager;
+import android.support.annotation.NonNull;
+import android.support.v13.app.FragmentPagerAdapter;
+
+import org.wordpress.android.ui.plans.PlanFragment;
+import org.wordpress.android.ui.plans.models.Plan;
+import org.wordpress.android.util.AppLog;
+
+/**
+ * ViewPager adapter for the main plans activity
+ */
+public class PlansPagerAdapter extends FragmentPagerAdapter {
+ private final Plan[] mSitePlans;
+ private static final String UNICODE_CHECKMARK = "\u2713";
+
+ public PlansPagerAdapter(FragmentManager fm, @NonNull Plan[] sitePlans) {
+ super(fm);
+ mSitePlans = sitePlans.clone();
+ }
+
+ @Override
+ public PlanFragment getItem(int position) {
+ return PlanFragment.newInstance(mSitePlans[position]);
+ }
+
+ @Override
+ public int getCount() {
+ return mSitePlans.length;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ if (isValidPosition(position)) {
+ Plan planDetails = mSitePlans[position];
+ if (planDetails == null) {
+ AppLog.w(AppLog.T.PLANS, "plans pager > empty plan details in getPageTitle");
+ return "";
+ } else if (mSitePlans[position].isCurrentPlan()) {
+ return UNICODE_CHECKMARK + " " + planDetails.getProductNameShort();
+ } else {
+ return planDetails.getProductNameShort();
+ }
+ }
+ return super.getPageTitle(position);
+ }
+
+ public boolean isValidPosition(int position) {
+ return (position >= 0 && position < getCount());
+ }
+
+ public int getPositionOfPlan(long planID) {
+ for (int i = 0; i < getCount(); i++) {
+ if (mSitePlans[i].getProductID() == planID) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Feature.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Feature.java
new file mode 100644
index 000000000..6a8455394
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Feature.java
@@ -0,0 +1,124 @@
+package org.wordpress.android.ui.plans.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class Feature {
+ /*
+
+ {
+ "product_slug": "space",
+ "title": "Space",
+ "description": "Increase your available storage space and add the ability to upload audio files.",
+ "icon": "",
+ "plans": {
+ "1": {
+ "title": "Media storage",
+ "description": "Upload up to 3GB of photos, videos, or music.",
+ "icon": ""
+ },
+ "1003": {
+ "title": "Expanded media storage",
+ "description": "Upload up to 13GB of photos, videos, or music.",
+ "icon": ""
+ },
+ "1008": {
+ "title": "Unlimited media storage",
+ "description": "You can upload unlimited photos, videos, or music.",
+ "icon": ""
+ }
+ }
+ },
+
+ OR
+
+ {
+ "product_slug": "ecommerce",
+ "title": "eCommerce",
+ "description": "Sell stuff right on your blog with Ecwid and Shopify.",
+ "icon": "",
+ "plans": {
+ "1008": true
+ }
+ },
+
+ */
+
+ private String mProductSlug;
+ private String mTitle;
+ private String mIcon;
+ private String mDescription;
+ private boolean mIsNotPartOfFreeTrial;
+ private final JSONObject mPlanIDToDescription;
+
+ public Feature(JSONObject featureJSONObject) throws JSONException {
+ mProductSlug = featureJSONObject.getString("product_slug");
+ mTitle = featureJSONObject.getString("title");
+ mIcon = featureJSONObject.optString("icon");
+ mDescription = featureJSONObject.getString("description");
+ mPlanIDToDescription = featureJSONObject.optJSONObject("plans");
+
+ if (featureJSONObject.has("not_part_of_free_trial") &&
+ JSONUtils.getBool(featureJSONObject, "not_part_of_free_trial")) {
+ // not part of free trial
+ mIsNotPartOfFreeTrial = true;
+ }
+ }
+
+ public String getProductSlug() {
+ return StringUtils.notNullStr(mProductSlug);
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(mTitle);
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(mDescription);
+ }
+
+ public boolean isNotPartOfFreeTrial() {
+ return mIsNotPartOfFreeTrial;
+ }
+
+ /**
+ * Return the description of this feature for a given plan.
+ * If description is not provided for the given plan, fallback to the global description of the feature.
+ */
+ public String getDescriptionForPlan(Long planID) {
+ return getPropertyForPlan(planID, "description", mDescription);
+ }
+
+ /**
+ * Return the title of this feature for a given plan.
+ * If title is not provided for the given plan, fallback to the global title of the feature.
+ */
+ public String getTitleForPlan(Long planID) {
+ return getPropertyForPlan(planID, "title", mTitle);
+ }
+
+ /**
+ * Return the icon of this feature for a given plan.
+ * If icon is not provided for the given plan, fallback to the global icon for this feature.
+ */
+ public String getIconForPlan(Long planID) {
+ return getPropertyForPlan(planID, "icon", mIcon);
+ }
+
+ private String getPropertyForPlan(Long planID, String propertyName, String fallback) {
+ String planIdAsString = String.valueOf(planID);
+ fallback = StringUtils.notNullStr(fallback);
+ if (mPlanIDToDescription != null && mPlanIDToDescription.has(planIdAsString)) {
+ JSONObject plan = mPlanIDToDescription.optJSONObject(planIdAsString);
+ if (plan != null) { // It's not a JSON object. Just `true` in the response. That means the plan has this feature with generic description/title/icon.
+ return plan.optString(
+ propertyName,
+ fallback
+ );
+ }
+ }
+ return fallback;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java
new file mode 100644
index 000000000..339a1b9ea
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java
@@ -0,0 +1,285 @@
+package org.wordpress.android.ui.plans.models;
+
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+public class Plan implements Serializable {
+ private long mProductID;
+ private String mProductName;
+ private final Hashtable<String, Integer> mPrices = new Hashtable<>();
+ private String mProductNameShort;
+ private String mProductSlug;
+ private String mTagline;
+ private String mDescription;
+ private long mCost;
+ private int mBillPeriod;
+ private String mProductType;
+ private boolean mIsAvailable;
+ private String mBillPeriodLabel;
+ private String mPrice;
+ private String mFormattedPrice;
+ private String mIconUrl;
+ private int mRawPrice;
+
+ // Optionals
+ private int mWidth;
+ private int mHeight;
+ private long mSaving; // diff from cost and original
+ private long mOriginal; // Original price
+ private String mFormattedOriginalPrice;
+ private int mStore;
+ private int mMulti;
+ private String mSupportDocument;
+ private String mCapability;
+ private ArrayList<Integer> mBundleProductIds;
+ private final ArrayList<PlanFeaturesHighlightSection> mFeaturesHighlightSections = new ArrayList<>();
+
+ // used to link with an actual product on the Store (ie: Used to load the price from the Store).
+ private String mAndroidSKU;
+
+ // Info attached to the current site/user
+ private int mRawDiscount;
+ private String mFormattedDiscount;
+ private boolean mIsCurrentPlan;
+ private boolean mCanStartTrial;
+ private String mExpiry;
+ private boolean mFreeTrial;
+ private String mUserFacingExpiry;
+ private String mSubscribedDate;
+ private String mBundleSubscriptionID;
+
+
+ public Plan(JSONObject planJSONObject) throws JSONException {
+ mProductID = planJSONObject.getLong("product_id");
+ mProductName = planJSONObject.getString("product_name");
+
+ // Unfold prices object
+ JSONObject priceJSONObject = planJSONObject.getJSONObject("prices");
+ JSONArray priceKeys = priceJSONObject.names();
+ if (priceKeys != null) {
+ for (int i=0; i < priceKeys.length(); i ++) {
+ String currentKey = priceKeys.getString(i);
+ int currentPrice = priceJSONObject.getInt(currentKey);
+ mPrices.put(currentKey, currentPrice);
+ }
+ }
+
+ mProductNameShort = planJSONObject.getString("product_name_short");
+ mProductSlug = planJSONObject.getString("product_slug");
+ mTagline = planJSONObject.getString("tagline");
+ mDescription = planJSONObject.getString("description");
+ mCost = planJSONObject.getLong("cost");
+ mBillPeriod = planJSONObject.getInt("bill_period");
+ mProductType = planJSONObject.getString("product_type");
+ mIsAvailable = JSONUtils.getBool(planJSONObject, "available");
+ mBillPeriodLabel = planJSONObject.getString("bill_period_label");
+ mPrice = planJSONObject.getString("price");
+ mFormattedPrice = planJSONObject.getString("formatted_price");
+ mRawPrice = planJSONObject.getInt("raw_price");
+ mIconUrl = planJSONObject.optString("icon");
+ mAndroidSKU = planJSONObject.optString("android_sku");
+
+ // Optionals
+ mWidth = planJSONObject.optInt("width");
+ mHeight = planJSONObject.optInt("height");
+ mSaving = planJSONObject.optLong("saving", 0L);
+ mOriginal = planJSONObject.optLong("original", mCost);
+ mFormattedOriginalPrice = planJSONObject.optString("formatted_original_price");
+ mSupportDocument = planJSONObject.optString("support_document");
+ mCapability = planJSONObject.optString("capability");
+ mStore = planJSONObject.optInt("store");
+ mMulti = planJSONObject.optInt("multi");
+
+ if (planJSONObject.has("bundle_product_ids")) {
+ JSONArray bundleIDS = planJSONObject.getJSONArray("bundle_product_ids");
+ mBundleProductIds = new ArrayList<>(bundleIDS.length());
+ for (int i=0; i < bundleIDS.length(); i ++) {
+ int currentBundleID = bundleIDS.getInt(i);
+ mBundleProductIds.add(currentBundleID);
+ }
+ }
+
+ if (planJSONObject.has("features_highlight")) {
+ JSONArray featuresHighlightSections = planJSONObject.getJSONArray("features_highlight");
+ for (int i=0; i < featuresHighlightSections.length(); i++) {
+ mFeaturesHighlightSections.add(
+ new PlanFeaturesHighlightSection(featuresHighlightSections.getJSONObject(i))
+ );
+ }
+ }
+
+ // Specific info liked with the current site
+ mRawDiscount = planJSONObject.optInt("raw_discount", 0);
+ mFormattedDiscount = planJSONObject.optString("formatted_discount");
+ mCanStartTrial = JSONUtils.getBool(planJSONObject, "can_start_trial");
+ mIsCurrentPlan = JSONUtils.getBool(planJSONObject, "current_plan");
+ mExpiry = planJSONObject.optString("expiry");
+ mUserFacingExpiry = planJSONObject.optString("user_facing_expiry");
+ mSubscribedDate = planJSONObject.optString("subscribed_date");
+ mFreeTrial = JSONUtils.getBool(planJSONObject, "free_trial");
+ mBundleSubscriptionID = planJSONObject.optString("bundle_subscription_id");
+ }
+
+
+ public long getProductID() {
+ return mProductID;
+ }
+
+ public String getProductName() {
+ return StringUtils.notNullStr(mProductName);
+ }
+
+ public Hashtable<String, Integer> getPrices() {
+ return mPrices;
+ }
+
+ public String getProductNameShort() {
+ return StringUtils.notNullStr(mProductNameShort);
+ }
+
+ public String getProductSlug() {
+ return StringUtils.notNullStr(mProductSlug);
+ }
+
+ public String getTagline() {
+ return StringUtils.notNullStr(mTagline);
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(mDescription);
+ }
+
+ public long getCost() {
+ return mCost;
+ }
+
+ public int getBillPeriod() {
+ return mBillPeriod;
+ }
+
+ public String getProductType() {
+ return StringUtils.notNullStr(mProductType);
+ }
+
+ public boolean isAvailable() {
+ return mIsAvailable;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public String getBillPeriodLabel() {
+ return StringUtils.notNullStr(mBillPeriodLabel);
+ }
+
+ public String getPrice() {
+ return StringUtils.notNullStr(mPrice);
+ }
+
+ public String getFormattedPrice() {
+ return StringUtils.notNullStr(mFormattedPrice);
+ }
+
+ public int getRawPrice() {
+ return mRawPrice;
+ }
+
+ public ArrayList<PlanFeaturesHighlightSection> getFeaturesHighlightSections() {
+ return mFeaturesHighlightSections;
+ }
+
+ public List<Integer> getBundleProductIds() {
+ return mBundleProductIds;
+ }
+
+ public String getCapability() {
+ return StringUtils.notNullStr(mCapability);
+ }
+
+ public String getSupportDocument() {
+ return StringUtils.notNullStr(mSupportDocument);
+ }
+
+ public int getMulti() {
+ return mMulti;
+ }
+
+ public int getStore() {
+ return mStore;
+ }
+
+ public String getFormattedOriginalPrice() {
+ return StringUtils.notNullStr(mFormattedOriginalPrice);
+ }
+
+ public long getSaving() {
+ return mSaving;
+ }
+
+ public long getOriginal() {
+ return mOriginal;
+ }
+
+ public String getIconUrl() {
+ return StringUtils.notNullStr(mIconUrl);
+ }
+ public void setIconUrl(String iconUrl) {
+ mIconUrl = StringUtils.notNullStr(iconUrl);
+ }
+ public boolean hasIconUrl() {
+ return !TextUtils.isEmpty(mIconUrl);
+ }
+
+ public int getRawDiscount() {
+ return mRawDiscount;
+ }
+
+ public String getFormattedDiscount() {
+ return StringUtils.notNullStr(mFormattedDiscount);
+ }
+
+
+ public boolean isCurrentPlan() {
+ return mIsCurrentPlan;
+ }
+
+ public boolean canStartTrial() {
+ return mCanStartTrial;
+ }
+
+ public String getSubscribedDate() {
+ return StringUtils.notNullStr(mSubscribedDate);
+ }
+
+ public String getUserFacingExpiry() {
+ return StringUtils.notNullStr(mUserFacingExpiry);
+ }
+
+ public boolean isFreeTrial() {
+ return mFreeTrial;
+ }
+
+ public String getExpiry() {
+ return StringUtils.notNullStr(mExpiry);
+ }
+
+ public String getBundleSubscriptionID() {
+ return StringUtils.notNullStr(mBundleSubscriptionID);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/models/PlanFeaturesHighlightSection.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/PlanFeaturesHighlightSection.java
new file mode 100644
index 000000000..d74e568de
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/PlanFeaturesHighlightSection.java
@@ -0,0 +1,53 @@
+package org.wordpress.android.ui.plans.models;
+
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/*
+Each single Plan has a list of features sections to highlight on the plan details screen. This class model
+a single section to highlight.
+ "features_highlight": [
+ {
+ "items": [
+ "custom-design",
+ "videopress",
+ "support",
+ "space",
+ "domain_map",
+ "no-adverts\/no-adverts.php"
+ ]
+ },
+ {
+ "title": "Included with all plans",
+ "items": [
+ "free-blog"
+ ]
+ }
+ ],
+ */
+public class PlanFeaturesHighlightSection implements Serializable {
+ private final String mTitle; // title (if available) of this section
+ private ArrayList<String> mItems; // slug of the features to highlight in this section
+
+ PlanFeaturesHighlightSection(JSONObject featureSection) throws JSONException{
+ mTitle = featureSection.optString("title");
+ JSONArray items = featureSection.getJSONArray("items");
+ mItems = new ArrayList<>(items.length());
+ for (int i=0; i < items.length(); i++) {
+ mItems.add(items.getString(i));
+ }
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public ArrayList<String> getFeatures() {
+ return mItems;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/AddCategoryActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/AddCategoryActivity.java
new file mode 100644
index 000000000..f706f0cb6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/AddCategoryActivity.java
@@ -0,0 +1,110 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.CategoryNode;
+
+import java.util.ArrayList;
+
+public class AddCategoryActivity extends AppCompatActivity {
+ private int id;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.add_category);
+
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ id = extras.getInt("id");
+ }
+ loadCategories();
+
+ final Button cancelButton = (Button) findViewById(R.id.cancel);
+ final Button okButton = (Button) findViewById(R.id.ok);
+
+ okButton.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ EditText categoryNameET = (EditText) findViewById(R.id.category_name);
+ String category_name = categoryNameET.getText().toString();
+ EditText categorySlugET = (EditText) findViewById(R.id.category_slug);
+ String category_slug = categorySlugET.getText().toString();
+ EditText categoryDescET = (EditText) findViewById(R.id.category_desc);
+ String category_desc = categoryDescET.getText().toString();
+ Spinner sCategories = (Spinner) findViewById(R.id.parent_category);
+ String parent_category = "";
+ if (sCategories.getSelectedItem() != null)
+ parent_category = ((CategoryNode) sCategories.getSelectedItem()).getName().trim();
+ int parent_id = 0;
+ if (sCategories.getSelectedItemPosition() != 0) {
+ parent_id = WordPress.wpDB.getCategoryId(id, parent_category);
+ }
+
+ if (category_name.replaceAll(" ", "").equals("")) {
+ // Name field cannot be empty
+
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(AddCategoryActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.required_field));
+ dialogBuilder.setMessage(getResources().getText(R.string.cat_name_required));
+ dialogBuilder.setPositiveButton("OK", new
+ DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // Just close the window.
+
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ } else {
+ Bundle bundle = new Bundle();
+
+ bundle.putString("category_name", category_name);
+ bundle.putString("category_slug", category_slug);
+ bundle.putString("category_desc", category_desc);
+ bundle.putInt("parent_id", parent_id);
+ bundle.putString("continue", "TRUE");
+ Intent mIntent = new Intent();
+ mIntent.putExtras(bundle);
+ setResult(RESULT_OK, mIntent);
+ finish();
+ }
+
+ }
+ });
+
+ cancelButton.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ Bundle bundle = new Bundle();
+
+ bundle.putString("continue", "FALSE");
+ Intent mIntent = new Intent();
+ mIntent.putExtras(bundle);
+ setResult(RESULT_OK, mIntent);
+ finish();
+ }
+ });
+ }
+
+ private void loadCategories() {
+ CategoryNode rootCategory = CategoryNode.createCategoryTreeFromDB(id);
+ ArrayList<CategoryNode> categoryLevels = CategoryNode.getSortedListOfCategoriesFromRoot(rootCategory);
+ categoryLevels.add(0, new CategoryNode(0, 0, getString(R.string.none)));
+ if (categoryLevels.size() > 0) {
+ ParentCategorySpinnerAdapter categoryAdapter = new ParentCategorySpinnerAdapter(this,
+ R.layout.categories_row_parent, categoryLevels);
+ Spinner sCategories = (Spinner) findViewById(R.id.parent_category);
+ sCategories.setAdapter(categoryAdapter);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/CategoryArrayAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/CategoryArrayAdapter.java
new file mode 100644
index 000000000..eb3cfd350
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/CategoryArrayAdapter.java
@@ -0,0 +1,41 @@
+package org.wordpress.android.ui.posts;
+
+import android.content.Context;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import org.wordpress.android.R;
+import org.wordpress.android.models.CategoryNode;
+
+import java.util.List;
+
+public class CategoryArrayAdapter extends ArrayAdapter<CategoryNode> {
+ int mResourceId;
+
+ public CategoryArrayAdapter(Context context, int resource, List<CategoryNode> objects) {
+ super(context, resource, objects);
+ mResourceId = resource;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rowView = inflater.inflate(mResourceId, parent, false);
+ TextView textView = (TextView) rowView.findViewById(R.id.categoryRowText);
+ ImageView levelIndicatorView = (ImageView) rowView.findViewById(R.id.categoryRowLevelIndicator);
+ textView.setText(Html.fromHtml(getItem(position).getName()));
+ int level = getItem(position).getLevel();
+ if (level == 1) { // hide ImageView
+ levelIndicatorView.setVisibility(View.GONE);
+ } else {
+ ViewGroup.LayoutParams params = levelIndicatorView.getLayoutParams();
+ params.width = (params.width / 2) * level;
+ levelIndicatorView.setLayoutParams(params);
+ }
+ return rowView;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java
new file mode 100644
index 000000000..dc8761fcc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java
@@ -0,0 +1,2224 @@
+package org.wordpress.android.ui.posts;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.SuggestionSpan;
+import android.view.ContextMenu;
+import android.view.DragEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.URLUtil;
+import android.widget.Toast;
+
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.Constants;
+import org.wordpress.android.JavaScriptException;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.editor.EditorFragment;
+import org.wordpress.android.editor.EditorFragmentAbstract;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorDragAndDropListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent;
+import org.wordpress.android.editor.EditorMediaUploadListener;
+import org.wordpress.android.editor.EditorWebViewAbstract.ErrorListener;
+import org.wordpress.android.editor.EditorWebViewCompatibility;
+import org.wordpress.android.editor.EditorWebViewCompatibility.ReflectionException;
+import org.wordpress.android.editor.ImageSettingsDialogFragment;
+import org.wordpress.android.editor.LegacyEditorFragment;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.MediaUploadState;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.media.MediaGalleryActivity;
+import org.wordpress.android.ui.media.MediaGalleryPickerActivity;
+import org.wordpress.android.ui.media.MediaGridFragment;
+import org.wordpress.android.ui.media.MediaPickerActivity;
+import org.wordpress.android.ui.media.MediaSourceWPImages;
+import org.wordpress.android.ui.media.MediaSourceWPVideos;
+import org.wordpress.android.ui.media.WordPressMediaUtils;
+import org.wordpress.android.ui.media.services.MediaEvents;
+import org.wordpress.android.ui.media.services.MediaUploadService;
+import org.wordpress.android.ui.posts.services.PostUploadService;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.prefs.SiteSettingsInterface;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.AutolinkUtils;
+import org.wordpress.android.util.CrashlyticsUtils;
+import org.wordpress.android.util.CrashlyticsUtils.ExceptionType;
+import org.wordpress.android.util.DeviceUtils;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.PermissionUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.WPHtml;
+import org.wordpress.android.util.WPUrlUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+import org.wordpress.android.util.helpers.MediaGalleryImageSpan;
+import org.wordpress.android.util.helpers.WPImageSpan;
+import org.wordpress.android.widgets.WPViewPager;
+import org.wordpress.mediapicker.MediaItem;
+import org.wordpress.mediapicker.source.MediaSource;
+import org.wordpress.mediapicker.source.MediaSourceDeviceImages;
+import org.wordpress.mediapicker.source.MediaSourceDeviceVideos;
+import org.wordpress.passcodelock.AppLockManager;
+import org.xmlrpc.android.ApiHelper;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.greenrobot.event.EventBus;
+
+public class EditPostActivity extends AppCompatActivity implements EditorFragmentListener, EditorDragAndDropListener,
+ ActivityCompat.OnRequestPermissionsResultCallback, EditorWebViewCompatibility.ReflectionFailureListener {
+ public static final String EXTRA_POSTID = "postId";
+ public static final String EXTRA_IS_PAGE = "isPage";
+ public static final String EXTRA_IS_NEW_POST = "isNewPost";
+ public static final String EXTRA_IS_QUICKPRESS = "isQuickPress";
+ public static final String EXTRA_QUICKPRESS_BLOG_ID = "quickPressBlogId";
+ public static final String EXTRA_SAVED_AS_LOCAL_DRAFT = "savedAsLocalDraft";
+ public static final String STATE_KEY_CURRENT_POST = "stateKeyCurrentPost";
+ public static final String STATE_KEY_ORIGINAL_POST = "stateKeyOriginalPost";
+ public static final String STATE_KEY_EDITOR_FRAGMENT = "editorFragment";
+ public static final String STATE_KEY_DROPPED_MEDIA_URIS = "stateKeyDroppedMediaUri";
+
+ // Context menu positioning
+ private static final int SELECT_PHOTO_MENU_POSITION = 0;
+ private static final int CAPTURE_PHOTO_MENU_POSITION = 1;
+ private static final int SELECT_VIDEO_MENU_POSITION = 2;
+ private static final int CAPTURE_VIDEO_MENU_POSITION = 3;
+ private static final int ADD_GALLERY_MENU_POSITION = 4;
+ private static final int SELECT_LIBRARY_MENU_POSITION = 5;
+ private static final int NEW_PICKER_MENU_POSITION = 6;
+
+ public static final int MEDIA_PERMISSION_REQUEST_CODE = 1;
+ public static final int LOCATION_PERMISSION_REQUEST_CODE = 2;
+ public static final int DRAG_AND_DROP_MEDIA_PERMISSION_REQUEST_CODE = 3;
+
+ private static int PAGE_CONTENT = 0;
+ private static int PAGE_SETTINGS = 1;
+ private static int PAGE_PREVIEW = 2;
+
+ private static final int AUTOSAVE_INTERVAL_MILLIS = 60000;
+
+ private Handler mHandler;
+ private boolean mShowNewEditor;
+
+ // Each element is a list of media IDs being uploaded to a gallery, keyed by gallery ID
+ private Map<Long, List<String>> mPendingGalleryUploads = new HashMap<>();
+
+ // -1=no response yet, 0=unavailable, 1=available
+ private int mBlogMediaStatus = -1;
+ private boolean mMediaUploadServiceStarted;
+ private List<String> mPendingVideoPressInfoRequests;
+
+ /**
+ * The {@link android.support.v4.view.PagerAdapter} that will provide
+ * fragments for each of the sections. We use a
+ * {@link FragmentPagerAdapter} derivative, which will keep every
+ * loaded fragment in memory. If this becomes too memory intensive, it
+ * may be best to switch to a
+ * {@link android.support.v13.app.FragmentStatePagerAdapter}.
+ */
+ SectionsPagerAdapter mSectionsPagerAdapter;
+
+ /**
+ * The {@link ViewPager} that will host the section contents.
+ */
+ WPViewPager mViewPager;
+
+ private Post mPost;
+ private Post mOriginalPost;
+
+ private EditorFragmentAbstract mEditorFragment;
+ private EditPostSettingsFragment mEditPostSettingsFragment;
+ private EditPostPreviewFragment mEditPostPreviewFragment;
+
+ private EditorMediaUploadListener mEditorMediaUploadListener;
+
+ private boolean mIsNewPost;
+ private boolean mIsPage;
+ private boolean mHasSetPostContent;
+
+ // For opening the context menu after permissions have been granted
+ private View mMenuView = null;
+
+ // for keeping the media uri while asking for permissions
+ private ArrayList<Uri> mDroppedMediaUris;
+
+ private Runnable mFetchMediaRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mDroppedMediaUris != null) {
+ final List<Uri> mediaUris = mDroppedMediaUris;
+ mDroppedMediaUris = null;
+
+ fetchMedia(mediaUris);
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.new_edit_post_activity);
+
+ // Check whether to show the visual editor
+ PreferenceManager.setDefaultValues(this, R.xml.account_settings, false);
+ mShowNewEditor = AppPrefs.isVisualEditorEnabled();
+
+ // Set up the action bar.
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ Bundle extras = getIntent().getExtras();
+ String action = getIntent().getAction();
+ if (savedInstanceState == null) {
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)
+ || NEW_MEDIA_GALLERY.equals(action)
+ || NEW_MEDIA_POST.equals(action)
+ || getIntent().hasExtra(EXTRA_IS_QUICKPRESS)
+ || (extras != null && extras.getInt("quick-media", -1) > -1)) {
+ if (getIntent().hasExtra(EXTRA_QUICKPRESS_BLOG_ID)) {
+ // QuickPress might want to use a different blog than the current blog
+ int blogId = getIntent().getIntExtra(EXTRA_QUICKPRESS_BLOG_ID, -1);
+ Blog quickPressBlog = WordPress.wpDB.instantiateBlogByLocalId(blogId);
+ if (quickPressBlog == null) {
+ showErrorAndFinish(R.string.blog_not_found);
+ return;
+ }
+ if (quickPressBlog.isHidden()) {
+ showErrorAndFinish(R.string.error_blog_hidden);
+ return;
+ }
+ WordPress.currentBlog = quickPressBlog;
+ }
+
+ // Create a new post for share intents and QuickPress
+ mPost = new Post(WordPress.getCurrentLocalTableBlogId(), false);
+ mPost.setCategories("[" + SiteSettingsInterface.getDefaultCategory(this) + "]");
+ mPost.setPostFormat(SiteSettingsInterface.getDefaultFormat(this));
+ WordPress.wpDB.savePost(mPost);
+ mIsNewPost = true;
+ } else if (extras != null) {
+ // Load post from the postId passed in extras
+ long localTablePostId = extras.getLong(EXTRA_POSTID, -1);
+ mIsPage = extras.getBoolean(EXTRA_IS_PAGE);
+ mIsNewPost = extras.getBoolean(EXTRA_IS_NEW_POST);
+ mPost = WordPress.wpDB.getPostForLocalTablePostId(localTablePostId);
+ mOriginalPost = WordPress.wpDB.getPostForLocalTablePostId(localTablePostId);
+ } else {
+ // A postId extra must be passed to this activity
+ showErrorAndFinish(R.string.post_not_found);
+ return;
+ }
+ } else {
+ mDroppedMediaUris = savedInstanceState.getParcelable(STATE_KEY_DROPPED_MEDIA_URIS);
+
+ if (savedInstanceState.containsKey(STATE_KEY_ORIGINAL_POST)) {
+ try {
+ mPost = (Post) savedInstanceState.getSerializable(STATE_KEY_CURRENT_POST);
+ mOriginalPost = (Post) savedInstanceState.getSerializable(STATE_KEY_ORIGINAL_POST);
+ } catch (ClassCastException e) {
+ mPost = null;
+ }
+ }
+ mEditorFragment = (EditorFragmentAbstract) fragmentManager.getFragment(savedInstanceState, STATE_KEY_EDITOR_FRAGMENT);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ mEditorMediaUploadListener = (EditorMediaUploadListener) mEditorFragment;
+ }
+ }
+
+ if (mHasSetPostContent = mEditorFragment != null) {
+ mEditorFragment.setImageLoader(WordPress.imageLoader);
+ }
+
+ // Ensure we have a valid blog
+ if (WordPress.getCurrentBlog() == null) {
+ showErrorAndFinish(R.string.blog_not_found);
+ return;
+ }
+
+ // Ensure we have a valid post
+ if (mPost == null) {
+ showErrorAndFinish(R.string.post_not_found);
+ return;
+ }
+
+ if (mIsNewPost) {
+ trackEditorCreatedPost(action, getIntent());
+ }
+
+ setTitle(StringUtils.unescapeHTML(WordPress.getCurrentBlog().getBlogName()));
+
+ mSectionsPagerAdapter = new SectionsPagerAdapter(fragmentManager);
+
+ // Set up the ViewPager with the sections adapter.
+ mViewPager = (WPViewPager) findViewById(R.id.pager);
+ mViewPager.setAdapter(mSectionsPagerAdapter);
+ mViewPager.setOffscreenPageLimit(2);
+ mViewPager.setPagingEnabled(false);
+
+ // When swiping between different sections, select the corresponding
+ // tab. We can also use ActionBar.Tab#select() to do this if we have
+ // a reference to the Tab.
+ mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ invalidateOptionsMenu();
+ if (position == PAGE_CONTENT) {
+ setTitle(StringUtils.unescapeHTML(WordPress.getCurrentBlog().getBlogName()));
+ } else if (position == PAGE_SETTINGS) {
+ setTitle(mPost.isPage() ? R.string.page_settings : R.string.post_settings);
+ } else if (position == PAGE_PREVIEW) {
+ setTitle(mPost.isPage() ? R.string.preview_page : R.string.preview_post);
+ savePostAsync(new AfterSavePostListener() {
+ @Override
+ public void onPostSave() {
+ if (mEditPostPreviewFragment != null) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mEditPostPreviewFragment != null) {
+ mEditPostPreviewFragment.loadPost();
+ }
+ }
+ });
+ }
+ }
+ });
+ }
+ }
+ });
+
+ ActivityId.trackLastActivity(ActivityId.POST_EDITOR);
+ }
+
+ private Runnable mAutoSave = new Runnable() {
+ @Override
+ public void run() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ updatePostObject(true);
+ savePostToDb();
+ if (mHandler != null) {
+ mHandler.postDelayed(mAutoSave, AUTOSAVE_INTERVAL_MILLIS);
+ }
+ }
+ }).start();
+ }
+ };
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mHandler = new Handler();
+ mHandler.postDelayed(mAutoSave, AUTOSAVE_INTERVAL_MILLIS);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ try {
+ unregisterReceiver(mGalleryReceiver);
+ } catch (IllegalArgumentException e) {
+ AppLog.d(T.EDITOR, "Illegal state! Can't unregister receiver that was no registered");
+ }
+
+ stopMediaUploadService();
+ mHandler.removeCallbacks(mAutoSave);
+ mHandler = null;
+ }
+
+ @Override
+ protected void onDestroy() {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.EDITOR_CLOSED);
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ // Saves both post objects so we can restore them in onCreate()
+ savePostAsync(null);
+ outState.putSerializable(STATE_KEY_CURRENT_POST, mPost);
+ outState.putSerializable(STATE_KEY_ORIGINAL_POST, mOriginalPost);
+
+ outState.putParcelableArrayList(STATE_KEY_DROPPED_MEDIA_URIS, mDroppedMediaUris);
+
+ if (mEditorFragment != null) {
+ getFragmentManager().putFragment(outState, STATE_KEY_EDITOR_FRAGMENT, mEditorFragment);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ if (mShowNewEditor) {
+ inflater.inflate(R.menu.edit_post, menu);
+ } else {
+ inflater.inflate(R.menu.edit_post_legacy, menu);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ boolean showMenuItems = true;
+ if (mViewPager != null && mViewPager.getCurrentItem() > PAGE_CONTENT) {
+ showMenuItems = false;
+ }
+
+ MenuItem previewMenuItem = menu.findItem(R.id.menu_preview_post);
+ MenuItem settingsMenuItem = menu.findItem(R.id.menu_post_settings);
+
+ if (previewMenuItem != null) {
+ previewMenuItem.setVisible(showMenuItems);
+ }
+
+ if (settingsMenuItem != null) {
+ settingsMenuItem.setVisible(showMenuItems);
+ }
+
+ // Set text of the save button in the ActionBar
+ if (mPost != null) {
+ MenuItem saveMenuItem = menu.findItem(R.id.menu_save_post);
+ if (saveMenuItem != null) {
+ switch (mPost.getStatusEnum()) {
+ case SCHEDULED:
+ saveMenuItem.setTitle(getString(R.string.schedule_verb));
+ break;
+ case PUBLISHED:
+ case UNKNOWN:
+ if (mPost.isLocalDraft()) {
+ saveMenuItem.setTitle(R.string.publish_post);
+ } else {
+ saveMenuItem.setTitle(R.string.update_verb);
+ }
+ break;
+ default:
+ if (mPost.isLocalDraft()) {
+ saveMenuItem.setTitle(R.string.save);
+ } else {
+ saveMenuItem.setTitle(R.string.update_verb);
+ }
+ }
+ }
+ }
+
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String permissions[],
+ @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case LOCATION_PERMISSION_REQUEST_CODE:
+ boolean shouldShowLocation = false;
+ // Check if at least one of the location permission (coarse or fine) is granted
+ for (int grantResult : grantResults) {
+ if (grantResult == PackageManager.PERMISSION_GRANTED) {
+ shouldShowLocation = true;
+ }
+ }
+ if (shouldShowLocation) {
+ // Permission request was granted, show Location buttons in Settings
+ mEditPostSettingsFragment.showLocationSearch();
+
+ // After permission request was granted add GeoTag to the new post (if GeoTagging is enabled)
+ if (SiteSettingsInterface.getGeotagging(this) && isNewPost()) {
+ mEditPostSettingsFragment.searchLocation();
+ }
+
+ return;
+ }
+ // Location permission denied
+ ToastUtils.showToast(this, getString(R.string.add_location_permission_required));
+ break;
+ case MEDIA_PERMISSION_REQUEST_CODE:
+ boolean shouldShowContextMenu = true;
+ for (int i = 0; i < grantResults.length; ++i) {
+ switch (permissions[i]) {
+ case Manifest.permission.CAMERA:
+ if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
+ shouldShowContextMenu = false;
+ }
+ break;
+ case Manifest.permission.WRITE_EXTERNAL_STORAGE:
+ if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
+ shouldShowContextMenu = false;
+ } else {
+ registerReceiver(mGalleryReceiver,
+ new IntentFilter(LegacyEditorFragment.ACTION_MEDIA_GALLERY_TOUCHED));
+ refreshBlogMedia();
+ }
+ break;
+ }
+ }
+ if (shouldShowContextMenu) {
+ if (mMenuView != null) {
+ super.openContextMenu(mMenuView);
+ mMenuView = null;
+ }
+ } else {
+ ToastUtils.showToast(this, getString(R.string.access_media_permission_required));
+ }
+ break;
+ case DRAG_AND_DROP_MEDIA_PERMISSION_REQUEST_CODE:
+ boolean mediaAccessGranted = false;
+ for (int i = 0; i < grantResults.length; ++i) {
+ switch (permissions[i]) {
+ case Manifest.permission.WRITE_EXTERNAL_STORAGE:
+ if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ mediaAccessGranted = true;
+ }
+ break;
+ }
+ }
+ if (mediaAccessGranted) {
+ runOnUiThread(mFetchMediaRunnable);
+ } else {
+ ToastUtils.showToast(this, getString(R.string.access_media_permission_required));
+ }
+ default:
+ break;
+ }
+ }
+
+ // Menu actions
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int itemId = item.getItemId();
+
+ if (itemId == android.R.id.home) {
+ Fragment fragment = getFragmentManager().findFragmentByTag(
+ ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG);
+ if (fragment != null && fragment.isVisible()) {
+ return false;
+ }
+ if (mViewPager.getCurrentItem() > PAGE_CONTENT) {
+ if (mViewPager.getCurrentItem() == PAGE_SETTINGS) {
+ mPost.setFeaturedImageId(mEditPostSettingsFragment.getFeaturedImageId());
+ mEditorFragment.setFeaturedImageId(mPost.getFeaturedImageId());
+ }
+ mViewPager.setCurrentItem(PAGE_CONTENT);
+ invalidateOptionsMenu();
+ } else {
+ saveAndFinish();
+ }
+ return true;
+ }
+
+ MediaUploadService mediaUploadService = MediaUploadService.getInstance();
+
+ // Disable format bar buttons while a media upload is in progress
+ if ((mediaUploadService != null && mediaUploadService.hasUploads()) || mEditorFragment.isUploadingMedia() ||
+ mEditorFragment.isActionInProgress()) {
+ ToastUtils.showToast(this, R.string.editor_toast_uploading_please_wait, Duration.SHORT);
+ return false;
+ }
+
+ if (itemId == R.id.menu_save_post) {
+ return publishPost();
+ } else if (itemId == R.id.menu_preview_post) {
+ mViewPager.setCurrentItem(PAGE_PREVIEW);
+ } else if (itemId == R.id.menu_post_settings) {
+ InputMethodManager imm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE));
+ imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
+ if (mShowNewEditor) {
+ mEditPostSettingsFragment.updateFeaturedImage(mPost.getFeaturedImageId());
+ }
+ mViewPager.setCurrentItem(PAGE_SETTINGS);
+ }
+ return false;
+ }
+
+ private boolean publishPost() {
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ ToastUtils.showToast(this, R.string.error_publish_no_network, Duration.SHORT);
+ return false;
+ }
+
+ // Show an Alert Dialog asking the user if he wants to remove all failed media before upload
+ if (mEditorFragment.hasFailedMediaUploads()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.editor_toast_failed_uploads)
+ .setPositiveButton(R.string.editor_remove_failed_uploads, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // Clear failed uploads
+ mEditorFragment.removeAllFailedMediaUploads();
+ }
+ }).setNegativeButton(android.R.string.cancel, null);
+ builder.create().show();
+ return true;
+ }
+
+ // Update post, save to db and publish in its own Thread, because 1. update can be pretty slow with a lot of
+ // text 2. better not to call `updatePostObject()` from the UI thread due to weird thread blocking behavior
+ // on API 16 with the visual editor.
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ updatePostObject(false);
+ savePostToDb();
+
+ // If the post is empty, don't publish
+ if (!mPost.isPublishable()) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ToastUtils.showToast(EditPostActivity.this, R.string.error_publish_empty_post, Duration.SHORT);
+ }
+ });
+ return;
+ }
+
+ PostUtils.trackSavePostAnalytics(mPost);
+
+ PostUploadService.addPostToUpload(mPost);
+ PostUploadService.setLegacyMode(!mShowNewEditor);
+ startService(new Intent(EditPostActivity.this, PostUploadService.class));
+ setResult(RESULT_OK);
+ finish();
+ }
+ }).start();
+ return true;
+ }
+
+ @Override
+ public void openContextMenu(View view) {
+ if (PermissionUtils.checkAndRequestCameraAndStoragePermissions(this, MEDIA_PERMISSION_REQUEST_CODE)) {
+ super.openContextMenu(view);
+ } else {
+ mMenuView = view;
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ menu.add(0, SELECT_PHOTO_MENU_POSITION, 0, getResources().getText(R.string.select_photo));
+ if (DeviceUtils.getInstance().hasCamera(this)) {
+ menu.add(0, CAPTURE_PHOTO_MENU_POSITION, 0, getResources().getText(R.string.media_add_popup_capture_photo));
+ }
+ menu.add(0, SELECT_VIDEO_MENU_POSITION, 0, getResources().getText(R.string.select_video));
+ if (DeviceUtils.getInstance().hasCamera(this)) {
+ menu.add(0, CAPTURE_VIDEO_MENU_POSITION, 0, getResources().getText(R.string.media_add_popup_capture_video));
+ }
+
+ menu.add(0, ADD_GALLERY_MENU_POSITION, 0, getResources().getText(R.string.media_add_new_media_gallery));
+ menu.add(0, SELECT_LIBRARY_MENU_POSITION, 0, getResources().getText(R.string.select_from_media_library));
+ menu.add(0, NEW_PICKER_MENU_POSITION, 0, getResources().getText(R.string.select_from_new_picker));
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case SELECT_PHOTO_MENU_POSITION:
+ launchPictureLibrary();
+ return true;
+ case CAPTURE_PHOTO_MENU_POSITION:
+ launchCamera();
+ return true;
+ case SELECT_VIDEO_MENU_POSITION:
+ launchVideoLibrary();
+ return true;
+ case CAPTURE_VIDEO_MENU_POSITION:
+ launchVideoCamera();
+ return true;
+ case ADD_GALLERY_MENU_POSITION:
+ startMediaGalleryActivity(null);
+ return true;
+ case SELECT_LIBRARY_MENU_POSITION:
+ startMediaGalleryAddActivity();
+ return true;
+ case NEW_PICKER_MENU_POSITION:
+ startMediaSelection();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void launchPictureLibrary() {
+ WordPressMediaUtils.launchPictureLibrary(this);
+ AppLockManager.getInstance().setExtendedTimeout();
+ }
+
+ private void launchVideoLibrary() {
+ WordPressMediaUtils.launchVideoLibrary(this);
+ AppLockManager.getInstance().setExtendedTimeout();
+ }
+
+ private void launchVideoCamera() {
+ WordPressMediaUtils.launchVideoCamera(this);
+ AppLockManager.getInstance().setExtendedTimeout();
+ }
+
+ private void showErrorAndFinish(int errorMessageId) {
+ Toast.makeText(this, getResources().getText(errorMessageId), Toast.LENGTH_LONG).show();
+ finish();
+ }
+
+ public Post getPost() {
+ return mPost;
+ }
+
+ private void trackEditorCreatedPost(String action, Intent intent) {
+ Map<String, Object> properties = new HashMap<String, Object>();
+ // Post created from the post list (new post button).
+ String normalizedSourceName = "post-list";
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ // Post created with share with WordPress
+ normalizedSourceName = "shared-from-external-app";
+ }
+ if (EditPostActivity.NEW_MEDIA_GALLERY.equals(action) || EditPostActivity.NEW_MEDIA_POST.equals(
+ action)) {
+ // Post created from the media library
+ normalizedSourceName = "media-library";
+ }
+ if (intent != null && intent.hasExtra(EXTRA_IS_QUICKPRESS)) {
+ // Quick press
+ normalizedSourceName = "quick-press";
+ }
+ if (intent != null && intent.getIntExtra("quick-media", -1) > -1) {
+ // Quick photo or quick video
+ normalizedSourceName = "quick-media";
+ }
+ properties.put("created_post_source", normalizedSourceName);
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.EDITOR_CREATED_POST,
+ WordPress.getBlog(mPost.getLocalTableBlogId()),
+ properties
+ );
+ }
+
+ private synchronized void updatePostObject(boolean isAutosave) {
+ if (mPost == null) {
+ AppLog.e(AppLog.T.POSTS, "Attempted to save an invalid Post.");
+ return;
+ }
+
+ // Update post object from fragment fields
+ if (mEditorFragment != null) {
+ if (mShowNewEditor) {
+ updatePostContentNewEditor(isAutosave, (String) mEditorFragment.getTitle(),
+ (String) mEditorFragment.getContent());
+ } else {
+ // TODO: Remove when legacy editor is dropped
+ updatePostContent(isAutosave);
+ }
+ }
+
+ if (mEditPostSettingsFragment != null) {
+ mEditPostSettingsFragment.updatePostSettings();
+ }
+ }
+
+ private void savePostAsync(final AfterSavePostListener listener) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ updatePostObject(false);
+ savePostToDb();
+ if (listener != null) {
+ listener.onPostSave();
+ }
+ }
+ }).start();
+ }
+
+ private interface AfterSavePostListener {
+ void onPostSave();
+ }
+
+ private synchronized void savePostToDb() {
+ WordPress.wpDB.updatePost(mPost);
+ }
+
+ @Override
+ public void onBackPressed() {
+ Fragment imageSettingsFragment = getFragmentManager().findFragmentByTag(
+ ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG);
+ if (imageSettingsFragment != null && imageSettingsFragment.isVisible()) {
+ ((ImageSettingsDialogFragment) imageSettingsFragment).dismissFragment();
+ return;
+ }
+
+ if (mViewPager.getCurrentItem() > PAGE_CONTENT) {
+ if (mViewPager.getCurrentItem() == PAGE_SETTINGS) {
+ mPost.setFeaturedImageId(mEditPostSettingsFragment.getFeaturedImageId());
+ mEditorFragment.setFeaturedImageId(mPost.getFeaturedImageId());
+ }
+ mViewPager.setCurrentItem(PAGE_CONTENT);
+ invalidateOptionsMenu();
+ return;
+ }
+
+ if (mEditorFragment != null && !mEditorFragment.onBackPressed()) {
+ saveAndFinish();
+ }
+ }
+
+ public boolean isNewPost() {
+ return mIsNewPost;
+ }
+
+ private class SaveAndFinishTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ // Fetch post title and content from editor fields and update the Post object
+ updatePostObject(false);
+
+ if (mEditorFragment != null && mPost.hasEmptyContentFields()) {
+ // new and empty post? delete it
+ if (mIsNewPost) {
+ WordPress.wpDB.deletePost(mPost);
+ return false;
+ }
+ } else if (mOriginalPost != null && !mPost.hasChanges(mOriginalPost)) {
+ // if no changes have been made to the post, set it back to the original don't save it
+ WordPress.wpDB.updatePost(mOriginalPost);
+ return false;
+ } else {
+ // changes have been made, save the post and ask for the post list to refresh.
+ // We consider this being "manual save", it will replace some Android "spans" by an html
+ // or a shortcode replacement (for instance for images and galleries)
+ if (mShowNewEditor) {
+ // Update the post object directly, without re-fetching the fields from the EditorFragment
+ updatePostContentNewEditor(false, mPost.getTitle(), mPost.getContent());
+ savePostToDb();
+ } else {
+ updatePostObject(false);
+ savePostToDb();
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean saved) {
+ if (saved) {
+ Intent i = new Intent();
+ i.putExtra(EXTRA_SAVED_AS_LOCAL_DRAFT, true);
+ i.putExtra(EXTRA_IS_PAGE, mIsPage);
+ setResult(RESULT_OK, i);
+ ToastUtils.showToast(EditPostActivity.this, R.string.editor_toast_changes_saved);
+ }
+ finish();
+ }
+ }
+
+ private void saveAndFinish() {
+ new SaveAndFinishTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ /**
+ * Disable visual editor mode and log the exception if we get a Reflection failure when the webview is being
+ * initialized.
+ */
+ @Override
+ public void onReflectionFailure(ReflectionException e) {
+ CrashlyticsUtils.logException(e, ExceptionType.SPECIFIC, T.EDITOR, "Reflection Failure on Visual Editor init");
+ // Disable visual editor and show an error message
+ AppPrefs.setVisualEditorEnabled(false);
+ ToastUtils.showToast(this, R.string.new_editor_reflection_error, Duration.LONG);
+ // Restart the activity (will start the legacy editor)
+ finish();
+ startActivity(getIntent());
+ }
+
+ /**
+ * A {@link FragmentPagerAdapter} that returns a fragment corresponding to
+ * one of the sections/tabs/pages.
+ */
+ public class SectionsPagerAdapter extends FragmentPagerAdapter {
+ // Show two pages for the visual editor, and add a third page for the EditPostPreviewFragment for legacy
+ private static final int NUM_PAGES_VISUAL_EDITOR = 2;
+ private static final int NUM_PAGES_LEGACY_EDITOR = 3;
+
+ public SectionsPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ // getItem is called to instantiate the fragment for the given page.
+ switch (position) {
+ case 0:
+ // TODO: switch between legacy and new editor here (AB test?)
+ if (mShowNewEditor) {
+ EditorWebViewCompatibility.setReflectionFailureListener(EditPostActivity.this);
+ return new EditorFragment();
+ } else {
+ return new LegacyEditorFragment();
+ }
+ case 1:
+ return new EditPostSettingsFragment();
+ default:
+ return new EditPostPreviewFragment();
+ }
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ Fragment fragment = (Fragment) super.instantiateItem(container, position);
+ switch (position) {
+ case 0:
+ mEditorFragment = (EditorFragmentAbstract) fragment;
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ mEditorMediaUploadListener = (EditorMediaUploadListener) mEditorFragment;
+
+ // Set up custom headers for the visual editor's internal WebView
+ mEditorFragment.setCustomHttpHeader("User-Agent", WordPress.getUserAgent());
+ }
+ break;
+ case 1:
+ mEditPostSettingsFragment = (EditPostSettingsFragment) fragment;
+ break;
+ case 2:
+ mEditPostPreviewFragment = (EditPostPreviewFragment) fragment;
+ break;
+ }
+ return fragment;
+ }
+
+ @Override
+ public int getCount() {
+ return (mShowNewEditor ? NUM_PAGES_VISUAL_EDITOR : NUM_PAGES_LEGACY_EDITOR);
+ }
+ }
+
+ public boolean isEditingPostContent() {
+ return (mViewPager.getCurrentItem() == PAGE_CONTENT);
+ }
+
+ // Moved from EditPostContentFragment
+ public static final String NEW_MEDIA_GALLERY = "NEW_MEDIA_GALLERY";
+ public static final String NEW_MEDIA_GALLERY_EXTRA_IDS = "NEW_MEDIA_GALLERY_EXTRA_IDS";
+ public static final String NEW_MEDIA_POST = "NEW_MEDIA_POST";
+ public static final String NEW_MEDIA_POST_EXTRA = "NEW_MEDIA_POST_ID";
+ private String mMediaCapturePath = "";
+ private int mMaxThumbWidth = 0;
+
+ private int getMaximumThumbnailWidthForEditor() {
+ if (mMaxThumbWidth == 0) {
+ mMaxThumbWidth = ImageUtils.getMaximumThumbnailWidthForEditor(this);
+ }
+ return mMaxThumbWidth;
+ }
+
+ private MediaFile createMediaFile(String blogId, final String mediaId) {
+ Cursor cursor = WordPress.wpDB.getMediaFile(blogId, mediaId);
+
+ if (cursor == null || !cursor.moveToFirst()) {
+ if (cursor != null) {
+ cursor.close();
+ }
+ return null;
+ }
+
+ String url = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL));
+ if (url == null) {
+ cursor.close();
+ return null;
+ }
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setMediaId(mediaId);
+ mediaFile.setBlogId(blogId);
+ mediaFile.setFileURL(url);
+ mediaFile.setCaption(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_CAPTION)));
+ mediaFile.setDescription(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DESCRIPTION)));
+ mediaFile.setTitle(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE)));
+ mediaFile.setWidth(cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH)));
+ mediaFile.setHeight(cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT)));
+ mediaFile.setFileName(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME)));
+ mediaFile.setDateCreatedGMT(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT)));
+ mediaFile.setVideoPressShortCode(cursor.getString(cursor.getColumnIndex(
+ WordPressDB.COLUMN_NAME_VIDEO_PRESS_SHORTCODE)));
+
+ String mimeType = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MIME_TYPE));
+ mediaFile.setMimeType(mimeType);
+
+ if (mimeType != null && !mimeType.isEmpty()) {
+ mediaFile.setVideo(mimeType.contains("video"));
+ } else {
+ mediaFile.setVideo(MediaUtils.isVideo(url));
+ }
+
+ // Make sure we're using a valid thumbnail for video. XML-RPC returns the video URL itself as the thumbnail URL
+ // for videos. If we can't get a real thumbnail for the Media Library video (currently only possible for
+ // VideoPress videos), we should not set any thumbnail.
+ String thumbnailUrl = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL));
+ if (mediaFile.isVideo() && !MediaUtils.isValidImage(thumbnailUrl)) {
+ if (WPUrlUtils.isWordPressCom(url)) {
+ thumbnailUrl = WordPressMediaUtils.getVideoPressVideoPosterFromURL(url);
+ } else {
+ thumbnailUrl = "";
+ }
+ }
+ mediaFile.setThumbnailURL(thumbnailUrl);
+
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ cursor.close();
+ return mediaFile;
+ }
+
+ private void addExistingMediaToEditor(String mediaId) {
+ if (WordPress.getCurrentBlog() == null) {
+ return;
+ }
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ MediaFile mediaFile = createMediaFile(blogId, mediaId);
+ if (mediaFile == null) {
+ return;
+ }
+ trackAddMediaEvents(mediaFile.isVideo(), true);
+ mEditorFragment.appendMediaFile(mediaFile, getMediaUrl(mediaFile), WordPress.imageLoader);
+ }
+
+ /**
+ * Get media url from a MediaFile, returns a photon URL if the selected blog is Photon capable.
+ */
+ private String getMediaUrl(MediaFile mediaFile) {
+ if (mediaFile == null) {
+ return null;
+ }
+
+ // Since Photon doesn't support video, skip Photon checking and return the existing file URL
+ // (using a Photon URL for video will result in a 404 error)
+ if (mediaFile.isVideo()) {
+ return mediaFile.getFileURL();
+ }
+
+ String imageURL;
+ if (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isPhotonCapable()) {
+ String photonUrl = mediaFile.getFileURL();
+ imageURL = StringUtils.getPhotonUrl(photonUrl, getMaximumThumbnailWidthForEditor());
+ } else {
+ // Not a Jetpack or wpcom blog
+ // imageURL = mediaFile.getThumbnailURL(); // do not use fileURL here since downloading picture
+ // of big dimensions can result in OOM Exception
+ imageURL = mediaFile.getFileURL() != null ? mediaFile.getFileURL() : mediaFile.getThumbnailURL();
+ }
+ return imageURL;
+ }
+
+ private class LoadPostContentTask extends AsyncTask<String, Spanned, Spanned> {
+ @Override
+ protected Spanned doInBackground(String... params) {
+ if (params.length < 1 || getPost() == null) {
+ return null;
+ }
+
+ String content = StringUtils.notNullStr(params[0]);
+ return WPHtml.fromHtml(content, EditPostActivity.this, getPost(), getMaximumThumbnailWidthForEditor());
+ }
+
+ @Override
+ protected void onPostExecute(Spanned spanned) {
+ if (spanned != null) {
+ mEditorFragment.setContent(spanned);
+ }
+ }
+ }
+
+ private String getUploadErrorHtml(String mediaId, String path) {
+ String replacement;
+ if (Build.VERSION.SDK_INT >= 19) {
+ replacement = String.format(Locale.US,
+ "<span id=\"img_container_%s\" class=\"img_container failed\" data-failed=\"%s\"><progress " +
+ "id=\"progress_%s\" value=\"0\" class=\"wp_media_indicator failed\" contenteditable=\"false\">" +
+ "</progress><img data-wpid=\"%s\" src=\"%s\" alt=\"\" class=\"failed\"></span>",
+ mediaId, getString(R.string.tap_to_try_again), mediaId, mediaId, path);
+ } else {
+ // Before API 19, the WebView didn't support progress tags. Use an upload overlay instead of a progress bar
+ replacement = String.format(Locale.US,
+ "<span id=\"img_container_%s\" class=\"img_container compat failed\" contenteditable=\"false\" " +
+ "data-failed=\"%s\"><span class=\"upload-overlay failed\" " +
+ "contenteditable=\"false\">Uploading…</span><span class=\"upload-overlay-bg\"></span>" +
+ "<img data-wpid=\"%s\" src=\"%s\" alt=\"\" class=\"failed\"></span>",
+ mediaId, getString(R.string.tap_to_try_again), mediaId, path);
+ }
+ return replacement;
+ }
+
+ private String migrateLegacyDraft(String content) {
+ if (content.contains("<img src=\"null\" android-uri=\"")) {
+ // We must replace image tags specific to the legacy editor local drafts:
+ // <img src="null" android-uri="file:///..." />
+ // And trigger an upload action for the specific image / video
+ Pattern pattern = Pattern.compile("<img src=\"null\" android-uri=\"([^\"]*)\".*>");
+ Matcher matcher = pattern.matcher(content);
+ StringBuffer stringBuffer = new StringBuffer();
+ while (matcher.find()) {
+ String path = null;
+ String stringUri = matcher.group(1);
+ Uri uri = Uri.parse(stringUri);
+ if (uri != null && stringUri.contains("content:")) {
+ path = getPathFromContentUri(uri);
+ if (path == null) {
+ continue;
+ }
+ } else {
+ path = stringUri.replace("file://", "");
+ }
+ MediaFile mediaFile = queueFileForUpload(path, null, "failed");
+ if (mediaFile == null) {
+ continue;
+ }
+ String replacement = getUploadErrorHtml(mediaFile.getMediaId(), mediaFile.getFilePath());
+ matcher.appendReplacement(stringBuffer, replacement);
+ }
+ matcher.appendTail(stringBuffer);
+ content = stringBuffer.toString();
+ }
+ if (content.contains("[caption")) {
+ // Convert old legacy post caption formatting to new format, to avoid being stripped by the visual editor
+ Pattern pattern = Pattern.compile("(\\[caption[^]]*caption=\"([^\"]*)\"[^]]*].+?)(\\[\\/caption])");
+ Matcher matcher = pattern.matcher(content);
+ StringBuffer stringBuffer = new StringBuffer();
+ while (matcher.find()) {
+ String replacement = matcher.group(1) + matcher.group(2) + matcher.group(3);
+ matcher.appendReplacement(stringBuffer, replacement);
+ }
+ matcher.appendTail(stringBuffer);
+ content = stringBuffer.toString();
+ }
+ return content;
+ }
+
+ private void fillContentEditorFields() {
+ // Needed blog settings needed by the editor
+ if (WordPress.getCurrentBlog() != null) {
+ mEditorFragment.setFeaturedImageSupported(WordPress.getCurrentBlog().isFeaturedImageCapable());
+ mEditorFragment.setBlogSettingMaxImageWidth(WordPress.getCurrentBlog().getMaxImageWidth());
+ }
+
+ // Set up the placeholder text
+ mEditorFragment.setContentPlaceholder(getString(R.string.editor_content_placeholder));
+ mEditorFragment.setTitlePlaceholder(getString(mIsPage ? R.string.editor_page_title_placeholder :
+ R.string.editor_post_title_placeholder));
+
+ // Set post title and content
+ Post post = getPost();
+ if (post != null) {
+ if (!TextUtils.isEmpty(post.getContent()) && !mHasSetPostContent) {
+ mHasSetPostContent = true;
+ if (post.isLocalDraft() && !mShowNewEditor) {
+ // TODO: Unnecessary for new editor, as all images are uploaded right away, even for local drafts
+ // Load local post content in the background, as it may take time to generate images
+ new LoadPostContentTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ post.getContent().replaceAll("\uFFFC", ""));
+ } else {
+ // TODO: Might be able to drop .replaceAll() when legacy editor is removed
+ String content = post.getContent().replaceAll("\uFFFC", "");
+ // Prepare eventual legacy editor local draft for the new editor
+ content = migrateLegacyDraft(content);
+ mEditorFragment.setContent(content);
+ }
+ }
+ if (!TextUtils.isEmpty(post.getTitle())) {
+ mEditorFragment.setTitle(post.getTitle());
+ }
+ // TODO: postSettingsButton.setText(post.isPage() ? R.string.page_settings : R.string.post_settings);
+ mEditorFragment.setLocalDraft(post.isLocalDraft());
+
+ mEditorFragment.setFeaturedImageId(mPost.getFeaturedImageId());
+ }
+
+ // Special actions
+ String action = getIntent().getAction();
+ int quickMediaType = getIntent().getIntExtra("quick-media", -1);
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ setPostContentFromShareAction();
+ } else if (NEW_MEDIA_GALLERY.equals(action)) {
+ prepareMediaGallery();
+ } else if (NEW_MEDIA_POST.equals(action)) {
+ prepareMediaPost();
+ } else if (quickMediaType >= 0) {
+ // User selected 'Quick Photo' in the menu drawer
+ if (quickMediaType == Constants.QUICK_POST_PHOTO_CAMERA) {
+ launchCamera();
+ } else if (quickMediaType == Constants.QUICK_POST_PHOTO_LIBRARY) {
+ WordPressMediaUtils.launchPictureLibrary(this);
+ }
+ if (post != null) {
+ post.setQuickPostType(Post.QUICK_MEDIA_TYPE_PHOTO);
+ }
+ }
+ }
+
+ private void launchCamera() {
+ WordPressMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID,
+ new WordPressMediaUtils.LaunchCameraCallback() {
+ @Override
+ public void onMediaCapturePathReady(String mediaCapturePath) {
+ mMediaCapturePath = mediaCapturePath;
+ AppLockManager.getInstance().setExtendedTimeout();
+ }
+ });
+ }
+
+ protected void setPostContentFromShareAction() {
+ Intent intent = getIntent();
+
+ // Check for shared text
+ String text = intent.getStringExtra(Intent.EXTRA_TEXT);
+ String title = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ if (text != null) {
+ if (title != null) {
+ mEditorFragment.setTitle(title);
+ }
+ // Create an <a href> element around links
+ text = AutolinkUtils.autoCreateLinks(text);
+ if (mEditorFragment instanceof EditorFragment) {
+ mEditorFragment.setContent(text);
+ } else {
+ mEditorFragment.setContent(WPHtml.fromHtml(StringUtils.addPTags(text), this, getPost(),
+ getMaximumThumbnailWidthForEditor()));
+ }
+ }
+
+ // Check for shared media
+ if (intent.hasExtra(Intent.EXTRA_STREAM)) {
+ String action = intent.getAction();
+ String type = intent.getType();
+ ArrayList<Uri> sharedUris;
+
+ if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ sharedUris = intent.getParcelableArrayListExtra((Intent.EXTRA_STREAM));
+ } else {
+ // For a single media share, we only allow images and video types
+ if (type != null && (type.startsWith("image") || type.startsWith("video"))) {
+ sharedUris = new ArrayList<Uri>();
+ sharedUris.add((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM));
+ } else {
+ return;
+ }
+ }
+
+ if (sharedUris != null) {
+ for (Uri uri : sharedUris) {
+ addMedia(uri);
+ }
+ }
+ }
+ }
+
+ private void startMediaGalleryActivity(MediaGallery mediaGallery) {
+ Intent intent = new Intent(this, MediaGalleryActivity.class);
+ intent.putExtra(MediaGalleryActivity.PARAMS_MEDIA_GALLERY, mediaGallery);
+ if (mediaGallery == null) {
+ intent.putExtra(MediaGalleryActivity.PARAMS_LAUNCH_PICKER, true);
+ }
+ startActivityForResult(intent, MediaGalleryActivity.REQUEST_CODE);
+ }
+
+ private void prepareMediaGallery() {
+ MediaGallery mediaGallery = new MediaGallery();
+ mediaGallery.setIds(getIntent().getStringArrayListExtra(NEW_MEDIA_GALLERY_EXTRA_IDS));
+ startMediaGalleryActivity(mediaGallery);
+ }
+
+ private void prepareMediaPost() {
+ String mediaId = getIntent().getStringExtra(NEW_MEDIA_POST_EXTRA);
+ addExistingMediaToEditor(mediaId);
+ }
+
+ // TODO: Replace with contents of the updatePostContentNewEditor() method when legacy editor is dropped
+ /**
+ * Updates post object with content of this fragment
+ */
+ public void updatePostContent(boolean isAutoSave) {
+ Post post = getPost();
+
+ if (post == null) {
+ return;
+ }
+ String title = StringUtils.notNullStr((String) mEditorFragment.getTitle());
+ SpannableStringBuilder postContent;
+ if (mEditorFragment.getSpannedContent() != null) {
+ // needed by the legacy editor to save local drafts
+ try {
+ postContent = new SpannableStringBuilder(mEditorFragment.getSpannedContent());
+ } catch (IndexOutOfBoundsException e) {
+ // A core android bug might cause an out of bounds exception, if so we'll just use the current editable
+ // See https://code.google.com/p/android/issues/detail?id=5164
+ postContent = new SpannableStringBuilder(StringUtils.notNullStr((String) mEditorFragment.getContent()));
+ }
+ } else {
+ postContent = new SpannableStringBuilder(StringUtils.notNullStr((String) mEditorFragment.getContent()));
+ }
+
+ String content;
+ if (post.isLocalDraft()) {
+ // remove suggestion spans, they cause craziness in WPHtml.toHTML().
+ CharacterStyle[] characterStyles = postContent.getSpans(0, postContent.length(), CharacterStyle.class);
+ for (CharacterStyle characterStyle : characterStyles) {
+ if (characterStyle instanceof SuggestionSpan) {
+ postContent.removeSpan(characterStyle);
+ }
+ }
+ content = WPHtml.toHtml(postContent);
+ // replace duplicate <p> tags so there's not duplicates, trac #86
+ content = content.replace("<p><p>", "<p>");
+ content = content.replace("</p></p>", "</p>");
+ content = content.replace("<br><br>", "<br>");
+ // sometimes the editor creates extra tags
+ content = content.replace("</strong><strong>", "").replace("</em><em>", "").replace("</u><u>", "")
+ .replace("</strike><strike>", "").replace("</blockquote><blockquote>", "");
+ } else {
+ if (!isAutoSave) {
+ // Add gallery shortcode
+ MediaGalleryImageSpan[] gallerySpans = postContent.getSpans(0, postContent.length(),
+ MediaGalleryImageSpan.class);
+ for (MediaGalleryImageSpan gallerySpan : gallerySpans) {
+ int start = postContent.getSpanStart(gallerySpan);
+ postContent.removeSpan(gallerySpan);
+ postContent.insert(start, WPHtml.getGalleryShortcode(gallerySpan));
+ }
+ }
+
+ WPImageSpan[] imageSpans = postContent.getSpans(0, postContent.length(), WPImageSpan.class);
+ if (imageSpans.length != 0) {
+ for (WPImageSpan wpIS : imageSpans) {
+ MediaFile mediaFile = wpIS.getMediaFile();
+ if (mediaFile == null)
+ continue;
+ if (mediaFile.getMediaId() != null) {
+ updateMediaFileOnServer(wpIS);
+ } else {
+ mediaFile.setFileName(wpIS.getImageSource().toString());
+ mediaFile.setFilePath(wpIS.getImageSource().toString());
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ }
+
+ int tagStart = postContent.getSpanStart(wpIS);
+ if (!isAutoSave) {
+ postContent.removeSpan(wpIS);
+
+ // network image has a mediaId
+ if (mediaFile.getMediaId() != null && mediaFile.getMediaId().length() > 0) {
+ postContent.insert(tagStart, WPHtml.getContent(wpIS));
+ } else {
+ // local image for upload
+ postContent.insert(tagStart,
+ "<img android-uri=\"" + wpIS.getImageSource().toString() + "\" />");
+ }
+ }
+ }
+ }
+ content = postContent.toString();
+ }
+
+ String moreTag = "<!--more-->";
+
+ post.setTitle(title);
+ // split up the post content if there's a more tag
+ if (post.isLocalDraft() && content.contains(moreTag)) {
+ post.setDescription(content.substring(0, content.indexOf(moreTag)));
+ post.setMoreText(content.substring(content.indexOf(moreTag) + moreTag.length(), content.length()));
+ } else {
+ post.setDescription(content);
+ post.setMoreText("");
+ }
+
+ if (!post.isLocalDraft()) {
+ post.setLocalChange(true);
+ }
+ }
+
+ /**
+ * Updates post object with given title and content
+ */
+ public void updatePostContentNewEditor(boolean isAutoSave, String title, String content) {
+ Post post = getPost();
+
+ if (post == null) {
+ return;
+ }
+
+ if (!isAutoSave) {
+ // TODO: Shortcode handling, media handling
+ }
+
+ String moreTag = "<!--more-->";
+
+ post.setTitle(title);
+ // split up the post content if there's a more tag
+ if (post.isLocalDraft() && content.contains(moreTag)) {
+ post.setDescription(content.substring(0, content.indexOf(moreTag)));
+ post.setMoreText(content.substring(content.indexOf(moreTag) + moreTag.length(), content.length()));
+ } else {
+ post.setDescription(content);
+ post.setMoreText("");
+ }
+
+ if (!post.isLocalDraft()) {
+ post.setLocalChange(true);
+ }
+ }
+
+ /**
+ * Media
+ */
+
+ private void fetchMedia(List<Uri> mediaUris) {
+ for (Uri mediaUri : mediaUris) {
+ if (mediaUri == null) {
+ Toast.makeText(EditPostActivity.this,
+ getResources().getText(R.string.gallery_error), Toast.LENGTH_SHORT).show();
+ continue;
+ }
+
+ if (!addMedia(mediaUri)) {
+ Toast.makeText(EditPostActivity.this, getResources().getText(R.string.gallery_error),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private void updateMediaFileOnServer(WPImageSpan wpIS) {
+ Blog currentBlog = WordPress.getCurrentBlog();
+ if (currentBlog == null || wpIS == null)
+ return;
+
+ MediaFile mf = wpIS.getMediaFile();
+
+ final String mediaId = mf.getMediaId();
+ final String title = mf.getTitle();
+ final String description = mf.getDescription();
+ final String caption = mf.getCaption();
+
+ ApiHelper.EditMediaItemTask task = new ApiHelper.EditMediaItemTask(mf.getMediaId(), mf.getTitle(),
+ mf.getDescription(), mf.getCaption(),
+ new ApiHelper.GenericCallback() {
+ @Override
+ public void onSuccess() {
+ if (WordPress.getCurrentBlog() == null) {
+ return;
+ }
+ String localBlogTableIndex = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ WordPress.wpDB.updateMediaFile(localBlogTableIndex, mediaId, title, description, caption);
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ Toast.makeText(EditPostActivity.this, R.string.media_edit_failure, Toast.LENGTH_LONG).show();
+ }
+ });
+
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(currentBlog);
+ task.execute(apiArgs);
+ }
+
+ private void trackAddMediaEvents(boolean isVideo, boolean fromMediaLibrary) {
+ if (isVideo) {
+ AnalyticsTracker.track(fromMediaLibrary ? Stat.EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY
+ : Stat.EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY);
+ } else {
+ AnalyticsTracker.track(fromMediaLibrary ? Stat.EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY
+ : Stat.EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY);
+ }
+ }
+
+ private boolean addMedia(Uri mediaUri) {
+ if (mediaUri != null && !MediaUtils.isInMediaStore(mediaUri) && !mediaUri.toString().startsWith("/")) {
+ mediaUri = MediaUtils.downloadExternalMedia(this, mediaUri);
+ }
+
+ if (mediaUri == null) {
+ return false;
+ }
+
+ boolean isVideo = MediaUtils.isVideo(mediaUri.toString());
+ trackAddMediaEvents(isVideo, false);
+
+ if (mShowNewEditor) {
+ // TODO: add video param
+ return addMediaVisualEditor(mediaUri);
+ } else {
+ return addMediaLegacyEditor(mediaUri, isVideo);
+ }
+ }
+
+ private String getPathFromContentUri(Uri imageUri) {
+ String path = null;
+ String[] projection = new String[]{MediaStore.Images.Media.DATA};
+ Cursor cur = getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ path = cur.getString(dataColumn);
+ cur.close();
+ }
+ return path;
+ }
+
+ private boolean addMediaVisualEditor(Uri imageUri) {
+ String path = "";
+ if (imageUri.toString().contains("content:")) {
+ path = getPathFromContentUri(imageUri);
+ } else {
+ // File is not in media library
+ path = imageUri.toString().replace("file://", "");
+ }
+
+ if (path == null) {
+ ToastUtils.showToast(this, R.string.file_not_found, Duration.SHORT);
+ return false;
+ }
+
+ Blog blog = WordPress.getCurrentBlog();
+ if (MediaUtils.getImageWidthSettingFromString(blog.getMaxImageWidth()) != Integer.MAX_VALUE) {
+ // If the user has selected a maximum image width for uploads, rescale the image accordingly
+ path = ImageUtils.createResizedImageWithMaxWidth(this, path, Integer.parseInt(blog.getMaxImageWidth()));
+ }
+
+ MediaFile mediaFile = queueFileForUpload(path, new ArrayList<String>());
+ if (mediaFile != null) {
+ mEditorFragment.appendMediaFile(mediaFile, path, WordPress.imageLoader);
+ }
+
+ return true;
+ }
+
+ private boolean addMediaLegacyEditor(Uri mediaUri, boolean isVideo) {
+ String mediaTitle;
+ if (isVideo) {
+ mediaTitle = getResources().getString(R.string.video);
+ } else {
+ mediaTitle = ImageUtils.getTitleForWPImageSpan(this, mediaUri.getEncodedPath());
+ }
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setPostID(getPost().getLocalTablePostId());
+ mediaFile.setTitle(mediaTitle);
+ mediaFile.setFilePath(mediaUri.toString());
+ if (mediaUri.getEncodedPath() != null) {
+ mediaFile.setVideo(isVideo);
+ }
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ mEditorFragment.appendMediaFile(mediaFile, mediaFile.getFilePath(), WordPress.imageLoader);
+ return true;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (data != null || ((requestCode == RequestCodes.TAKE_PHOTO ||
+ requestCode == RequestCodes.TAKE_VIDEO))) {
+ switch (requestCode) {
+ case MediaPickerActivity.ACTIVITY_REQUEST_CODE_MEDIA_SELECTION:
+ if (resultCode == MediaPickerActivity.ACTIVITY_RESULT_CODE_MEDIA_SELECTED) {
+ handleMediaSelectionResult(data);
+ } else if (resultCode == MediaPickerActivity.ACTIVITY_RESULT_CODE_GALLERY_CREATED) {
+ handleGalleryResult(data);
+ }
+ break;
+ case MediaGalleryActivity.REQUEST_CODE:
+ if (resultCode == Activity.RESULT_OK) {
+ handleMediaGalleryResult(data);
+ }
+ break;
+ case MediaGalleryPickerActivity.REQUEST_CODE:
+ if (resultCode == Activity.RESULT_OK) {
+ handleMediaGalleryPickerResult(data);
+ }
+ break;
+ case RequestCodes.PICTURE_LIBRARY:
+ Uri imageUri = data.getData();
+ fetchMedia(Arrays.asList(imageUri));
+ break;
+ case RequestCodes.TAKE_PHOTO:
+ if (resultCode == Activity.RESULT_OK) {
+ try {
+ File f = new File(mMediaCapturePath);
+ Uri capturedImageUri = Uri.fromFile(f);
+ if (!addMedia(capturedImageUri)) {
+ ToastUtils.showToast(this, R.string.gallery_error, Duration.SHORT);
+ }
+ this.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"
+ + Environment.getExternalStorageDirectory())));
+ } catch (RuntimeException e) {
+ AppLog.e(T.POSTS, e);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(T.POSTS, e);
+ }
+ } else if (TextUtils.isEmpty(mEditorFragment.getContent())) {
+ // TODO: check if it was mQuickMediaType > -1
+ // Quick Photo was cancelled, delete post and finish activity
+ WordPress.wpDB.deletePost(getPost());
+ finish();
+ }
+ break;
+ case RequestCodes.VIDEO_LIBRARY:
+ Uri videoUri = data.getData();
+ fetchMedia(Arrays.asList(videoUri));
+ break;
+ case RequestCodes.TAKE_VIDEO:
+ if (resultCode == Activity.RESULT_OK) {
+ Uri capturedVideoUri = MediaUtils.getLastRecordedVideoUri(this);
+ if (!addMedia(capturedVideoUri)) {
+ ToastUtils.showToast(this, R.string.gallery_error, Duration.SHORT);
+ }
+ } else if (TextUtils.isEmpty(mEditorFragment.getContent())) {
+ // TODO: check if it was mQuickMediaType > -1
+ // Quick Photo was cancelled, delete post and finish activity
+ WordPress.wpDB.deletePost(getPost());
+ finish();
+ }
+ break;
+ }
+ }
+ }
+
+ private void startMediaGalleryAddActivity() {
+ Intent intent = new Intent(this, MediaGalleryPickerActivity.class);
+ intent.putExtra(MediaGalleryPickerActivity.PARAM_SELECT_ONE_ITEM, true);
+ startActivityForResult(intent, MediaGalleryPickerActivity.REQUEST_CODE);
+ }
+
+ private void handleMediaGalleryPickerResult(Intent data) {
+ ArrayList<String> ids = data.getStringArrayListExtra(MediaGalleryPickerActivity.RESULT_IDS);
+ if (ids == null || ids.size() == 0) {
+ return;
+ }
+
+ String mediaId = ids.get(0);
+ addExistingMediaToEditor(mediaId);
+ }
+
+ private void handleMediaGalleryResult(Intent data) {
+ MediaGallery gallery = (MediaGallery) data.getSerializableExtra(MediaGalleryActivity.RESULT_MEDIA_GALLERY);
+
+ // if blank gallery returned, don't add to span
+ if (gallery == null || gallery.getIds().size() == 0) {
+ return;
+ }
+ mEditorFragment.appendGallery(gallery);
+ }
+
+ /**
+ * Handles result from {@link org.wordpress.android.ui.media.MediaPickerActivity}. Uploads local
+ * media to users blog then adds a gallery to the Post with all the selected media.
+ *
+ * @param data
+ * contains the selected media content with key
+ * {@link org.wordpress.android.ui.media.MediaPickerActivity#SELECTED_CONTENT_RESULTS_KEY}
+ */
+ private void handleGalleryResult(Intent data) {
+ if (data != null) {
+ List<MediaItem> selectedContent = data.getParcelableArrayListExtra(MediaPickerActivity.SELECTED_CONTENT_RESULTS_KEY);
+
+ if (selectedContent != null && selectedContent.size() > 0) {
+ ArrayList<String> blogMediaIds = new ArrayList<>();
+ ArrayList<String> localMediaIds = new ArrayList<>();
+
+ for (MediaItem content : selectedContent) {
+ Uri source = content.getSource();
+ final String id = content.getTag();
+
+ if (source != null && id != null) {
+ final String sourceString = source.toString();
+
+ if (MediaUtils.isVideo(sourceString)) {
+ // Videos cannot be added to a gallery, insert inline instead
+ addMedia(source);
+ } else if (URLUtil.isNetworkUrl(sourceString)) {
+ blogMediaIds.add(id);
+ } else if (MediaUtils.isValidImage(sourceString)) {
+ queueFileForUpload(sourceString, localMediaIds);
+ }
+ }
+ }
+
+ MediaGallery gallery = new MediaGallery();
+ gallery.setIds(blogMediaIds);
+
+ if (localMediaIds.size() > 0) {
+ NotificationManager notificationManager = (NotificationManager) getSystemService(
+ Context.NOTIFICATION_SERVICE);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext());
+ builder.setSmallIcon(android.R.drawable.stat_sys_upload);
+ builder.setContentTitle("Uploading gallery");
+ notificationManager.notify(10, builder.build());
+
+ mPendingGalleryUploads.put(gallery.getUniqueId(), new ArrayList<>(localMediaIds));
+ }
+
+ // Only insert gallery span if images were added
+ if (localMediaIds.size() > 0 || blogMediaIds.size() > 0) {
+ mEditorFragment.appendGallery(gallery);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles result from {@link org.wordpress.android.ui.media.MediaPickerActivity} by adding the
+ * selected media to the Post.
+ *
+ * @param data
+ * result {@link android.content.Intent} with selected media items
+ */
+ private void handleMediaSelectionResult(Intent data) {
+ if (data == null) {
+ return;
+ }
+ final List<MediaItem> selectedContent =
+ data.getParcelableArrayListExtra(MediaPickerActivity.SELECTED_CONTENT_RESULTS_KEY);
+ if (selectedContent != null && selectedContent.size() > 0) {
+ for (MediaItem media : selectedContent) {
+ if (URLUtil.isNetworkUrl(media.getSource().toString())) {
+ addExistingMediaToEditor(media.getTag());
+ } else {
+ addMedia(media.getSource());
+ }
+ }
+ }
+ }
+
+ /**
+ * Create image {@link org.wordpress.mediapicker.source.MediaSource}'s for media selection.
+ *
+ * @return
+ * list containing all sources to gather image media from
+ */
+ private ArrayList<MediaSource> imageMediaSelectionSources() {
+ ArrayList<MediaSource> imageMediaSources = new ArrayList<>();
+ imageMediaSources.add(new MediaSourceDeviceImages());
+
+ return imageMediaSources;
+ }
+
+ private ArrayList<MediaSource> blogImageMediaSelectionSources() {
+ ArrayList<MediaSource> imageMediaSources = new ArrayList<>();
+ imageMediaSources.add(new MediaSourceWPImages());
+
+ return imageMediaSources;
+ }
+
+ private ArrayList<MediaSource> blogVideoMediaSelectionSources() {
+ ArrayList<MediaSource> imageMediaSources = new ArrayList<>();
+ imageMediaSources.add(new MediaSourceWPVideos());
+
+ return imageMediaSources;
+ }
+
+ /**
+ * Create video {@link org.wordpress.mediapicker.source.MediaSource}'s for media selection.
+ *
+ * @return
+ * list containing all sources to gather video media from
+ */
+ private ArrayList<MediaSource> videoMediaSelectionSources() {
+ ArrayList<MediaSource> videoMediaSources = new ArrayList<>();
+ videoMediaSources.add(new MediaSourceDeviceVideos());
+
+ return videoMediaSources;
+ }
+
+ private BroadcastReceiver mGalleryReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (LegacyEditorFragment.ACTION_MEDIA_GALLERY_TOUCHED.equals(intent.getAction())) {
+ startMediaGalleryActivity((MediaGallery)intent.getSerializableExtra(LegacyEditorFragment.EXTRA_MEDIA_GALLERY));
+ }
+ }
+ };
+
+ /**
+ * Handles media upload notifications. Used by the visual editor when uploading local media, and for both
+ * the visual and the legacy editor to create a gallery after media selection from local media.
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(MediaEvents.MediaUploadSucceeded event) {
+ for (Long galleryId : mPendingGalleryUploads.keySet()) {
+ if (mPendingGalleryUploads.get(galleryId).contains(event.mLocalMediaId)) {
+ if (mEditorMediaUploadListener != null) {
+ // Notify the visual editor of gallery image upload
+ int remaining = mPendingGalleryUploads.get(galleryId).size() - 1;
+ mEditorMediaUploadListener.onGalleryMediaUploadSucceeded(galleryId, event.mRemoteMediaId, remaining);
+ } else {
+ handleGalleryImageUploadedLegacyEditor(galleryId, event.mLocalMediaId, event.mRemoteMediaId);
+ }
+
+ mPendingGalleryUploads.get(galleryId).remove(event.mLocalMediaId);
+ if (mPendingGalleryUploads.get(galleryId).size() == 0) {
+ mPendingGalleryUploads.remove(galleryId);
+ }
+
+ if (mPendingGalleryUploads.size() == 0) {
+ stopMediaUploadService();
+ NotificationManager notificationManager = (NotificationManager) getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(10);
+ }
+
+ return;
+ }
+ }
+
+ // Notify visual editor that a normal media item has finished uploading (not part of a gallery)
+ if (mEditorMediaUploadListener != null) {
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setPostID(getPost().getLocalTablePostId());
+ mediaFile.setMediaId(event.mRemoteMediaId);
+ mediaFile.setFileURL(event.mRemoteMediaUrl);
+ mediaFile.setVideoPressShortCode(event.mSecondaryRemoteMediaId);
+ mediaFile.setThumbnailURL(WordPressMediaUtils.getVideoPressVideoPosterFromURL(event.mRemoteMediaUrl));
+
+ mEditorMediaUploadListener.onMediaUploadSucceeded(event.mLocalMediaId, mediaFile);
+ }
+ }
+
+ public void onEventMainThread(MediaEvents.MediaUploadFailed event) {
+ AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_FAILED);
+ if (mEditorMediaUploadListener != null) {
+ if (event.mIsGenericMessage) {
+ mEditorMediaUploadListener.onMediaUploadFailed(event.mLocalMediaId, getString(R.string.tap_to_try_again));
+ } else {
+ mEditorMediaUploadListener.onMediaUploadFailed(event.mLocalMediaId, event.mErrorMessage);
+ }
+ }
+ }
+
+ public void onEventMainThread(MediaEvents.MediaUploadProgress event) {
+ if (mEditorMediaUploadListener != null) {
+ mEditorMediaUploadListener.onMediaUploadProgress(event.mLocalMediaId, event.mProgress);
+ }
+ }
+
+ private void handleGalleryImageUploadedLegacyEditor(Long galleryId, String localId, String remoteId) {
+ SpannableStringBuilder postContent;
+ if (mEditorFragment.getSpannedContent() != null) {
+ // needed by the legacy editor to save local drafts
+ postContent = new SpannableStringBuilder(mEditorFragment.getSpannedContent());
+ } else {
+ postContent = new SpannableStringBuilder(StringUtils.notNullStr((String)
+ mEditorFragment.getContent()));
+ }
+ int selectionStart = 0;
+ int selectionEnd = postContent.length();
+
+ MediaGalleryImageSpan[] gallerySpans = postContent.getSpans(selectionStart, selectionEnd,
+ MediaGalleryImageSpan.class);
+ if (gallerySpans.length != 0) {
+ for (MediaGalleryImageSpan gallerySpan : gallerySpans) {
+ MediaGallery gallery = gallerySpan.getMediaGallery();
+ if (gallery.getUniqueId() == galleryId) {
+ ArrayList<String> galleryIds = gallery.getIds();
+ galleryIds.add(remoteId);
+ gallery.setIds(galleryIds);
+ gallerySpan.setMediaGallery(gallery);
+ int spanStart = postContent.getSpanStart(gallerySpan);
+ int spanEnd = postContent.getSpanEnd(gallerySpan);
+ postContent.setSpan(gallerySpan, spanStart, spanEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts {@link org.wordpress.android.ui.media.MediaPickerActivity} after refreshing the blog media.
+ */
+ private void startMediaSelection() {
+ Intent intent = new Intent(this, MediaPickerActivity.class);
+ intent.putExtra(MediaPickerActivity.ACTIVITY_TITLE_KEY, getString(R.string.add_to_post));
+ intent.putParcelableArrayListExtra(MediaPickerActivity.DEVICE_IMAGE_MEDIA_SOURCES_KEY,
+ imageMediaSelectionSources());
+ intent.putParcelableArrayListExtra(MediaPickerActivity.DEVICE_VIDEO_MEDIA_SOURCES_KEY,
+ videoMediaSelectionSources());
+ if (mBlogMediaStatus != 0) {
+ intent.putParcelableArrayListExtra(MediaPickerActivity.BLOG_IMAGE_MEDIA_SOURCES_KEY,
+ blogImageMediaSelectionSources());
+ intent.putParcelableArrayListExtra(MediaPickerActivity.BLOG_VIDEO_MEDIA_SOURCES_KEY,
+ blogVideoMediaSelectionSources());
+ }
+
+ startActivityForResult(intent, MediaPickerActivity.ACTIVITY_REQUEST_CODE_MEDIA_SELECTION);
+ overridePendingTransition(R.anim.slide_up, R.anim.fade_out);
+ }
+
+ private void refreshBlogMedia() {
+ if (NetworkUtils.isNetworkAvailable(this)) {
+ List<Object> apiArgs = new ArrayList<Object>();
+ apiArgs.add(WordPress.getCurrentBlog());
+ ApiHelper.SyncMediaLibraryTask.Callback callback = new ApiHelper.SyncMediaLibraryTask.Callback() {
+ @Override
+ public void onSuccess(int count) {
+ mBlogMediaStatus = 1;
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mPendingVideoPressInfoRequests != null && !mPendingVideoPressInfoRequests.isEmpty()) {
+ // If there are pending requests for video URLs from VideoPress ids, query the DB for
+ // them again and notify the editor
+ String blogId = String.valueOf(WordPress.currentBlog.getLocalTableBlogId());
+ for (String videoId : mPendingVideoPressInfoRequests) {
+ String videoUrl = WordPress.wpDB.getMediaUrlByVideoPressId(blogId, videoId);
+ String posterUrl = WordPressMediaUtils.getVideoPressVideoPosterFromURL(videoUrl);
+
+ mEditorFragment.setUrlForVideoPressId(videoId, videoUrl, posterUrl);
+ }
+
+ mPendingVideoPressInfoRequests.clear();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(final ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ mBlogMediaStatus = 0;
+ ToastUtils.showToast(EditPostActivity.this, R.string.error_refresh_media, ToastUtils.Duration.SHORT);
+ }
+ };
+ ApiHelper.SyncMediaLibraryTask getMediaTask = new ApiHelper.SyncMediaLibraryTask(0,
+ MediaGridFragment.Filter.ALL, callback);
+ getMediaTask.execute(apiArgs);
+ } else {
+ mBlogMediaStatus = 0;
+ ToastUtils.showToast(this, R.string.error_refresh_media, ToastUtils.Duration.SHORT);
+ }
+ }
+
+ /**
+ * Starts the upload service to upload selected media.
+ */
+ private void startMediaUploadService() {
+ if (!mMediaUploadServiceStarted) {
+ startService(new Intent(this, MediaUploadService.class));
+ mMediaUploadServiceStarted = true;
+ }
+ }
+
+ /**
+ * Stops the upload service.
+ */
+ private void stopMediaUploadService() {
+ if (mMediaUploadServiceStarted) {
+ stopService(new Intent(this, MediaUploadService.class));
+ mMediaUploadServiceStarted = false;
+ }
+ }
+
+ /**
+ * Queues a media file for upload and starts the MediaUploadService. Toasts will alert the user
+ * if there are issues with the file.
+ *
+ * @param path
+ * local path of the media file to upload
+ * @param mediaIdOut
+ * the new {@link org.wordpress.android.util.helpers.MediaFile} ID is added if non-null
+ */
+ private MediaFile queueFileForUpload(String path, ArrayList<String> mediaIdOut) {
+ return queueFileForUpload(path, mediaIdOut, "queued");
+ }
+
+ private MediaFile queueFileForUpload(String path, ArrayList<String> mediaIdOut, String startingState) {
+ // Invalid file path
+ if (TextUtils.isEmpty(path)) {
+ Toast.makeText(this, R.string.editor_toast_invalid_path, Toast.LENGTH_SHORT).show();
+ return null;
+ }
+
+ // File not found
+ File file = new File(path);
+ if (!file.exists()) {
+ Toast.makeText(this, R.string.file_not_found, Toast.LENGTH_SHORT).show();
+ return null;
+ }
+
+ Blog blog = WordPress.getCurrentBlog();
+ long currentTime = System.currentTimeMillis();
+ String mimeType = MediaUtils.getMediaFileMimeType(file);
+ String fileName = MediaUtils.getMediaFileName(file, mimeType);
+ MediaFile mediaFile = new MediaFile();
+
+ mediaFile.setBlogId(String.valueOf(blog.getLocalTableBlogId()));
+ mediaFile.setFileName(fileName);
+ mediaFile.setFilePath(path);
+ mediaFile.setUploadState(startingState);
+ mediaFile.setDateCreatedGMT(currentTime);
+ mediaFile.setMediaId(String.valueOf(currentTime));
+ mediaFile.setVideo(MediaUtils.isVideo(path));
+
+ if (mimeType != null && mimeType.startsWith("image")) {
+ // get width and height
+ BitmapFactory.Options bfo = new BitmapFactory.Options();
+ bfo.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, bfo);
+ mediaFile.setWidth(bfo.outWidth);
+ mediaFile.setHeight(bfo.outHeight);
+ }
+
+ if (!TextUtils.isEmpty(mimeType)) {
+ mediaFile.setMimeType(mimeType);
+ }
+
+ if (mediaIdOut != null) {
+ mediaIdOut.add(mediaFile.getMediaId());
+ }
+
+ saveMediaFile(mediaFile);
+ startMediaUploadService();
+
+ return mediaFile;
+ }
+
+ /**
+ * EditorFragmentListener methods
+ */
+
+ @Override
+ public void onSettingsClicked() {
+ mViewPager.setCurrentItem(PAGE_SETTINGS);
+ }
+
+ @Override
+ public void onAddMediaClicked() {
+ // no op
+ }
+
+ @Override
+ public void onMediaDropped(final ArrayList<Uri> mediaUris) {
+ mDroppedMediaUris = mediaUris;
+
+ if (PermissionUtils.checkAndRequestStoragePermission(this, DRAG_AND_DROP_MEDIA_PERMISSION_REQUEST_CODE)) {
+ runOnUiThread(mFetchMediaRunnable);
+ }
+ }
+
+ @Override
+ public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ requestTemporaryPermissions(dragEvent);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private void requestTemporaryPermissions(DragEvent dragEvent) {
+ requestDragAndDropPermissions(dragEvent);
+ }
+
+ @Override
+ public void onMediaRetryClicked(String mediaId) {
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId());
+ WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.QUEUED);
+
+ MediaUploadService mediaUploadService = MediaUploadService.getInstance();
+ if (mediaUploadService == null) {
+ startMediaUploadService();
+ } else {
+ mediaUploadService.processQueue();
+ }
+ AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_RETRIED);
+ }
+
+ @Override
+ public void onMediaUploadCancelClicked(String mediaId, boolean delete) {
+ MediaUploadService mediaUploadService = MediaUploadService.getInstance();
+ if (mediaUploadService != null) {
+ mediaUploadService.cancelUpload(mediaId, delete);
+ }
+ }
+
+ @Override
+ public void onFeaturedImageChanged(long mediaId) {
+ mPost.setFeaturedImageId(mediaId);
+ mEditPostSettingsFragment.updateFeaturedImage(mediaId);
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(final String videoId) {
+ String blogId = String.valueOf(WordPress.currentBlog.getLocalTableBlogId());
+ String videoUrl = WordPress.wpDB.getMediaUrlByVideoPressId(blogId, videoId);
+
+ if (videoUrl.isEmpty()) {
+ if (PermissionUtils.checkAndRequestCameraAndStoragePermissions(this, MEDIA_PERMISSION_REQUEST_CODE)) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mPendingVideoPressInfoRequests == null) {
+ mPendingVideoPressInfoRequests = new ArrayList<>();
+ }
+ mPendingVideoPressInfoRequests.add(videoId);
+ refreshBlogMedia();
+ }
+ });
+ }
+ }
+
+ String posterUrl = WordPressMediaUtils.getVideoPressVideoPosterFromURL(videoUrl);
+
+ mEditorFragment.setUrlForVideoPressId(videoId, videoUrl, posterUrl);
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ String authHeader = "";
+ Blog currentBlog = WordPress.getCurrentBlog();
+ String token = AccountHelper.getDefaultAccount().getAccessToken();
+
+ if (currentBlog != null && currentBlog.isPrivate() && WPUrlUtils.safeToAddWordPressComAuthToken(url) &&
+ !TextUtils.isEmpty(token)) {
+ authHeader = "Bearer " + token;
+ }
+ return authHeader;
+ }
+
+ @Override
+ public void onEditorFragmentInitialized() {
+ fillContentEditorFields();
+ // Set the error listener
+ if (mEditorFragment instanceof EditorFragment) {
+ mEditorFragment.setDebugModeEnabled(BuildConfig.DEBUG);
+ ((EditorFragment) mEditorFragment).setWebViewErrorListener(new ErrorListener() {
+ @Override
+ public void onJavaScriptError(String sourceFile, int lineNumber, String message) {
+ CrashlyticsUtils.logException(new JavaScriptException(sourceFile, lineNumber, message),
+ ExceptionType.SPECIFIC, T.EDITOR,
+ String.format(Locale.US, "%s:%d: %s", sourceFile, lineNumber, message));
+ }
+
+ @Override
+ public void onJavaScriptAlert(String url, String message) {
+ // no op
+ }
+ });
+ }
+ }
+
+ @Override
+ public void saveMediaFile(MediaFile mediaFile) {
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ }
+
+ @Override
+ public void onTrackableEvent(TrackableEvent event) {
+ switch (event) {
+ case HTML_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_HTML);
+ break;
+ case MEDIA_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_IMAGE);
+ break;
+ case UNLINK_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNLINK);
+ break;
+ case LINK_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_LINK);
+ break;
+ case IMAGE_EDITED:
+ AnalyticsTracker.track(Stat.EDITOR_EDITED_IMAGE);
+ break;
+ case BOLD_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_BOLD);
+ break;
+ case ITALIC_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_ITALIC);
+ break;
+ case OL_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_ORDERED_LIST);
+ break;
+ case UL_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNORDERED_LIST);
+ break;
+ case BLOCKQUOTE_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_BLOCKQUOTE);
+ break;
+ case STRIKETHROUGH_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_STRIKETHROUGH);
+ break;
+ case UNDERLINE_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNDERLINE);
+ break;
+ case MORE_BUTTON_TAPPED:
+ AnalyticsTracker.track(Stat.EDITOR_TAPPED_MORE);
+ break;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPreviewFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPreviewFragment.java
new file mode 100644
index 000000000..5e898d92a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPreviewFragment.java
@@ -0,0 +1,125 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Fragment;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.webkit.WebView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPHtml;
+
+public class EditPostPreviewFragment extends Fragment {
+ // TODO: remove mActivity and rely on getActivity()
+ private EditPostActivity mActivity;
+ private WebView mWebView;
+ private TextView mTextView;
+ private LoadPostPreviewTask mLoadTask;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mActivity = (EditPostActivity)getActivity();
+
+ ViewGroup rootView = (ViewGroup) inflater
+ .inflate(R.layout.edit_post_preview_fragment, container, false);
+ mWebView = (WebView) rootView.findViewById(R.id.post_preview_webview);
+ mTextView = (TextView) rootView.findViewById(R.id.post_preview_textview);
+ mTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (mActivity != null) {
+ loadPost();
+ }
+ mTextView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ }
+ });
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (mActivity != null && !mTextView.isLayoutRequested()) {
+ loadPost();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ if (mLoadTask != null) {
+ mLoadTask.cancel(true);
+ mLoadTask = null;
+ }
+ }
+
+ public void loadPost() {
+ if (mLoadTask == null) {
+ mLoadTask = new LoadPostPreviewTask();
+ mLoadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ // Load post content in the background
+ private class LoadPostPreviewTask extends AsyncTask<Void, Void, Spanned> {
+ @Override
+ protected Spanned doInBackground(Void... params) {
+ Spanned contentSpannable;
+
+ if (mActivity == null || mActivity.getPost() == null) {
+ return null;
+ }
+
+ Post post = mActivity.getPost();
+
+ String postTitle = "<h1>" + post.getTitle() + "</h1>";
+ String postContent = postTitle + post.getDescription() + "\n\n" + post.getMoreText();
+
+ if (post.isLocalDraft()) {
+ contentSpannable = WPHtml.fromHtml(
+ postContent.replaceAll("\uFFFC", ""),
+ mActivity,
+ post,
+ Math.min(mTextView.getWidth(), mTextView.getHeight())
+ );
+ } else {
+ String htmlText = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"webview.css\" /></head><body><div id=\"container\">%s</div></body></html>";
+ htmlText = String.format(htmlText, StringUtils.addPTags(postContent));
+ contentSpannable = new SpannableString(htmlText);
+ }
+
+ return contentSpannable;
+ }
+
+ @Override
+ protected void onPostExecute(Spanned spanned) {
+ if (mActivity != null && mActivity.getPost() != null && spanned != null) {
+ if (mActivity.getPost().isLocalDraft()) {
+ mTextView.setVisibility(View.VISIBLE);
+ mWebView.setVisibility(View.GONE);
+ mTextView.setText(spanned);
+ } else {
+ mTextView.setVisibility(View.GONE);
+ mWebView.setVisibility(View.VISIBLE);
+
+ mWebView.loadDataWithBaseURL("file:///android_asset/", spanned.toString(),
+ "text/html", "utf-8", null);
+ }
+ }
+
+ mLoadTask = null;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java
new file mode 100644
index 000000000..cdcde6efc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java
@@ -0,0 +1,1054 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.location.Address;
+import android.location.Location;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.widget.AppCompatButton;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.inputmethod.EditorInfo;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TimePicker;
+import android.widget.Toast;
+
+import com.android.volley.toolbox.NetworkImageView;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.json.JSONArray;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostLocation;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.ui.RequestCodes;
+import org.wordpress.android.ui.media.MediaGalleryPickerActivity;
+import org.wordpress.android.ui.media.WordPressMediaUtils;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.prefs.SiteSettingsInterface;
+import org.wordpress.android.ui.suggestion.adapters.TagSuggestionAdapter;
+import org.wordpress.android.ui.suggestion.util.SuggestionServiceConnectionManager;
+import org.wordpress.android.ui.suggestion.util.SuggestionUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.GeocoderUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.PermissionUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.helpers.LocationHelper;
+import org.wordpress.android.widgets.SuggestionAutoCompleteText;
+import org.xmlrpc.android.ApiHelper;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+public class EditPostSettingsFragment extends Fragment
+ implements View.OnClickListener, TextView.OnEditorActionListener {
+ private static final int ACTIVITY_REQUEST_CODE_SELECT_CATEGORIES = 5;
+ private static final String CATEGORY_PREFIX_TAG = "category-";
+
+ private static final int SELECT_LIBRARY_MENU_POSITION = 100;
+ private static final int CLEAR_FEATURED_IMAGE_MENU_POSITION = 101;
+
+ private Post mPost;
+
+ private Spinner mStatusSpinner, mPostFormatSpinner;
+ private EditText mPasswordEditText, mExcerptEditText;
+ private TextView mPubDateText;
+ private ViewGroup mSectionCategories;
+ private ViewGroup mRootView;
+ private TextView mFeaturedImageLabel;
+ private NetworkImageView mFeaturedImageView;
+ private Button mFeaturedImageButton;
+ private SuggestionAutoCompleteText mTagsEditText;
+
+ private SuggestionServiceConnectionManager mSuggestionServiceConnectionManager;
+
+ private long mFeaturedImageId;
+
+ private ArrayList<String> mCategories;
+
+ private PostLocation mPostLocation;
+ private LocationHelper mLocationHelper;
+
+ private int mYear, mMonth, mDay, mHour, mMinute;
+ private long mCustomPubDate = 0;
+ private boolean mIsCustomPubDate;
+
+ private String[] mPostFormats;
+ private String[] mPostFormatTitles;
+
+ private enum LocationStatus {NONE, FOUND, NOT_FOUND, SEARCHING}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getActivity() != null) {
+ PreferenceManager.setDefaultValues(getActivity(), R.xml.account_settings, false);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mSuggestionServiceConnectionManager != null) {
+ mSuggestionServiceConnectionManager.unbindFromService();
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mPost = ((EditPostActivity) getActivity()).getPost();
+ mRootView = (ViewGroup) inflater.inflate(R.layout.edit_post_settings_fragment, container, false);
+
+ if (mRootView == null || mPost == null) {
+ return null;
+ }
+
+ Calendar c = Calendar.getInstance();
+ mYear = c.get(Calendar.YEAR);
+ mMonth = c.get(Calendar.MONTH);
+ mDay = c.get(Calendar.DAY_OF_MONTH);
+ mHour = c.get(Calendar.HOUR_OF_DAY);
+ mMinute = c.get(Calendar.MINUTE);
+ mCategories = new ArrayList<String>();
+
+ mExcerptEditText = (EditText) mRootView.findViewById(R.id.postExcerpt);
+ mPasswordEditText = (EditText) mRootView.findViewById(R.id.post_password);
+ mPubDateText = (TextView) mRootView.findViewById(R.id.pubDate);
+ mPubDateText.setOnClickListener(this);
+ mStatusSpinner = (Spinner) mRootView.findViewById(R.id.status);
+ mStatusSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ updatePostSettingsAndSaveButton();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+ });
+ mSectionCategories = ((ViewGroup) mRootView.findViewById(R.id.sectionCategories));
+
+ mFeaturedImageLabel = (TextView) mRootView.findViewById(R.id.featuredImageLabel);
+ mFeaturedImageView = (NetworkImageView) mRootView.findViewById(R.id.featuredImage);
+ mFeaturedImageButton = (Button) mRootView.findViewById(R.id.addFeaturedImage);
+
+ if (AppPrefs.isVisualEditorEnabled()) {
+ registerForContextMenu(mFeaturedImageView);
+ mFeaturedImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ view.showContextMenu();
+ }
+ });
+
+ mFeaturedImageButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ launchMediaGalleryActivity();
+ }
+ });
+ } else {
+ mFeaturedImageLabel.setVisibility(View.GONE);
+ mFeaturedImageView.setVisibility(View.GONE);
+ mFeaturedImageButton.setVisibility(View.GONE);
+ }
+
+ if (mPost.isPage()) { // remove post specific views
+ mExcerptEditText.setVisibility(View.GONE);
+ mRootView.findViewById(R.id.sectionTags).setVisibility(View.GONE);
+ mRootView.findViewById(R.id.sectionCategories).setVisibility(View.GONE);
+ mRootView.findViewById(R.id.postFormatLabel).setVisibility(View.GONE);
+ mRootView.findViewById(R.id.postFormat).setVisibility(View.GONE);
+ } else {
+ mPostFormatTitles = getResources().getStringArray(R.array.post_formats_array);
+ mPostFormats =
+ new String[]{"aside", "audio", "chat", "gallery", "image", "link", "quote", "standard", "status",
+ "video"};
+ if (WordPress.getCurrentBlog().getPostFormats().equals("")) {
+ new ApiHelper.GetPostFormatsTask().execute(WordPress.getCurrentBlog());
+ } else {
+ try {
+ Gson gson = new Gson();
+ Type type = new TypeToken<Map<String, String>>() {}.getType();
+ Map<String, String> jsonPostFormats = gson.fromJson(WordPress.getCurrentBlog().getPostFormats(),
+ type);
+ mPostFormats = new String[jsonPostFormats.size()];
+ mPostFormatTitles = new String[jsonPostFormats.size()];
+ int i = 0;
+ for (Map.Entry<String, String> entry : jsonPostFormats.entrySet()) {
+ String key = entry.getKey();
+ String val = entry.getValue();
+ mPostFormats[i] = key;
+ mPostFormatTitles[i] = StringEscapeUtils.unescapeHtml(val);
+ i++;
+ }
+ } catch (RuntimeException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ mPostFormatSpinner = (Spinner) mRootView.findViewById(R.id.postFormat);
+ ArrayAdapter<String> pfAdapter = new ArrayAdapter<>(getActivity(), R.layout.simple_spinner_item,
+ mPostFormatTitles);
+ pfAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mPostFormatSpinner.setAdapter(pfAdapter);
+ String activePostFormat = "standard";
+
+ if (!TextUtils.isEmpty(mPost.getPostFormat())) {
+ activePostFormat = mPost.getPostFormat();
+ }
+
+ for (int i = 0; i < mPostFormats.length; i++) {
+ if (mPostFormats[i].equals(activePostFormat))
+ mPostFormatSpinner.setSelection(i);
+ }
+
+ mPostFormatSpinner.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ return false;
+ }
+ }
+ );
+
+ mTagsEditText = (SuggestionAutoCompleteText) mRootView.findViewById(R.id.tags);
+ if (mTagsEditText != null) {
+ mTagsEditText.setTokenizer(new SuggestionAutoCompleteText.CommaTokenizer());
+
+ setupSuggestionServiceAndAdapter();
+ }
+ }
+
+ initSettingsFields();
+ populateSelectedCategories();
+ initLocation(mRootView);
+ return mRootView;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ menu.add(0, SELECT_LIBRARY_MENU_POSITION, 0, getResources().getText(R.string.select_from_media_library));
+ menu.add(0, CLEAR_FEATURED_IMAGE_MENU_POSITION, 0, "Remove featured image");
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case SELECT_LIBRARY_MENU_POSITION:
+ launchMediaGalleryActivity();
+ return true;
+ case CLEAR_FEATURED_IMAGE_MENU_POSITION:
+ mFeaturedImageId = -1;
+ mFeaturedImageView.setVisibility(View.GONE);
+ mFeaturedImageButton.setVisibility(View.VISIBLE);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void setupSuggestionServiceAndAdapter() {
+ if (!isAdded()) return;
+
+ int remoteBlogId = -1;
+ String blogID = WordPress.getCurrentRemoteBlogId();
+ remoteBlogId = StringUtils.stringToInt(blogID, -1);
+
+ mSuggestionServiceConnectionManager = new SuggestionServiceConnectionManager(getActivity(), remoteBlogId);
+ TagSuggestionAdapter tagSuggestionAdapter = SuggestionUtils.setupTagSuggestions(remoteBlogId, getActivity(), mSuggestionServiceConnectionManager);
+ if (tagSuggestionAdapter != null) {
+ mTagsEditText.setAdapter(tagSuggestionAdapter);
+ }
+ }
+
+ private void initSettingsFields() {
+ mExcerptEditText.setText(mPost.getPostExcerpt());
+
+ String[] items = new String[]{ getResources().getString(R.string.publish_post),
+ getResources().getString(R.string.draft),
+ getResources().getString(R.string.pending_review),
+ getResources().getString(R.string.post_private) };
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item, items);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mStatusSpinner.setAdapter(adapter);
+ mStatusSpinner.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ return false;
+ }
+ }
+ );
+
+ long pubDate = mPost.getDate_created_gmt();
+ if (pubDate != 0) {
+ try {
+ int flags = 0;
+ flags |= android.text.format.DateUtils.FORMAT_SHOW_DATE;
+ flags |= android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
+ flags |= android.text.format.DateUtils.FORMAT_SHOW_YEAR;
+ flags |= android.text.format.DateUtils.FORMAT_SHOW_TIME;
+ String formattedDate = DateUtils.formatDateTime(getActivity(), pubDate,
+ flags);
+ mPubDateText.setText(formattedDate);
+ } catch (RuntimeException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ if (!TextUtils.isEmpty(mPost.getPassword())) {
+ mPasswordEditText.setText(mPost.getPassword());
+ }
+
+ switch (mPost.getStatusEnum()) {
+ case PUBLISHED:
+ case SCHEDULED:
+ case UNKNOWN:
+ mStatusSpinner.setSelection(0, true);
+ break;
+ case DRAFT:
+ mStatusSpinner.setSelection(1, true);
+ break;
+ case PENDING:
+ mStatusSpinner.setSelection(2, true);
+ break;
+ case PRIVATE:
+ mStatusSpinner.setSelection(3, true);
+ break;
+ }
+
+ if (!mPost.isPage()) {
+ if (mPost.getJSONCategories() != null) {
+ mCategories = JSONUtils.fromJSONArrayToStringList(mPost.getJSONCategories());
+ }
+ }
+ String tags = mPost.getKeywords();
+ if (!tags.equals("")) {
+ mTagsEditText.setText(tags);
+ }
+
+ if (AppPrefs.isVisualEditorEnabled()) {
+ updateFeaturedImage(mPost.getFeaturedImageId());
+ }
+ }
+
+ public long getFeaturedImageId() {
+ return mFeaturedImageId;
+ }
+
+ public void updateFeaturedImage(long id) {
+ if (mFeaturedImageId != id) {
+ mFeaturedImageId = id;
+ if (mFeaturedImageId > 0) {
+ int blogId = WordPress.getCurrentBlog().getLocalTableBlogId();
+ Cursor cursor = WordPress.wpDB.getMediaFile(String.valueOf(blogId), String.valueOf(mFeaturedImageId));
+ if (cursor != null && cursor.moveToFirst()) {
+ mFeaturedImageView.setVisibility(View.VISIBLE);
+ mFeaturedImageButton.setVisibility(View.GONE);
+
+ // Get max width for photon thumbnail
+ int maxWidth = getResources().getDisplayMetrics().widthPixels;
+ int padding = DisplayUtils.dpToPx(getActivity(), 16);
+ int imageWidth = (maxWidth - padding);
+
+ String thumbUrl = WordPressMediaUtils.getNetworkThumbnailUrl(cursor, imageWidth);
+ WordPressMediaUtils.loadNetworkImage(thumbUrl, mFeaturedImageView);
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+ } else {
+ mFeaturedImageView.setVisibility(View.GONE);
+ mFeaturedImageButton.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ private void launchMediaGalleryActivity() {
+ Intent intent = new Intent(getActivity(), MediaGalleryPickerActivity.class);
+ intent.putExtra(MediaGalleryPickerActivity.PARAM_SELECT_ONE_ITEM, true);
+ startActivityForResult(intent, MediaGalleryPickerActivity.REQUEST_CODE);
+ }
+
+ private String getPostStatusForSpinnerPosition(int position) {
+ switch (position) {
+ case 0:
+ return PostStatus.toString(PostStatus.PUBLISHED);
+ case 1:
+ return PostStatus.toString(PostStatus.DRAFT);
+ case 2:
+ return PostStatus.toString(PostStatus.PENDING);
+ case 3:
+ return PostStatus.toString(PostStatus.PRIVATE);
+ default:
+ return PostStatus.toString(PostStatus.UNKNOWN);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (data != null || ((requestCode == RequestCodes.TAKE_PHOTO ||
+ requestCode == RequestCodes.TAKE_VIDEO))) {
+ Bundle extras;
+
+ switch (requestCode) {
+ case ACTIVITY_REQUEST_CODE_SELECT_CATEGORIES:
+ extras = data.getExtras();
+ if (extras != null && extras.containsKey("selectedCategories")) {
+ mCategories = (ArrayList<String>) extras.getSerializable("selectedCategories");
+ populateSelectedCategories();
+ }
+ break;
+ case MediaGalleryPickerActivity.REQUEST_CODE:
+ if (resultCode == Activity.RESULT_OK) {
+ ArrayList<String> ids = data.getStringArrayListExtra(MediaGalleryPickerActivity.RESULT_IDS);
+ if (ids == null || ids.size() == 0) {
+ return;
+ }
+
+ updateFeaturedImage(Long.parseLong(ids.get(0)));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.pubDate) {
+ showPostDateSelectionDialog();
+ } else if (id == R.id.selectCategories) {
+ Bundle bundle = new Bundle();
+ bundle.putInt("id", WordPress.getCurrentBlog().getLocalTableBlogId());
+ if (mCategories != null && mCategories.size() > 0) {
+ bundle.putSerializable("categories", new HashSet<String>(mCategories));
+ }
+ Intent categoriesIntent = new Intent(getActivity(), SelectCategoriesActivity.class);
+ categoriesIntent.putExtras(bundle);
+ startActivityForResult(categoriesIntent, ACTIVITY_REQUEST_CODE_SELECT_CATEGORIES);
+ } else if (id == R.id.categoryButton) {
+ onCategoryButtonClick(v);
+ } else if (id == R.id.locationText) {
+ viewLocation();
+ } else if (id == R.id.updateLocation) {
+ showLocationSearch();
+ } else if (id == R.id.removeLocation) {
+ removeLocation();
+ showLocationAdd();
+ } else if (id == R.id.addLocation) {
+ // Init Location settings when we switch to the fragment, that could trigger the opening of
+ // a dialog asking the user to enable the Geolocation permission (starting Android 6.+).
+ if (checkForLocationPermission()) {
+ showLocationSearch();
+ }
+ } else if (id == R.id.searchLocation) {
+ if (checkForLocationPermission()) {
+ searchLocation();
+ }
+ }
+ }
+
+ @Override
+ public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
+ boolean handled = false;
+ int id = view.getId();
+ if (id == R.id.searchLocationText && actionId == EditorInfo.IME_ACTION_SEARCH && checkForLocationPermission()) {
+ searchLocation();
+ handled = true;
+ }
+ return handled;
+ }
+
+ private void showPostDateSelectionDialog() {
+ final DatePicker datePicker = new DatePicker(getActivity());
+ datePicker.init(mYear, mMonth, mDay, null);
+ datePicker.setCalendarViewShown(false);
+
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.select_date)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mYear = datePicker.getYear();
+ mMonth = datePicker.getMonth();
+ mDay = datePicker.getDayOfMonth();
+ showPostTimeSelectionDialog();
+ }
+ })
+ .setNeutralButton(getResources().getText(R.string.immediately),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface,
+ int i) {
+ mIsCustomPubDate = true;
+ mPubDateText.setText(R.string.immediately);
+ updatePostSettingsAndSaveButton();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ }
+ }
+ ).setView(datePicker).show();
+
+ }
+
+ private void showPostTimeSelectionDialog() {
+ final TimePicker timePicker = new TimePicker(getActivity());
+ timePicker.setIs24HourView(DateFormat.is24HourFormat(getActivity()));
+ timePicker.setCurrentHour(mHour);
+ timePicker.setCurrentMinute(mMinute);
+
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.select_time)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mHour = timePicker.getCurrentHour();
+ mMinute = timePicker.getCurrentMinute();
+
+ Date d = new Date(mYear - 1900, mMonth, mDay, mHour, mMinute);
+ long timestamp = d.getTime();
+
+ try {
+ int flags = 0;
+ flags |= DateUtils.FORMAT_SHOW_DATE;
+ flags |= DateUtils.FORMAT_ABBREV_MONTH;
+ flags |= DateUtils.FORMAT_SHOW_YEAR;
+ flags |= DateUtils.FORMAT_SHOW_TIME;
+ String formattedDate = DateUtils.formatDateTime(getActivity(), timestamp, flags);
+ mCustomPubDate = timestamp;
+ mPubDateText.setText(formattedDate);
+ mIsCustomPubDate = true;
+
+ updatePostSettingsAndSaveButton();
+ } catch (RuntimeException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ }
+ }).setView(timePicker).show();
+ }
+
+ /**
+ * Updates post object with content of this fragment
+ */
+ public void updatePostSettings() {
+ if (!isAdded() || mPost == null) {
+ return;
+ }
+
+ String password = EditTextUtils.getText(mPasswordEditText);
+ String pubDate = EditTextUtils.getText(mPubDateText);
+ String excerpt = EditTextUtils.getText(mExcerptEditText);
+
+ long pubDateTimestamp = 0;
+ if (mIsCustomPubDate && pubDate.equals(getText(R.string.immediately)) && !mPost.isLocalDraft()) {
+ Date d = new Date();
+ pubDateTimestamp = d.getTime();
+ } else if (!pubDate.equals(getText(R.string.immediately))) {
+ if (mIsCustomPubDate)
+ pubDateTimestamp = mCustomPubDate;
+ else if (mPost.getDate_created_gmt() > 0)
+ pubDateTimestamp = mPost.getDate_created_gmt();
+ } else if (pubDate.equals(getText(R.string.immediately)) && mPost.isLocalDraft()) {
+ mPost.setDate_created_gmt(0);
+ mPost.setDateCreated(0);
+ }
+
+ String tags = "", postFormat = "";
+ if (!mPost.isPage()) {
+ tags = EditTextUtils.getText(mTagsEditText);
+
+ // post format
+ if (mPostFormats != null && mPostFormatSpinner != null &&
+ mPostFormatSpinner.getSelectedItemPosition() < mPostFormats.length) {
+ postFormat = mPostFormats[mPostFormatSpinner.getSelectedItemPosition()];
+ }
+ }
+
+ String status;
+ if (mStatusSpinner != null) {
+ status = getPostStatusForSpinnerPosition(mStatusSpinner.getSelectedItemPosition());
+ } else {
+ status = mPost.getPostStatus();
+ }
+
+ // We want to flag this post as having changed statuses from draft to published so that we
+ // properly track stats we care about for when users first publish posts.
+ if (!mPost.isLocalDraft() && mPost.getPostStatus().equals(PostStatus.toString(PostStatus.DRAFT))
+ && status.equals(PostStatus.toString(PostStatus.PUBLISHED))) {
+ mPost.setChangedFromDraftToPublished(true);
+ }
+
+ if (mPost.supportsLocation()) {
+ mPost.setLocation(mPostLocation);
+ }
+
+ if (mCategories != null) {
+ mPost.setJSONCategories(new JSONArray(mCategories));
+ }
+
+ if (AppPrefs.isVisualEditorEnabled()) {
+ mPost.setFeaturedImageId(mFeaturedImageId);
+ }
+
+ mPost.setPostExcerpt(excerpt);
+ mPost.setDate_created_gmt(pubDateTimestamp);
+ mPost.setKeywords(tags);
+ mPost.setPostStatus(status);
+ mPost.setPassword(password);
+ mPost.setPostFormat(postFormat);
+ }
+
+ /*
+ * Saves settings to post object and updates save button text in the ActionBar
+ */
+ private void updatePostSettingsAndSaveButton() {
+ if (isAdded()) {
+ updatePostSettings();
+ getActivity().invalidateOptionsMenu();
+ }
+ }
+
+ /**
+ * Location methods
+ */
+
+ /*
+ * retrieves and displays the friendly address for a lat/long location
+ */
+ private class GetAddressTask extends AsyncTask<Double, Void, Address> {
+ double latitude;
+ double longitude;
+
+ @Override
+ protected void onPreExecute() {
+ setLocationStatus(LocationStatus.SEARCHING);
+ showLocationView();
+ }
+
+ @Override
+ protected Address doInBackground(Double... args) {
+ // args will be the latitude, longitude to look up
+ latitude = args[0];
+ longitude = args[1];
+
+ return GeocoderUtils.getAddressFromCoords(getActivity(), latitude, longitude);
+ }
+
+ protected void onPostExecute(Address address) {
+ setLocationStatus(LocationStatus.FOUND);
+ if (address == null) {
+ // show lat/long when Geocoder fails (ugly, but better than not showing anything
+ // or showing an error since the location has been assigned to the post already)
+ updateLocationText(Double.toString(latitude) + ", " + Double.toString(longitude));
+ } else {
+ String locationName = GeocoderUtils.getLocationNameFromAddress(address);
+ updateLocationText(locationName);
+ }
+ }
+ }
+
+ private class GetCoordsTask extends AsyncTask<String, Void, Address> {
+ @Override
+ protected void onPreExecute() {
+ setLocationStatus(LocationStatus.SEARCHING);
+ showLocationView();
+ }
+
+ @Override
+ protected Address doInBackground(String... args) {
+ String locationName = args[0];
+
+ return GeocoderUtils.getAddressFromLocationName(getActivity(), locationName);
+ }
+
+ @Override
+ protected void onPostExecute(Address address) {
+ setLocationStatus(LocationStatus.FOUND);
+ showLocationView();
+
+ if (address != null) {
+ double[] coordinates = GeocoderUtils.getCoordsFromAddress(address);
+ setLocation(coordinates[0], coordinates[1]);
+
+ String locationName = GeocoderUtils.getLocationNameFromAddress(address);
+ updateLocationText(locationName);
+ } else {
+ showLocationNotAvailableError();
+ showLocationSearch();
+ }
+ }
+ }
+
+ private LocationHelper.LocationResult locationResult = new LocationHelper.LocationResult() {
+ @Override
+ public void gotLocation(final Location location) {
+ if (getActivity() == null)
+ return;
+ // note that location will be null when requesting location fails
+ getActivity().runOnUiThread(new Runnable() {
+ public void run() {
+ setLocation(location);
+ }
+ });
+ }
+ };
+
+ private View mLocationAddSection;
+ private View mLocationSearchSection;
+ private View mLocationViewSection;
+ private TextView mLocationText;
+ private EditText mLocationEditText;
+ private Button mButtonSearchLocation;
+
+ private TextWatcher mLocationEditTextWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) { }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ String buttonText;
+ if (s.length() > 0) {
+ buttonText = getResources().getString(R.string.search_location);
+ } else {
+ buttonText = getResources().getString(R.string.search_current_location);
+ }
+ mButtonSearchLocation.setText(buttonText);
+ }
+ };
+
+ /*
+ * called when activity is created to initialize the location provider, show views related
+ * to location if enabled for this blog, and retrieve the current location if necessary
+ */
+ private void initLocation(ViewGroup rootView) {
+ if (!mPost.supportsLocation()) return;
+
+ // show the location views if a provider was found and this is a post on a blog that has location enabled
+ View locationRootView = ((ViewStub) rootView.findViewById(R.id.stub_post_location_settings)).inflate();
+
+ TextView locationLabel = ((TextView) locationRootView.findViewById(R.id.locationLabel));
+ locationLabel.setText(getResources().getString(R.string.location).toUpperCase());
+
+ mLocationText = (TextView) locationRootView.findViewById(R.id.locationText);
+ mLocationText.setOnClickListener(this);
+
+ mLocationAddSection = locationRootView.findViewById(R.id.sectionLocationAdd);
+ mLocationSearchSection = locationRootView.findViewById(R.id.sectionLocationSearch);
+ mLocationViewSection = locationRootView.findViewById(R.id.sectionLocationView);
+
+ Button addLocation = (Button) locationRootView.findViewById(R.id.addLocation);
+ addLocation.setOnClickListener(this);
+
+ mButtonSearchLocation = (Button) locationRootView.findViewById(R.id.searchLocation);
+ mButtonSearchLocation.setOnClickListener(this);
+
+ mLocationEditText = (EditText) locationRootView.findViewById(R.id.searchLocationText);
+ mLocationEditText.setOnEditorActionListener(this);
+ mLocationEditText.addTextChangedListener(mLocationEditTextWatcher);
+
+ Button updateLocation = (Button) locationRootView.findViewById(R.id.updateLocation);
+ Button removeLocation = (Button) locationRootView.findViewById(R.id.removeLocation);
+ updateLocation.setOnClickListener(this);
+ removeLocation.setOnClickListener(this);
+
+ // this is where we ask for location permission when EditPostActivity is oppened
+ if (SiteSettingsInterface.getGeotagging(getActivity()) && !checkForLocationPermission()) return;
+
+ // if this post has location attached to it, look up the location address
+ if (mPost.hasLocation()) {
+ showLocationView();
+ PostLocation location = mPost.getLocation();
+ setLocation(location.getLatitude(), location.getLongitude());
+ } else {
+ // Search for current location to geotag post if preferences allow
+ EditPostActivity activity = (EditPostActivity) getActivity();
+ if (SiteSettingsInterface.getGeotagging(activity) && activity.isNewPost()) {
+ searchLocation();
+ } else {
+ showLocationAdd();
+ }
+ }
+ }
+
+ private boolean checkForLocationPermission() {
+ return isAdded() && PermissionUtils.checkLocationPermissions(getActivity(),
+ EditPostActivity.LOCATION_PERMISSION_REQUEST_CODE);
+ }
+
+ public void showLocationSearch() {
+ mLocationAddSection.setVisibility(View.GONE);
+ mLocationSearchSection.setVisibility(View.VISIBLE);
+ mLocationViewSection.setVisibility(View.GONE);
+
+ EditTextUtils.showSoftInput(mLocationEditText);
+ }
+
+ private void showLocationAdd() {
+ mLocationAddSection.setVisibility(View.VISIBLE);
+ mLocationSearchSection.setVisibility(View.GONE);
+ mLocationViewSection.setVisibility(View.GONE);
+ }
+
+ private void showLocationView() {
+ mLocationAddSection.setVisibility(View.GONE);
+ mLocationSearchSection.setVisibility(View.GONE);
+ mLocationViewSection.setVisibility(View.VISIBLE);
+ }
+
+ public void searchLocation() {
+ if(!isAdded() || mLocationEditText == null) return;
+
+ EditTextUtils.hideSoftInput(mLocationEditText);
+ String location = EditTextUtils.getText(mLocationEditText);
+
+ removeLocation();
+
+ if (location.isEmpty()) {
+ fetchCurrentLocation();
+ } else {
+ new GetCoordsTask().execute(location);
+ }
+ }
+
+ /*
+ * get the current location
+ */
+ private void fetchCurrentLocation() {
+ if (!isAdded()) {
+ return;
+ }
+ if (mLocationHelper == null) {
+ mLocationHelper = new LocationHelper();
+ }
+ boolean canGetLocation = mLocationHelper.getLocation(getActivity(), locationResult);
+
+ if (canGetLocation) {
+ setLocationStatus(LocationStatus.SEARCHING);
+ showLocationView();
+ } else {
+ setLocation(null);
+ showLocationNotAvailableError();
+ showLocationAdd();
+ }
+ }
+
+ /*
+ * called when location is retrieved/updated for this post - looks up the address to
+ * display for the lat/long
+ */
+ private void setLocation(Location location) {
+ if (location != null) {
+ setLocation(location.getLatitude(), location.getLongitude());
+ } else {
+ updateLocationText(getString(R.string.location_not_found));
+ setLocationStatus(LocationStatus.NOT_FOUND);
+ }
+ }
+
+ private void setLocation(double latitude, double longitude) {
+ mPostLocation = new PostLocation(latitude, longitude);
+ new GetAddressTask().execute(mPostLocation.getLatitude(), mPostLocation.getLongitude());
+ }
+
+ private void removeLocation() {
+ mPostLocation = null;
+ mPost.unsetLocation();
+
+ updateLocationText("");
+ setLocationStatus(LocationStatus.NONE);
+ }
+
+ private void viewLocation() {
+ if (mPostLocation != null && mPostLocation.isValid()) {
+ String uri = "geo:" + mPostLocation.getLatitude() + "," + mPostLocation.getLongitude();
+ startActivity(new Intent(android.content.Intent.ACTION_VIEW, Uri.parse(uri)));
+ } else {
+ showLocationNotAvailableError();
+ showLocationAdd();
+ }
+ }
+
+ private void showLocationNotAvailableError() {
+ if (!isAdded()) {
+ return;
+ }
+ Toast.makeText(getActivity(), getResources().getText(R.string.location_not_found), Toast.LENGTH_SHORT).show();
+ }
+
+ private void updateLocationText(String locationName) {
+ mLocationText.setText(locationName);
+ }
+
+ /*
+ * changes the left drawable on the location text to match the passed status
+ */
+ private void setLocationStatus(LocationStatus status) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // animate location text when searching
+ if (status == LocationStatus.SEARCHING) {
+ updateLocationText(getString(R.string.loading));
+
+ Animation aniBlink = AnimationUtils.loadAnimation(getActivity(), R.anim.blink);
+ if (aniBlink != null) {
+ mLocationText.startAnimation(aniBlink);
+ }
+ } else {
+ mLocationText.clearAnimation();
+ }
+
+ final int drawableId;
+ switch (status) {
+ case FOUND:
+ drawableId = R.drawable.ic_action_location_found;
+ break;
+ case NOT_FOUND:
+ drawableId = R.drawable.ic_action_location_off;
+ break;
+ case SEARCHING:
+ drawableId = R.drawable.ic_action_location_searching;
+ break;
+ case NONE:
+ drawableId = 0;
+ break;
+ default:
+ return;
+ }
+
+ mLocationText.setCompoundDrawablesWithIntrinsicBounds(drawableId, 0, 0, 0);
+ }
+
+ /**
+ * Categories
+ */
+
+ private void onCategoryButtonClick(View v) {
+ if (mCategories == null) {
+ ToastUtils.showToast(getActivity(), R.string.error_generic);
+ return;
+ }
+
+ // Get category name by removing prefix from the tag
+ boolean listChanged = false;
+ String categoryName = (String) v.getTag();
+ categoryName = categoryName.replaceFirst(CATEGORY_PREFIX_TAG, "");
+
+ // Remove clicked category from list
+ for (int i = 0; i < mCategories.size(); i++) {
+ if (mCategories.get(i).equals(categoryName)) {
+ mCategories.remove(i);
+ listChanged = true;
+ break;
+ }
+ }
+
+ // Recreate category views
+ if (listChanged) {
+ populateSelectedCategories();
+ }
+ }
+
+ private void populateSelectedCategories() {
+ // Remove previous category buttons if any + select category button
+ List<View> viewsToRemove = new ArrayList<View>();
+ for (int i = 0; i < mSectionCategories.getChildCount(); i++) {
+ View v = mSectionCategories.getChildAt(i);
+ if (v == null)
+ return;
+ Object tag = v.getTag();
+ if (tag != null && tag.getClass() == String.class &&
+ (((String) tag).startsWith(CATEGORY_PREFIX_TAG) || tag.equals("select-category"))) {
+ viewsToRemove.add(v);
+ }
+ }
+ for (View viewToRemove : viewsToRemove) {
+ mSectionCategories.removeView(viewToRemove);
+ }
+ viewsToRemove.clear();
+
+ // New category buttons
+ LayoutInflater layoutInflater = getActivity().getLayoutInflater();
+
+ if (mCategories != null) {
+ for (String categoryName : mCategories) {
+ AppCompatButton buttonCategory = (AppCompatButton) layoutInflater.inflate(R.layout.category_button, null);
+ if (categoryName != null && buttonCategory != null) {
+ buttonCategory.setText(Html.fromHtml(categoryName));
+ buttonCategory.setTag(CATEGORY_PREFIX_TAG + categoryName);
+ buttonCategory.setOnClickListener(this);
+ mSectionCategories.addView(buttonCategory);
+ }
+ }
+ }
+
+ // Add select category button
+ Button selectCategory = (Button) layoutInflater.inflate(R.layout.category_select_button, null);
+ if (selectCategory != null) {
+ selectCategory.setOnClickListener(this);
+ mSectionCategories.addView(selectCategory);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/ParentCategorySpinnerAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/ParentCategorySpinnerAdapter.java
new file mode 100644
index 000000000..e87242df2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/ParentCategorySpinnerAdapter.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.posts;
+
+import android.content.Context;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+import org.wordpress.android.R;
+import org.wordpress.android.models.CategoryNode;
+
+import java.util.List;
+
+
+public class ParentCategorySpinnerAdapter extends BaseAdapter implements SpinnerAdapter {
+ int mResourceId;
+ List<CategoryNode> mObjects;
+ Context mContext;
+
+ public int getCount() {
+ return mObjects.size();
+ }
+
+ public CategoryNode getItem(int position) {
+ return mObjects.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public ParentCategorySpinnerAdapter(Context context, int resource, List<CategoryNode> objects) {
+ super();
+ mContext = context;
+ mObjects = objects;
+ mResourceId = resource;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rowView = inflater.inflate(mResourceId, parent, false);
+ TextView textView = (TextView) rowView.findViewById(R.id.categoryRowText);
+ ImageView levelIndicatorView = (ImageView) rowView.findViewById(R.id.categoryRowLevelIndicator);
+ textView.setText(Html.fromHtml(getItem(position).getName()));
+ int level = getItem(position).getLevel();
+ if (level == 1) { // hide ImageView
+ levelIndicatorView.setVisibility(View.GONE);
+ } else {
+ ViewGroup.LayoutParams params = levelIndicatorView.getLayoutParams();
+ params.width = (params.width / 2) * level;
+ levelIndicatorView.setLayoutParams(params);
+ }
+ return rowView;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rowView = inflater.inflate(mResourceId, parent, false);
+ TextView textView = (TextView) rowView.findViewById(R.id.categoryRowText);
+ ImageView levelIndicatorView = (ImageView) rowView.findViewById(R.id.categoryRowLevelIndicator);
+ textView.setText(Html.fromHtml(getItem(position).getName()));
+ levelIndicatorView.setVisibility(View.GONE);
+ return rowView;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewActivity.java
new file mode 100644
index 000000000..2c8caa41f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewActivity.java
@@ -0,0 +1,328 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.posts.services.PostEvents;
+import org.wordpress.android.ui.posts.services.PostUploadService;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.xmlrpc.android.ApiHelper;
+
+import de.greenrobot.event.EventBus;
+
+public class PostPreviewActivity extends AppCompatActivity {
+
+ public static final String ARG_LOCAL_POST_ID = "local_post_id";
+ public static final String ARG_LOCAL_BLOG_ID = "local_blog_id";
+ public static final String ARG_IS_PAGE = "is_page";
+
+ private long mLocalPostId;
+ private int mLocalBlogId;
+ private boolean mIsPage;
+ private boolean mIsUpdatingPost;
+
+ private Post mPost;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.post_preview_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(true);
+ }
+
+ if (savedInstanceState != null) {
+ mLocalPostId = savedInstanceState.getLong(ARG_LOCAL_POST_ID);
+ mLocalBlogId = savedInstanceState.getInt(ARG_LOCAL_BLOG_ID);
+ mIsPage = savedInstanceState.getBoolean(ARG_IS_PAGE);
+ } else {
+ mLocalPostId = getIntent().getLongExtra(ARG_LOCAL_POST_ID, 0);
+ mLocalBlogId = getIntent().getIntExtra(ARG_LOCAL_BLOG_ID, 0);
+ mIsPage = getIntent().getBooleanExtra(ARG_IS_PAGE, false);
+ }
+
+ setTitle(mIsPage ? getString(R.string.preview_page) : getString(R.string.preview_post));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ EventBus.getDefault().register(this);
+
+ mPost = WordPress.wpDB.getPostForLocalTablePostId(mLocalPostId);
+ if (hasPreviewFragment()) {
+ refreshPreview();
+ } else {
+ showPreviewFragment();
+ }
+ showMessageViewIfNecessary();
+ }
+
+ @Override
+ protected void onPause() {
+ EventBus.getDefault().unregister(this);
+ super.onPause();
+ }
+
+ private void showPreviewFragment() {
+ FragmentManager fm = getFragmentManager();
+ fm.executePendingTransactions();
+
+ String tagForFragment = getString(R.string.fragment_tag_post_preview);
+ Fragment fragment = PostPreviewFragment.newInstance(mLocalBlogId, mLocalPostId);
+
+ fm.beginTransaction()
+ .replace(R.id.fragment_container, fragment, tagForFragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ .commitAllowingStateLoss();
+ }
+
+ private boolean hasPreviewFragment() {
+ return (getPreviewFragment() != null);
+ }
+
+ private PostPreviewFragment getPreviewFragment() {
+ String tagForFragment = getString(R.string.fragment_tag_post_preview);
+ Fragment fragment = getFragmentManager().findFragmentByTag(tagForFragment);
+ if (fragment != null) {
+ return (PostPreviewFragment) fragment;
+ } else {
+ return null;
+ }
+ }
+
+ private void refreshPreview() {
+ if (!isFinishing()) {
+ PostPreviewFragment fragment = getPreviewFragment();
+ if (fragment != null) {
+ fragment.refreshPreview();
+ }
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putLong(ARG_LOCAL_POST_ID, mLocalPostId);
+ outState.putInt(ARG_LOCAL_BLOG_ID, mLocalBlogId);
+ outState.putBoolean(ARG_IS_PAGE, mIsPage);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.post_preview, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (item.getItemId() == R.id.menu_edit) {
+ ActivityLauncher.editBlogPostOrPageForResult(this, mLocalPostId, mIsPage);
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /*
+ * if this is a local draft or has local changes, show the message explaining what these
+ * states mean, and hook up the publish and revert buttons
+ */
+ private void showMessageViewIfNecessary() {
+ final ViewGroup messageView = (ViewGroup) findViewById(R.id.message_container);
+
+ if (mPost == null
+ || mIsUpdatingPost
+ || PostUploadService.isPostUploading(mPost.getLocalTablePostId())
+ || (!mPost.isLocalChange() && !mPost.isLocalDraft()) && mPost.getStatusEnum() != PostStatus.DRAFT) {
+ messageView.setVisibility(View.GONE);
+ return;
+ }
+
+ TextView messageText = (TextView) messageView.findViewById(R.id.message_text);
+ if (mPost.isLocalChange()) {
+ messageText.setText(R.string.local_changes_explainer);
+ } else if (mPost.isLocalDraft()) {
+ messageText.setText(R.string.local_draft_explainer);
+ } else if (mPost.getStatusEnum() == PostStatus.DRAFT) {
+ messageText.setText(R.string.draft_explainer);
+ }
+
+ // publish applies to both local draft and local changes
+ View btnPublish = messageView.findViewById(R.id.btn_publish);
+ btnPublish.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AniUtils.animateBottomBar(messageView, false);
+ publishPost();
+ }
+ });
+
+ // revert applies to only local changes
+ View btnRevert = messageView.findViewById(R.id.btn_revert);
+ btnRevert.setVisibility(mPost.isLocalChange() ? View.VISIBLE : View.GONE);
+ if (mPost.isLocalChange() ) {
+ btnRevert.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AniUtils.animateBottomBar(messageView, false);
+ revertPost();
+ AnalyticsTracker.track(Stat.EDITOR_DISCARDED_CHANGES);
+ }
+ });
+ }
+
+ // if both buttons are visible, show them below the message instead of to the right of it
+ if (mPost.isLocalChange()) {
+ RelativeLayout.LayoutParams paramsMessage = (RelativeLayout.LayoutParams) messageText.getLayoutParams();
+ // passing "0" removes the param (necessary since removeRule() is API 17+)
+ paramsMessage.addRule(RelativeLayout.LEFT_OF, 0);
+ paramsMessage.addRule(RelativeLayout.CENTER_VERTICAL, 0);
+ ViewGroup.MarginLayoutParams marginsMessage = (ViewGroup.MarginLayoutParams) messageText.getLayoutParams();
+ marginsMessage.bottomMargin = getResources().getDimensionPixelSize(R.dimen.margin_small);
+
+ ViewGroup buttonsView = (ViewGroup) messageView.findViewById(R.id.layout_buttons);
+ RelativeLayout.LayoutParams paramsButtons = (RelativeLayout.LayoutParams) buttonsView.getLayoutParams();
+ paramsButtons.addRule(RelativeLayout.BELOW, R.id.message_text);
+ ViewGroup.MarginLayoutParams marginsButtons = (ViewGroup.MarginLayoutParams) buttonsView.getLayoutParams();
+ marginsButtons.bottomMargin = getResources().getDimensionPixelSize(R.dimen.margin_large);
+ }
+
+ // first set message bar to invisible so it takes up space, then animate it in
+ // after a brief delay to give time for preview to render first
+ messageView.setVisibility(View.INVISIBLE);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing() && messageView.getVisibility() != View.VISIBLE) {
+ AniUtils.animateBottomBar(messageView, true);
+ }
+ }
+ }, 1000);
+ }
+
+ /*
+ * reverts local changes for this post, replacing it with the latest version from the server
+ */
+ private void revertPost() {
+ if (isFinishing() || !NetworkUtils.checkConnection(this)) {
+ return;
+ }
+
+ if (mIsUpdatingPost) {
+ AppLog.d(AppLog.T.POSTS, "post preview > already updating post");
+ } else {
+ new UpdatePostTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ private void publishPost() {
+ if (!isFinishing() && NetworkUtils.checkConnection(this)) {
+ if (!mPost.isLocalDraft()) {
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.EDITOR_UPDATED_POST,
+ WordPress.getBlog(mPost.getLocalTableBlogId())
+ );
+ }
+
+ if (mPost.getStatusEnum() == PostStatus.DRAFT) {
+ mPost.setPostStatus(PostStatus.toString(PostStatus.PUBLISHED));
+ }
+
+ PostUploadService.addPostToUpload(mPost);
+ startService(new Intent(this, PostUploadService.class));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PostEvents.PostUploadStarted event) {
+ if (event.mLocalBlogId == mLocalBlogId) {
+ showProgress();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PostEvents.PostUploadEnded event) {
+ if (event.mLocalBlogId == mLocalBlogId) {
+ hideProgress();
+ refreshPreview();
+ }
+ }
+
+ private void showProgress() {
+ if (!isFinishing()) {
+ findViewById(R.id.progress).setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideProgress() {
+ if (!isFinishing()) {
+ findViewById(R.id.progress).setVisibility(View.GONE);
+ }
+ }
+
+ private class UpdatePostTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsUpdatingPost = true;
+ showProgress();
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsUpdatingPost = false;
+ hideProgress();
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... nada) {
+ return ApiHelper.updateSinglePost(mLocalBlogId, mPost.getRemotePostId(), mIsPage);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (!isFinishing()) {
+ hideProgress();
+ if (result) {
+ refreshPreview();
+ }
+ }
+ mIsUpdatingPost = false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewFragment.java
new file mode 100644
index 000000000..e1afd0cd3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostPreviewFragment.java
@@ -0,0 +1,133 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPWebViewClient;
+
+public class PostPreviewFragment extends Fragment {
+
+ private int mLocalBlogId;
+ private long mLocalPostId;
+ private WebView mWebView;
+
+ public static PostPreviewFragment newInstance(int localBlogId, long localPostId) {
+ Bundle args = new Bundle();
+ args.putInt(PostPreviewActivity.ARG_LOCAL_BLOG_ID, localBlogId);
+ args.putLong(PostPreviewActivity.ARG_LOCAL_POST_ID, localPostId);
+ PostPreviewFragment fragment = new PostPreviewFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void setArguments(Bundle args) {
+ super.setArguments(args);
+ mLocalBlogId = args.getInt(PostPreviewActivity.ARG_LOCAL_BLOG_ID);
+ mLocalPostId = args.getLong(PostPreviewActivity.ARG_LOCAL_POST_ID);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mLocalBlogId = savedInstanceState.getInt(PostPreviewActivity.ARG_LOCAL_BLOG_ID);
+ mLocalPostId = savedInstanceState.getLong(PostPreviewActivity.ARG_LOCAL_POST_ID);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(PostPreviewActivity.ARG_LOCAL_BLOG_ID, mLocalBlogId);
+ outState.putLong(PostPreviewActivity.ARG_LOCAL_POST_ID, mLocalPostId);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.post_preview_fragment, container, false);
+
+ mWebView = (WebView) view.findViewById(R.id.webView);
+ WPWebViewClient client = new WPWebViewClient(WordPress.wpDB.instantiateBlogByLocalId(mLocalBlogId));
+ mWebView.setWebViewClient(client);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ refreshPreview();
+ }
+
+ void refreshPreview() {
+ if (!isAdded()) return;
+
+ new Thread() {
+ @Override
+ public void run() {
+ Post post = WordPress.wpDB.getPostForLocalTablePostId(mLocalPostId);
+ final String htmlContent = formatPostContentForWebView(getActivity(), post);
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAdded()) return;
+
+ if (htmlContent != null) {
+ mWebView.loadDataWithBaseURL(
+ "file:///android_asset/",
+ htmlContent,
+ "text/html",
+ "utf-8",
+ null);
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.post_not_found);
+ }
+ }
+ });
+ }
+ }.start();
+ }
+
+ private String formatPostContentForWebView(Context context, Post post) {
+ if (context == null || post == null) {
+ return null;
+ }
+
+ String title = (TextUtils.isEmpty(post.getTitle())
+ ? "(" + getResources().getText(R.string.untitled) + ")"
+ : StringUtils.unescapeHTML(post.getTitle()));
+
+ String postContent = PostUtils.collapseShortcodes(post.getDescription());
+ if (!TextUtils.isEmpty(post.getMoreText())) {
+ postContent += "\n\n" + post.getMoreText();
+ }
+
+ // if this is a local draft, remove src="null" from image tags then replace the "android-uri"
+ // tag added for local image with a valid "src" tag so local images can be viewed
+ if (post.isLocalDraft()) {
+ postContent = postContent.replace("src=\"null\"", "").replace("android-uri=", "src=");
+ }
+
+ return "<!DOCTYPE html><html><head><meta charset='UTF-8' />"
+ + "<meta name='viewport' content='width=device-width, initial-scale=1'>"
+ + "<link rel='stylesheet' href='editor.css'>"
+ + "<link rel='stylesheet' href='editor-android.css'>"
+ + "</head><body>"
+ + "<h1>" + title + "</h1>"
+ + StringUtils.addPTags(postContent)
+ + "</body></html>";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java
new file mode 100644
index 000000000..05a4a8d2d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java
@@ -0,0 +1,130 @@
+package org.wordpress.android.ui.posts;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PostUtils {
+
+ private static final HashSet<String> mShortcodeTable = new HashSet<>();
+
+ /*
+ * collapses shortcodes in the passed post content, stripping anything between the
+ * shortcode name and the closing brace
+ * ex: collapseShortcodes("[gallery ids="1206,1205,1191"]") -> "[gallery]"
+ */
+ public static String collapseShortcodes(final String postContent) {
+ // speed things up by skipping regex if content doesn't contain a brace
+ if (postContent == null || !postContent.contains("[")) {
+ return postContent;
+ }
+
+ String shortCode;
+ Pattern p = Pattern.compile("(\\[ *([^ ]+) [^\\[\\]]*\\])");
+ Matcher m = p.matcher(postContent);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ shortCode = m.group(2);
+ if (isKnownShortcode(shortCode)) {
+ m.appendReplacement(sb, "[" + shortCode + "]");
+ } else {
+ AppLog.d(AppLog.T.POSTS, "unknown shortcode - " + shortCode);
+ }
+ }
+ m.appendTail(sb);
+
+ return sb.toString();
+ }
+
+ private static boolean isKnownShortcode(String shortCode) {
+ if (shortCode == null) return false;
+
+ // populate on first use
+ if (mShortcodeTable.size() == 0) {
+ // default shortcodes
+ mShortcodeTable.add("audio");
+ mShortcodeTable.add("caption");
+ mShortcodeTable.add("embed");
+ mShortcodeTable.add("gallery");
+ mShortcodeTable.add("playlist");
+ mShortcodeTable.add("video");
+ mShortcodeTable.add("wp_caption");
+ // audio/video
+ mShortcodeTable.add("dailymotion");
+ mShortcodeTable.add("flickr");
+ mShortcodeTable.add("hulu");
+ mShortcodeTable.add("kickstarter");
+ mShortcodeTable.add("soundcloud");
+ mShortcodeTable.add("vimeo");
+ mShortcodeTable.add("vine");
+ mShortcodeTable.add("wpvideo");
+ mShortcodeTable.add("youtube");
+ // images and documents
+ mShortcodeTable.add("instagram");
+ mShortcodeTable.add("scribd");
+ mShortcodeTable.add("slideshare");
+ mShortcodeTable.add("slideshow");
+ mShortcodeTable.add("presentation");
+ mShortcodeTable.add("googleapps");
+ mShortcodeTable.add("office");
+ // other
+ mShortcodeTable.add("googlemaps");
+ mShortcodeTable.add("polldaddy");
+ mShortcodeTable.add("recipe");
+ mShortcodeTable.add("sitemap");
+ mShortcodeTable.add("twitter-timeline");
+ mShortcodeTable.add("upcomingevents");
+ }
+
+ return mShortcodeTable.contains(shortCode);
+ }
+
+ public static void trackSavePostAnalytics(Post post) {
+ PostStatus status = post.getStatusEnum();
+ switch (status) {
+ case PUBLISHED:
+ if (!post.isLocalDraft()) {
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.EDITOR_UPDATED_POST,
+ WordPress.getBlog(post.getLocalTableBlogId())
+ );
+ } else {
+ // Analytics for the event EDITOR_PUBLISHED_POST are tracked in PostUploadService
+ }
+ break;
+ case SCHEDULED:
+ if (!post.isLocalDraft()) {
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.EDITOR_UPDATED_POST,
+ WordPress.getBlog(post.getLocalTableBlogId())
+ );
+ } else {
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("word_count", AnalyticsUtils.getWordCount(post.getContent()));
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.EDITOR_SCHEDULED_POST,
+ WordPress.getBlog(post.getLocalTableBlogId()),
+ properties
+ );
+ }
+ break;
+ case DRAFT:
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.EDITOR_SAVED_DRAFT,
+ WordPress.getBlog(post.getLocalTableBlogId())
+ );
+ break;
+ default:
+ // No-op
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.java
new file mode 100644
index 000000000..29ee88504
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.java
@@ -0,0 +1,113 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.AlertDialog;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.util.ToastUtils;
+
+public class PostsListActivity extends AppCompatActivity {
+ public static final String EXTRA_VIEW_PAGES = "viewPages";
+ public static final String EXTRA_ERROR_MSG = "errorMessage";
+ public static final String EXTRA_BLOG_LOCAL_ID = "EXTRA_BLOG_LOCAL_ID";
+
+ private boolean mIsPage = false;
+ private PostsListFragment mPostList;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.post_list_activity);
+
+ mIsPage = getIntent().getBooleanExtra(EXTRA_VIEW_PAGES, false);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(getString(mIsPage ? R.string.pages : R.string.posts));
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fm = getFragmentManager();
+ mPostList = (PostsListFragment) fm.findFragmentById(R.id.postList);
+
+ showErrorDialogIfNeeded(getIntent().getExtras());
+ showWarningToastIfNeeded(getIntent().getExtras());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(mIsPage ? ActivityId.PAGES : ActivityId.POSTS);
+ }
+
+ /**
+ * intent extras will contain error info if this activity was started from an
+ * upload error notification
+ */
+ private void showErrorDialogIfNeeded(Bundle extras) {
+ if (extras == null || !extras.containsKey(EXTRA_ERROR_MSG) || isFinishing()) {
+ return;
+ }
+
+ final String errorMessage = extras.getString(EXTRA_ERROR_MSG);
+
+ if (TextUtils.isEmpty(errorMessage)) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getResources().getText(R.string.error))
+ .setMessage(errorMessage)
+ .setPositiveButton(R.string.ok, null)
+ .setCancelable(true);
+
+ builder.create().show();
+ }
+
+ /**
+ * Show a toast when the user taps a Post Upload notification referencing a post that's not from the current
+ * selected Blog
+ */
+ private void showWarningToastIfNeeded(Bundle extras) {
+ if (extras == null || !extras.containsKey(EXTRA_BLOG_LOCAL_ID) || isFinishing()) {
+ return;
+ }
+ if (extras.getInt(EXTRA_BLOG_LOCAL_ID, -1) != WordPress.getCurrentLocalTableBlogId()) {
+ ToastUtils.showToast(this, R.string.error_open_list_from_notification);
+ }
+ }
+
+ public boolean isRefreshing() {
+ return mPostList.isRefreshing();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+ super.onSaveInstanceState(outState);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListFragment.java
new file mode 100644
index 000000000..d6716a267
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListFragment.java
@@ -0,0 +1,522 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.models.PostsListPost;
+import org.wordpress.android.models.PostsListPostList;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.EmptyViewMessageType;
+import org.wordpress.android.ui.posts.adapters.PostsListAdapter;
+import org.wordpress.android.ui.posts.services.PostEvents;
+import org.wordpress.android.ui.posts.services.PostUpdateService;
+import org.wordpress.android.ui.posts.services.PostUploadService;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.wordpress.android.widgets.PostListButton;
+import org.wordpress.android.widgets.RecyclerItemDecoration;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.ErrorType;
+
+import de.greenrobot.event.EventBus;
+
+public class PostsListFragment extends Fragment
+ implements PostsListAdapter.OnPostsLoadedListener,
+ PostsListAdapter.OnLoadMoreListener,
+ PostsListAdapter.OnPostSelectedListener,
+ PostsListAdapter.OnPostButtonClickListener {
+
+ public static final int POSTS_REQUEST_COUNT = 20;
+
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private PostsListAdapter mPostsListAdapter;
+ private View mFabView;
+
+ private RecyclerView mRecyclerView;
+ private View mEmptyView;
+ private ProgressBar mProgressLoadMore;
+ private TextView mEmptyViewTitle;
+ private ImageView mEmptyViewImage;
+
+ private boolean mCanLoadMorePosts = true;
+ private boolean mIsPage;
+ private boolean mIsFetchingPosts;
+
+ private final PostsListPostList mTrashedPosts = new PostsListPostList();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+
+ if (isAdded()) {
+ Bundle extras = getActivity().getIntent().getExtras();
+ if (extras != null) {
+ mIsPage = extras.getBoolean(PostsListActivity.EXTRA_VIEW_PAGES);
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.post_list_fragment, container, false);
+
+ mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
+ mProgressLoadMore = (ProgressBar) view.findViewById(R.id.progress);
+ mFabView = view.findViewById(R.id.fab_button);
+
+ mEmptyView = view.findViewById(R.id.empty_view);
+ mEmptyViewTitle = (TextView) mEmptyView.findViewById(R.id.title_empty);
+ mEmptyViewImage = (ImageView) mEmptyView.findViewById(R.id.image_empty);
+
+ Context context = getActivity();
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+
+ int spacingVertical = mIsPage ? 0 : context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters);
+ int spacingHorizontal = context.getResources().getDimensionPixelSize(R.dimen.content_margin);
+ mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
+
+ // hide the fab so we can animate it in - note that we only do this on Lollipop and higher
+ // due to a bug in the current implementation which prevents it from being hidden
+ // correctly on pre-L devices (which makes animating it in/out ugly)
+ // https://code.google.com/p/android/issues/detail?id=175331
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mFabView.setVisibility(View.GONE);
+ }
+
+ mFabView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ newPost();
+ }
+ });
+
+ return view;
+ }
+
+ private void initSwipeToRefreshHelper() {
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(
+ getActivity(),
+ (CustomSwipeRefreshLayout) getView().findViewById(R.id.ptr_layout),
+ new RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!isAdded()) {
+ return;
+ }
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ setRefreshing(false);
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ return;
+ }
+ requestPosts(false);
+ }
+ });
+ }
+
+ public PostsListAdapter getPostListAdapter() {
+ if (mPostsListAdapter == null) {
+ mPostsListAdapter = new PostsListAdapter(getActivity(), WordPress.getCurrentBlog(), mIsPage);
+ mPostsListAdapter.setOnLoadMoreListener(this);
+ mPostsListAdapter.setOnPostsLoadedListener(this);
+ mPostsListAdapter.setOnPostSelectedListener(this);
+ mPostsListAdapter.setOnPostButtonClickListener(this);
+ }
+
+ return mPostsListAdapter;
+ }
+
+ private boolean isPostAdapterEmpty() {
+ return (mPostsListAdapter != null && mPostsListAdapter.getItemCount() == 0);
+ }
+
+ private void loadPosts() {
+ getPostListAdapter().loadPosts();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle bundle) {
+ super.onActivityCreated(bundle);
+
+ initSwipeToRefreshHelper();
+
+ // since setRetainInstance(true) is used, we only need to request latest
+ // posts the first time this is called (ie: not after device rotation)
+ if (bundle == null && NetworkUtils.checkConnection(getActivity())) {
+ requestPosts(false);
+ }
+ }
+
+ private void newPost() {
+ if (!isAdded()) return;
+
+ if (WordPress.getCurrentBlog() != null) {
+ ActivityLauncher.addNewBlogPostOrPageForResult(getActivity(), WordPress.getCurrentBlog(), mIsPage);
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.blog_not_found);
+ }
+ }
+
+ public void onResume() {
+ super.onResume();
+
+ if (WordPress.getCurrentBlog() != null && mRecyclerView.getAdapter() == null) {
+ mRecyclerView.setAdapter(getPostListAdapter());
+ }
+
+ if (WordPress.getCurrentBlog() != null) {
+ // always (re)load when resumed to reflect changes made elsewhere
+ loadPosts();
+ }
+
+ // scale in the fab after a brief delay if it's not already showing
+ if (mFabView.getVisibility() != View.VISIBLE) {
+ long delayMs = getResources().getInteger(R.integer.fab_animation_delay);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isAdded()) {
+ AniUtils.scaleIn(mFabView, AniUtils.Duration.MEDIUM);
+ }
+ }
+ }, delayMs);
+ }
+ }
+
+ public boolean isRefreshing() {
+ return mSwipeToRefreshHelper.isRefreshing();
+ }
+
+ private void setRefreshing(boolean refreshing) {
+ mSwipeToRefreshHelper.setRefreshing(refreshing);
+ }
+
+ private void requestPosts(boolean loadMore) {
+ if (!isAdded() || mIsFetchingPosts) {
+ return;
+ }
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ return;
+ }
+
+ mIsFetchingPosts = true;
+ if (loadMore) {
+ showLoadMoreProgress();
+ }
+ PostUpdateService.startServiceForBlog(getActivity(), WordPress.getCurrentLocalTableBlogId(), mIsPage, loadMore);
+ }
+
+ private void showLoadMoreProgress() {
+ if (mProgressLoadMore != null) {
+ mProgressLoadMore.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideLoadMoreProgress() {
+ if (mProgressLoadMore != null) {
+ mProgressLoadMore.setVisibility(View.GONE);
+ }
+ }
+
+ /*
+ * PostMediaService has downloaded the media info for a post's featured image, tell
+ * the adapter so it can show the featured image now that we have its URL
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PostEvents.PostMediaInfoUpdated event) {
+ if (isAdded() && WordPress.getCurrentBlog() != null) {
+ getPostListAdapter().mediaUpdated(event.getMediaId(), event.getMediaUrl());
+ }
+ }
+
+ /*
+ * upload start, reload so correct status on uploading post appears
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PostEvents.PostUploadStarted event) {
+ if (isAdded() && WordPress.getCurrentLocalTableBlogId() == event.mLocalBlogId) {
+ loadPosts();
+ }
+ }
+
+ /*
+ * upload ended, reload regardless of success/fail so correct status of uploaded post appears
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PostEvents.PostUploadEnded event) {
+ if (isAdded() && WordPress.getCurrentLocalTableBlogId() == event.mLocalBlogId) {
+ loadPosts();
+ }
+ }
+
+ /*
+ * PostUpdateService finished a request to retrieve new posts
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(PostEvents.RequestPosts event) {
+ mIsFetchingPosts = false;
+ if (isAdded() && event.getBlogId() == WordPress.getCurrentLocalTableBlogId()) {
+ setRefreshing(false);
+ hideLoadMoreProgress();
+ if (!event.getFailed()) {
+ mCanLoadMorePosts = event.canLoadMore();
+ loadPosts();
+ } else {
+ ApiHelper.ErrorType errorType = event.getErrorType();
+ if (errorType != null && errorType != ErrorType.TASK_CANCELLED && errorType != ErrorType.NO_ERROR) {
+ switch (errorType) {
+ case UNAUTHORIZED:
+ updateEmptyView(EmptyViewMessageType.PERMISSION_ERROR);
+ break;
+ default:
+ updateEmptyView(EmptyViewMessageType.GENERIC_ERROR);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void updateEmptyView(EmptyViewMessageType emptyViewMessageType) {
+ int stringId;
+ switch (emptyViewMessageType) {
+ case LOADING:
+ stringId = mIsPage ? R.string.pages_fetching : R.string.posts_fetching;
+ break;
+ case NO_CONTENT:
+ stringId = mIsPage ? R.string.pages_empty_list : R.string.posts_empty_list;
+ break;
+ case NETWORK_ERROR:
+ stringId = R.string.no_network_message;
+ break;
+ case PERMISSION_ERROR:
+ stringId = mIsPage ? R.string.error_refresh_unauthorized_pages :
+ R.string.error_refresh_unauthorized_posts;
+ break;
+ case GENERIC_ERROR:
+ stringId = mIsPage ? R.string.error_refresh_pages : R.string.error_refresh_posts;
+ break;
+ default:
+ return;
+ }
+
+ mEmptyViewTitle.setText(getText(stringId));
+ mEmptyViewImage.setVisibility(emptyViewMessageType == EmptyViewMessageType.NO_CONTENT ? View.VISIBLE : View.GONE);
+ mEmptyView.setVisibility(isPostAdapterEmpty() ? View.VISIBLE : View.GONE);
+ }
+
+ private void hideEmptyView() {
+ if (isAdded() && mEmptyView != null) {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ /*
+ * called by the adapter after posts have been loaded
+ */
+ @Override
+ public void onPostsLoaded(int postCount) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (postCount == 0 && !mIsFetchingPosts) {
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ } else {
+ updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ }
+ } else if (postCount > 0) {
+ hideEmptyView();
+ }
+ }
+
+ /*
+ * called by the adapter to load more posts when the user scrolls towards the last post
+ */
+ @Override
+ public void onLoadMore() {
+ if (mCanLoadMorePosts && !mIsFetchingPosts) {
+ requestPosts(true);
+ }
+ }
+
+ /*
+ * called by the adapter when the user clicks a post
+ */
+ @Override
+ public void onPostSelected(PostsListPost post) {
+ onPostButtonClicked(PostListButton.BUTTON_PREVIEW, post);
+ }
+
+ /*
+ * called by the adapter when the user clicks the edit/view/stats/trash button for a post
+ */
+ @Override
+ public void onPostButtonClicked(int buttonType, PostsListPost post) {
+ if (!isAdded()) return;
+
+ Post fullPost = WordPress.wpDB.getPostForLocalTablePostId(post.getPostId());
+ if (fullPost == null) {
+ ToastUtils.showToast(getActivity(), R.string.post_not_found);
+ return;
+ }
+
+ switch (buttonType) {
+ case PostListButton.BUTTON_EDIT:
+ ActivityLauncher.editBlogPostOrPageForResult(getActivity(), post.getPostId(), mIsPage);
+ break;
+ case PostListButton.BUTTON_PUBLISH:
+ publishPost(fullPost);
+ break;
+ case PostListButton.BUTTON_VIEW:
+ ActivityLauncher.browsePostOrPage(getActivity(), WordPress.getCurrentBlog(), fullPost);
+ break;
+ case PostListButton.BUTTON_PREVIEW:
+ ActivityLauncher.viewPostPreviewForResult(getActivity(), fullPost, mIsPage);
+ break;
+ case PostListButton.BUTTON_STATS:
+ ActivityLauncher.viewStatsSinglePostDetails(getActivity(), fullPost, mIsPage);
+ break;
+ case PostListButton.BUTTON_TRASH:
+ case PostListButton.BUTTON_DELETE:
+ // prevent deleting post while it's being uploaded
+ if (!post.isUploading()) {
+ trashPost(post);
+ }
+ break;
+ }
+ }
+
+ private void publishPost(final Post post) {
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ ToastUtils.showToast(getActivity(), R.string.error_publish_no_network, ToastUtils.Duration.SHORT);
+ return;
+ }
+
+ // If the post is empty, don't publish
+ if (!post.isPublishable()) {
+ ToastUtils.showToast(getActivity(), R.string.error_publish_empty_post, ToastUtils.Duration.SHORT);
+ return;
+ }
+
+ post.setPostStatus(PostStatus.toString(PostStatus.PUBLISHED));
+ post.setChangedFromDraftToPublished(true);
+
+ PostUploadService.addPostToUpload(post);
+ getActivity().startService(new Intent(getActivity(), PostUploadService.class));
+
+ PostUtils.trackSavePostAnalytics(post);
+ }
+
+ /*
+ * send the passed post to the trash with undo
+ */
+ private void trashPost(final PostsListPost post) {
+ //only check if network is available in case this is not a local draft - local drafts have not yet
+ //been posted to the server so they can be trashed w/o further care
+ if (!isAdded() || (!post.isLocalDraft() && !NetworkUtils.checkConnection(getActivity()))) {
+ return;
+ }
+
+ final Post fullPost = WordPress.wpDB.getPostForLocalTablePostId(post.getPostId());
+ if (fullPost == null) {
+ ToastUtils.showToast(getActivity(), R.string.post_not_found);
+ return;
+ }
+
+ // remove post from the list and add it to the list of trashed posts
+ getPostListAdapter().hidePost(post);
+ mTrashedPosts.add(post);
+
+ // make sure empty view shows if user deleted the only post
+ if (getPostListAdapter().getItemCount() == 0) {
+ updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+
+ View.OnClickListener undoListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // user undid the trash, so unhide the post and remove it from the list of trashed posts
+ mTrashedPosts.remove(post);
+ getPostListAdapter().unhidePost(post);
+ hideEmptyView();
+ }
+ };
+
+ // different undo text if this is a local draft since it will be deleted rather than trashed
+ String text;
+ if (post.isLocalDraft()) {
+ text = mIsPage ? getString(R.string.page_deleted) : getString(R.string.post_deleted);
+ } else {
+ text = mIsPage ? getString(R.string.page_trashed) : getString(R.string.post_trashed);
+ }
+
+ Snackbar snackbar = Snackbar.make(getView().findViewById(R.id.coordinator), text, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, undoListener);
+
+ // wait for the undo snackbar to disappear before actually deleting the post
+ snackbar.setCallback(new Snackbar.Callback() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+
+ // if the post no longer exists in the list of trashed posts it's because the
+ // user undid the trash, so don't perform the deletion
+ if (!mTrashedPosts.contains(post)) {
+ return;
+ }
+
+ // remove from the list of trashed posts in case onDismissed is called multiple
+ // times - this way the above check prevents us making the call to delete it twice
+ // https://code.google.com/p/android/issues/detail?id=190529
+ mTrashedPosts.remove(post);
+
+ WordPress.wpDB.deletePost(fullPost);
+
+ if (!post.isLocalDraft()) {
+ new ApiHelper.DeleteSinglePostTask().execute(WordPress.getCurrentBlog(),
+ fullPost.getRemotePostId(), mIsPage);
+ }
+ }
+ });
+
+ snackbar.show();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PromoDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PromoDialog.java
new file mode 100644
index 000000000..71b76a50e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PromoDialog.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.graphics.drawable.VectorDrawableCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.widgets.WPTextView;
+
+public class PromoDialog extends DialogFragment {
+ private int mDrawableId;
+ private int mTitleId;
+ private int mDescriptionId;
+ private int mButtonLabelId;
+
+ public static PromoDialog newInstance(int drawableId, int titleId, int descriptionId, int buttonLabelId) {
+ PromoDialog fragment = new PromoDialog();
+ Bundle args = new Bundle();
+ args.putInt("drawableId", drawableId);
+ args.putInt("titleId", titleId);
+ args.putInt("descriptionId", descriptionId);
+ args.putInt("buttonLabelId", buttonLabelId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ mDrawableId = getArguments().getInt("drawableId");
+ mTitleId = getArguments().getInt("titleId");
+ mDescriptionId = getArguments().getInt("descriptionId");
+ mButtonLabelId = getArguments().getInt("buttonLabelId");
+ // request a window without the title
+ dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.setCancelable(false);
+ return dialog;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.promo_dialog, container);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ Button btn = (Button) view.findViewById(R.id.promo_dialog_cancel_button);
+ btn.setText(mButtonLabelId);
+ ImageView image = (ImageView) view.findViewById(R.id.promo_dialog_image);
+ Drawable drawable = VectorDrawableCompat.create(getResources(), mDrawableId, null);
+ image.setImageDrawable(drawable);
+ WPTextView title = (WPTextView) view.findViewById(R.id.promo_dialog_title);
+ title.setText(mTitleId);
+ WPTextView desc = (WPTextView) view.findViewById(R.id.promo_dialog_description);
+ desc.setText(mDescriptionId);
+ btn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getDialog().cancel();
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java
new file mode 100644
index 000000000..aedf94767
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java
@@ -0,0 +1,415 @@
+package org.wordpress.android.ui.posts;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.util.SparseBooleanArray;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryNode;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.helpers.ListScrollPositionManager;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+public class SelectCategoriesActivity extends AppCompatActivity {
+ String finalResult = "";
+ private final Handler mHandler = new Handler();
+ private Blog blog;
+ private ListView mListView;
+ private TextView mEmptyView;
+ private ListScrollPositionManager mListScrollPositionManager;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private HashSet<String> mSelectedCategories;
+ private CategoryNode mCategories;
+ private ArrayList<CategoryNode> mCategoryLevels;
+ private Map<String, Integer> mCategoryNames = new HashMap<String, Integer>();
+ XMLRPCClientInterface mClient;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.select_categories);
+ setTitle(getResources().getString(R.string.select_categories));
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mListView = (ListView)findViewById(android.R.id.list);
+ mListScrollPositionManager = new ListScrollPositionManager(mListView, false);
+ mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ mListView.setItemsCanFocus(false);
+
+ mEmptyView = (TextView) findViewById(R.id.empty_view);
+ mListView.setEmptyView(mEmptyView);
+
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
+ if (getCheckedItemCount(mListView) > 1) {
+ boolean uncategorizedNeedToBeSelected = false;
+ for (int i = 0; i < mCategoryLevels.size(); i++) {
+ if (mCategoryLevels.get(i).getName().equalsIgnoreCase("uncategorized")) {
+ mListView.setItemChecked(i, uncategorizedNeedToBeSelected);
+ }
+ }
+ }
+ }
+ });
+
+ mSelectedCategories = new HashSet<String>();
+
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ int blogId = extras.getInt("id");
+ blog = WordPress.wpDB.instantiateBlogByLocalId(blogId);
+ if (blog == null) {
+ Toast.makeText(this, getResources().getText(R.string.blog_not_found), Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ if (extras.containsKey("categories")) {
+ mSelectedCategories = (HashSet<String>) extras.getSerializable("categories");
+ }
+ }
+ if (mSelectedCategories == null) {
+ mSelectedCategories = new HashSet<String>();
+ }
+
+ // swipe to refresh setup
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+ refreshCategories();
+ }
+ });
+
+ populateCategoryList();
+
+ if (NetworkUtils.isNetworkAvailable(this)) {
+ mEmptyView.setText(R.string.empty_list_default);
+ if (isCategoryListEmpty()) {
+ refreshCategories();
+ }
+ } else {
+ mEmptyView.setText(R.string.no_network_title);
+ }
+ }
+
+ private boolean isCategoryListEmpty() {
+ if (mListView.getAdapter() != null) {
+ return mListView.getAdapter().isEmpty();
+ } else {
+ return true;
+ }
+ }
+
+ private void populateCategoryList() {
+ mCategories = CategoryNode.createCategoryTreeFromDB(blog.getLocalTableBlogId());
+ mCategoryLevels = CategoryNode.getSortedListOfCategoriesFromRoot(mCategories);
+ for (int i = 0; i < mCategoryLevels.size(); i++) {
+ mCategoryNames.put(StringUtils.unescapeHTML(mCategoryLevels.get(i).getName()), i);
+ }
+
+ CategoryArrayAdapter categoryAdapter = new CategoryArrayAdapter(this, R.layout.categories_row, mCategoryLevels);
+ mListView.setAdapter(categoryAdapter);
+ if (mSelectedCategories != null) {
+ for (String selectedCategory : mSelectedCategories) {
+ if (mCategoryNames.keySet().contains(selectedCategory)) {
+ mListView.setItemChecked(mCategoryNames.get(selectedCategory), true);
+ }
+ }
+ }
+ mListScrollPositionManager.restoreScrollOffset();
+ }
+
+ final Runnable mUpdateResults = new Runnable() {
+ public void run() {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ if (finalResult.equals("addCategory_success")) {
+ populateCategoryList();
+ if (!isFinishing()) {
+ ToastUtils.showToast(SelectCategoriesActivity.this, R.string.adding_cat_success, Duration.SHORT);
+ }
+ } else if (finalResult.equals("addCategory_failed")) {
+ if (!isFinishing()) {
+ ToastUtils.showToast(SelectCategoriesActivity.this, R.string.adding_cat_failed, Duration.LONG);
+ }
+ } else if (finalResult.equals("gotCategories")) {
+ populateCategoryList();
+ } else if (finalResult.equals("FAIL")) {
+ if (!isFinishing()) {
+ ToastUtils.showToast(SelectCategoriesActivity.this, R.string.category_refresh_error, Duration.LONG);
+ }
+ }
+ }
+ };
+
+ /**
+ * Gets the categories via a xmlrpc call
+ * @return result message
+ */
+ public String fetchCategories() {
+ String returnMessage;
+ Object result[] = null;
+ Object[] params = {blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(),};
+ mClient = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(), blog.getHttppassword());
+ boolean success = false;
+ try {
+ result = (Object[]) mClient.call(Method.GET_CATEGORIES, params);
+ success = true;
+ } catch (XMLRPCException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ }
+
+ if (success) {
+ // wipe out the categories table
+ WordPress.wpDB.clearCategories(blog.getLocalTableBlogId());
+
+ for (Object aResult : result) {
+ Map<?, ?> curHash = (Map<?, ?>) aResult;
+ String categoryName = curHash.get("categoryName").toString();
+ String categoryID = curHash.get("categoryId").toString();
+ String categoryParentID = curHash.get("parentId").toString();
+ int convertedCategoryID = Integer.parseInt(categoryID);
+ int convertedCategoryParentID = Integer.parseInt(categoryParentID);
+ WordPress.wpDB.insertCategory(blog.getLocalTableBlogId(), convertedCategoryID, convertedCategoryParentID, categoryName);
+ }
+ returnMessage = "gotCategories";
+ } else {
+ returnMessage = "FAIL";
+ }
+ return returnMessage;
+ }
+
+ public String addCategory(final String category_name, String category_slug, String category_desc, int parent_id) {
+ // Return string
+ String returnString = "addCategory_failed";
+
+ // Save selected categories
+ updateSelectedCategoryList();
+ mListScrollPositionManager.saveScrollOffset();
+
+ // Store the parameters for wp.addCategory
+ Map<String, Object> struct = new HashMap<String, Object>();
+ struct.put("name", category_name);
+ struct.put("slug", category_slug);
+ struct.put("description", category_desc);
+ struct.put("parent_id", parent_id);
+ mClient = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(), blog.getHttppassword());
+ Object[] params = { blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(), struct };
+
+ Object result = null;
+ try {
+ result = mClient.call(Method.NEW_CATEGORY, params);
+ } catch (XMLRPCException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ }
+
+ if (result != null) {
+ // Category successfully created. "result" is the ID of the new category
+ // Initialize the category database
+ // Convert "result" (= category_id) from type Object to int
+ int category_id = Integer.parseInt(result.toString());
+
+ // Fetch canonical name, can't to do this asynchronously because the new category_name is needed for
+ // insertCategory
+ final String new_category_name = getCanonicalCategoryName(category_id);
+ if (new_category_name == null) {
+ return returnString;
+ }
+ final Activity that = this;
+ if (!new_category_name.equals(category_name)) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(that, String.format(String.valueOf(getText(R.string.category_automatically_renamed)),
+ category_name, new_category_name), Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ // Insert the new category into database
+ WordPress.wpDB.insertCategory(blog.getLocalTableBlogId(), category_id, parent_id, new_category_name);
+ returnString = "addCategory_success";
+ // auto select new category
+ mSelectedCategories.add(new_category_name);
+ }
+
+ return returnString;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (data != null) {
+ final Bundle extras = data.getExtras();
+
+ switch (requestCode) {
+ case 0: // Add category
+ // Does the user want to continue, or did he press "dismiss"?
+ if (extras.getString("continue").equals("TRUE")) {
+ // Get name, slug and desc from Intent
+ final String category_name = extras.getString("category_name");
+ final String category_slug = extras.getString("category_slug");
+ final String category_desc = extras.getString("category_desc");
+ final int parent_id = extras.getInt("parent_id");
+
+ // Check if the category name already exists
+ if (!mCategoryNames.keySet().contains(category_name)) {
+ mSwipeToRefreshHelper.setRefreshing(true);
+ Thread th = new Thread() {
+ public void run() {
+ finalResult = addCategory(category_name, category_slug, category_desc, parent_id);
+ mHandler.post(mUpdateResults);
+ }
+ };
+ th.start();
+ }
+ break;
+ }
+ }// end null check
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.categories, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == R.id.menu_new_category) {
+ if (NetworkUtils.checkConnection(this)) {
+ Bundle bundle = new Bundle();
+ bundle.putInt("id", blog.getLocalTableBlogId());
+ Intent i = new Intent(SelectCategoriesActivity.this, AddCategoryActivity.class);
+ i.putExtras(bundle);
+ startActivityForResult(i, 0);
+ }
+ return true;
+ } else if (itemId == android.R.id.home) {
+ saveAndFinish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private String getCanonicalCategoryName(int category_id) {
+ String new_category_name = null;
+ Map<?, ?> result = null;
+ Object[] params = { blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(), "category", category_id };
+ mClient = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(), blog.getHttppassword());
+ try {
+ result = (Map<?, ?>) mClient.call(Method.GET_TERM, params);
+ } catch (XMLRPCException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ }
+
+ if (result != null) {
+ if (result.containsKey("name")) {
+ new_category_name = result.get("name").toString();
+ }
+ }
+ return new_category_name;
+ }
+
+ private void refreshCategories() {
+ mSwipeToRefreshHelper.setRefreshing(true);
+ mListScrollPositionManager.saveScrollOffset();
+ updateSelectedCategoryList();
+ Thread th = new Thread() {
+ public void run() {
+ finalResult = fetchCategories();
+ mHandler.post(mUpdateResults);
+ }
+ };
+ th.start();
+ }
+
+ @Override
+ public void onBackPressed() {
+ saveAndFinish();
+ super.onBackPressed();
+ }
+
+ private void updateSelectedCategoryList() {
+ SparseBooleanArray selectedItems = mListView.getCheckedItemPositions();
+ for (int i = 0; i < selectedItems.size(); i++) {
+ String currentName = StringUtils.unescapeHTML(mCategoryLevels.get(selectedItems.keyAt(i)).getName());
+ if (selectedItems.get(selectedItems.keyAt(i))) {
+ mSelectedCategories.add(currentName);
+ } else {
+ mSelectedCategories.remove(currentName);
+ }
+ }
+ }
+
+ private void saveAndFinish() {
+ Bundle bundle = new Bundle();
+ updateSelectedCategoryList();
+ bundle.putSerializable("selectedCategories", new ArrayList<String>(mSelectedCategories));
+ Intent mIntent = new Intent();
+ mIntent.putExtras(bundle);
+ setResult(RESULT_OK, mIntent);
+ finish();
+ }
+
+ private int getCheckedItemCount(ListView listView) {
+ return listView.getCheckedItemCount();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PageMenuAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PageMenuAdapter.java
new file mode 100644
index 000000000..3279df2dd
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PageMenuAdapter.java
@@ -0,0 +1,97 @@
+package org.wordpress.android.ui.posts.adapters;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.models.PostsListPost;
+import org.wordpress.android.widgets.PostListButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * adapter for the popup menu that appears when clicking "..." in the pages list - each item
+ * in the menu item array is an integer that matches a specific PostListButton button type
+ */
+public class PageMenuAdapter extends BaseAdapter {
+
+ private final LayoutInflater mInflater;
+ private final List<Integer> mMenuItems = new ArrayList<>();
+
+ public PageMenuAdapter(Context context, @NonNull PostsListPost page) {
+ super();
+ mInflater = LayoutInflater.from(context);
+
+ boolean showViewItem = !page.isLocalDraft() && page.getStatusEnum() == PostStatus.PUBLISHED;
+ boolean showStatsItem = !page.isLocalDraft() && page.getStatusEnum() == PostStatus.PUBLISHED;
+ boolean showTrashItem = !page.isLocalDraft();
+ boolean showDeleteItem = !showTrashItem;
+
+ // edit item always appears
+ mMenuItems.add(PostListButton.BUTTON_EDIT);
+
+ if (showViewItem) {
+ mMenuItems.add(PostListButton.BUTTON_VIEW);
+ }
+ if (showStatsItem) {
+ mMenuItems.add(PostListButton.BUTTON_STATS);
+ }
+ if (showTrashItem) {
+ mMenuItems.add(PostListButton.BUTTON_TRASH);
+ }
+ if (showDeleteItem) {
+ mMenuItems.add(PostListButton.BUTTON_DELETE);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return mMenuItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ PageMenuHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.popup_menu_item, parent, false);
+ holder = new PageMenuHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (PageMenuHolder) convertView.getTag();
+ }
+
+ int buttonType = mMenuItems.get(position);
+ holder.text.setText(PostListButton.getButtonTextResId(buttonType));
+ holder.icon.setImageResource(PostListButton.getButtonIconResId(buttonType));
+
+ return convertView;
+ }
+
+ class PageMenuHolder {
+ private final TextView text;
+ private final ImageView icon;
+
+ PageMenuHolder(View view) {
+ text = (TextView) view.findViewById(R.id.text);
+ icon = (ImageView) view.findViewById(R.id.image);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PostsListAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PostsListAdapter.java
new file mode 100644
index 000000000..4439349a2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/PostsListAdapter.java
@@ -0,0 +1,718 @@
+package org.wordpress.android.ui.posts.adapters;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.ListPopupWindow;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.AdapterView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.models.PostsListPost;
+import org.wordpress.android.models.PostsListPostList;
+import org.wordpress.android.ui.posts.PostUtils;
+import org.wordpress.android.ui.posts.PostsListFragment;
+import org.wordpress.android.ui.posts.services.PostMediaService;
+import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.widgets.PostListButton;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Adapter for Posts/Pages list
+ */
+public class PostsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ public interface OnPostButtonClickListener {
+ void onPostButtonClicked(int buttonId, PostsListPost post);
+ }
+
+ private OnLoadMoreListener mOnLoadMoreListener;
+ private OnPostsLoadedListener mOnPostsLoadedListener;
+ private OnPostSelectedListener mOnPostSelectedListener;
+ private OnPostButtonClickListener mOnPostButtonClickListener;
+
+ private final int mLocalTableBlogId;
+ private final int mPhotonWidth;
+ private final int mPhotonHeight;
+ private final int mEndlistIndicatorHeight;
+
+ private final boolean mIsPage;
+ private final boolean mIsPrivateBlog;
+ private final boolean mIsStatsSupported;
+ private final boolean mAlwaysShowAllButtons;
+
+ private boolean mIsLoadingPosts;
+
+ private final PostsListPostList mPosts = new PostsListPostList();
+ private final LayoutInflater mLayoutInflater;
+
+ private final List<PostsListPost> mHiddenPosts = new ArrayList<>();
+
+ private static final long ROW_ANIM_DURATION = 150;
+
+ private static final int VIEW_TYPE_POST_OR_PAGE = 0;
+ private static final int VIEW_TYPE_ENDLIST_INDICATOR = 1;
+
+ public PostsListAdapter(Context context, @NonNull Blog blog, boolean isPage) {
+ mIsPage = isPage;
+ mLayoutInflater = LayoutInflater.from(context);
+
+ mLocalTableBlogId = blog.getLocalTableBlogId();
+ mIsPrivateBlog = blog.isPrivate();
+ mIsStatsSupported = blog.isDotcomFlag() || blog.isJetpackPowered();
+
+ int displayWidth = DisplayUtils.getDisplayPixelWidth(context);
+ int contentSpacing = context.getResources().getDimensionPixelSize(R.dimen.content_margin);
+ mPhotonWidth = displayWidth - (contentSpacing * 2);
+ mPhotonHeight = context.getResources().getDimensionPixelSize(R.dimen.reader_featured_image_height);
+
+ // endlist indicator height is hard-coded here so that its horz line is in the middle of the fab
+ mEndlistIndicatorHeight = DisplayUtils.dpToPx(context, mIsPage ? 82 : 74);
+
+ // on larger displays we can always show all buttons
+ mAlwaysShowAllButtons = (displayWidth >= 1080);
+ }
+
+ public void setOnLoadMoreListener(OnLoadMoreListener listener) {
+ mOnLoadMoreListener = listener;
+ }
+
+ public void setOnPostsLoadedListener(OnPostsLoadedListener listener) {
+ mOnPostsLoadedListener = listener;
+ }
+
+ public void setOnPostSelectedListener(OnPostSelectedListener listener) {
+ mOnPostSelectedListener = listener;
+ }
+
+ public void setOnPostButtonClickListener(OnPostButtonClickListener listener) {
+ mOnPostButtonClickListener = listener;
+ }
+
+ private PostsListPost getItem(int position) {
+ if (isValidPostPosition(position)) {
+ return mPosts.get(position);
+ }
+ return null;
+ }
+
+ private boolean isValidPostPosition(int position) {
+ return (position >= 0 && position < mPosts.size());
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == mPosts.size()) {
+ return VIEW_TYPE_ENDLIST_INDICATOR;
+ }
+ return VIEW_TYPE_POST_OR_PAGE;
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mPosts.size() == 0) {
+ return 0;
+ } else {
+ return mPosts.size() + 1; // +1 for the endlist indicator
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (viewType == VIEW_TYPE_ENDLIST_INDICATOR) {
+ View view = mLayoutInflater.inflate(R.layout.endlist_indicator, parent, false);
+ view.getLayoutParams().height = mEndlistIndicatorHeight;
+ return new EndListViewHolder(view);
+ } else if (mIsPage) {
+ View view = mLayoutInflater.inflate(R.layout.page_item, parent, false);
+ return new PageViewHolder(view);
+ } else {
+ View view = mLayoutInflater.inflate(R.layout.post_cardview, parent, false);
+ return new PostViewHolder(view);
+ }
+ }
+
+ private boolean canShowStatsForPost(PostsListPost post) {
+ return mIsStatsSupported
+ && post.getStatusEnum() == PostStatus.PUBLISHED
+ && !post.isLocalDraft()
+ && !post.hasLocalChanges();
+ }
+
+ private boolean canPublishPost(PostsListPost post) {
+ return post != null && !post.isUploading() &&
+ (post.hasLocalChanges() || post.isLocalDraft() || post.getStatusEnum() == PostStatus.DRAFT);
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ // nothing to do if this is the static endlist indicator
+ if (getItemViewType(position) == VIEW_TYPE_ENDLIST_INDICATOR) {
+ return;
+ }
+
+ final PostsListPost post = mPosts.get(position);
+ Context context = holder.itemView.getContext();
+
+ if (holder instanceof PostViewHolder) {
+ PostViewHolder postHolder = (PostViewHolder) holder;
+
+ if (post.hasTitle()) {
+ postHolder.txtTitle.setText(post.getTitle());
+ } else {
+ postHolder.txtTitle.setText("(" + context.getResources().getText(R.string.untitled) + ")");
+ }
+
+ if (post.hasExcerpt()) {
+ postHolder.txtExcerpt.setVisibility(View.VISIBLE);
+ postHolder.txtExcerpt.setText(PostUtils.collapseShortcodes(post.getExcerpt()));
+ } else {
+ postHolder.txtExcerpt.setVisibility(View.GONE);
+ }
+
+ if (post.hasFeaturedImageId() || post.hasFeaturedImageUrl()) {
+ postHolder.imgFeatured.setVisibility(View.VISIBLE);
+ postHolder.imgFeatured.setImageUrl(post.getFeaturedImageUrl(), WPNetworkImageView.ImageType.PHOTO);
+ } else {
+ postHolder.imgFeatured.setVisibility(View.GONE);
+ }
+
+ // local drafts say "delete" instead of "trash"
+ if (post.isLocalDraft()) {
+ postHolder.txtDate.setVisibility(View.GONE);
+ postHolder.btnTrash.setButtonType(PostListButton.BUTTON_DELETE);
+ } else {
+ postHolder.txtDate.setText(post.getFormattedDate());
+ postHolder.txtDate.setVisibility(View.VISIBLE);
+ postHolder.btnTrash.setButtonType(PostListButton.BUTTON_TRASH);
+ }
+
+ if (post.isUploading()) {
+ postHolder.disabledOverlay.setVisibility(View.VISIBLE);
+ } else {
+ postHolder.disabledOverlay.setVisibility(View.GONE);
+ }
+
+ updateStatusText(postHolder.txtStatus, post);
+ configurePostButtons(postHolder, post);
+ } else if (holder instanceof PageViewHolder) {
+ PageViewHolder pageHolder = (PageViewHolder) holder;
+ if (post.hasTitle()) {
+ pageHolder.txtTitle.setText(post.getTitle());
+ } else {
+ pageHolder.txtTitle.setText("(" + context.getResources().getText(R.string.untitled) + ")");
+ }
+
+ String dateStr = getPageDateHeaderText(context, post);
+ pageHolder.txtDate.setText(dateStr);
+
+ updateStatusText(pageHolder.txtStatus, post);
+
+ // don't show date header if same as previous
+ boolean showDate;
+ if (position > 0) {
+ String prevDateStr = getPageDateHeaderText(context, mPosts.get(position - 1));
+ showDate = !prevDateStr.equals(dateStr);
+ } else {
+ showDate = true;
+ }
+ pageHolder.dateHeader.setVisibility(showDate ? View.VISIBLE : View.GONE);
+
+ // no "..." more button when uploading
+ pageHolder.btnMore.setVisibility(post.isUploading() ? View.GONE : View.VISIBLE);
+ pageHolder.btnMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showPagePopupMenu(v, post);
+ }
+ });
+
+ // only show the top divider for the first item
+ pageHolder.dividerTop.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
+
+ if (post.isUploading()) {
+ pageHolder.disabledOverlay.setVisibility(View.VISIBLE);
+ } else {
+ pageHolder.disabledOverlay.setVisibility(View.GONE);
+ }
+ }
+
+ // load more posts when we near the end
+ if (mOnLoadMoreListener != null && position >= mPosts.size() - 1
+ && position >= PostsListFragment.POSTS_REQUEST_COUNT - 1) {
+ mOnLoadMoreListener.onLoadMore();
+ }
+
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnPostSelectedListener != null) {
+ mOnPostSelectedListener.onPostSelected(post);
+ }
+ }
+ });
+ }
+
+ /*
+ * returns the caption to show in the date header for the passed page - pages with the same
+ * caption will be grouped together
+ * - if page is local draft, returns "Local draft"
+ * - if page is scheduled, returns formatted date w/o time
+ * - if created today or yesterday, returns "Today" or "Yesterday"
+ * - if created this month, returns the number of days ago
+ * - if created this year, returns the month name
+ * - if created before this year, returns the month name with year
+ */
+ private static String getPageDateHeaderText(Context context, PostsListPost page) {
+ if (page.isLocalDraft()) {
+ return context.getString(R.string.local_draft);
+ } else if (page.getStatusEnum() == PostStatus.SCHEDULED) {
+ return DateUtils.formatDateTime(context, page.getDateCreatedGmt(), DateUtils.FORMAT_ABBREV_ALL);
+ } else {
+ Date dtCreated = new Date(page.getDateCreatedGmt());
+ Date dtNow = DateTimeUtils.nowUTC();
+ int daysBetween = DateTimeUtils.daysBetween(dtCreated, dtNow);
+ if (daysBetween == 0) {
+ return context.getString(R.string.today);
+ } else if (daysBetween == 1) {
+ return context.getString(R.string.yesterday);
+ } else if (DateTimeUtils.isSameMonthAndYear(dtCreated, dtNow)) {
+ return String.format(context.getString(R.string.days_ago), daysBetween);
+ } else if (DateTimeUtils.isSameYear(dtCreated, dtNow)) {
+ return new SimpleDateFormat("MMMM").format(dtCreated);
+ } else {
+ return new SimpleDateFormat("MMMM yyyy").format(dtCreated);
+ }
+ }
+ }
+
+ /*
+ * user tapped "..." next to a page, show a popup menu of choices
+ */
+ private void showPagePopupMenu(View view, final PostsListPost page) {
+ Context context = view.getContext();
+ final ListPopupWindow listPopup = new ListPopupWindow(context);
+ listPopup.setAnchorView(view);
+
+ listPopup.setWidth(context.getResources().getDimensionPixelSize(R.dimen.menu_item_width));
+ listPopup.setModal(true);
+ listPopup.setAdapter(new PageMenuAdapter(context, page));
+ listPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ listPopup.dismiss();
+ if (mOnPostButtonClickListener != null) {
+ int buttonId = (int) id;
+ mOnPostButtonClickListener.onPostButtonClicked(buttonId, page);
+ }
+ }
+ });
+ listPopup.show();
+ }
+
+ private void updateStatusText(TextView txtStatus, PostsListPost post) {
+ if ((post.getStatusEnum() == PostStatus.PUBLISHED) && !post.isLocalDraft() && !post.hasLocalChanges()) {
+ txtStatus.setVisibility(View.GONE);
+ } else {
+ int statusTextResId = 0;
+ int statusIconResId = 0;
+ int statusColorResId = R.color.grey_darken_10;
+
+ if (post.isUploading()) {
+ statusTextResId = R.string.post_uploading;
+ statusColorResId = R.color.alert_yellow;
+ } else if (post.isLocalDraft()) {
+ statusTextResId = R.string.local_draft;
+ statusIconResId = R.drawable.noticon_scheduled;
+ statusColorResId = R.color.alert_yellow;
+ } else if (post.hasLocalChanges()) {
+ statusTextResId = R.string.local_changes;
+ statusIconResId = R.drawable.noticon_scheduled;
+ statusColorResId = R.color.alert_yellow;
+ } else {
+ switch (post.getStatusEnum()) {
+ case DRAFT:
+ statusTextResId = R.string.draft;
+ statusIconResId = R.drawable.noticon_scheduled;
+ statusColorResId = R.color.alert_yellow;
+ break;
+ case PRIVATE:
+ statusTextResId = R.string.post_private;
+ break;
+ case PENDING:
+ statusTextResId = R.string.pending_review;
+ statusIconResId = R.drawable.noticon_scheduled;
+ statusColorResId = R.color.alert_yellow;
+ break;
+ case SCHEDULED:
+ statusTextResId = R.string.scheduled;
+ statusIconResId = R.drawable.noticon_scheduled;
+ statusColorResId = R.color.alert_yellow;
+ break;
+ case TRASHED:
+ statusTextResId = R.string.trashed;
+ statusIconResId = R.drawable.noticon_trashed;
+ statusColorResId = R.color.alert_red;
+ break;
+ }
+ }
+
+ Resources resources = txtStatus.getContext().getResources();
+ txtStatus.setTextColor(resources.getColor(statusColorResId));
+ txtStatus.setText(statusTextResId != 0 ? resources.getString(statusTextResId) : "");
+ Drawable drawable = (statusIconResId != 0 ? resources.getDrawable(statusIconResId) : null);
+ txtStatus.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ txtStatus.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void configurePostButtons(final PostViewHolder holder,
+ final PostsListPost post) {
+ // posts with local changes have preview rather than view button
+ if (post.isLocalDraft() || post.hasLocalChanges()) {
+ holder.btnView.setButtonType(PostListButton.BUTTON_PREVIEW);
+ } else {
+ holder.btnView.setButtonType(PostListButton.BUTTON_VIEW);
+ }
+
+ boolean canShowStatsButton = canShowStatsForPost(post);
+ boolean canShowPublishButton = canPublishPost(post);
+
+ int numVisibleButtons = 3;
+ if (canShowPublishButton) numVisibleButtons++;
+ if (canShowStatsButton) numVisibleButtons++;
+
+ // edit / view are always visible
+ holder.btnEdit.setVisibility(View.VISIBLE);
+ holder.btnView.setVisibility(View.VISIBLE);
+
+ // if we have enough room to show all buttons, hide the back/more buttons and show stats/trash/publish
+ if (mAlwaysShowAllButtons || numVisibleButtons <= 3) {
+ holder.btnMore.setVisibility(View.GONE);
+ holder.btnBack.setVisibility(View.GONE);
+ holder.btnTrash.setVisibility(View.VISIBLE);
+ holder.btnStats.setVisibility(canShowStatsButton ? View.VISIBLE : View.GONE);
+ holder.btnPublish.setVisibility(canShowPublishButton ? View.VISIBLE : View.GONE);
+ } else {
+ holder.btnMore.setVisibility(View.VISIBLE);
+ holder.btnBack.setVisibility(View.GONE);
+ holder.btnTrash.setVisibility(View.GONE);
+ holder.btnStats.setVisibility(View.GONE);
+ holder.btnPublish.setVisibility(View.GONE);
+ }
+
+ View.OnClickListener btnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // handle back/more here, pass other actions to activity/fragment
+ int buttonType = ((PostListButton) view).getButtonType();
+ switch (buttonType) {
+ case PostListButton.BUTTON_MORE:
+ animateButtonRows(holder, post, false);
+ break;
+ case PostListButton.BUTTON_BACK:
+ animateButtonRows(holder, post, true);
+ break;
+ default:
+ if (mOnPostButtonClickListener != null) {
+ mOnPostButtonClickListener.onPostButtonClicked(buttonType, post);
+ }
+ break;
+ }
+ }
+ };
+ holder.btnEdit.setOnClickListener(btnClickListener);
+ holder.btnView.setOnClickListener(btnClickListener);
+ holder.btnStats.setOnClickListener(btnClickListener);
+ holder.btnTrash.setOnClickListener(btnClickListener);
+ holder.btnMore.setOnClickListener(btnClickListener);
+ holder.btnBack.setOnClickListener(btnClickListener);
+ holder.btnPublish.setOnClickListener(btnClickListener);
+ }
+
+ /*
+ * buttons may appear in two rows depending on display size and number of visible
+ * buttons - these rows are toggled through the "more" and "back" buttons - this
+ * routine is used to animate the new row in and the old row out
+ */
+ private void animateButtonRows(final PostViewHolder holder,
+ final PostsListPost post,
+ final boolean showRow1) {
+ // first animate out the button row, then show/hide the appropriate buttons,
+ // then animate the row layout back in
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 0f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 0f);
+ ObjectAnimator animOut = ObjectAnimator.ofPropertyValuesHolder(holder.layoutButtons, scaleX, scaleY);
+ animOut.setDuration(ROW_ANIM_DURATION);
+ animOut.setInterpolator(new AccelerateInterpolator());
+
+ animOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+
+ // row 1
+ holder.btnEdit.setVisibility(showRow1 ? View.VISIBLE : View.GONE);
+ holder.btnView.setVisibility(showRow1 ? View.VISIBLE : View.GONE);
+ holder.btnMore.setVisibility(showRow1 ? View.VISIBLE : View.GONE);
+ // row 2
+ holder.btnStats.setVisibility(!showRow1 && canShowStatsForPost(post) ? View.VISIBLE : View.GONE);
+ holder.btnPublish.setVisibility(!showRow1 && canPublishPost(post) ? View.VISIBLE : View.GONE);
+ holder.btnTrash.setVisibility(!showRow1 ? View.VISIBLE : View.GONE);
+ holder.btnBack.setVisibility(!showRow1 ? View.VISIBLE : View.GONE);
+
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 0f, 1f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f, 1f);
+ ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(holder.layoutButtons, scaleX, scaleY);
+ animIn.setDuration(ROW_ANIM_DURATION);
+ animIn.setInterpolator(new DecelerateInterpolator());
+ animIn.start();
+ }
+ });
+
+ animOut.start();
+ }
+
+ public void loadPosts() {
+ if (mIsLoadingPosts) {
+ AppLog.d(AppLog.T.POSTS, "post adapter > already loading posts");
+ } else {
+ new LoadPostsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ /*
+ * hides the post - used when the post is trashed by the user but the network request
+ * to delete the post hasn't completed yet
+ */
+ public void hidePost(PostsListPost post) {
+ mHiddenPosts.add(post);
+
+ int position = mPosts.indexOfPost(post);
+ if (position > -1) {
+ mPosts.remove(position);
+ if (mPosts.size() > 0) {
+ notifyItemRemoved(position);
+
+ //when page is removed update the next one in case we need to show a header
+ if (mIsPage) {
+ notifyItemChanged(position);
+ }
+ } else {
+ // we must call notifyDataSetChanged when the only post has been deleted - if we
+ // call notifyItemRemoved the recycler will throw an IndexOutOfBoundsException
+ // because removing the last post also removes the end list indicator
+ notifyDataSetChanged();
+ }
+ }
+ }
+
+ public void unhidePost(PostsListPost post) {
+ if (mHiddenPosts.remove(post)) {
+ loadPosts();
+ }
+ }
+
+ public interface OnLoadMoreListener {
+ void onLoadMore();
+ }
+
+ public interface OnPostSelectedListener {
+ void onPostSelected(PostsListPost post);
+ }
+
+ public interface OnPostsLoadedListener {
+ void onPostsLoaded(int postCount);
+ }
+
+ class PostViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtTitle;
+ private final TextView txtExcerpt;
+ private final TextView txtDate;
+ private final TextView txtStatus;
+
+ private final PostListButton btnEdit;
+ private final PostListButton btnView;
+ private final PostListButton btnPublish;
+ private final PostListButton btnMore;
+
+ private final PostListButton btnStats;
+ private final PostListButton btnTrash;
+ private final PostListButton btnBack;
+
+ private final WPNetworkImageView imgFeatured;
+ private final ViewGroup layoutButtons;
+
+ private final View disabledOverlay;
+
+ public PostViewHolder(View view) {
+ super(view);
+
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtExcerpt = (TextView) view.findViewById(R.id.text_excerpt);
+ txtDate = (TextView) view.findViewById(R.id.text_date);
+ txtStatus = (TextView) view.findViewById(R.id.text_status);
+
+ btnEdit = (PostListButton) view.findViewById(R.id.btn_edit);
+ btnView = (PostListButton) view.findViewById(R.id.btn_view);
+ btnPublish = (PostListButton) view.findViewById(R.id.btn_publish);
+ btnMore = (PostListButton) view.findViewById(R.id.btn_more);
+
+ btnStats = (PostListButton) view.findViewById(R.id.btn_stats);
+ btnTrash = (PostListButton) view.findViewById(R.id.btn_trash);
+ btnBack = (PostListButton) view.findViewById(R.id.btn_back);
+
+ imgFeatured = (WPNetworkImageView) view.findViewById(R.id.image_featured);
+ layoutButtons = (ViewGroup) view.findViewById(R.id.layout_buttons);
+
+ disabledOverlay = view.findViewById(R.id.disabled_overlay);
+ }
+ }
+
+ class PageViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtTitle;
+ private final TextView txtDate;
+ private final TextView txtStatus;
+ private final ViewGroup dateHeader;
+ private final View btnMore;
+ private final View dividerTop;
+ private final View disabledOverlay;
+
+ public PageViewHolder(View view) {
+ super(view);
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtStatus = (TextView) view.findViewById(R.id.text_status);
+ btnMore = view.findViewById(R.id.btn_more);
+ dividerTop = view.findViewById(R.id.divider_top);
+ dateHeader = (ViewGroup) view.findViewById(R.id.header_date);
+ txtDate = (TextView) dateHeader.findViewById(R.id.text_date);
+ disabledOverlay = view.findViewById(R.id.disabled_overlay);
+ }
+ }
+
+ class EndListViewHolder extends RecyclerView.ViewHolder {
+ public EndListViewHolder(View view) {
+ super(view);
+ }
+ }
+
+ /*
+ * called after the media (featured image) for a post has been downloaded - locate the post
+ * and set its featured image url to the passed url
+ */
+ public void mediaUpdated(long mediaId, String mediaUrl) {
+ int position = mPosts.indexOfFeaturedMediaId(mediaId);
+ if (isValidPostPosition(position)) {
+ mPosts.get(position).setFeaturedImageUrl(mediaUrl);
+ notifyItemChanged(position);
+ }
+ }
+
+ private class LoadPostsTask extends AsyncTask<Void, Void, Boolean> {
+ private PostsListPostList tmpPosts;
+ private final ArrayList<Long> mediaIdsToUpdate = new ArrayList<>();
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsLoadingPosts = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsLoadingPosts = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... nada) {
+ tmpPosts = WordPress.wpDB.getPostsListPosts(mLocalTableBlogId, mIsPage);
+
+ // make sure we don't return any hidden posts
+ for (PostsListPost hiddenPost : mHiddenPosts) {
+ tmpPosts.remove(hiddenPost);
+ }
+
+ // go no further if existing post list is the same
+ if (mPosts.isSameList(tmpPosts)) {
+ return false;
+ }
+
+ // generate the featured image url for each post
+ String imageUrl;
+ for (PostsListPost post : tmpPosts) {
+ if (post.isLocalDraft()) {
+ imageUrl = null;
+ } else if (post.getFeaturedImageId() != 0) {
+ imageUrl = WordPress.wpDB.getMediaThumbnailUrl(mLocalTableBlogId, post.getFeaturedImageId());
+ // if the imageUrl isn't found it means the featured image info hasn't been added to
+ // the local media library yet, so add to the list of media IDs to request info for
+ if (TextUtils.isEmpty(imageUrl)) {
+ mediaIdsToUpdate.add(post.getFeaturedImageId());
+ }
+ } else if (post.hasDescription()) {
+ ReaderImageScanner scanner = new ReaderImageScanner(post.getDescription(), mIsPrivateBlog);
+ imageUrl = scanner.getLargestImage();
+ } else {
+ imageUrl = null;
+ }
+
+ if (!TextUtils.isEmpty(imageUrl)) {
+ post.setFeaturedImageUrl(
+ ReaderUtils.getResizedImageUrl(
+ imageUrl,
+ mPhotonWidth,
+ mPhotonHeight,
+ mIsPrivateBlog));
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ mPosts.clear();
+ mPosts.addAll(tmpPosts);
+ notifyDataSetChanged();
+
+ if (mediaIdsToUpdate.size() > 0) {
+ PostMediaService.startService(WordPress.getContext(), mLocalTableBlogId, mediaIdsToUpdate);
+ }
+ }
+
+ mIsLoadingPosts = false;
+
+ if (mOnPostsLoadedListener != null) {
+ mOnPostsLoadedListener.onPostsLoaded(mPosts.size());
+ }
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostEvents.java
new file mode 100644
index 000000000..2ac90686e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostEvents.java
@@ -0,0 +1,78 @@
+package org.wordpress.android.ui.posts.services;
+
+import org.wordpress.android.util.StringUtils;
+import org.xmlrpc.android.ApiHelper;
+
+public class PostEvents {
+
+ public static class PostUploadStarted {
+ public final int mLocalBlogId;
+
+ PostUploadStarted(int localBlogId) {
+ mLocalBlogId = localBlogId;
+ }
+ }
+
+ public static class PostUploadEnded {
+ public final int mLocalBlogId;
+ public final boolean mSucceeded;
+
+ PostUploadEnded(boolean succeeded, int localBlogId) {
+ mSucceeded = succeeded;
+ mLocalBlogId = localBlogId;
+ }
+ }
+
+ public static class PostMediaInfoUpdated {
+ private long mMediaId;
+ private String mMediaUrl;
+
+ PostMediaInfoUpdated(long mediaId, String mediaUrl) {
+ mMediaId = mediaId;
+ mMediaUrl = mediaUrl;
+ }
+ public long getMediaId() {
+ return mMediaId;
+ }
+ public String getMediaUrl() {
+ return StringUtils.notNullStr(mMediaUrl);
+ }
+ }
+
+ public static class RequestPosts {
+ private final int mBlogId;
+ private final boolean mIsPage;
+ private boolean mCanLoadMore;
+ private boolean mFailed;
+ private ApiHelper.ErrorType mErrorType = null;
+
+ RequestPosts(int blogId, boolean isPage) {
+ mBlogId = blogId;
+ mIsPage = isPage;
+ mFailed = false;
+ }
+ public int getBlogId() {
+ return mBlogId;
+ }
+ public boolean isPage() {
+ return mIsPage;
+ }
+ public boolean canLoadMore() {
+ return mCanLoadMore;
+ }
+ public void setCanLoadMore(boolean canLoadMore) {
+ mCanLoadMore = canLoadMore;
+ }
+ public boolean getFailed() {
+ return mFailed;
+ }
+ public ApiHelper.ErrorType getErrorType() {
+ return mErrorType;
+ }
+ public void setErrorType(ApiHelper.ErrorType errorType) {
+ mErrorType = errorType;
+ mFailed = true;
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostMediaService.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostMediaService.java
new file mode 100644
index 000000000..4af0a2125
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostMediaService.java
@@ -0,0 +1,127 @@
+package org.wordpress.android.ui.posts.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which retrieves media info for a list of media IDs in a specific blog - currently used
+ * only for featured images in the post list but could be used for any blog-specific media
+ */
+
+public class PostMediaService extends Service {
+
+ private static final String ARG_BLOG_ID = "blog_id";
+ private static final String ARG_MEDIA_IDS = "media_ids";
+
+ private final ConcurrentLinkedQueue<Long> mMediaIdQueue = new ConcurrentLinkedQueue<>();
+ private Blog mBlog;
+
+ public static void startService(Context context, int blogId, ArrayList<Long> mediaIds) {
+ if (context == null || mediaIds == null || mediaIds.size() == 0) {
+ return;
+ }
+
+ Intent intent = new Intent(context, PostMediaService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_MEDIA_IDS, mediaIds);
+ context.startService(intent);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.POSTS, "PostMediaService > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.POSTS, "PostMediaService > destroyed");
+ super.onDestroy();
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) return START_NOT_STICKY;
+
+ int blogId = intent.getIntExtra(ARG_BLOG_ID, 0);
+ mBlog = WordPress.getBlog(blogId);
+
+ Serializable serializable = intent.getSerializableExtra(ARG_MEDIA_IDS);
+ if (serializable != null && serializable instanceof List) {
+ List list = (List) serializable;
+ for (Object id : list) {
+ if (id instanceof Long) {
+ mMediaIdQueue.add((Long) id);
+ }
+ }
+ }
+
+ if (mMediaIdQueue.size() > 0 && mBlog != null) {
+ new Thread() {
+ @Override
+ public void run() {
+ while (!mMediaIdQueue.isEmpty()) {
+ long mediaId = mMediaIdQueue.poll();
+ downloadMediaItem(mediaId);
+ }
+ }
+ }.start();
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void downloadMediaItem(long mediaId) {
+ Object[] apiParams = {
+ mBlog.getRemoteBlogId(),
+ mBlog.getUsername(),
+ mBlog.getPassword(),
+ mediaId};
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(
+ mBlog.getUri(),
+ mBlog.getHttpuser(),
+ mBlog.getHttppassword());
+
+ try {
+ Map<?, ?> results = (Map<?, ?>) client.call(Method.GET_MEDIA_ITEM, apiParams);
+ if (results != null) {
+ String strBlogId = Integer.toString(mBlog.getLocalTableBlogId());
+ MediaFile mediaFile = new MediaFile(strBlogId, results, mBlog.isDotcomFlag());
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ AppLog.d(AppLog.T.POSTS, "PostMediaService > downloaded " + mediaFile.getFileURL());
+ EventBus.getDefault().post(new PostEvents.PostMediaInfoUpdated(mediaId, mediaFile.getFileURL()));
+ }
+ } catch (ClassCastException | XMLRPCException | XmlPullParserException | IOException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUpdateService.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUpdateService.java
new file mode 100644
index 000000000..ee6d35afe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUpdateService.java
@@ -0,0 +1,161 @@
+package org.wordpress.android.ui.posts.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.AppLog;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+import org.xmlrpc.android.XMLRPCFault;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which retrieves posts for the post list
+ */
+
+public class PostUpdateService extends Service {
+
+ private static final String ARG_BLOG_ID = "blog_id";
+ private static final String ARG_LOAD_MORE = "load_more";
+ private static final String ARG_IS_PAGE = "is_page";
+
+ private static final int NUM_POSTS_TO_REQUEST = 20;
+
+ /*
+ * fetch posts/pages in a specific blog
+ */
+ public static void startServiceForBlog(Context context, int blogId, boolean isPage, boolean loadMore) {
+ Intent intent = new Intent(context, PostUpdateService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_IS_PAGE, isPage);
+ intent.putExtra(ARG_LOAD_MORE, loadMore);
+ context.startService(intent);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.POSTS, "PostUpdateService > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.POSTS, "PostUpdateService > destroyed");
+ super.onDestroy();
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, int flags, int startId) {
+ if (intent == null) return START_NOT_STICKY;
+
+ new Thread() {
+ @Override
+ public void run() {
+ int blogId = intent.getIntExtra(ARG_BLOG_ID, 0);
+ boolean isPage = intent.getBooleanExtra(ARG_IS_PAGE, false);
+ boolean loadMore = intent.getBooleanExtra(ARG_LOAD_MORE, false);
+ fetchPostsInBlog(blogId, isPage, loadMore);
+ }
+ }.start();
+
+ return START_NOT_STICKY;
+ }
+
+ private void fetchPostsInBlog(int blogId, boolean isPage, boolean loadMore) {
+ Blog blog = WordPress.getBlog(blogId);
+ if (blog == null) {
+ return;
+ }
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(
+ blog.getUri(),
+ blog.getHttpuser(),
+ blog.getHttppassword());
+
+ int numPostsToRequest;
+ if (loadMore) {
+ int numExisting = WordPress.wpDB.getUploadedCountInBlog(blogId, isPage);
+ numPostsToRequest = numExisting + NUM_POSTS_TO_REQUEST;
+ } else {
+ numPostsToRequest = NUM_POSTS_TO_REQUEST;
+ }
+
+ Object[] result;
+ Object[] xmlrpcParams = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ numPostsToRequest};
+
+ PostEvents.RequestPosts event = new PostEvents.RequestPosts(blogId, isPage);
+ try {
+ boolean canLoadMore;
+
+ result = (Object[]) client.call(isPage ? Method.GET_PAGES : "metaWeblog.getRecentPosts", xmlrpcParams);
+ if (result != null && result.length > 0) {
+ canLoadMore = true;
+
+ // If we're loading more posts, only save the posts at the end of the array.
+ // NOTE: Switching to wp.getPosts wouldn't require janky solutions like this
+ // since it allows for an offset parameter.
+ int startPosition = 0;
+ if (loadMore && result.length > NUM_POSTS_TO_REQUEST) {
+ startPosition = result.length - NUM_POSTS_TO_REQUEST;
+ }
+
+ List<Map<?, ?>> postsList = new ArrayList<>();
+ for (int ctr = startPosition; ctr < result.length; ctr++) {
+ Map<?, ?> postMap = (Map<?, ?>) result[ctr];
+ postsList.add(postMap);
+ }
+
+ if (!loadMore) {
+ WordPress.wpDB.deleteUploadedPosts(blogId, isPage);
+ }
+ WordPress.wpDB.savePosts(postsList, blogId, isPage, false);
+ } else {
+ canLoadMore = false;
+ }
+
+ event.setCanLoadMore(canLoadMore);
+
+ } catch (XMLRPCException | IOException | XmlPullParserException e){
+ AppLog.e(AppLog.T.POSTS, e);
+ ApiHelper.ErrorType errorType;
+ if (e instanceof XMLRPCFault) {
+ if (((XMLRPCFault)(e)).getFaultCode() == 401) {
+ errorType = ApiHelper.ErrorType.UNAUTHORIZED;
+ } else {
+ errorType = ApiHelper.ErrorType.NETWORK_XMLRPC;
+ }
+ } else {
+ errorType = ApiHelper.ErrorType.INVALID_RESULT;
+ }
+
+ event.setErrorType(errorType);
+ }
+
+ EventBus.getDefault().post(event);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java
new file mode 100644
index 000000000..9b62c84b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java
@@ -0,0 +1,1026 @@
+package org.wordpress.android.ui.posts.services;
+
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ThumbnailUtils;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostLocation;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.ui.notifications.ShareAndDismissNotificationReceiver;
+import org.wordpress.android.ui.posts.PostsListActivity;
+import org.wordpress.android.ui.posts.services.PostEvents.PostUploadEnded;
+import org.wordpress.android.ui.posts.services.PostEvents.PostUploadStarted;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.CrashlyticsUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.SystemServiceFactory;
+import org.wordpress.android.util.WPMeShortlinks;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClient;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.greenrobot.event.EventBus;
+
+public class PostUploadService extends Service {
+ private static Context mContext;
+ private static final ArrayList<Post> mPostsList = new ArrayList<Post>();
+ private static Post mCurrentUploadingPost = null;
+ private static boolean mUseLegacyMode;
+ private UploadPostTask mCurrentTask = null;
+
+ public static void addPostToUpload(Post currentPost) {
+ synchronized (mPostsList) {
+ mPostsList.add(currentPost);
+ }
+ }
+
+ public static void setLegacyMode(boolean enabled) {
+ mUseLegacyMode = enabled;
+ }
+
+ /*
+ * returns true if the passed post is either uploading or waiting to be uploaded
+ */
+ public static boolean isPostUploading(long localPostId) {
+ // first check the currently uploading post
+ if (mCurrentUploadingPost != null && mCurrentUploadingPost.getLocalTablePostId() == localPostId) {
+ return true;
+ }
+ // then check the list of posts waiting to be uploaded
+ if (mPostsList.size() > 0) {
+ synchronized (mPostsList) {
+ for (Post post : mPostsList) {
+ if (post.getLocalTablePostId() == localPostId) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = this.getApplicationContext();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // Cancel current task, it will reset post from "uploading" to "local draft"
+ if (mCurrentTask != null) {
+ AppLog.d(T.POSTS, "cancelling current upload task");
+ mCurrentTask.cancel(true);
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ synchronized (mPostsList) {
+ if (mPostsList.size() == 0 || mContext == null) {
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ }
+
+ uploadNextPost();
+ // We want this service to continue running until it is explicitly stopped, so return sticky.
+ return START_STICKY;
+ }
+
+ private void uploadNextPost() {
+ synchronized (mPostsList) {
+ if (mCurrentTask == null) { //make sure nothing is running
+ mCurrentUploadingPost = null;
+ if (mPostsList.size() > 0) {
+ mCurrentUploadingPost = mPostsList.remove(0);
+ mCurrentTask = new UploadPostTask();
+ mCurrentTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mCurrentUploadingPost);
+ } else {
+ stopSelf();
+ }
+ }
+ }
+ }
+
+ private void postUploaded() {
+ synchronized (mPostsList) {
+ mCurrentTask = null;
+ mCurrentUploadingPost = null;
+ }
+ uploadNextPost();
+ }
+
+ private class UploadPostTask extends AsyncTask<Post, Boolean, Boolean> {
+ private Post mPost;
+ private Blog mBlog;
+ private PostUploadNotifier mPostUploadNotifier;
+
+ private String mErrorMessage = "";
+ private boolean mIsMediaError = false;
+ private int featuredImageID = -1;
+ private XMLRPCClientInterface mClient;
+
+ // True when the post goes from draft or local draft to published status
+ boolean mIsFirstPublishing = false;
+
+ // Used when the upload succeed
+ private Bitmap mLatestIcon;
+
+ // Used for analytics
+ private boolean mHasImage, mHasVideo, mHasCategory;
+
+ @Override
+ protected void onPostExecute(Boolean postUploadedSuccessfully) {
+ if (postUploadedSuccessfully) {
+ WordPress.wpDB.deleteMediaFilesForPost(mPost);
+ mPostUploadNotifier.cancelNotification();
+ mPostUploadNotifier.updateNotificationSuccess(mPost, mLatestIcon, mIsFirstPublishing);
+ } else {
+ mPostUploadNotifier.updateNotificationError(mErrorMessage, mIsMediaError, mPost.isPage());
+ }
+
+ postUploaded();
+ EventBus.getDefault().post(new PostUploadEnded(postUploadedSuccessfully, mPost.getLocalTableBlogId()));
+ }
+
+ @Override
+ protected void onCancelled(Boolean aBoolean) {
+ super.onCancelled(aBoolean);
+ // mPostUploadNotifier and mPost can be null if onCancelled is called before doInBackground
+ if (mPostUploadNotifier != null && mPost != null) {
+ mPostUploadNotifier.updateNotificationError(mErrorMessage, mIsMediaError, mPost.isPage());
+ }
+ }
+
+ @Override
+ protected Boolean doInBackground(Post... posts) {
+ mPost = posts[0];
+
+ String postTitle = TextUtils.isEmpty(mPost.getTitle()) ? getString(R.string.untitled) : mPost.getTitle();
+ String uploadingPostTitle = String.format(getString(R.string.posting_post), postTitle);
+ String uploadingPostMessage = String.format(
+ getString(R.string.sending_content),
+ mPost.isPage() ? getString(R.string.page).toLowerCase() : getString(R.string.post).toLowerCase()
+ );
+ mPostUploadNotifier = new PostUploadNotifier(mPost, uploadingPostTitle, uploadingPostMessage);
+
+ mBlog = WordPress.wpDB.instantiateBlogByLocalId(mPost.getLocalTableBlogId());
+ if (mBlog == null) {
+ mErrorMessage = mContext.getString(R.string.blog_not_found);
+ return false;
+ }
+
+ // Create the XML-RPC client
+ mClient = XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(),
+ mBlog.getHttppassword());
+
+ if (TextUtils.isEmpty(mPost.getPostStatus())) {
+ mPost.setPostStatus(PostStatus.toString(PostStatus.PUBLISHED));
+ }
+
+ String descriptionContent = processPostMedia(mPost.getDescription());
+
+ String moreContent = "";
+ if (!TextUtils.isEmpty(mPost.getMoreText())) {
+ moreContent = processPostMedia(mPost.getMoreText());
+ }
+
+ // If media file upload failed, let's stop here and prompt the user
+ if (mIsMediaError) {
+ return false;
+ }
+
+ JSONArray categoriesJsonArray = mPost.getJSONCategories();
+ String[] postCategories = null;
+ if (categoriesJsonArray != null) {
+ if (categoriesJsonArray.length() > 0) {
+ mHasCategory = true;
+ }
+
+ postCategories = new String[categoriesJsonArray.length()];
+ for (int i = 0; i < categoriesJsonArray.length(); i++) {
+ try {
+ postCategories[i] = TextUtils.htmlEncode(categoriesJsonArray.getString(i));
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ }
+
+ Map<String, Object> contentStruct = new HashMap<String, Object>();
+
+ // Post format
+ if (!mPost.isPage()) {
+ if (!TextUtils.isEmpty(mPost.getPostFormat())) {
+ contentStruct.put("wp_post_format", mPost.getPostFormat());
+ }
+ }
+
+ contentStruct.put("post_type", (mPost.isPage()) ? "page" : "post");
+ contentStruct.put("title", mPost.getTitle());
+ long pubDate = mPost.getDate_created_gmt();
+ if (pubDate != 0) {
+ Date date_created_gmt = new Date(pubDate);
+ contentStruct.put("date_created_gmt", date_created_gmt);
+ Date dateCreated = new Date(pubDate + (date_created_gmt.getTimezoneOffset() * 60000));
+ contentStruct.put("dateCreated", dateCreated);
+ }
+
+ if (!TextUtils.isEmpty(moreContent)) {
+ descriptionContent = descriptionContent.trim() + "<!--more-->" + moreContent;
+ mPost.setMoreText("");
+ }
+
+ // get rid of the p and br tags that the editor adds.
+ if (mPost.isLocalDraft()) {
+ descriptionContent = descriptionContent.replace("<p>", "").replace("</p>", "\n").replace("<br>", "");
+ }
+
+ // gets rid of the weird character android inserts after images
+ descriptionContent = descriptionContent.replaceAll("\uFFFC", "");
+
+ contentStruct.put("description", descriptionContent);
+ if (!mPost.isPage()) {
+ contentStruct.put("mt_keywords", mPost.getKeywords());
+
+ if (postCategories != null && postCategories.length > 0) {
+ contentStruct.put("categories", postCategories);
+ }
+ }
+
+ contentStruct.put("mt_excerpt", mPost.getPostExcerpt());
+ contentStruct.put((mPost.isPage()) ? "page_status" : "post_status", mPost.getPostStatus());
+
+ // Geolocation
+ if (mPost.supportsLocation()) {
+ JSONObject remoteGeoLatitude = mPost.getCustomField("geo_latitude");
+ JSONObject remoteGeoLongitude = mPost.getCustomField("geo_longitude");
+ JSONObject remoteGeoPublic = mPost.getCustomField("geo_public");
+
+ Map<Object, Object> hLatitude = new HashMap<Object, Object>();
+ Map<Object, Object> hLongitude = new HashMap<Object, Object>();
+ Map<Object, Object> hPublic = new HashMap<Object, Object>();
+
+ try {
+ if (remoteGeoLatitude != null) {
+ hLatitude.put("id", remoteGeoLatitude.getInt("id"));
+ }
+
+ if (remoteGeoLongitude != null) {
+ hLongitude.put("id", remoteGeoLongitude.getInt("id"));
+ }
+
+ if (remoteGeoPublic != null) {
+ hPublic.put("id", remoteGeoPublic.getInt("id"));
+ }
+
+ if (mPost.hasLocation()) {
+ PostLocation location = mPost.getLocation();
+ hLatitude.put("key", "geo_latitude");
+ hLongitude.put("key", "geo_longitude");
+ hPublic.put("key", "geo_public");
+ hLatitude.put("value", location.getLatitude());
+ hLongitude.put("value", location.getLongitude());
+ hPublic.put("value", 1);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.EDITOR, e);
+ }
+
+ if (!hLatitude.isEmpty() && !hLongitude.isEmpty() && !hPublic.isEmpty()) {
+ Object[] geo = {hLatitude, hLongitude, hPublic};
+ contentStruct.put("custom_fields", geo);
+ }
+ }
+
+ // Featured images
+ if (mUseLegacyMode) {
+ // Support for legacy editor - images are identified as featured as they're being uploaded with the post
+ if (featuredImageID != -1) {
+ contentStruct.put("wp_post_thumbnail", featuredImageID);
+ }
+ } else if (mPost.featuredImageHasChanged()) {
+ if (mPost.getFeaturedImageId() < 1 && !mPost.isLocalDraft()) {
+ // The featured image was removed from a live post
+ contentStruct.put("wp_post_thumbnail", "");
+ } else {
+ contentStruct.put("wp_post_thumbnail", mPost.getFeaturedImageId());
+ }
+ }
+
+ if (!TextUtils.isEmpty(mPost.getQuickPostType())) {
+ mClient.addQuickPostHeader(mPost.getQuickPostType());
+ }
+
+ contentStruct.put("wp_password", mPost.getPassword());
+
+ Object[] params;
+ if (mPost.isLocalDraft())
+ params = new Object[]{mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword(),
+ contentStruct, false};
+ else
+ params = new Object[]{mPost.getRemotePostId(), mBlog.getUsername(), mBlog.getPassword(), contentStruct,
+ false};
+
+ try {
+ EventBus.getDefault().post(new PostUploadStarted(mPost.getLocalTableBlogId()));
+
+ if (mPost.isLocalDraft()) {
+ Object object = mClient.call("metaWeblog.newPost", params);
+ if (object instanceof String) {
+ mPost.setRemotePostId((String) object);
+ }
+ } else {
+ mClient.call("metaWeblog.editPost", params);
+ }
+
+ // Check if it's the first publishing before changing post status.
+ mIsFirstPublishing = mPost.hasChangedFromDraftToPublished()
+ || (mPost.isLocalDraft() && mPost.getStatusEnum() == PostStatus.PUBLISHED);
+
+ mPost.setLocalDraft(false);
+ mPost.setLocalChange(false);
+ WordPress.wpDB.updatePost(mPost);
+
+ // Track analytics only if the post is newly published
+ if (mIsFirstPublishing) {
+ trackUploadAnalytics();
+ }
+
+ // request the new/updated post from the server to ensure local copy matches server
+ ApiHelper.updateSinglePost(mBlog.getLocalTableBlogId(), mPost.getRemotePostId(), mPost.isPage());
+
+ return true;
+ } catch (final XMLRPCException e) {
+ setUploadPostErrorMessage(e);
+ } catch (IOException e) {
+ setUploadPostErrorMessage(e);
+ } catch (XmlPullParserException e) {
+ setUploadPostErrorMessage(e);
+ }
+
+ return false;
+ }
+
+ private boolean hasGallery() {
+ Pattern galleryTester = Pattern.compile("\\[.*?gallery.*?\\]");
+ Matcher matcher = galleryTester.matcher(mPost.getContent());
+ return matcher.find();
+ }
+
+ private void trackUploadAnalytics() {
+ // Calculate the words count
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("word_count", AnalyticsUtils.getWordCount(mPost.getContent()));
+
+ if (hasGallery()) {
+ properties.put("with_galleries", true);
+ }
+ if (mHasImage) {
+ properties.put("with_photos", true);
+ }
+ if (mHasVideo) {
+ properties.put("with_videos", true);
+ }
+ if (mHasCategory) {
+ properties.put("with_categories", true);
+ }
+ if (!TextUtils.isEmpty(mPost.getKeywords())) {
+ properties.put("with_tags", true);
+ }
+ properties.put("via_new_editor", AppPrefs.isVisualEditorEnabled());
+ AnalyticsUtils.trackWithBlogDetails(Stat.EDITOR_PUBLISHED_POST, mBlog, properties);
+ }
+
+ /**
+ * Finds media in post content, uploads them, and returns the HTML to insert in the post
+ */
+ private String processPostMedia(String postContent) {
+ String imageTagsPattern = "<img[^>]+android-uri\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>";
+ Pattern pattern = Pattern.compile(imageTagsPattern);
+ Matcher matcher = pattern.matcher(postContent);
+
+ int totalMediaItems = 0;
+ List<String> imageTags = new ArrayList<String>();
+ while (matcher.find()) {
+ imageTags.add(matcher.group());
+ totalMediaItems++;
+ }
+
+ mPostUploadNotifier.setTotalMediaItems(totalMediaItems);
+
+ int mediaItemCount = 0;
+ for (String tag : imageTags) {
+ Pattern p = Pattern.compile("android-uri=\"([^\"]+)\"");
+ Matcher m = p.matcher(tag);
+ if (m.find()) {
+ String imageUri = m.group(1);
+ if (!imageUri.equals("")) {
+ MediaFile mediaFile = WordPress.wpDB.getMediaFile(imageUri, mPost);
+ if (mediaFile != null) {
+ // Get image thumbnail for notification icon
+ Bitmap imageIcon = ImageUtils.getWPImageSpanThumbnailFromFilePath(
+ mContext,
+ imageUri,
+ DisplayUtils.dpToPx(mContext, 128)
+ );
+
+ // Crop the thumbnail to be squared in the center
+ if (imageIcon != null) {
+ int squaredSize = DisplayUtils.dpToPx(mContext, 64);
+ imageIcon = ThumbnailUtils.extractThumbnail(imageIcon, squaredSize, squaredSize);
+ mLatestIcon = imageIcon;
+ }
+
+ mediaItemCount++;
+ mPostUploadNotifier.setCurrentMediaItem(mediaItemCount);
+ mPostUploadNotifier.updateNotificationIcon(imageIcon);
+
+ String mediaUploadOutput;
+ if (mediaFile.isVideo()) {
+ mHasVideo = true;
+ mediaUploadOutput = uploadVideo(mediaFile);
+ } else {
+ mHasImage = true;
+ mediaUploadOutput = uploadImage(mediaFile);
+ }
+
+ if (mediaUploadOutput != null) {
+ postContent = postContent.replace(tag, mediaUploadOutput);
+ } else {
+ postContent = postContent.replace(tag, "");
+ mIsMediaError = true;
+ }
+ }
+ }
+ }
+ }
+
+ return postContent;
+ }
+
+ private String uploadImage(MediaFile mediaFile) {
+ AppLog.d(T.POSTS, "uploadImage: " + mediaFile.getFilePath());
+
+ if (mediaFile.getFilePath() == null) {
+ return null;
+ }
+
+ Uri imageUri = Uri.parse(mediaFile.getFilePath());
+ File imageFile = null;
+ String mimeType = "", path = "";
+
+ if (imageUri.toString().contains("content:")) {
+ String[] projection = new String[]{Images.Media._ID, Images.Media.DATA, Images.Media.MIME_TYPE};
+
+ Cursor cur = mContext.getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(Images.Media.DATA);
+ int mimeTypeColumn = cur.getColumnIndex(Images.Media.MIME_TYPE);
+
+ String thumbData = cur.getString(dataColumn);
+ mimeType = cur.getString(mimeTypeColumn);
+ imageFile = new File(thumbData);
+ path = thumbData;
+ mediaFile.setFilePath(imageFile.getPath());
+ }
+ } else { // file is not in media library
+ path = imageUri.toString().replace("file://", "");
+ imageFile = new File(path);
+ mediaFile.setFilePath(path);
+ }
+
+ // check if the file exists
+ if (imageFile == null) {
+ mErrorMessage = mContext.getString(R.string.file_not_found);
+ return null;
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = MediaUtils.getMediaFileMimeType(imageFile);
+ }
+ String fileName = MediaUtils.getMediaFileName(imageFile, mimeType);
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileName).toLowerCase();
+
+ int orientation = ImageUtils.getImageOrientation(mContext, path);
+
+ String resizedPictureURL = null;
+
+ // We need to upload a resized version of the picture when the blog settings != original size, or when
+ // the user has selected a smaller size for the current picture in the picture settings screen
+ // We won't resize gif images to keep them awesome.
+ boolean shouldUploadResizedVersion = false;
+ // If it's not a gif and blog don't keep original size, there is a chance we need to resize
+ if (!mimeType.equals("image/gif") && MediaUtils.getImageWidthSettingFromString(mBlog.getMaxImageWidth())
+ != Integer.MAX_VALUE) {
+ // check the picture settings
+ int pictureSettingWidth = mediaFile.getWidth();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ int[] dimensions = {imageWidth, imageHeight};
+ if (dimensions[0] != 0 && dimensions[0] != pictureSettingWidth) {
+ shouldUploadResizedVersion = true;
+ }
+ }
+
+ boolean shouldAddImageWidthCSS = false;
+
+ if (shouldUploadResizedVersion) {
+ MediaFile resizedMediaFile = new MediaFile(mediaFile);
+ // Create resized image
+ byte[] bytes = ImageUtils.createThumbnailFromUri(mContext, imageUri, resizedMediaFile.getWidth(),
+ fileExtension, orientation);
+
+ if (bytes == null) {
+ // We weren't able to resize the image, so we will upload the full size image with css to resize it
+ shouldUploadResizedVersion = false;
+ shouldAddImageWidthCSS = true;
+ } else {
+ // Save temp image
+ String tempFilePath;
+ File resizedImageFile;
+ try {
+ resizedImageFile = File.createTempFile("wp-image-", fileExtension);
+ FileOutputStream out = new FileOutputStream(resizedImageFile);
+ out.write(bytes);
+ out.close();
+ tempFilePath = resizedImageFile.getPath();
+ } catch (IOException e) {
+ AppLog.w(T.POSTS, "failed to create image temp file");
+ mErrorMessage = mContext.getString(R.string.error_media_upload);
+ return null;
+ }
+
+ // upload resized picture
+ if (!TextUtils.isEmpty(tempFilePath)) {
+ resizedMediaFile.setFilePath(tempFilePath);
+ Map<String, Object> parameters = new HashMap<String, Object>();
+
+ parameters.put("name", fileName);
+ parameters.put("type", mimeType);
+ parameters.put("bits", resizedMediaFile);
+ parameters.put("overwrite", true);
+ resizedPictureURL = uploadImageFile(parameters, resizedMediaFile, mBlog);
+ if (resizedPictureURL == null) {
+ AppLog.w(T.POSTS, "failed to upload resized picture");
+ return null;
+ } else if (resizedImageFile.exists()) {
+ resizedImageFile.delete();
+ }
+ } else {
+ AppLog.w(T.POSTS, "failed to create resized picture");
+ mErrorMessage = mContext.getString(R.string.out_of_memory);
+ return null;
+ }
+ }
+ }
+
+ String fullSizeUrl = null;
+ // Upload the full size picture if "Original Size" is selected in settings,
+ // or if 'link to full size' is checked.
+ if (!shouldUploadResizedVersion || mBlog.isFullSizeImage()) {
+ Map<String, Object> parameters = new HashMap<String, Object>();
+ parameters.put("name", fileName);
+ parameters.put("type", mimeType);
+ parameters.put("bits", mediaFile);
+ parameters.put("overwrite", true);
+
+ fullSizeUrl = uploadImageFile(parameters, mediaFile, mBlog);
+ if (fullSizeUrl == null) {
+ mErrorMessage = mContext.getString(R.string.error_media_upload);
+ return null;
+ }
+ }
+
+ return mediaFile.getImageHtmlForUrls(fullSizeUrl, resizedPictureURL, shouldAddImageWidthCSS);
+ }
+
+ private String uploadVideo(MediaFile mediaFile) {
+ // create temp file for media upload
+ String tempFileName = "wp-" + System.currentTimeMillis();
+ try {
+ mContext.openFileOutput(tempFileName, Context.MODE_PRIVATE);
+ } catch (FileNotFoundException e) {
+ mErrorMessage = getResources().getString(R.string.file_error_create);
+ return null;
+ }
+
+ if (mediaFile.getFilePath() == null) {
+ mErrorMessage = mContext.getString(R.string.error_media_upload);
+ return null;
+ }
+
+ Uri videoUri = Uri.parse(mediaFile.getFilePath());
+ File videoFile = null;
+ String mimeType = "", xRes = "", yRes = "";
+
+ if (videoUri.toString().contains("content:")) {
+ String[] projection = new String[]{Video.Media._ID, Video.Media.DATA, Video.Media.MIME_TYPE,
+ Video.Media.RESOLUTION};
+ Cursor cur = mContext.getContentResolver().query(videoUri, projection, null, null, null);
+
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(Video.Media.DATA);
+ int mimeTypeColumn = cur.getColumnIndex(Video.Media.MIME_TYPE);
+ int resolutionColumn = cur.getColumnIndex(Video.Media.RESOLUTION);
+
+ mediaFile = new MediaFile();
+
+ String thumbData = cur.getString(dataColumn);
+ mimeType = cur.getString(mimeTypeColumn);
+
+ videoFile = new File(thumbData);
+ mediaFile.setFilePath(videoFile.getPath());
+ String resolution = cur.getString(resolutionColumn);
+ if (resolution != null) {
+ String[] resolutions = resolution.split("x");
+ if (resolutions.length >= 2) {
+ xRes = resolutions[0];
+ yRes = resolutions[1];
+ }
+ } else {
+ // set the width of the video to the thumbnail width, else 640x480
+ if (MediaUtils.getImageWidthSettingFromString(mBlog.getMaxImageWidth()) != Integer.MAX_VALUE) {
+ xRes = mBlog.getMaxImageWidth();
+ yRes = String.valueOf(Math.round(Integer.valueOf(mBlog.getMaxImageWidth()) * 0.75));
+ } else {
+ xRes = "640";
+ yRes = "480";
+ }
+ }
+ }
+ } else { // file is not in media library
+ String filePath = videoUri.toString().replace("file://", "");
+ mediaFile.setFilePath(filePath);
+ videoFile = new File(filePath);
+ }
+
+ if (videoFile == null) {
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload);
+ return null;
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = MediaUtils.getMediaFileMimeType(videoFile);
+ }
+ String videoName = MediaUtils.getMediaFileName(videoFile, mimeType);
+
+ // try to upload the video
+ Map<String, Object> m = new HashMap<String, Object>();
+ m.put("name", videoName);
+ m.put("type", mimeType);
+ m.put("bits", mediaFile);
+ m.put("overwrite", true);
+
+ Object[] params = {1, mBlog.getUsername(), mBlog.getPassword(), m};
+
+ File tempFile;
+ try {
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(videoName);
+ tempFile = createTempUploadFile(fileExtension);
+ } catch (IOException e) {
+ mErrorMessage = getResources().getString(R.string.file_error_create);
+ return null;
+ }
+
+ Object result = uploadFileHelper(params, tempFile);
+ Map<?, ?> resultMap = (HashMap<?, ?>) result;
+ if (resultMap != null && resultMap.containsKey("url")) {
+ String resultURL = resultMap.get("url").toString();
+ if (resultMap.containsKey(MediaFile.VIDEOPRESS_SHORTCODE_ID)) {
+ resultURL = resultMap.get(MediaFile.VIDEOPRESS_SHORTCODE_ID).toString() + "\n";
+ } else {
+ resultURL = String.format(
+ "<video width=\"%s\" height=\"%s\" controls=\"controls\"><source src=\"%s\" type=\"%s\" /><a href=\"%s\">Click to view video</a>.</video>",
+ xRes, yRes, resultURL, mimeType, resultURL);
+ }
+
+ return resultURL;
+ } else {
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload);
+ return null;
+ }
+ }
+
+
+ private void setUploadPostErrorMessage(Exception e) {
+ mErrorMessage = String.format(mContext.getResources().getText(R.string.error_upload).toString(),
+ mPost.isPage() ? mContext.getResources().getText(R.string.page).toString() :
+ mContext.getResources().getText(R.string.post).toString()) + " " + e.getMessage();
+ mIsMediaError = false;
+ AppLog.e(T.EDITOR, mErrorMessage, e);
+ }
+
+ private String uploadImageFile(Map<String, Object> pictureParams, MediaFile mf, Blog blog) {
+ // create temporary upload file
+ File tempFile;
+ try {
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(mf.getFileName());
+ tempFile = createTempUploadFile(fileExtension);
+ } catch (IOException e) {
+ mIsMediaError = true;
+ mErrorMessage = mContext.getString(R.string.file_not_found);
+ return null;
+ }
+
+ Object[] params = {1, blog.getUsername(), blog.getPassword(), pictureParams};
+ Object result = uploadFileHelper(params, tempFile);
+ if (result == null) {
+ mIsMediaError = true;
+ return null;
+ }
+
+ Map<?, ?> contentHash = (HashMap<?, ?>) result;
+ String pictureURL = contentHash.get("url").toString();
+
+ if (mf.isFeatured()) {
+ try {
+ if (contentHash.get("id") != null) {
+ featuredImageID = Integer.parseInt(contentHash.get("id").toString());
+ if (!mf.isFeaturedInPost())
+ return "";
+ }
+ } catch (NumberFormatException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ return pictureURL;
+ }
+
+ private Object uploadFileHelper(Object[] params, final File tempFile) {
+ // Create listener for tracking upload progress in the notification
+ if (mClient instanceof XMLRPCClient) {
+ XMLRPCClient xmlrpcClient = (XMLRPCClient) mClient;
+ xmlrpcClient.setOnBytesUploadedListener(new XMLRPCClient.OnBytesUploadedListener() {
+ @Override
+ public void onBytesUploaded(long uploadedBytes) {
+ if (tempFile.length() == 0) {
+ return;
+ }
+ float percentage = (uploadedBytes * 100) / tempFile.length();
+ mPostUploadNotifier.updateNotificationProgress(percentage);
+ }
+ });
+ }
+
+ try {
+ return mClient.call(Method.UPLOAD_FILE, params, tempFile);
+ } catch (XMLRPCException e) {
+ // well formed XML-RPC response from the server, but it's an error. Ok to print the error message
+ AppLog.e(T.API, e);
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload) + ": " + e.getMessage();
+ return null;
+ } catch (IOException e) {
+ // I/O-related error. Show a generic connection error message
+ AppLog.e(T.API, e);
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload_connection);
+ return null;
+ } catch (XmlPullParserException e) {
+ // XML-RPC response isn't well formed or valid. DO NOT print the real error message
+ AppLog.e(T.API, e);
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload);
+ return null;
+ } finally {
+ // remove the temporary upload file now that we're done with it
+ if (tempFile != null && tempFile.exists()) {
+ tempFile.delete();
+ }
+ }
+ }
+ }
+
+ private File createTempUploadFile(String fileExtension) throws IOException {
+ return File.createTempFile("wp-", fileExtension, mContext.getCacheDir());
+ }
+
+ private class PostUploadNotifier {
+ private final NotificationManager mNotificationManager;
+ private final Builder mNotificationBuilder;
+ private final int mNotificationId;
+ private int mNotificationErrorId = 0;
+ private int mTotalMediaItems;
+ private int mCurrentMediaItem;
+ private float mItemProgressSize;
+
+ public PostUploadNotifier(Post post, String title, String message) {
+ // add the uploader to the notification bar
+ mNotificationManager = (NotificationManager) SystemServiceFactory.get(mContext,
+ Context.NOTIFICATION_SERVICE);
+ mNotificationBuilder = new Notification.Builder(getApplicationContext());
+ mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_upload);
+ if (title != null) {
+ mNotificationBuilder.setContentTitle(title);
+ }
+ if (message != null) {
+ mNotificationBuilder.setContentText(message);
+ }
+ mNotificationId = (new Random()).nextInt() + post.getLocalTableBlogId();
+ startForeground(mNotificationId, mNotificationBuilder.build());
+ }
+
+ public void updateNotificationIcon(Bitmap icon) {
+ if (icon != null) {
+ mNotificationBuilder.setLargeIcon(icon);
+ }
+ doNotify(mNotificationId, mNotificationBuilder.build());
+ }
+
+ public void cancelNotification() {
+ mNotificationManager.cancel(mNotificationId);
+ }
+
+ public void updateNotificationSuccess(Post post, Bitmap largeIcon, boolean isFirstPublishing) {
+ AppLog.d(T.POSTS, "updateNotificationSuccess");
+
+ // Get the sharableUrl
+ String sharableUrl = WPMeShortlinks.getPostShortlink(post);
+ if (sharableUrl == null && !TextUtils.isEmpty(post.getPermaLink())) {
+ sharableUrl = post.getPermaLink();
+ }
+
+ // Notification builder
+ Builder notificationBuilder = new Notification.Builder(getApplicationContext());
+ String notificationTitle = (String) (post.isPage() ? mContext.getResources().getText(R.string
+ .page_published) : mContext.getResources().getText(R.string.post_published));
+ if (!isFirstPublishing) {
+ notificationTitle = (String) (post.isPage() ? mContext.getResources().getText(R.string
+ .page_updated) : mContext.getResources().getText(R.string.post_updated));
+ }
+ notificationBuilder.setSmallIcon(android.R.drawable.stat_sys_upload_done);
+ if (largeIcon == null) {
+ notificationBuilder.setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(),
+ R.mipmap.app_icon));
+ } else {
+ notificationBuilder.setLargeIcon(largeIcon);
+ }
+ notificationBuilder.setContentTitle(notificationTitle);
+ notificationBuilder.setContentText(post.getTitle());
+ notificationBuilder.setAutoCancel(true);
+
+ // Tap notification intent (open the post list)
+ Intent notificationIntent = new Intent(mContext, PostsListActivity.class);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ notificationIntent.putExtra(PostsListActivity.EXTRA_BLOG_LOCAL_ID, post.getLocalTableBlogId());
+ notificationIntent.putExtra(PostsListActivity.EXTRA_VIEW_PAGES, post.isPage());
+ PendingIntent pendingIntentPost = PendingIntent.getActivity(mContext, 0,
+ notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ notificationBuilder.setContentIntent(pendingIntentPost);
+
+ // Share intent - started if the user tap the share link button - only if the link exist
+ int notificationId = getNotificationIdForPost(post);
+ if (sharableUrl != null && post.getStatusEnum() == PostStatus.PUBLISHED) {
+ Intent shareIntent = new Intent(mContext, ShareAndDismissNotificationReceiver.class);
+ shareIntent.putExtra(ShareAndDismissNotificationReceiver.NOTIFICATION_ID_KEY, notificationId);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, sharableUrl);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, post.getTitle());
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, shareIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ notificationBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_action),
+ pendingIntent);
+ }
+ doNotify(notificationId, notificationBuilder.build());
+ }
+
+ private int getNotificationIdForPost(Post post) {
+ int remotePostId = StringUtils.stringToInt(post.getRemotePostId());
+ // We can't use the local table post id here because it can change between first post (local draft) to
+ // first edit (post pulled from the server)
+ return post.getLocalTableBlogId() + remotePostId;
+ }
+
+ public void updateNotificationError(String mErrorMessage, boolean isMediaError, boolean isPage) {
+ AppLog.d(T.POSTS, "updateNotificationError: " + mErrorMessage);
+
+ Builder notificationBuilder = new Notification.Builder(getApplicationContext());
+ String postOrPage = (String) (isPage ? mContext.getResources().getText(R.string.page_id)
+ : mContext.getResources().getText(R.string.post_id));
+ Intent notificationIntent = new Intent(mContext, PostsListActivity.class);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ notificationIntent.putExtra(PostsListActivity.EXTRA_VIEW_PAGES, isPage);
+ notificationIntent.putExtra(PostsListActivity.EXTRA_ERROR_MSG, mErrorMessage);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0,
+ notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ String errorText = mContext.getResources().getText(R.string.upload_failed).toString();
+ if (isMediaError) {
+ errorText = mContext.getResources().getText(R.string.media) + " "
+ + mContext.getResources().getText(R.string.error);
+ }
+
+ notificationBuilder.setSmallIcon(android.R.drawable.stat_notify_error);
+ notificationBuilder.setContentTitle((isMediaError) ? errorText :
+ mContext.getResources().getText(R.string.upload_failed));
+ notificationBuilder.setContentText((isMediaError) ? mErrorMessage : postOrPage + " " + errorText
+ + ": " + mErrorMessage);
+ notificationBuilder.setContentIntent(pendingIntent);
+ notificationBuilder.setAutoCancel(true);
+ if (mNotificationErrorId == 0) {
+ mNotificationErrorId = mNotificationId + (new Random()).nextInt();
+ }
+ doNotify(mNotificationErrorId, notificationBuilder.build());
+ }
+
+ public void updateNotificationProgress(float progress) {
+ if (mTotalMediaItems == 0) {
+ return;
+ }
+
+ // Simple way to show progress of entire post upload
+ // Would be better if we could get total bytes for all media items.
+ double currentChunkProgress = (mItemProgressSize * progress) / 100;
+
+ if (mCurrentMediaItem > 1) {
+ currentChunkProgress += mItemProgressSize * (mCurrentMediaItem - 1);
+ }
+
+ mNotificationBuilder.setProgress(100, (int)Math.ceil(currentChunkProgress), false);
+ doNotify(mNotificationId, mNotificationBuilder.build());
+ }
+
+ private synchronized void doNotify(int id, Notification notification) {
+ try {
+ mNotificationManager.notify(id, notification);
+ } catch (RuntimeException runtimeException) {
+ CrashlyticsUtils.logException(runtimeException, CrashlyticsUtils.ExceptionType.SPECIFIC,
+ AppLog.T.UTILS, "See issue #2858 / #3966");
+ AppLog.d(T.POSTS, "See issue #2858 / #3966; notify failed with:" + runtimeException);
+ }
+ }
+
+ public void setTotalMediaItems(int totalMediaItems) {
+ if (totalMediaItems <= 0) {
+ totalMediaItems = 1;
+ }
+
+ mTotalMediaItems = totalMediaItems;
+ mItemProgressSize = 100.0f / mTotalMediaItems;
+ }
+
+ public void setCurrentMediaItem(int currentItem) {
+ mCurrentMediaItem = currentItem;
+
+ mNotificationBuilder.setContentText(String.format(getString(R.string.uploading_total), mCurrentMediaItem,
+ mTotalMediaItems));
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java
new file mode 100644
index 000000000..d463fa574
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.widgets.WPTextView;
+import org.wordpress.passcodelock.AppLockManager;
+
+import java.util.Calendar;
+
+public class AboutActivity extends AppCompatActivity implements OnClickListener {
+ private static final String URL_TOS = "http://en.wordpress.com/tos";
+ private static final String URL_AUTOMATTIC = "http://automattic.com";
+ private static final String URL_PRIVACY_POLICY = "/privacy";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.about_activity);
+
+ WPTextView version = (WPTextView) findViewById(R.id.about_version);
+ version.setText(getString(R.string.version) + " " + WordPress.versionName);
+
+ WPTextView tos = (WPTextView) findViewById(R.id.about_tos);
+ tos.setOnClickListener(this);
+
+ WPTextView pp = (WPTextView) findViewById(R.id.about_privacy);
+ pp.setOnClickListener(this);
+
+ WPTextView publisher = (WPTextView) findViewById(R.id.about_publisher);
+ publisher.setText(getString(R.string.publisher) + " " + getString(R.string.automattic_inc));
+
+ WPTextView copyright = (WPTextView) findViewById(R.id.about_copyright);
+ copyright.setText("©" + Calendar.getInstance().get(Calendar.YEAR) + " " + getString(R.string.automattic_inc));
+
+ WPTextView about = (WPTextView) findViewById(R.id.about_url);
+ about.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ Uri uri;
+ int id = v.getId();
+ if (id == R.id.about_url) {
+ uri = Uri.parse(URL_AUTOMATTIC);
+ } else if (id == R.id.about_tos) {
+ uri = Uri.parse(URL_TOS);
+ } else if (id == R.id.about_privacy) {
+ uri = Uri.parse(URL_AUTOMATTIC + URL_PRIVACY_POLICY);
+ } else {
+ return;
+ }
+ AppLockManager.getInstance().setExtendedTimeout();
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java
new file mode 100644
index 000000000..444e52da7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import org.wordpress.android.ui.ActivityLauncher;
+
+public class AccountSettingsActivity extends AppCompatActivity {
+ private static final String KEY_ACCOUNT_SETTINGS_FRAGMENT = "account-settings-fragment";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ AccountSettingsFragment accountSettingsFragment = (AccountSettingsFragment) fragmentManager.findFragmentByTag(KEY_ACCOUNT_SETTINGS_FRAGMENT);
+ if (accountSettingsFragment == null) {
+ accountSettingsFragment = new AccountSettingsFragment();
+
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, accountSettingsFragment, KEY_ACCOUNT_SETTINGS_FRAGMENT)
+ .commit();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java
new file mode 100644
index 000000000..0cc8d0122
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java
@@ -0,0 +1,278 @@
+package org.wordpress.android.ui.prefs;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.text.InputType;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.AccountModel;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+@SuppressWarnings("deprecation")
+public class AccountSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {
+ private Preference mUsernamePreference;
+ private EditTextPreferenceWithValidation mEmailPreference;
+ private DetailListPreference mPrimarySitePreference;
+ private EditTextPreferenceWithValidation mWebAddressPreference;
+ private Snackbar mEmailSnackbar;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setRetainInstance(true);
+ addPreferencesFromResource(R.xml.account_settings);
+
+ mUsernamePreference = findPreference(getString(R.string.pref_key_username));
+ mEmailPreference = (EditTextPreferenceWithValidation) findPreference(getString(R.string.pref_key_email));
+ mPrimarySitePreference = (DetailListPreference) findPreference(getString(R.string.pref_key_primary_site));
+ mWebAddressPreference = (EditTextPreferenceWithValidation) findPreference(getString(R.string.pref_key_web_address));
+
+ mEmailPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ mEmailPreference.setValidationType(EditTextPreferenceWithValidation.ValidationType.EMAIL);
+ mWebAddressPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
+ mWebAddressPreference.setValidationType(EditTextPreferenceWithValidation.ValidationType.URL);
+ mWebAddressPreference.setDialogMessage(R.string.web_address_dialog_hint);
+
+ mEmailPreference.setOnPreferenceChangeListener(this);
+ mPrimarySitePreference.setOnPreferenceChangeListener(this);
+ mWebAddressPreference.setOnPreferenceChangeListener(this);
+
+ // load site list asynchronously
+ new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View coordinatorView = inflater.inflate(R.layout.preference_coordinator, container, false);
+ CoordinatorLayout coordinator = (CoordinatorLayout) coordinatorView.findViewById(R.id.coordinator);
+ View preferenceView = super.onCreateView(inflater, coordinator, savedInstanceState);
+ coordinator.addView(preferenceView);
+ return coordinatorView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ refreshAccountDetails();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ AccountHelper.getDefaultAccount().fetchAccountSettings();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue == null) return false;
+
+ if (preference == mEmailPreference) {
+ updateEmail(newValue.toString());
+ showPendingEmailChangeSnackbar(newValue.toString());
+ mEmailPreference.setEnabled(false);
+ return false;
+ } else if (preference == mPrimarySitePreference) {
+ changePrimaryBlogPreference(newValue.toString());
+ updatePrimaryBlog(newValue.toString());
+ return false;
+ } else if (preference == mWebAddressPreference) {
+ mWebAddressPreference.setSummary(newValue.toString());
+ updateWebAddress(newValue.toString());
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void refreshAccountDetails() {
+ Account account = AccountHelper.getDefaultAccount();
+ mUsernamePreference.setSummary(account.getUserName());
+ mEmailPreference.setSummary(account.getEmail());
+ mWebAddressPreference.setSummary(account.getWebAddress());
+
+ String blogId = String.valueOf(account.getPrimaryBlogId());
+ changePrimaryBlogPreference(blogId);
+
+ checkIfEmailChangeIsPending();
+ }
+
+ private void checkIfEmailChangeIsPending() {
+ final Account account = AccountHelper.getDefaultAccount();
+ if (account.getPendingEmailChange()) {
+ showPendingEmailChangeSnackbar(account.getNewEmail());
+ } else if (mEmailSnackbar != null && mEmailSnackbar.isShown()){
+ mEmailSnackbar.dismiss();
+ }
+ mEmailPreference.setEnabled(!account.getPendingEmailChange());
+ }
+
+ private void showPendingEmailChangeSnackbar(String newEmail) {
+ if (getView() != null) {
+ if (mEmailSnackbar == null || !mEmailSnackbar.isShown()) {
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancelPendingEmailChange();
+ }
+ };
+
+ mEmailSnackbar = Snackbar
+ .make(getView(), "", Snackbar.LENGTH_INDEFINITE).setAction(getString(R.string.button_revert), clickListener);
+ mEmailSnackbar.getView().setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.grey_dark));
+ mEmailSnackbar.setActionTextColor(ContextCompat.getColor(getActivity(), R.color.blue_medium));
+ TextView textView = (TextView) mEmailSnackbar.getView().findViewById(android.support.design.R.id.snackbar_text);
+ textView.setMaxLines(4);
+ }
+ // instead of creating a new snackbar, update the current one to avoid the jumping animation
+ mEmailSnackbar.setText(getString(R.string.pending_email_change_snackbar, newEmail));
+ if (!mEmailSnackbar.isShown()) {
+ mEmailSnackbar.show();
+ }
+ }
+ }
+
+ private void cancelPendingEmailChange() {
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.EMAIL_CHANGE_PENDING.getDescription(), "false");
+ AccountHelper.getDefaultAccount().postAccountSettings(params);
+ if (mEmailSnackbar != null && mEmailSnackbar.isShown()) {
+ mEmailSnackbar.dismiss();
+ }
+ }
+
+ private void changePrimaryBlogPreference(String blogId) {
+ mPrimarySitePreference.setValue(blogId);
+ Blog primaryBlog = WordPress.wpDB.getBlogForDotComBlogId(blogId);
+ if (primaryBlog != null) {
+ mPrimarySitePreference.setSummary(StringUtils.unescapeHTML(primaryBlog.getNameOrHostUrl()));
+ mPrimarySitePreference.refreshAdapter();
+ }
+ }
+
+ private void updateEmail(String newEmail) {
+ Account account = AccountHelper.getDefaultAccount();
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.EMAIL.getDescription(), newEmail);
+ account.postAccountSettings(params);
+ }
+
+ private void updatePrimaryBlog(String blogId) {
+ Account account = AccountHelper.getDefaultAccount();
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.PRIMARY_BLOG.getDescription(), blogId);
+ account.postAccountSettings(params);
+ }
+
+ public void updateWebAddress(String newWebAddress) {
+ Account account = AccountHelper.getDefaultAccount();
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.WEB_ADDRESS.getDescription(), newWebAddress);
+ account.postAccountSettings(params);
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchSuccess event) {
+ if (isAdded()) {
+ refreshAccountDetails();
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsPostSuccess event) {
+ if (isAdded()) {
+ refreshAccountDetails();
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchError event) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.error_fetch_account_settings, ToastUtils.Duration.LONG);
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsPostError event) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.error_post_account_settings, ToastUtils.Duration.LONG);
+
+ // we optimistically show the email change snackbar, if that request fails, we should remove the snackbar
+ checkIfEmailChangeIsPending();
+ }
+ }
+
+ /*
+ * AsyncTask which loads sites from database for primary site preference
+ */
+ private class LoadSitesTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ List<Map<String, Object>> blogList = WordPress.wpDB.getBlogsBy("dotcomFlag=1", new String[]{"homeURL"});
+ mPrimarySitePreference.setEntries(BlogUtils.getBlogNamesFromAccountMapList(blogList));
+ mPrimarySitePreference.setEntryValues(BlogUtils.getBlogIdsFromAccountMapList(blogList));
+ mPrimarySitePreference.setDetails(BlogUtils.getHomeURLOrHostNamesFromAccountMapList(blogList));
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void results) {
+ super.onPostExecute(results);
+ mPrimarySitePreference.refreshAdapter();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
new file mode 100644
index 000000000..60b088521
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -0,0 +1,407 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.PeopleListFilter;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.stats.StatsTimeframe;
+
+public class AppPrefs {
+ private static final int THEME_IMAGE_SIZE_WIDTH_DEFAULT = 400;
+
+ public interface PrefKey {
+ String name();
+ String toString();
+ }
+
+ /**
+ * Application related preferences. When the user disconnects, these preferences are erased.
+ */
+ public enum DeletablePrefKey implements PrefKey {
+ // name of last shown activity
+ LAST_ACTIVITY_STR,
+
+ // last selected tag in the reader
+ READER_TAG_NAME,
+ READER_TAG_TYPE,
+
+ // title of the last active page in ReaderSubsActivity
+ READER_SUBS_PAGE_TITLE,
+
+ // email retrieved and attached to mixpanel profile
+ MIXPANEL_EMAIL_ADDRESS,
+
+ // index of the last active tab in main activity
+ MAIN_TAB_INDEX,
+
+ // index of the last active item in Stats activity
+ STATS_ITEM_INDEX,
+
+ // Keep the associations between each widget_id/blog_id added to the app
+ STATS_WIDGET_KEYS_BLOGS,
+
+ // last data stored for the Stats Widgets
+ STATS_WIDGET_DATA,
+
+ // visual editor enabled
+ VISUAL_EDITOR_ENABLED,
+
+ // Store the number of times Stats are loaded without errors. It's used to show the Widget promo dialog.
+ STATS_WIDGET_PROMO_ANALYTICS,
+
+ // index of the last active status type in Comments activity
+ COMMENTS_STATUS_TYPE_INDEX,
+
+ // index of the last active people list filter in People Management activity
+ PEOPLE_LIST_FILTER_INDEX,
+ }
+
+ /**
+ * These preferences won't be deleted when the user disconnects. They should be used for device specifics or user
+ * independent prefs.
+ */
+ public enum UndeletablePrefKey implements PrefKey {
+ // Theme image size retrieval
+ THEME_IMAGE_SIZE_WIDTH,
+
+ // index of the last app-version
+ LAST_APP_VERSION_INDEX,
+
+ // visual editor available
+ VISUAL_EDITOR_AVAILABLE,
+
+ // When we need to show the Visual Editor Promo Dialog
+ VISUAL_EDITOR_PROMO_REQUIRED,
+
+ // Global plans features
+ GLOBAL_PLANS_PLANS_FEATURES,
+
+ // When we need to sync IAP data with the wpcom backend
+ IAP_SYNC_REQUIRED,
+
+ // When we need to show the Gravatar Change Promo Tooltip
+ GRAVATAR_CHANGE_PROMO_REQUIRED,
+ }
+
+ private static SharedPreferences prefs() {
+ return PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
+ }
+
+ private static String getString(PrefKey key) {
+ return getString(key, "");
+ }
+
+ private static String getString(PrefKey key, String defaultValue) {
+ return prefs().getString(key.name(), defaultValue);
+ }
+
+ private static void setString(PrefKey key, String value) {
+ SharedPreferences.Editor editor = prefs().edit();
+ if (TextUtils.isEmpty(value)) {
+ editor.remove(key.name());
+ } else {
+ editor.putString(key.name(), value);
+ }
+ editor.apply();
+ }
+
+ private static long getLong(PrefKey key) {
+ try {
+ String value = getString(key);
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static void setLong(PrefKey key, long value) {
+ setString(key, Long.toString(value));
+ }
+
+ private static int getInt(PrefKey key) {
+ try {
+ String value = getString(key);
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static void setInt(PrefKey key, int value) {
+ setString(key, Integer.toString(value));
+ }
+
+ private static boolean getBoolean(PrefKey key, boolean def) {
+
+ String value = getString(key, Boolean.toString(def));
+ return Boolean.parseBoolean(value);
+ }
+
+ private static void setBoolean(PrefKey key, boolean value) {
+ setString(key, Boolean.toString(value));
+ }
+
+ private static void remove(PrefKey key) {
+ prefs().edit().remove(key.name()).apply();
+ }
+
+ // Exposed methods
+
+ /**
+ * remove all user-related preferences
+ */
+ public static void reset() {
+ SharedPreferences.Editor editor = prefs().edit();
+ for (DeletablePrefKey key : DeletablePrefKey.values()) {
+ editor.remove(key.name());
+ }
+ editor.apply();
+ }
+
+ public static ReaderTag getReaderTag() {
+ String tagName = getString(DeletablePrefKey.READER_TAG_NAME);
+ if (TextUtils.isEmpty(tagName)) {
+ return null;
+ }
+ int tagType = getInt(DeletablePrefKey.READER_TAG_TYPE);
+ return ReaderUtils.getTagFromTagName(tagName, ReaderTagType.fromInt(tagType));
+ }
+
+ public static void setReaderTag(ReaderTag tag) {
+ if (tag != null && !TextUtils.isEmpty(tag.getTagSlug())) {
+ setString(DeletablePrefKey.READER_TAG_NAME, tag.getTagSlug());
+ setInt(DeletablePrefKey.READER_TAG_TYPE, tag.tagType.toInt());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.READER_TAG_NAME.name())
+ .remove(DeletablePrefKey.READER_TAG_TYPE.name())
+ .apply();
+ }
+ }
+
+ /**
+ * title of the last active page in ReaderSubsActivity - this is stored rather than
+ * the index of the page so we can re-order pages without affecting this value
+ */
+ public static String getReaderSubsPageTitle() {
+ return getString(DeletablePrefKey.READER_SUBS_PAGE_TITLE);
+ }
+
+ public static void setReaderSubsPageTitle(String pageTitle) {
+ setString(DeletablePrefKey.READER_SUBS_PAGE_TITLE, pageTitle);
+ }
+
+ public static StatsTimeframe getStatsTimeframe() {
+ int idx = getInt(DeletablePrefKey.STATS_ITEM_INDEX);
+ StatsTimeframe[] timeframeValues = StatsTimeframe.values();
+ if (timeframeValues.length < idx) {
+ return timeframeValues[0];
+ } else {
+ return timeframeValues[idx];
+ }
+ }
+
+ public static void setStatsTimeframe(StatsTimeframe timeframe) {
+ if (timeframe != null) {
+ setInt(DeletablePrefKey.STATS_ITEM_INDEX, timeframe.ordinal());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.STATS_ITEM_INDEX.name())
+ .apply();
+ }
+ }
+
+ public static CommentStatus getCommentsStatusFilter() {
+ int idx = getInt(DeletablePrefKey.COMMENTS_STATUS_TYPE_INDEX);
+ CommentStatus[] commentStatusValues = CommentStatus.values();
+ if (commentStatusValues.length < idx) {
+ return commentStatusValues[0];
+ } else {
+ return commentStatusValues[idx];
+ }
+ }
+ public static void setCommentsStatusFilter(CommentStatus commentstatus) {
+ if (commentstatus != null) {
+ setInt(DeletablePrefKey.COMMENTS_STATUS_TYPE_INDEX, commentstatus.ordinal());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.COMMENTS_STATUS_TYPE_INDEX.name())
+ .apply();
+ }
+ }
+
+ public static PeopleListFilter getPeopleListFilter() {
+ int idx = getInt(DeletablePrefKey.PEOPLE_LIST_FILTER_INDEX);
+ PeopleListFilter[] values = PeopleListFilter.values();
+ if (values.length < idx) {
+ return values[0];
+ } else {
+ return values[idx];
+ }
+ }
+ public static void setPeopleListFilter(PeopleListFilter peopleListFilter) {
+ if (peopleListFilter != null) {
+ setInt(DeletablePrefKey.PEOPLE_LIST_FILTER_INDEX, peopleListFilter.ordinal());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.PEOPLE_LIST_FILTER_INDEX.name())
+ .apply();
+ }
+ }
+
+ // Store the version code of the app. Used to check it the app was upgraded.
+ public static int getLastAppVersionCode() {
+ return getInt(UndeletablePrefKey.LAST_APP_VERSION_INDEX);
+ }
+
+ public static void setLastAppVersionCode(int versionCode) {
+ setInt(UndeletablePrefKey.LAST_APP_VERSION_INDEX, versionCode);
+ }
+
+ /**
+ * name of the last shown activity - used at startup to restore the previously selected
+ * activity, also used by analytics tracker
+ */
+ public static String getLastActivityStr() {
+ return getString(DeletablePrefKey.LAST_ACTIVITY_STR, ActivityId.UNKNOWN.name());
+ }
+
+ public static void setLastActivityStr(String value) {
+ setString(DeletablePrefKey.LAST_ACTIVITY_STR, value);
+ }
+
+ public static void resetLastActivityStr() {
+ remove(DeletablePrefKey.LAST_ACTIVITY_STR);
+ }
+
+ // Mixpanel email retrieval check
+
+ public static String getMixpanelUserEmail() {
+ return getString(DeletablePrefKey.MIXPANEL_EMAIL_ADDRESS, null);
+ }
+
+ public static void setMixpanelUserEmail(String email) {
+ setString(DeletablePrefKey.MIXPANEL_EMAIL_ADDRESS, email);
+ }
+
+ public static int getMainTabIndex() {
+ return getInt(DeletablePrefKey.MAIN_TAB_INDEX);
+ }
+
+ public static void setMainTabIndex(int index) {
+ setInt(DeletablePrefKey.MAIN_TAB_INDEX, index);
+ }
+
+ // Stats Widgets
+ public static void resetStatsWidgetsKeys() {
+ remove(DeletablePrefKey.STATS_WIDGET_KEYS_BLOGS);
+ }
+
+ public static String getStatsWidgetsKeys() {
+ return getString(DeletablePrefKey.STATS_WIDGET_KEYS_BLOGS);
+ }
+
+ public static void setStatsWidgetsKeys(String widgetData) {
+ setString(DeletablePrefKey.STATS_WIDGET_KEYS_BLOGS, widgetData);
+ }
+
+ public static String getStatsWidgetsData() {
+ return getString(DeletablePrefKey.STATS_WIDGET_DATA);
+ }
+
+ public static void setStatsWidgetsData(String widgetData) {
+ setString(DeletablePrefKey.STATS_WIDGET_DATA, widgetData);
+ }
+
+ public static void resetStatsWidgetsData() {
+ remove(DeletablePrefKey.STATS_WIDGET_DATA);
+ }
+
+ // Themes
+ public static void setThemeImageSizeWidth(int width) {
+ setInt(UndeletablePrefKey.THEME_IMAGE_SIZE_WIDTH, width);
+ }
+
+ public static int getThemeImageSizeWidth() {
+ int value = getInt(UndeletablePrefKey.THEME_IMAGE_SIZE_WIDTH);
+ if (value == 0) {
+ return THEME_IMAGE_SIZE_WIDTH_DEFAULT;
+ } else {
+ return getInt(UndeletablePrefKey.THEME_IMAGE_SIZE_WIDTH);
+ }
+ }
+
+ // Visual Editor
+ public static void setVisualEditorEnabled(boolean visualEditorEnabled) {
+ setBoolean(DeletablePrefKey.VISUAL_EDITOR_ENABLED, visualEditorEnabled);
+ AnalyticsTracker.track(visualEditorEnabled ? Stat.EDITOR_TOGGLED_ON : Stat.EDITOR_TOGGLED_OFF);
+ }
+
+ public static void setVisualEditorAvailable(boolean visualEditorAvailable) {
+ setBoolean(UndeletablePrefKey.VISUAL_EDITOR_AVAILABLE, visualEditorAvailable);
+ if (visualEditorAvailable) {
+ AnalyticsTracker.track(Stat.EDITOR_ENABLED_NEW_VERSION);
+ }
+ }
+
+ public static boolean isVisualEditorAvailable() {
+ return getBoolean(UndeletablePrefKey.VISUAL_EDITOR_AVAILABLE, false);
+ }
+
+ public static boolean isVisualEditorEnabled() {
+ return isVisualEditorAvailable() && getBoolean(DeletablePrefKey.VISUAL_EDITOR_ENABLED, true);
+ }
+
+ public static boolean isVisualEditorPromoRequired() {
+ return getBoolean(UndeletablePrefKey.VISUAL_EDITOR_PROMO_REQUIRED, true);
+ }
+
+ public static void setVisualEditorPromoRequired(boolean required) {
+ setBoolean(UndeletablePrefKey.VISUAL_EDITOR_PROMO_REQUIRED, required);
+ }
+
+ public static boolean isGravatarChangePromoRequired() {
+ return getBoolean(UndeletablePrefKey.GRAVATAR_CHANGE_PROMO_REQUIRED, true);
+ }
+
+ public static void setGravatarChangePromoRequired(boolean required) {
+ setBoolean(UndeletablePrefKey.GRAVATAR_CHANGE_PROMO_REQUIRED, required);
+ }
+
+ // Store the number of times Stats are loaded successfully before showing the Promo Dialog
+ public static void bumpAnalyticsForStatsWidgetPromo() {
+ int current = getAnalyticsForStatsWidgetPromo();
+ setInt(DeletablePrefKey.STATS_WIDGET_PROMO_ANALYTICS, current + 1);
+ }
+
+ public static int getAnalyticsForStatsWidgetPromo() {
+ return getInt(DeletablePrefKey.STATS_WIDGET_PROMO_ANALYTICS);
+ }
+
+ public static void setGlobalPlansFeatures(String jsonOfFeatures) {
+ if (jsonOfFeatures != null) {
+ setString(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES, jsonOfFeatures);
+ } else {
+ remove(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES);
+ }
+ }
+ public static String getGlobalPlansFeatures() {
+ return getString(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES, "");
+ }
+
+ public static boolean isInAppPurchaseRefreshRequired() {
+ return getBoolean(UndeletablePrefKey.IAP_SYNC_REQUIRED, false);
+ }
+ public static void setInAppPurchaseRefreshRequired(boolean required) {
+ setBoolean(UndeletablePrefKey.IAP_SYNC_REQUIRED, required);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java
new file mode 100644
index 000000000..1d16b4253
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import org.wordpress.passcodelock.AppLockManager;
+import org.wordpress.passcodelock.PasscodePreferenceFragment;
+
+public class AppSettingsActivity extends AppCompatActivity {
+ private static final String KEY_APP_SETTINGS_FRAGMENT = "app-settings-fragment";
+ private static final String KEY_PASSCODE_FRAGMENT = "passcode-fragment";
+
+ private AppSettingsFragment mAppSettingsFragment;
+ private PasscodePreferenceFragment mPasscodePreferenceFragment;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ mAppSettingsFragment = (AppSettingsFragment) fragmentManager.findFragmentByTag(KEY_APP_SETTINGS_FRAGMENT);
+ mPasscodePreferenceFragment = (PasscodePreferenceFragment) fragmentManager.findFragmentByTag(KEY_PASSCODE_FRAGMENT);
+ if (mAppSettingsFragment == null || mPasscodePreferenceFragment == null) {
+ Bundle passcodeArgs = new Bundle();
+ passcodeArgs.putBoolean(PasscodePreferenceFragment.KEY_SHOULD_INFLATE, false);
+ mAppSettingsFragment = new AppSettingsFragment();
+ mPasscodePreferenceFragment = new PasscodePreferenceFragment();
+ mPasscodePreferenceFragment.setArguments(passcodeArgs);
+
+ fragmentManager.beginTransaction()
+ .replace(android.R.id.content, mPasscodePreferenceFragment, KEY_PASSCODE_FRAGMENT)
+ .add(android.R.id.content, mAppSettingsFragment, KEY_APP_SETTINGS_FRAGMENT)
+ .commit();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ Preference togglePref =
+ mAppSettingsFragment.findPreference(getString(org.wordpress.passcodelock.R.string
+ .pref_key_passcode_toggle));
+ Preference changePref =
+ mAppSettingsFragment.findPreference(getString(org.wordpress.passcodelock.R.string
+ .pref_key_change_passcode));
+
+ if (togglePref != null && changePref != null) {
+ mPasscodePreferenceFragment.setPreferences(togglePref, changePref);
+ ((SwitchPreference) togglePref).setChecked(
+ AppLockManager.getInstance().getAppLock().isPasswordLocked());
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java
new file mode 100644
index 000000000..2fb651203
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java
@@ -0,0 +1,194 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.MenuItem;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.WPPrefUtils;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+public class AppSettingsFragment extends PreferenceFragment implements OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
+ public static final String LANGUAGE_PREF_KEY = "language-pref";
+ public static final int LANGUAGE_CHANGED = 1000;
+
+ private DetailListPreference mLanguagePreference;
+ private SharedPreferences mSettings;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setRetainInstance(true);
+ addPreferencesFromResource(R.xml.app_settings);
+
+ mLanguagePreference = (DetailListPreference) findPreference(getString(R.string.pref_key_language));
+ mLanguagePreference.setOnPreferenceChangeListener(this);
+
+ findPreference(getString(R.string.pref_key_language))
+ .setOnPreferenceClickListener(this);
+ findPreference(getString(R.string.pref_key_app_about))
+ .setOnPreferenceClickListener(this);
+ findPreference(getString(R.string.pref_key_oss_licenses))
+ .setOnPreferenceClickListener(this);
+
+ mSettings = PreferenceManager.getDefaultSharedPreferences(getActivity());
+
+ updateVisualEditorSettings();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ updateLanguagePreference(getResources().getConfiguration().locale.toString());
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ String preferenceKey = preference != null ? preference.getKey() : "";
+
+ if (preferenceKey.equals(getString(R.string.pref_key_app_about))) {
+ return handleAboutPreferenceClick();
+ } else if (preferenceKey.equals(getString(R.string.pref_key_oss_licenses))) {
+ return handleOssPreferenceClick();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue == null) return false;
+
+ if (preference == mLanguagePreference) {
+ changeLanguage(newValue.toString());
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void updateVisualEditorSettings() {
+ if (!AppPrefs.isVisualEditorAvailable()) {
+ PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference(getActivity()
+ .getString(R.string.pref_key_account_settings_root));
+ PreferenceCategory editor = (PreferenceCategory) findPreference(getActivity()
+ .getString(R.string.pref_key_editor));
+ if (preferenceScreen != null && editor != null) {
+ preferenceScreen.removePreference(editor);
+ }
+ } else {
+ final SwitchPreference visualEditorSwitch = (SwitchPreference) findPreference(getActivity()
+ .getString(R.string.pref_key_visual_editor_enabled));
+ visualEditorSwitch.setChecked(AppPrefs.isVisualEditorEnabled());
+ visualEditorSwitch.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ visualEditorSwitch.setChecked(!visualEditorSwitch.isChecked());
+ AppPrefs.setVisualEditorEnabled(visualEditorSwitch.isChecked());
+ return false;
+ }
+ });
+ }
+ }
+
+ private void changeLanguage(String languageCode) {
+ if (mLanguagePreference == null || TextUtils.isEmpty(languageCode)) return;
+
+ Resources res = getResources();
+ Configuration conf = res.getConfiguration();
+ Locale currentLocale = conf.locale != null ? conf.locale : LanguageUtils.getCurrentDeviceLanguage(WordPress.getContext());
+
+ if (currentLocale.toString().equals(languageCode)) return;
+
+ updateLanguagePreference(languageCode);
+
+ // update configuration
+ Locale newLocale = WPPrefUtils.languageLocale(languageCode);
+ conf.locale = newLocale;
+ res.updateConfiguration(conf, res.getDisplayMetrics());
+
+ if (LanguageUtils.getCurrentDeviceLanguage(WordPress.getContext()).equals(newLocale)) {
+ // remove custom locale key when original device locale is selected
+ mSettings.edit().remove(LANGUAGE_PREF_KEY).apply();
+ } else {
+ mSettings.edit().putString(LANGUAGE_PREF_KEY, newLocale.toString()).apply();
+ }
+
+ // Track language change on Mixpanel because we have both the device language and app selected language
+ // data in Tracks metadata.
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("app_locale", conf.locale.toString());
+ AnalyticsTracker.track(Stat.ACCOUNT_SETTINGS_LANGUAGE_CHANGED, properties);
+
+ // Language is now part of metadata, so we need to refresh them
+ AnalyticsUtils.refreshMetadata();
+
+ // Refresh the app
+ Intent refresh = new Intent(getActivity(), getActivity().getClass());
+ startActivity(refresh);
+ getActivity().setResult(LANGUAGE_CHANGED);
+ getActivity().finish();
+ }
+
+ private void updateLanguagePreference(String languageCode) {
+ if (mLanguagePreference == null || TextUtils.isEmpty(languageCode)) return;
+
+ Locale languageLocale = WPPrefUtils.languageLocale(languageCode);
+ String[] availableLocales = getResources().getStringArray(R.array.available_languages);
+
+ Pair<String[], String[]> pair = WPPrefUtils.createSortedLanguageDisplayStrings(availableLocales, languageLocale);
+ // check for a possible NPE
+ if (pair == null) return;
+
+ String[] sortedEntries = pair.first;
+ String[] sortedValues = pair.second;
+
+ mLanguagePreference.setEntries(sortedEntries);
+ mLanguagePreference.setEntryValues(sortedValues);
+ mLanguagePreference.setDetails(WPPrefUtils.createLanguageDetailDisplayStrings(sortedValues));
+
+ mLanguagePreference.setValue(languageCode);
+ mLanguagePreference.setSummary(WPPrefUtils.getLanguageString(languageCode, languageLocale));
+ mLanguagePreference.refreshAdapter();
+ }
+
+ private boolean handleAboutPreferenceClick() {
+ startActivity(new Intent(getActivity(), AboutActivity.class));
+ return true;
+ }
+
+ private boolean handleOssPreferenceClick() {
+ startActivity(new Intent(getActivity(), LicensesActivity.class));
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java
new file mode 100644
index 000000000..41a5417dc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java
@@ -0,0 +1,354 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.ConnectionChangeReceiver;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.CoreEvents.UserSignedOutCompletely;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Activity for configuring blog specific settings.
+ */
+public class BlogPreferencesActivity extends AppCompatActivity {
+ public static final String ARG_LOCAL_BLOG_ID = SiteSettingsFragment.ARG_LOCAL_BLOG_ID;
+ public static final int RESULT_BLOG_REMOVED = RESULT_FIRST_USER;
+
+ private static final String KEY_SETTINGS_FRAGMENT = "settings-fragment";
+
+ // The blog this activity is managing settings for.
+ private Blog blog;
+ private boolean mBlogDeleted;
+ private EditText mUsernameET;
+ private EditText mPasswordET;
+ private EditText mHttpUsernameET;
+ private EditText mHttpPasswordET;
+ private CheckBox mFullSizeCB;
+ private CheckBox mScaledCB;
+ private Spinner mImageWidthSpinner;
+ private EditText mScaledImageWidthET;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Integer id = getIntent().getIntExtra(ARG_LOCAL_BLOG_ID, -1);
+ blog = WordPress.getBlog(id);
+ if (WordPress.getBlog(id) == null) {
+ Toast.makeText(this, getString(R.string.blog_not_found), Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ if (blog.isDotcomFlag()) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ Fragment siteSettingsFragment = fragmentManager.findFragmentByTag(KEY_SETTINGS_FRAGMENT);
+
+ if (siteSettingsFragment == null) {
+ siteSettingsFragment = new SiteSettingsFragment();
+ siteSettingsFragment.setArguments(getIntent().getExtras());
+ fragmentManager.beginTransaction()
+ .replace(android.R.id.content, siteSettingsFragment, KEY_SETTINGS_FRAGMENT)
+ .commit();
+ }
+ } else {
+ setContentView(R.layout.blog_preferences);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(StringUtils.unescapeHTML(blog.getNameOrHostUrl()));
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mUsernameET = (EditText) findViewById(R.id.username);
+ mPasswordET = (EditText) findViewById(R.id.password);
+ mHttpUsernameET = (EditText) findViewById(R.id.httpuser);
+ mHttpPasswordET = (EditText) findViewById(R.id.httppassword);
+ mScaledImageWidthET = (EditText) findViewById(R.id.scaledImageWidth);
+ mFullSizeCB = (CheckBox) findViewById(R.id.fullSizeImage);
+ mScaledCB = (CheckBox) findViewById(R.id.scaledImage);
+ mImageWidthSpinner = (Spinner) findViewById(R.id.maxImageWidth);
+ Button removeBlogButton = (Button) findViewById(R.id.remove_account);
+
+ // remove blog & credentials apply only to dot org
+ if (blog.isDotcomFlag()) {
+ View credentialsRL = findViewById(R.id.sectionContent);
+ credentialsRL.setVisibility(View.GONE);
+ removeBlogButton.setVisibility(View.GONE);
+ } else {
+ removeBlogButton.setVisibility(View.VISIBLE);
+ removeBlogButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ removeBlogWithConfirmation();
+ }
+ });
+ }
+
+ loadSettingsForBlog();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (blog.isDotcomFlag() || mBlogDeleted) {
+ return;
+ }
+
+ blog.setUsername(mUsernameET.getText().toString());
+ blog.setPassword(mPasswordET.getText().toString());
+ blog.setHttpuser(mHttpUsernameET.getText().toString());
+ blog.setHttppassword(mHttpPasswordET.getText().toString());
+
+ blog.setFullSizeImage(mFullSizeCB.isChecked());
+ blog.setScaledImage(mScaledCB.isChecked());
+ if (blog.isScaledImage()) {
+ EditText scaledImgWidth = (EditText) findViewById(R.id.scaledImageWidth);
+
+ boolean error = false;
+ int width = 0;
+ try {
+ width = Integer.parseInt(scaledImgWidth.getText().toString().trim());
+ } catch (NumberFormatException e) {
+ error = true;
+ }
+
+ if (width == 0) {
+ error = true;
+ }
+
+ if (error) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(BlogPreferencesActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(getResources().getText(R.string.scaled_image_error));
+ dialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ return;
+ } else {
+ blog.setScaledImageWidth(width);
+ }
+ }
+
+ blog.setMaxImageWidth(mImageWidthSpinner.getSelectedItem().toString());
+
+ WordPress.wpDB.saveBlog(blog);
+
+ if (WordPress.getCurrentBlog().getLocalTableBlogId() == blog.getLocalTableBlogId()) {
+ WordPress.currentBlog = blog;
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemID = item.getItemId();
+ if (itemID == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) {
+ FragmentManager fragmentManager = getFragmentManager();
+ SiteSettingsFragment siteSettingsFragment =
+ (SiteSettingsFragment) fragmentManager.findFragmentByTag(KEY_SETTINGS_FRAGMENT);
+
+ if (siteSettingsFragment != null) {
+ if (!event.isConnected()) {
+ ToastUtils.showToast(this, getString(R.string.site_settings_disconnected_toast));
+ }
+ siteSettingsFragment.setEditingEnabled(event.isConnected());
+
+ // TODO: add this back when delete blog is back
+ //https://github.com/wordpress-mobile/WordPress-Android/commit/6a90e3fe46e24ee40abdc4a7f8f0db06f157900c
+ // Checks for stats widgets that were synched with a blog that could be gone now.
+// StatsWidgetProvider.updateWidgetsOnLogout(this);
+ }
+ }
+
+ private void loadSettingsForBlog() {
+ ArrayAdapter<Object> spinnerArrayAdapter = new ArrayAdapter<Object>(this,
+ R.layout.simple_spinner_item, new String[]{
+ "Original Size", "100", "200", "300", "400", "500", "600", "700", "800",
+ "900", "1000", "1100", "1200", "1300", "1400", "1500", "1600", "1700",
+ "1800", "1900", "2000"
+ });
+ spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mImageWidthSpinner.setAdapter(spinnerArrayAdapter);
+ mImageWidthSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ CheckBox fullSizeImageCheckBox = (CheckBox) findViewById(R.id.fullSizeImage);
+ // Original size selected. Do not show the link to full image.
+ if (id == 0) {
+ fullSizeImageCheckBox.setVisibility(View.GONE);
+ } else {
+ fullSizeImageCheckBox.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> arg0) {
+ }
+ });
+
+ mUsernameET.setText(blog.getUsername());
+ mPasswordET.setText(blog.getPassword());
+ mHttpUsernameET.setText(blog.getHttpuser());
+ mHttpPasswordET.setText(blog.getHttppassword());
+ TextView httpUserLabel = (TextView) findViewById(R.id.l_httpuser);
+ if (blog.isDotcomFlag()) {
+ mHttpUsernameET.setVisibility(View.GONE);
+ mHttpPasswordET.setVisibility(View.GONE);
+ httpUserLabel.setVisibility(View.GONE);
+ } else {
+ mHttpUsernameET.setVisibility(View.VISIBLE);
+ mHttpPasswordET.setVisibility(View.VISIBLE);
+ httpUserLabel.setVisibility(View.VISIBLE);
+ }
+
+ mFullSizeCB.setChecked(blog.isFullSizeImage());
+ mScaledCB.setChecked(blog.isScaledImage());
+
+ this.mScaledImageWidthET.setText("" + blog.getScaledImageWidth());
+ showScaledSetting(blog.isScaledImage());
+
+ CheckBox scaledImage = (CheckBox) findViewById(R.id.scaledImage);
+ scaledImage.setChecked(false);
+ scaledImage.setVisibility(View.GONE);
+
+ // sets up a state listener for the full-size checkbox
+ CheckBox fullSizeImageCheckBox = (CheckBox) findViewById(R.id.fullSizeImage);
+ fullSizeImageCheckBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ CheckBox fullSize = (CheckBox) findViewById(R.id.fullSizeImage);
+ if (fullSize.isChecked()) {
+ CheckBox scaledImage = (CheckBox) findViewById(R.id.scaledImage);
+ if (scaledImage.isChecked()) {
+ scaledImage.setChecked(false);
+ showScaledSetting(false);
+ }
+ }
+ }
+ });
+
+ int imageWidthPosition = spinnerArrayAdapter.getPosition(blog.getMaxImageWidth());
+ mImageWidthSpinner.setSelection((imageWidthPosition >= 0) ? imageWidthPosition : 0);
+ if (mImageWidthSpinner.getSelectedItemPosition() ==
+ 0) //Original size selected. Do not show the link to full image.
+ {
+ fullSizeImageCheckBox.setVisibility(View.GONE);
+ } else {
+ fullSizeImageCheckBox.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Hides / shows the scaled image settings
+ */
+ private void showScaledSetting(boolean show) {
+ TextView tw = (TextView) findViewById(R.id.l_scaledImage);
+ EditText et = (EditText) findViewById(R.id.scaledImageWidth);
+ tw.setVisibility(show ? View.VISIBLE : View.GONE);
+ et.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Remove the blog this activity is managing settings for.
+ */
+ private void removeBlogWithConfirmation() {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setTitle(getResources().getText(R.string.remove_account));
+ dialogBuilder.setMessage(getResources().getText(R.string.sure_to_remove_account));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ removeBlog();
+ }
+ });
+ dialogBuilder.setNegativeButton(getResources().getText(R.string.no), null);
+ dialogBuilder.setCancelable(false);
+ dialogBuilder.create().show();
+ }
+
+ private void removeBlog() {
+ if (WordPress.wpDB.deleteBlog(this, blog.getLocalTableBlogId())) {
+ StatsTable.deleteStatsForBlog(this,blog.getLocalTableBlogId()); // Remove stats data
+ AnalyticsUtils.refreshMetadata();
+ ToastUtils.showToast(this, R.string.blog_removed_successfully);
+ WordPress.wpDB.deleteLastBlogId();
+ WordPress.currentBlog = null;
+ mBlogDeleted = true;
+ setResult(RESULT_BLOG_REMOVED);
+
+ // If the last blog is removed and the user is not signed in wpcom, broadcast a UserSignedOut event
+ if (!AccountHelper.isSignedIn()) {
+ EventBus.getDefault().post(new UserSignedOutCompletely());
+ }
+
+ // Checks for stats widgets that were synched with a blog that could be gone now.
+ StatsWidgetProvider.updateWidgetsOnLogout(this);
+
+ finish();
+ } else {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(getResources().getText(R.string.could_not_remove_account));
+ dialogBuilder.setPositiveButton("OK", null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java
new file mode 100644
index 000000000..bcc3119da
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java
@@ -0,0 +1,128 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextWatcher;
+import android.text.style.StyleSpan;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.util.AnalyticsUtils;
+
+public class DeleteSiteDialogFragment extends DialogFragment implements TextWatcher, DialogInterface.OnShowListener {
+ public static final String SITE_DOMAIN_KEY = "site-domain";
+
+ private AlertDialog mDeleteSiteDialog;
+ private EditText mUrlConfirmation;
+ private Button mDeleteButton;
+ private String mSiteDomain = "";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_ACCESSED);
+ retrieveSiteDomain();
+ configureAlertViewBuilder(builder);
+
+ mDeleteSiteDialog = builder.create();
+ mDeleteSiteDialog.setOnShowListener(this);
+
+ return mDeleteSiteDialog;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (isUrlConfirmationTextValid()) {
+ mDeleteButton.setEnabled(true);
+ } else {
+ mDeleteButton.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void onShow(DialogInterface dialog) {
+ mDeleteButton = mDeleteSiteDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ mDeleteButton.setEnabled(false);
+ }
+
+ private void configureAlertViewBuilder(AlertDialog.Builder builder) {
+ builder.setTitle(R.string.confirm_delete_site);
+ builder.setMessage(confirmationPromptString());
+
+ configureUrlConfirmation(builder);
+ configureButtons(builder);
+ }
+
+ private void configureButtons(AlertDialog.Builder builder) {
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ });
+ builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Fragment target = getTargetFragment();
+ if (target != null) {
+ target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, null);
+ }
+
+ dismiss();
+ }
+ });
+ }
+
+ private Spannable confirmationPromptString() {
+ String deletePrompt = String.format(getString(R.string.confirm_delete_site_prompt), mSiteDomain);
+ Spannable promptSpannable = new SpannableString(deletePrompt);
+ int beginning = deletePrompt.indexOf(mSiteDomain);
+ int end = beginning + mSiteDomain.length();
+ promptSpannable.setSpan(new StyleSpan(Typeface.BOLD), beginning, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ return promptSpannable;
+ }
+
+ private void configureUrlConfirmation(AlertDialog.Builder builder) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.delete_site_dialog, null);
+ mUrlConfirmation = (EditText) view.findViewById(R.id.url_confirmation);
+ mUrlConfirmation.addTextChangedListener(this);
+ builder.setView(view);
+ }
+
+ private void retrieveSiteDomain() {
+ Bundle args = getArguments();
+ mSiteDomain = getString(R.string.wordpress_dot_com).toLowerCase();
+ if (args != null) {
+ mSiteDomain = args.getString(SITE_DOMAIN_KEY);
+ }
+ }
+
+ private boolean isUrlConfirmationTextValid() {
+ String confirmationText = mUrlConfirmation.getText().toString().trim().toLowerCase();
+ String hintText = mSiteDomain.toLowerCase();
+
+ return confirmationText.equals(hintText);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java
new file mode 100644
index 000000000..d8de0038c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java
@@ -0,0 +1,265 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+/**
+ * Custom {@link ListPreference} used to display detail text per item.
+ */
+
+public class DetailListPreference extends ListPreference
+ implements PreferenceHint {
+ private DetailListAdapter mListAdapter;
+ private String[] mDetails;
+ private String mStartingValue;
+ private int mSelectedIndex;
+ private String mHint;
+ private AlertDialog mDialog;
+ private int mWhichButtonClicked;
+
+ public DetailListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DetailListPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.DetailListPreference_entryDetails) {
+ int id = array.getResourceId(index, -1);
+ if (id != -1) {
+ mDetails = array.getResources().getStringArray(id);
+ }
+ } else if (index == R.styleable.DetailListPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+
+ mSelectedIndex = -1;
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ setupView((TextView) view.findViewById(android.R.id.title),
+ R.dimen.text_sz_large, R.color.grey_dark, R.color.grey_lighten_10);
+ setupView((TextView) view.findViewById(android.R.id.summary),
+ R.dimen.text_sz_medium, R.color.grey_darken_10, R.color.grey_lighten_10);
+ }
+
+ @Override
+ public CharSequence getEntry() {
+ int index = findIndexOfValue(getValue());
+ CharSequence[] entries = getEntries();
+
+ if (entries != null && index >= 0 && index < entries.length) {
+ return entries[index];
+ }
+ return null;
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ Context context = getContext();
+ Resources res = context.getResources();
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Calypso_AlertDialog);
+
+ mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(res.getString(R.string.cancel).toUpperCase(), this);
+
+ if (mDetails == null) {
+ mDetails = new String[getEntries() == null ? 1 : getEntries().length];
+ }
+
+ mListAdapter = new DetailListAdapter(getContext(), R.layout.detail_list_preference, mDetails);
+ mStartingValue = getValue();
+ mSelectedIndex = findIndexOfValue(mStartingValue);
+
+ builder.setSingleChoiceItems(mListAdapter, mSelectedIndex,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mSelectedIndex = which;
+ }
+ });
+
+ View titleView = View.inflate(getContext(), R.layout.detail_list_preference_title, null);
+
+ if (titleView != null) {
+ TextView titleText = (TextView) titleView.findViewById(R.id.title);
+ if (titleText != null) {
+ titleText.setText(getTitle());
+ }
+
+ builder.setCustomTitle(titleView);
+ } else {
+ builder.setTitle(getTitle());
+ }
+
+ if ((mDialog = builder.create()) == null) return;
+
+ if (state != null) {
+ mDialog.onRestoreInstanceState(state);
+ }
+ mDialog.setOnDismissListener(this);
+ mDialog.show();
+
+ ListView listView = mDialog.getListView();
+ Button positive = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ Typeface typeface = WPPrefUtils.getSemiboldTypeface(getContext());
+
+ if (listView != null) {
+ listView.setDividerHeight(0);
+ listView.setClipToPadding(true);
+ listView.setPadding(0, 0, 0, res.getDimensionPixelSize(R.dimen.site_settings_divider_height));
+ }
+
+ if (positive != null) {
+ //noinspection deprecation
+ positive.setTextColor(res.getColor(R.color.blue_medium));
+ positive.setTypeface(typeface);
+ }
+
+ if (negative != null) {
+ //noinspection deprecation
+ negative.setTextColor(res.getColor(R.color.blue_medium));
+ negative.setTypeface(typeface);
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mDialog = null;
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ int index = positiveResult ? mSelectedIndex : findIndexOfValue(mStartingValue);
+ CharSequence[] values = getEntryValues();
+ if (values != null && index >= 0 && index < values.length) {
+ String value = String.valueOf(values[index]);
+ callChangeListener(value);
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+
+ public void refreshAdapter() {
+ if (mListAdapter != null) {
+ mListAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void setDetails(String[] details) {
+ mDetails = details;
+ refreshAdapter();
+ }
+
+ /**
+ * Helper method to style the Preference screen view
+ */
+ private void setupView(TextView view, int sizeRes, int enabledColorRes, int disabledColorRes) {
+ if (view != null) {
+ Resources res = getContext().getResources();
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(sizeRes));
+ //noinspection deprecation
+ view.setTextColor(res.getColor(isEnabled() ? enabledColorRes : disabledColorRes));
+ }
+ }
+
+ private class DetailListAdapter extends ArrayAdapter<String> {
+ public DetailListAdapter(Context context, int resource, String[] objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = View.inflate(getContext(), R.layout.detail_list_preference, null);
+ }
+
+ final RadioButton radioButton = (RadioButton) convertView.findViewById(R.id.radio);
+ TextView mainText = (TextView) convertView.findViewById(R.id.main_text);
+ TextView detailText = (TextView) convertView.findViewById(R.id.detail_text);
+
+ if (mainText != null && getEntries() != null && position < getEntries().length) {
+ mainText.setText(getEntries()[position]);
+ }
+
+ if (detailText != null) {
+ if (mDetails != null && position < mDetails.length && !TextUtils.isEmpty(mDetails[position])) {
+ detailText.setVisibility(View.VISIBLE);
+ detailText.setText(mDetails[position]);
+ } else {
+ detailText.setVisibility(View.GONE);
+ }
+ }
+
+ if (radioButton != null) {
+ radioButton.setChecked(mSelectedIndex == position);
+ radioButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeSelection(position);
+ }
+ });
+ }
+
+ convertView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeSelection(position);
+ }
+ });
+
+ return convertView;
+ }
+
+ private void changeSelection(int position) {
+ mSelectedIndex = position;
+ notifyDataSetChanged();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java
new file mode 100644
index 000000000..b59174ab5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java
@@ -0,0 +1,383 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+class DotComSiteSettings extends SiteSettingsInterface {
+ // WP.com REST keys used in response to a settings GET and POST request
+ public static final String LANGUAGE_ID_KEY = "lang_id";
+ public static final String PRIVACY_KEY = "blog_public";
+ public static final String URL_KEY = "URL";
+ public static final String DEF_CATEGORY_KEY = "default_category";
+ public static final String DEF_POST_FORMAT_KEY = "default_post_format";
+ public static final String RELATED_POSTS_ALLOWED_KEY = "jetpack_relatedposts_allowed";
+ public static final String RELATED_POSTS_ENABLED_KEY = "jetpack_relatedposts_enabled";
+ public static final String RELATED_POSTS_HEADER_KEY = "jetpack_relatedposts_show_headline";
+ public static final String RELATED_POSTS_IMAGES_KEY = "jetpack_relatedposts_show_thumbnails";
+ public static final String ALLOW_COMMENTS_KEY = "default_comment_status";
+ public static final String SEND_PINGBACKS_KEY = "default_pingback_flag";
+ public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status";
+ public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts";
+ public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old";
+ public static final String THREAD_COMMENTS_KEY = "thread_comments";
+ public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth";
+ public static final String PAGE_COMMENTS_KEY = "page_comments";
+ public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page";
+ public static final String COMMENT_SORT_ORDER_KEY = "comment_order";
+ public static final String COMMENT_MODERATION_KEY = "comment_moderation";
+ public static final String REQUIRE_IDENTITY_KEY = "require_name_email";
+ public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration";
+ public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist";
+ public static final String MAX_LINKS_KEY = "comment_max_links";
+ public static final String MODERATION_KEYS_KEY = "moderation_keys";
+ public static final String BLACKLIST_KEYS_KEY = "blacklist_keys";
+
+ // WP.com REST keys used to GET certain site settings
+ public static final String GET_TITLE_KEY = "name";
+ public static final String GET_DESC_KEY = "description";
+
+ // WP.com REST keys used to POST updates to site settings
+ private static final String SET_TITLE_KEY = "blogname";
+ private static final String SET_DESC_KEY = "blogdescription";
+
+ // JSON response keys
+ private static final String SETTINGS_KEY = "settings";
+ private static final String UPDATED_KEY = "updated";
+
+ // WP.com REST keys used in response to a categories GET request
+ private static final String CAT_ID_KEY = "ID";
+ private static final String CAT_NAME_KEY = "name";
+ private static final String CAT_SLUG_KEY = "slug";
+ private static final String CAT_DESC_KEY = "description";
+ private static final String CAT_PARENT_ID_KEY = "parent";
+ private static final String CAT_POST_COUNT_KEY = "post_count";
+ private static final String CAT_NUM_POSTS_KEY = "found";
+ private static final String CATEGORIES_KEY = "categories";
+
+ /**
+ * Only instantiated by {@link SiteSettingsInterface}.
+ */
+ DotComSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) {
+ super(host, blog, listener);
+ }
+
+ @Override
+ public void saveSettings() {
+ super.saveSettings();
+
+ final Map<String, String> params = serializeDotComParams();
+ if (params == null || params.isEmpty()) return;
+
+ WordPress.getRestClientUtils().setGeneralSiteSettings(
+ String.valueOf(mBlog.getRemoteBlogId()), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(AppLog.T.API, "Site Settings saved remotely");
+ notifySavedOnUiThread(null);
+ mRemoteSettings.copyFrom(mSettings);
+
+ if (response != null) {
+ JSONObject updated = response.optJSONObject(UPDATED_KEY);
+ if (updated == null) return;
+ HashMap<String, Object> properties = new HashMap<>();
+ Iterator<String> keys = updated.keys();
+ while (keys.hasNext()) {
+ String currentKey = keys.next();
+ Object currentValue = updated.opt(currentKey);
+ if (currentValue != null) {
+ properties.put(SAVED_ITEM_PREFIX + currentKey, currentValue);
+ }
+ }
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.w(AppLog.T.API, "Error POSTing site settings changes: " + error);
+ notifySavedOnUiThread(error);
+ }
+ }, params);
+ }
+
+ /**
+ * Request remote site data via the WordPress REST API.
+ */
+ @Override
+ protected void fetchRemoteData() {
+ fetchCategories();
+ WordPress.getRestClientUtils().getGeneralSettings(
+ String.valueOf(mBlog.getRemoteBlogId()), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(AppLog.T.API, "Received response to Settings REST request.");
+ credentialsVerified(true);
+
+ mRemoteSettings.localTableId = mBlog.getRemoteBlogId();
+ deserializeDotComRestResponse(mBlog, response);
+ if (!mRemoteSettings.equals(mSettings)) {
+ // postFormats setting is not returned by this api call so copy it over
+ final Map<String, String> currentPostFormats = mSettings.postFormats;
+
+ mSettings.copyFrom(mRemoteSettings);
+
+ mSettings.postFormats = currentPostFormats;
+
+ SiteSettingsTable.saveSettings(mSettings);
+ notifyUpdatedOnUiThread(null);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.w(AppLog.T.API, "Error response to Settings REST request: " + error);
+ notifyUpdatedOnUiThread(error);
+ }
+ });
+ }
+
+ /**
+ * Sets values from a .com REST response object.
+ */
+ public void deserializeDotComRestResponse(Blog blog, JSONObject response) {
+ if (blog == null || response == null) return;
+ JSONObject settingsObject = response.optJSONObject(SETTINGS_KEY);
+
+ mRemoteSettings.username = blog.getUsername();
+ mRemoteSettings.password = blog.getPassword();
+ mRemoteSettings.address = response.optString(URL_KEY, "");
+ mRemoteSettings.title = response.optString(GET_TITLE_KEY, "");
+ mRemoteSettings.tagline = response.optString(GET_DESC_KEY, "");
+ mRemoteSettings.languageId = settingsObject.optInt(LANGUAGE_ID_KEY, -1);
+ mRemoteSettings.privacy = settingsObject.optInt(PRIVACY_KEY, -2);
+ mRemoteSettings.defaultCategory = settingsObject.optInt(DEF_CATEGORY_KEY, 0);
+ mRemoteSettings.defaultPostFormat = settingsObject.optString(DEF_POST_FORMAT_KEY, "0");
+ mRemoteSettings.language = languageIdToLanguageCode(Integer.toString(mRemoteSettings.languageId));
+ mRemoteSettings.allowComments = settingsObject.optBoolean(ALLOW_COMMENTS_KEY, true);
+ mRemoteSettings.sendPingbacks = settingsObject.optBoolean(SEND_PINGBACKS_KEY, false);
+ mRemoteSettings.receivePingbacks = settingsObject.optBoolean(RECEIVE_PINGBACKS_KEY, true);
+ mRemoteSettings.shouldCloseAfter = settingsObject.optBoolean(CLOSE_OLD_COMMENTS_KEY, false);
+ mRemoteSettings.closeCommentAfter = settingsObject.optInt(CLOSE_OLD_COMMENTS_DAYS_KEY, 0);
+ mRemoteSettings.shouldThreadComments = settingsObject.optBoolean(THREAD_COMMENTS_KEY, false);
+ mRemoteSettings.threadingLevels = settingsObject.optInt(THREAD_COMMENTS_DEPTH_KEY, 0);
+ mRemoteSettings.shouldPageComments = settingsObject.optBoolean(PAGE_COMMENTS_KEY, false);
+ mRemoteSettings.commentsPerPage = settingsObject.optInt(PAGE_COMMENT_COUNT_KEY, 0);
+ mRemoteSettings.commentApprovalRequired = settingsObject.optBoolean(COMMENT_MODERATION_KEY, false);
+ mRemoteSettings.commentsRequireIdentity = settingsObject.optBoolean(REQUIRE_IDENTITY_KEY, false);
+ mRemoteSettings.commentsRequireUserAccount = settingsObject.optBoolean(REQUIRE_USER_ACCOUNT_KEY, true);
+ mRemoteSettings.commentAutoApprovalKnownUsers = settingsObject.optBoolean(WHITELIST_KNOWN_USERS_KEY, false);
+ mRemoteSettings.maxLinks = settingsObject.optInt(MAX_LINKS_KEY, 0);
+ mRemoteSettings.holdForModeration = new ArrayList<>();
+ mRemoteSettings.blacklist = new ArrayList<>();
+
+ String modKeys = settingsObject.optString(MODERATION_KEYS_KEY, "");
+ if (modKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n"));
+ }
+ String blacklistKeys = settingsObject.optString(BLACKLIST_KEYS_KEY, "");
+ if (blacklistKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n"));
+ }
+
+ if (settingsObject.optString(COMMENT_SORT_ORDER_KEY, "").equals("asc")) {
+ mRemoteSettings.sortCommentsBy = ASCENDING_SORT;
+ } else {
+ mRemoteSettings.sortCommentsBy = DESCENDING_SORT;
+ }
+
+ if (settingsObject.optBoolean(RELATED_POSTS_ALLOWED_KEY, false)) {
+ mRemoteSettings.showRelatedPosts = settingsObject.optBoolean(RELATED_POSTS_ENABLED_KEY, false);
+ mRemoteSettings.showRelatedPostHeader = settingsObject.optBoolean(RELATED_POSTS_HEADER_KEY, false);
+ mRemoteSettings.showRelatedPostImages = settingsObject.optBoolean(RELATED_POSTS_IMAGES_KEY, false);
+ }
+ }
+
+ /**
+ * Helper method to create the parameters for the site settings POST request
+ *
+ * Using undocumented endpoint WPCOM_JSON_API_Site_Settings_Endpoint
+ * https://wpcom.trac.automattic.com/browser/trunk/public.api/rest/json-endpoints.php#L1903
+ */
+ public Map<String, String> serializeDotComParams() {
+ Map<String, String> params = new HashMap<>();
+
+ if (mSettings.title!= null && !mSettings.title.equals(mRemoteSettings.title)) {
+ params.put(SET_TITLE_KEY, mSettings.title);
+ }
+ if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) {
+ params.put(SET_DESC_KEY, mSettings.tagline);
+ }
+ if (mSettings.languageId != mRemoteSettings.languageId) {
+ params.put(LANGUAGE_ID_KEY, String.valueOf((mSettings.languageId)));
+ }
+ if (mSettings.privacy != mRemoteSettings.privacy) {
+ params.put(PRIVACY_KEY, String.valueOf((mSettings.privacy)));
+ }
+ if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) {
+ params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory));
+ }
+ if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) {
+ params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat);
+ }
+ if (mSettings.showRelatedPosts != mRemoteSettings.showRelatedPosts ||
+ mSettings.showRelatedPostHeader != mRemoteSettings.showRelatedPostHeader ||
+ mSettings.showRelatedPostImages != mRemoteSettings.showRelatedPostImages) {
+ params.put(RELATED_POSTS_ENABLED_KEY, String.valueOf(mSettings.showRelatedPosts));
+ params.put(RELATED_POSTS_HEADER_KEY, String.valueOf(mSettings.showRelatedPostHeader));
+ params.put(RELATED_POSTS_IMAGES_KEY, String.valueOf(mSettings.showRelatedPostImages));
+ }
+ if (mSettings.allowComments != mRemoteSettings.allowComments) {
+ params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments));
+ }
+ if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) {
+ params.put(SEND_PINGBACKS_KEY, String.valueOf(mSettings.sendPingbacks));
+ }
+ if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) {
+ params.put(RECEIVE_PINGBACKS_KEY, String.valueOf(mSettings.receivePingbacks));
+ }
+ if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) {
+ params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired));
+ }
+ if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter
+ || mSettings.shouldCloseAfter != mRemoteSettings.shouldCloseAfter) {
+ params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(mSettings.shouldCloseAfter));
+ params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter));
+ }
+ if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) {
+ if (mSettings.sortCommentsBy == ASCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "asc");
+ } else if (mSettings.sortCommentsBy == DESCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "desc");
+ }
+ }
+ if (mSettings.threadingLevels != mRemoteSettings.threadingLevels
+ || mSettings.shouldThreadComments != mRemoteSettings.shouldThreadComments) {
+ params.put(THREAD_COMMENTS_KEY, String.valueOf(mSettings.shouldThreadComments));
+ params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels));
+ }
+ if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage
+ || mSettings.shouldPageComments != mRemoteSettings.shouldPageComments) {
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(mSettings.shouldPageComments));
+ params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage));
+ }
+ if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) {
+ params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity));
+ }
+ if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) {
+ params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount));
+ }
+ if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) {
+ params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers));
+ }
+ if (mSettings.maxLinks != mRemoteSettings.maxLinks) {
+ params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks));
+ }
+ if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.holdForModeration) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(MODERATION_KEYS_KEY, "");
+ }
+ }
+ if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.blacklist) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(BLACKLIST_KEYS_KEY, "");
+ }
+ }
+
+ return params;
+ }
+
+ /**
+ * Request a list of post categories for a site via the WordPress REST API.
+ */
+ private void fetchCategories() {
+ WordPress.getRestClientUtilsV1_1().getCategories(String.valueOf(mBlog.getRemoteBlogId()),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(AppLog.T.API, "Received response to Categories REST request.");
+ credentialsVerified(true);
+
+ CategoryModel[] models = deserializeJsonRestResponse(response);
+ if (models == null) return;
+
+ SiteSettingsTable.saveCategories(models);
+ mRemoteSettings.categories = models;
+ mSettings.categories = models;
+ notifyUpdatedOnUiThread(null);
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.d(AppLog.T.API, "Error fetching WP.com categories:" + error);
+ }
+ });
+ }
+
+ private CategoryModel deserializeCategoryFromJson(JSONObject category) throws JSONException {
+ if (category == null) return null;
+
+ CategoryModel model = new CategoryModel();
+ model.id = category.getInt(CAT_ID_KEY);
+ model.name = category.getString(CAT_NAME_KEY);
+ model.slug = category.getString(CAT_SLUG_KEY);
+ model.description = category.getString(CAT_DESC_KEY);
+ model.parentId = category.getInt(CAT_PARENT_ID_KEY);
+ model.postCount = category.getInt(CAT_POST_COUNT_KEY);
+
+ return model;
+ }
+
+ private CategoryModel[] deserializeJsonRestResponse(JSONObject response) {
+ try {
+ int num = response.getInt(CAT_NUM_POSTS_KEY);
+ JSONArray categories = response.getJSONArray(CATEGORIES_KEY);
+ CategoryModel[] models = new CategoryModel[num];
+
+ for (int i = 0; i < num; ++i) {
+ JSONObject category = categories.getJSONObject(i);
+ models[i] = deserializeCategoryFromJson(category);
+ }
+
+ AppLog.d(AppLog.T.API, "Successfully fetched WP.com categories");
+
+ return models;
+ } catch (JSONException exception) {
+ AppLog.d(AppLog.T.API, "Error parsing WP.com categories response:" + response);
+ return null;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java
new file mode 100644
index 000000000..1ddb8ac80
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java
@@ -0,0 +1,98 @@
+package org.wordpress.android.ui.prefs;
+
+import android.support.v7.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Patterns;
+import android.view.View;
+import android.widget.Button;
+
+import org.wordpress.android.R;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EditTextPreferenceWithValidation extends SummaryEditTextPreference {
+ private ValidationType mValidationType = ValidationType.NONE;
+
+ public EditTextPreferenceWithValidation(Context context) {
+ super(context);
+ }
+
+ public EditTextPreferenceWithValidation(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public EditTextPreferenceWithValidation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ super.showDialog(state);
+
+ final AlertDialog dialog = (AlertDialog) getDialog();
+ Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ if (positiveButton != null) {
+ positiveButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String error = null;
+ CharSequence text = getEditText().getText();
+ if (mValidationType == ValidationType.EMAIL) {
+ error = validateEmail(text);
+ } else if (!TextUtils.isEmpty(text) && mValidationType == ValidationType.URL) {
+ error = validateUrl(text);
+ }
+
+ if (error != null) {
+ getEditText().setError(error);
+ } else {
+ callChangeListener(text);
+ dialog.dismiss();
+ }
+ }
+ });
+ }
+
+ CharSequence summary = getSummary();
+ if (TextUtils.isEmpty(summary)) {
+ getEditText().setText("");
+ } else {
+ getEditText().setText(summary);
+ getEditText().setSelection(0, summary.length());
+ }
+
+ // clear previous errors
+ getEditText().setError(null);
+ }
+
+ private String validateEmail(CharSequence text) {
+ final Pattern emailRegExPattern = Patterns.EMAIL_ADDRESS;
+ Matcher matcher = emailRegExPattern.matcher(text);
+ if (!matcher.matches()) {
+ return getContext().getString(R.string.invalid_email_message);
+ }
+ return null;
+ }
+
+ private String validateUrl(CharSequence text) {
+ final Pattern urlRegExPattern = Patterns.WEB_URL;
+ Matcher matcher = urlRegExPattern.matcher(text);
+ if (!matcher.matches()) {
+ return getContext().getString(R.string.invalid_url_message);
+ }
+ return null;
+ }
+
+ public void setValidationType(ValidationType validationType) {
+ mValidationType = validationType;
+ }
+
+ public enum ValidationType {
+ NONE, EMAIL, URL
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java
new file mode 100644
index 000000000..f48affb53
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java
@@ -0,0 +1,72 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * RecyclerView with setEmptyView method which displays a view when RecyclerView adapter is empty.
+ */
+public class EmptyViewRecyclerView extends RecyclerView {
+ private View mEmptyView;
+
+ final private AdapterDataObserver observer = new AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ toggleEmptyView();
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ toggleEmptyView();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ toggleEmptyView();
+ }
+ };
+
+ public EmptyViewRecyclerView(Context context) {
+ super(context);
+ }
+
+ public EmptyViewRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EmptyViewRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void setAdapter(Adapter adapterNew) {
+ final RecyclerView.Adapter adapterOld = getAdapter();
+
+ if (adapterOld != null) {
+ adapterOld.unregisterAdapterDataObserver(observer);
+ }
+
+ super.setAdapter(adapterNew);
+
+ if (adapterNew != null) {
+ adapterNew.registerAdapterDataObserver(observer);
+ }
+
+ toggleEmptyView();
+ }
+
+ public void setEmptyView(View emptyView) {
+ mEmptyView = emptyView;
+ toggleEmptyView();
+ }
+
+ private void toggleEmptyView() {
+ if (mEmptyView != null && getAdapter() != null) {
+ final boolean empty = getAdapter().getItemCount() == 0;
+ mEmptyView.setVisibility(empty ? VISIBLE : GONE);
+ this.setVisibility(empty ? GONE : VISIBLE);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java
new file mode 100644
index 000000000..b06932c69
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java
@@ -0,0 +1,175 @@
+package org.wordpress.android.ui.prefs;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.ToastUtils;
+
+public class LearnMorePreference extends Preference
+ implements PreferenceHint, View.OnClickListener {
+ private static final String WP_SUPPORT_URL = "https://en.support.wordpress.com/settings/discussion-settings/#default-article-settings";
+ private static final String SUPPORT_MOBILE_ID = "mobile-only-usage";
+ private static final String SUPPORT_CONTENT_JS = "javascript:(function(){" +
+ "var mobileSupport = document.getElementById('" + SUPPORT_MOBILE_ID + "');" +
+ "mobileSupport.style.display = 'inline';" +
+ "var newHtml = '<' + mobileSupport.tagName + '>' + mobileSupport.innerHTML + '</' + mobileSupport.tagName + '>';" +
+ "document.body.innerHTML = newHtml;" +
+ "document.body.setAttribute('style', 'padding:24px 24px 0px 24px !important');" +
+ "})();";
+
+ private String mHint;
+ private Dialog mDialog;
+
+ public LearnMorePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected View onCreateView(@NonNull ViewGroup parent) {
+ super.onCreateView(parent);
+
+ View view = View.inflate(getContext(), R.layout.learn_more_pref, null);
+ view.findViewById(R.id.learn_more_button).setOnClickListener(this);
+
+ return view;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mDialog != null && mDialog.isShowing()) {
+ mDialog.dismiss();
+ return new SavedState(super.onSaveInstanceState());
+ }
+ return super.onSaveInstanceState();
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {// per documentation, state is always non-null
+ super.onRestoreInstanceState(state);
+ } else {
+ super.onRestoreInstanceState(((SavedState) state).getSuperState());
+ showDialog();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mDialog != null) return;
+
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_LEARN_MORE_CLICKED);
+ showDialog();
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+
+ private void showDialog() {
+ final WebView webView = loadSupportWebView();
+ mDialog = new Dialog(getContext());
+ mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ webView.stopLoading();
+ mDialog = null;
+ }
+ });
+ mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ mDialog.setContentView(R.layout.learn_more_pref_screen);
+ WindowManager.LayoutParams params = mDialog.getWindow().getAttributes();
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
+ params.height = WindowManager.LayoutParams.MATCH_PARENT;
+ params.gravity = Gravity.CENTER;
+ params.x = 12;
+ params.y = 12;
+ mDialog.getWindow().setAttributes(params);
+ mDialog.show();
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private WebView loadSupportWebView() {
+ WebView webView = new WebView(getContext());
+ WebSettings webSettings = webView.getSettings();
+ webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
+ webSettings.setJavaScriptEnabled(true);
+ webView.setWebViewClient(new LearnMoreClient());
+ webView.loadUrl(WP_SUPPORT_URL);
+ return webView;
+ }
+
+ private static class SavedState extends BaseSavedState {
+ public SavedState(Parcel source) { super(source); }
+
+ public SavedState(Parcelable superState) { super(superState); }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) { return new SavedState(in); }
+
+ public SavedState[] newArray(int size) { return new SavedState[size]; }
+ };
+ }
+
+ private class LearnMoreClient extends WebViewClient {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView webView, String url) {
+ return !WP_SUPPORT_URL.equals(url) && !SUPPORT_CONTENT_JS.equals(url);
+ }
+
+ @Override
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+ super.onReceivedError(view, request, error);
+
+ if (mDialog != null && mDialog.isShowing()) {
+ ToastUtils.showToast(getContext(), R.string.could_not_load_page);
+ mDialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onPageFinished(WebView webView, String url) {
+ super.onPageFinished(webView, url);
+ if (mDialog != null) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_LEARN_MORE_LOADED);
+ webView.loadUrl(SUPPORT_CONTENT_JS);
+ mDialog.setContentView(webView);
+ webView.scrollTo(0, 0);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java
new file mode 100644
index 000000000..0b5ce8383
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java
@@ -0,0 +1,22 @@
+package org.wordpress.android.ui.prefs;
+
+import android.os.Bundle;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.WebViewActivity;
+
+/**
+ * Display open source licenses for the application.
+ */
+public class LicensesActivity extends WebViewActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(getResources().getText(R.string.open_source_licenses));
+ }
+
+ @Override
+ protected void loadContent() {
+ loadUrl("file:///android_asset/licenses.html");
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java
new file mode 100644
index 000000000..7d70c6bac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java
@@ -0,0 +1,100 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+import java.util.List;
+
+/**
+ * RecyclerView.Adapter for selecting multiple list items with simple layout (TextView + divider).
+ */
+public class MultiSelectRecyclerViewAdapter extends RecyclerView.Adapter<MultiSelectRecyclerViewAdapter.ItemHolder> {
+ private final List<String> mItems;
+ private final SparseBooleanArray mItemsSelected;
+ private final int mSelectedColor;
+ private final int mUnselectedColor;
+
+ public MultiSelectRecyclerViewAdapter(Context context, List<String> items) {
+ this.mSelectedColor = ContextCompat.getColor(context, R.color.white);
+ this.mUnselectedColor = ContextCompat.getColor(context, R.color.transparent);
+ this.mItems = items;
+ this.mItemsSelected = new SparseBooleanArray();
+ }
+
+ public class ItemHolder extends RecyclerView.ViewHolder {
+ private final LinearLayout container;
+ private final TextView text;
+
+ public ItemHolder(View view) {
+ super(view);
+ this.container = (LinearLayout) view.findViewById(R.id.container);
+ this.text = (TextView) view.findViewById(R.id.text);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public void onBindViewHolder(final ItemHolder holder, int position) {
+ String item = getItem(holder.getAdapterPosition());
+ holder.text.setText(item);
+ holder.container.setBackgroundColor(
+ isItemSelected(position) ?
+ mSelectedColor :
+ mUnselectedColor
+ );
+ }
+
+ @Override
+ public ItemHolder onCreateViewHolder(ViewGroup parent, int type) {
+ return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.wp_simple_list_item_1, parent, false));
+ }
+
+ public String getItem(int position) {
+ return mItems.get(position);
+ }
+
+ public SparseBooleanArray getItemsSelected() {
+ return mItemsSelected;
+ }
+
+ private boolean isItemSelected(int position) {
+ String item = getItem(position);
+ return item != null && mItemsSelected.get(position);
+ }
+
+ public void removeItemsSelected() {
+ mItemsSelected.clear();
+ notifyDataSetChanged();
+ }
+
+ public void setItemSelected(int position) {
+ if (!mItemsSelected.get(position)) {
+ mItemsSelected.put(position, true);
+ }
+
+ notifyItemChanged(position);
+ }
+
+ public void toggleItemSelected(int position) {
+ if (!mItemsSelected.get(position)) {
+ mItemsSelected.put(position, true);
+ } else {
+ mItemsSelected.delete(position);
+ }
+
+ notifyItemChanged(position);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java
new file mode 100644
index 000000000..600ad13f0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java
@@ -0,0 +1,42 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+public class MyProfileActivity extends AppCompatActivity {
+ private static final String KEY_MY_PROFILE_FRAGMENT = "my-profile-fragment";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ MyProfileFragment myProfileFragment =
+ (MyProfileFragment) fragmentManager.findFragmentByTag(KEY_MY_PROFILE_FRAGMENT);
+ if (myProfileFragment == null) {
+ myProfileFragment = MyProfileFragment.newInstance();
+
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, myProfileFragment, KEY_MY_PROFILE_FRAGMENT)
+ .commit();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java
new file mode 100644
index 000000000..5b6272a8a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java
@@ -0,0 +1,175 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.AccountModel;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.WPTextView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class MyProfileFragment extends Fragment implements ProfileInputDialogFragment.Callback {
+ private final String DIALOG_TAG = "DIALOG";
+
+ private WPTextView mFirstName;
+ private WPTextView mLastName;
+ private WPTextView mDisplayName;
+ private WPTextView mAboutMe;
+
+ public static MyProfileFragment newInstance() {
+ return new MyProfileFragment();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refreshDetails();
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ AccountHelper.getDefaultAccount().fetchAccountSettings();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.my_profile_fragment, container, false);
+
+ mFirstName = (WPTextView) rootView.findViewById(R.id.first_name);
+ mLastName = (WPTextView) rootView.findViewById(R.id.last_name);
+ mDisplayName = (WPTextView) rootView.findViewById(R.id.display_name);
+ mAboutMe = (WPTextView) rootView.findViewById(R.id.about_me);
+
+ rootView.findViewById(R.id.first_name_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.first_name),
+ null,
+ mFirstName,
+ false));
+ rootView.findViewById(R.id.last_name_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.last_name),
+ null,
+ mLastName,
+ false));
+ rootView.findViewById(R.id.display_name_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.public_display_name),
+ getString(R.string.public_display_name_hint),
+ mDisplayName,
+ false));
+ rootView.findViewById(R.id.about_me_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.about_me),
+ getString(R.string.about_me_hint),
+ mAboutMe,
+ true));
+
+ return rootView;
+ }
+
+ private void refreshDetails() {
+ if (!isAdded()) return;
+
+ Account account = AccountHelper.getDefaultAccount();
+ updateLabel(mFirstName, account != null ? StringUtils.unescapeHTML(account.getFirstName()) : null);
+ updateLabel(mLastName, account != null ? StringUtils.unescapeHTML(account.getLastName()) : null);
+ updateLabel(mDisplayName, account != null ? StringUtils.unescapeHTML(account.getDisplayName()) : null);
+ updateLabel(mAboutMe, account != null ? StringUtils.unescapeHTML(account.getAboutMe()) : null);
+ }
+
+ private void updateLabel(WPTextView textView, String text) {
+ textView.setText(text);
+ if (TextUtils.isEmpty(text)) {
+ if (textView == mDisplayName) {
+ Account account = AccountHelper.getDefaultAccount();
+ mDisplayName.setText(account.getUserName());
+ } else {
+ textView.setVisibility(View.GONE);
+ }
+ }
+ else {
+ textView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ // helper method to create onClickListener to avoid code duplication
+ private View.OnClickListener createOnClickListener(final String dialogTitle,
+ final String hint,
+ final WPTextView textView,
+ final boolean isMultiline) {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ProfileInputDialogFragment inputDialog = ProfileInputDialogFragment.newInstance(dialogTitle,
+ textView.getText().toString(), hint, isMultiline, textView.getId());
+ inputDialog.setTargetFragment(MyProfileFragment.this, 0);
+ inputDialog.show(getFragmentManager(), DIALOG_TAG);
+ }
+ };
+ }
+
+ @Override
+ public void onSuccessfulInput(String input, int callbackId) {
+ View rootView = getView();
+ if (rootView == null) return;
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ ToastUtils.showToast(getActivity(), R.string.error_post_my_profile_no_connection);
+ return;
+ }
+
+ WPTextView textView = (WPTextView) rootView.findViewById(callbackId);
+ updateLabel(textView, input);
+ updateMyProfileForLabel(textView);
+ }
+
+ private void updateMyProfileForLabel(TextView textView) {
+ Map<String, String> params = new HashMap<>();
+ params.put(restParamForTextView(textView), textView.getText().toString());
+ AccountHelper.getDefaultAccount().postAccountSettings(params);
+ }
+
+ // helper method to get the rest parameter for a text view
+ private String restParamForTextView(TextView textView) {
+ if (textView == mFirstName) {
+ return AccountModel.RestParam.FIRST_NAME.getDescription();
+ } else if (textView == mLastName) {
+ return AccountModel.RestParam.LAST_NAME.getDescription();
+ } else if (textView == mDisplayName) {
+ return AccountModel.RestParam.DISPLAY_NAME.getDescription();
+ } else if (textView == mAboutMe) {
+ return AccountModel.RestParam.ABOUT_ME.getDescription();
+ }
+ return null;
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchSuccess event) {
+ refreshDetails();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java
new file mode 100644
index 000000000..2f72ab6fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java
@@ -0,0 +1,166 @@
+package org.wordpress.android.ui.prefs;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.widget.SwitchCompat;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.NumberPicker;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+public class NumberPickerDialog extends DialogFragment
+ implements DialogInterface.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+
+ public static final String SHOW_SWITCH_KEY = "show-switch";
+ public static final String SWITCH_ENABLED_KEY = "switch-enabled";
+ public static final String SWITCH_TITLE_KEY = "switch-title";
+ public static final String SWITCH_DESC_KEY = "switch-description";
+ public static final String TITLE_KEY = "dialog-title";
+ public static final String HEADER_TEXT_KEY = "header-text";
+ public static final String MIN_VALUE_KEY = "min-value";
+ public static final String MAX_VALUE_KEY = "max-value";
+ public static final String CUR_VALUE_KEY = "cur-value";
+
+ private static final int DEFAULT_MIN_VALUE = 0;
+ private static final int DEFAULT_MAX_VALUE = 99;
+
+ private SwitchCompat mSwitch;
+ private TextView mHeaderText;
+ private NumberPicker mNumberPicker;
+ private NumberPicker.Formatter mFormat;
+ private int mMinValue;
+ private int mMaxValue;
+ private boolean mConfirmed;
+
+ public NumberPickerDialog() {
+ mMinValue = DEFAULT_MIN_VALUE;
+ mMaxValue = DEFAULT_MAX_VALUE;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ View view = View.inflate(getActivity(), R.layout.number_picker_dialog, null);
+ TextView switchText = (TextView) view.findViewById(R.id.number_picker_text);
+ mSwitch = (SwitchCompat) view.findViewById(R.id.number_picker_switch);
+ mHeaderText = (TextView) view.findViewById(R.id.number_picker_header);
+ mNumberPicker = (NumberPicker) view.findViewById(R.id.number_picker);
+ int value = mMinValue;
+
+ Bundle args = getArguments();
+ if (args != null) {
+ if (args.getBoolean(SHOW_SWITCH_KEY, false)) {
+ mSwitch.setVisibility(View.VISIBLE);
+ mSwitch.setText(args.getString(SWITCH_TITLE_KEY, ""));
+ mSwitch.setChecked(args.getBoolean(SWITCH_ENABLED_KEY, false));
+ final View toggleContainer = view.findViewById(R.id.number_picker_toggleable);
+ toggleContainer.setEnabled(mSwitch.isChecked());
+ mNumberPicker.setEnabled(mSwitch.isChecked());
+ } else {
+ mSwitch.setVisibility(View.GONE);
+ }
+ switchText.setText(args.getString(SWITCH_DESC_KEY, ""));
+ mHeaderText.setText(args.getString(HEADER_TEXT_KEY, ""));
+ mMinValue = args.getInt(MIN_VALUE_KEY, DEFAULT_MIN_VALUE);
+ mMaxValue = args.getInt(MAX_VALUE_KEY, DEFAULT_MAX_VALUE);
+ value = args.getInt(CUR_VALUE_KEY, mMinValue);
+
+ builder.setCustomTitle(getDialogTitleView(args.getString(TITLE_KEY, "")));
+ }
+
+ mNumberPicker.setFormatter(mFormat);
+ mNumberPicker.setMinValue(mMinValue);
+ mNumberPicker.setMaxValue(mMaxValue);
+ mNumberPicker.setValue(value);
+
+ mSwitch.setOnCheckedChangeListener(this);
+
+ // hide empty text views
+ if (TextUtils.isEmpty(switchText.getText())) {
+ switchText.setVisibility(View.GONE);
+ }
+ if (TextUtils.isEmpty(mHeaderText.getText())) {
+ mHeaderText.setVisibility(View.GONE);
+ }
+
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(R.string.cancel, this);
+ builder.setView(view);
+
+ return builder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ AlertDialog dialog = (AlertDialog) getDialog();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mConfirmed = which == DialogInterface.BUTTON_POSITIVE;
+ dismiss();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mNumberPicker.setEnabled(isChecked);
+ mHeaderText.setEnabled(isChecked);
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ Fragment target = getTargetFragment();
+ if (target != null) {
+ target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent());
+ }
+
+ super.onDismiss(dialog);
+ }
+
+ public void setNumberFormat(NumberPicker.Formatter format) {
+ mFormat = format;
+ }
+
+ private View getDialogTitleView(String title) {
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ @SuppressLint("InflateParams")
+ View titleView = inflater.inflate(R.layout.detail_list_preference_title, null);
+ TextView titleText = ((TextView) titleView.findViewById(R.id.title));
+ titleText.setText(title);
+ titleText.setLayoutParams(new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT));
+ return titleView;
+ }
+
+ private Intent getResultIntent() {
+ if (mConfirmed) {
+ return new Intent()
+ .putExtra(SWITCH_ENABLED_KEY, mSwitch.isChecked())
+ .putExtra(CUR_VALUE_KEY, mNumberPicker.getValue());
+ }
+
+ return null;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java
new file mode 100644
index 000000000..5047ad17e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.ui.prefs;
+
+public interface PreferenceHint {
+ boolean hasHint();
+ String getHint();
+ void setHint(String hint);
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java
new file mode 100644
index 000000000..a39849324
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java
@@ -0,0 +1,20 @@
+package org.wordpress.android.ui.prefs;
+
+import com.android.volley.VolleyError;
+
+public class PrefsEvents {
+ public static class AccountSettingsFetchSuccess {}
+ public static class AccountSettingsPostSuccess {}
+ public static class AccountSettingsFetchError {
+ public final VolleyError mVolleyError;
+ public AccountSettingsFetchError(VolleyError error) {
+ mVolleyError = error;
+ }
+ }
+ public static class AccountSettingsPostError {
+ public final VolleyError mVolleyError;
+ public AccountSettingsPostError(VolleyError error) {
+ mVolleyError = error;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java
new file mode 100644
index 000000000..dca7db6bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java
@@ -0,0 +1,111 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.WPPrefUtils;
+import org.wordpress.android.widgets.WPEditText;
+import org.wordpress.android.widgets.WPTextView;
+
+public class ProfileInputDialogFragment extends DialogFragment {
+ private static final String TITLE_TAG = "title";
+ private static final String INITIAL_TEXT_TAG = "initial_text";
+ private static final String HINT_TAG = "hint";
+ private static final String IS_MULTILINE_TAG = "is_multiline";
+ private static final String CALLBACK_ID_TAG = "callback_id";
+
+ public static ProfileInputDialogFragment newInstance(String title,
+ String initialText,
+ String hint,
+ boolean isMultiline,
+ int callbackId) {
+
+ ProfileInputDialogFragment profileInputDialogFragment = new ProfileInputDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putString(TITLE_TAG, title);
+ args.putString(INITIAL_TEXT_TAG, initialText);
+ args.putString(HINT_TAG, hint);
+ args.putBoolean(IS_MULTILINE_TAG, isMultiline);
+ args.putInt(CALLBACK_ID_TAG, callbackId);
+
+ profileInputDialogFragment.setArguments(args);
+ return profileInputDialogFragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
+ View promptView = layoutInflater.inflate(R.layout.my_profile_dialog, null);
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
+ alertDialogBuilder.setView(promptView);
+
+ final WPTextView textView = (WPTextView) promptView.findViewById(R.id.my_profile_dialog_label);
+ final WPEditText editText = (WPEditText) promptView.findViewById(R.id.my_profile_dialog_input);
+ final WPTextView hintView = (WPTextView) promptView.findViewById(R.id.my_profile_dialog_hint);
+
+ Bundle args = getArguments();
+ String title = args.getString(TITLE_TAG);
+ String hint = args.getString(HINT_TAG);
+ Boolean isMultiline = args.getBoolean(IS_MULTILINE_TAG);
+ String initialText = args.getString(INITIAL_TEXT_TAG);
+ final int callbackId = args.getInt(CALLBACK_ID_TAG);
+
+ textView.setText(title);
+ if (!TextUtils.isEmpty(hint)) {
+ hintView.setText(hint);
+ } else {
+ hintView.setVisibility(View.GONE);
+ }
+
+ if (!isMultiline) {
+ editText.setMaxLines(1);
+ }
+ if (!TextUtils.isEmpty(initialText)) {
+ editText.setText(initialText);
+ editText.setSelection(0, initialText.length());
+ }
+
+ alertDialogBuilder.setCancelable(true)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ if (getTargetFragment() instanceof Callback) {
+ ((Callback) getTargetFragment()).onSuccessfulInput(editText.getText().toString(), callbackId);
+ } else {
+ AppLog.e(AppLog.T.UTILS, "Target fragment doesn't implement ProfileInputDialogFragment.Callback");
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+
+ return alertDialogBuilder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ AlertDialog dialog = (AlertDialog) getDialog();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ public interface Callback {
+ void onSuccessfulInput(String input, int callbackId);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java
new file mode 100644
index 000000000..8c6752cfa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class RecyclerViewItemClickListener implements RecyclerView.OnItemTouchListener {
+ private final GestureDetector mGestureDetector;
+ private final OnItemClickListener mListener;
+
+ public interface OnItemClickListener {
+ public void onItemClick(View view, int position);
+ public void onLongItemClick(View view, int position);
+ }
+
+ public RecyclerViewItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) {
+ mListener = listener;
+
+ mGestureDetector = new GestureDetector(
+ context,
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent motionEvent) {
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
+
+ if (child != null && mListener != null) {
+ mListener.onLongItemClick(child, recyclerView.getChildAdapterPosition(child));
+ }
+ }
+ }
+ );
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent motionEvent) {
+ View childView = view.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
+
+ if (childView != null && mListener != null && mGestureDetector.onTouchEvent(motionEvent)) {
+ mListener.onItemClick(childView, view.getChildAdapterPosition(childView));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent (boolean disallowIntercept){
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) {
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java
new file mode 100644
index 000000000..446545bde
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java
@@ -0,0 +1,184 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+import org.wordpress.android.widgets.WPSwitch;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RelatedPostsDialog extends DialogFragment
+ implements DialogInterface.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+
+ /**
+ * boolean
+ *
+ * Sets the default state of the Show Related Posts switch. The switch is off by default.
+ */
+ public static final String SHOW_RELATED_POSTS_KEY = "related-posts";
+
+ /**
+ * boolean
+ *
+ * Sets the default state of the Show Headers checkbox. The checkbox is off by default.
+ */
+ public static final String SHOW_HEADER_KEY = "show-header";
+
+ /**
+ * boolean
+ *
+ * Sets the default state of the Show Images checkbox. The checkbox is off by default.
+ */
+ public static final String SHOW_IMAGES_KEY = "show-images";
+
+ private WPSwitch mShowRelatedPosts;
+ private CheckBox mShowHeader;
+ private CheckBox mShowImages;
+ private TextView mPreviewHeader;
+ private TextView mRelatedPostsListHeader;
+ private LinearLayout mRelatedPostsList;
+ private List<ImageView> mPreviewImages;
+ private boolean mConfirmed;
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View v = inflater.inflate(R.layout.related_posts_dialog, null, false);
+
+ mShowRelatedPosts = (WPSwitch) v.findViewById(R.id.toggle_related_posts_switch);
+ mShowHeader = (CheckBox) v.findViewById(R.id.show_header_checkbox);
+ mShowImages = (CheckBox) v.findViewById(R.id.show_images_checkbox);
+ mPreviewHeader = (TextView) v.findViewById(R.id.preview_header);
+ mRelatedPostsListHeader = (TextView) v.findViewById(R.id.related_posts_list_header);
+ mRelatedPostsList = (LinearLayout) v.findViewById(R.id.related_posts_list);
+
+ mPreviewImages = new ArrayList<>();
+ mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image1));
+ mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image2));
+ mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image3));
+
+ Bundle args = getArguments();
+ if (args != null) {
+ mShowRelatedPosts.setChecked(args.getBoolean(SHOW_RELATED_POSTS_KEY));
+ mShowHeader.setChecked(args.getBoolean(SHOW_HEADER_KEY));
+ mShowImages.setChecked(args.getBoolean(SHOW_IMAGES_KEY));
+ }
+
+ toggleShowHeader(mShowHeader.isChecked());
+ toggleShowImages(mShowImages.isChecked());
+
+ mShowRelatedPosts.setOnCheckedChangeListener(this);
+ mShowHeader.setOnCheckedChangeListener(this);
+ mShowImages.setOnCheckedChangeListener(this);
+
+ toggleViews(mShowRelatedPosts.isChecked());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ View titleView = inflater.inflate(R.layout.detail_list_preference_title, null);
+ TextView titleText = ((TextView) titleView.findViewById(R.id.title));
+ titleText.setText(R.string.site_settings_related_posts_title);
+ titleText.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
+ builder.setCustomTitle(titleView);
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(R.string.cancel, this);
+ builder.setView(v);
+
+ return builder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ AlertDialog dialog = (AlertDialog) getDialog();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mConfirmed = which == DialogInterface.BUTTON_POSITIVE;
+ dismiss();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (buttonView == mShowRelatedPosts) {
+ toggleViews(isChecked);
+ } else if (buttonView == mShowHeader) {
+ toggleShowHeader(isChecked);
+ } else if (buttonView == mShowImages) {
+ toggleShowImages(isChecked);
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ Fragment target = getTargetFragment();
+ if (target != null) {
+ target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent());
+ }
+
+ super.onDismiss(dialog);
+ }
+
+ private void toggleShowHeader(boolean show) {
+ if (show) {
+ mRelatedPostsListHeader.setVisibility(View.VISIBLE);
+ } else {
+ mRelatedPostsListHeader.setVisibility(View.GONE);
+ }
+ }
+
+ private void toggleShowImages(boolean show) {
+ int visibility = show ? View.VISIBLE : View.GONE;
+ for (ImageView view : mPreviewImages) {
+ view.setVisibility(visibility);
+ }
+ }
+
+ private Intent getResultIntent() {
+ if (mConfirmed) {
+ return new Intent()
+ .putExtra(SHOW_RELATED_POSTS_KEY, mShowRelatedPosts.isChecked())
+ .putExtra(SHOW_HEADER_KEY, mShowHeader.isChecked())
+ .putExtra(SHOW_IMAGES_KEY, mShowImages.isChecked());
+ }
+
+ return null;
+ }
+
+ private void toggleViews(boolean enabled) {
+ mShowHeader.setEnabled(enabled);
+ mShowImages.setEnabled(enabled);
+ mPreviewHeader.setEnabled(enabled);
+ mRelatedPostsListHeader.setEnabled(enabled);
+
+ if (enabled) {
+ mRelatedPostsList.setAlpha(1.0f);
+ } else {
+ mRelatedPostsList.setAlpha(0.5f);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java
new file mode 100644
index 000000000..3389b9e8e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java
@@ -0,0 +1,421 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.text.TextUtils;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.models.SiteSettingsModel;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.MapUtils;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+class SelfHostedSiteSettings extends SiteSettingsInterface {
+ // XML-RPC wp.getOptions keys
+ public static final String PRIVACY_KEY = "blog_public";
+ public static final String DEF_CATEGORY_KEY = "default_category";
+ public static final String DEF_POST_FORMAT_KEY = "default_post_format";
+ public static final String ALLOW_COMMENTS_KEY = "default_comment_status";
+ public static final String SEND_PINGBACKS_KEY = "default_pingback_flag";
+ public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status";
+ public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts";
+ public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old";
+ public static final String THREAD_COMMENTS_KEY = "thread_comments";
+ public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth";
+ public static final String PAGE_COMMENTS_KEY = "page_comments";
+ public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page";
+ public static final String COMMENT_SORT_ORDER_KEY = "comment_order";
+ public static final String COMMENT_MODERATION_KEY = "comment_moderation";
+ public static final String REQUIRE_IDENTITY_KEY = "require_name_email";
+ public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration";
+ public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist";
+ public static final String MAX_LINKS_KEY = "comment_max_links";
+ public static final String MODERATION_KEYS_KEY = "moderation_keys";
+ public static final String BLACKLIST_KEYS_KEY = "blacklist_keys";
+ public static final String SOFTWARE_VERSION_KEY = "software_version";
+
+ private static final String BLOG_URL_KEY = "blog_url";
+ private static final String BLOG_TITLE_KEY = "blog_title";
+ private static final String BLOG_USERNAME_KEY = "username";
+ private static final String BLOG_PASSWORD_KEY = "password";
+ private static final String BLOG_TAGLINE_KEY = "blog_tagline";
+ private static final String BLOG_CATEGORY_ID_KEY = "categoryId";
+ private static final String BLOG_CATEGORY_PARENT_ID_KEY = "parentId";
+ private static final String BLOG_CATEGORY_DESCRIPTION_KEY = "categoryDescription";
+ private static final String BLOG_CATEGORY_NAME_KEY = "categoryName";
+
+ // Requires WordPress 4.5.x or higher
+ private static final int REQUIRED_MAJOR_VERSION = 4;
+ private static final int REQUIRED_MINOR_VERSION = 3;
+
+ private static final String OPTION_ALLOWED = "open";
+ private static final String OPTION_DISALLOWED = "closed";
+
+ SelfHostedSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) {
+ super(host, blog, listener);
+ }
+
+ @Override
+ public SiteSettingsInterface init(boolean fetch) {
+ super.init(fetch);
+
+ if (mSettings.defaultCategory == 0) {
+ mSettings.defaultCategory = siteSettingsPreferences(mActivity).getInt(DEF_CATEGORY_PREF_KEY, 0);
+ }
+ if (TextUtils.isEmpty(mSettings.defaultPostFormat) || mSettings.defaultPostFormat.equals("0")) {
+ mSettings.defaultPostFormat = siteSettingsPreferences(mActivity).getString(DEF_FORMAT_PREF_KEY, "0");
+ }
+ mSettings.language = siteSettingsPreferences(mActivity).getString(LANGUAGE_PREF_KEY, LanguageUtils.getPatchedCurrentDeviceLanguage(null));
+
+ return this;
+ }
+
+ @Override
+ public void saveSettings() {
+ super.saveSettings();
+
+ final Map<String, String> params = serializeSelfHostedParams();
+ if (params == null || params.isEmpty()) return;
+
+ XMLRPCCallback callback = new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, final Object result) {
+ notifySavedOnUiThread(null);
+ mRemoteSettings.copyFrom(mSettings);
+
+ if (result != null) {
+ HashMap<String, Object> properties = new HashMap<>();
+ if (result instanceof Map) {
+ Map<String, Object> resultMap = (Map) result;
+ Set<String> keys = resultMap.keySet();
+ for (String key : keys) {
+ Object currentValue = resultMap.get(key);
+ if (currentValue != null) {
+ properties.put(SAVED_ITEM_PREFIX + key, currentValue);
+ }
+ }
+ }
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties);
+ }
+ }
+
+ @Override
+ public void onFailure(long id, final Exception error) {
+ notifySavedOnUiThread(error);
+ }
+ };
+ final Object[] callParams = {
+ mBlog.getRemoteBlogId(), mSettings.username, mSettings.password, params
+ };
+
+ XMLRPCClientInterface xmlrpcInterface = instantiateInterface();
+ if (xmlrpcInterface == null) return;
+ xmlrpcInterface.callAsync(callback, Method.SET_OPTIONS, callParams);
+ }
+
+ /**
+ * Request remote site data via XML-RPC.
+ */
+ @Override
+ protected void fetchRemoteData() {
+ new Thread() {
+ @Override
+ public void run() {
+ Object[] params = {mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword()};
+
+ // Need two interfaces or the first call gets aborted
+ instantiateInterface().callAsync(mOptionsCallback, Method.GET_OPTIONS, params);
+ instantiateInterface().callAsync(mCategoriesCallback, Method.GET_CATEGORIES, params);
+ }
+ }.start();
+ }
+
+ /**
+ * Handles response to fetching self-hosted site categories via XML-RPC.
+ */
+ private final XMLRPCCallback mCategoriesCallback = new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, Object result) {
+ if (result instanceof Object[]) {
+ AppLog.d(AppLog.T.API, "Received Categories XML-RPC response.");
+ credentialsVerified(true);
+
+ mRemoteSettings.localTableId = mBlog.getRemoteBlogId();
+ deserializeCategoriesResponse(mRemoteSettings, (Object[]) result);
+ mSettings.categories = mRemoteSettings.categories;
+ SiteSettingsTable.saveCategories(mSettings.categories);
+ notifyUpdatedOnUiThread(null);
+ } else {
+ // Response is considered an error if we are unable to parse it
+ AppLog.w(AppLog.T.API, "Error parsing Categories XML-RPC response: " + result);
+ notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object"));
+ }
+ }
+
+ @Override
+ public void onFailure(long id, Exception error) {
+ AppLog.w(AppLog.T.API, "Error Categories XML-RPC response: " + error);
+ notifyUpdatedOnUiThread(error);
+ }
+ };
+
+ /**
+ * Handles response to fetching self-hosted site options via XML-RPC.
+ */
+ private final XMLRPCCallback mOptionsCallback = new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, final Object result) {
+ if (result instanceof Map) {
+ AppLog.d(AppLog.T.API, "Received Options XML-RPC response.");
+
+ if (!versionSupported((Map) result) && mActivity != null) {
+ notifyUpdatedOnUiThread(new XMLRPCException(mActivity.getString(R.string.site_settings_unsupported_version_error)));
+ return;
+ }
+
+ credentialsVerified(true);
+
+ deserializeOptionsResponse(mRemoteSettings, (Map) result);
+
+ // postFormats setting is not returned by this api call so copy it over
+ final Map<String, String> currentPostFormats = mSettings.postFormats;
+
+ mSettings.copyFrom(mRemoteSettings);
+
+ mSettings.postFormats = currentPostFormats;
+
+ SiteSettingsTable.saveSettings(mSettings);
+ notifyUpdatedOnUiThread(null);
+ } else {
+ // Response is considered an error if we are unable to parse it
+ AppLog.w(AppLog.T.API, "Error parsing Options XML-RPC response: " + result);
+ notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object"));
+ }
+ }
+
+ @Override
+ public void onFailure(long id, final Exception error) {
+ AppLog.w(AppLog.T.API, "Error Options XML-RPC response: " + error);
+ notifyUpdatedOnUiThread(error);
+ }
+ };
+
+ private boolean versionSupported(Map map) {
+ String version = getNestedMapValue(map, SOFTWARE_VERSION_KEY);
+ if (TextUtils.isEmpty(version)) return false;
+ String[] split = version.split("\\.");
+ return split.length > 0 &&
+ Integer.valueOf(split[0]) >= REQUIRED_MAJOR_VERSION &&
+ Integer.valueOf(split[1]) >= REQUIRED_MINOR_VERSION;
+ }
+
+ private Map<String, String> serializeSelfHostedParams() {
+ Map<String, String> params = new HashMap<>();
+
+ if (mSettings.title != null && !mSettings.title.equals(mRemoteSettings.title)) {
+ params.put(BLOG_TITLE_KEY, mSettings.title);
+ }
+ if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) {
+ params.put(BLOG_TAGLINE_KEY, mSettings.tagline);
+ }
+ if (mSettings.privacy != mRemoteSettings.privacy) {
+ params.put(PRIVACY_KEY, String.valueOf(mSettings.privacy));
+ }
+ if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) {
+ params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory));
+ }
+ if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) {
+ params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat);
+ }
+ if (mSettings.allowComments != mRemoteSettings.allowComments) {
+ params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments));
+ }
+ if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) {
+ params.put(SEND_PINGBACKS_KEY, mSettings.sendPingbacks ? "1" : "0");
+ }
+ if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) {
+ params.put(RECEIVE_PINGBACKS_KEY, mSettings.receivePingbacks ? OPTION_ALLOWED : OPTION_DISALLOWED);
+ }
+ if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) {
+ params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired));
+ }
+ if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter) {
+ if (mSettings.closeCommentAfter <= 0) {
+ params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(0));
+ } else {
+ params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(1));
+ params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter));
+ }
+ }
+ if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) {
+ if (mSettings.sortCommentsBy == ASCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "asc");
+ } else if (mSettings.sortCommentsBy == DESCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "desc");
+ }
+ }
+ if (mSettings.threadingLevels != mRemoteSettings.threadingLevels) {
+ if (mSettings.threadingLevels <= 1) {
+ params.put(THREAD_COMMENTS_KEY, String.valueOf(0));
+ } else {
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(1));
+ params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels));
+ }
+ }
+ if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage) {
+ if (mSettings.commentsPerPage <= 0) {
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(0));
+ } else{
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(1));
+ params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage));
+ }
+ }
+ if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) {
+ params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity ? 1 : 0));
+ }
+ if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) {
+ params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount ? 1 : 0));
+ }
+ if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) {
+ params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers));
+ }
+ if (mSettings.maxLinks != mRemoteSettings.maxLinks) {
+ params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks));
+ }
+ if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.holdForModeration) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(MODERATION_KEYS_KEY, "");
+ }
+ }
+ if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.blacklist) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(BLACKLIST_KEYS_KEY, "");
+ }
+ }
+
+ return params;
+ }
+
+ /**
+ * Sets values from a self-hosted XML-RPC response object.
+ */
+ private void deserializeOptionsResponse(SiteSettingsModel model, Map response) {
+ if (mBlog == null || response == null) return;
+
+ model.username = mBlog.getUsername();
+ model.password = mBlog.getPassword();
+ model.address = getNestedMapValue(response, BLOG_URL_KEY);
+ model.title = getNestedMapValue(response, BLOG_TITLE_KEY);
+ model.tagline = getNestedMapValue(response, BLOG_TAGLINE_KEY);
+ model.privacy = Integer.valueOf(getNestedMapValue(response, PRIVACY_KEY));
+ model.defaultCategory = Integer.valueOf(getNestedMapValue(response, DEF_CATEGORY_KEY));
+ model.defaultPostFormat = getNestedMapValue(response, DEF_POST_FORMAT_KEY);
+ model.allowComments = OPTION_ALLOWED.equals(getNestedMapValue(response, ALLOW_COMMENTS_KEY));
+ model.receivePingbacks = OPTION_ALLOWED.equals(getNestedMapValue(response, RECEIVE_PINGBACKS_KEY));
+ String sendPingbacks = getNestedMapValue(response, SEND_PINGBACKS_KEY);
+ String approvalRequired = getNestedMapValue(response, COMMENT_MODERATION_KEY);
+ String identityRequired = getNestedMapValue(response, REQUIRE_IDENTITY_KEY);
+ String accountRequired = getNestedMapValue(response, REQUIRE_USER_ACCOUNT_KEY);
+ String knownUsers = getNestedMapValue(response, WHITELIST_KNOWN_USERS_KEY);
+ model.sendPingbacks = !TextUtils.isEmpty(sendPingbacks) && Integer.valueOf(sendPingbacks) > 0;
+ model.commentApprovalRequired = !TextUtils.isEmpty(approvalRequired) && Boolean.valueOf(approvalRequired);
+ model.commentsRequireIdentity = !TextUtils.isEmpty(identityRequired) && Integer.valueOf(identityRequired) > 0;
+ model.commentsRequireUserAccount = !TextUtils.isEmpty(accountRequired) && Integer.valueOf(identityRequired) > 0;
+ model.commentAutoApprovalKnownUsers = !TextUtils.isEmpty(knownUsers) && Boolean.valueOf(knownUsers);
+ model.maxLinks = Integer.valueOf(getNestedMapValue(response, MAX_LINKS_KEY));
+ mRemoteSettings.holdForModeration = new ArrayList<>();
+ mRemoteSettings.blacklist = new ArrayList<>();
+
+ String modKeys = getNestedMapValue(response, MODERATION_KEYS_KEY);
+ if (modKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n"));
+ }
+ String blacklistKeys = getNestedMapValue(response, BLACKLIST_KEYS_KEY);
+ if (blacklistKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n"));
+ }
+
+ String close = getNestedMapValue(response, CLOSE_OLD_COMMENTS_KEY);
+ if (!TextUtils.isEmpty(close) && Boolean.valueOf(close)) {
+ mRemoteSettings.closeCommentAfter = Integer.valueOf(getNestedMapValue(response, CLOSE_OLD_COMMENTS_DAYS_KEY));
+ } else {
+ mRemoteSettings.closeCommentAfter = 0;
+ }
+
+ String thread = getNestedMapValue(response, THREAD_COMMENTS_KEY);
+ if (!TextUtils.isEmpty(thread) && Integer.valueOf(thread) > 0) {
+ mRemoteSettings.threadingLevels = Integer.valueOf(getNestedMapValue(response, THREAD_COMMENTS_DEPTH_KEY));
+ } else {
+ mRemoteSettings.threadingLevels = 0;
+ }
+
+ String page = getNestedMapValue(response, PAGE_COMMENTS_KEY);
+ if (!TextUtils.isEmpty(page) && Boolean.valueOf(page)) {
+ mRemoteSettings.commentsPerPage = Integer.valueOf(getNestedMapValue(response, PAGE_COMMENT_COUNT_KEY));
+ } else {
+ mRemoteSettings.commentsPerPage = 0;
+ }
+
+ if (getNestedMapValue(response, COMMENT_SORT_ORDER_KEY).equals("asc")) {
+ mRemoteSettings.sortCommentsBy = ASCENDING_SORT;
+ } else {
+ mRemoteSettings.sortCommentsBy = DESCENDING_SORT;
+ }
+ }
+
+ private void deserializeCategoriesResponse(SiteSettingsModel model, Object[] response) {
+ model.categories = new CategoryModel[response.length];
+
+ for (int i = 0; i < response.length; ++i) {
+ if (response[i] instanceof Map) {
+ Map category = (Map) response[i];
+ CategoryModel categoryModel = new CategoryModel();
+ categoryModel.id = MapUtils.getMapInt(category, BLOG_CATEGORY_ID_KEY);
+ categoryModel.parentId = MapUtils.getMapInt(category, BLOG_CATEGORY_PARENT_ID_KEY);
+ categoryModel.description = MapUtils.getMapStr(category, BLOG_CATEGORY_DESCRIPTION_KEY);
+ categoryModel.name = MapUtils.getMapStr(category, BLOG_CATEGORY_NAME_KEY);
+ model.categories[i] = categoryModel;
+ }
+ }
+ }
+
+ /**
+ * Helper method to get a value from a nested Map. Used to parse self-hosted response objects.
+ */
+ private String getNestedMapValue(Map map, String key) {
+ if (map != null && key != null) {
+ return MapUtils.getMapStr((Map) map.get(key), "value");
+ }
+
+ return "";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
new file mode 100644
index 000000000..13fc10c5b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
@@ -0,0 +1,1392 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
+import android.view.ContextThemeWrapper;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.NumberPicker.Formatter;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.android.util.WPPrefUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Allows interfacing with WordPress site settings. Works with WP.com and WP.org v4.5+ (pending).
+ *
+ * Settings are synced automatically when local changes are made.
+ */
+
+public class SiteSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener,
+ Preference.OnPreferenceClickListener,
+ AdapterView.OnItemLongClickListener,
+ ViewGroup.OnHierarchyChangeListener,
+ Dialog.OnDismissListener,
+ SiteSettingsInterface.SiteSettingsListener {
+
+ /**
+ * Use this argument to pass the {@link Integer} local blog ID to this fragment.
+ */
+ public static final String ARG_LOCAL_BLOG_ID = "local_blog_id";
+
+ /**
+ * When the user removes a site (by selecting Delete Site) the parent {@link Activity} result
+ * is set to this value and {@link Activity#finish()} is invoked.
+ */
+ public static final int RESULT_BLOG_REMOVED = Activity.RESULT_FIRST_USER;
+
+ /**
+ * Provides the regex to identify domain HTTP(S) protocol and/or 'www' sub-domain.
+ *
+ * Used to format user-facing {@link String}'s in certain preferences.
+ */
+ public static final String ADDRESS_FORMAT_REGEX = "^(https?://(w{3})?|www\\.)";
+
+ /**
+ * url that points to wordpress.com purchases
+ */
+ public static final String WORDPRESS_PURCHASES_URL = "https://wordpress.com/purchases";
+
+ /**
+ * Used to move the Uncategorized category to the beginning of the category list.
+ */
+ private static final int UNCATEGORIZED_CATEGORY_ID = 1;
+
+ /**
+ * Request code used when creating the {@link RelatedPostsDialog}.
+ */
+ private static final int RELATED_POSTS_REQUEST_CODE = 1;
+ private static final int THREADING_REQUEST_CODE = 2;
+ private static final int PAGING_REQUEST_CODE = 3;
+ private static final int CLOSE_AFTER_REQUEST_CODE = 4;
+ private static final int MULTIPLE_LINKS_REQUEST_CODE = 5;
+ private static final int DELETE_SITE_REQUEST_CODE = 6;
+ private static final String DELETE_SITE_TAG = "delete-site";
+ private static final String PURCHASE_ORIGINAL_RESPONSE_KEY = "originalResponse";
+ private static final String PURCHASE_ACTIVE_KEY = "active";
+ private static final String ANALYTICS_ERROR_PROPERTY_KEY = "error";
+
+ private static final long FETCH_DELAY = 1000;
+
+ // Reference to blog obtained from passed ID (ARG_LOCAL_BLOG_ID)
+ private Blog mBlog;
+
+ // Can interface with WP.com or WP.org
+ private SiteSettingsInterface mSiteSettings;
+
+ // Reference to the list of items being edited in the current list editor
+ private List<String> mEditingList;
+
+ // Used to ensure that settings are only fetched once throughout the lifecycle of the fragment
+ private boolean mShouldFetch;
+
+ // General settings
+ private EditTextPreference mTitlePref;
+ private EditTextPreference mTaglinePref;
+ private EditTextPreference mAddressPref;
+ private DetailListPreference mPrivacyPref;
+ private DetailListPreference mLanguagePref;
+
+ // Account settings (NOTE: only for WP.org)
+ private EditTextPreference mUsernamePref;
+ private EditTextPreference mPasswordPref;
+
+ // Writing settings
+ private WPSwitchPreference mLocationPref;
+ private DetailListPreference mCategoryPref;
+ private DetailListPreference mFormatPref;
+ private Preference mRelatedPostsPref;
+
+ // Discussion settings preview
+ private WPSwitchPreference mAllowCommentsPref;
+ private WPSwitchPreference mSendPingbacksPref;
+ private WPSwitchPreference mReceivePingbacksPref;
+
+ // Discussion settings -> Defaults for New Posts
+ private WPSwitchPreference mAllowCommentsNested;
+ private WPSwitchPreference mSendPingbacksNested;
+ private WPSwitchPreference mReceivePingbacksNested;
+ private PreferenceScreen mMorePreference;
+
+ // Discussion settings -> Comments
+ private WPSwitchPreference mIdentityRequiredPreference;
+ private WPSwitchPreference mUserAccountRequiredPref;
+ private Preference mCloseAfterPref;
+ private DetailListPreference mSortByPref;
+ private Preference mThreadingPref;
+ private Preference mPagingPref;
+ private DetailListPreference mWhitelistPref;
+ private Preference mMultipleLinksPref;
+ private Preference mModerationHoldPref;
+ private Preference mBlacklistPref;
+
+ // This Device settings
+ private DetailListPreference mImageWidthPref;
+ private WPSwitchPreference mUploadAndLinkPref;
+
+ // Advanced settings
+ private Preference mStartOverPref;
+ private Preference mExportSitePref;
+ private Preference mDeleteSitePref;
+
+ private boolean mEditingEnabled = true;
+
+ // Reference to the state of the fragment
+ private boolean mIsFragmentPaused = false;
+
+ // Hold for Moderation and Blacklist settings
+ private Dialog mDialog;
+ private ActionMode mActionMode;
+ private MultiSelectRecyclerViewAdapter mAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Activity activity = getActivity();
+
+ // make sure we have local site data and a network connection, otherwise finish activity
+ mBlog = WordPress.getBlog(getArguments().getInt(ARG_LOCAL_BLOG_ID, -1));
+ if (mBlog == null || !NetworkUtils.checkConnection(activity)) {
+ getActivity().finish();
+ return;
+ }
+
+ // track successful settings screen access
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_ACCESSED);
+
+ // setup state to fetch remote settings
+ mShouldFetch = true;
+
+ // initialize the appropriate settings interface (WP.com or WP.org)
+ mSiteSettings = SiteSettingsInterface.getInterface(activity, mBlog, this);
+
+ setRetainInstance(true);
+ addPreferencesFromResource(R.xml.site_settings);
+
+ // toggle which preferences are shown and set references
+ initPreferences();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ WordPress.wpDB.saveBlog(mBlog);
+ mIsFragmentPaused = true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Fragment#onResume() is called after FragmentActivity#onPostResume().
+ // The latter is the most secure way of keeping track of the activity's state, and avoid calls to commitAllowingStateLoss.
+ mIsFragmentPaused = false;
+
+ // always load cached settings
+ mSiteSettings.init(false);
+
+ if (mShouldFetch) {
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // initialize settings with locally cached values, fetch remote on first pass
+ mSiteSettings.init(true);
+ }
+ }, FETCH_DELAY);
+ // stop future calls from fetching remote settings
+ mShouldFetch = false;
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ removeMoreScreenToolbar();
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (data != null) {
+ switch (requestCode) {
+ case RELATED_POSTS_REQUEST_CODE:
+ // data is null if user cancelled editing Related Posts settings
+ mSiteSettings.setShowRelatedPosts(data.getBooleanExtra(
+ RelatedPostsDialog.SHOW_RELATED_POSTS_KEY, false));
+ mSiteSettings.setShowRelatedPostHeader(data.getBooleanExtra(
+ RelatedPostsDialog.SHOW_HEADER_KEY, false));
+ mSiteSettings.setShowRelatedPostImages(data.getBooleanExtra(
+ RelatedPostsDialog.SHOW_IMAGES_KEY, false));
+ onPreferenceChange(mRelatedPostsPref, mSiteSettings.getRelatedPostsDescription());
+ break;
+ case THREADING_REQUEST_CODE:
+ int levels = data.getIntExtra(NumberPickerDialog.CUR_VALUE_KEY, -1);
+ mSiteSettings.setShouldThreadComments(levels > 1 && data.getBooleanExtra
+ (NumberPickerDialog.SWITCH_ENABLED_KEY, false));
+ onPreferenceChange(mThreadingPref, levels);
+ break;
+ case PAGING_REQUEST_CODE:
+ mSiteSettings.setShouldPageComments(data.getBooleanExtra
+ (NumberPickerDialog.SWITCH_ENABLED_KEY, false));
+ onPreferenceChange(mPagingPref, data.getIntExtra(
+ NumberPickerDialog.CUR_VALUE_KEY, -1));
+ break;
+ case CLOSE_AFTER_REQUEST_CODE:
+ mSiteSettings.setShouldCloseAfter(data.getBooleanExtra
+ (NumberPickerDialog.SWITCH_ENABLED_KEY, false));
+ onPreferenceChange(mCloseAfterPref, data.getIntExtra(
+ NumberPickerDialog.CUR_VALUE_KEY, -1));
+ break;
+ case MULTIPLE_LINKS_REQUEST_CODE:
+ int numLinks = data.getIntExtra(NumberPickerDialog.CUR_VALUE_KEY, -1);
+ if (numLinks < 0 || numLinks == mSiteSettings.getMultipleLinks()) return;
+ onPreferenceChange(mMultipleLinksPref, numLinks);
+ break;
+ }
+ } else {
+ switch (requestCode) {
+ case DELETE_SITE_REQUEST_CODE:
+ deleteSite();
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container,
+ Bundle savedInstanceState) {
+ // use a wrapper to apply the Calypso theme
+ Context themer = new ContextThemeWrapper(getActivity(), R.style.Calypso_SiteSettingsTheme);
+ LayoutInflater localInflater = inflater.cloneInContext(themer);
+ View view = super.onCreateView(localInflater, container, savedInstanceState);
+
+ if (view != null) {
+ setupPreferenceList((ListView) view.findViewById(android.R.id.list), getResources());
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ removeMoreScreenToolbar();
+ super.onSaveInstanceState(outState);
+ setupMorePreferenceScreen();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (savedInstanceState != null) setupMorePreferenceScreen();
+ }
+
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ if (child.getId() == android.R.id.title && child instanceof TextView) {
+ // style preference category title views
+ TextView title = (TextView) child;
+ WPPrefUtils.layoutAsBody2(title);
+ } else {
+ // style preference title views
+ TextView title = (TextView) child.findViewById(android.R.id.title);
+ if (title != null) WPPrefUtils.layoutAsSubhead(title);
+ }
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+ // NOP
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) {
+ super.onPreferenceTreeClick(screen, preference);
+
+ // More preference selected, style the Discussion screen
+ if (preference == mMorePreference) {
+ // track user accessing the full Discussion settings screen
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_ACCESSED_MORE_SETTINGS);
+
+ return setupMorePreferenceScreen();
+ } else if (preference == findPreference(getString(R.string.pref_key_site_start_over_screen))) {
+ Dialog dialog = ((PreferenceScreen) preference).getDialog();
+ if (dialog == null) return false;
+
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_START_OVER_ACCESSED);
+
+ setupPreferenceList((ListView) dialog.findViewById(android.R.id.list), getResources());
+ String title = getString(R.string.start_over);
+ WPActivityUtils.addToolbarToDialog(this, dialog, title);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference == mRelatedPostsPref) {
+ showRelatedPostsDialog();
+ } else if (preference == mMultipleLinksPref) {
+ showMultipleLinksDialog();
+ } else if (preference == mModerationHoldPref) {
+ mEditingList = mSiteSettings.getModerationKeys();
+ showListEditorDialog(R.string.site_settings_moderation_hold_title,
+ R.string.site_settings_hold_for_moderation_description);
+ } else if (preference == mBlacklistPref) {
+ mEditingList = mSiteSettings.getBlacklistKeys();
+ showListEditorDialog(R.string.site_settings_blacklist_title,
+ R.string.site_settings_blacklist_description);
+ } else if (preference == mStartOverPref) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED);
+ HelpshiftHelper.getInstance().showConversation(getActivity(), HelpshiftHelper.Tag.ORIGIN_START_OVER);
+ } else if (preference == mCloseAfterPref) {
+ showCloseAfterDialog();
+ } else if (preference == mPagingPref) {
+ showPagingDialog();
+ } else if (preference == mThreadingPref) {
+ showThreadingDialog();
+ } else if (preference == mCategoryPref || preference == mFormatPref) {
+ return !shouldShowListPreference((DetailListPreference) preference);
+ } else if (preference == mExportSitePref) {
+ showExportContentDialog();
+ } else if (preference == mDeleteSitePref) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_ACCESSED);
+ requestPurchasesForDeletionCheck();
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue == null || !mEditingEnabled) return false;
+
+ if (preference == mTitlePref) {
+ mSiteSettings.setTitle(newValue.toString());
+ changeEditTextPreferenceValue(mTitlePref, mSiteSettings.getTitle());
+ } else if (preference == mTaglinePref) {
+ mSiteSettings.setTagline(newValue.toString());
+ changeEditTextPreferenceValue(mTaglinePref, mSiteSettings.getTagline());
+ } else if (preference == mAddressPref) {
+ mSiteSettings.setAddress(newValue.toString());
+ changeEditTextPreferenceValue(mAddressPref, mSiteSettings.getAddress());
+ } else if (preference == mLanguagePref) {
+ if (!mSiteSettings.setLanguageCode(newValue.toString())) {
+ AppLog.w(AppLog.T.SETTINGS, "Unknown language code " + newValue.toString() + " selected in Site Settings.");
+ ToastUtils.showToast(getActivity(), R.string.site_settings_unknown_language_code_error);
+ }
+ changeLanguageValue(mSiteSettings.getLanguageCode());
+ } else if (preference == mPrivacyPref) {
+ mSiteSettings.setPrivacy(Integer.parseInt(newValue.toString()));
+ setDetailListPreferenceValue(mPrivacyPref,
+ String.valueOf(mSiteSettings.getPrivacy()),
+ mSiteSettings.getPrivacyDescription());
+ } else if (preference == mAllowCommentsPref || preference == mAllowCommentsNested) {
+ setAllowComments((Boolean) newValue);
+ } else if (preference == mSendPingbacksPref || preference == mSendPingbacksNested) {
+ setSendPingbacks((Boolean) newValue);
+ } else if (preference == mReceivePingbacksPref || preference == mReceivePingbacksNested) {
+ setReceivePingbacks((Boolean) newValue);
+ } else if (preference == mCloseAfterPref) {
+ mSiteSettings.setCloseAfter(Integer.parseInt(newValue.toString()));
+ mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescription());
+ } else if (preference == mSortByPref) {
+ mSiteSettings.setCommentSorting(Integer.parseInt(newValue.toString()));
+ setDetailListPreferenceValue(mSortByPref,
+ newValue.toString(),
+ mSiteSettings.getSortingDescription());
+ } else if (preference == mThreadingPref) {
+ mSiteSettings.setThreadingLevels(Integer.parseInt(newValue.toString()));
+ mThreadingPref.setSummary(mSiteSettings.getThreadingDescription());
+ } else if (preference == mPagingPref) {
+ mSiteSettings.setPagingCount(Integer.parseInt(newValue.toString()));
+ mPagingPref.setSummary(mSiteSettings.getPagingDescription());
+ } else if (preference == mIdentityRequiredPreference) {
+ mSiteSettings.setIdentityRequired((Boolean) newValue);
+ } else if (preference == mUserAccountRequiredPref) {
+ mSiteSettings.setUserAccountRequired((Boolean) newValue);
+ } else if (preference == mWhitelistPref) {
+ updateWhitelistSettings(Integer.parseInt(newValue.toString()));
+ } else if (preference == mMultipleLinksPref) {
+ mSiteSettings.setMultipleLinks(Integer.parseInt(newValue.toString()));
+ String s = StringUtils.getQuantityString(getActivity(), R.string.site_settings_multiple_links_summary_zero,
+ R.string.site_settings_multiple_links_summary_one,
+ R.string.site_settings_multiple_links_summary_other, mSiteSettings.getMultipleLinks());
+ mMultipleLinksPref.setSummary(s);
+ } else if (preference == mUsernamePref) {
+ mSiteSettings.setUsername(newValue.toString());
+ changeEditTextPreferenceValue(mUsernamePref, mSiteSettings.getUsername());
+ } else if (preference == mPasswordPref) {
+ mSiteSettings.setPassword(newValue.toString());
+ changeEditTextPreferenceValue(mPasswordPref, mSiteSettings.getPassword());
+ } else if (preference == mLocationPref) {
+ mSiteSettings.setLocation((Boolean) newValue);
+ } else if (preference == mCategoryPref) {
+ mSiteSettings.setDefaultCategory(Integer.parseInt(newValue.toString()));
+ setDetailListPreferenceValue(mCategoryPref,
+ newValue.toString(),
+ mSiteSettings.getDefaultCategoryForDisplay());
+ } else if (preference == mFormatPref) {
+ mSiteSettings.setDefaultFormat(newValue.toString());
+ setDetailListPreferenceValue(mFormatPref,
+ newValue.toString(),
+ mSiteSettings.getDefaultPostFormatDisplay());
+ } else if (preference == mImageWidthPref) {
+ mBlog.setMaxImageWidth(newValue.toString());
+ setDetailListPreferenceValue(mImageWidthPref,
+ mBlog.getMaxImageWidth(),
+ mBlog.getMaxImageWidth());
+ } else if (preference == mUploadAndLinkPref) {
+ mBlog.setFullSizeImage(Boolean.valueOf(newValue.toString()));
+ } else if (preference == mRelatedPostsPref) {
+ mRelatedPostsPref.setSummary(newValue.toString());
+ } else if (preference == mModerationHoldPref) {
+ mModerationHoldPref.setSummary(mSiteSettings.getModerationHoldDescription());
+ } else if (preference == mBlacklistPref) {
+ mBlacklistPref.setSummary(mSiteSettings.getBlacklistDescription());
+ } else {
+ return false;
+ }
+
+ mSiteSettings.saveSettings();
+
+ return true;
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ ListView listView = (ListView) parent;
+ ListAdapter listAdapter = listView.getAdapter();
+ Object obj = listAdapter.getItem(position);
+
+ if (obj != null) {
+ if (obj instanceof View.OnLongClickListener) {
+ View.OnLongClickListener longListener = (View.OnLongClickListener) obj;
+ return longListener.onLongClick(view);
+ } else if (obj instanceof PreferenceHint) {
+ PreferenceHint hintObj = (PreferenceHint) obj;
+ if (hintObj.hasHint()) {
+ HashMap<String, Object> properties = new HashMap<>();
+ properties.put("hint_shown", hintObj.getHint());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_HINT_TOAST_SHOWN, properties);
+ ToastUtils.showToast(getActivity(), hintObj.getHint(), ToastUtils.Duration.SHORT);
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mEditingList == mSiteSettings.getModerationKeys()) {
+ onPreferenceChange(mModerationHoldPref, mEditingList.size());
+ } else if (mEditingList == mSiteSettings.getBlacklistKeys()) {
+ onPreferenceChange(mBlacklistPref, mEditingList.size());
+ }
+ mEditingList = null;
+ }
+
+ @Override
+ public void onSettingsUpdated(Exception error) {
+ if (error != null) {
+ ToastUtils.showToast(getActivity(), R.string.error_fetch_remote_site_settings);
+ getActivity().finish();
+ return;
+ }
+
+ if (isAdded()) setPreferencesFromSiteSettings();
+ }
+
+ @Override
+ public void onSettingsSaved(Exception error) {
+ if (error != null) {
+ ToastUtils.showToast(WordPress.getContext(), R.string.error_post_remote_site_settings);
+ return;
+ }
+ mBlog.setBlogName(mSiteSettings.getTitle());
+ WordPress.wpDB.saveBlog(mBlog);
+
+ // update the global current Blog so WordPress.getCurrentBlog() callers will get the updated object
+ WordPress.setCurrentBlog(mBlog.getLocalTableBlogId());
+
+ EventBus.getDefault().post(new CoreEvents.BlogListChanged());
+ }
+
+ @Override
+ public void onCredentialsValidated(Exception error) {
+ if (error != null) {
+ ToastUtils.showToast(WordPress.getContext(), R.string.username_or_password_incorrect);
+ }
+ }
+
+ private void setupPreferenceList(ListView prefList, Resources res) {
+ if (prefList == null || res == null) return;
+
+ // customize list dividers
+ //noinspection deprecation
+ prefList.setDivider(res.getDrawable(R.drawable.preferences_divider));
+ prefList.setDividerHeight(res.getDimensionPixelSize(R.dimen.site_settings_divider_height));
+ // handle long clicks on preferences to display hints
+ prefList.setOnItemLongClickListener(this);
+ // required to customize (Calypso) preference views
+ prefList.setOnHierarchyChangeListener(this);
+ // remove footer divider bar
+ prefList.setFooterDividersEnabled(false);
+ //noinspection deprecation
+ prefList.setOverscrollFooter(res.getDrawable(R.color.transparent));
+ }
+
+ /**
+ * Helper method to retrieve {@link Preference} references and initialize any data.
+ */
+ private void initPreferences() {
+ mTitlePref = (EditTextPreference) getChangePref(R.string.pref_key_site_title);
+ mTaglinePref = (EditTextPreference) getChangePref(R.string.pref_key_site_tagline);
+ mAddressPref = (EditTextPreference) getChangePref(R.string.pref_key_site_address);
+ mPrivacyPref = (DetailListPreference) getChangePref(R.string.pref_key_site_visibility);
+ mLanguagePref = (DetailListPreference) getChangePref(R.string.pref_key_site_language);
+ mUsernamePref = (EditTextPreference) getChangePref(R.string.pref_key_site_username);
+ mPasswordPref = (EditTextPreference) getChangePref(R.string.pref_key_site_password);
+ mLocationPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_location);
+ mCategoryPref = (DetailListPreference) getChangePref(R.string.pref_key_site_category);
+ mFormatPref = (DetailListPreference) getChangePref(R.string.pref_key_site_format);
+ mAllowCommentsPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_allow_comments);
+ mAllowCommentsNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_allow_comments_nested);
+ mSendPingbacksPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_send_pingbacks);
+ mSendPingbacksNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_send_pingbacks_nested);
+ mReceivePingbacksPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_receive_pingbacks);
+ mReceivePingbacksNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_receive_pingbacks_nested);
+ mIdentityRequiredPreference = (WPSwitchPreference) getChangePref(R.string.pref_key_site_identity_required);
+ mUserAccountRequiredPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_user_account_required);
+ mSortByPref = (DetailListPreference) getChangePref(R.string.pref_key_site_sort_by);
+ mWhitelistPref = (DetailListPreference) getChangePref(R.string.pref_key_site_whitelist);
+ mMorePreference = (PreferenceScreen) getClickPref(R.string.pref_key_site_more_discussion);
+ mRelatedPostsPref = getClickPref(R.string.pref_key_site_related_posts);
+ mCloseAfterPref = getClickPref(R.string.pref_key_site_close_after);
+ mPagingPref = getClickPref(R.string.pref_key_site_paging);
+ mThreadingPref = getClickPref(R.string.pref_key_site_threading);
+ mMultipleLinksPref = getClickPref(R.string.pref_key_site_multiple_links);
+ mModerationHoldPref = getClickPref(R.string.pref_key_site_moderation_hold);
+ mBlacklistPref = getClickPref(R.string.pref_key_site_blacklist);
+ mImageWidthPref = (DetailListPreference) getChangePref(R.string.pref_key_site_image_width);
+ mUploadAndLinkPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_upload_and_link_image);
+ mStartOverPref = getClickPref(R.string.pref_key_site_start_over);
+ mExportSitePref = getClickPref(R.string.pref_key_site_export_site);
+ mDeleteSitePref = getClickPref(R.string.pref_key_site_delete_site);
+
+ sortLanguages();
+
+ // .com sites hide the Account category, self-hosted sites hide the Related Posts preference
+ if (mBlog.isDotcomFlag()) {
+ removeSelfHostedOnlyPreferences();
+ } else {
+ removeDotComOnlyPreferences();
+ }
+
+ // hide all options except for Delete site and Enable Location if user is not admin
+ if (!mBlog.isAdmin()) hideAdminRequiredPreferences();
+ }
+
+ public void setEditingEnabled(boolean enabled) {
+ // excludes mAddressPref, mMorePreference
+ final Preference[] editablePreference = {
+ mTitlePref , mTaglinePref, mPrivacyPref, mLanguagePref, mUsernamePref,
+ mPasswordPref, mLocationPref, mCategoryPref, mFormatPref, mAllowCommentsPref,
+ mAllowCommentsNested, mSendPingbacksPref, mSendPingbacksNested, mReceivePingbacksPref,
+ mReceivePingbacksNested, mIdentityRequiredPreference, mUserAccountRequiredPref,
+ mSortByPref, mWhitelistPref, mRelatedPostsPref, mCloseAfterPref, mPagingPref,
+ mThreadingPref, mMultipleLinksPref, mModerationHoldPref, mBlacklistPref,
+ mImageWidthPref, mUploadAndLinkPref, mDeleteSitePref
+ };
+
+ for(Preference preference : editablePreference) {
+ if(preference!=null) preference.setEnabled(enabled);
+ }
+
+ mEditingEnabled = enabled;
+ }
+
+ private void showRelatedPostsDialog() {
+ DialogFragment relatedPosts = new RelatedPostsDialog();
+ Bundle args = new Bundle();
+ args.putBoolean(RelatedPostsDialog.SHOW_RELATED_POSTS_KEY, mSiteSettings.getShowRelatedPosts());
+ args.putBoolean(RelatedPostsDialog.SHOW_HEADER_KEY, mSiteSettings.getShowRelatedPostHeader());
+ args.putBoolean(RelatedPostsDialog.SHOW_IMAGES_KEY, mSiteSettings.getShowRelatedPostImages());
+ relatedPosts.setArguments(args);
+ relatedPosts.setTargetFragment(this, RELATED_POSTS_REQUEST_CODE);
+ relatedPosts.show(getFragmentManager(), "related-posts");
+ }
+
+ private void showNumberPickerDialog(Bundle args, int requestCode, String tag) {
+ showNumberPickerDialog(args, requestCode, tag, null);
+ }
+
+ private void showNumberPickerDialog(Bundle args, int requestCode, String tag, Formatter format) {
+ NumberPickerDialog dialog = new NumberPickerDialog();
+ dialog.setNumberFormat(format);
+ dialog.setArguments(args);
+ dialog.setTargetFragment(this, requestCode);
+ dialog.show(getFragmentManager(), tag);
+ }
+
+ private void showPagingDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true);
+ args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldPageComments());
+ args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_paging_title));
+ args.putString(NumberPickerDialog.SWITCH_DESC_KEY, getString(R.string.site_settings_paging_dialog_description));
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_paging_title));
+ args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_paging_dialog_header));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 1);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.paging_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getPagingCount());
+ showNumberPickerDialog(args, PAGING_REQUEST_CODE, "paging-dialog");
+ }
+
+ private void showThreadingDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true);
+ args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldThreadComments());
+ args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_threading_title));
+ args.putString(NumberPickerDialog.SWITCH_DESC_KEY, getString(R.string.site_settings_threading_dialog_description));
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_threading_title));
+ args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_threading_dialog_header));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 2);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.threading_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getThreadingLevels());
+ showNumberPickerDialog(args, THREADING_REQUEST_CODE, "threading-dialog", new Formatter() {
+ @Override
+ public String format(int value) {
+ return mSiteSettings.getThreadingDescriptionForLevel(value);
+ }
+ });
+ }
+
+ private void showExportContentDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.export_your_content);
+ String email = AccountHelper.getDefaultAccount().getEmail();
+ builder.setMessage(getString(R.string.export_your_content_message, email));
+ builder.setPositiveButton(R.string.site_settings_export_content_title, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_REQUESTED);
+ exportSite();
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+
+ builder.show();
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_ACCESSED);
+ }
+
+ private void dismissProgressDialog(ProgressDialog progressDialog) {
+ if (progressDialog != null && progressDialog.isShowing()) {
+ try {
+ progressDialog.dismiss();
+ } catch (IllegalArgumentException e) {
+ // dialog doesn't exist
+ }
+ }
+ }
+
+ private void requestPurchasesForDeletionCheck() {
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(), "", getString(R.string.checking_purchases), true, false);
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED);
+ WordPress.getRestClientUtils().getSitePurchases(currentBlog.getDotComBlogId(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ dismissProgressDialog(progressDialog);
+ if (isAdded()) {
+ showPurchasesOrDeleteSiteDialog(response, currentBlog);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ dismissProgressDialog(progressDialog);
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), getString(R.string.purchases_request_error));
+ AppLog.e(AppLog.T.API, "Error occurred while requesting purchases for deletion check: " + error.toString());
+ }
+ }
+ });
+ }
+
+ private void showPurchasesOrDeleteSiteDialog(JSONObject response, final Blog currentBlog) {
+ try {
+ JSONArray purchases = response.getJSONArray(PURCHASE_ORIGINAL_RESPONSE_KEY);
+ if (hasActivePurchases(purchases)) {
+ showPurchasesDialog(currentBlog);
+ } else {
+ showDeleteSiteDialog();
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.API, "Error occurred while trying to delete site: " + e.toString());
+ }
+ }
+
+ private void showPurchasesDialog(final Blog currentBlog) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.premium_upgrades_title);
+ builder.setMessage(R.string.premium_upgrades_message);
+ builder.setPositiveButton(R.string.show_purchases, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED);
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(getActivity(), WORDPRESS_PURCHASES_URL, AccountHelper.getCurrentUsernameForBlog(currentBlog));
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ builder.show();
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN);
+ }
+
+ private boolean hasActivePurchases(JSONArray purchases) throws JSONException {
+ for (int i = 0; i < purchases.length(); i++) {
+ JSONObject purchase = purchases.getJSONObject(i);
+ int active = purchase.getInt(PURCHASE_ACTIVE_KEY);
+
+ if (active == 1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void showDeleteSiteDialog() {
+ if (mIsFragmentPaused) return; // Do not show the DeleteSiteDialogFragment if the fragment was paused.
+ // DialogFragment internally uses commit(), and not commitAllowingStateLoss, crashing the app in case like that.
+ Bundle args = new Bundle();
+ args.putString(DeleteSiteDialogFragment.SITE_DOMAIN_KEY, UrlUtils.getHost(mBlog.getHomeURL()));
+ DeleteSiteDialogFragment deleteSiteDialogFragment = new DeleteSiteDialogFragment();
+ deleteSiteDialogFragment.setArguments(args);
+ deleteSiteDialogFragment.setTargetFragment(this, DELETE_SITE_REQUEST_CODE);
+ deleteSiteDialogFragment.show(getFragmentManager(), DELETE_SITE_TAG);
+ }
+
+ private void showCloseAfterDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true);
+ args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldCloseAfter());
+ args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_close_after_dialog_switch_text));
+ args.putString(NumberPickerDialog.SWITCH_DESC_KEY, getString(R.string.site_settings_close_after_dialog_description));
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_close_after_dialog_title));
+ args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_close_after_dialog_header));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 1);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.close_after_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getCloseAfter());
+ showNumberPickerDialog(args, CLOSE_AFTER_REQUEST_CODE, "close-after-dialog");
+ }
+
+ private void showMultipleLinksDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, false);
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_multiple_links_title));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 0);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.max_links_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getMultipleLinks());
+ showNumberPickerDialog(args, MULTIPLE_LINKS_REQUEST_CODE, "multiple-links-dialog");
+ }
+
+ private void setPreferencesFromSiteSettings() {
+ mLocationPref.setChecked(mSiteSettings.getLocation());
+ changeEditTextPreferenceValue(mTitlePref, mSiteSettings.getTitle());
+ changeEditTextPreferenceValue(mTaglinePref, mSiteSettings.getTagline());
+ changeEditTextPreferenceValue(mAddressPref, mSiteSettings.getAddress());
+ changeEditTextPreferenceValue(mUsernamePref, mSiteSettings.getUsername());
+ changeEditTextPreferenceValue(mPasswordPref, mSiteSettings.getPassword());
+ changeLanguageValue(mSiteSettings.getLanguageCode());
+ setDetailListPreferenceValue(mPrivacyPref,
+ String.valueOf(mSiteSettings.getPrivacy()),
+ mSiteSettings.getPrivacyDescription());
+ setDetailListPreferenceValue(mImageWidthPref,
+ mBlog.getMaxImageWidth(),
+ mBlog.getMaxImageWidth());
+ setCategories();
+ setPostFormats();
+ setAllowComments(mSiteSettings.getAllowComments());
+ setSendPingbacks(mSiteSettings.getSendPingbacks());
+ setReceivePingbacks(mSiteSettings.getReceivePingbacks());
+ setDetailListPreferenceValue(mSortByPref,
+ String.valueOf(mSiteSettings.getCommentSorting()),
+ mSiteSettings.getSortingDescription());
+ int approval = mSiteSettings.getManualApproval() ?
+ mSiteSettings.getUseCommentWhitelist() ? 0
+ : -1 : 1;
+ setDetailListPreferenceValue(mWhitelistPref, String.valueOf(approval), getWhitelistSummary(approval));
+ String s = StringUtils.getQuantityString(getActivity(), R.string.site_settings_multiple_links_summary_zero,
+ R.string.site_settings_multiple_links_summary_one,
+ R.string.site_settings_multiple_links_summary_other, mSiteSettings.getMultipleLinks());
+ mMultipleLinksPref.setSummary(s);
+ mUploadAndLinkPref.setChecked(mBlog.isFullSizeImage());
+ mIdentityRequiredPreference.setChecked(mSiteSettings.getIdentityRequired());
+ mUserAccountRequiredPref.setChecked(mSiteSettings.getUserAccountRequired());
+ mThreadingPref.setSummary(mSiteSettings.getThreadingDescription());
+ mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescriptionForPeriod());
+ mPagingPref.setSummary(mSiteSettings.getPagingDescription());
+ mRelatedPostsPref.setSummary(mSiteSettings.getRelatedPostsDescription());
+ mModerationHoldPref.setSummary(mSiteSettings.getModerationHoldDescription());
+ mBlacklistPref.setSummary(mSiteSettings.getBlacklistDescription());
+ }
+
+ private void setCategories() {
+ // Ignore if there are no changes
+ if (mSiteSettings.isSameCategoryList(mCategoryPref.getEntryValues())) {
+ mCategoryPref.setValue(String.valueOf(mSiteSettings.getDefaultCategory()));
+ mCategoryPref.setSummary(mSiteSettings.getDefaultCategoryForDisplay());
+ return;
+ }
+
+ Map<Integer, String> categories = mSiteSettings.getCategoryNames();
+ CharSequence[] entries = new CharSequence[categories.size()];
+ CharSequence[] values = new CharSequence[categories.size()];
+ int i = 0;
+ for (Integer key : categories.keySet()) {
+ entries[i] = categories.get(key);
+ values[i] = String.valueOf(key);
+ if (key == UNCATEGORIZED_CATEGORY_ID) {
+ CharSequence temp = entries[0];
+ entries[0] = entries[i];
+ entries[i] = temp;
+ temp = values[0];
+ values[0] = values[i];
+ values[i] = temp;
+ }
+ ++i;
+ }
+
+ mCategoryPref.setEntries(entries);
+ mCategoryPref.setEntryValues(values);
+ mCategoryPref.setValue(String.valueOf(mSiteSettings.getDefaultCategory()));
+ mCategoryPref.setSummary(mSiteSettings.getDefaultCategoryForDisplay());
+ }
+
+ private void setPostFormats() {
+ // Ignore if there are no changes
+ if (mSiteSettings.isSameFormatList(mFormatPref.getEntryValues())) {
+ mFormatPref.setValue(String.valueOf(mSiteSettings.getDefaultPostFormat()));
+ mFormatPref.setSummary(mSiteSettings.getDefaultPostFormatDisplay());
+ return;
+ }
+
+ // clone the post formats map
+ final Map<String, String> postFormats = new HashMap<>(mSiteSettings.getFormats());
+
+ // transform the keys and values into arrays and set the ListPreference's data
+ mFormatPref.setEntries(postFormats.values().toArray(new String[0]));
+ mFormatPref.setEntryValues(postFormats.keySet().toArray(new String[0]));
+ mFormatPref.setValue(String.valueOf(mSiteSettings.getDefaultPostFormat()));
+ mFormatPref.setSummary(mSiteSettings.getDefaultPostFormatDisplay());
+ }
+
+ private void setAllowComments(boolean newValue) {
+ mSiteSettings.setAllowComments(newValue);
+ mAllowCommentsPref.setChecked(newValue);
+ mAllowCommentsNested.setChecked(newValue);
+ }
+
+ private void setSendPingbacks(boolean newValue) {
+ mSiteSettings.setSendPingbacks(newValue);
+ mSendPingbacksPref.setChecked(newValue);
+ mSendPingbacksNested.setChecked(newValue);
+ }
+
+ private void setReceivePingbacks(boolean newValue) {
+ mSiteSettings.setReceivePingbacks(newValue);
+ mReceivePingbacksPref.setChecked(newValue);
+ mReceivePingbacksNested.setChecked(newValue);
+ }
+
+ private void setDetailListPreferenceValue(DetailListPreference pref, String value, String summary) {
+ pref.setValue(value);
+ pref.setSummary(summary);
+ pref.refreshAdapter();
+ }
+
+ /**
+ * Helper method to perform validation and set multiple properties on an EditTextPreference.
+ * If newValue is equal to the current preference text no action will be taken.
+ */
+ private void changeEditTextPreferenceValue(EditTextPreference pref, String newValue) {
+ if (newValue == null || pref == null || pref.getEditText().isInEditMode()) return;
+
+ if (!newValue.equals(pref.getSummary())) {
+ String formattedValue = StringUtils.unescapeHTML(newValue.replaceFirst(ADDRESS_FORMAT_REGEX, ""));
+
+ pref.setText(formattedValue);
+ pref.setSummary(formattedValue);
+ }
+ }
+
+ /**
+ * Detail strings for the dialog are generated in the selected language.
+ *
+ * @param newValue
+ * languageCode
+ */
+ private void changeLanguageValue(String newValue) {
+ if (mLanguagePref == null || newValue == null) return;
+
+ if (TextUtils.isEmpty(mLanguagePref.getSummary()) ||
+ !newValue.equals(mLanguagePref.getValue())) {
+ mLanguagePref.setValue(newValue);
+ String summary = WPPrefUtils.getLanguageString(newValue, WPPrefUtils.languageLocale(newValue));
+ mLanguagePref.setSummary(summary);
+ mLanguagePref.refreshAdapter();
+ }
+ }
+
+ private void sortLanguages() {
+ if (mLanguagePref == null) return;
+
+ Pair<String[], String[]> pair = WPPrefUtils.createSortedLanguageDisplayStrings(mLanguagePref.getEntryValues(), WPPrefUtils.languageLocale(null));
+ if (pair != null) {
+ String[] sortedEntries = pair.first;
+ String[] sortedValues = pair.second;
+
+ mLanguagePref.setEntries(sortedEntries);
+ mLanguagePref.setEntryValues(sortedValues);
+ mLanguagePref.setDetails(WPPrefUtils.createLanguageDetailDisplayStrings(sortedValues));
+ }
+ }
+
+ private String getWhitelistSummary(int value) {
+ if (isAdded()) {
+ switch (value) {
+ case -1:
+ return getString(R.string.site_settings_whitelist_none_summary);
+ case 0:
+ return getString(R.string.site_settings_whitelist_known_summary);
+ case 1:
+ return getString(R.string.site_settings_whitelist_all_summary);
+ }
+ }
+ return "";
+ }
+
+ private void updateWhitelistSettings(int val) {
+ mSiteSettings.setManualApproval(val == -1);
+ mSiteSettings.setUseCommentWhitelist(val == 0);
+ setDetailListPreferenceValue(mWhitelistPref,
+ String.valueOf(val),
+ getWhitelistSummary(val));
+ }
+
+ private void showListEditorDialog(int titleRes, int headerRes) {
+ mDialog = new Dialog(getActivity(), R.style.Calypso_SiteSettingsTheme);
+ mDialog.setOnDismissListener(this);
+ mDialog.setContentView(getListEditorView(getString(headerRes)));
+ mDialog.show();
+ WPActivityUtils.addToolbarToDialog(this, mDialog, getString(titleRes));
+ }
+
+ private View getListEditorView(String headerText) {
+ Context themer = new ContextThemeWrapper(getActivity(), R.style.Calypso_SiteSettingsTheme);
+ View view = View.inflate(themer, R.layout.list_editor, null);
+ ((TextView) view.findViewById(R.id.list_editor_header_text)).setText(headerText);
+
+ mAdapter = null;
+ final EmptyViewRecyclerView list = (EmptyViewRecyclerView) view.findViewById(android.R.id.list);
+ list.setLayoutManager(
+ new SmoothScrollLinearLayoutManager(
+ getActivity(),
+ LinearLayoutManager.VERTICAL,
+ false,
+ getResources().getInteger(android.R.integer.config_mediumAnimTime)
+ )
+ );
+ list.setAdapter(getAdapter());
+ list.setEmptyView(view.findViewById(R.id.empty_view));
+ list.addOnItemTouchListener(
+ new RecyclerViewItemClickListener(
+ getActivity(),
+ list,
+ new RecyclerViewItemClickListener.OnItemClickListener() {
+ @Override
+ public void onItemClick(View view, int position) {
+ if (mActionMode != null) {
+ getAdapter().toggleItemSelected(position);
+ mActionMode.invalidate();
+
+ if (getAdapter().getItemsSelected().size() <= 0) {
+ mActionMode.finish();
+ }
+ }
+ }
+
+ @Override
+ public void onLongItemClick(View view, int position) {
+ if (mActionMode == null) {
+ if (view.isHapticFeedbackEnabled()) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+
+ mDialog.getWindow().getDecorView().startActionMode(new ActionModeCallback());
+ getAdapter().setItemSelected(position);
+ mActionMode.invalidate();
+ }
+ }
+ }
+ )
+ );
+ view.findViewById(R.id.fab_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ final EditText input = new EditText(getActivity());
+ WPPrefUtils.layoutAsInput(input);
+ input.setWidth(getResources().getDimensionPixelSize(R.dimen.list_editor_input_max_width));
+ input.setHint(R.string.site_settings_list_editor_input_hint);
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String entry = input.getText().toString();
+ if (!TextUtils.isEmpty(entry) && !mEditingList.contains(entry)) {
+ mEditingList.add(entry);
+ getAdapter().notifyItemInserted(getAdapter().getItemCount() - 1);
+ list.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ list.smoothScrollToPosition(getAdapter().getItemCount() - 1);
+ }
+ }
+ );
+ mSiteSettings.saveSettings();
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_ADDED_LIST_ITEM);
+ }
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ final AlertDialog alertDialog = builder.create();
+ int spacing = getResources().getDimensionPixelSize(R.dimen.dlp_padding_start);
+ alertDialog.setView(input, spacing, spacing, spacing, 0);
+ alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ alertDialog.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ alertDialog.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ }
+ });
+ alertDialog.show();
+ Button positive = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+ });
+
+ return view;
+ }
+
+ private void removeBlog() {
+ if (WordPress.wpDB.deleteBlog(getActivity(), mBlog.getLocalTableBlogId())) {
+ StatsTable.deleteStatsForBlog(getActivity(), mBlog.getLocalTableBlogId()); // Remove stats data
+ AnalyticsUtils.refreshMetadata();
+ ToastUtils.showToast(getActivity(), R.string.blog_removed_successfully);
+ WordPress.wpDB.deleteLastBlogId();
+ WordPress.currentBlog = null;
+ getActivity().setResult(RESULT_BLOG_REMOVED);
+
+ // If the last blog is removed and the user is not signed in wpcom, broadcast a UserSignedOut event
+ if (!AccountHelper.isSignedIn()) {
+ EventBus.getDefault().post(new CoreEvents.UserSignedOutCompletely());
+ }
+
+ // Checks for stats widgets that were synched with a blog that could be gone now.
+ StatsWidgetProvider.updateWidgetsOnLogout(getActivity());
+
+ getActivity().finish();
+ } else {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(getResources().getText(R.string.could_not_remove_account));
+ dialogBuilder.setPositiveButton(R.string.ok, null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+ }
+
+ private boolean shouldShowListPreference(DetailListPreference preference) {
+ return preference != null && preference.getEntries() != null && preference.getEntries().length > 0;
+ }
+
+ private boolean setupMorePreferenceScreen() {
+ if (mMorePreference == null || !isAdded()) return false;
+ String title = getString(R.string.site_settings_discussion_title);
+ Dialog dialog = mMorePreference.getDialog();
+ if (dialog != null) {
+ setupPreferenceList((ListView) dialog.findViewById(android.R.id.list), getResources());
+ WPActivityUtils.addToolbarToDialog(this, dialog, title);
+ return true;
+ }
+ return false;
+ }
+
+ private void removeMoreScreenToolbar() {
+ if (mMorePreference == null || !isAdded()) return;
+ Dialog moreDialog = mMorePreference.getDialog();
+ WPActivityUtils.removeToolbarFromDialog(this, moreDialog);
+ }
+
+ private void hideAdminRequiredPreferences() {
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_general);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_account);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_discussion);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_category);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_format);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts);
+ }
+
+ private void removeDotComOnlyPreferences() {
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_general, R.string.pref_key_site_language);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts);
+ }
+
+ private void removeSelfHostedOnlyPreferences() {
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_account);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_delete_site_screen);
+ }
+
+ private Preference getChangePref(int id) {
+ return WPPrefUtils.getPrefAndSetChangeListener(this, id, this);
+ }
+
+ private Preference getClickPref(int id) {
+ return WPPrefUtils.getPrefAndSetClickListener(this, id, this);
+ }
+
+ private void handleDeleteSiteError() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.error_deleting_site);
+ builder.setMessage(R.string.error_deleting_site_summary);
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ builder.setPositiveButton(R.string.contact_support, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ HelpshiftHelper.getInstance().showConversation(getActivity(), HelpshiftHelper.Tag.ORIGIN_DELETE_SITE);
+ }
+ });
+ builder.show();
+ }
+
+ private void exportSite() {
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ if (currentBlog.isDotcomFlag()) {
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(), "", getActivity().getString(R.string.exporting_content_progress), true, true);
+ WordPress.getRestClientUtils().exportContentAll(currentBlog.getDotComBlogId(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (isAdded()) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK);
+ dismissProgressDialog(progressDialog);
+ Snackbar.make(getView(), R.string.export_email_sent, Snackbar.LENGTH_LONG).show();
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (isAdded()) {
+ HashMap<String, Object> errorProperty = new HashMap<>();
+ errorProperty.put(ANALYTICS_ERROR_PROPERTY_KEY, error.getMessage());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR, errorProperty);
+ dismissProgressDialog(progressDialog);
+ }
+ }
+ });
+ }
+ }
+
+ private void deleteSite() {
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ if (currentBlog.isDotcomFlag()) {
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(), "", getString(R.string.delete_site_progress), true, false);
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_REQUESTED);
+ WordPress.getRestClientUtils().deleteSite(currentBlog.getDotComBlogId(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_RESPONSE_OK);
+ progressDialog.dismiss();
+ removeBlog();
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ HashMap<String, Object> errorProperty = new HashMap<>();
+ errorProperty.put(ANALYTICS_ERROR_PROPERTY_KEY, error.getMessage());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR, errorProperty);
+ dismissProgressDialog(progressDialog);
+ handleDeleteSiteError();
+ }
+ });
+ }
+ }
+
+ private MultiSelectRecyclerViewAdapter getAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new MultiSelectRecyclerViewAdapter(getActivity(), mEditingList);
+ }
+
+ return mAdapter;
+ }
+
+ private final class ActionModeCallback implements ActionMode.Callback {
+ @Override
+ public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case R.id.menu_delete:
+ SparseBooleanArray checkedItems = getAdapter().getItemsSelected();
+
+ HashMap<String, Object> properties = new HashMap<>();
+ properties.put("num_items_deleted", checkedItems.size());
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.SITE_SETTINGS_DELETED_LIST_ITEMS, properties);
+
+ for (int i = checkedItems.size() - 1; i >= 0; i--) {
+ final int index = checkedItems.keyAt(i);
+
+ if (checkedItems.get(index)) {
+ mEditingList.remove(index);
+ }
+ }
+
+ mSiteSettings.saveSettings();
+ mActionMode.finish();
+ return true;
+ case R.id.menu_select_all:
+ for (int i = 0; i < getAdapter().getItemCount(); i++) {
+ getAdapter().setItemSelected(i);
+ }
+
+ mActionMode.invalidate();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
+ WPActivityUtils.setStatusBarColor(mDialog.getWindow(), R.color.action_mode_status_bar_tint);
+ mActionMode = actionMode;
+ MenuInflater inflater = actionMode.getMenuInflater();
+ inflater.inflate(R.menu.list_editor, menu);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ WPActivityUtils.setStatusBarColor(mDialog.getWindow(), R.color.status_bar_tint);
+ getAdapter().removeItemsSelected();
+ mActionMode = null;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
+ actionMode.setTitle(getString(
+ R.string.site_settings_list_editor_action_mode_title,
+ getAdapter().getItemsSelected().size())
+ );
+ return true;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java
new file mode 100644
index 000000000..0dc980e2d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java
@@ -0,0 +1,871 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.TextUtils;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.models.SiteSettingsModel;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPPrefUtils;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.ApiHelper.Param;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for WordPress (.com and .org) Site Settings. The {@link SiteSettingsModel} class is
+ * used to store the following settings:
+ *
+ * - Title
+ * - Tagline
+ * - Address
+ * - Privacy
+ * - Language
+ * - Username (.org only)
+ * - Password (.org only)
+ * - Location (local device setting, not saved remotely)
+ * - Default Category
+ * - Default Format
+ * - Related Posts
+ * - Allow Comments
+ * - Send Pingbacks
+ * - Receive Pingbacks
+ * - Identity Required
+ * - User Account Required
+ * - Close Comments After
+ * - Comment Sort Order
+ * - Comment Threading
+ * - Comment Paging
+ * - Comment User Whitelist
+ * - Comment Link Limit
+ * - Comment Moderation Hold Filter
+ * - Comment Blacklist Filter
+ *
+ * This class is marked abstract. This is due to the fact that .org (self-hosted) and .com sites
+ * expose different API's to query and edit their respective settings (even though the options
+ * offered by each is roughly the same). To get an instance of this interface class use the
+ * {@link SiteSettingsInterface#getInterface(Activity, Blog, SiteSettingsListener)} method. It will
+ * determine which interface ({@link SelfHostedSiteSettings} or {@link DotComSiteSettings}) is
+ * appropriate for the given blog.
+ */
+
+public abstract class SiteSettingsInterface {
+
+ /**
+ * Name of the {@link SharedPreferences} that is used to store local settings.
+ */
+ public static final String SITE_SETTINGS_PREFS = "site-settings-prefs";
+
+ /**
+ * Key used to access the language preference stored in {@link SharedPreferences}.
+ */
+ public static final String LANGUAGE_PREF_KEY = "site-settings-language-pref";
+
+ /**
+ * Key used to access the location preference stored in {@link SharedPreferences}.
+ */
+ public static final String LOCATION_PREF_KEY = "site-settings-location-pref";
+
+ /**
+ * Key used to access the default category preference stored in {@link SharedPreferences}.
+ */
+ public static final String DEF_CATEGORY_PREF_KEY = "site-settings-category-pref";
+
+ /**
+ * Key used to access the default post format preference stored in {@link SharedPreferences}.
+ */
+ public static final String DEF_FORMAT_PREF_KEY = "site-settings-format-pref";
+
+ /**
+ * Identifies an Ascending (oldest to newest) sort order.
+ */
+ public static final int ASCENDING_SORT = 0;
+
+ /**
+ * Identifies an Descending (newest to oldest) sort order.
+ */
+ public static final int DESCENDING_SORT = 1;
+
+ /**
+ * Used to prefix keys in an analytics property list.
+ */
+ protected static final String SAVED_ITEM_PREFIX = "item_saved_";
+
+ /**
+ * Key for the Standard post format. Used as default if post format is not set/known.
+ */
+ private static final String STANDARD_POST_FORMAT_KEY = "standard";
+
+ /**
+ * Standard post format value. Used as default display value if post format is unknown.
+ */
+ private static final String STANDARD_POST_FORMAT = "Standard";
+
+ /**
+ * Instantiates the appropriate (self-hosted or .com) SiteSettingsInterface.
+ */
+ public static SiteSettingsInterface getInterface(Activity host, Blog blog, SiteSettingsListener listener) {
+ if (host == null || blog == null) return null;
+
+ if (blog.isDotcomFlag()) {
+ return new DotComSiteSettings(host, blog, listener);
+ } else {
+ return new SelfHostedSiteSettings(host, blog, listener);
+ }
+ }
+
+ /**
+ * Returns an instance of the {@link this#SITE_SETTINGS_PREFS} {@link SharedPreferences}.
+ */
+ public static SharedPreferences siteSettingsPreferences(Context context) {
+ return context.getSharedPreferences(SITE_SETTINGS_PREFS, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Gets the geo-tagging value stored in {@link SharedPreferences}, false by default.
+ */
+ public static boolean getGeotagging(Context context) {
+ return siteSettingsPreferences(context).getBoolean(LOCATION_PREF_KEY, false);
+ }
+
+ /**
+ * Gets the default category value stored in {@link SharedPreferences}, 0 by default.
+ */
+ public static String getDefaultCategory(Context context) {
+ int id = siteSettingsPreferences(context).getInt(DEF_CATEGORY_PREF_KEY, 0);
+
+ if (id != 0) {
+ CategoryModel category = new CategoryModel();
+ Cursor cursor = SiteSettingsTable.getCategory(id);
+ if (cursor != null && cursor.moveToFirst()) {
+ category.deserializeFromDatabase(cursor);
+ return category.name;
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * Gets the default post format value stored in {@link SharedPreferences}, "" by default.
+ */
+ public static String getDefaultFormat(Context context) {
+ return siteSettingsPreferences(context).getString(DEF_FORMAT_PREF_KEY, "");
+ }
+
+ /**
+ * Thrown when provided credentials are not valid.
+ */
+ public class AuthenticationError extends Exception { }
+
+ /**
+ * Interface callbacks for settings events.
+ */
+ public interface SiteSettingsListener {
+ /**
+ * Called when settings have been updated with remote changes.
+ *
+ * @param error
+ * null if successful
+ */
+ void onSettingsUpdated(Exception error);
+
+ /**
+ * Called when attempt to update remote settings is finished.
+ *
+ * @param error
+ * null if successful
+ */
+ void onSettingsSaved(Exception error);
+
+ /**
+ * Called when a request to validate current credentials has completed.
+ *
+ * @param error
+ * null if successful
+ */
+ void onCredentialsValidated(Exception error);
+ }
+
+ /**
+ * {@link SiteSettingsInterface} implementations should use this method to start a background
+ * task to load settings data from a remote source.
+ */
+ protected abstract void fetchRemoteData();
+
+ protected final Activity mActivity;
+ protected final Blog mBlog;
+ protected final SiteSettingsListener mListener;
+ protected final SiteSettingsModel mSettings;
+ protected final SiteSettingsModel mRemoteSettings;
+
+ private final Map<String, String> mLanguageCodes;
+
+ protected SiteSettingsInterface(Activity host, Blog blog, SiteSettingsListener listener) {
+ mActivity = host;
+ mBlog = blog;
+ mListener = listener;
+ mSettings = new SiteSettingsModel();
+ mRemoteSettings = new SiteSettingsModel();
+ mLanguageCodes = WPPrefUtils.generateLanguageMap(host);
+ }
+
+ public void saveSettings() {
+ SiteSettingsTable.saveSettings(mSettings);
+ siteSettingsPreferences(mActivity).edit().putString(LANGUAGE_PREF_KEY, mSettings.language).apply();
+ siteSettingsPreferences(mActivity).edit().putBoolean(LOCATION_PREF_KEY, mSettings.location).apply();
+ siteSettingsPreferences(mActivity).edit().putInt(DEF_CATEGORY_PREF_KEY, mSettings.defaultCategory).apply();
+ siteSettingsPreferences(mActivity).edit().putString(DEF_FORMAT_PREF_KEY, mSettings.defaultPostFormat).apply();
+ }
+
+ public @NonNull String getTitle() {
+ return mSettings.title == null ? "" : mSettings.title;
+ }
+
+ public @NonNull String getTagline() {
+ return mSettings.tagline == null ? "" : mSettings.tagline;
+ }
+
+ public @NonNull String getAddress() {
+ return mSettings.address == null ? "" : mSettings.address;
+ }
+
+ public int getPrivacy() {
+ return mSettings.privacy;
+ }
+
+ public @NonNull String getPrivacyDescription() {
+ if (mActivity != null) {
+ switch (getPrivacy()) {
+ case -1:
+ return mActivity.getString(R.string.site_settings_privacy_private_summary);
+ case 0:
+ return mActivity.getString(R.string.site_settings_privacy_hidden_summary);
+ case 1:
+ return mActivity.getString(R.string.site_settings_privacy_public_summary);
+ }
+ }
+ return "";
+ }
+
+ public @NonNull String getLanguageCode() {
+ return mSettings.language == null ? "" : mSettings.language;
+ }
+
+ public @NonNull String getUsername() {
+ return mSettings.username == null ? "" : mSettings.username;
+ }
+
+ public @NonNull String getPassword() {
+ return mSettings.password == null ? "" : mSettings.password;
+ }
+
+ public boolean getLocation() {
+ return mSettings.location;
+ }
+
+ public @NonNull Map<String, String> getFormats() {
+ if (mSettings.postFormats == null) mSettings.postFormats = new HashMap<>();
+ return mSettings.postFormats;
+ }
+
+ public @NonNull CategoryModel[] getCategories() {
+ if (mSettings.categories == null) mSettings.categories = new CategoryModel[0];
+ return mSettings.categories;
+ }
+
+ public @NonNull Map<Integer, String> getCategoryNames() {
+ Map<Integer, String> categoryNames = new HashMap<>();
+ if (mSettings.categories != null && mSettings.categories.length > 0) {
+ for (CategoryModel model : mSettings.categories) {
+ categoryNames.put(model.id, Html.fromHtml(model.name).toString());
+ }
+ }
+
+ return categoryNames;
+ }
+
+ public int getDefaultCategory() {
+ return mSettings.defaultCategory;
+ }
+
+ public @NonNull String getDefaultCategoryForDisplay() {
+ for (CategoryModel model : getCategories()) {
+ if (model != null && model.id == getDefaultCategory()) {
+ return Html.fromHtml(model.name).toString();
+ }
+ }
+
+ return "";
+ }
+
+ public @NonNull String getDefaultPostFormat() {
+ if (TextUtils.isEmpty(mSettings.defaultPostFormat) || !getFormats().containsKey(mSettings.defaultPostFormat)) {
+ mSettings.defaultPostFormat = STANDARD_POST_FORMAT_KEY;
+ }
+ return mSettings.defaultPostFormat;
+ }
+
+ public @NonNull String getDefaultPostFormatDisplay() {
+ String defaultFormat = getFormats().get(getDefaultPostFormat());
+ if (TextUtils.isEmpty(defaultFormat)) defaultFormat = STANDARD_POST_FORMAT;
+ return defaultFormat;
+ }
+
+ public boolean getShowRelatedPosts() {
+ return mSettings.showRelatedPosts;
+ }
+
+ public boolean getShowRelatedPostHeader() {
+ return mSettings.showRelatedPostHeader;
+ }
+
+ public boolean getShowRelatedPostImages() {
+ return mSettings.showRelatedPostImages;
+ }
+
+ public @NonNull String getRelatedPostsDescription() {
+ if (mActivity == null) return "";
+ String desc = mActivity.getString(getShowRelatedPosts() ? R.string.on : R.string.off);
+ return StringUtils.capitalize(desc);
+ }
+
+ public boolean getAllowComments() {
+ return mSettings.allowComments;
+ }
+
+ public boolean getSendPingbacks() {
+ return mSettings.sendPingbacks;
+ }
+
+ public boolean getReceivePingbacks() {
+ return mSettings.receivePingbacks;
+ }
+
+ public boolean getShouldCloseAfter() {
+ return mSettings.shouldCloseAfter;
+ }
+
+ public int getCloseAfter() {
+ return mSettings.closeCommentAfter;
+ }
+
+ public @NonNull String getCloseAfterDescriptionForPeriod() {
+ return getCloseAfterDescriptionForPeriod(getCloseAfter());
+ }
+
+ public int getCloseAfterPeriodForDescription() {
+ return !getShouldCloseAfter() ? 0 : getCloseAfter();
+ }
+
+ public @NonNull String getCloseAfterDescription() {
+ return getCloseAfterDescriptionForPeriod(getCloseAfterPeriodForDescription());
+ }
+
+ public @NonNull String getCloseAfterDescriptionForPeriod(int period) {
+ if (mActivity == null) return "";
+
+ if (!getShouldCloseAfter()) return mActivity.getString(R.string.never);
+
+ return StringUtils.getQuantityString(mActivity, R.string.never, R.string.days_quantity_one,
+ R.string.days_quantity_other, period);
+ }
+
+ public int getCommentSorting() {
+ return mSettings.sortCommentsBy;
+ }
+
+ public @NonNull String getSortingDescription() {
+ if (mActivity == null) return "";
+
+ int order = getCommentSorting();
+ switch (order) {
+ case SiteSettingsInterface.ASCENDING_SORT:
+ return mActivity.getString(R.string.oldest_first);
+ case SiteSettingsInterface.DESCENDING_SORT:
+ return mActivity.getString(R.string.newest_first);
+ default:
+ return mActivity.getString(R.string.unknown);
+ }
+ }
+
+ public boolean getShouldThreadComments() {
+ return mSettings.shouldThreadComments;
+ }
+
+ public int getThreadingLevels() {
+ return mSettings.threadingLevels;
+ }
+
+ public int getThreadingLevelsForDescription() {
+ return !getShouldThreadComments() ? 1 : getThreadingLevels();
+ }
+
+ public @NonNull String getThreadingDescription() {
+ return getThreadingDescriptionForLevel(getThreadingLevelsForDescription());
+ }
+
+ public @NonNull String getThreadingDescriptionForLevel(int level) {
+ if (mActivity == null) return "";
+
+ if (level <= 1) return mActivity.getString(R.string.none);
+ return String.format(mActivity.getString(R.string.site_settings_threading_summary), level);
+ }
+
+ public boolean getShouldPageComments() {
+ return mSettings.shouldPageComments;
+ }
+
+ public int getPagingCount() {
+ return mSettings.commentsPerPage;
+ }
+
+ public int getPagingCountForDescription() {
+ return !getShouldPageComments() ? 0 : getPagingCount();
+ }
+
+ public @NonNull String getPagingDescription() {
+ if (mActivity == null) return "";
+
+ if (!getShouldPageComments()) {
+ return mActivity.getString(R.string.disabled);
+ }
+
+ int count = getPagingCountForDescription();
+ return StringUtils.getQuantityString(mActivity, R.string.none, R.string.site_settings_paging_summary_one,
+ R.string.site_settings_paging_summary_other, count);
+ }
+
+ public boolean getManualApproval() {
+ return mSettings.commentApprovalRequired;
+ }
+
+ public boolean getIdentityRequired() {
+ return mSettings.commentsRequireIdentity;
+ }
+
+ public boolean getUserAccountRequired() {
+ return mSettings.commentsRequireUserAccount;
+ }
+
+ public boolean getUseCommentWhitelist() {
+ return mSettings.commentAutoApprovalKnownUsers;
+ }
+
+ public int getMultipleLinks() {
+ return mSettings.maxLinks;
+ }
+
+ public @NonNull List<String> getModerationKeys() {
+ if (mSettings.holdForModeration == null) mSettings.holdForModeration = new ArrayList<>();
+ return mSettings.holdForModeration;
+ }
+
+ public @NonNull String getModerationHoldDescription() {
+ return getKeysDescription(getModerationKeys().size());
+ }
+
+ public @NonNull List<String> getBlacklistKeys() {
+ if (mSettings.blacklist == null) mSettings.blacklist = new ArrayList<>();
+ return mSettings.blacklist;
+ }
+
+ public @NonNull String getBlacklistDescription() {
+ return getKeysDescription(getBlacklistKeys().size());
+ }
+
+ public @NonNull String getKeysDescription(int count) {
+ if (mActivity == null) return "";
+
+ return StringUtils.getQuantityString(mActivity, R.string.site_settings_list_editor_no_items_text,
+ R.string.site_settings_list_editor_summary_one,
+ R.string.site_settings_list_editor_summary_other, count);
+
+ }
+
+ public void setTitle(String title) {
+ mSettings.title = title;
+ }
+
+ public void setTagline(String tagline) {
+ mSettings.tagline = tagline;
+ }
+
+ public void setAddress(String address) {
+ mSettings.address = address;
+ }
+
+ public void setPrivacy(int privacy) {
+ mSettings.privacy = privacy;
+ }
+
+ public boolean setLanguageCode(String languageCode) {
+ if (!mLanguageCodes.containsKey(languageCode) ||
+ TextUtils.isEmpty(mLanguageCodes.get(languageCode))) return false;
+ mSettings.language = languageCode;
+ mSettings.languageId = Integer.valueOf(mLanguageCodes.get(languageCode));
+ return true;
+ }
+
+ public void setLanguageId(int languageId) {
+ // want to prevent O(n) language code lookup if there is no change
+ if (mSettings.languageId != languageId) {
+ mSettings.languageId = languageId;
+ mSettings.language = languageIdToLanguageCode(Integer.toString(languageId));
+ }
+ }
+
+ public void setUsername(String username) {
+ mSettings.username = username;
+ }
+
+ public void setPassword(String password) {
+ mSettings.password = password;
+ }
+
+ public void setLocation(boolean location) {
+ mSettings.location = location;
+ }
+
+ public void setAllowComments(boolean allowComments) {
+ mSettings.allowComments = allowComments;
+ }
+
+ public void setSendPingbacks(boolean sendPingbacks) {
+ mSettings.sendPingbacks = sendPingbacks;
+ }
+
+ public void setReceivePingbacks(boolean receivePingbacks) {
+ mSettings.receivePingbacks = receivePingbacks;
+ }
+
+ public void setShouldCloseAfter(boolean shouldCloseAfter) {
+ mSettings.shouldCloseAfter = shouldCloseAfter;
+ }
+
+ public void setCloseAfter(int period) {
+ mSettings.closeCommentAfter = period;
+ }
+
+ public void setCommentSorting(int method) {
+ mSettings.sortCommentsBy = method;
+ }
+
+ public void setShouldThreadComments(boolean shouldThread) {
+ mSettings.shouldThreadComments = shouldThread;
+ }
+
+ public void setThreadingLevels(int levels) {
+ mSettings.threadingLevels = levels;
+ }
+
+ public void setShouldPageComments(boolean shouldPage) {
+ mSettings.shouldPageComments= shouldPage;
+ }
+
+ public void setPagingCount(int count) {
+ mSettings.commentsPerPage = count;
+ }
+
+ public void setManualApproval(boolean required) {
+ mSettings.commentApprovalRequired = required;
+ }
+
+ public void setIdentityRequired(boolean required) {
+ mSettings.commentsRequireIdentity = required;
+ }
+
+ public void setUserAccountRequired(boolean required) {
+ mSettings.commentsRequireUserAccount = required;
+ }
+
+ public void setUseCommentWhitelist(boolean useWhitelist) {
+ mSettings.commentAutoApprovalKnownUsers = useWhitelist;
+ }
+
+ public void setMultipleLinks(int count) {
+ mSettings.maxLinks = count;
+ }
+
+ public void setModerationKeys(List<String> keys) {
+ mSettings.holdForModeration = keys;
+ }
+
+ public void setBlacklistKeys(List<String> keys) {
+ mSettings.blacklist = keys;
+ }
+
+ public void setDefaultCategory(int category) {
+ mSettings.defaultCategory = category;
+ }
+
+ /**
+ * Sets the default post format.
+ *
+ * @param format
+ * if null or empty default format is set to {@link SiteSettingsInterface#STANDARD_POST_FORMAT_KEY}
+ */
+ public void setDefaultFormat(String format) {
+ if (TextUtils.isEmpty(format)) {
+ mSettings.defaultPostFormat = STANDARD_POST_FORMAT_KEY;
+ } else {
+ mSettings.defaultPostFormat = format.toLowerCase();
+ }
+ }
+
+ public void setShowRelatedPosts(boolean relatedPosts) {
+ mSettings.showRelatedPosts = relatedPosts;
+ }
+
+ public void setShowRelatedPostHeader(boolean showHeader) {
+ mSettings.showRelatedPostHeader = showHeader;
+ }
+
+ public void setShowRelatedPostImages(boolean showImages) {
+ mSettings.showRelatedPostImages = showImages;
+ }
+
+ /**
+ * Determines if the current Moderation Hold list contains a given value.
+ */
+ public boolean moderationHoldListContains(String value) {
+ return getModerationKeys().contains(value);
+ }
+
+ /**
+ * Determines if the current Blacklist list contains a given value.
+ */
+ public boolean blacklistListContains(String value) {
+ return getBlacklistKeys().contains(value);
+ }
+
+ /**
+ * Checks if the provided list of post format IDs is the same (order dependent) as the current
+ * list of Post Formats in the local settings object.
+ *
+ * @param ids
+ * an array of post format IDs
+ * @return
+ * true unless the provided IDs are different from the current IDs or in a different order
+ */
+ public boolean isSameFormatList(CharSequence[] ids) {
+ if (ids == null) return mSettings.postFormats == null;
+ if (mSettings.postFormats == null || ids.length != mSettings.postFormats.size()) return false;
+
+ String[] keys = mSettings.postFormats.keySet().toArray(new String[mSettings.postFormats.size()]);
+ for (int i = 0; i < ids.length; ++i) {
+ if (!keys[i].equals(ids[i])) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the provided list of category IDs is the same (order dependent) as the current
+ * list of Categories in the local settings object.
+ *
+ * @param ids
+ * an array of integers stored as Strings (for convenience)
+ * @return
+ * true unless the provided IDs are different from the current IDs or in a different order
+ */
+ public boolean isSameCategoryList(CharSequence[] ids) {
+ if (ids == null) return mSettings.categories == null;
+ if (mSettings.categories == null || ids.length != mSettings.categories.length) return false;
+
+ for (int i = 0; i < ids.length; ++i) {
+ if (Integer.valueOf(ids[i].toString()) != mSettings.categories[i].id) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Needed so that subclasses can be created before initializing. The final member variables
+ * are null until object has been created so XML-RPC callbacks will not run.
+ *
+ * @return
+ * returns itself for the convenience of
+ * {@link SiteSettingsInterface#getInterface(Activity, Blog, SiteSettingsListener)}
+ */
+ public SiteSettingsInterface init(boolean fetchRemote) {
+ loadCachedSettings();
+
+ if (fetchRemote) {
+ fetchRemoteData();
+ fetchPostFormats();
+ }
+
+ return this;
+ }
+
+ /**
+ * If there is a change in verification status the listener is notified.
+ */
+ protected void credentialsVerified(boolean valid) {
+ Exception e = valid ? null : new AuthenticationError();
+ if (mSettings.hasVerifiedCredentials != valid) notifyCredentialsVerifiedOnUiThread(e);
+ mRemoteSettings.hasVerifiedCredentials = mSettings.hasVerifiedCredentials = valid;
+ }
+
+ /**
+ * Helper method to create an XML-RPC interface for the current blog.
+ */
+ protected XMLRPCClientInterface instantiateInterface() {
+ if (mBlog == null) return null;
+ return XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(), mBlog.getHttppassword());
+ }
+
+ /**
+ * Language IDs, used only by WordPress, are integer values that map to a language code.
+ * https://github.com/Automattic/calypso-pre-oss/blob/72c2029b0805a73b749a2b64dd1d8655cae528d0/config/production.json#L86-L227
+ *
+ * Language codes are unique two-letter identifiers defined by ISO 639-1. Region dialects can
+ * be defined by appending a -** where ** is the region code (en-GB -> English, Great Britain).
+ * https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+ */
+ protected String languageIdToLanguageCode(String id) {
+ if (id != null) {
+ for (String key : mLanguageCodes.keySet()) {
+ if (id.equals(mLanguageCodes.get(key))) {
+ return key;
+ }
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * Need to defer loading the cached settings to a thread so it completes after initialization.
+ */
+ private void loadCachedSettings() {
+ Cursor localSettings = SiteSettingsTable.getSettings(mBlog.getRemoteBlogId());
+
+ if (localSettings != null) {
+ Map<Integer, CategoryModel> cachedModels = SiteSettingsTable.getAllCategories();
+ mSettings.deserializeOptionsDatabaseCursor(localSettings, cachedModels);
+ mSettings.language = languageIdToLanguageCode(Integer.toString(mSettings.languageId));
+ if (mSettings.language == null) {
+ setLanguageCode(LanguageUtils.getPatchedCurrentDeviceLanguage(null));
+ }
+ mRemoteSettings.language = mSettings.language;
+ mRemoteSettings.languageId = mSettings.languageId;
+ mRemoteSettings.location = mSettings.location;
+ localSettings.close();
+ notifyUpdatedOnUiThread(null);
+ } else {
+ mSettings.isInLocalTable = false;
+ setAddress(mBlog.getHomeURL());
+ setUsername(mBlog.getUsername());
+ setPassword(mBlog.getPassword());
+ setTitle(mBlog.getBlogName());
+ }
+ }
+
+ /**
+ * Gets available post formats via XML-RPC. Since both self-hosted and .com sites retrieve the
+ * format list via XML-RPC there is no need to implement this in the sub-classes.
+ */
+ private void fetchPostFormats() {
+ XMLRPCClientInterface client = instantiateInterface();
+ if (client == null) return;
+
+ Map<String, String> args = new HashMap<>();
+ args.put(Param.SHOW_SUPPORTED_POST_FORMATS, "true");
+ Object[] params = { mBlog.getRemoteBlogId(), mBlog.getUsername(),
+ mBlog.getPassword(), args};
+ client.callAsync(new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, Object result) {
+ credentialsVerified(true);
+
+ if (result != null && result instanceof HashMap) {
+ Map<?, ?> resultMap = (HashMap<?, ?>) result;
+ Map allFormats;
+ Object[] supportedFormats;
+ if (resultMap.containsKey("supported")) {
+ allFormats = (Map) resultMap.get("all");
+ supportedFormats = (Object[]) resultMap.get("supported");
+ } else {
+ allFormats = resultMap;
+ supportedFormats = allFormats.keySet().toArray();
+ }
+
+ mRemoteSettings.postFormats = new HashMap<>();
+ mRemoteSettings.postFormats.put("standard", "Standard");
+ for (Object supportedFormat : supportedFormats) {
+ if (allFormats.containsKey(supportedFormat)) {
+ mRemoteSettings.postFormats.put(supportedFormat.toString(), allFormats.get(supportedFormat).toString());
+ }
+ }
+ mSettings.postFormats = new HashMap<>(mRemoteSettings.postFormats);
+ SiteSettingsTable.saveSettings(mSettings);
+
+ notifyUpdatedOnUiThread(null);
+ }
+ }
+
+ @Override
+ public void onFailure(long id, Exception error) {
+ }
+ }, Method.GET_POST_FORMATS, params);
+ }
+
+ /**
+ * Notifies listener that credentials have been validated or are incorrect.
+ */
+ private void notifyCredentialsVerifiedOnUiThread(final Exception error) {
+ if (mActivity == null || mListener == null) return;
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onCredentialsValidated(error);
+ }
+ });
+ }
+
+ /**
+ * Notifies listener that settings have been updated with the latest remote data.
+ */
+ protected void notifyUpdatedOnUiThread(final Exception error) {
+ if (mActivity == null || mActivity.isFinishing() || mListener == null) return;
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onSettingsUpdated(error);
+ }
+ });
+ }
+
+ /**
+ * Notifies listener that settings have been saved or an error occurred while saving.
+ */
+ protected void notifySavedOnUiThread(final Exception error) {
+ if (mActivity == null || mListener == null) return;
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onSettingsSaved(error);
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java
new file mode 100644
index 000000000..9a4b04467
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java
@@ -0,0 +1,58 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * LinearLayoutManager with smooth scrolling and custom duration (in milliseconds).
+ */
+public class SmoothScrollLinearLayoutManager extends LinearLayoutManager {
+ private final int mDuration;
+
+ public SmoothScrollLinearLayoutManager(Context context, int orientation, boolean reverseLayout, int duration) {
+ super(context, orientation, reverseLayout);
+ this.mDuration = duration;
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+ final View firstVisibleChild = recyclerView.getChildAt(0);
+ final int itemHeight = firstVisibleChild.getHeight();
+ final int currentPosition = recyclerView.getChildPosition(firstVisibleChild);
+ int distanceInPixels = Math.abs((currentPosition - position) * itemHeight);
+
+ if (distanceInPixels == 0) {
+ distanceInPixels = (int) Math.abs(firstVisibleChild.getY());
+ }
+
+ final SmoothScroller smoothScroller = new SmoothScroller(recyclerView.getContext(), distanceInPixels, mDuration);
+ smoothScroller.setTargetPosition(position);
+ startSmoothScroll(smoothScroller);
+ }
+
+ private class SmoothScroller extends LinearSmoothScroller {
+ private final float mDistanceInPixels;
+ private final float mDuration;
+
+ public SmoothScroller(Context context, int distanceInPixels, int duration) {
+ super(context);
+ this.mDistanceInPixels = distanceInPixels;
+ this.mDuration = duration;
+ }
+
+ @Override
+ protected int calculateTimeForScrolling(int distance) {
+ final float proportion = (float) distance / mDistanceInPixels;
+ return (int) (mDuration * proportion);
+ }
+
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ return SmoothScrollLinearLayoutManager.this.computeScrollVectorForPosition(targetPosition);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java
new file mode 100644
index 000000000..b3998ab24
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java
@@ -0,0 +1,211 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+/**
+ * Standard EditTextPreference that has attributes to limit summary length.
+ *
+ * Created for and used by {@link SiteSettingsFragment} to style some Preferences.
+ *
+ * When declaring this class in a layout file you can use the following attributes:
+ * - app:summaryLines : sets the number of lines to display in the Summary field
+ * (see {@link TextView#setLines(int)} for details)
+ * - app:maxSummaryLines : sets the maximum number of lines the Summary field can display
+ * (see {@link TextView#setMaxLines(int)} for details)
+ * - app:longClickHint : sets the string to be shown in a Toast when preference is long clicked
+ */
+
+public class SummaryEditTextPreference extends EditTextPreference implements PreferenceHint {
+ private int mLines;
+ private int mMaxLines;
+ private String mHint;
+ private AlertDialog mDialog;
+ private int mWhichButtonClicked;
+
+ public SummaryEditTextPreference(Context context) {
+ super(context);
+ }
+
+ public SummaryEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public SummaryEditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mLines = -1;
+ mMaxLines = -1;
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.SummaryEditTextPreference_summaryLines) {
+ mLines = array.getInt(index, -1);
+ } else if (index == R.styleable.SummaryEditTextPreference_maxSummaryLines) {
+ mMaxLines = array.getInt(index, -1);
+ } else if (index == R.styleable.SummaryEditTextPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ TextView titleView = (TextView) view.findViewById(android.R.id.title);
+ TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
+
+ if (titleView != null) WPPrefUtils.layoutAsSubhead(titleView);
+
+ if (summaryView != null) {
+ WPPrefUtils.layoutAsBody1(summaryView);
+ summaryView.setEllipsize(TextUtils.TruncateAt.END);
+ summaryView.setInputType(getEditText().getInputType());
+ if (mLines != -1) summaryView.setLines(mLines);
+ if (mMaxLines != -1) summaryView.setMaxLines(mMaxLines);
+ }
+ }
+
+ @Override
+ public Dialog getDialog() {
+ return mDialog;
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ Context context = getContext();
+ Resources res = context.getResources();
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Calypso_AlertDialog);
+ View titleView = View.inflate(getContext(), R.layout.detail_list_preference_title, null);
+ mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(res.getString(R.string.cancel).toUpperCase(), this);
+ if (titleView != null) {
+ TextView titleText = (TextView) titleView.findViewById(R.id.title);
+ if (titleText != null) {
+ titleText.setText(getTitle());
+ }
+
+ builder.setCustomTitle(titleView);
+ } else {
+ builder.setTitle(getTitle());
+ }
+
+ View view = View.inflate(getContext(), getDialogLayoutResource(), null);
+ if (view != null) {
+ onBindDialogView(view);
+ builder.setView(view);
+ }
+
+ if ((mDialog = builder.create()) == null) return;
+
+ if (state != null) {
+ mDialog.onRestoreInstanceState(state);
+ }
+ mDialog.setOnDismissListener(this);
+ mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ mDialog.show();
+
+ Button positive = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ @Override
+ protected void onBindDialogView(final View view) {
+ super.onBindDialogView(view);
+ if (view == null) return;
+
+ EditText editText = getEditText();
+ ViewParent oldParent = editText.getParent();
+ if (oldParent != view) {
+ if (oldParent != null && oldParent instanceof ViewGroup) {
+ ViewGroup groupParent = (ViewGroup) oldParent;
+ groupParent.removeView(editText);
+ groupParent.setPadding(groupParent.getPaddingLeft(), 0, groupParent.getPaddingRight(), groupParent.getPaddingBottom());
+ }
+ onAddEditTextToDialogView(view, editText);
+ }
+ WPPrefUtils.layoutAsInput(editText);
+ editText.setSelection(editText.getText().length());
+
+ TextView message = (TextView) view.findViewById(android.R.id.message);
+ WPPrefUtils.layoutAsDialogMessage(message);
+
+ // Dialog message has some extra bottom margin we don't want
+ ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) message.getLayoutParams();
+ int leftMargin = 0;
+ int bottomMargin = view.getResources().getDimensionPixelSize(R.dimen.margin_small);
+ // Different versions handle the message view's margin differently
+ // This is a small hack to try to make it align with the input for earlier versions
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ leftMargin = view.getResources().getDimensionPixelSize(R.dimen.margin_small);
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ leftMargin = view.getResources().getDimensionPixelSize(R.dimen.margin_large);
+ }
+ layoutParams.setMargins(leftMargin, layoutParams.topMargin, layoutParams.rightMargin, bottomMargin);
+ message.setLayoutParams(layoutParams);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+ if (positiveResult) {
+ callChangeListener(getEditText().getText());
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java
new file mode 100644
index 000000000..47ca17b6d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java
@@ -0,0 +1,65 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.preference.Preference;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+public class WPPreference extends Preference implements PreferenceHint {
+ private String mHint;
+
+ public WPPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DetailListPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.DetailListPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ Resources res = getContext().getResources();
+ TextView titleView = (TextView) view.findViewById(android.R.id.title);
+ TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
+ if (titleView != null) {
+ titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_large));
+ titleView.setTextColor(res.getColor(isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10));
+ }
+ if (summaryView != null) {
+ summaryView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_medium));
+ summaryView.setTextColor(res.getColor(isEnabled() ? R.color.grey_darken_10 : R.color.grey_lighten_10));
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java
new file mode 100644
index 000000000..0fed50c24
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java
@@ -0,0 +1,82 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.graphics.drawable.VectorDrawableCompat;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.UrlUtils;
+
+/**
+ * Calypso-style Preference that has an icon and a widget in the correct place. If there is a button
+ * with id R.id.button, an onPreferenceClick listener is added.
+ */
+
+public class WPStartOverPreference extends WPPreference {
+ private String mButtonText;
+ private int mButtonTextColor;
+ private boolean mButtonTextAllCaps;
+ private Drawable mPrefIcon;
+
+ public WPStartOverPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.WPStartOverPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.WPStartOverPreference_buttonText) {
+ mButtonText = array.getString(index);
+ } else if (index == R.styleable.WPStartOverPreference_buttonTextColor) {
+ mButtonTextColor = array.getColor(index, ContextCompat.getColor(context, R.color.black));
+ } else if (index == R.styleable.WPStartOverPreference_buttonTextAllCaps) {
+ mButtonTextAllCaps = array.getBoolean(index, false);
+ } else if (index == R.styleable.WPStartOverPreference_preficon) {
+ mPrefIcon = VectorDrawableCompat.create(context.getResources(), array.getResourceId(index, 0), null);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ if (view.findViewById(R.id.pref_icon) != null) {
+ ImageView imageView = (ImageView) view.findViewById(R.id.pref_icon);
+ imageView.setImageDrawable(mPrefIcon);
+ }
+
+ if (view.findViewById(R.id.button) != null) {
+ final WPStartOverPreference wpStartOverPreference = this;
+
+ Button button = (Button) view.findViewById(R.id.button);
+ button.setText(mButtonText);
+ button.setTextColor(mButtonTextColor);
+ button.setAllCaps(mButtonTextAllCaps);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getOnPreferenceClickListener().onPreferenceClick(wpStartOverPreference);
+ }
+ });
+ }
+
+ if (view.findViewById(R.id.domain) != null) {
+ TextView textView = (TextView) view.findViewById(R.id.domain);
+ Blog blog = WordPress.getCurrentBlog();
+ textView.setText(UrlUtils.getHost(blog.getHomeURL()));
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java
new file mode 100644
index 000000000..f895e21f7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.preference.SwitchPreference;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+public class WPSwitchPreference extends SwitchPreference implements PreferenceHint {
+ private String mHint;
+
+ public WPSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.SummaryEditTextPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ TextView titleView = (TextView) view.findViewById(android.R.id.title);
+ if (titleView != null) {
+ Resources res = getContext().getResources();
+ titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_large));
+ titleView.setTextColor(res.getColor(isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10));
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java
new file mode 100644
index 000000000..a86a698bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.ui.prefs.notifications;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+
+import de.greenrobot.event.EventBus;
+
+// Simple wrapper activity for NotificationsSettingsFragment
+public class NotificationsSettingsActivity extends AppCompatActivity {
+ private View mMessageContainer;
+ private TextView mMessageTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ setContentView(R.layout.notifications_settings_activity);
+
+ setTitle(R.string.notification_settings);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ if (savedInstanceState == null) {
+ fragmentManager.beginTransaction()
+ .add(R.id.fragment_container, new NotificationsSettingsFragment())
+ .commit();
+ }
+
+ mMessageContainer = findViewById(R.id.notifications_settings_message_container);
+ mMessageTextView = (TextView)findViewById(R.id.notifications_settings_message);
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(NotificationEvents.NotificationsSettingsStatusChanged event) {
+ if (TextUtils.isEmpty(event.getMessage())) {
+ mMessageContainer.setVisibility(View.GONE);
+ } else {
+ mMessageContainer.setVisibility(View.VISIBLE);
+ mMessageTextView.setText(event.getMessage());
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java
new file mode 100644
index 000000000..5a34316b6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java
@@ -0,0 +1,171 @@
+package org.wordpress.android.ui.prefs.notifications;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.models.NotificationsSettings;
+import org.wordpress.android.models.NotificationsSettings.Channel;
+import org.wordpress.android.models.NotificationsSettings.Type;
+import org.wordpress.android.ui.stats.ScrollViewExt;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.Iterator;
+
+// A dialog preference that displays settings for a NotificationSettings Channel and Type
+public class NotificationsSettingsDialogPreference extends DialogPreference {
+ private static final String SETTING_VALUE_ACHIEVEMENT = "achievement";
+
+ private NotificationsSettings.Channel mChannel;
+ private NotificationsSettings.Type mType;
+ private NotificationsSettings mSettings;
+ private JSONObject mUpdatedJson = new JSONObject();
+ private long mBlogId;
+
+ private OnNotificationsSettingsChangedListener mOnNotificationsSettingsChangedListener;
+
+ public interface OnNotificationsSettingsChangedListener {
+ void onSettingsChanged(Channel channel, Type type, long siteId, JSONObject newValues);
+ }
+
+ public NotificationsSettingsDialogPreference(Context context, AttributeSet attrs, Channel channel,
+ Type type, long blogId, NotificationsSettings settings,
+ OnNotificationsSettingsChangedListener listener) {
+ super(context, attrs);
+
+ mChannel = channel;
+ mType = type;
+ mBlogId = blogId;
+ mSettings = settings;
+ mOnNotificationsSettingsChangedListener = listener;
+ }
+
+ @Override
+ protected void onBindDialogView(@NonNull View view) {
+ super.onBindDialogView(view);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+
+ ScrollView outerView = new ScrollView(getContext());
+ outerView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+
+ LinearLayout innerView = new LinearLayout(getContext());
+ innerView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
+ innerView.setOrientation(LinearLayout.VERTICAL);
+
+ View spacerView = new View(getContext());
+ int spacerHeight = getContext().getResources().getDimensionPixelSize(R.dimen.margin_medium);
+ spacerView.setLayoutParams(new ViewGroup.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, spacerHeight));
+ innerView.addView(spacerView);
+
+ outerView.addView(innerView);
+ configureLayoutForView(innerView);
+
+ return outerView;
+ }
+
+ private View configureLayoutForView(LinearLayout view) {
+ JSONObject settingsJson = null;
+
+ String[] settingsArray = new String[0], settingsValues = new String[0], summaryArray = new String[0];
+ String typeString = mType.toString();
+
+ switch (mChannel) {
+ case BLOGS:
+ settingsJson = JSONUtils.queryJSON(mSettings.getBlogSettings().get(mBlogId),
+ typeString, new JSONObject());
+ settingsArray = getContext().getResources().getStringArray(R.array.notifications_blog_settings);
+ settingsValues = getContext().getResources().getStringArray(R.array.notifications_blog_settings_values);
+ break;
+ case OTHER:
+ settingsJson = JSONUtils.queryJSON(mSettings.getOtherSettings(),
+ typeString, new JSONObject());
+ settingsArray = getContext().getResources().getStringArray(R.array.notifications_other_settings);
+ settingsValues = getContext().getResources().getStringArray(R.array.notifications_other_settings_values);
+ break;
+ case DOTCOM:
+ settingsJson = mSettings.getDotcomSettings();
+ settingsArray = getContext().getResources().getStringArray(R.array.notifications_wpcom_settings);
+ settingsValues = getContext().getResources().getStringArray(R.array.notifications_wpcom_settings_values);
+ summaryArray = getContext().getResources().getStringArray(R.array.notifications_wpcom_settings_summaries);
+ break;
+ }
+
+ if (settingsJson != null && settingsArray.length == settingsValues.length) {
+ for (int i = 0; i < settingsArray.length; i++) {
+ String settingName = settingsArray[i];
+ String settingValue = settingsValues[i];
+
+ // Skip a few settings for 'Email' section
+ if (mType == Type.EMAIL && settingValue.equals(SETTING_VALUE_ACHIEVEMENT)) {
+ continue;
+ }
+
+ View commentsSetting = View.inflate(getContext(), R.layout.notifications_settings_switch, null);
+ TextView title = (TextView) commentsSetting.findViewById(R.id.notifications_switch_title);
+ title.setText(settingName);
+
+ // Add special summary text for the DOTCOM section
+ if (mChannel == Channel.DOTCOM && i < summaryArray.length) {
+ String summaryText = summaryArray[i];
+ TextView summary = (TextView) commentsSetting.findViewById(R.id.notifications_switch_summary);
+ summary.setVisibility(View.VISIBLE);
+ summary.setText(summaryText);
+ }
+
+ Switch toggleSwitch = (Switch) commentsSetting.findViewById(R.id.notifications_switch);
+ toggleSwitch.setChecked(JSONUtils.queryJSON(settingsJson, settingValue, true));
+ toggleSwitch.setTag(settingValue);
+ toggleSwitch.setOnCheckedChangeListener(mOnCheckedChangedListener);
+
+ view.addView(commentsSetting);
+ }
+ }
+
+ return view;
+ }
+
+ private CompoundButton.OnCheckedChangeListener mOnCheckedChangedListener = new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
+ try {
+ mUpdatedJson.put(compoundButton.getTag().toString(), isChecked);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not add notification setting change to JSONObject");
+ }
+ }
+ };
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ if (positiveResult && mUpdatedJson.length() > 0 && mOnNotificationsSettingsChangedListener != null) {
+ mOnNotificationsSettingsChangedListener.onSettingsChanged(mChannel, mType, mBlogId, mUpdatedJson);
+
+ // Update the settings json
+ Iterator<?> keys = mUpdatedJson.keys();
+ while( keys.hasNext() ) {
+ String settingName = (String)keys.next();
+ mSettings.updateSettingForChannelAndType(
+ mChannel, mType, settingName,
+ mUpdatedJson.optBoolean(settingName), mBlogId
+ );
+ }
+
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java
new file mode 100644
index 000000000..a0b47e7ac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java
@@ -0,0 +1,451 @@
+package org.wordpress.android.ui.prefs.notifications;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.NotificationsSettings;
+import org.wordpress.android.models.NotificationsSettings.Channel;
+import org.wordpress.android.models.NotificationsSettings.Type;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPActivityUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsSettingsFragment extends PreferenceFragment {
+
+ private static final String KEY_SEARCH_QUERY = "search_query";
+ private static final int SITE_SEARCH_VISIBILITY_COUNT = 15;
+ // The number of notification types we support (e.g. timeline, email, mobile)
+ private static final int TYPE_COUNT = 3;
+
+ private NotificationsSettings mNotificationsSettings;
+ private SearchView mSearchView;
+ private MenuItem mSearchMenuItem;
+
+ private String mDeviceId;
+ private String mRestoredQuery;
+ private boolean mNotificationsEnabled;
+ private int mSiteCount;
+
+ private final List<PreferenceCategory> mTypePreferenceCategories = new ArrayList<>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.notifications_settings);
+ setHasOptionsMenu(true);
+
+ // Bump Analytics
+ if (savedInstanceState == null) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_LIST_OPENED);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ mDeviceId = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, "");
+
+ if (hasNotificationsSettings()) {
+ loadNotificationsAndUpdateUI(true);
+ }
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SEARCH_QUERY)) {
+ mRestoredQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
+ }
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ mNotificationsEnabled = NotificationsUtils.isNotificationsEnabled(getActivity());
+
+ refreshSettings();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.notifications_settings, menu);
+
+ mSearchMenuItem = menu.findItem(R.id.menu_notifications_settings_search);
+ mSearchView = (SearchView) MenuItemCompat.getActionView(mSearchMenuItem);
+ mSearchView.setQueryHint(getString(R.string.search_sites));
+
+ mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ configureBlogsSettings();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ configureBlogsSettings();
+ return true;
+ }
+ });
+
+ updateSearchMenuVisibility();
+
+ // Check for a restored search query (if device was rotated, etc)
+ if (!TextUtils.isEmpty(mRestoredQuery)) {
+ mSearchMenuItem.expandActionView();
+ mSearchView.setQuery(mRestoredQuery, true);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) {
+ outState.putString(KEY_SEARCH_QUERY, mSearchView.getQuery().toString());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private void refreshSettings() {
+ if (!hasNotificationsSettings()) {
+ EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(getString(R.string.loading)));
+ }
+
+ if (hasNotificationsSettings()) {
+ updateUIForNotificationsEnabledState();
+ }
+
+ NotificationsUtils.getPushNotificationSettings(getActivity(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(T.NOTIFS, "Get settings action succeeded");
+ if (!isAdded()) return;
+
+ boolean settingsExisted = hasNotificationsSettings();
+ if (!settingsExisted) {
+ EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(null));
+ }
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, response.toString());
+ editor.apply();
+
+ loadNotificationsAndUpdateUI(!settingsExisted);
+ updateUIForNotificationsEnabledState();
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (!isAdded()) return;
+ AppLog.e(T.NOTIFS, "Get settings action failed", error);
+
+ if (!hasNotificationsSettings()) {
+ EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(getString(R.string.error_loading_notifications)));
+ }
+ }
+ });
+ }
+
+ private void loadNotificationsAndUpdateUI(boolean shouldUpdateUI) {
+ JSONObject settingsJson;
+ try {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ settingsJson = new JSONObject(
+ sharedPreferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, "")
+ );
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not parse notifications settings JSON");
+ return;
+ }
+
+ if (mNotificationsSettings == null) {
+ mNotificationsSettings = new NotificationsSettings(settingsJson);
+ } else {
+ mNotificationsSettings.updateJson(settingsJson);
+ }
+
+ if (shouldUpdateUI) {
+ configureBlogsSettings();
+ configureOtherSettings();
+ configureDotcomSettings();
+ }
+ }
+
+ private boolean hasNotificationsSettings() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+
+ return sharedPreferences.contains(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS);
+ }
+
+ // Updates the UI for preference screens based on if notifications are enabled or not
+ private void updateUIForNotificationsEnabledState() {
+ if (mTypePreferenceCategories == null || mTypePreferenceCategories.size() == 0) {
+ return;
+ }
+
+ for (final PreferenceCategory category : mTypePreferenceCategories) {
+ if (mNotificationsEnabled && category.getPreferenceCount() > TYPE_COUNT) {
+ category.removePreference(category.getPreference(TYPE_COUNT));
+ } else if (!mNotificationsEnabled && category.getPreferenceCount() == TYPE_COUNT) {
+ Preference disabledMessage = new Preference(getActivity());
+ disabledMessage.setSummary(R.string.notifications_disabled);
+ disabledMessage.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ Uri uri = Uri.fromParts("package", getActivity().getApplicationContext().getPackageName(), null);
+ intent.setData(uri);
+
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ category.addPreference(disabledMessage);
+ }
+
+ if (category.getPreferenceCount() >= TYPE_COUNT &&
+ category.getPreference(TYPE_COUNT - 1) != null) {
+ category.getPreference(TYPE_COUNT - 1).setEnabled(mNotificationsEnabled);
+ }
+ }
+
+ }
+
+ private void configureBlogsSettings() {
+ if (!isAdded()) return;
+ // Retrieve blogs (including jetpack sites) originally retrieved through FetchBlogListWPCom
+ // They will have an empty (but encrypted) password
+ String args = "password='" + WordPressDB.encryptPassword("") + "'";
+
+ // Check if user has typed in a search query
+ String trimmedQuery = null;
+ if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) {
+ trimmedQuery = mSearchView.getQuery().toString().trim();
+ args += " AND (url LIKE '%" + trimmedQuery + "%' OR blogName LIKE '%" + trimmedQuery + "%')";
+ }
+
+ List<Map<String, Object>> blogs = WordPress.wpDB.getBlogsBy(args, null, 0, false);
+ mSiteCount = blogs.size();
+
+ Context context = getActivity();
+
+ PreferenceCategory blogsCategory = (PreferenceCategory) findPreference(
+ getString(R.string.pref_notification_blogs));
+ blogsCategory.removeAll();
+
+ for (Map blog : blogs) {
+ if (context == null) return;
+
+ String siteUrl = MapUtils.getMapStr(blog, "url");
+ String title = MapUtils.getMapStr(blog, "blogName");
+ long blogId = MapUtils.getMapLong(blog, "blogId");
+
+ PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(context);
+ prefScreen.setTitle(title);
+ prefScreen.setSummary(UrlUtils.getHost(siteUrl));
+
+ addPreferencesForPreferenceScreen(prefScreen, Channel.BLOGS, blogId);
+ blogsCategory.addPreference(prefScreen);
+ }
+
+ // Add a message in a preference if there are no matching search results
+ if (mSiteCount == 0 && !TextUtils.isEmpty(trimmedQuery)) {
+ Preference searchResultsPref = new Preference(context);
+ searchResultsPref.setSummary(String.format(getString(R.string.notifications_no_search_results), trimmedQuery));
+ blogsCategory.addPreference(searchResultsPref);
+ }
+
+ updateSearchMenuVisibility();
+ }
+
+ private void updateSearchMenuVisibility() {
+ // Show the search menu item in the toolbar if we have enough sites
+ if (mSearchMenuItem != null) {
+ mSearchMenuItem.setVisible(mSiteCount > SITE_SEARCH_VISIBILITY_COUNT);
+ }
+ }
+
+ private void configureOtherSettings() {
+ PreferenceScreen otherBlogsScreen = (PreferenceScreen) findPreference(
+ getString(R.string.pref_notification_other_blogs));
+ addPreferencesForPreferenceScreen(otherBlogsScreen, Channel.OTHER, 0);
+ }
+
+ private void configureDotcomSettings() {
+ PreferenceCategory otherPreferenceCategory = (PreferenceCategory) findPreference(
+ getString(R.string.pref_notification_other_category));
+ NotificationsSettingsDialogPreference devicePreference = new NotificationsSettingsDialogPreference(
+ getActivity(), null, Channel.DOTCOM, NotificationsSettings.Type.DEVICE, 0, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ devicePreference.setTitle(R.string.notifications_account_emails);
+ devicePreference.setDialogTitle(R.string.notifications_account_emails);
+ devicePreference.setSummary(R.string.notifications_account_emails_summary);
+ otherPreferenceCategory.addPreference(devicePreference);
+ }
+
+ private void addPreferencesForPreferenceScreen(PreferenceScreen preferenceScreen, Channel channel, long blogId) {
+ Context context = getActivity();
+ if (context == null) return;
+
+ PreferenceCategory rootCategory = new PreferenceCategory(context);
+ rootCategory.setTitle(R.string.notification_types);
+ preferenceScreen.addPreference(rootCategory);
+
+ NotificationsSettingsDialogPreference timelinePreference = new NotificationsSettingsDialogPreference(
+ context, null, channel, NotificationsSettings.Type.TIMELINE, blogId, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ timelinePreference.setIcon(R.drawable.ic_bell_grey);
+ timelinePreference.setTitle(R.string.notifications_tab);
+ timelinePreference.setDialogTitle(R.string.notifications_tab);
+ timelinePreference.setSummary(R.string.notifications_tab_summary);
+ rootCategory.addPreference(timelinePreference);
+
+ NotificationsSettingsDialogPreference emailPreference = new NotificationsSettingsDialogPreference(
+ context, null, channel, NotificationsSettings.Type.EMAIL, blogId, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ emailPreference.setIcon(R.drawable.ic_email_grey);
+ emailPreference.setTitle(R.string.email);
+ emailPreference.setDialogTitle(R.string.email);
+ emailPreference.setSummary(R.string.notifications_email_summary);
+ rootCategory.addPreference(emailPreference);
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ String deviceID = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, null);
+ if (!TextUtils.isEmpty(deviceID)) {
+ NotificationsSettingsDialogPreference devicePreference = new NotificationsSettingsDialogPreference(
+ context, null, channel, NotificationsSettings.Type.DEVICE, blogId, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ devicePreference.setIcon(R.drawable.ic_phone_grey);
+ devicePreference.setTitle(R.string.app_notifications);
+ devicePreference.setDialogTitle(R.string.app_notifications);
+ devicePreference.setSummary(R.string.notifications_push_summary);
+ devicePreference.setEnabled(mNotificationsEnabled);
+ rootCategory.addPreference(devicePreference);
+ }
+
+ mTypePreferenceCategories.add(rootCategory);
+ }
+
+ private final NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener mOnSettingsChangedListener =
+ new NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onSettingsChanged(Channel channel, NotificationsSettings.Type type, long blogId, JSONObject newValues) {
+ if (!isAdded()) return;
+
+ // Construct a new settings JSONObject to send back to WP.com
+ JSONObject settingsObject = new JSONObject();
+ switch (channel) {
+ case BLOGS:
+ try {
+ JSONObject blogObject = new JSONObject();
+ blogObject.put(NotificationsSettings.KEY_BLOG_ID, blogId);
+
+ JSONArray blogsArray = new JSONArray();
+ if (type == Type.DEVICE) {
+ newValues.put(NotificationsSettings.KEY_DEVICE_ID, Long.parseLong(mDeviceId));
+ JSONArray devicesArray = new JSONArray();
+ devicesArray.put(newValues);
+ blogObject.put(NotificationsSettings.KEY_DEVICES, devicesArray);
+ blogsArray.put(blogObject);
+ } else {
+ blogObject.put(type.toString(), newValues);
+ blogsArray.put(blogObject);
+ }
+
+ settingsObject.put(NotificationsSettings.KEY_BLOGS, blogsArray);
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not build notification settings object");
+ }
+ break;
+ case OTHER:
+ try {
+ JSONObject otherObject = new JSONObject();
+ if (type == Type.DEVICE) {
+ newValues.put(NotificationsSettings.KEY_DEVICE_ID, Long.parseLong(mDeviceId));
+ JSONArray devicesArray = new JSONArray();
+ devicesArray.put(newValues);
+ otherObject.put(NotificationsSettings.KEY_DEVICES, devicesArray);
+ } else {
+ otherObject.put(type.toString(), newValues);
+ }
+
+ settingsObject.put(NotificationsSettings.KEY_OTHER, otherObject);
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not build notification settings object");
+ }
+ break;
+ case DOTCOM:
+ try {
+ settingsObject.put(NotificationsSettings.KEY_DOTCOM, newValues);
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not build notification settings object");
+ }
+ break;
+ }
+
+ if (settingsObject.length() > 0) {
+ WordPress.getRestClientUtilsV1_1().post("/me/notifications/settings", settingsObject, null, null, null);
+ }
+ }
+ };
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, @NonNull Preference preference) {
+ super.onPreferenceTreeClick(preferenceScreen, preference);
+
+ if (preference instanceof PreferenceScreen) {
+ Dialog prefDialog = ((PreferenceScreen) preference).getDialog();
+ if (prefDialog != null) {
+ String title = String.valueOf(preference.getTitle());
+ WPActivityUtils.addToolbarToDialog(this, prefDialog, title);
+ }
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_STREAMS_OPENED);
+ } else {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_DETAILS_OPENED);
+ }
+
+ return false;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java
new file mode 100644
index 000000000..2767bc5dc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java
@@ -0,0 +1,263 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.text.TextUtils;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPUrlUtils;
+import org.wordpress.passcodelock.AppLockManager;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ReaderActivityLauncher {
+
+ /*
+ * show a single reader post in the detail view - simply calls showReaderPostPager
+ * with a single post
+ */
+ public static void showReaderPostDetail(Context context, long blogId, long postId) {
+ showReaderPostDetail(context, blogId, postId, false);
+ }
+ public static void showReaderPostDetail(Context context,
+ long blogId,
+ long postId,
+ boolean isRelatedPost) {
+ Intent intent = new Intent(context, ReaderPostPagerActivity.class);
+ intent.putExtra(ReaderConstants.ARG_BLOG_ID, blogId);
+ intent.putExtra(ReaderConstants.ARG_POST_ID, postId);
+ intent.putExtra(ReaderConstants.ARG_IS_SINGLE_POST, true);
+ intent.putExtra(ReaderConstants.ARG_IS_RELATED_POST, isRelatedPost);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show pager view of posts with a specific tag - passed blogId/postId is the post
+ * to select after the pager is populated
+ */
+ public static void showReaderPostPagerForTag(Context context,
+ ReaderTag tag,
+ ReaderPostListType postListType,
+ long blogId,
+ long postId) {
+ if (tag == null) {
+ return;
+ }
+
+ Intent intent = new Intent(context, ReaderPostPagerActivity.class);
+ intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, postListType);
+ intent.putExtra(ReaderConstants.ARG_TAG, tag);
+ intent.putExtra(ReaderConstants.ARG_BLOG_ID, blogId);
+ intent.putExtra(ReaderConstants.ARG_POST_ID, postId);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show pager view of posts in a specific blog
+ */
+ public static void showReaderPostPagerForBlog(Context context,
+ long blogId,
+ long postId) {
+ Intent intent = new Intent(context, ReaderPostPagerActivity.class);
+ intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
+ intent.putExtra(ReaderConstants.ARG_BLOG_ID, blogId);
+ intent.putExtra(ReaderConstants.ARG_POST_ID, postId);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show a list of posts in a specific blog
+ */
+ public static void showReaderBlogPreview(Context context, long blogId) {
+ if (blogId == 0) {
+ return;
+ }
+
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_PREVIEWED, blogId);
+ Intent intent = new Intent(context, ReaderPostListActivity.class);
+ intent.putExtra(ReaderConstants.ARG_BLOG_ID, blogId);
+ intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
+ context.startActivity(intent);
+ }
+
+ public static void showReaderBlogPreview(Context context, ReaderPost post) {
+ if (post == null) {
+ return;
+ }
+ if (post.isExternal) {
+ showReaderFeedPreview(context, post.feedId);
+ } else {
+ showReaderBlogPreview(context, post.blogId);
+ }
+ }
+
+ public static void showReaderFeedPreview(Context context, long feedId) {
+ if (feedId == 0) {
+ return;
+ }
+
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_PREVIEWED);
+ Intent intent = new Intent(context, ReaderPostListActivity.class);
+ intent.putExtra(ReaderConstants.ARG_FEED_ID, feedId);
+ intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show a list of posts with a specific tag
+ */
+ public static void showReaderTagPreview(Context context, ReaderTag tag) {
+ if (tag == null) {
+ return;
+ }
+ Map<String, String> properties = new HashMap<>();
+ properties.put("tag", tag.getTagSlug());
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_TAG_PREVIEWED, properties);
+ Intent intent = new Intent(context, ReaderPostListActivity.class);
+ intent.putExtra(ReaderConstants.ARG_TAG, tag);
+ intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.TAG_PREVIEW);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show comments for the passed Ids
+ */
+ public static void showReaderComments(Context context, long blogId, long postId) {
+ showReaderComments(context, blogId, postId, 0);
+ }
+
+
+ /*
+ * Show comments for passed Ids. Passing a commentId will scroll that comment into view
+ */
+ public static void showReaderComments(Context context, long blogId, long postId, long commentId) {
+ Intent intent = new Intent(context, ReaderCommentListActivity.class);
+ intent.putExtra(ReaderConstants.ARG_BLOG_ID, blogId);
+ intent.putExtra(ReaderConstants.ARG_POST_ID, postId);
+ intent.putExtra(ReaderConstants.ARG_COMMENT_ID, commentId);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show users who liked a post
+ */
+ public static void showReaderLikingUsers(Context context, long blogId, long postId) {
+ Intent intent = new Intent(context, ReaderUserListActivity.class);
+ intent.putExtra(ReaderConstants.ARG_BLOG_ID, blogId);
+ intent.putExtra(ReaderConstants.ARG_POST_ID, postId);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show followed tags & blogs
+ */
+ public static void showReaderSubs(Context context) {
+ Intent intent = new Intent(context, ReaderSubsActivity.class);
+ context.startActivity(intent);
+ }
+
+ /*
+ * show the passed imageUrl in the fullscreen photo activity - optional content is the
+ * content of the post the image is in, used by the activity to show all images in
+ * the post
+ */
+ public enum PhotoViewerOption {
+ IS_PRIVATE_IMAGE,
+ IS_GALLERY_IMAGE
+ }
+ public static void showReaderPhotoViewer(Context context,
+ String imageUrl,
+ String content,
+ View sourceView,
+ EnumSet<PhotoViewerOption> imageOptions,
+ int startX,
+ int startY) {
+ if (context == null || TextUtils.isEmpty(imageUrl)) {
+ return;
+ }
+
+ boolean isPrivate = imageOptions != null && imageOptions.contains(PhotoViewerOption.IS_PRIVATE_IMAGE);
+ boolean isGallery = imageOptions != null && imageOptions.contains(PhotoViewerOption.IS_GALLERY_IMAGE);
+
+ Intent intent = new Intent(context, ReaderPhotoViewerActivity.class);
+ intent.putExtra(ReaderConstants.ARG_IMAGE_URL, imageUrl);
+ intent.putExtra(ReaderConstants.ARG_IS_PRIVATE, isPrivate);
+ intent.putExtra(ReaderConstants.ARG_IS_GALLERY, isGallery);
+ if (!TextUtils.isEmpty(content)) {
+ intent.putExtra(ReaderConstants.ARG_CONTENT, content);
+ }
+
+ if (context instanceof Activity) {
+ Activity activity = (Activity) context;
+ ActivityOptionsCompat options =
+ ActivityOptionsCompat.makeScaleUpAnimation(sourceView, startX, startY, 0, 0);
+ ActivityCompat.startActivity(activity, intent, options.toBundle());
+ } else {
+ context.startActivity(intent);
+ }
+ }
+ public static void showReaderPhotoViewer(Context context,
+ String imageUrl,
+ EnumSet<PhotoViewerOption> imageOptions) {
+ showReaderPhotoViewer(context, imageUrl, null, null, imageOptions, 0, 0);
+ }
+
+ public enum OpenUrlType { INTERNAL, EXTERNAL }
+ public static void openUrl(Context context, String url) {
+ openUrl(context, url, OpenUrlType.INTERNAL);
+ }
+ public static void openUrl(Context context, String url, OpenUrlType openUrlType) {
+ if (context == null || TextUtils.isEmpty(url)) return;
+
+ if (openUrlType == OpenUrlType.INTERNAL) {
+ openUrlInternal(context, url);
+ } else {
+ openUrlExternal(context, url);
+ }
+ }
+
+ /*
+ * open the passed url in the app's internal WebView activity
+ */
+ private static void openUrlInternal(Context context, @NonNull String url) {
+ // That won't work on wpcom sites with custom urls
+ if (WPUrlUtils.isWordPressCom(url)) {
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(context, url,
+ AccountHelper.getDefaultAccount().getUserName());
+ } else {
+ WPWebViewActivity.openURL(context, url, ReaderConstants.HTTP_REFERER_URL);
+ }
+ }
+
+ /*
+ * open the passed url in the device's external browser
+ */
+ private static void openUrlExternal(Context context, @NonNull String url) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ context.startActivity(intent);
+ AppLockManager.getInstance().setExtendedTimeout();
+ } catch (ActivityNotFoundException e) {
+ String readerToastErrorUrlIntent = context.getString(R.string.reader_toast_err_url_intent);
+ ToastUtils.showToast(context, String.format(readerToastErrorUrlIntent, url), ToastUtils.Duration.LONG);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderAnim.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderAnim.java
new file mode 100644
index 000000000..922c29faf
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderAnim.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.reader;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.os.Build;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import org.wordpress.android.util.AniUtils;
+
+public class ReaderAnim {
+
+ /*
+ * animation when user taps a like button
+ */
+ private enum ReaderButton { LIKE_ON, LIKE_OFF}
+ public static void animateLikeButton(final View target, boolean isAskingToLike) {
+ animateButton(target, isAskingToLike ? ReaderButton.LIKE_ON : ReaderButton.LIKE_OFF);
+ }
+ private static void animateButton(final View target, ReaderButton button) {
+ if (target == null || button == null) {
+ return;
+ }
+
+ ObjectAnimator animX = ObjectAnimator.ofFloat(target, View.SCALE_X, 1f, 1.2f);
+ animX.setRepeatMode(ValueAnimator.REVERSE);
+ animX.setRepeatCount(1);
+
+ ObjectAnimator animY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 1f, 1.2f);
+ animY.setRepeatMode(ValueAnimator.REVERSE);
+ animY.setRepeatCount(1);
+
+ AnimatorSet set = new AnimatorSet();
+
+ switch (button) {
+ case LIKE_ON: case LIKE_OFF:
+ // rotate like button +/- 72 degrees (72 = 360/5, 5 is the number of points in the star)
+ float endRotate = (button == ReaderButton.LIKE_ON ? 72f : -72f);
+ ObjectAnimator animRotate = ObjectAnimator.ofFloat(target, View.ROTATION, 0f, endRotate);
+ animRotate.setRepeatMode(ValueAnimator.REVERSE);
+ animRotate.setRepeatCount(1);
+ set.play(animX).with(animY).with(animRotate);
+ // on Android 4.4.3 the rotation animation may cause the drawable to fade out unless
+ // we set the layer type - https://code.google.com/p/android/issues/detail?id=70914
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
+ target.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+ break;
+ default:
+ set.play(animX).with(animY);
+ break;
+ }
+
+ long durationMillis = AniUtils.Duration.SHORT.toMillis(target.getContext());
+ set.setDuration(durationMillis);
+ set.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ set.start();
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderBlogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderBlogFragment.java
new file mode 100644
index 000000000..8c526b606
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderBlogFragment.java
@@ -0,0 +1,175 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderRecommendedBlog;
+import org.wordpress.android.ui.reader.adapters.ReaderBlogAdapter;
+import org.wordpress.android.ui.reader.adapters.ReaderBlogAdapter.ReaderBlogType;
+import org.wordpress.android.ui.reader.views.ReaderRecyclerView;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.WPActivityUtils;
+
+/*
+ * fragment hosted by ReaderSubsActivity which shows either recommended blogs and followed blogs
+ */
+public class ReaderBlogFragment extends Fragment
+ implements ReaderBlogAdapter.BlogClickListener {
+ private ReaderRecyclerView mRecyclerView;
+ private ReaderBlogAdapter mAdapter;
+ private ReaderBlogType mBlogType;
+ private boolean mWasPaused;
+ private static final String ARG_BLOG_TYPE = "blog_type";
+
+ static ReaderBlogFragment newInstance(ReaderBlogType blogType) {
+ AppLog.d(AppLog.T.READER, "reader blog fragment > newInstance");
+ Bundle args = new Bundle();
+ args.putSerializable(ARG_BLOG_TYPE, blogType);
+ ReaderBlogFragment fragment = new ReaderBlogFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void setArguments(Bundle args) {
+ super.setArguments(args);
+ restoreState(args);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ AppLog.d(AppLog.T.READER, "reader blog fragment > restoring instance state");
+ restoreState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.reader_fragment_list, container, false);
+ mRecyclerView = (ReaderRecyclerView) view.findViewById(R.id.recycler_view);
+ return view;
+ }
+
+ private void checkEmptyView() {
+ if (!isAdded()) return;
+
+ TextView emptyView = (TextView) getView().findViewById(R.id.text_empty);
+ if (emptyView == null) return;
+
+ boolean isEmpty = hasBlogAdapter() && getBlogAdapter().isEmpty();
+ if (isEmpty) {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ emptyView.setText(R.string.reader_empty_recommended_blogs);
+ break;
+ case FOLLOWED:
+ emptyView.setText(R.string.reader_empty_followed_blogs_title);
+ break;
+ }
+ }
+ emptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mRecyclerView.setAdapter(getBlogAdapter());
+ refresh();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putSerializable(ARG_BLOG_TYPE, getBlogType());
+ outState.putBoolean(ReaderConstants.KEY_WAS_PAUSED, mWasPaused);
+ super.onSaveInstanceState(outState);
+ }
+
+ private void restoreState(Bundle args) {
+ if (args != null) {
+ mWasPaused = args.getBoolean(ReaderConstants.KEY_WAS_PAUSED);
+ if (args.containsKey(ARG_BLOG_TYPE)) {
+ mBlogType = (ReaderBlogType) args.getSerializable(ARG_BLOG_TYPE);
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mWasPaused = true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // refresh the adapter if the fragment is resuming from a paused state so that changes
+ // made in another activity (such as follow state) are reflected here
+ if (mWasPaused) {
+ mWasPaused = false;
+ refresh();
+ }
+ }
+
+ void refresh() {
+ if (hasBlogAdapter()) {
+ AppLog.d(AppLog.T.READER, "reader subs > refreshing blog fragment " + getBlogType().name());
+ getBlogAdapter().refresh();
+ }
+ }
+
+ private boolean hasBlogAdapter() {
+ return (mAdapter != null);
+ }
+
+ private ReaderBlogAdapter getBlogAdapter() {
+ if (mAdapter == null) {
+ Context context = WPActivityUtils.getThemedContext(getActivity());
+ mAdapter = new ReaderBlogAdapter(context, getBlogType());
+ mAdapter.setBlogClickListener(this);
+ mAdapter.setDataLoadedListener(new ReaderInterfaces.DataLoadedListener() {
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ checkEmptyView();
+ }
+ });
+
+ }
+ return mAdapter;
+ }
+
+ public ReaderBlogType getBlogType() {
+ return mBlogType;
+ }
+
+ @Override
+ public void onBlogClicked(Object item) {
+ long blogId;
+ long feedId;
+ if (item instanceof ReaderRecommendedBlog) {
+ ReaderRecommendedBlog blog = (ReaderRecommendedBlog) item;
+ blogId = blog.blogId;
+ feedId = 0;
+ } else if (item instanceof ReaderBlog) {
+ ReaderBlog blog = (ReaderBlog) item;
+ blogId = blog.blogId;
+ feedId = blog.feedId;
+ } else {
+ return;
+ }
+
+ if (feedId != 0) {
+ ReaderActivityLauncher.showReaderFeedPreview(getActivity(), feedId);
+ } else if (blogId != 0) {
+ ReaderActivityLauncher.showReaderBlogPreview(getActivity(), blogId);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java
new file mode 100644
index 000000000..1f15a87f6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java
@@ -0,0 +1,547 @@
+package org.wordpress.android.ui.reader;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.SuggestionTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderCommentActions;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.reader.adapters.ReaderCommentAdapter;
+import org.wordpress.android.ui.reader.services.ReaderCommentService;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.views.ReaderRecyclerView;
+import org.wordpress.android.ui.suggestion.adapters.SuggestionAdapter;
+import org.wordpress.android.ui.suggestion.service.SuggestionEvents;
+import org.wordpress.android.ui.suggestion.util.SuggestionServiceConnectionManager;
+import org.wordpress.android.ui.suggestion.util.SuggestionUtils;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.wordpress.android.widgets.RecyclerItemDecoration;
+import org.wordpress.android.widgets.SuggestionAutoCompleteText;
+
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderCommentListActivity extends AppCompatActivity {
+
+ private static final String KEY_REPLY_TO_COMMENT_ID = "reply_to_comment_id";
+ private static final String KEY_HAS_UPDATED_COMMENTS = "has_updated_comments";
+
+ private long mPostId;
+ private long mBlogId;
+ private ReaderPost mPost;
+ private ReaderCommentAdapter mCommentAdapter;
+ private SuggestionAdapter mSuggestionAdapter;
+ private SuggestionServiceConnectionManager mSuggestionServiceConnectionManager;
+
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private ReaderRecyclerView mRecyclerView;
+ private SuggestionAutoCompleteText mEditComment;
+ private View mSubmitReplyBtn;
+ private ViewGroup mCommentBox;
+
+ private boolean mIsUpdatingComments;
+ private boolean mHasUpdatedComments;
+ private boolean mIsSubmittingComment;
+ private long mReplyToCommentId;
+ private long mCommentId;
+ private int mRestorePosition;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.reader_activity_comment_list);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ if (toolbar != null) {
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
+ }
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (savedInstanceState != null) {
+ mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
+ mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID);
+ mRestorePosition = savedInstanceState.getInt(ReaderConstants.KEY_RESTORE_POSITION);
+ mHasUpdatedComments = savedInstanceState.getBoolean(KEY_HAS_UPDATED_COMMENTS);
+ } else {
+ mBlogId = getIntent().getLongExtra(ReaderConstants.ARG_BLOG_ID, 0);
+ mPostId = getIntent().getLongExtra(ReaderConstants.ARG_POST_ID, 0);
+ mCommentId = getIntent().getLongExtra(ReaderConstants.ARG_COMMENT_ID, 0);
+ // we need to re-request comments every time this activity is shown in order to
+ // correctly reflect deletions and nesting changes - skipped when there's no
+ // connection so we can show existing comments while offline
+ if (NetworkUtils.isNetworkAvailable(this)) {
+ ReaderCommentTable.purgeCommentsForPost(mBlogId, mPostId);
+ }
+ }
+
+
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this,
+ (CustomSwipeRefreshLayout) findViewById(R.id.swipe_to_refresh),
+ new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ updatePostAndComments();
+ }
+ });
+
+ mRecyclerView = (ReaderRecyclerView) findViewById(R.id.recycler_view);
+ int spacingHorizontal = 0;
+ int spacingVertical = DisplayUtils.dpToPx(this, 1);
+ mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
+
+ mCommentBox = (ViewGroup) findViewById(R.id.layout_comment_box);
+ mEditComment = (SuggestionAutoCompleteText) mCommentBox.findViewById(R.id.edit_comment);
+ mEditComment.getAutoSaveTextHelper().setUniqueId(
+ String.format(LanguageUtils.getCurrentDeviceLanguage(this),
+ "%s%d%d",
+ AccountHelper.getCurrentUsernameForBlog(null), mPostId, mBlogId));
+ mSubmitReplyBtn = mCommentBox.findViewById(R.id.btn_submit_reply);
+
+ if (!loadPost()) {
+ ToastUtils.showToast(this, R.string.reader_toast_err_get_post);
+ finish();
+ return;
+ }
+
+ mRecyclerView.setAdapter(getCommentAdapter());
+
+ if (savedInstanceState != null) {
+ setReplyToCommentId(savedInstanceState.getLong(KEY_REPLY_TO_COMMENT_ID));
+ }
+
+ refreshComments();
+
+ mSuggestionServiceConnectionManager = new SuggestionServiceConnectionManager(this, (int) mBlogId);
+ mSuggestionAdapter = SuggestionUtils.setupSuggestions((int) mBlogId, this, mSuggestionServiceConnectionManager,
+ mPost.isWP());
+ if (mSuggestionAdapter != null) {
+ mEditComment.setAdapter(mSuggestionAdapter);
+ }
+ }
+
+
+ private void updatePostAndComments() {
+ //to do a complete refresh we need to get updated post and new comments
+ ReaderPostActions.updatePost(mPost, new ReaderActions.UpdateResultListener() {
+ @Override
+ public void onUpdateResult(ReaderActions.UpdateResult result) {
+ if (isFinishing()) {
+ return;
+ }
+
+ if (result.isNewOrChanged()) {
+ getCommentAdapter().setPost(mPost); //pass updated post to the adapter
+ ReaderCommentTable.purgeCommentsForPost(mBlogId, mPostId); //clear all the previous comments
+ updateComments(false, false); //load first page of comments
+ } else {
+ setRefreshing(false);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ EventBus.getDefault().register(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(SuggestionEvents.SuggestionNameListUpdated event) {
+ // check if the updated suggestions are for the current blog and update the suggestions
+ if (event.mRemoteBlogId != 0 && event.mRemoteBlogId == mBlogId && mSuggestionAdapter != null) {
+ List<Suggestion> suggestions = SuggestionTable.getSuggestionsForSite(event.mRemoteBlogId);
+ mSuggestionAdapter.setSuggestionList(suggestions);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ EventBus.getDefault().unregister(this);
+ }
+
+ private void setReplyToCommentId(long commentId) {
+ mReplyToCommentId = commentId;
+ mEditComment.setHint(mReplyToCommentId == 0 ?
+ R.string.reader_hint_comment_on_post : R.string.reader_hint_comment_on_comment);
+
+ // if a comment is being replied to, highlight it and scroll it to the top so the user can
+ // see which comment they're replying to - note that scrolling is delayed to give time for
+ // listView to reposition due to soft keyboard appearing
+ if (mReplyToCommentId != 0) {
+ mEditComment.requestFocus();
+ EditTextUtils.showSoftInput(mEditComment);
+ getCommentAdapter().setHighlightCommentId(mReplyToCommentId, false);
+ mRecyclerView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ scrollToCommentId(mReplyToCommentId);
+ }
+ }, 300);
+
+ // reset to replying to the post when user hasn't entered any text and hits
+ // the back button in the editText to hide the soft keyboard
+ mEditComment.setOnBackListener(new SuggestionAutoCompleteText.OnEditTextBackListener() {
+ @Override
+ public void onEditTextBack() {
+ if (EditTextUtils.isEmpty(mEditComment)) {
+ setReplyToCommentId(0);
+ }
+ }
+ });
+ } else {
+ mEditComment.setOnBackListener(null);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putLong(ReaderConstants.ARG_BLOG_ID, mBlogId);
+ outState.putLong(ReaderConstants.ARG_POST_ID, mPostId);
+ outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, getCurrentPosition());
+ outState.putLong(KEY_REPLY_TO_COMMENT_ID, mReplyToCommentId);
+ outState.putBoolean(KEY_HAS_UPDATED_COMMENTS, mHasUpdatedComments);
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private void showCommentsClosedMessage(boolean show) {
+ TextView txtCommentsClosed = (TextView) findViewById(R.id.text_comments_closed);
+ if (txtCommentsClosed != null) {
+ txtCommentsClosed.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private boolean loadPost() {
+ mPost = ReaderPostTable.getPost(mBlogId, mPostId, true);
+ if (mPost == null) {
+ return false;
+ }
+
+ if (ReaderUtils.isLoggedOutReader()) {
+ mCommentBox.setVisibility(View.GONE);
+ showCommentsClosedMessage(false);
+ } else if (mPost.isCommentsOpen) {
+ mCommentBox.setVisibility(View.VISIBLE);
+ showCommentsClosedMessage(false);
+
+ mEditComment.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_SEND) {
+ submitComment();
+ }
+ return false;
+ }
+ });
+
+ mSubmitReplyBtn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submitComment();
+ }
+ });
+ } else {
+ mCommentBox.setVisibility(View.GONE);
+ mEditComment.setEnabled(false);
+ showCommentsClosedMessage(true);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mSuggestionServiceConnectionManager != null) {
+ mSuggestionServiceConnectionManager.unbindFromService();
+ }
+ super.onDestroy();
+ }
+
+ private boolean hasCommentAdapter() {
+ return (mCommentAdapter != null);
+ }
+
+ private ReaderCommentAdapter getCommentAdapter() {
+ if (mCommentAdapter == null) {
+ mCommentAdapter = new ReaderCommentAdapter(WPActivityUtils.getThemedContext(this), getPost());
+
+ // adapter calls this when user taps reply icon
+ mCommentAdapter.setReplyListener(new ReaderCommentAdapter.RequestReplyListener() {
+ @Override
+ public void onRequestReply(long commentId) {
+ setReplyToCommentId(commentId);
+ }
+ });
+
+ // Enable post title click if we came from notifications with a commentId
+ if (mCommentId > 0) {
+ mCommentAdapter.enableHeaderClicks();
+ }
+
+ // adapter calls this when data has been loaded & displayed
+ mCommentAdapter.setDataLoadedListener(new ReaderInterfaces.DataLoadedListener() {
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ if (!isFinishing()) {
+ if (isEmpty || !mHasUpdatedComments) {
+ updateComments(isEmpty, false);
+ } else if (mRestorePosition > 0) {
+ mRecyclerView.scrollToPosition(mRestorePosition);
+ } else if (mCommentId > 0) {
+ // Scroll to the commentId once if it was passed to this activity
+ smoothScrollToCommentId(mCommentId);
+ mCommentId = 0;
+ }
+ mRestorePosition = 0;
+ checkEmptyView();
+ }
+ }
+ });
+
+ // adapter uses this to request more comments from server when it reaches the end and
+ // detects that more comments exist on the server than are stored locally
+ mCommentAdapter.setDataRequestedListener(new ReaderActions.DataRequestedListener() {
+ @Override
+ public void onRequestData() {
+ if (!mIsUpdatingComments) {
+ AppLog.i(T.READER, "reader comments > requesting next page of comments");
+ updateComments(true, true);
+ }
+ }
+ });
+ }
+ return mCommentAdapter;
+ }
+
+ private ReaderPost getPost() {
+ return mPost;
+ }
+
+ private void showProgress() {
+ ProgressBar progress = (ProgressBar) findViewById(R.id.progress_loading);
+ if (progress != null) {
+ progress.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideProgress() {
+ ProgressBar progress = (ProgressBar) findViewById(R.id.progress_loading);
+ if (progress != null) {
+ progress.setVisibility(View.GONE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.UpdateCommentsStarted event) {
+ mIsUpdatingComments = true;
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.UpdateCommentsEnded event) {
+ if (isFinishing()) return;
+
+ mIsUpdatingComments = false;
+ mHasUpdatedComments = true;
+ hideProgress();
+
+ if (event.getResult().isNewOrChanged()) {
+ mRestorePosition = getCurrentPosition();
+ refreshComments();
+ } else {
+ checkEmptyView();
+ }
+
+ setRefreshing(false);
+ }
+
+ /*
+ * request comments for this post
+ */
+ private void updateComments(boolean showProgress, boolean requestNextPage) {
+ if (mIsUpdatingComments) {
+ AppLog.w(T.READER, "reader comments > already updating comments");
+ setRefreshing(false);
+ return;
+ }
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ AppLog.w(T.READER, "reader comments > no connection, update canceled");
+ setRefreshing(false);
+ return;
+ }
+
+ if (showProgress) {
+ showProgress();
+ }
+ ReaderCommentService.startService(this, mPost.blogId, mPost.postId, requestNextPage);
+ }
+
+ private void checkEmptyView() {
+ TextView txtEmpty = (TextView) findViewById(R.id.text_empty);
+ if (txtEmpty == null) return;
+
+ boolean isEmpty = hasCommentAdapter()
+ && getCommentAdapter().isEmpty()
+ && !mIsSubmittingComment;
+ if (isEmpty && !NetworkUtils.isNetworkAvailable(this)) {
+ txtEmpty.setText(R.string.no_network_message);
+ txtEmpty.setVisibility(View.VISIBLE);
+ } else if (isEmpty && mHasUpdatedComments) {
+ txtEmpty.setText(R.string.reader_empty_comments);
+ txtEmpty.setVisibility(View.VISIBLE);
+ } else {
+ txtEmpty.setVisibility(View.GONE);
+ }
+ }
+
+ /*
+ * refresh adapter so latest comments appear
+ */
+ private void refreshComments() {
+ AppLog.d(T.READER, "reader comments > refreshComments");
+ getCommentAdapter().refreshComments();
+ }
+
+ /*
+ * scrolls the passed comment to the top of the listView
+ */
+ private void scrollToCommentId(long commentId) {
+ int position = getCommentAdapter().positionOfCommentId(commentId);
+ if (position > -1) {
+ mRecyclerView.scrollToPosition(position);
+ }
+ }
+
+ /*
+ * Smoothly scrolls the passed comment to the top of the listView
+ */
+ private void smoothScrollToCommentId(long commentId) {
+ int position = getCommentAdapter().positionOfCommentId(commentId);
+ if (position > -1) {
+ mRecyclerView.smoothScrollToPosition(position);
+ }
+ }
+
+ /*
+ * submit the text typed into the comment box as a comment on the current post
+ */
+ private void submitComment() {
+ final String commentText = EditTextUtils.getText(mEditComment);
+ if (TextUtils.isEmpty(commentText)) {
+ return;
+ }
+
+ if (!NetworkUtils.checkConnection(this)) {
+ return;
+ }
+
+ AnalyticsUtils.trackWithReaderPostDetails(
+ AnalyticsTracker.Stat.READER_ARTICLE_COMMENTED_ON, mPost);
+
+ mSubmitReplyBtn.setEnabled(false);
+ mEditComment.setEnabled(false);
+ mIsSubmittingComment = true;
+
+ // generate a "fake" comment id to assign to the new comment so we can add it to the db
+ // and reflect it in the adapter before the API call returns
+ final long fakeCommentId = ReaderCommentActions.generateFakeCommentId();
+
+ ReaderActions.CommentActionListener actionListener = new ReaderActions.CommentActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded, ReaderComment newComment) {
+ if (isFinishing()) {
+ return;
+ }
+ mIsSubmittingComment = false;
+ mSubmitReplyBtn.setEnabled(true);
+ mEditComment.setEnabled(true);
+ if (succeeded) {
+ // stop highlighting the fake comment and replace it with the real one
+ getCommentAdapter().setHighlightCommentId(0, false);
+ getCommentAdapter().replaceComment(fakeCommentId, newComment);
+ setReplyToCommentId(0);
+ mEditComment.getAutoSaveTextHelper().clearSavedText(mEditComment);
+ } else {
+ mEditComment.setText(commentText);
+ getCommentAdapter().removeComment(fakeCommentId);
+ ToastUtils.showToast(
+ ReaderCommentListActivity.this, R.string.reader_toast_err_comment_failed, ToastUtils.Duration.LONG);
+ }
+ checkEmptyView();
+ }
+ };
+
+ ReaderComment newComment = ReaderCommentActions.submitPostComment(
+ getPost(),
+ fakeCommentId,
+ commentText,
+ mReplyToCommentId,
+ actionListener);
+
+ if (newComment != null) {
+ mEditComment.setText(null);
+ // add the "fake" comment to the adapter, highlight it, and show a progress bar
+ // next to it while it's submitted
+ getCommentAdapter().setHighlightCommentId(newComment.commentId, true);
+ getCommentAdapter().addComment(newComment);
+ // make sure it's scrolled into view
+ scrollToCommentId(fakeCommentId);
+ checkEmptyView();
+ }
+ }
+
+ private int getCurrentPosition() {
+ if (mRecyclerView != null && hasCommentAdapter()) {
+ return ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
+ } else {
+ return 0;
+ }
+ }
+
+ private void setRefreshing(boolean refreshing) {
+ mSwipeToRefreshHelper.setRefreshing(refreshing);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java
new file mode 100644
index 000000000..026d9c3f0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java
@@ -0,0 +1,53 @@
+package org.wordpress.android.ui.reader;
+
+public class ReaderConstants {
+ public static final int READER_MAX_POSTS_TO_REQUEST = 20; // max # posts to request when updating posts
+ public static final int READER_MAX_SEARCH_POSTS_TO_REQUEST = 10; // max # posts to request when searching posts
+ public static final int READER_MAX_POSTS_TO_DISPLAY = 200; // max # posts to display
+ public static final int READER_MAX_COMMENTS_TO_REQUEST = 20; // max # top-level comments to request when updating comments
+ public static final int READER_MAX_USERS_TO_DISPLAY = 500; // max # users to show in ReaderUserListActivity
+ public static final long READER_AUTO_UPDATE_DELAY_MINUTES = 10; // 10 minute delay between automatic updates
+ public static final int READER_MAX_RECOMMENDED_TO_REQUEST = 20; // max # of recommended blogs to request
+
+ public static final int MIN_FEATURED_IMAGE_WIDTH = 640; // min width for an image to be suitable featured image
+
+ public static final long DISCOVER_SITE_ID = 53424024; // site id for discover.wordpress.com
+
+ // min size for images in post content to be shown in a gallery (thumbnail strip) - matches
+ // the Calypso web reader
+ public static int MIN_GALLERY_IMAGE_WIDTH = 144;
+
+ public static final String HTTP_REFERER_URL = "https://wordpress.com"; // referrer url for reader posts opened in a browser
+
+ public static final String UNICODE_BULLET_WITH_SPACE = " \u2022 ";
+
+ // intent arguments / keys
+ static final String ARG_TAG = "tag";
+ static final String ARG_BLOG_ID = "blog_id";
+ static final String ARG_FEED_ID = "feed_id";
+ static final String ARG_POST_ID = "post_id";
+ static final String ARG_COMMENT_ID = "comment_id";
+ static final String ARG_IMAGE_URL = "image_url";
+ static final String ARG_IS_PRIVATE = "is_private";
+ static final String ARG_IS_GALLERY = "is_gallery";
+ static final String ARG_POST_LIST_TYPE = "post_list_type";
+ static final String ARG_CONTENT = "content";
+ static final String ARG_IS_SINGLE_POST = "is_single_post";
+ static final String ARG_IS_RELATED_POST = "is_related_post";
+ static final String ARG_SEARCH_QUERY = "search_query";
+
+ static final String KEY_ALREADY_UPDATED = "already_updated";
+ static final String KEY_ALREADY_REQUESTED = "already_requested";
+ static final String KEY_RESTORE_POSITION = "restore_position";
+ static final String KEY_WAS_PAUSED = "was_paused";
+ static final String KEY_ERROR_MESSAGE = "error_message";
+ static final String KEY_FIRST_LOAD = "first_load";
+
+ // JSON key names
+ // tag endpoints
+ public static final String JSON_TAG_TAGS_ARRAY = "tags";
+ public static final String JSON_TAG_TITLE = "title";
+ public static final String JSON_TAG_DISPLAY_NAME = "tag_display_name";
+ public static final String JSON_TAG_SLUG = "slug";
+ public static final String JSON_TAG_URL = "URL";
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java
new file mode 100644
index 000000000..676e40588
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java
@@ -0,0 +1,135 @@
+package org.wordpress.android.ui.reader;
+
+import android.support.annotation.NonNull;
+
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.models.ReaderRelatedPostList;
+import org.wordpress.android.ui.reader.services.ReaderPostService;
+import org.wordpress.android.util.StringUtils;
+
+/**
+ * Reader-related EventBus event classes
+ */
+public class ReaderEvents {
+
+ private ReaderEvents() {
+ throw new AssertionError();
+ }
+
+ public static class FollowedTagsChanged {}
+ public static class RecommendedTagsChanged{}
+
+ public static class TagAdded {
+ private final String mTagName;
+ public TagAdded(String tagName) {
+ mTagName = tagName;
+ }
+ public String getTagName() {
+ return StringUtils.notNullStr(mTagName);
+ }
+ }
+
+ public static class FollowedBlogsChanged {}
+ public static class RecommendedBlogsChanged {}
+
+ public static class UpdatePostsStarted {
+ private final ReaderPostService.UpdateAction mAction;
+ public UpdatePostsStarted(ReaderPostService.UpdateAction action) {
+ mAction = action;
+ }
+ public ReaderPostService.UpdateAction getAction() {
+ return mAction;
+ }
+ }
+
+ public static class UpdatePostsEnded {
+ private final ReaderTag mReaderTag;
+ private final ReaderActions.UpdateResult mResult;
+ private final ReaderPostService.UpdateAction mAction;
+ public UpdatePostsEnded(ReaderActions.UpdateResult result,
+ ReaderPostService.UpdateAction action) {
+ mResult = result;
+ mAction = action;
+ mReaderTag = null;
+ }
+ public UpdatePostsEnded(ReaderTag readerTag,
+ ReaderActions.UpdateResult result,
+ ReaderPostService.UpdateAction action) {
+ mReaderTag = readerTag;
+ mResult = result;
+ mAction = action;
+ }
+ public ReaderTag getReaderTag() {
+ return mReaderTag;
+ }
+ public ReaderActions.UpdateResult getResult() {
+ return mResult;
+ }
+ public ReaderPostService.UpdateAction getAction() {
+ return mAction;
+ }
+ }
+
+ public static class SearchPostsStarted {
+ private final String mQuery;
+ private final int mOffset;
+ public SearchPostsStarted(@NonNull String query, int offset) {
+ mQuery = query;
+ mOffset = offset;
+ }
+ public String getQuery() {
+ return mQuery;
+ }
+ public int getOffset() {
+ return mOffset;
+ }
+ }
+ public static class SearchPostsEnded {
+ private final String mQuery;
+ private final boolean mDidSucceed;
+ private final int mOffset;
+ public SearchPostsEnded(@NonNull String query, int offset, boolean didSucceed) {
+ mQuery = query;
+ mOffset = offset;
+ mDidSucceed = didSucceed;
+ }
+ public boolean didSucceed() {
+ return mDidSucceed;
+ }
+ public String getQuery() {
+ return mQuery;
+ }
+ public int getOffset() {
+ return mOffset;
+ }
+ }
+
+ public static class UpdateCommentsStarted {}
+ public static class UpdateCommentsEnded {
+ private final ReaderActions.UpdateResult mResult;
+ public UpdateCommentsEnded(ReaderActions.UpdateResult result) {
+ mResult = result;
+ }
+ public ReaderActions.UpdateResult getResult() {
+ return mResult;
+ }
+ }
+
+ public static class RelatedPostsUpdated {
+ private final ReaderPost mSourcePost;
+ private final ReaderRelatedPostList mRelatedPosts;
+ public RelatedPostsUpdated(@NonNull ReaderPost sourcePost, @NonNull ReaderPostList relatedPosts) {
+ mSourcePost = sourcePost;
+ mRelatedPosts = new ReaderRelatedPostList(relatedPosts);
+ }
+ public ReaderPost getSourcePost() {
+ return mSourcePost;
+ }
+ public ReaderRelatedPostList getRelatedPosts() {
+ return mRelatedPosts;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderInterfaces.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderInterfaces.java
new file mode 100644
index 000000000..f6d3840a1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderInterfaces.java
@@ -0,0 +1,42 @@
+package org.wordpress.android.ui.reader;
+
+import android.view.View;
+
+import org.wordpress.android.models.ReaderPost;
+
+public class ReaderInterfaces {
+
+ private ReaderInterfaces() {
+ throw new AssertionError();
+ }
+
+ public interface OnPostSelectedListener {
+ void onPostSelected(ReaderPost post);
+ }
+
+ public interface OnTagSelectedListener {
+ void onTagSelected(String tagName);
+ }
+
+ /*
+ * called from post detail fragment so toolbar can animate in/out when scrolling
+ */
+ public interface AutoHideToolbarListener {
+ void onShowHideToolbar(boolean show);
+ }
+
+ /*
+ * called when user taps the dropdown arrow next to a post to show the popup menu
+ */
+ public interface OnPostPopupListener {
+ void onShowPostPopup(View view, ReaderPost post);
+ }
+
+ /*
+ * used by adapters to notify when data has been loaded
+ */
+ public interface DataLoadedListener {
+ void onDataLoaded(boolean isEmpty);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java
new file mode 100644
index 000000000..191c9e331
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java
@@ -0,0 +1,237 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v13.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.reader.ReaderViewPagerTransformer.TransformType;
+import org.wordpress.android.ui.reader.models.ReaderImageList;
+import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
+import org.wordpress.android.ui.reader.views.ReaderPhotoView.PhotoViewListener;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.widgets.WPViewPager;
+
+/**
+ * Full-screen photo viewer - uses a ViewPager to enable scrolling between images in a blog
+ * post, but also supports viewing a single image
+ */
+public class ReaderPhotoViewerActivity extends AppCompatActivity
+ implements PhotoViewListener {
+
+ private String mInitialImageUrl;
+ private boolean mIsPrivate;
+ private boolean mIsGallery;
+ private String mContent;
+ private WPViewPager mViewPager;
+ private PhotoPagerAdapter mAdapter;
+ private TextView mTxtTitle;
+ private boolean mIsTitleVisible;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.reader_activity_photo_viewer);
+
+ mViewPager = (WPViewPager) findViewById(R.id.viewpager);
+ mTxtTitle = (TextView) findViewById(R.id.text_title);
+
+ // title is hidden until we know we can show it
+ mTxtTitle.setVisibility(View.GONE);
+
+ if (savedInstanceState != null) {
+ mInitialImageUrl = savedInstanceState.getString(ReaderConstants.ARG_IMAGE_URL);
+ mIsPrivate = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_PRIVATE);
+ mIsGallery = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_GALLERY);
+ mContent = savedInstanceState.getString(ReaderConstants.ARG_CONTENT);
+ } else if (getIntent() != null) {
+ mInitialImageUrl = getIntent().getStringExtra(ReaderConstants.ARG_IMAGE_URL);
+ mIsPrivate = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_PRIVATE, false);
+ mIsGallery = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_GALLERY, false);
+ mContent = getIntent().getStringExtra(ReaderConstants.ARG_CONTENT);
+ }
+
+ mViewPager.setPageTransformer(false, new ReaderViewPagerTransformer(TransformType.FLOW));
+ mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ updateTitle(position);
+ }
+ });
+
+ mViewPager.setAdapter(getAdapter());
+ loadImageList();
+ }
+
+ private void loadImageList() {
+ // content will be empty when viewing a single image, otherwise content is HTML
+ // so parse images from it
+ final ReaderImageList imageList;
+ if (TextUtils.isEmpty(mContent)) {
+ imageList = new ReaderImageList(mIsPrivate);
+ } else if (mIsGallery) {
+ imageList = new ReaderImageScanner(mContent, mIsPrivate).getGalleryImageList();
+ } else {
+ imageList = new ReaderImageScanner(mContent, mIsPrivate).getImageList();
+ }
+
+ // make sure initial image is in the list
+ if (!TextUtils.isEmpty(mInitialImageUrl) && !imageList.hasImageUrl(mInitialImageUrl)) {
+ imageList.addImageUrl(0, mInitialImageUrl);
+ }
+
+ getAdapter().setImageList(imageList, mInitialImageUrl);
+ }
+
+ private PhotoPagerAdapter getAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new PhotoPagerAdapter(getFragmentManager());
+ }
+ return mAdapter;
+ }
+
+ private boolean hasAdapter() {
+ return (mAdapter != null);
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ if (hasAdapter()) {
+ String imageUrl = getAdapter().getImageUrl(mViewPager.getCurrentItem());
+ outState.putString(ReaderConstants.ARG_IMAGE_URL, imageUrl);
+ }
+
+ outState.putBoolean(ReaderConstants.ARG_IS_PRIVATE, mIsPrivate);
+ outState.putBoolean(ReaderConstants.ARG_IS_GALLERY, mIsGallery);
+ outState.putString(ReaderConstants.ARG_CONTENT, mContent);
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private int getImageCount() {
+ if (hasAdapter()) {
+ return getAdapter().getCount();
+ } else {
+ return 0;
+ }
+ }
+
+ private void updateTitle(int position) {
+ if (isFinishing() || !canShowTitle()) {
+ return;
+ }
+
+ String titlePhotoViewer = getString(R.string.reader_title_photo_viewer);
+ String title = String.format(titlePhotoViewer, position + 1, getImageCount());
+ if (title.equals(mTxtTitle.getText())) {
+ return;
+ }
+
+ mTxtTitle.setText(title);
+ }
+
+ /*
+ * title (image count) is only shown if there are multiple images
+ */
+ private boolean canShowTitle() {
+ return (getImageCount() > 1);
+ }
+
+ private void toggleTitle() {
+ if (isFinishing() || !canShowTitle()) {
+ return;
+ }
+
+ mTxtTitle.clearAnimation();
+ if (mIsTitleVisible) {
+ AniUtils.fadeOut(mTxtTitle, AniUtils.Duration.SHORT);
+ } else {
+ AniUtils.fadeIn(mTxtTitle, AniUtils.Duration.SHORT);
+ }
+ mIsTitleVisible = !mIsTitleVisible;
+ }
+
+ @Override
+ public void onTapPhotoView() {
+ toggleTitle();
+ }
+
+ private class PhotoPagerAdapter extends FragmentStatePagerAdapter {
+ private ReaderImageList mImageList;
+
+ PhotoPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ void setImageList(ReaderImageList imageList, String initialImageUrl) {
+ mImageList = (ReaderImageList) imageList.clone();
+ notifyDataSetChanged();
+
+ int position = indexOfImageUrl(initialImageUrl);
+ if (isValidPosition(position)) {
+ mViewPager.setCurrentItem(position);
+ if (canShowTitle()) {
+ mTxtTitle.setVisibility(View.VISIBLE);
+ mIsTitleVisible = true;
+ updateTitle(position);
+ } else {
+ mIsTitleVisible = false;
+ }
+ }
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ // work around "Fragement no longer exists for key" Android bug
+ // by catching the IllegalStateException
+ // https://code.google.com/p/android/issues/detail?id=42601
+ try {
+ super.restoreState(state, loader);
+ } catch (IllegalStateException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return ReaderPhotoViewerFragment.newInstance(
+ mImageList.get(position),
+ mImageList.isPrivate());
+ }
+
+ @Override
+ public int getCount() {
+ return (mImageList != null ? mImageList.size(): 0);
+ }
+
+ private int indexOfImageUrl(String imageUrl) {
+ if (mImageList == null) {
+ return -1;
+ }
+ return mImageList.indexOfImageUrl(imageUrl);
+ }
+
+ private boolean isValidPosition(int position) {
+ return (mImageList != null
+ && position >= 0
+ && position < getCount());
+ }
+
+ private String getImageUrl(int position) {
+ if (isValidPosition(position)) {
+ return mImageList.get(position);
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerFragment.java
new file mode 100644
index 000000000..a8f5221c8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerFragment.java
@@ -0,0 +1,94 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.reader.views.ReaderPhotoView;
+import org.wordpress.android.ui.reader.views.ReaderPhotoView.PhotoViewListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+
+public class ReaderPhotoViewerFragment extends Fragment {
+ private String mImageUrl;
+ private boolean mIsPrivate;
+
+ private ReaderPhotoView mPhotoView;
+ private PhotoViewListener mPhotoViewListener;
+
+ /**
+ * @param imageUrl the url of the image to load
+ * @param isPrivate whether image is from a private blog
+ */
+ static ReaderPhotoViewerFragment newInstance(String imageUrl, boolean isPrivate) {
+ AppLog.d(AppLog.T.READER, "reader photo fragment > newInstance");
+
+ Bundle args = new Bundle();
+ args.putString(ReaderConstants.ARG_IMAGE_URL, imageUrl);
+ args.putBoolean(ReaderConstants.ARG_IS_PRIVATE, isPrivate);
+
+ ReaderPhotoViewerFragment fragment = new ReaderPhotoViewerFragment();
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void setArguments(Bundle args) {
+ super.setArguments(args);
+ if (args != null) {
+ mImageUrl = args.getString(ReaderConstants.ARG_IMAGE_URL);
+ mIsPrivate = args.getBoolean(ReaderConstants.ARG_IS_PRIVATE);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.reader_fragment_photo_viewer, container, false);
+ mPhotoView = (ReaderPhotoView) view.findViewById(R.id.photo_view);
+
+ if (savedInstanceState != null) {
+ mImageUrl = savedInstanceState.getString(ReaderConstants.ARG_IMAGE_URL);
+ mIsPrivate = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_PRIVATE);
+ }
+
+ return view;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (activity instanceof PhotoViewListener) {
+ mPhotoViewListener = (PhotoViewListener) activity;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ showImage();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putString(ReaderConstants.ARG_IMAGE_URL, mImageUrl);
+ outState.putBoolean(ReaderConstants.ARG_IS_PRIVATE, mIsPrivate);
+ super.onSaveInstanceState(outState);
+ }
+
+ private void showImage() {
+ if (isAdded() && !TextUtils.isEmpty(mImageUrl)) {
+ // use max of width/height so image is cached the same regardless of orientation
+ Point pt = DisplayUtils.getDisplayPixelSize(getActivity());
+ int hiResWidth = Math.max(pt.x, pt.y);
+ mPhotoView.setImageUrl(mImageUrl, hiResWidth, mIsPrivate, mPhotoViewListener);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java
new file mode 100644
index 000000000..da635bfe4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java
@@ -0,0 +1,1171 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostDiscoverData;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption;
+import org.wordpress.android.ui.reader.ReaderInterfaces.AutoHideToolbarListener;
+import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderBlogActions;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.ui.reader.models.ReaderRelatedPost;
+import org.wordpress.android.ui.reader.models.ReaderRelatedPostList;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.utils.ReaderVideoUtils;
+import org.wordpress.android.ui.reader.views.ReaderFollowButton;
+import org.wordpress.android.ui.reader.views.ReaderIconCountView;
+import org.wordpress.android.ui.reader.views.ReaderLikingUsersView;
+import org.wordpress.android.ui.reader.views.ReaderWebView;
+import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderCustomViewListener;
+import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderWebViewPageFinishedListener;
+import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderWebViewUrlClickListener;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.wordpress.android.widgets.WPNetworkImageView;
+import org.wordpress.android.widgets.WPScrollView;
+import org.wordpress.android.widgets.WPScrollView.ScrollDirectionListener;
+
+import java.util.EnumSet;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderPostDetailFragment extends Fragment
+ implements WPMainActivity.OnActivityBackPressedListener,
+ ScrollDirectionListener,
+ ReaderCustomViewListener,
+ ReaderWebViewPageFinishedListener,
+ ReaderWebViewUrlClickListener {
+
+ private long mPostId;
+ private long mBlogId;
+ private ReaderPost mPost;
+ private ReaderPostRenderer mRenderer;
+ private ReaderPostListType mPostListType;
+
+ private final ReaderPostHistory mPostHistory = new ReaderPostHistory();
+
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private WPScrollView mScrollView;
+ private ViewGroup mLayoutFooter;
+ private ReaderWebView mReaderWebView;
+ private ReaderLikingUsersView mLikingUsersView;
+ private View mLikingUsersDivider;
+ private View mLikingUsersLabel;
+
+ private boolean mHasAlreadyUpdatedPost;
+ private boolean mHasAlreadyRequestedPost;
+ private boolean mIsLoggedOutReader;
+ private boolean mIsWebViewPaused;
+ private boolean mIsRelatedPost;
+
+ private int mToolbarHeight;
+ private String mErrorMessage;
+
+ private boolean mIsToolbarShowing = true;
+ private AutoHideToolbarListener mAutoHideToolbarListener;
+
+ // min scroll distance before toggling toolbar
+ private static final float MIN_SCROLL_DISTANCE_Y = 10;
+
+ public static ReaderPostDetailFragment newInstance(long blogId, long postId) {
+ return newInstance(blogId, postId, false, null);
+ }
+
+ public static ReaderPostDetailFragment newInstance(long blogId,
+ long postId,
+ boolean isRelatedPost,
+ ReaderPostListType postListType) {
+ AppLog.d(T.READER, "reader post detail > newInstance");
+
+ Bundle args = new Bundle();
+ args.putLong(ReaderConstants.ARG_BLOG_ID, blogId);
+ args.putLong(ReaderConstants.ARG_POST_ID, postId);
+ args.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, isRelatedPost);
+ if (postListType != null) {
+ args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, postListType);
+ }
+
+ ReaderPostDetailFragment fragment = new ReaderPostDetailFragment();
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsLoggedOutReader = ReaderUtils.isLoggedOutReader();
+ if (savedInstanceState != null) {
+ mPostHistory.restoreInstance(savedInstanceState);
+ }
+ }
+
+ @Override
+ public void setArguments(Bundle args) {
+ super.setArguments(args);
+ if (args != null) {
+ mBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID);
+ mPostId = args.getLong(ReaderConstants.ARG_POST_ID);
+ mIsRelatedPost = args.getBoolean(ReaderConstants.ARG_IS_RELATED_POST);
+ if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (activity instanceof AutoHideToolbarListener) {
+ mAutoHideToolbarListener = (AutoHideToolbarListener) activity;
+ }
+ mToolbarHeight = activity.getResources().getDimensionPixelSize(R.dimen.toolbar_height);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.reader_fragment_post_detail, container, false);
+
+ CustomSwipeRefreshLayout swipeRefreshLayout = (CustomSwipeRefreshLayout) view.findViewById(R.id.swipe_to_refresh);
+
+ //this fragment hides/shows toolbar with scrolling, which messes up ptr animation position
+ //so we have to set it manually
+ int swipeToRefreshOffset = getResources().getDimensionPixelSize(R.dimen.toolbar_content_offset);
+ swipeRefreshLayout.setProgressViewOffset(false, 0, swipeToRefreshOffset);
+
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(getActivity(), swipeRefreshLayout, new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!isAdded()) {
+ return;
+ }
+
+ updatePost();
+ }
+ });
+
+ mScrollView = (WPScrollView) view.findViewById(R.id.scroll_view_reader);
+ mScrollView.setScrollDirectionListener(this);
+
+ mLayoutFooter = (ViewGroup) view.findViewById(R.id.layout_post_detail_footer);
+ mLikingUsersView = (ReaderLikingUsersView) view.findViewById(R.id.layout_liking_users_view);
+ mLikingUsersDivider = view.findViewById(R.id.layout_liking_users_divider);
+ mLikingUsersLabel = view.findViewById(R.id.text_liking_users_label);
+
+ // setup the ReaderWebView
+ mReaderWebView = (ReaderWebView) view.findViewById(R.id.reader_webview);
+ mReaderWebView.setCustomViewListener(this);
+ mReaderWebView.setUrlClickListener(this);
+ mReaderWebView.setPageFinishedListener(this);
+
+ // hide footer and scrollView until the post is loaded
+ mLayoutFooter.setVisibility(View.INVISIBLE);
+ mScrollView.setVisibility(View.INVISIBLE);
+
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mReaderWebView != null) {
+ mReaderWebView.destroy();
+ }
+ }
+
+ private boolean hasPost() {
+ return (mPost != null);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ menu.clear();
+ inflater.inflate(R.menu.reader_detail, menu);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ // browse & share require the post to have a URL (some feed-based posts don't have one)
+ boolean postHasUrl = hasPost() && mPost.hasUrl();
+ MenuItem mnuBrowse = menu.findItem(R.id.menu_browse);
+ if (mnuBrowse != null) {
+ mnuBrowse.setVisible(postHasUrl);
+ }
+ MenuItem mnuShare = menu.findItem(R.id.menu_share);
+ if (mnuShare != null) {
+ mnuShare.setVisible(postHasUrl);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int i = item.getItemId();
+ if (i == R.id.menu_browse) {
+ if (hasPost()) {
+ ReaderActivityLauncher.openUrl(getActivity(), mPost.getUrl(), OpenUrlType.EXTERNAL);
+ }
+ return true;
+ } else if (i == R.id.menu_share) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.SHARED_ITEM);
+ sharePage();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private ReaderPostListType getPostListType() {
+ return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putLong(ReaderConstants.ARG_BLOG_ID, mBlogId);
+ outState.putLong(ReaderConstants.ARG_POST_ID, mPostId);
+
+ outState.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, mIsRelatedPost);
+ outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasAlreadyUpdatedPost);
+ outState.putBoolean(ReaderConstants.KEY_ALREADY_REQUESTED, mHasAlreadyRequestedPost);
+
+ outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType());
+
+ mPostHistory.saveInstance(outState);
+
+ if (!TextUtils.isEmpty(mErrorMessage)) {
+ outState.putString(ReaderConstants.KEY_ERROR_MESSAGE, mErrorMessage);
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ setHasOptionsMenu(true);
+ restoreState(savedInstanceState);
+ }
+
+ private void restoreState(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
+ mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID);
+ mIsRelatedPost = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_RELATED_POST);
+ mHasAlreadyUpdatedPost = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED);
+ mHasAlreadyRequestedPost = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_REQUESTED);
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
+ }
+ if (savedInstanceState.containsKey(ReaderConstants.KEY_ERROR_MESSAGE)) {
+ mErrorMessage = savedInstanceState.getString(ReaderConstants.KEY_ERROR_MESSAGE);
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ if (!hasPost()) {
+ showPost();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ }
+
+ /*
+ * called by the activity when user hits the back button - returns true if the back button
+ * is handled here and should be ignored by the activity
+ */
+ @Override
+ public boolean onActivityBackPressed() {
+ return goBackInPostHistory();
+ }
+
+ /*
+ * changes the like on the passed post
+ */
+ private void togglePostLike() {
+ if (!isAdded() || !hasPost() || !NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ boolean isAskingToLike = !mPost.isLikedByCurrentUser;
+ ReaderIconCountView likeCount = (ReaderIconCountView) getView().findViewById(R.id.count_likes);
+ likeCount.setSelected(isAskingToLike);
+ ReaderAnim.animateLikeButton(likeCount.getImageView(), isAskingToLike);
+
+ boolean success = ReaderPostActions.performLikeAction(mPost, isAskingToLike);
+ if (!success) {
+ likeCount.setSelected(!isAskingToLike);
+ return;
+ }
+
+ // get the post again since it has changed, then refresh to show changes
+ mPost = ReaderPostTable.getPost(mBlogId, mPostId, false);
+ refreshLikes();
+ refreshIconCounts();
+
+ if (isAskingToLike) {
+ AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, mPost);
+ } else {
+ AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_UNLIKED, mPost);
+ }
+ }
+
+ /*
+ * user tapped follow button to follow/unfollow the blog this post is from
+ */
+ private void togglePostFollowed() {
+ if (!isAdded() || !hasPost()) {
+ return;
+ }
+
+ final boolean isAskingToFollow = !ReaderPostTable.isPostFollowed(mPost);
+ final ReaderFollowButton followButton = (ReaderFollowButton) getView().findViewById(R.id.follow_button);
+
+ ReaderActions.ActionListener listener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (!isAdded()) {
+ return;
+ }
+ followButton.setEnabled(true);
+ if (!succeeded) {
+ int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog);
+ ToastUtils.showToast(getActivity(), resId);
+ followButton.setIsFollowedAnimated(!isAskingToFollow);
+ }
+ }
+ };
+
+ followButton.setEnabled(false);
+
+ if (ReaderBlogActions.followBlogForPost(mPost, isAskingToFollow, listener)) {
+ followButton.setIsFollowedAnimated(isAskingToFollow);
+ }
+ }
+
+ /*
+ * display the standard Android share chooser to share this post
+ */
+ private static final int MAX_SHARE_TITLE_LEN = 100;
+
+ private void sharePage() {
+ if (!isAdded() || !hasPost()) {
+ return;
+ }
+
+ final String url = (mPost.hasShortUrl() ? mPost.getShortUrl() : mPost.getUrl());
+ final String shareText;
+
+ if (mPost.hasTitle()) {
+ final String title;
+ // we don't know where the user will choose to share, so enforce a max title length
+ // in order to fit a tweet with some extra room for the URL and user edits
+ if (mPost.getTitle().length() > MAX_SHARE_TITLE_LEN) {
+ title = mPost.getTitle().substring(0, MAX_SHARE_TITLE_LEN).trim() + "…";
+ } else {
+ title = mPost.getTitle().trim();
+ }
+ shareText = title + " - " + url;
+ } else {
+ shareText = url;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, shareText);
+ intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.reader_share_subject, getString(R.string.app_name)));
+ try {
+ startActivity(Intent.createChooser(intent, getString(R.string.reader_share_link)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ ToastUtils.showToast(getActivity(), R.string.reader_toast_err_share_intent);
+ }
+ }
+
+ /*
+ * replace the current post with the passed one
+ */
+ private void replacePost(long blogId, long postId) {
+ mBlogId = blogId;
+ mPostId = postId;
+ mHasAlreadyRequestedPost = false;
+ mHasAlreadyUpdatedPost = false;
+
+ // hide views that would show info for the previous post - these will be re-displayed
+ // with the correct info once the new post loads
+ getView().findViewById(R.id.container_related_posts).setVisibility(View.GONE);
+ getView().findViewById(R.id.text_related_posts_label).setVisibility(View.GONE);
+
+ mLikingUsersView.setVisibility(View.GONE);
+ mLikingUsersDivider.setVisibility(View.GONE);
+ mLikingUsersLabel.setVisibility(View.GONE);
+
+ // clear the webView - otherwise it will remain scrolled to where the user scrolled to
+ mReaderWebView.clearContent();
+
+ // make sure the toolbar and footer are showing
+ showToolbar(true);
+ showFooter(true);
+
+ // now show the passed post
+ showPost();
+ }
+
+ /*
+ * request posts related to the current one - only available for wp.com
+ */
+ private void requestRelatedPosts() {
+ if (hasPost() && mPost.isWP()) {
+ ReaderPostActions.requestRelatedPosts(mPost);
+ }
+ }
+
+ /*
+ * related posts were retrieved, so show them
+ */
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.RelatedPostsUpdated event) {
+ if (!isAdded() || !hasPost()) return;
+
+ // make sure this is for the current post
+ if (event.getSourcePost().postId == mPostId && event.getSourcePost().blogId == mBlogId) {
+ showRelatedPosts(event.getRelatedPosts());
+ }
+ }
+
+ private void showRelatedPosts(@NonNull ReaderRelatedPostList relatedPosts) {
+ // locate the related posts container and remove any existing related post views
+ ViewGroup container = (ViewGroup) getView().findViewById(R.id.container_related_posts);
+ container.removeAllViews();
+
+ // add a separate view for each related post
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ int imageSize = DisplayUtils.dpToPx(getActivity(), getResources().getDimensionPixelSize(R.dimen.reader_related_post_image_size));
+ for (int index = 0; index < relatedPosts.size(); index++) {
+ final ReaderRelatedPost relatedPost = relatedPosts.get(index);
+
+ View postView = inflater.inflate(R.layout.reader_related_post, container, false);
+ TextView txtTitle = (TextView) postView.findViewById(R.id.text_related_post_title);
+ TextView txtByline = (TextView) postView.findViewById(R.id.text_related_post_byline);
+ WPNetworkImageView imgFeatured = (WPNetworkImageView) postView.findViewById(R.id.image_related_post);
+
+ txtTitle.setText(relatedPost.getTitle());
+ txtByline.setText(relatedPost.getByline());
+
+ imgFeatured.setVisibility(relatedPost.hasFeaturedImage() ? View.VISIBLE : View.GONE);
+ if (relatedPost.hasFeaturedImage()) {
+ String imageUrl = PhotonUtils.getPhotonImageUrl(relatedPost.getFeaturedImage(), imageSize, imageSize);
+ imgFeatured.setImageUrl(imageUrl, WPNetworkImageView.ImageType.PHOTO_ROUNDED);
+ imgFeatured.setVisibility(View.VISIBLE);
+ }
+
+ // tapping this view should open the related post detail
+ postView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ showRelatedPostDetail(relatedPost.getBlogId(), relatedPost.getPostId());
+ }
+ });
+
+ container.addView(postView);
+
+ // add a divider below all but the last related post
+ if (index < relatedPosts.size() - 1) {
+ View dividerView = inflater.inflate(R.layout.reader_related_post_divider, container, false);
+ container.addView(dividerView);
+ }
+ }
+
+ View label = getView().findViewById(R.id.text_related_posts_label);
+ if (label.getVisibility() != View.VISIBLE) {
+ AniUtils.fadeIn(label, AniUtils.Duration.MEDIUM);
+ }
+ if (container.getVisibility() != View.VISIBLE) {
+ AniUtils.fadeIn(container, AniUtils.Duration.MEDIUM);
+ }
+ }
+
+ /*
+ * user clicked a single related post - if we're already viewing a related post, add it to the
+ * history stack so the user can back-button through the history - otherwise start a new detail
+ * activity for this related post
+ */
+ private void showRelatedPostDetail(long blogId, long postId) {
+ AnalyticsUtils.trackWithReaderPostDetails(
+ AnalyticsTracker.Stat.READER_RELATED_POST_CLICKED, blogId, postId);
+
+ if (mIsRelatedPost) {
+ mPostHistory.push(new ReaderBlogIdPostId(mPost.blogId, mPost.postId));
+ replacePost(blogId, postId);
+ } else {
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), blogId, postId, true);
+ }
+ }
+
+ /*
+ * if the fragment is maintaining a backstack of posts, navigate to the previous one
+ */
+ protected boolean goBackInPostHistory() {
+ if (!mPostHistory.isEmpty()) {
+ ReaderBlogIdPostId ids = mPostHistory.pop();
+ replacePost(ids.getBlogId(), ids.getPostId());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /*
+ * get the latest version of this post
+ */
+ private void updatePost() {
+ if (!hasPost() || !mPost.isWP()) {
+ setRefreshing(false);
+ return;
+ }
+
+ final int numLikesBefore = ReaderLikeTable.getNumLikesForPost(mPost);
+
+ ReaderActions.UpdateResultListener resultListener = new ReaderActions.UpdateResultListener() {
+ @Override
+ public void onUpdateResult(ReaderActions.UpdateResult result) {
+ if (!isAdded()) {
+ return;
+ }
+ // if the post has changed, reload it from the db and update the like/comment counts
+ if (result.isNewOrChanged()) {
+ mPost = ReaderPostTable.getPost(mBlogId, mPostId, false);
+ refreshIconCounts();
+ }
+ // refresh likes if necessary - done regardless of whether the post has changed
+ // since it's possible likes weren't stored until the post was updated
+ if (result != ReaderActions.UpdateResult.FAILED
+ && numLikesBefore != ReaderLikeTable.getNumLikesForPost(mPost)) {
+ refreshLikes();
+ }
+
+ setRefreshing(false);
+ }
+ };
+ ReaderPostActions.updatePost(mPost, resultListener);
+ }
+
+ private void refreshIconCounts() {
+ if (!isAdded() || !hasPost() || !canShowFooter()) {
+ return;
+ }
+
+ final ReaderIconCountView countLikes = (ReaderIconCountView) getView().findViewById(R.id.count_likes);
+ final ReaderIconCountView countComments = (ReaderIconCountView) getView().findViewById(R.id.count_comments);
+
+ if (canShowCommentCount()) {
+ countComments.setCount(mPost.numReplies);
+ countComments.setVisibility(View.VISIBLE);
+ countComments.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderActivityLauncher.showReaderComments(getActivity(), mPost.blogId, mPost.postId);
+ }
+ });
+ } else {
+ countComments.setVisibility(View.INVISIBLE);
+ countComments.setOnClickListener(null);
+ }
+
+ if (canShowLikeCount()) {
+ countLikes.setCount(mPost.numLikes);
+ countLikes.setVisibility(View.VISIBLE);
+ countLikes.setSelected(mPost.isLikedByCurrentUser);
+ if (mIsLoggedOutReader) {
+ countLikes.setEnabled(false);
+ } else if (mPost.canLikePost()) {
+ countLikes.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ togglePostLike();
+ }
+ });
+ }
+ // if we know refreshLikes() is going to show the liking users, force liking user
+ // views to take up space right now
+ if (mPost.numLikes > 0 && mLikingUsersView.getVisibility() == View.GONE) {
+ mLikingUsersView.setVisibility(View.INVISIBLE);
+ mLikingUsersDivider.setVisibility(View.INVISIBLE);
+ mLikingUsersLabel.setVisibility(View.INVISIBLE);
+ }
+ } else {
+ countLikes.setVisibility(View.INVISIBLE);
+ countLikes.setOnClickListener(null);
+ }
+ }
+
+ /*
+ * show latest likes for this post
+ */
+ private void refreshLikes() {
+ if (!isAdded() || !hasPost() || !mPost.canLikePost()) {
+ return;
+ }
+
+ // nothing more to do if no likes
+ if (mPost.numLikes == 0) {
+ mLikingUsersView.setVisibility(View.GONE);
+ mLikingUsersDivider.setVisibility(View.GONE);
+ mLikingUsersLabel.setVisibility(View.GONE);
+ return;
+ }
+
+ // clicking likes view shows activity displaying all liking users
+ mLikingUsersView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderLikingUsers(getActivity(), mPost.blogId, mPost.postId);
+ }
+ });
+
+ mLikingUsersDivider.setVisibility(View.VISIBLE);
+ mLikingUsersLabel.setVisibility(View.VISIBLE);
+ mLikingUsersView.setVisibility(View.VISIBLE);
+ mLikingUsersView.showLikingUsers(mPost);
+ }
+
+ private boolean showPhotoViewer(String imageUrl, View sourceView, int startX, int startY) {
+ if (!isAdded() || TextUtils.isEmpty(imageUrl)) {
+ return false;
+ }
+
+ // make sure this is a valid web image (could be file: or data:)
+ if (!imageUrl.startsWith("http")) {
+ return false;
+ }
+
+ String postContent = (mRenderer != null ? mRenderer.getRenderedHtml() : null);
+ boolean isPrivatePost = (mPost != null && mPost.isPrivate);
+ EnumSet<PhotoViewerOption> options = EnumSet.noneOf(PhotoViewerOption.class);
+ if (isPrivatePost) {
+ options.add(ReaderActivityLauncher.PhotoViewerOption.IS_PRIVATE_IMAGE);
+ }
+
+ ReaderActivityLauncher.showReaderPhotoViewer(
+ getActivity(),
+ imageUrl,
+ postContent,
+ sourceView,
+ options,
+ startX,
+ startY);
+
+ return true;
+ }
+
+ /*
+ * called when the post doesn't exist in local db, need to get it from server
+ */
+ private void requestPost() {
+ final ProgressBar progress = (ProgressBar) getView().findViewById(R.id.progress_loading);
+ progress.setVisibility(View.VISIBLE);
+ progress.bringToFront();
+
+ ReaderActions.OnRequestListener listener = new ReaderActions.OnRequestListener() {
+ @Override
+ public void onSuccess() {
+ if (isAdded()) {
+ progress.setVisibility(View.GONE);
+ showPost();
+ }
+ }
+
+ @Override
+ public void onFailure(int statusCode) {
+ if (isAdded()) {
+ progress.setVisibility(View.GONE);
+ int errMsgResId;
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ errMsgResId = R.string.no_network_message;
+ } else {
+ switch (statusCode) {
+ case 401:
+ case 403:
+ errMsgResId = R.string.reader_err_get_post_not_authorized;
+ break;
+ case 404:
+ errMsgResId = R.string.reader_err_get_post_not_found;
+ break;
+ default:
+ errMsgResId = R.string.reader_err_get_post_generic;
+ break;
+ }
+ }
+ showError(getString(errMsgResId));
+ }
+ }
+ };
+ ReaderPostActions.requestPost(mBlogId, mPostId, listener);
+ }
+
+ /*
+ * shows an error message in the middle of the screen - used when requesting post fails
+ */
+ private void showError(String errorMessage) {
+ if (!isAdded()) return;
+
+ TextView txtError = (TextView) getView().findViewById(R.id.text_error);
+ txtError.setText(errorMessage);
+ if (txtError.getVisibility() != View.VISIBLE) {
+ AniUtils.fadeIn(txtError, AniUtils.Duration.MEDIUM);
+ }
+ mErrorMessage = errorMessage;
+ }
+
+ private void showPost() {
+ if (mIsPostTaskRunning) {
+ AppLog.w(T.READER, "reader post detail > show post task already running");
+ return;
+ }
+
+ new ShowPostTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ /*
+ * AsyncTask to retrieve this post from SQLite and display it
+ */
+ private boolean mIsPostTaskRunning = false;
+
+ private class ShowPostTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected void onPreExecute() {
+ mIsPostTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsPostTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ mPost = ReaderPostTable.getPost(mBlogId, mPostId, false);
+ if (mPost == null) {
+ return false;
+ }
+
+ // "discover" Editor Pick posts should open the original (source) post
+ if (mPost.isDiscoverPost()) {
+ ReaderPostDiscoverData discoverData = mPost.getDiscoverData();
+ if (discoverData != null
+ && discoverData.getDiscoverType() == ReaderPostDiscoverData.DiscoverType.EDITOR_PICK
+ && discoverData.getBlogId() != 0
+ && discoverData.getPostId() != 0) {
+ mBlogId = discoverData.getBlogId();
+ mPostId = discoverData.getPostId();
+ mPost = ReaderPostTable.getPost(mBlogId, mPostId, false);
+ if (mPost == null) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mIsPostTaskRunning = false;
+
+ if (!isAdded()) return;
+
+ // make sure options menu reflects whether we now have a post
+ getActivity().invalidateOptionsMenu();
+
+ if (!result) {
+ // post couldn't be loaded which means it doesn't exist in db, so request it from
+ // the server if it hasn't already been requested
+ if (!mHasAlreadyRequestedPost) {
+ mHasAlreadyRequestedPost = true;
+ AppLog.i(T.READER, "reader post detail > post not found, requesting it");
+ requestPost();
+ } else if (!TextUtils.isEmpty(mErrorMessage)) {
+ // post has already been requested and failed, so restore previous error message
+ showError(mErrorMessage);
+ }
+ return;
+ }
+
+ mReaderWebView.setIsPrivatePost(mPost.isPrivate);
+ mReaderWebView.setBlogSchemeIsHttps(UrlUtils.isHttps(mPost.getBlogUrl()));
+
+ TextView txtTitle = (TextView) getView().findViewById(R.id.text_title);
+ TextView txtBlogName = (TextView) getView().findViewById(R.id.text_blog_name);
+ TextView txtDomain = (TextView) getView().findViewById(R.id.text_domain);
+ TextView txtDateline = (TextView) getView().findViewById(R.id.text_dateline);
+ TextView txtTag = (TextView) getView().findViewById(R.id.text_tag);
+
+ WPNetworkImageView imgBlavatar = (WPNetworkImageView) getView().findViewById(R.id.image_blavatar);
+ WPNetworkImageView imgAvatar = (WPNetworkImageView) getView().findViewById(R.id.image_avatar);
+
+ ViewGroup layoutHeader = (ViewGroup) getView().findViewById(R.id.layout_post_detail_header);
+ ReaderFollowButton followButton = (ReaderFollowButton) layoutHeader.findViewById(R.id.follow_button);
+
+ if (!canShowFooter()) {
+ mLayoutFooter.setVisibility(View.GONE);
+ }
+
+ // add padding to the scrollView to make room for the top and bottom toolbars - this also
+ // ensures the scrollbar matches the content so it doesn't disappear behind the toolbars
+ int topPadding = (mAutoHideToolbarListener != null ? mToolbarHeight : 0);
+ int bottomPadding = (canShowFooter() ? mLayoutFooter.getHeight() : 0);
+ mScrollView.setPadding(0, topPadding, 0, bottomPadding);
+
+ // scrollView was hidden in onCreateView, show it now that we have the post
+ mScrollView.setVisibility(View.VISIBLE);
+
+ // render the post in the webView
+ mRenderer = new ReaderPostRenderer(mReaderWebView, mPost);
+ mRenderer.beginRender();
+
+ txtTitle.setText(mPost.hasTitle() ? mPost.getTitle() : getString(R.string.reader_untitled_post));
+
+ followButton.setVisibility(mIsLoggedOutReader ? View.GONE : View.VISIBLE);
+ if (!mIsLoggedOutReader) {
+ followButton.setIsFollowed(mPost.isFollowedByCurrentUser);
+ followButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ togglePostFollowed();
+ }
+ });
+ }
+
+ // clicking the header shows blog preview
+ if (getPostListType() != ReaderPostListType.BLOG_PREVIEW) {
+ layoutHeader.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderActivityLauncher.showReaderBlogPreview(v.getContext(), mPost);
+ }
+ });
+ }
+
+ if (mPost.hasBlogName()) {
+ txtBlogName.setText(mPost.getBlogName());
+ } else if (mPost.hasAuthorName()) {
+ txtBlogName.setText(mPost.getAuthorName());
+ } else {
+ txtBlogName.setText(null);
+ }
+
+ if (mPost.hasBlogUrl()) {
+ int blavatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium);
+ String imageUrl = GravatarUtils.blavatarFromUrl(mPost.getBlogUrl(), blavatarSz);
+ imgBlavatar.setImageUrl(imageUrl, WPNetworkImageView.ImageType.BLAVATAR);
+ txtDomain.setText(UrlUtils.getHost(mPost.getBlogUrl()));
+ } else {
+ imgBlavatar.showDefaultBlavatarImage();
+ txtDomain.setText(null);
+ }
+
+ if (mPost.hasPostAvatar()) {
+ int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_tiny);
+ imgAvatar.setImageUrl(mPost.getPostAvatarForDisplay(avatarSz), WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ imgAvatar.showDefaultGravatarImage();
+ }
+
+ String timestamp = DateTimeUtils.javaDateToTimeSpan(mPost.getDisplayDate(), WordPress.getContext());
+ if (mPost.hasAuthorName()) {
+ txtDateline.setText(mPost.getAuthorName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp);
+ } else if (mPost.hasBlogName()) {
+ txtDateline.setText(mPost.getBlogName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp);
+ } else {
+ txtDateline.setText(timestamp);
+ }
+
+ final String tagToDisplay = mPost.getTagForDisplay(null);
+ if (!TextUtils.isEmpty(tagToDisplay)) {
+ txtTag.setText(ReaderUtils.makeHashTag(tagToDisplay));
+ txtTag.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderTag tag = ReaderUtils.getTagFromTagName(tagToDisplay, ReaderTagType.FOLLOWED);
+ ReaderActivityLauncher.showReaderTagPreview(v.getContext(), tag);
+ }
+ });
+ }
+
+ if (canShowFooter() && mLayoutFooter.getVisibility() != View.VISIBLE) {
+ AniUtils.fadeIn(mLayoutFooter, AniUtils.Duration.LONG);
+ }
+
+ refreshIconCounts();
+ }
+ }
+
+ /*
+ * called by the web view when the content finishes loading - likes aren't displayed
+ * until this is triggered, to avoid having them appear before the webView content
+ */
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (url != null && url.equals("about:blank")) {
+ // brief delay before showing comments/likes to give page time to render
+ view.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+ refreshLikes();
+ if (!mHasAlreadyUpdatedPost) {
+ mHasAlreadyUpdatedPost = true;
+ updatePost();
+ }
+ requestRelatedPosts();
+ }
+ }, 300);
+ } else {
+ AppLog.w(T.READER, "reader post detail > page finished - " + url);
+ }
+ }
+
+ /*
+ * return the container view that should host the full screen video
+ */
+ @Override
+ public ViewGroup onRequestCustomView() {
+ if (isAdded()) {
+ return (ViewGroup) getView().findViewById(R.id.layout_custom_view_container);
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * return the container view that should be hidden when full screen video is shown
+ */
+ @Override
+ public ViewGroup onRequestContentView() {
+ if (isAdded()) {
+ return (ViewGroup) getView().findViewById(R.id.layout_post_detail_container);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onCustomViewShown() {
+ // full screen video has just been shown so hide the ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+ }
+
+ @Override
+ public void onCustomViewHidden() {
+ // user returned from full screen video so re-display the ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+
+ boolean isCustomViewShowing() {
+ return mReaderWebView != null && mReaderWebView.isCustomViewShowing();
+ }
+
+ void hideCustomView() {
+ if (mReaderWebView != null) {
+ mReaderWebView.hideCustomView();
+ }
+ }
+
+ @Override
+ public boolean onUrlClick(String url) {
+ // if this is a "wordpress://blogpreview?" link, show blog preview for the blog - this is
+ // used for Discover posts that highlight a blog
+ if (ReaderUtils.isBlogPreviewUrl(url)) {
+ long blogId = ReaderUtils.getBlogIdFromBlogPreviewUrl(url);
+ if (blogId != 0) {
+ ReaderActivityLauncher.showReaderBlogPreview(getActivity(), blogId);
+ }
+ return true;
+ }
+
+ // open YouTube videos in external app so they launch the YouTube player, open all other
+ // urls using an AuthenticatedWebViewActivity
+ final OpenUrlType openUrlType;
+ if (ReaderVideoUtils.isYouTubeVideoLink(url)) {
+ openUrlType = OpenUrlType.EXTERNAL;
+ } else {
+ openUrlType = OpenUrlType.INTERNAL;
+ }
+ ReaderActivityLauncher.openUrl(getActivity(), url, openUrlType);
+ return true;
+ }
+
+ @Override
+ public boolean onImageUrlClick(String imageUrl, View view, int x, int y) {
+ return showPhotoViewer(imageUrl, view, x, y);
+ }
+
+ private ActionBar getActionBar() {
+ if (isAdded() && getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ AppLog.w(T.READER, "reader post detail > getActionBar returned null");
+ return null;
+ }
+ }
+
+ void pauseWebView() {
+ if (mReaderWebView == null) {
+ AppLog.w(T.READER, "reader post detail > attempt to pause null webView");
+ } else if (!mIsWebViewPaused) {
+ AppLog.d(T.READER, "reader post detail > pausing webView");
+ mReaderWebView.hideCustomView();
+ mReaderWebView.onPause();
+ mIsWebViewPaused = true;
+ }
+ }
+
+ void resumeWebViewIfPaused() {
+ if (mReaderWebView == null) {
+ AppLog.w(T.READER, "reader post detail > attempt to resume null webView");
+ } else if (mIsWebViewPaused) {
+ AppLog.d(T.READER, "reader post detail > resuming paused webView");
+ mReaderWebView.onResume();
+ mIsWebViewPaused = false;
+ }
+ }
+
+ @Override
+ public void onScrollUp(float distanceY) {
+ if (!mIsToolbarShowing
+ && -distanceY >= MIN_SCROLL_DISTANCE_Y) {
+ showToolbar(true);
+ showFooter(true);
+ }
+ }
+
+ @Override
+ public void onScrollDown(float distanceY) {
+ if (mIsToolbarShowing
+ && distanceY >= MIN_SCROLL_DISTANCE_Y
+ && mScrollView.canScrollDown()
+ && mScrollView.canScrollUp()
+ && mScrollView.getScrollY() > mToolbarHeight) {
+ showToolbar(false);
+ showFooter(false);
+ }
+ }
+
+ @Override
+ public void onScrollCompleted() {
+ if (!mIsToolbarShowing
+ && (!mScrollView.canScrollDown() || !mScrollView.canScrollUp())) {
+ showToolbar(true);
+ showFooter(true);
+ }
+ }
+
+ private void showToolbar(boolean show) {
+ mIsToolbarShowing = show;
+ if (mAutoHideToolbarListener != null) {
+ mAutoHideToolbarListener.onShowHideToolbar(show);
+ }
+ }
+
+ private void showFooter(boolean show) {
+ if (isAdded() && canShowFooter()) {
+ AniUtils.animateBottomBar(mLayoutFooter, show);
+ }
+ }
+
+ /*
+ * can we show the footer bar which contains the like & comment counts?
+ */
+ private boolean canShowFooter() {
+ return canShowLikeCount() || canShowCommentCount();
+ }
+
+ private boolean canShowCommentCount() {
+ if (mPost == null) {
+ return false;
+ }
+ if (mIsLoggedOutReader) {
+ return mPost.numReplies > 0;
+ }
+ return mPost.isWP()
+ && !mPost.isJetpack
+ && !mPost.isDiscoverPost()
+ && (mPost.isCommentsOpen || mPost.numReplies > 0);
+ }
+
+ private boolean canShowLikeCount() {
+ if (mPost == null) {
+ return false;
+ }
+ if (mIsLoggedOutReader) {
+ return mPost.numLikes > 0;
+ }
+ return mPost.canLikePost() || mPost.numLikes > 0;
+ }
+
+ private void setRefreshing(boolean refreshing) {
+ mSwipeToRefreshHelper.setRefreshing(refreshing);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostHistory.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostHistory.java
new file mode 100644
index 000000000..1af43b9ea
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostHistory.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.ui.reader;
+
+import android.os.Bundle;
+
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Stack;
+
+/*
+ * used to maintain a history of posts viewed in the detail fragment so we can navigate back
+ * through them when the user hits the back button - currently used only for related posts
+ */
+class ReaderPostHistory extends Stack<ReaderBlogIdPostId> {
+ private static final String HISTORY_KEY_NAME = "reader_post_history";
+
+ void restoreInstance(Bundle bundle) {
+ clear();
+ if (bundle != null && bundle.containsKey(HISTORY_KEY_NAME)) {
+ ArrayList<String> history = bundle.getStringArrayList(HISTORY_KEY_NAME);
+ if (history != null) {
+ this.fromArrayList(history);
+ }
+ }
+ }
+
+ void saveInstance(Bundle bundle) {
+ if (bundle != null && !isEmpty()) {
+ bundle.putStringArrayList(HISTORY_KEY_NAME, this.toArrayList());
+ }
+ }
+
+ private ArrayList<String> toArrayList() {
+ ArrayList<String> list = new ArrayList<>();
+ for (ReaderBlogIdPostId ids : this) {
+ list.add(ids.getBlogId() + ":" + ids.getPostId());
+ }
+ return list;
+ }
+
+ private void fromArrayList(ArrayList<String> list) {
+ if (list == null || list.isEmpty()) return;
+
+ for (String idPair: list) {
+ String[] split = idPair.split(":");
+ long blogId = StringUtils.stringToLong(split[0]);
+ long postId = StringUtils.stringToLong(split[1]);
+ this.add(new ReaderBlogIdPostId(blogId, postId));
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java
new file mode 100644
index 000000000..fcad59159
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java
@@ -0,0 +1,181 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.AppBarLayout;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
+
+/*
+ * serves as the host for ReaderPostListFragment when showing blog preview & tag preview
+ */
+
+public class ReaderPostListActivity extends AppCompatActivity {
+
+ private ReaderPostListType mPostListType;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.reader_activity_post_list);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (getIntent().hasExtra(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) getIntent().getSerializableExtra(ReaderConstants.ARG_POST_LIST_TYPE);
+ } else {
+ mPostListType = ReaderTypes.DEFAULT_POST_LIST_TYPE;
+ }
+
+ if (getPostListType() == ReaderPostListType.TAG_PREVIEW || getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
+ // show an X in the toolbar which closes the activity - if this is tag preview, then
+ // using the back button will navigate through tags if the user explores beyond a single tag
+ toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+
+ if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
+ setTitle(R.string.reader_title_blog_preview);
+ if (savedInstanceState == null) {
+ long blogId = getIntent().getLongExtra(ReaderConstants.ARG_BLOG_ID, 0);
+ long feedId = getIntent().getLongExtra(ReaderConstants.ARG_FEED_ID, 0);
+ if (feedId != 0) {
+ showListFragmentForFeed(feedId);
+ } else {
+ showListFragmentForBlog(blogId);
+ }
+ }
+ } else if (getPostListType() == ReaderPostListType.TAG_PREVIEW) {
+ setTitle(R.string.reader_title_tag_preview);
+ ReaderTag tag = (ReaderTag) getIntent().getSerializableExtra(ReaderConstants.ARG_TAG);
+ if (tag != null && savedInstanceState == null) {
+ showListFragmentForTag(tag, mPostListType);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onResumeFragments() {
+ super.onResumeFragments();
+ //this particular Activity doesn't show filtering, so we'll disable the FilteredRecyclerView toolbar here
+ disableFilteredRecyclerViewToolbar();
+ }
+
+ /*
+ * This method hides the FilteredRecyclerView toolbar with spinner so to disable content filtering, for reusability
+ * */
+ private void disableFilteredRecyclerViewToolbar(){
+ // make it invisible - setting height to zero here because setting visibility to View.GONE wouldn't take the
+ // occupied space, as otherwise expected
+ AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
+ if (appBarLayout != null) {
+ CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appBarLayout.getLayoutParams();
+ lp.height = 0;
+ appBarLayout.setLayoutParams(lp);
+ }
+
+ // disabling any CoordinatorLayout behavior for scrolling
+ Toolbar toolbarWithSpinner = (Toolbar) findViewById(R.id.toolbar_with_spinner);
+ if (toolbarWithSpinner != null){
+ AppBarLayout.LayoutParams p = (AppBarLayout.LayoutParams) toolbarWithSpinner.getLayoutParams();
+ p.setScrollFlags(0);
+ toolbarWithSpinner.setLayoutParams(p);
+ }
+ }
+
+ private ReaderPostListType getPostListType() {
+ return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE);
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onBackPressed() {
+ ReaderPostListFragment fragment = getListFragment();
+ if (fragment == null || !fragment.onActivityBackPressed()) {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /*
+ * show fragment containing list of latest posts for a specific tag
+ */
+ private void showListFragmentForTag(final ReaderTag tag, ReaderPostListType listType) {
+ if (isFinishing()) {
+ return;
+ }
+ Fragment fragment = ReaderPostListFragment.newInstanceForTag(tag, listType);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment, getString(R.string.fragment_tag_reader_post_list))
+ .commit();
+ }
+
+ /*
+ * show fragment containing list of latest posts in a specific blog
+ */
+ private void showListFragmentForBlog(long blogId) {
+ if (isFinishing()) {
+ return;
+ }
+ Fragment fragment = ReaderPostListFragment.newInstanceForBlog(blogId);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment, getString(R.string.fragment_tag_reader_post_list))
+ .commit();
+ }
+
+ private void showListFragmentForFeed(long feedId) {
+ if (isFinishing()) {
+ return;
+ }
+ Fragment fragment = ReaderPostListFragment.newInstanceForFeed(feedId);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment, getString(R.string.fragment_tag_reader_post_list))
+ .commit();
+ }
+
+ private ReaderPostListFragment getListFragment() {
+ Fragment fragment = getFragmentManager().findFragmentByTag(getString(R.string.fragment_tag_reader_post_list));
+ if (fragment == null) {
+ return null;
+ }
+ return ((ReaderPostListFragment) fragment);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java
new file mode 100644
index 000000000..69409385e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java
@@ -0,0 +1,1612 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.ListPopupWindow;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.SearchView;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.AutoCompleteTextView;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.datasets.ReaderDatabase;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderSearchTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.FilterCriteria;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostDiscoverData;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.EmptyViewMessageType;
+import org.wordpress.android.ui.FilteredRecyclerView;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderBlogActions;
+import org.wordpress.android.ui.reader.actions.ReaderBlogActions.BlockedBlogResult;
+import org.wordpress.android.ui.reader.adapters.ReaderMenuAdapter;
+import org.wordpress.android.ui.reader.adapters.ReaderPostAdapter;
+import org.wordpress.android.ui.reader.adapters.ReaderSearchSuggestionAdapter;
+import org.wordpress.android.ui.reader.services.ReaderPostService;
+import org.wordpress.android.ui.reader.services.ReaderPostService.UpdateAction;
+import org.wordpress.android.ui.reader.services.ReaderSearchService;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.android.widgets.RecyclerItemDecoration;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderPostListFragment extends Fragment
+ implements ReaderInterfaces.OnPostSelectedListener,
+ ReaderInterfaces.OnTagSelectedListener,
+ ReaderInterfaces.OnPostPopupListener,
+ WPMainActivity.OnActivityBackPressedListener,
+ WPMainActivity.OnScrollToTopListener {
+
+ private ReaderPostAdapter mPostAdapter;
+ private ReaderSearchSuggestionAdapter mSearchSuggestionAdapter;
+
+ private FilteredRecyclerView mRecyclerView;
+ private boolean mFirstLoad = true;
+ private final ReaderTagList mTags = new ReaderTagList();
+
+ private View mNewPostsBar;
+ private View mEmptyView;
+ private View mEmptyViewBoxImages;
+ private ProgressBar mProgress;
+
+ private SearchView mSearchView;
+ private MenuItem mSettingsMenuItem;
+ private MenuItem mSearchMenuItem;
+
+ private ReaderTag mCurrentTag;
+ private long mCurrentBlogId;
+ private long mCurrentFeedId;
+ private String mCurrentSearchQuery;
+ private ReaderPostListType mPostListType;
+
+ private int mRestorePosition;
+
+ private boolean mIsUpdating;
+ private boolean mWasPaused;
+ private boolean mHasUpdatedPosts;
+ private boolean mIsAnimatingOutNewPostsBar;
+
+ private static boolean mHasPurgedReaderDb;
+ private static Date mLastAutoUpdateDt;
+
+ private final HistoryStack mTagPreviewHistory = new HistoryStack("tag_preview_history");
+
+ private static class HistoryStack extends Stack<String> {
+ private final String keyName;
+ HistoryStack(@SuppressWarnings("SameParameterValue") String keyName) {
+ this.keyName = keyName;
+ }
+ void restoreInstance(Bundle bundle) {
+ clear();
+ if (bundle.containsKey(keyName)) {
+ ArrayList<String> history = bundle.getStringArrayList(keyName);
+ if (history != null) {
+ this.addAll(history);
+ }
+ }
+ }
+ void saveInstance(Bundle bundle) {
+ if (!isEmpty()) {
+ ArrayList<String> history = new ArrayList<>();
+ history.addAll(this);
+ bundle.putStringArrayList(keyName, history);
+ }
+ }
+ }
+
+ public static ReaderPostListFragment newInstance() {
+ ReaderTag tag = AppPrefs.getReaderTag();
+ if (tag == null) {
+ tag = ReaderUtils.getDefaultTag();
+ }
+ return newInstanceForTag(tag, ReaderPostListType.TAG_FOLLOWED);
+ }
+
+ /*
+ * show posts with a specific tag (either TAG_FOLLOWED or TAG_PREVIEW)
+ */
+ static ReaderPostListFragment newInstanceForTag(ReaderTag tag, ReaderPostListType listType) {
+ AppLog.d(T.READER, "reader post list > newInstance (tag)");
+
+ Bundle args = new Bundle();
+ args.putSerializable(ReaderConstants.ARG_TAG, tag);
+ args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, listType);
+
+ ReaderPostListFragment fragment = new ReaderPostListFragment();
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ /*
+ * show posts in a specific blog
+ */
+ public static ReaderPostListFragment newInstanceForBlog(long blogId) {
+ AppLog.d(T.READER, "reader post list > newInstance (blog)");
+
+ Bundle args = new Bundle();
+ args.putLong(ReaderConstants.ARG_BLOG_ID, blogId);
+ args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
+
+ ReaderPostListFragment fragment = new ReaderPostListFragment();
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ public static ReaderPostListFragment newInstanceForFeed(long feedId) {
+ AppLog.d(T.READER, "reader post list > newInstance (blog)");
+
+ Bundle args = new Bundle();
+ args.putLong(ReaderConstants.ARG_FEED_ID, feedId);
+ args.putLong(ReaderConstants.ARG_BLOG_ID, feedId);
+ args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
+
+ ReaderPostListFragment fragment = new ReaderPostListFragment();
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void setArguments(Bundle args) {
+ super.setArguments(args);
+
+ if (args != null) {
+ if (args.containsKey(ReaderConstants.ARG_TAG)) {
+ mCurrentTag = (ReaderTag) args.getSerializable(ReaderConstants.ARG_TAG);
+ }
+ if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
+ }
+
+ mCurrentBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID);
+ mCurrentFeedId = args.getLong(ReaderConstants.ARG_FEED_ID);
+ mCurrentSearchQuery = args.getString(ReaderConstants.ARG_SEARCH_QUERY);
+
+ if (getPostListType() == ReaderPostListType.TAG_PREVIEW && hasCurrentTag()) {
+ mTagPreviewHistory.push(getCurrentTagName());
+ }
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ AppLog.d(T.READER, "reader post list > restoring instance state");
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_TAG)) {
+ mCurrentTag = (ReaderTag) savedInstanceState.getSerializable(ReaderConstants.ARG_TAG);
+ }
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_BLOG_ID)) {
+ mCurrentBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
+ }
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_FEED_ID)) {
+ mCurrentFeedId = savedInstanceState.getLong(ReaderConstants.ARG_FEED_ID);
+ }
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_SEARCH_QUERY)) {
+ mCurrentSearchQuery = savedInstanceState.getString(ReaderConstants.ARG_SEARCH_QUERY);
+ }
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
+ }
+ if (getPostListType() == ReaderPostListType.TAG_PREVIEW) {
+ mTagPreviewHistory.restoreInstance(savedInstanceState);
+ }
+ mRestorePosition = savedInstanceState.getInt(ReaderConstants.KEY_RESTORE_POSITION);
+ mWasPaused = savedInstanceState.getBoolean(ReaderConstants.KEY_WAS_PAUSED);
+ mHasUpdatedPosts = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED);
+ mFirstLoad = savedInstanceState.getBoolean(ReaderConstants.KEY_FIRST_LOAD);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mWasPaused = true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ checkPostAdapter();
+
+ if (mWasPaused) {
+ AppLog.d(T.READER, "reader post list > resumed from paused state");
+ mWasPaused = false;
+ if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
+ resumeFollowedTag();
+ } else {
+ refreshPosts();
+ }
+
+ // if the user was searching, make sure the filter toolbar is showing
+ // so the user can see the search keyword they entered
+ if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) {
+ mRecyclerView.showToolbar();
+ }
+ }
+ }
+
+ /*
+ * called when fragment is resumed and we're looking at posts in a followed tag
+ */
+ private void resumeFollowedTag() {
+ Object event = EventBus.getDefault().getStickyEvent(ReaderEvents.TagAdded.class);
+ if (event != null) {
+ // user just added a tag so switch to it.
+ String tagName = ((ReaderEvents.TagAdded) event).getTagName();
+ EventBus.getDefault().removeStickyEvent(event);
+ ReaderTag newTag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED);
+ setCurrentTag(newTag);
+ } else if (!ReaderTagTable.tagExists(getCurrentTag())) {
+ // current tag no longer exists, revert to default
+ AppLog.d(T.READER, "reader post list > current tag no longer valid");
+ setCurrentTag(ReaderUtils.getDefaultTag());
+ } else {
+ // otherwise, refresh posts to make sure any changes are reflected and auto-update
+ // posts in the current tag if it's time
+ refreshPosts();
+ updateCurrentTagIfTime();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+
+ reloadTags();
+
+ // purge database and update followed tags/blog if necessary - note that we don't purge unless
+ // there's a connection to avoid removing posts the user would expect to see offline
+ if (getPostListType() == ReaderPostListType.TAG_FOLLOWED && NetworkUtils.isNetworkAvailable(getActivity())) {
+ purgeDatabaseIfNeeded();
+ updateFollowedTagsAndBlogsIfNeeded();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ }
+
+ /*
+ * ensures the adapter is created and posts are updated if they haven't already been
+ */
+ private void checkPostAdapter() {
+ if (isAdded() && mRecyclerView.getAdapter() == null) {
+ mRecyclerView.setAdapter(getPostAdapter());
+
+ if (!mHasUpdatedPosts && NetworkUtils.isNetworkAvailable(getActivity())) {
+ mHasUpdatedPosts = true;
+ if (getPostListType().isTagType()) {
+ updateCurrentTagIfTime();
+ } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
+ updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_NEWER);
+ }
+ }
+ }
+ }
+
+ /*
+ * reset the post adapter to initial state and create it again using the passed list type
+ */
+ private void resetPostAdapter(ReaderPostListType postListType) {
+ mPostListType = postListType;
+ mPostAdapter = null;
+ mRecyclerView.setAdapter(null);
+ mRecyclerView.setAdapter(getPostAdapter());
+ mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported());
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) {
+ if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
+ // reload the tag filter since tags have changed
+ reloadTags();
+
+ // update the current tag if the list fragment is empty - this will happen if
+ // the tag table was previously empty (ie: first run)
+ if (isPostAdapterEmpty()) {
+ updateCurrentTag();
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.FollowedBlogsChanged event) {
+ // refresh posts if user is viewing "Followed Sites"
+ if (getPostListType() == ReaderPostListType.TAG_FOLLOWED
+ && hasCurrentTag()
+ && getCurrentTag().isFollowedSites()) {
+ refreshPosts();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ AppLog.d(T.READER, "reader post list > saving instance state");
+
+ if (mCurrentTag != null) {
+ outState.putSerializable(ReaderConstants.ARG_TAG, mCurrentTag);
+ }
+ if (getPostListType() == ReaderPostListType.TAG_PREVIEW) {
+ mTagPreviewHistory.saveInstance(outState);
+ }
+
+ outState.putLong(ReaderConstants.ARG_BLOG_ID, mCurrentBlogId);
+ outState.putLong(ReaderConstants.ARG_FEED_ID, mCurrentFeedId);
+ outState.putString(ReaderConstants.ARG_SEARCH_QUERY, mCurrentSearchQuery);
+ outState.putBoolean(ReaderConstants.KEY_WAS_PAUSED, mWasPaused);
+ outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasUpdatedPosts);
+ outState.putBoolean(ReaderConstants.KEY_FIRST_LOAD, mFirstLoad);
+ outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, getCurrentPosition());
+ outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType());
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private int getCurrentPosition() {
+ if (mRecyclerView != null && hasPostAdapter()) {
+ return mRecyclerView.getCurrentPosition();
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.reader_fragment_post_cards, container, false);
+ mRecyclerView = (FilteredRecyclerView) rootView.findViewById(R.id.reader_recycler_view);
+
+ Context context = container.getContext();
+
+ // view that appears when current tag/blog has no posts - box images in this view are
+ // displayed and animated for tags only
+ mEmptyView = rootView.findViewById(R.id.empty_custom_view);
+ mEmptyViewBoxImages = mEmptyView.findViewById(R.id.layout_box_images);
+
+ mRecyclerView.setLogT(AppLog.T.READER);
+ mRecyclerView.setCustomEmptyView(mEmptyView);
+ mRecyclerView.setFilterListener(new FilteredRecyclerView.FilterListener() {
+ @Override
+ public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) {
+ return null;
+ }
+
+ @Override
+ public void onLoadFilterCriteriaOptionsAsync(
+ FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) {
+
+ loadTags(listener);
+ }
+
+ @Override
+ public void onLoadData() {
+ if (!isAdded()) {
+ return;
+ }
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ mRecyclerView.setRefreshing(false);
+ return;
+ }
+
+ if (mFirstLoad){
+ /* let onResume() take care of this logic, as the FilteredRecyclerView.FilterListener onLoadData method
+ * is called on two moments: once for first time load, and then each time the swipe to refresh gesture
+ * triggers a refresh
+ */
+ mRecyclerView.setRefreshing(false);
+ mFirstLoad = false;
+ } else {
+ switch (getPostListType()) {
+ case TAG_FOLLOWED:
+ case TAG_PREVIEW:
+ updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_NEWER);
+ break;
+ case BLOG_PREVIEW:
+ updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_NEWER);
+ break;
+ }
+ // make sure swipe-to-refresh progress shows since this is a manual refresh
+ mRecyclerView.setRefreshing(true);
+ }
+ }
+
+ @Override
+ public void onFilterSelected(int position, FilterCriteria criteria) {
+ onTagChanged((ReaderTag)criteria);
+ }
+
+ @Override
+ public FilterCriteria onRecallSelection() {
+ if (hasCurrentTag()) {
+ return getCurrentTag();
+ } else {
+ AppLog.w(T.READER, "reader post list > no current tag in onRecallSelection");
+ return ReaderUtils.getDefaultTag();
+ }
+ }
+
+ @Override
+ public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) {
+ return null;
+ }
+
+ @Override
+ public void onShowCustomEmptyView (EmptyViewMessageType emptyViewMsgType) {
+ setEmptyTitleAndDescription(
+ EmptyViewMessageType.NETWORK_ERROR.equals(emptyViewMsgType)
+ || EmptyViewMessageType.PERMISSION_ERROR.equals(emptyViewMsgType)
+ || EmptyViewMessageType.GENERIC_ERROR.equals(emptyViewMsgType));
+ }
+
+ });
+
+ // add the item decoration (dividers) to the recycler, skipping the first item if the first
+ // item is the tag toolbar (shown when viewing posts in followed tags) - this is to avoid
+ // having the tag toolbar take up more vertical space than necessary
+ int spacingHorizontal = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin);
+ int spacingVertical = context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters);
+ mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical, false));
+
+ // the following will change the look and feel of the toolbar to match the current design
+ mRecyclerView.setToolbarBackgroundColor(ContextCompat.getColor(context, R.color.blue_medium));
+ mRecyclerView.setToolbarSpinnerTextColor(ContextCompat.getColor(context, R.color.white));
+ mRecyclerView.setToolbarSpinnerDrawable(R.drawable.arrow);
+ mRecyclerView.setToolbarLeftAndRightPadding(
+ getResources().getDimensionPixelSize(R.dimen.margin_medium) + spacingHorizontal,
+ getResources().getDimensionPixelSize(R.dimen.margin_extra_large) + spacingHorizontal);
+
+ // add a menu to the filtered recycler's toolbar
+ if (!ReaderUtils.isLoggedOutReader()
+ && (getPostListType() == ReaderPostListType.TAG_FOLLOWED || getPostListType() == ReaderPostListType.SEARCH_RESULTS)) {
+ setupRecyclerToolbar();
+ }
+
+ mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported());
+
+ // bar that appears at top after new posts are loaded
+ mNewPostsBar = rootView.findViewById(R.id.layout_new_posts);
+ mNewPostsBar.setVisibility(View.GONE);
+ mNewPostsBar.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mRecyclerView.scrollRecycleViewToPosition(0);
+ refreshPosts();
+ }
+ });
+
+ // progress bar that appears when loading more posts
+ mProgress = (ProgressBar) rootView.findViewById(R.id.progress_footer);
+ mProgress.setVisibility(View.GONE);
+
+ return rootView;
+ }
+
+ /*
+ * adds a menu to the recycler's toolbar containing settings & search items - only called
+ * for followed tags
+ */
+ private void setupRecyclerToolbar() {
+ Menu menu = mRecyclerView.addToolbarMenu(R.menu.reader_list);
+ mSettingsMenuItem = menu.findItem(R.id.menu_reader_settings);
+ mSearchMenuItem = menu.findItem(R.id.menu_reader_search);
+
+ mSettingsMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ ReaderActivityLauncher.showReaderSubs(getActivity());
+ return true;
+ }
+ });
+
+ mSearchView = (SearchView) mSearchMenuItem.getActionView();
+ mSearchView.setQueryHint(getString(R.string.reader_hint_post_search));
+ mSearchView.setSubmitButtonEnabled(false);
+ mSearchView.setIconifiedByDefault(true);
+ mSearchView.setIconified(true);
+
+ // force the search view to take up as much horizontal space as possible (without this
+ // it looks truncated on landscape)
+ int maxWidth = DisplayUtils.getDisplayPixelWidth(getActivity());
+ mSearchView.setMaxWidth(maxWidth);
+
+ // this is hacky, but we want to change the SearchView's autocomplete to show suggestions
+ // after a single character is typed, and there's no less hacky way to do this...
+ View view = mSearchView.findViewById(android.support.v7.appcompat.R.id.search_src_text);
+ if (view instanceof AutoCompleteTextView) {
+ ((AutoCompleteTextView) view).setThreshold(1);
+ }
+
+ MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, new MenuItemCompat.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ if (getPostListType() != ReaderPostListType.SEARCH_RESULTS) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_LOADED);
+ }
+ resetPostAdapter(ReaderPostListType.SEARCH_RESULTS);
+ showSearchMessage();
+ mSettingsMenuItem.setVisible(false);
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ hideSearchMessage();
+ resetSearchSuggestionAdapter();
+ mSettingsMenuItem.setVisible(true);
+ mCurrentSearchQuery = null;
+
+ // return to the followed tag that was showing prior to searching
+ resetPostAdapter(ReaderPostListType.TAG_FOLLOWED);
+
+ return true;
+ }
+ });
+
+ mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ submitSearchQuery(query);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (TextUtils.isEmpty(newText)) {
+ showSearchMessage();
+ } else {
+ populateSearchSuggestionAdapter(newText);
+ }
+ return true;
+ }
+ }
+ );
+ }
+
+ /*
+ * start the search service to search for posts matching the current query - the passed
+ * offset is used during infinite scroll, pass zero for initial search
+ */
+ private void updatePostsInCurrentSearch(int offset) {
+ ReaderSearchService.startService(getActivity(), mCurrentSearchQuery, offset);
+ }
+
+ private void submitSearchQuery(@NonNull String query) {
+ if (!isAdded()) return;
+
+ mSearchView.clearFocus(); // this will hide suggestions and the virtual keyboard
+ hideSearchMessage();
+
+ // remember this query for future suggestions
+ String trimQuery = query != null ? query.trim() : "";
+ ReaderSearchTable.addOrUpdateQueryString(trimQuery);
+
+ // remove cached results for this search - search results are ephemeral so each search
+ // should be treated as a "fresh" one
+ ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(trimQuery);
+ ReaderPostTable.deletePostsWithTag(searchTag);
+
+ mPostAdapter.setCurrentTag(searchTag);
+ mCurrentSearchQuery = trimQuery;
+ updatePostsInCurrentSearch(0);
+
+ // track that the user performed a search
+ if (!trimQuery.equals("")) {
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("query", trimQuery);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_PERFORMED, properties);
+ }
+ }
+
+ /*
+ * reuse "empty" view to let user know what they're querying
+ */
+ private void showSearchMessage() {
+ if (!isAdded()) return;
+
+ // clear posts so only the empty view is visible
+ getPostAdapter().clear();
+
+ setEmptyTitleAndDescription(false);
+ showEmptyView();
+ }
+
+ private void hideSearchMessage() {
+ hideEmptyView();
+ }
+
+ /*
+ * create and assign the suggestion adapter for the search view
+ */
+ private void createSearchSuggestionAdapter() {
+ mSearchSuggestionAdapter = new ReaderSearchSuggestionAdapter(getActivity());
+ mSearchView.setSuggestionsAdapter(mSearchSuggestionAdapter);
+
+ mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
+ @Override
+ public boolean onSuggestionSelect(int position) {
+ return false;
+ }
+
+ @Override
+ public boolean onSuggestionClick(int position) {
+ String query = mSearchSuggestionAdapter.getSuggestion(position);
+ if (!TextUtils.isEmpty(query)) {
+ mSearchView.setQuery(query, true);
+ }
+ return true;
+ }
+ });
+ }
+
+ private void populateSearchSuggestionAdapter(String query) {
+ if (mSearchSuggestionAdapter == null) {
+ createSearchSuggestionAdapter();
+ }
+ mSearchSuggestionAdapter.setFilter(query);
+ }
+
+ private void resetSearchSuggestionAdapter() {
+ mSearchView.setSuggestionsAdapter(null);
+ mSearchSuggestionAdapter = null;
+ }
+
+ /*
+ * is the search input showing?
+ */
+ private boolean isSearchViewExpanded() {
+ return mSearchView != null && !mSearchView.isIconified();
+ }
+
+ private boolean isSearchViewEmpty() {
+ return mSearchView != null && mSearchView.getQuery().length() == 0;
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.SearchPostsStarted event) {
+ if (!isAdded()) return;
+
+ UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER;
+ setIsUpdating(true, updateAction);
+ setEmptyTitleAndDescription(false);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.SearchPostsEnded event) {
+ if (!isAdded()) return;
+
+ UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER;
+ setIsUpdating(false, updateAction);
+
+ // load the results if the search succeeded and it's the current search - note that success
+ // means the search didn't fail, not necessarily that is has results - which is fine because
+ // if there aren't results then refreshing will show the empty message
+ if (event.didSucceed()
+ && getPostListType() == ReaderPostListType.SEARCH_RESULTS
+ && event.getQuery().equals(mCurrentSearchQuery)) {
+ refreshPosts();
+ }
+ }
+
+ /*
+ * called when user taps follow item in popup menu for a post
+ */
+ private void toggleFollowStatusForPost(final ReaderPost post) {
+ if (post == null
+ || !hasPostAdapter()
+ || !NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ final boolean isAskingToFollow = !ReaderPostTable.isPostFollowed(post);
+
+ ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (isAdded() && !succeeded) {
+ int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog);
+ ToastUtils.showToast(getActivity(), resId);
+ getPostAdapter().setFollowStatusForBlog(post.blogId, !isAskingToFollow);
+ }
+ }
+ };
+
+ if (ReaderBlogActions.followBlogForPost(post, isAskingToFollow, actionListener)) {
+ getPostAdapter().setFollowStatusForBlog(post.blogId, isAskingToFollow);
+ }
+ }
+
+ /*
+ * blocks the blog associated with the passed post and removes all posts in that blog
+ * from the adapter
+ */
+ private void blockBlogForPost(final ReaderPost post) {
+ if (post == null
+ || !isAdded()
+ || !hasPostAdapter()
+ || !NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (!succeeded && isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.reader_toast_err_block_blog, ToastUtils.Duration.LONG);
+ }
+ }
+ };
+
+ // perform call to block this blog - returns list of posts deleted by blocking so
+ // they can be restored if the user undoes the block
+ final BlockedBlogResult blockResult = ReaderBlogActions.blockBlogFromReader(post.blogId, actionListener);
+ // Only pass the blogID if available. Do not track feedID
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.READER_BLOG_BLOCKED,
+ mCurrentBlogId != 0 ? mCurrentBlogId : null
+ );
+
+ // remove posts in this blog from the adapter
+ getPostAdapter().removePostsInBlog(post.blogId);
+
+ // show the undo snackbar enabling the user to undo the block
+ View.OnClickListener undoListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderBlogActions.undoBlockBlogFromReader(blockResult);
+ refreshPosts();
+ }
+ };
+ Snackbar.make(getView(), getString(R.string.reader_toast_blog_blocked), Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, undoListener)
+ .show();
+ }
+
+ /*
+ * box/pages animation that appears when loading an empty list
+ */
+ private boolean shouldShowBoxAndPagesAnimation() {
+ return getPostListType().isTagType();
+ }
+
+ private void startBoxAndPagesAnimation() {
+ if (!isAdded()) return;
+
+ ImageView page1 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page1);
+ ImageView page2 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page2);
+ ImageView page3 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page3);
+
+ page1.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page1));
+ page2.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page2));
+ page3.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page3));
+ }
+
+ private void setEmptyTitleAndDescription(boolean requestFailed) {
+ if (!isAdded()) return;
+
+ String title;
+ String description = null;
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ title = getString(R.string.reader_empty_posts_no_connection);
+ } else if (requestFailed) {
+ title = getString(R.string.reader_empty_posts_request_failed);
+ } else if (isUpdating() && getPostListType() != ReaderPostListType.SEARCH_RESULTS) {
+ title = getString(R.string.reader_empty_posts_in_tag_updating);
+ } else {
+ switch (getPostListType()) {
+ case TAG_FOLLOWED:
+ if (getCurrentTag().isFollowedSites()) {
+ if (ReaderBlogTable.hasFollowedBlogs()) {
+ title = getString(R.string.reader_empty_followed_blogs_no_recent_posts_title);
+ description = getString(R.string.reader_empty_followed_blogs_no_recent_posts_description);
+ } else {
+ title = getString(R.string.reader_empty_followed_blogs_title);
+ description = getString(R.string.reader_empty_followed_blogs_description);
+ }
+ } else if (getCurrentTag().isPostsILike()) {
+ title = getString(R.string.reader_empty_posts_liked);
+ } else if (getCurrentTag().isListTopic()) {
+ title = getString(R.string.reader_empty_posts_in_custom_list);
+ } else {
+ title = getString(R.string.reader_empty_posts_in_tag);
+ }
+ break;
+
+ case BLOG_PREVIEW:
+ title = getString(R.string.reader_empty_posts_in_blog);
+ break;
+
+ case SEARCH_RESULTS:
+ if (isSearchViewEmpty() || TextUtils.isEmpty(mCurrentSearchQuery)) {
+ title = getString(R.string.reader_label_post_search_explainer);
+ } else if (isUpdating()) {
+ title = getString(R.string.reader_label_post_search_running);
+ } else {
+ title = getString(R.string.reader_empty_posts_in_search_title);
+ String formattedQuery = "<em>" + mCurrentSearchQuery + "</em>";
+ description = String.format(getString(R.string.reader_empty_posts_in_search_description), formattedQuery);
+ }
+ break;
+
+ default:
+ title = getString(R.string.reader_empty_posts_in_tag);
+ break;
+ }
+ }
+
+ setEmptyTitleAndDescription(title, description);
+ }
+
+ private void setEmptyTitleAndDescription(@NonNull String title, String description) {
+ if (!isAdded()) return;
+
+ TextView titleView = (TextView) mEmptyView.findViewById(R.id.title_empty);
+ titleView.setText(title);
+
+ TextView descriptionView = (TextView) mEmptyView.findViewById(R.id.description_empty);
+ if (description == null) {
+ descriptionView.setVisibility(View.INVISIBLE);
+ } else {
+ if (description.contains("<") && description.contains(">")) {
+ descriptionView.setText(Html.fromHtml(description));
+ } else {
+ descriptionView.setText(description);
+ }
+ descriptionView.setVisibility(View.VISIBLE);
+ }
+
+ mEmptyViewBoxImages.setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE);
+ }
+
+ private void showEmptyView() {
+ if (isAdded()) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideEmptyView() {
+ if (isAdded()) {
+ mEmptyView.setVisibility(View.GONE);
+ }
+ }
+
+ /*
+ * called by post adapter when data has been loaded
+ */
+ private final ReaderInterfaces.DataLoadedListener mDataLoadedListener = new ReaderInterfaces.DataLoadedListener() {
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ if (!isAdded()) {
+ return;
+ }
+ mRecyclerView.setRefreshing(false);
+ if (isEmpty) {
+ setEmptyTitleAndDescription(false);
+ showEmptyView();
+ if (shouldShowBoxAndPagesAnimation()) {
+ startBoxAndPagesAnimation();
+ }
+ } else {
+ hideEmptyView();
+ if (mRestorePosition > 0) {
+ AppLog.d(T.READER, "reader post list > restoring position");
+ mRecyclerView.scrollRecycleViewToPosition(mRestorePosition);
+ }
+ }
+ mRestorePosition = 0;
+ }
+ };
+
+ /*
+ * called by post adapter to load older posts when user scrolls to the last post
+ */
+ private final ReaderActions.DataRequestedListener mDataRequestedListener = new ReaderActions.DataRequestedListener() {
+ @Override
+ public void onRequestData() {
+ // skip if update is already in progress
+ if (isUpdating()) {
+ return;
+ }
+
+ // request older posts unless we already have the max # to show
+ switch (getPostListType()) {
+ case TAG_FOLLOWED:
+ case TAG_PREVIEW:
+ if (ReaderPostTable.getNumPostsWithTag(mCurrentTag) < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) {
+ // request older posts
+ updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_OLDER);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL);
+ }
+ break;
+
+ case BLOG_PREVIEW:
+ int numPosts;
+ if (mCurrentFeedId != 0) {
+ numPosts = ReaderPostTable.getNumPostsInFeed(mCurrentFeedId);
+ } else {
+ numPosts = ReaderPostTable.getNumPostsInBlog(mCurrentBlogId);
+ }
+ if (numPosts < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) {
+ updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_OLDER);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL);
+ }
+ break;
+
+ case SEARCH_RESULTS:
+ ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery);
+ int offset = ReaderPostTable.getNumPostsWithTag(searchTag);
+ if (offset < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) {
+ updatePostsInCurrentSearch(offset);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL);
+ }
+ break;
+ }
+ }
+ };
+
+ private ReaderPostAdapter getPostAdapter() {
+ if (mPostAdapter == null) {
+ AppLog.d(T.READER, "reader post list > creating post adapter");
+ Context context = WPActivityUtils.getThemedContext(getActivity());
+ mPostAdapter = new ReaderPostAdapter(context, getPostListType());
+ mPostAdapter.setOnPostSelectedListener(this);
+ mPostAdapter.setOnTagSelectedListener(this);
+ mPostAdapter.setOnPostPopupListener(this);
+ mPostAdapter.setOnDataLoadedListener(mDataLoadedListener);
+ mPostAdapter.setOnDataRequestedListener(mDataRequestedListener);
+ if (getActivity() instanceof ReaderSiteHeaderView.OnBlogInfoLoadedListener) {
+ mPostAdapter.setOnBlogInfoLoadedListener((ReaderSiteHeaderView.OnBlogInfoLoadedListener) getActivity());
+ }
+ if (getPostListType().isTagType()) {
+ mPostAdapter.setCurrentTag(getCurrentTag());
+ } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
+ mPostAdapter.setCurrentBlogAndFeed(mCurrentBlogId, mCurrentFeedId);
+ } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) {
+ ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery);
+ mPostAdapter.setCurrentTag(searchTag);
+ }
+ }
+ return mPostAdapter;
+ }
+
+ private boolean hasPostAdapter() {
+ return (mPostAdapter != null);
+ }
+
+ private boolean isPostAdapterEmpty() {
+ return (mPostAdapter == null || mPostAdapter.isEmpty());
+ }
+
+ private boolean isCurrentTag(final ReaderTag tag) {
+ return ReaderTag.isSameTag(tag, mCurrentTag);
+ }
+ private boolean isCurrentTagName(String tagName) {
+ return (tagName != null && tagName.equalsIgnoreCase(getCurrentTagName()));
+ }
+
+ private ReaderTag getCurrentTag() {
+ return mCurrentTag;
+ }
+
+ private String getCurrentTagName() {
+ return (mCurrentTag != null ? mCurrentTag.getTagSlug() : "");
+ }
+
+ private boolean hasCurrentTag() {
+ return mCurrentTag != null;
+ }
+
+ private void setCurrentTag(final ReaderTag tag) {
+ if (tag == null) {
+ return;
+ }
+
+ // skip if this is already the current tag and the post adapter is already showing it
+ if (isCurrentTag(tag)
+ && hasPostAdapter()
+ && getPostAdapter().isCurrentTag(tag)) {
+ return;
+ }
+
+ mCurrentTag = tag;
+
+ switch (getPostListType()) {
+ case TAG_FOLLOWED:
+ // remember this as the current tag if viewing followed tag
+ AppPrefs.setReaderTag(tag);
+ break;
+ case TAG_PREVIEW:
+ mTagPreviewHistory.push(tag.getTagSlug());
+ break;
+ }
+
+ getPostAdapter().setCurrentTag(tag);
+ hideNewPostsBar();
+ showLoadingProgress(false);
+
+ updateCurrentTagIfTime();
+ }
+
+ /*
+ * called by the activity when user hits the back button - returns true if the back button
+ * is handled here and should be ignored by the activity
+ */
+ @Override
+ public boolean onActivityBackPressed() {
+ if (isSearchViewExpanded()) {
+ mSearchMenuItem.collapseActionView();
+ return true;
+ } else if (goBackInTagHistory()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /*
+ * when previewing posts with a specific tag, a history of previewed tags is retained so
+ * the user can navigate back through them - this is faster and requires less memory
+ * than creating a new fragment for each previewed tag
+ */
+ private boolean goBackInTagHistory() {
+ if (mTagPreviewHistory.empty()) {
+ return false;
+ }
+
+ String tagName = mTagPreviewHistory.pop();
+ if (isCurrentTagName(tagName)) {
+ if (mTagPreviewHistory.empty()) {
+ return false;
+ }
+ tagName = mTagPreviewHistory.pop();
+ }
+
+ ReaderTag newTag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED);
+ setCurrentTag(newTag);
+
+ return true;
+ }
+
+ /*
+ * load tags on which the main data will be filtered
+ */
+ private void loadTags(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener) {
+ new LoadTagsTask(listener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ /*
+ * refresh adapter so latest posts appear
+ */
+ private void refreshPosts() {
+ hideNewPostsBar();
+ if (hasPostAdapter()) {
+ getPostAdapter().refresh();
+ }
+ }
+
+ /*
+ * same as above but clears posts before refreshing
+ */
+ private void reloadPosts() {
+ hideNewPostsBar();
+ if (hasPostAdapter()) {
+ getPostAdapter().reload();
+ }
+ }
+
+ /*
+ * reload the list of tags for the dropdown filter
+ */
+ private void reloadTags() {
+ if (isAdded() && mRecyclerView != null) {
+ mRecyclerView.refreshFilterCriteriaOptions();
+ }
+ }
+
+ /*
+ * get posts for the current blog from the server
+ */
+ private void updatePostsInCurrentBlogOrFeed(final UpdateAction updateAction) {
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ AppLog.i(T.READER, "reader post list > network unavailable, canceled blog update");
+ return;
+ }
+ if (mCurrentFeedId != 0) {
+ ReaderPostService.startServiceForFeed(getActivity(), mCurrentFeedId, updateAction);
+ } else {
+ ReaderPostService.startServiceForBlog(getActivity(), mCurrentBlogId, updateAction);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.UpdatePostsStarted event) {
+ if (!isAdded()) return;
+
+ setIsUpdating(true, event.getAction());
+ setEmptyTitleAndDescription(false);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) {
+ if (!isAdded()) return;
+
+ setIsUpdating(false, event.getAction());
+ if (event.getReaderTag() != null && !isCurrentTag(event.getReaderTag())) {
+ return;
+ }
+
+ // don't show new posts if user is searching - posts will automatically
+ // appear when search is exited
+ if (isSearchViewExpanded()
+ || getPostListType() == ReaderPostListType.SEARCH_RESULTS) {
+ return;
+ }
+
+ // determine whether to show the "new posts" bar - when this is shown, the newly
+ // downloaded posts aren't displayed until the user taps the bar - only appears
+ // when there are new posts in a followed tag and the user has scrolled the list
+ // beyond the first post
+ if (event.getResult() == ReaderActions.UpdateResult.HAS_NEW
+ && event.getAction() == UpdateAction.REQUEST_NEWER
+ && getPostListType() == ReaderPostListType.TAG_FOLLOWED
+ && !isPostAdapterEmpty()
+ && (!isAdded() || !mRecyclerView.isFirstItemVisible())) {
+ showNewPostsBar();
+ } else if (event.getResult().isNewOrChanged()) {
+ refreshPosts();
+ } else {
+ boolean requestFailed = (event.getResult() == ReaderActions.UpdateResult.FAILED);
+ setEmptyTitleAndDescription(requestFailed);
+ // if we requested posts in order to fill a gap but the request failed or didn't
+ // return any posts, reload the adapter so the gap marker is reset (hiding its
+ // progress bar)
+ if (event.getAction() == UpdateAction.REQUEST_OLDER_THAN_GAP) {
+ reloadPosts();
+ }
+ }
+ }
+
+ /*
+ * get latest posts for this tag from the server
+ */
+ private void updatePostsWithTag(ReaderTag tag, UpdateAction updateAction) {
+ if (!isAdded()) return;
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ AppLog.i(T.READER, "reader post list > network unavailable, canceled tag update");
+ return;
+ }
+ if (tag == null) {
+ AppLog.w(T.READER, "null tag passed to updatePostsWithTag");
+ return;
+ }
+ AppLog.d(T.READER, "reader post list > updating tag " + tag.getTagNameForLog() + ", updateAction=" + updateAction.name());
+ ReaderPostService.startServiceForTag(getActivity(), tag, updateAction);
+ }
+
+ private void updateCurrentTag() {
+ updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_NEWER);
+ }
+
+ /*
+ * update the current tag if it's time to do so - note that the check is done in the
+ * background since it can be expensive and this is called when the fragment is
+ * resumed, which on slower devices can result in a janky experience
+ */
+ private void updateCurrentTagIfTime() {
+ if (!isAdded() || !hasCurrentTag()) {
+ return;
+ }
+ new Thread() {
+ @Override
+ public void run() {
+ if (ReaderTagTable.shouldAutoUpdateTag(getCurrentTag()) && isAdded()) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateCurrentTag();
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ private boolean isUpdating() {
+ return mIsUpdating;
+ }
+
+ /*
+ * show/hide progress bar which appears at the bottom of the activity when loading more posts
+ */
+ private void showLoadingProgress(boolean showProgress) {
+ if (isAdded() && mProgress != null) {
+ if (showProgress) {
+ mProgress.bringToFront();
+ mProgress.setVisibility(View.VISIBLE);
+ } else {
+ mProgress.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void setIsUpdating(boolean isUpdating, UpdateAction updateAction) {
+ if (!isAdded() || mIsUpdating == isUpdating) {
+ return;
+ }
+
+ if (updateAction == UpdateAction.REQUEST_OLDER) {
+ // show/hide progress bar at bottom if these are older posts
+ showLoadingProgress(isUpdating);
+ } else if (isUpdating && isPostAdapterEmpty()) {
+ // show swipe-to-refresh if update started and no posts are showing
+ mRecyclerView.setRefreshing(true);
+ } else if (!isUpdating) {
+ // hide swipe-to-refresh progress if update is complete
+ mRecyclerView.setRefreshing(false);
+ }
+ mIsUpdating = isUpdating;
+
+ // if swipe-to-refresh isn't active, keep it disabled during an update - this prevents
+ // doing a refresh while another update is already in progress
+ if (mRecyclerView != null && !mRecyclerView.isRefreshing()) {
+ mRecyclerView.setSwipeToRefreshEnabled(!isUpdating && isSwipeToRefreshSupported());
+ }
+ }
+
+ /*
+ * swipe-to-refresh isn't supported for search results since they're really brief snapshots
+ * and are unlikely to show new posts due to the way they're sorted
+ */
+ private boolean isSwipeToRefreshSupported() {
+ return getPostListType() != ReaderPostListType.SEARCH_RESULTS;
+ }
+
+ /*
+ * bar that appears at the top when new posts have been retrieved
+ */
+ private boolean isNewPostsBarShowing() {
+ return (mNewPostsBar != null && mNewPostsBar.getVisibility() == View.VISIBLE);
+ }
+
+ /*
+ * scroll listener assigned to the recycler when the "new posts" bar is shown to hide
+ * it upon scrolling
+ */
+ private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+ hideNewPostsBar();
+ }
+ };
+
+ private void showNewPostsBar() {
+ if (!isAdded() || isNewPostsBarShowing()) {
+ return;
+ }
+
+ AniUtils.startAnimation(mNewPostsBar, R.anim.reader_top_bar_in);
+ mNewPostsBar.setVisibility(View.VISIBLE);
+
+ // assign the scroll listener to hide the bar when the recycler is scrolled, but don't assign
+ // it right away since the user may be scrolling when the bar appears (which would cause it
+ // to disappear as soon as it's displayed)
+ mRecyclerView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isAdded() && isNewPostsBarShowing()) {
+ mRecyclerView.addOnScrollListener(mOnScrollListener);
+ }
+ }
+ }, 1000L);
+
+ // remove the gap marker if it's showing, since it's no longer valid
+ getPostAdapter().removeGapMarker();
+ }
+
+ private void hideNewPostsBar() {
+ if (!isAdded() || !isNewPostsBarShowing() || mIsAnimatingOutNewPostsBar) {
+ return;
+ }
+
+ mIsAnimatingOutNewPostsBar = true;
+
+ // remove the onScrollListener assigned in showNewPostsBar()
+ mRecyclerView.removeOnScrollListener(mOnScrollListener);
+
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (isAdded()) {
+ mNewPostsBar.setVisibility(View.GONE);
+ mIsAnimatingOutNewPostsBar = false;
+ }
+ }
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+ AniUtils.startAnimation(mNewPostsBar, R.anim.reader_top_bar_out, listener);
+ }
+
+ /*
+ * are we showing all posts with a specific tag (followed or previewed), or all
+ * posts in a specific blog?
+ */
+ private ReaderPostListType getPostListType() {
+ return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE);
+ }
+
+ /*
+ * called from adapter when user taps a post
+ */
+ @Override
+ public void onPostSelected(ReaderPost post) {
+ if (!isAdded() || post == null) return;
+
+ // "discover" posts that highlight another post should open the original (source) post when tapped
+ if (post.isDiscoverPost()) {
+ ReaderPostDiscoverData discoverData = post.getDiscoverData();
+ if (discoverData != null && discoverData.getDiscoverType() == ReaderPostDiscoverData.DiscoverType.EDITOR_PICK) {
+ if (discoverData.getBlogId() != 0 && discoverData.getPostId() != 0) {
+ ReaderActivityLauncher.showReaderPostDetail(
+ getActivity(),
+ discoverData.getBlogId(),
+ discoverData.getPostId());
+ return;
+ } else if (discoverData.hasPermalink()) {
+ // if we don't have a blogId/postId, we sadly resort to showing the post
+ // in a WebView activity - this will happen for non-JP self-hosted
+ ReaderActivityLauncher.openUrl(getActivity(), discoverData.getPermaLink());
+ return;
+ }
+ }
+ }
+
+ // if this is a cross-post, we want to show the original post
+ if (post.isXpost()) {
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.xpostBlogId, post.xpostPostId);
+ return;
+ }
+
+ ReaderPostListType type = getPostListType();
+
+ switch (type) {
+ case TAG_FOLLOWED:
+ case TAG_PREVIEW:
+ ReaderActivityLauncher.showReaderPostPagerForTag(
+ getActivity(),
+ getCurrentTag(),
+ getPostListType(),
+ post.blogId,
+ post.postId);
+ break;
+ case BLOG_PREVIEW:
+ ReaderActivityLauncher.showReaderPostPagerForBlog(
+ getActivity(),
+ post.blogId,
+ post.postId);
+ break;
+ case SEARCH_RESULTS:
+ AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_SEARCH_RESULT_TAPPED, post);
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.blogId, post.postId);
+ break;
+ }
+ }
+
+ /*
+ * called from adapter when user taps a tag on a post to display tag preview
+ */
+ @Override
+ public void onTagSelected(String tagName) {
+ if (!isAdded()) return;
+
+ ReaderTag tag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED);
+ if (getPostListType().equals(ReaderPostListType.TAG_PREVIEW)) {
+ // user is already previewing a tag, so change current tag in existing preview
+ setCurrentTag(tag);
+ } else {
+ // user isn't previewing a tag, so open in tag preview
+ ReaderActivityLauncher.showReaderTagPreview(getActivity(), tag);
+ }
+ }
+
+ /*
+ * called when user selects a tag from the tag toolbar
+ */
+ private void onTagChanged(ReaderTag tag) {
+ if (!isAdded() || isCurrentTag(tag)) return;
+
+ trackTagLoaded(tag);
+ AppLog.d(T.READER, String.format("reader post list > tag %s displayed", tag.getTagNameForLog()));
+ setCurrentTag(tag);
+ }
+
+ private void trackTagLoaded(ReaderTag tag) {
+ AnalyticsTracker.Stat stat = null;
+
+ if (tag.isDiscover()) {
+ stat = AnalyticsTracker.Stat.READER_DISCOVER_VIEWED;
+ } else if (tag.isTagTopic()) {
+ stat = AnalyticsTracker.Stat.READER_TAG_LOADED;
+ } else if (tag.isListTopic()) {
+ stat = AnalyticsTracker.Stat.READER_LIST_LOADED;
+ }
+
+ if (stat == null) return;
+
+ Map<String, String> properties = new HashMap<>();
+ properties.put("tag", tag.getTagSlug());
+
+ AnalyticsTracker.track(stat, properties);
+ }
+
+ /*
+ * called when user taps "..." icon next to a post
+ */
+ @Override
+ public void onShowPostPopup(View view, final ReaderPost post) {
+ if (view == null || post == null || !isAdded()) return;
+
+ Context context = view.getContext();
+ final ListPopupWindow listPopup = new ListPopupWindow(context);
+ listPopup.setAnchorView(view);
+ listPopup.setWidth(context.getResources().getDimensionPixelSize(R.dimen.menu_item_width));
+ listPopup.setModal(true);
+
+ List<Integer> menuItems = new ArrayList<>();
+ boolean isFollowed = ReaderPostTable.isPostFollowed(post);
+ if (isFollowed) {
+ menuItems.add(ReaderMenuAdapter.ITEM_UNFOLLOW);
+ } else {
+ menuItems.add(ReaderMenuAdapter.ITEM_FOLLOW);
+ }
+ if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
+ menuItems.add(ReaderMenuAdapter.ITEM_BLOCK);
+ }
+ listPopup.setAdapter(new ReaderMenuAdapter(context, menuItems));
+ listPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (!isAdded()) return;
+
+ listPopup.dismiss();
+ switch((int) id) {
+ case ReaderMenuAdapter.ITEM_FOLLOW:
+ case ReaderMenuAdapter.ITEM_UNFOLLOW:
+ toggleFollowStatusForPost(post);
+ break;
+ case ReaderMenuAdapter.ITEM_BLOCK:
+ blockBlogForPost(post);
+ break;
+ }
+ }
+ });
+ listPopup.show();
+ }
+
+ /*
+ * purge reader db if it hasn't been done yet
+ */
+ private void purgeDatabaseIfNeeded() {
+ if (!mHasPurgedReaderDb) {
+ AppLog.d(T.READER, "reader post list > purging database");
+ mHasPurgedReaderDb = true;
+ ReaderDatabase.purgeAsync();
+ }
+ }
+
+ /*
+ * start background service to get the latest followed tags and blogs if it's time to do so
+ */
+ private void updateFollowedTagsAndBlogsIfNeeded() {
+ if (mLastAutoUpdateDt != null) {
+ int minutesSinceLastUpdate = DateTimeUtils.minutesBetween(mLastAutoUpdateDt, new Date());
+ if (minutesSinceLastUpdate < 120) {
+ return;
+ }
+ }
+
+ AppLog.d(T.READER, "reader post list > updating tags and blogs");
+ mLastAutoUpdateDt = new Date();
+ ReaderUpdateService.startService(getActivity(), EnumSet.of(UpdateTask.TAGS, UpdateTask.FOLLOWED_BLOGS));
+ }
+
+ @Override
+ public void onScrollToTop() {
+ if (isAdded() && getCurrentPosition() > 0) {
+ mRecyclerView.smoothScrollToPosition(0);
+ mRecyclerView.showToolbar();
+ }
+ }
+
+ public static void resetLastUpdateDate() {
+ mLastAutoUpdateDt = null;
+ }
+
+ private class LoadTagsTask extends AsyncTask<Void, Void, ReaderTagList> {
+
+ private final FilteredRecyclerView.FilterCriteriaAsyncLoaderListener mFilterCriteriaLoaderListener;
+
+ public LoadTagsTask(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener){
+ mFilterCriteriaLoaderListener = listener;
+ }
+
+ @Override
+ protected ReaderTagList doInBackground(Void... voids) {
+ ReaderTagList tagList = ReaderTagTable.getDefaultTags();
+ tagList.addAll(ReaderTagTable.getCustomListTags());
+ tagList.addAll(ReaderTagTable.getFollowedTags());
+ return tagList;
+ }
+
+ @Override
+ protected void onPostExecute(ReaderTagList tagList) {
+ if (tagList != null && !tagList.isSameList(mTags)) {
+ mTags.clear();
+ mTags.addAll(tagList);
+ if (mFilterCriteriaLoaderListener != null)
+ //noinspection unchecked
+ mFilterCriteriaLoaderListener.onFilterCriteriasLoaded((List)mTags);
+ }
+ }
+ }
+
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java
new file mode 100644
index 000000000..5d2d4cbe5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java
@@ -0,0 +1,532 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v13.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.SparseArray;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostIdList;
+import org.wordpress.android.ui.reader.services.ReaderPostService;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.widgets.WPViewPager;
+
+import java.util.HashSet;
+
+import de.greenrobot.event.EventBus;
+
+/*
+ * shows reader post detail fragments in a ViewPager - primarily used for easy swiping between
+ * posts with a specific tag or in a specific blog, but can also be used to show a single
+ * post detail
+ */
+public class ReaderPostPagerActivity extends AppCompatActivity
+ implements ReaderInterfaces.AutoHideToolbarListener {
+
+ private WPViewPager mViewPager;
+ private ProgressBar mProgress;
+ private Toolbar mToolbar;
+
+ private ReaderTag mCurrentTag;
+ private long mBlogId;
+ private long mPostId;
+ private int mLastSelectedPosition = -1;
+ private ReaderPostListType mPostListType;
+
+ private boolean mIsRequestingMorePosts;
+ private boolean mIsSinglePostView;
+ private boolean mIsRelatedPostView;
+
+ private final HashSet<Integer> mTrackedPositions = new HashSet<>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.reader_activity_post_pager);
+
+ mToolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(mToolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mViewPager = (WPViewPager) findViewById(R.id.viewpager);
+ mProgress = (ProgressBar) findViewById(R.id.progress_loading);
+
+ if (savedInstanceState != null) {
+ mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
+ mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID);
+ mIsSinglePostView = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_SINGLE_POST);
+ mIsRelatedPostView = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_RELATED_POST);
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
+ }
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_TAG)) {
+ mCurrentTag = (ReaderTag) savedInstanceState.getSerializable(ReaderConstants.ARG_TAG);
+ }
+ } else {
+ mBlogId = getIntent().getLongExtra(ReaderConstants.ARG_BLOG_ID, 0);
+ mPostId = getIntent().getLongExtra(ReaderConstants.ARG_POST_ID, 0);
+ mIsSinglePostView = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_SINGLE_POST, false);
+ mIsRelatedPostView = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_RELATED_POST, false);
+ if (getIntent().hasExtra(ReaderConstants.ARG_POST_LIST_TYPE)) {
+ mPostListType = (ReaderPostListType) getIntent().getSerializableExtra(ReaderConstants.ARG_POST_LIST_TYPE);
+ }
+ if (getIntent().hasExtra(ReaderConstants.ARG_TAG)) {
+ mCurrentTag = (ReaderTag) getIntent().getSerializableExtra(ReaderConstants.ARG_TAG);
+ }
+ }
+
+ if (mPostListType == null) {
+ mPostListType = ReaderPostListType.TAG_FOLLOWED;
+ }
+
+ setTitle(mIsRelatedPostView ? R.string.reader_title_related_post_detail : R.string.reader_title_post_detail);
+
+ // for related posts, show an X in the toolbar which closes the activity - using the
+ // back button will navigate through related posts
+ if (mIsRelatedPostView) {
+ mToolbar.setNavigationIcon(R.drawable.ic_close_white_24dp);
+ mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+ }
+
+ mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ onShowHideToolbar(true);
+ trackPostAtPositionIfNeeded(position);
+
+ // pause the previous web view - important because otherwise embedded content
+ // will continue to play
+ if (mLastSelectedPosition > -1 && mLastSelectedPosition != position) {
+ ReaderPostDetailFragment lastFragment = getDetailFragmentAtPosition(mLastSelectedPosition);
+ if (lastFragment != null) {
+ lastFragment.pauseWebView();
+ }
+ }
+
+ // resume the newly active webView if it was previously paused
+ ReaderPostDetailFragment thisFragment = getDetailFragmentAtPosition(position);
+ if (thisFragment != null) {
+ thisFragment.resumeWebViewIfPaused();
+ }
+
+ mLastSelectedPosition = position;
+ }
+ });
+
+ mViewPager.setPageTransformer(false,
+ new ReaderViewPagerTransformer(ReaderViewPagerTransformer.TransformType.SLIDE_OVER));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ EventBus.getDefault().register(this);
+ if (!hasPagerAdapter()) {
+ loadPosts(mBlogId, mPostId);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ EventBus.getDefault().unregister(this);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean hasPagerAdapter() {
+ return (mViewPager != null && mViewPager.getAdapter() != null);
+ }
+
+ private PostPagerAdapter getPagerAdapter() {
+ if (mViewPager != null && mViewPager.getAdapter() != null) {
+ return (PostPagerAdapter) mViewPager.getAdapter();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putBoolean(ReaderConstants.ARG_IS_SINGLE_POST, mIsSinglePostView);
+ outState.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, mIsRelatedPostView);
+
+ if (hasCurrentTag()) {
+ outState.putSerializable(ReaderConstants.ARG_TAG, getCurrentTag());
+ }
+ if (getPostListType() != null) {
+ outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType());
+ }
+
+ ReaderBlogIdPostId id = getAdapterCurrentBlogIdPostId();
+ if (id != null) {
+ outState.putLong(ReaderConstants.ARG_BLOG_ID, id.getBlogId());
+ outState.putLong(ReaderConstants.ARG_POST_ID, id.getPostId());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private ReaderBlogIdPostId getAdapterCurrentBlogIdPostId() {
+ PostPagerAdapter adapter = getPagerAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return adapter.getCurrentBlogIdPostId();
+ }
+
+ private ReaderBlogIdPostId getAdapterBlogIdPostIdAtPosition(int position) {
+ PostPagerAdapter adapter = getPagerAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return adapter.getBlogIdPostIdAtPosition(position);
+ }
+
+ @Override
+ public void onBackPressed() {
+ ReaderPostDetailFragment fragment = getActiveDetailFragment();
+ if (fragment != null && fragment.isCustomViewShowing()) {
+ // if full screen video is showing, hide the custom view rather than navigate back
+ fragment.hideCustomView();
+ } else if (fragment != null && fragment.goBackInPostHistory()) {
+ // noop - fragment moved back to a previous post
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ /*
+ * perform analytics tracking and bump the page view for the post at the passed position
+ * if it hasn't already been done
+ */
+ private void trackPostAtPositionIfNeeded(int position) {
+ if (!hasPagerAdapter() || mTrackedPositions.contains(position)) return;
+
+ ReaderBlogIdPostId idPair = getAdapterBlogIdPostIdAtPosition(position);
+ if (idPair == null) return;
+
+ AppLog.d(AppLog.T.READER, "reader pager > tracking post at position " + position);
+ mTrackedPositions.add(position);
+
+ // bump the page view
+ ReaderPostActions.bumpPageViewForPost(idPair.getBlogId(), idPair.getPostId());
+
+ // analytics tracking
+ AnalyticsUtils.trackWithReaderPostDetails(
+ AnalyticsTracker.Stat.READER_ARTICLE_OPENED,
+ ReaderPostTable.getPost(idPair.getBlogId(), idPair.getPostId(), true));
+ }
+
+ /*
+ * loads the blogId/postId pairs used to populate the pager adapter - passed blogId/postId will
+ * be made active after loading unless gotoNext=true, in which case the post after the passed
+ * one will be made active
+ */
+ private void loadPosts(final long blogId, final long postId) {
+ new Thread() {
+ @Override
+ public void run() {
+ final ReaderBlogIdPostIdList idList;
+ if (mIsSinglePostView) {
+ idList = new ReaderBlogIdPostIdList();
+ idList.add(new ReaderBlogIdPostId(blogId, postId));
+ } else {
+ int maxPosts = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY;
+ switch (getPostListType()) {
+ case TAG_FOLLOWED:
+ case TAG_PREVIEW:
+ idList = ReaderPostTable.getBlogIdPostIdsWithTag(getCurrentTag(), maxPosts);
+ break;
+ case BLOG_PREVIEW:
+ idList = ReaderPostTable.getBlogIdPostIdsInBlog(blogId, maxPosts);
+ break;
+ default:
+ return;
+ }
+ }
+
+ final int currentPosition = mViewPager.getCurrentItem();
+ final int newPosition = idList.indexOf(blogId, postId);
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ AppLog.d(AppLog.T.READER, "reader pager > creating adapter");
+ PostPagerAdapter adapter =
+ new PostPagerAdapter(getFragmentManager(), idList);
+ mViewPager.setAdapter(adapter);
+ if (adapter.isValidPosition(newPosition)) {
+ mViewPager.setCurrentItem(newPosition);
+ trackPostAtPositionIfNeeded(newPosition);
+ } else if (adapter.isValidPosition(currentPosition)) {
+ mViewPager.setCurrentItem(currentPosition);
+ trackPostAtPositionIfNeeded(currentPosition);
+ }
+ }
+ });
+ }
+ }.start();
+ }
+
+ private ReaderTag getCurrentTag() {
+ return mCurrentTag;
+ }
+
+ private boolean hasCurrentTag() {
+ return mCurrentTag != null;
+ }
+
+ private ReaderPostListType getPostListType() {
+ return mPostListType;
+ }
+
+ private Fragment getActivePagerFragment() {
+ PostPagerAdapter adapter = getPagerAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return adapter.getActiveFragment();
+ }
+
+ private ReaderPostDetailFragment getActiveDetailFragment() {
+ Fragment fragment = getActivePagerFragment();
+ if (fragment instanceof ReaderPostDetailFragment) {
+ return (ReaderPostDetailFragment) fragment;
+ } else {
+ return null;
+ }
+ }
+
+ private Fragment getPagerFragmentAtPosition(int position) {
+ PostPagerAdapter adapter = getPagerAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return adapter.getFragmentAtPosition(position);
+ }
+
+ private ReaderPostDetailFragment getDetailFragmentAtPosition(int position) {
+ Fragment fragment = getPagerFragmentAtPosition(position);
+ if (fragment instanceof ReaderPostDetailFragment) {
+ return (ReaderPostDetailFragment) fragment;
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * called when user scrolls towards the last posts - requests older posts with the
+ * current tag or in the current blog
+ */
+ private void requestMorePosts() {
+ if (mIsRequestingMorePosts) return;
+
+ AppLog.d(AppLog.T.READER, "reader pager > requesting older posts");
+ switch (getPostListType()) {
+ case TAG_PREVIEW:
+ case TAG_FOLLOWED:
+ ReaderPostService.startServiceForTag(
+ this,
+ getCurrentTag(),
+ ReaderPostService.UpdateAction.REQUEST_OLDER);
+ break;
+
+ case BLOG_PREVIEW:
+ ReaderPostService.startServiceForBlog(
+ this,
+ mBlogId,
+ ReaderPostService.UpdateAction.REQUEST_OLDER);
+ break;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.UpdatePostsStarted event) {
+ if (isFinishing()) return;
+
+ mIsRequestingMorePosts = true;
+ mProgress.setVisibility(View.VISIBLE);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) {
+ if (isFinishing()) return;
+
+ PostPagerAdapter adapter = getPagerAdapter();
+ if (adapter == null) return;
+
+ mIsRequestingMorePosts = false;
+ mProgress.setVisibility(View.GONE);
+
+ if (event.getResult() == ReaderActions.UpdateResult.HAS_NEW) {
+ AppLog.d(AppLog.T.READER, "reader pager > older posts received");
+ // remember which post to keep active
+ ReaderBlogIdPostId id = adapter.getCurrentBlogIdPostId();
+ long blogId = (id != null ? id.getBlogId() : 0);
+ long postId = (id != null ? id.getPostId() : 0);
+ loadPosts(blogId, postId);
+ } else {
+ AppLog.d(AppLog.T.READER, "reader pager > all posts loaded");
+ adapter.mAllPostsLoaded = true;
+ }
+ }
+
+ /*
+ * called by detail fragment to show/hide the toolbar when user scrolls
+ */
+ @Override
+ public void onShowHideToolbar(boolean show) {
+ if (!isFinishing()) {
+ AniUtils.animateTopBar(mToolbar, show);
+ }
+ }
+
+ /**
+ * pager adapter containing post detail fragments
+ **/
+ private class PostPagerAdapter extends FragmentStatePagerAdapter {
+ private ReaderBlogIdPostIdList mIdList = new ReaderBlogIdPostIdList();
+ private boolean mAllPostsLoaded;
+
+ // this is used to retain created fragments so we can access them in
+ // getFragmentAtPosition() - necessary because the pager provides no
+ // built-in way to do this - note that destroyItem() removes fragments
+ // from this map when they're removed from the adapter, so this doesn't
+ // retain *every* fragment
+ private final SparseArray<Fragment> mFragmentMap = new SparseArray<>();
+
+ PostPagerAdapter(FragmentManager fm, ReaderBlogIdPostIdList ids) {
+ super(fm);
+ mIdList = (ReaderBlogIdPostIdList)ids.clone();
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ // work around "Fragement no longer exists for key" Android bug
+ // by catching the IllegalStateException
+ // https://code.google.com/p/android/issues/detail?id=42601
+ try {
+ AppLog.d(AppLog.T.READER, "reader pager > adapter restoreState");
+ super.restoreState(state, loader);
+ } catch (IllegalStateException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+
+ @Override
+ public Parcelable saveState() {
+ AppLog.d(AppLog.T.READER, "reader pager > adapter saveState");
+ return super.saveState();
+ }
+
+ private boolean canRequestMostPosts() {
+ return !mAllPostsLoaded
+ && !mIsSinglePostView
+ && mIdList.size() < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY
+ && NetworkUtils.isNetworkAvailable(ReaderPostPagerActivity.this);
+ }
+
+ boolean isValidPosition(int position) {
+ return (position >= 0 && position < getCount());
+ }
+
+ @Override
+ public int getCount() {
+ return mIdList.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if ((position == getCount() - 1) && canRequestMostPosts()) {
+ requestMorePosts();
+ }
+
+ return ReaderPostDetailFragment.newInstance(
+ mIdList.get(position).getBlogId(),
+ mIdList.get(position).getPostId(),
+ mIsRelatedPostView,
+ getPostListType());
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ Object item = super.instantiateItem(container, position);
+ if (item instanceof Fragment) {
+ mFragmentMap.put(position, (Fragment) item);
+ }
+ return item;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ mFragmentMap.remove(position);
+ super.destroyItem(container, position, object);
+ }
+
+ private Fragment getActiveFragment() {
+ return getFragmentAtPosition(mViewPager.getCurrentItem());
+ }
+
+ private Fragment getFragmentAtPosition(int position) {
+ if (isValidPosition(position)) {
+ return mFragmentMap.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ private ReaderBlogIdPostId getCurrentBlogIdPostId() {
+ return getBlogIdPostIdAtPosition(mViewPager.getCurrentItem());
+
+ }
+
+ ReaderBlogIdPostId getBlogIdPostIdAtPosition(int position) {
+ if (isValidPosition(position)) {
+ return mIdList.get(position);
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java
new file mode 100644
index 000000000..1a2552331
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java
@@ -0,0 +1,580 @@
+package org.wordpress.android.ui.reader;
+
+import android.annotation.SuppressLint;
+import android.net.Uri;
+import android.os.Handler;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostDiscoverData;
+import org.wordpress.android.ui.reader.utils.ImageSizeMap;
+import org.wordpress.android.ui.reader.utils.ImageSizeMap.ImageSize;
+import org.wordpress.android.ui.reader.utils.ReaderHtmlUtils;
+import org.wordpress.android.ui.reader.utils.ReaderIframeScanner;
+import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.views.ReaderWebView;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.regex.Pattern;
+
+/**
+ * generates and displays the HTML for post detail content - main purpose is to assign the
+ * height/width attributes on image tags to (1) avoid the webView resizing as images are
+ * loaded, and (2) avoid requesting images at a size larger than the display
+ *
+ * important to note that displayed images rely on dp rather than px sizes due to the
+ * fact that WebView "converts CSS pixel values to density-independent pixel values"
+ * http://developer.android.com/guide/webapps/targeting.html
+ */
+class ReaderPostRenderer {
+
+ private final ReaderResourceVars mResourceVars;
+ private final ReaderPost mPost;
+ private final int mMinFullSizeWidthDp;
+ private final int mMinMidSizeWidthDp;
+ private final WeakReference<ReaderWebView> mWeakWebView;
+
+ private StringBuilder mRenderBuilder;
+ private String mRenderedHtml;
+ private ImageSizeMap mAttachmentSizes;
+
+ @SuppressLint("SetJavaScriptEnabled")
+ ReaderPostRenderer(ReaderWebView webView, ReaderPost post) {
+ if (webView == null) {
+ throw new IllegalArgumentException("ReaderPostRenderer requires a webView");
+ }
+ if (post == null) {
+ throw new IllegalArgumentException("ReaderPostRenderer requires a post");
+ }
+
+ mPost = post;
+ mWeakWebView = new WeakReference<>(webView);
+ mResourceVars = new ReaderResourceVars(webView.getContext());
+
+ mMinFullSizeWidthDp = pxToDp(mResourceVars.fullSizeImageWidthPx / 3);
+ mMinMidSizeWidthDp = mMinFullSizeWidthDp / 2;
+
+ // enable JavaScript in the webView, otherwise videos and other embedded content won't
+ // work - note that the content is scrubbed on the backend so this is considered safe
+ webView.getSettings().setJavaScriptEnabled(true);
+ }
+
+ void beginRender() {
+ final Handler handler = new Handler();
+ mRenderBuilder = new StringBuilder(getPostContent());
+
+ new Thread() {
+ @Override
+ public void run() {
+ final boolean hasTiledGallery = hasTiledGallery(mRenderBuilder.toString());
+
+ if (!(hasTiledGallery && mResourceVars.isWideDisplay)) {
+ resizeImages();
+ }
+
+ resizeIframes();
+
+ final String htmlContent = formatPostContentForWebView(mRenderBuilder.toString(), hasTiledGallery,
+ mResourceVars.isWideDisplay);
+ mRenderBuilder = null;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ renderHtmlContent(htmlContent);
+ }
+ });
+ }
+ }.start();
+ }
+
+ public static boolean hasTiledGallery(String text) {
+ // determine whether a tiled-gallery exists in the content
+ return Pattern.compile("tiled-gallery[\\s\"']").matcher(text).find();
+ }
+
+ /*
+ * scan the content for images and make sure they're correctly sized for the device
+ */
+ private void resizeImages() {
+ ReaderHtmlUtils.HtmlScannerListener imageListener = new ReaderHtmlUtils.HtmlScannerListener() {
+ @Override
+ public void onTagFound(String imageTag, String imageUrl) {
+ if (!imageUrl.contains("wpcom-smileys")) {
+ replaceImageTag(imageTag, imageUrl);
+ }
+ }
+ };
+ ReaderImageScanner scanner = new ReaderImageScanner(mRenderBuilder.toString(), mPost.isPrivate);
+ scanner.beginScan(imageListener);
+ }
+
+ /*
+ * scan the content for iframes and make sure they're correctly sized for the device
+ */
+ private void resizeIframes() {
+ ReaderHtmlUtils.HtmlScannerListener iframeListener = new ReaderHtmlUtils.HtmlScannerListener() {
+ @Override
+ public void onTagFound(String tag, String src) {
+ replaceIframeTag(tag, src);
+ }
+ };
+ ReaderIframeScanner scanner = new ReaderIframeScanner(mRenderBuilder.toString());
+ scanner.beginScan(iframeListener);
+ }
+
+ /*
+ * called once the content is ready to be rendered in the webView
+ */
+ private void renderHtmlContent(final String htmlContent) {
+ mRenderedHtml = htmlContent;
+
+ // make sure webView is still valid (containing fragment may have been detached)
+ ReaderWebView webView = mWeakWebView.get();
+ if (webView == null || webView.getContext() == null || webView.isDestroyed()) {
+ AppLog.w(AppLog.T.READER, "reader renderer > webView invalid");
+ return;
+ }
+
+ // IMPORTANT: use loadDataWithBaseURL() since loadData() may fail
+ // https://code.google.com/p/android/issues/detail?id=4401
+ // also important to use null as the baseUrl since onPageFinished
+ // doesn't appear to fire when it's set to an actual url
+ webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null);
+ }
+
+ /*
+ * called when image scanner finds an image, tries to replace the image tag with one that
+ * has height & width attributes set correctly for the current display, if that fails
+ * replaces it with one that has our 'size-none' class
+ */
+ private void replaceImageTag(final String imageTag, final String imageUrl) {
+ ImageSize origSize = getImageSize(imageTag, imageUrl);
+ boolean hasWidth = (origSize != null && origSize.width > 0);
+ boolean isFullSize = hasWidth && (origSize.width >= mMinFullSizeWidthDp);
+ boolean isMidSize = hasWidth
+ && (origSize.width >= mMinMidSizeWidthDp)
+ && (origSize.width < mMinFullSizeWidthDp);
+
+ final String newImageTag;
+ if (isFullSize) {
+ newImageTag = makeFullSizeImageTag(imageUrl, origSize.width, origSize.height);
+ } else if (isMidSize) {
+ newImageTag = makeImageTag(imageUrl, origSize.width, origSize.height, "size-medium");
+ } else if (hasWidth) {
+ newImageTag = makeImageTag(imageUrl, origSize.width, origSize.height, "size-none");
+ } else {
+ newImageTag = "<img class='size-none' src='" + imageUrl + "' />";
+ }
+
+ int start = mRenderBuilder.indexOf(imageTag);
+ if (start == -1) {
+ AppLog.w(AppLog.T.READER, "reader renderer > image not found in builder");
+ return;
+ }
+
+ mRenderBuilder.replace(start, start + imageTag.length(), newImageTag);
+ }
+
+ private String makeImageTag(final String imageUrl, int width, int height, final String imageClass) {
+ String newImageUrl = ReaderUtils.getResizedImageUrl(imageUrl, width, height, mPost.isPrivate);
+ if (height > 0) {
+ return "<img class='" + imageClass + "'" +
+ " src='" + newImageUrl + "'" +
+ " width='" + pxToDp(width) + "'" +
+ " height='" + pxToDp(height) + "' />";
+ } else {
+ return "<img class='" + imageClass + "'" +
+ "src='" + newImageUrl + "'" +
+ " width='" + pxToDp(width) + "' />";
+ }
+ }
+
+ private String makeFullSizeImageTag(final String imageUrl, int width, int height) {
+ int newWidth;
+ int newHeight;
+ if (width > 0 && height > 0) {
+ if (height > width) {
+ //noinspection SuspiciousNameCombination
+ newHeight = mResourceVars.fullSizeImageWidthPx;
+ float ratio = ((float) width / (float) height);
+ newWidth = (int) (newHeight * ratio);
+ } else {
+ float ratio = ((float) height / (float) width);
+ newWidth = mResourceVars.fullSizeImageWidthPx;
+ newHeight = (int) (newWidth * ratio);
+ }
+ } else {
+ newWidth = mResourceVars.fullSizeImageWidthPx;
+ newHeight = 0;
+ }
+
+ return makeImageTag(imageUrl, newWidth, newHeight, "size-full");
+ }
+
+ /*
+ * returns true if the post has a featured image and there are no images in the
+ * post's content - when this is the case, the featured image is inserted at
+ * the top of the content
+ */
+ private boolean shouldAddFeaturedImage() {
+ return mPost.hasFeaturedImage()
+ && !mPost.getText().contains("<img")
+ && !PhotonUtils.isMshotsUrl(mPost.getFeaturedImage());
+ }
+
+ /*
+ * returns the basic content of the post tweaked for use here
+ */
+ private String getPostContent() {
+ // some content (such as Vimeo embeds) don't have "http:" before links
+ String content = mPost.getText().replace("src=\"//", "src=\"http://");
+
+ // add the featured image (if any)
+ if (shouldAddFeaturedImage()) {
+ AppLog.d(AppLog.T.READER, "reader renderer > added featured image");
+ content = getFeaturedImageHtml() + content;
+ }
+
+ // if this is a Discover post, add a link which shows the blog preview
+ if (mPost.isDiscoverPost()) {
+ ReaderPostDiscoverData discoverData = mPost.getDiscoverData();
+ if (discoverData != null && discoverData.getBlogId() != 0 && discoverData.hasBlogName()) {
+ String label = String.format(
+ WordPress.getContext().getString(R.string.reader_discover_visit_blog), discoverData.getBlogName());
+ String url = ReaderUtils.makeBlogPreviewUrl(discoverData.getBlogId());
+
+ String htmlDiscover = "<div id='discover'>"
+ + "<a href='" + url + "'>" + label + "</a>"
+ + "</div>";
+ content += htmlDiscover;
+ }
+ }
+
+ return content;
+ }
+
+ /*
+ * returns the HTML that was last rendered, will be null prior to rendering
+ */
+ String getRenderedHtml() {
+ return mRenderedHtml;
+ }
+
+ /*
+ * returns the HTML to use when inserting a featured image into the rendered content
+ */
+ private String getFeaturedImageHtml() {
+ String imageUrl = ReaderUtils.getResizedImageUrl(
+ mPost.getFeaturedImage(),
+ mResourceVars.fullSizeImageWidthPx,
+ mResourceVars.featuredImageHeightPx,
+ mPost.isPrivate);
+
+ return "<img class='size-full' src='" + imageUrl + "'/>";
+ }
+
+ /*
+ * replace the passed iframe tag with one that's correctly sized for the device
+ */
+ private void replaceIframeTag(final String tag, final String src) {
+ int width = ReaderHtmlUtils.getWidthAttrValue(tag);
+ int height = ReaderHtmlUtils.getHeightAttrValue(tag);
+
+ int newHeight;
+ int newWidth;
+ if (width > 0 && height > 0) {
+ float ratio = ((float) height / (float) width);
+ newWidth = mResourceVars.videoWidthPx;
+ newHeight = (int) (newWidth * ratio);
+ } else {
+ newWidth = mResourceVars.videoWidthPx;
+ newHeight = mResourceVars.videoHeightPx;
+ }
+
+ String newTag = "<iframe src='" + src + "'" +
+ " frameborder='0' allowfullscreen='true' allowtransparency='true'" +
+ " width='" + pxToDp(newWidth) + "'" +
+ " height='" + pxToDp(newHeight) + "' />";
+
+ int start = mRenderBuilder.indexOf(tag);
+ if (start == -1) {
+ AppLog.w(AppLog.T.READER, "reader renderer > iframe not found in builder");
+ return;
+ }
+
+ mRenderBuilder.replace(start, start + tag.length(), newTag);
+ }
+
+ /*
+ * returns the full content, including CSS, that will be shown in the WebView for this post
+ */
+ private String formatPostContentForWebView(final String content, boolean hasTiledGallery, boolean isWideDisplay) {
+ final boolean renderAsTiledGallery = hasTiledGallery && isWideDisplay;
+
+ // unique CSS class assigned to the gallery elements for easy selection
+ final String galleryOnlyClass = "gallery-only-class" + new Random().nextInt(1000);
+
+ @SuppressWarnings("StringBufferReplaceableByString")
+ StringBuilder sbHtml = new StringBuilder("<!DOCTYPE html><html><head><meta charset='UTF-8' />");
+
+ // title isn't necessary, but it's invalid html5 without one
+ sbHtml.append("<title>Reader Post</title>")
+
+ // https://developers.google.com/chrome/mobile/docs/webview/pixelperfect
+ .append("<meta name='viewport' content='width=device-width, initial-scale=1'>")
+
+ // use Merriweather font assets
+ .append("<link href='file:///android_asset/merriweather.css' rel='stylesheet' type='text/css'>")
+
+ .append("<style type='text/css'>")
+ .append(" body { font-family: Merriweather, serif; font-weight: 400; margin: 0px; padding: 0px;}")
+ .append(" body, p, div { max-width: 100% !important; word-wrap: break-word; }")
+
+ // set line-height, font-size but not for gallery divs when rendering as tiled gallery as those will be
+ // handled with the .tiled-gallery rules bellow.
+ .append(" p, div" + (renderAsTiledGallery ? ":not(." + galleryOnlyClass + ")" : "") +
+ ", li { line-height: 1.6em; font-size: 100%; }")
+
+ .append(" h1, h2 { line-height: 1.2em; }")
+
+ // counteract pre-defined height/width styles, except for the tiled-gallery divs when rendering as tiled gallery
+ // as those will be handled with the .tiled-gallery rules bellow.
+ .append(" p, div" + (renderAsTiledGallery ? ":not(." + galleryOnlyClass + ")" : "") +
+ ", dl, table { width: auto !important; height: auto !important; }")
+
+ // make sure long strings don't force the user to scroll horizontally
+ .append(" body, p, div, a { word-wrap: break-word; }")
+
+ // use a consistent top/bottom margin for paragraphs, with no top margin for the first one
+ .append(" p { margin-top: ").append(mResourceVars.marginMediumPx).append("px;")
+ .append(" margin-bottom: ").append(mResourceVars.marginMediumPx).append("px; }")
+ .append(" p:first-child { margin-top: 0px; }")
+
+ // add background color and padding to pre blocks, and add overflow scrolling
+ // so user can scroll the block if it's wider than the display
+ .append(" pre { overflow-x: scroll;")
+ .append(" background-color: ").append(mResourceVars.greyExtraLightStr).append("; ")
+ .append(" padding: ").append(mResourceVars.marginMediumPx).append("px; }")
+
+ // add a left border to blockquotes
+ .append(" blockquote { margin-left: ").append(mResourceVars.marginMediumPx).append("px; ")
+ .append(" padding-left: ").append(mResourceVars.marginMediumPx).append("px; ")
+ .append(" border-left: 3px solid ").append(mResourceVars.greyLightStr).append("; }")
+
+ // show links in the same color they are elsewhere in the app
+ .append(" a { text-decoration: none; color: ").append(mResourceVars.linkColorStr).append("; }")
+
+ // make sure images aren't wider than the display, strictly enforced for images without size
+ .append(" img { max-width: 100%; width: auto; height: auto; }")
+ .append(" img.size-none { max-width: 100% !important; height: auto !important; }")
+
+ // center large/medium images, provide a small bottom margin, and add a background color
+ // so the user sees something while they're loading
+ .append(" img.size-full, img.size-large, img.size-medium {")
+ .append(" display: block; margin-left: auto; margin-right: auto;")
+ .append(" background-color: ").append(mResourceVars.greyExtraLightStr).append(";")
+ .append(" margin-bottom: ").append(mResourceVars.marginMediumPx).append("px; }");
+
+ if (isWideDisplay) {
+ sbHtml
+ .append(".alignleft {")
+ .append(" max-width: 100%;")
+ .append(" float: left;")
+ .append(" margin-top: 12px;")
+ .append(" margin-bottom: 12px;")
+ .append(" margin-right: 32px;}")
+ .append(".alignright {")
+ .append(" max-width: 100%;")
+ .append(" float: right;")
+ .append(" margin-top: 12px;")
+ .append(" margin-bottom: 12px;")
+ .append(" margin-left: 32px;}");
+ }
+
+ if (renderAsTiledGallery) {
+ // tiled-gallery related styles
+ sbHtml
+ .append(".tiled-gallery {")
+ .append(" clear:both;")
+ .append(" overflow:hidden;}")
+ .append(".tiled-gallery img {")
+ .append(" margin:2px !important;}")
+ .append(".tiled-gallery .gallery-group {")
+ .append(" float:left;")
+ .append(" position:relative;}")
+ .append(".tiled-gallery .tiled-gallery-item {")
+ .append(" float:left;")
+ .append(" margin:0;")
+ .append(" position:relative;")
+ .append(" width:inherit;}")
+ .append(".tiled-gallery .gallery-row {")
+ .append(" position: relative;")
+ .append(" left: 50%;")
+ .append(" -webkit-transform: translateX(-50%);")
+ .append(" -moz-transform: translateX(-50%);")
+ .append(" transform: translateX(-50%);")
+ .append(" overflow:hidden;}")
+ .append(".tiled-gallery .tiled-gallery-item a {")
+ .append(" background:transparent;")
+ .append(" border:none;")
+ .append(" color:inherit;")
+ .append(" margin:0;")
+ .append(" padding:0;")
+ .append(" text-decoration:none;")
+ .append(" width:auto;}")
+ .append(".tiled-gallery .tiled-gallery-item img,")
+ .append(".tiled-gallery .tiled-gallery-item img:hover {")
+ .append(" background:none;")
+ .append(" border:none;")
+ .append(" box-shadow:none;")
+ .append(" max-width:100%;")
+ .append(" padding:0;")
+ .append(" vertical-align:middle;}")
+ .append(".tiled-gallery-caption {")
+ .append(" background:#eee;")
+ .append(" background:rgba( 255,255,255,0.8 );")
+ .append(" color:#333;")
+ .append(" font-size:13px;")
+ .append(" font-weight:400;")
+ .append(" overflow:hidden;")
+ .append(" padding:10px 0;")
+ .append(" position:absolute;")
+ .append(" bottom:0;")
+ .append(" text-indent:10px;")
+ .append(" text-overflow:ellipsis;")
+ .append(" width:100%;")
+ .append(" white-space:nowrap;}")
+ .append(".tiled-gallery .tiled-gallery-item-small .tiled-gallery-caption {")
+ .append(" font-size:11px;}")
+ .append(".widget-gallery .tiled-gallery-unresized {")
+ .append(" visibility:hidden;")
+ .append(" height:0px;")
+ .append(" overflow:hidden;}")
+ .append(".tiled-gallery .tiled-gallery-item img.grayscale {")
+ .append(" position:absolute;")
+ .append(" left:0;")
+ .append(" top:0;}")
+ .append(".tiled-gallery .tiled-gallery-item img.grayscale:hover {")
+ .append(" opacity:0;}")
+ .append(".tiled-gallery.type-circle .tiled-gallery-item img {")
+ .append(" border-radius:50% !important;}")
+ .append(".tiled-gallery.type-circle .tiled-gallery-caption {")
+ .append(" display:none;")
+ .append(" opacity:0;}");
+ }
+
+ // see http://codex.wordpress.org/CSS#WordPress_Generated_Classes
+ sbHtml
+ .append(" .wp-caption img { margin-top: 0px; margin-bottom: 0px; }")
+ .append(" .wp-caption .wp-caption-text {")
+ .append(" font-size: smaller; line-height: 1.2em; margin: 0px;")
+ .append(" text-align: center;")
+ .append(" padding: ").append(mResourceVars.marginMediumPx).append("px; ")
+ .append(" color: ").append(mResourceVars.greyMediumDarkStr).append("; }")
+
+ // attribution for Discover posts
+ .append(" div#discover { ")
+ .append(" margin-top: ").append(mResourceVars.marginMediumPx).append("px;")
+ .append(" font-family: sans-serif;")
+ .append(" }")
+
+ // horizontally center iframes
+ .append(" iframe { display: block; margin: 0 auto; }")
+
+ // make sure html5 videos fit the browser width and use 16:9 ratio (YouTube standard)
+ .append(" video {")
+ .append(" width: ").append(pxToDp(mResourceVars.videoWidthPx)).append("px !important;")
+ .append(" height: ").append(pxToDp(mResourceVars.videoHeightPx)).append("px !important; }")
+
+ .append("</style>");
+
+ // add a custom CSS class to (any) tiled gallery elements to make them easier selectable for various rules
+ final List<String> classAmendRegexes = Arrays.asList(
+ "(tiled-gallery)([\\s\"\'])",
+ "(gallery-row)([\\s\"'])",
+ "(gallery-group)([\\s\"'])",
+ "(tiled-gallery-item)([\\s\"'])");
+ String contentCustomised = content;
+ for (String classToAmend : classAmendRegexes) {
+ contentCustomised = contentCustomised.replaceAll(classToAmend, "$1 " + galleryOnlyClass + "$2");
+ }
+
+ sbHtml.append("</head><body>")
+ .append(contentCustomised)
+ .append("</body></html>");
+
+ return sbHtml.toString();
+ }
+
+ private ImageSize getImageSize(final String imageTag, final String imageUrl) {
+ ImageSize size = getImageSizeFromAttachments(imageUrl);
+ if (size == null && imageTag.contains("data-orig-size=")) {
+ size = getImageOriginalSizeFromAttributes(imageTag);
+ }
+ if (size == null && imageUrl.contains("?")) {
+ size = getImageSizeFromQueryParams(imageUrl);
+ }
+ if (size == null && imageTag.contains("width=")) {
+ size = getImageSizeFromAttributes(imageTag);
+ }
+ return size;
+ }
+
+ private ImageSize getImageSizeFromAttachments(final String imageUrl) {
+ if (mAttachmentSizes == null) {
+ mAttachmentSizes = new ImageSizeMap(mPost.getAttachmentsJson());
+ }
+ return mAttachmentSizes.getImageSize(imageUrl);
+ }
+
+ private ImageSize getImageSizeFromQueryParams(final String imageUrl) {
+ if (imageUrl.contains("w=")) {
+ Uri uri = Uri.parse(imageUrl.replace("&#038;", "&"));
+ return new ImageSize(
+ StringUtils.stringToInt(uri.getQueryParameter("w")),
+ StringUtils.stringToInt(uri.getQueryParameter("h")));
+ } else if (imageUrl.contains("resize=")) {
+ Uri uri = Uri.parse(imageUrl.replace("&#038;", "&"));
+ String param = uri.getQueryParameter("resize");
+ if (param != null) {
+ String[] sizes = param.split(",");
+ if (sizes.length == 2) {
+ return new ImageSize(
+ StringUtils.stringToInt(sizes[0]),
+ StringUtils.stringToInt(sizes[1]));
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private ImageSize getImageOriginalSizeFromAttributes(final String imageTag) {
+ return new ImageSize(
+ ReaderHtmlUtils.getOriginalWidthAttrValue(imageTag),
+ ReaderHtmlUtils.getOriginalHeightAttrValue(imageTag));
+ }
+
+ private ImageSize getImageSizeFromAttributes(final String imageTag) {
+ return new ImageSize(
+ ReaderHtmlUtils.getWidthAttrValue(imageTag),
+ ReaderHtmlUtils.getHeightAttrValue(imageTag));
+ }
+
+ private int pxToDp(int px) {
+ if (px == 0) {
+ return 0;
+ }
+ return DisplayUtils.pxToDp(WordPress.getContext(), px);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java
new file mode 100644
index 000000000..9bae403fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java
@@ -0,0 +1,54 @@
+package org.wordpress.android.ui.reader;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.HtmlUtils;
+
+/*
+ * class which holds all resource-based variables used when rendering post detail
+ */
+class ReaderResourceVars {
+ final int marginMediumPx;
+
+ final boolean isWideDisplay;
+
+ final int fullSizeImageWidthPx;
+ final int featuredImageHeightPx;
+
+ final int videoWidthPx;
+ final int videoHeightPx;
+
+ final String linkColorStr;
+ final String greyMediumDarkStr;
+ final String greyLightStr;
+ final String greyExtraLightStr;
+
+ ReaderResourceVars(Context context) {
+ Resources resources = context.getResources();
+
+ int displayWidthPx = DisplayUtils.getDisplayPixelWidth(context);
+
+ isWideDisplay = DisplayUtils.pxToDp(context, displayWidthPx) > 640;
+
+ int marginLargePx = resources.getDimensionPixelSize(R.dimen.margin_large);
+ int detailMarginWidthPx = resources.getDimensionPixelOffset(R.dimen.reader_detail_margin);
+
+ featuredImageHeightPx = resources.getDimensionPixelSize(R.dimen.reader_featured_image_height);
+ marginMediumPx = resources.getDimensionPixelSize(R.dimen.margin_medium);
+
+ linkColorStr = HtmlUtils.colorResToHtmlColor(context, R.color.reader_hyperlink);
+ greyMediumDarkStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_darken_30);
+ greyLightStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_light);
+ greyExtraLightStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_lighten_30);
+
+ // full-size image width must take margin into account
+ fullSizeImageWidthPx = displayWidthPx - (detailMarginWidthPx * 2);
+
+ // 16:9 ratio (YouTube standard)
+ videoWidthPx = fullSizeImageWidthPx - (marginLargePx * 2);
+ videoHeightPx = (int) (videoWidthPx * 0.5625f);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java
new file mode 100644
index 000000000..b5e0f1ffe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java
@@ -0,0 +1,541 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.TabLayout;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.webkit.URLUtil;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderBlogActions;
+import org.wordpress.android.ui.reader.actions.ReaderTagActions;
+import org.wordpress.android.ui.reader.adapters.ReaderBlogAdapter.ReaderBlogType;
+import org.wordpress.android.ui.reader.adapters.ReaderTagAdapter;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService;
+import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPViewPager;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * activity which shows the user's subscriptions and recommended subscriptions - includes
+ * followed tags, followed blogs, and recommended blogs
+ */
+public class ReaderSubsActivity extends AppCompatActivity
+ implements ReaderTagAdapter.TagDeletedListener {
+
+ private EditText mEditAdd;
+ private ImageButton mBtnAdd;
+ private WPViewPager mViewPager;
+ private SubsPageAdapter mPageAdapter;
+
+ private String mLastAddedTagName;
+ private boolean mHasPerformedUpdate;
+
+ private static final String KEY_LAST_ADDED_TAG_NAME = "last_added_tag_name";
+
+ private static final int NUM_TABS = 3;
+
+ private static final int TAB_IDX_FOLLOWED_TAGS = 0;
+ private static final int TAB_IDX_FOLLOWED_BLOGS = 1;
+ private static final int TAB_IDX_RECOMMENDED_BLOGS = 2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.reader_activity_subs);
+ restoreState(savedInstanceState);
+
+ mViewPager = (WPViewPager) findViewById(R.id.viewpager);
+ mViewPager.setOffscreenPageLimit(NUM_TABS - 1);
+ mViewPager.setAdapter(getPageAdapter());
+
+ TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
+ tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
+ int normalColor = ContextCompat.getColor(this, R.color.blue_light);
+ int selectedColor = ContextCompat.getColor(this, R.color.white);
+ tabLayout.setTabTextColors(normalColor, selectedColor);
+ tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
+ tabLayout.setupWithViewPager(mViewPager);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ if (toolbar != null) {
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
+ }
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ // Shadow removed on Activities with a tab toolbar
+ actionBar.setElevation(0.0f);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mEditAdd = (EditText) findViewById(R.id.edit_add);
+ mEditAdd.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ addCurrentEntry();
+ }
+ return false;
+ }
+ });
+
+ mBtnAdd = (ImageButton) findViewById(R.id.btn_add);
+ mBtnAdd.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ addCurrentEntry();
+ }
+ });
+
+ if (savedInstanceState == null) {
+ // return to the page the user was on the last time they viewed this activity
+ restorePreviousPage();
+ }
+
+ // remember which page the user last viewed - note this listener must be assigned
+ // after we've already called restorePreviousPage()
+ mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ String pageTitle = (String) getPageAdapter().getPageTitle(position);
+ AppPrefs.setReaderSubsPageTitle(pageTitle);
+ }
+ });
+ }
+
+ @Override
+ protected void onPause() {
+ EventBus.getDefault().unregister(this);
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ EventBus.getDefault().register(this);
+
+ // update list of tags and blogs from the server
+ if (!mHasPerformedUpdate) {
+ performUpdate();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) {
+ AppLog.d(AppLog.T.READER, "reader subs > followed tags changed");
+ getPageAdapter().refreshFollowedTagFragment();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.FollowedBlogsChanged event) {
+ AppLog.d(AppLog.T.READER, "reader subs > followed blogs changed");
+ getPageAdapter().refreshBlogFragments(ReaderBlogType.FOLLOWED);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ReaderEvents.RecommendedBlogsChanged event) {
+ AppLog.d(AppLog.T.READER, "reader subs > recommended blogs changed");
+ getPageAdapter().refreshBlogFragments(ReaderBlogType.RECOMMENDED);
+ }
+
+ private void performUpdate() {
+ performUpdate(EnumSet.of(
+ UpdateTask.TAGS,
+ UpdateTask.FOLLOWED_BLOGS,
+ UpdateTask.RECOMMENDED_BLOGS));
+ }
+
+ private void performUpdate(EnumSet<UpdateTask> tasks) {
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ return;
+ }
+
+ ReaderUpdateService.startService(this, tasks);
+ mHasPerformedUpdate = true;
+ }
+
+ private void restoreState(Bundle state) {
+ if (state != null) {
+ mLastAddedTagName = state.getString(KEY_LAST_ADDED_TAG_NAME);
+ mHasPerformedUpdate = state.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED);
+ }
+ }
+
+ private SubsPageAdapter getPageAdapter() {
+ if (mPageAdapter == null) {
+ List<Fragment> fragments = new ArrayList<>();
+
+ fragments.add(ReaderTagFragment.newInstance());
+ fragments.add(ReaderBlogFragment.newInstance(ReaderBlogType.FOLLOWED));
+ fragments.add(ReaderBlogFragment.newInstance(ReaderBlogType.RECOMMENDED));
+
+ FragmentManager fm = getFragmentManager();
+ mPageAdapter = new SubsPageAdapter(fm, fragments);
+ }
+ return mPageAdapter;
+ }
+
+ private boolean hasPageAdapter() {
+ return mPageAdapter != null;
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasPerformedUpdate);
+ if (mLastAddedTagName != null) {
+ outState.putString(KEY_LAST_ADDED_TAG_NAME, mLastAddedTagName);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!TextUtils.isEmpty(mLastAddedTagName)) {
+ EventBus.getDefault().postSticky(new ReaderEvents.TagAdded(mLastAddedTagName));
+ }
+ super.onBackPressed();
+ }
+
+ /*
+ * follow the tag or url the user typed into the EditText
+ */
+ private void addCurrentEntry() {
+ String entry = EditTextUtils.getText(mEditAdd).trim();
+ if (TextUtils.isEmpty(entry)) {
+ return;
+ }
+
+ // is it a url or a tag?
+ boolean isUrl = !entry.contains(" ")
+ && (entry.contains(".") || entry.contains("://"));
+ if (isUrl) {
+ addAsUrl(entry);
+ } else {
+ addAsTag(entry);
+ }
+ }
+
+ /*
+ * follow editText entry as a tag
+ */
+ private void addAsTag(final String entry) {
+ if (TextUtils.isEmpty(entry)) {
+ return;
+ }
+
+ if (!ReaderTag.isValidTagName(entry)) {
+ ToastUtils.showToast(this, R.string.reader_toast_err_tag_invalid);
+ return;
+ }
+
+ if (ReaderTagTable.isFollowedTagName(entry)) {
+ ToastUtils.showToast(this, R.string.reader_toast_err_tag_exists);
+ return;
+ }
+
+ // tag is valid, follow it
+ mEditAdd.setText(null);
+ EditTextUtils.hideSoftInput(mEditAdd);
+ performAddTag(entry);
+ }
+
+ /*
+ * follow editText entry as a url
+ */
+ private void addAsUrl(final String entry) {
+ if (TextUtils.isEmpty(entry)) {
+ return;
+ }
+
+ // normalize the url and prepend protocol if not supplied
+ final String normUrl;
+ if (!entry.contains("://")) {
+ normUrl = UrlUtils.normalizeUrl("http://" + entry);
+ } else {
+ normUrl = UrlUtils.normalizeUrl(entry);
+ }
+
+ // if this isn't a valid URL, add original entry as a tag
+ if (!URLUtil.isNetworkUrl(normUrl)) {
+ addAsTag(entry);
+ return;
+ }
+
+ // make sure it isn't already followed
+ if (ReaderBlogTable.isFollowedBlogUrl(normUrl) || ReaderBlogTable.isFollowedFeedUrl(normUrl)) {
+ ToastUtils.showToast(this, R.string.reader_toast_err_already_follow_blog);
+ return;
+ }
+
+ // URL is valid, so follow it
+ performAddUrl(normUrl);
+ }
+
+ /*
+ * called when user manually enters a tag - passed tag is assumed to be validated
+ */
+ private void performAddTag(final String tagName) {
+ if (!NetworkUtils.checkConnection(this)) {
+ return;
+ }
+
+ ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (isFinishing()) return;
+
+ getPageAdapter().refreshFollowedTagFragment();
+
+ if (!succeeded) {
+ ToastUtils.showToast(ReaderSubsActivity.this, R.string.reader_toast_err_add_tag);
+ mLastAddedTagName = null;
+ }
+ }
+ };
+
+ ReaderTag tag = ReaderUtils.createTagFromTagName(tagName, ReaderTagType.FOLLOWED);
+
+ if (ReaderTagActions.addTag(tag, actionListener)) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_TAG_FOLLOWED);
+ mLastAddedTagName = tag.getTagSlug();
+ // make sure addition is reflected on followed tags
+ getPageAdapter().refreshFollowedTagFragment();
+ String labelAddedTag = getString(R.string.reader_label_added_tag);
+ showInfoToast(String.format(labelAddedTag, tag.getLabel()));
+ }
+ }
+
+ /*
+ * start a two-step process to follow a blog by url:
+ * 1. test whether the url is reachable (API will follow any url, even if it doesn't exist)
+ * 2. perform the actual follow
+ * note that the passed URL is assumed to be normalized and validated
+ */
+ private void performAddUrl(final String blogUrl) {
+ if (!NetworkUtils.checkConnection(this)) {
+ return;
+ }
+
+ showAddUrlProgress();
+
+ ReaderActions.OnRequestListener requestListener = new ReaderActions.OnRequestListener() {
+ @Override
+ public void onSuccess() {
+ if (!isFinishing()) {
+ followBlogUrl(blogUrl);
+ }
+ }
+ @Override
+ public void onFailure(int statusCode) {
+ if (!isFinishing()) {
+ hideAddUrlProgress();
+ String errMsg;
+ switch (statusCode) {
+ case 401:
+ errMsg = getString(R.string.reader_toast_err_follow_blog_not_authorized);
+ break;
+ case 0: // can happen when host name not found
+ case 404:
+ errMsg = getString(R.string.reader_toast_err_follow_blog_not_found);
+ break;
+ default:
+ errMsg = getString(R.string.reader_toast_err_follow_blog) + " (" + Integer.toString(statusCode) + ")";
+ break;
+ }
+ ToastUtils.showToast(ReaderSubsActivity.this, errMsg);
+ }
+ }
+ };
+ ReaderBlogActions.checkUrlReachable(blogUrl, requestListener);
+ }
+
+ private void followBlogUrl(String normUrl) {
+ ReaderActions.ActionListener followListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (isFinishing()) {
+ return;
+ }
+ hideAddUrlProgress();
+ if (succeeded) {
+ // clear the edit text and hide the soft keyboard
+ mEditAdd.setText(null);
+ EditTextUtils.hideSoftInput(mEditAdd);
+ showInfoToast(getString(R.string.reader_label_followed_blog));
+ getPageAdapter().refreshBlogFragments(ReaderBlogType.FOLLOWED);
+ } else {
+ ToastUtils.showToast(ReaderSubsActivity.this, R.string.reader_toast_err_follow_blog);
+ }
+ }
+ };
+ // note that this uses the endpoint to follow as a feed since typed URLs are more
+ // likely to point to a feed than a wp blog (and the endpoint should internally
+ // follow it as a blog if it is one)
+ ReaderBlogActions.followFeedByUrl(normUrl, followListener);
+ }
+
+ /*
+ * called prior to following a url to show progress and disable controls
+ */
+ private void showAddUrlProgress() {
+ final ProgressBar progress = (ProgressBar) findViewById(R.id.progress_follow);
+ progress.setVisibility(View.VISIBLE);
+ mEditAdd.setEnabled(false);
+ mBtnAdd.setEnabled(false);
+ }
+
+ /*
+ * called after following a url to hide progress and re-enable controls
+ */
+ private void hideAddUrlProgress() {
+ final ProgressBar progress = (ProgressBar) findViewById(R.id.progress_follow);
+ progress.setVisibility(View.GONE);
+ mEditAdd.setEnabled(true);
+ mBtnAdd.setEnabled(true);
+ }
+
+ /*
+ * toast message shown when adding/removing a tag - appears above the edit text at the bottom
+ */
+ private void showInfoToast(String text) {
+ int yOffset = findViewById(R.id.layout_bottom).getHeight() + DisplayUtils.dpToPx(this, 8);
+ Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
+ toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, yOffset);
+ toast.show();
+ }
+ /*
+ * triggered by a tag fragment's adapter after user removes a tag - note that the network
+ * request has already been made when this is called
+ */
+ @Override
+ public void onTagDeleted(ReaderTag tag) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_TAG_UNFOLLOWED);
+ if (mLastAddedTagName != null && mLastAddedTagName.equalsIgnoreCase(tag.getTagSlug())) {
+ mLastAddedTagName = null;
+ }
+ String labelRemovedTag = getString(R.string.reader_label_removed_tag);
+ showInfoToast(String.format(labelRemovedTag, tag.getLabel()));
+ }
+
+ /*
+ * return to the previously selected page in the viewPager
+ */
+ private void restorePreviousPage() {
+ if (mViewPager == null || !hasPageAdapter()) {
+ return;
+ }
+
+ String pageTitle = AppPrefs.getReaderSubsPageTitle();
+ if (TextUtils.isEmpty(pageTitle)) {
+ return;
+ }
+
+ PagerAdapter adapter = getPageAdapter();
+ for (int i = 0; i < adapter.getCount(); i++) {
+ if (pageTitle.equals(adapter.getPageTitle(i))) {
+ mViewPager.setCurrentItem(i);
+ return;
+ }
+ }
+ }
+
+ private class SubsPageAdapter extends FragmentPagerAdapter {
+ private final List<Fragment> mFragments;
+
+ SubsPageAdapter(FragmentManager fm, List<Fragment> fragments) {
+ super(fm);
+ mFragments = fragments;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case TAB_IDX_FOLLOWED_TAGS:
+ return getString(R.string.reader_page_followed_tags);
+ case TAB_IDX_RECOMMENDED_BLOGS:
+ return getString(R.string.reader_page_recommended_blogs);
+ case TAB_IDX_FOLLOWED_BLOGS:
+ return getString(R.string.reader_page_followed_blogs);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return mFragments.get(position);
+ }
+
+ @Override
+ public int getCount() {
+ return mFragments.size();
+ }
+
+ private void refreshFollowedTagFragment() {
+ for (Fragment fragment: mFragments) {
+ if (fragment instanceof ReaderTagFragment) {
+ ReaderTagFragment tagFragment = (ReaderTagFragment) fragment;
+ tagFragment.refresh();
+ }
+ }
+ }
+
+ private void refreshBlogFragments(ReaderBlogType blogType) {
+ for (Fragment fragment: mFragments) {
+ if (fragment instanceof ReaderBlogFragment) {
+ ReaderBlogFragment blogFragment = (ReaderBlogFragment) fragment;
+ if (blogType == null || blogType.equals(blogFragment.getBlogType())) {
+ blogFragment.refresh();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java
new file mode 100644
index 000000000..4bd81de69
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java
@@ -0,0 +1,95 @@
+package org.wordpress.android.ui.reader;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.adapters.ReaderTagAdapter;
+import org.wordpress.android.ui.reader.views.ReaderRecyclerView;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.WPActivityUtils;
+
+/*
+ * fragment hosted by ReaderSubsActivity which shows followed tags
+ */
+public class ReaderTagFragment extends Fragment implements ReaderTagAdapter.TagDeletedListener {
+ private ReaderRecyclerView mRecyclerView;
+ private ReaderTagAdapter mTagAdapter;
+
+ static ReaderTagFragment newInstance() {
+ AppLog.d(AppLog.T.READER, "reader tag list > newInstance");
+ return new ReaderTagFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.reader_fragment_list, container, false);
+ mRecyclerView = (ReaderRecyclerView) view.findViewById(R.id.recycler_view);
+ return view;
+ }
+
+ private void checkEmptyView() {
+ if (!isAdded()) return;
+
+ TextView emptyView = (TextView) getView().findViewById(R.id.text_empty);
+ if (emptyView != null) {
+ boolean isEmpty = hasTagAdapter() && getTagAdapter().isEmpty();
+ emptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
+ if (isEmpty) {
+ emptyView.setText(R.string.reader_empty_followed_tags);
+ }
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mRecyclerView.setAdapter(getTagAdapter());
+ refresh();
+ }
+
+ void refresh() {
+ if (hasTagAdapter()) {
+ AppLog.d(AppLog.T.READER, "reader subs > refreshing tag fragment");
+ getTagAdapter().refresh();
+ }
+ }
+
+ private ReaderTagAdapter getTagAdapter() {
+ if (mTagAdapter == null) {
+ Context context = WPActivityUtils.getThemedContext(getActivity());
+ mTagAdapter = new ReaderTagAdapter(context);
+ mTagAdapter.setTagDeletedListener(this);
+ mTagAdapter.setDataLoadedListener(new ReaderInterfaces.DataLoadedListener() {
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ checkEmptyView();
+ }
+ });
+ }
+ return mTagAdapter;
+ }
+
+ private boolean hasTagAdapter() {
+ return (mTagAdapter != null);
+ }
+
+ /*
+ * called from adapter when user removes a tag - note that the network request
+ * has been made by the time this is called
+ */
+ @Override
+ public void onTagDeleted(ReaderTag tag) {
+ checkEmptyView();
+ // let the host activity know about the change
+ if (getActivity() instanceof ReaderTagAdapter.TagDeletedListener) {
+ ((ReaderTagAdapter.TagDeletedListener) getActivity()).onTagDeleted(tag);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java
new file mode 100644
index 000000000..09b65dd29
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.ui.reader;
+
+
+public class ReaderTypes {
+
+ public static final ReaderPostListType DEFAULT_POST_LIST_TYPE = ReaderPostListType.TAG_FOLLOWED;
+
+ public enum ReaderPostListType {
+ TAG_FOLLOWED, // list posts in a followed tag
+ TAG_PREVIEW, // list posts in a specific tag
+ BLOG_PREVIEW, // list posts in a specific blog/feed
+ SEARCH_RESULTS; // list posts matching a specific search keyword or phrase
+
+ public boolean isTagType() {
+ return this.equals(TAG_FOLLOWED) || this.equals(TAG_PREVIEW);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java
new file mode 100644
index 000000000..9272c2e4f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java
@@ -0,0 +1,146 @@
+package org.wordpress.android.ui.reader;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.adapters.ReaderUserAdapter;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.views.ReaderRecyclerView;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.widgets.RecyclerItemDecoration;
+
+/*
+ * displays a list of users who like a specific reader post
+ */
+public class ReaderUserListActivity extends AppCompatActivity {
+
+ private ReaderRecyclerView mRecyclerView;
+ private ReaderUserAdapter mAdapter;
+ private int mRestorePosition;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.reader_activity_userlist);
+ setTitle(null);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ if (toolbar != null) {
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
+ }
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setDisplayShowTitleEnabled(true);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (savedInstanceState != null) {
+ mRestorePosition = savedInstanceState.getInt(ReaderConstants.KEY_RESTORE_POSITION);
+ }
+
+ int spacingHorizontal = 0;
+ int spacingVertical = DisplayUtils.dpToPx(this, 1);
+ mRecyclerView = (ReaderRecyclerView) findViewById(R.id.recycler_view);
+ mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
+
+ long blogId = getIntent().getLongExtra(ReaderConstants.ARG_BLOG_ID, 0);
+ long postId = getIntent().getLongExtra(ReaderConstants.ARG_POST_ID, 0);
+ long commentId = getIntent().getLongExtra(ReaderConstants.ARG_COMMENT_ID, 0);
+ loadUsers(blogId, postId, commentId);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ int position = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
+ if (position > 0) {
+ outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, position);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ private ReaderUserAdapter getAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new ReaderUserAdapter(this);
+ mAdapter.setDataLoadedListener(new ReaderInterfaces.DataLoadedListener() {
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ if (!isEmpty && mRestorePosition > 0) {
+ mRecyclerView.scrollToPosition(mRestorePosition);
+ }
+ mRestorePosition = 0;
+ }
+ });
+ mRecyclerView.setAdapter(mAdapter);
+ }
+ return mAdapter;
+ }
+
+ private void loadUsers(final long blogId,
+ final long postId,
+ final long commentId) {
+ new Thread() {
+ @Override
+ public void run() {
+ final String title = getTitleString(blogId, postId, commentId);
+
+ final ReaderUserList users;
+ if (commentId == 0) {
+ // commentId is empty (not passed), so we're showing users who like a post
+ users = ReaderUserTable.getUsersWhoLikePost(
+ blogId,
+ postId,
+ ReaderConstants.READER_MAX_USERS_TO_DISPLAY);
+ } else {
+ // commentId is non-empty, so we're showing users who like a comment
+ users = ReaderUserTable.getUsersWhoLikeComment(
+ blogId,
+ commentId,
+ ReaderConstants.READER_MAX_USERS_TO_DISPLAY);
+ }
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ setTitle(title);
+ getAdapter().setUsers(users);
+ }
+ }
+ });
+ }
+ }.start();
+ }
+
+ private String getTitleString(final long blogId,
+ final long postId,
+ final long commentId) {
+ final int numLikes;
+ final boolean isLikedByCurrentUser;
+ if (commentId == 0) {
+ numLikes = ReaderPostTable.getNumLikesForPost(blogId, postId);
+ isLikedByCurrentUser = ReaderPostTable.isPostLikedByCurrentUser(blogId, postId);
+ } else {
+ numLikes = ReaderCommentTable.getNumLikesForComment(blogId, postId, commentId);
+ isLikedByCurrentUser = ReaderCommentTable.isCommentLikedByCurrentUser(blogId, postId, commentId);
+ }
+ return ReaderUtils.getLongLikeLabelText(this, numLikes, isLikedByCurrentUser);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderViewPagerTransformer.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderViewPagerTransformer.java
new file mode 100644
index 000000000..e75c1785a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderViewPagerTransformer.java
@@ -0,0 +1,104 @@
+package org.wordpress.android.ui.reader;
+
+import android.support.v4.view.ViewPager;
+import android.view.View;
+
+/*
+ * ViewPager transformation animation invoked when a visible/attached page is scrolled - before
+ * changing this, first see https://code.google.com/p/android/issues/detail?id=58918#c5
+ * tl;dr make sure to remove X translation when a page is no longer fully visible
+ *
+ * zoom & depth are based on examples here, with many fixes and simplifications:
+ * http://developer.android.com/training/animation/screen-slide.html#pagetransformer
+ */
+class ReaderViewPagerTransformer implements ViewPager.PageTransformer {
+ enum TransformType {
+ FLOW,
+ DEPTH,
+ ZOOM,
+ SLIDE_OVER
+ }
+ private final TransformType mTransformType;
+
+ ReaderViewPagerTransformer(TransformType transformType) {
+ mTransformType = transformType;
+ }
+
+ private static final float MIN_SCALE_DEPTH = 0.75f;
+ private static final float MIN_SCALE_ZOOM = 0.85f;
+ private static final float MIN_ALPHA_ZOOM = 0.5f;
+ private static final float SCALE_FACTOR_SLIDE = 0.85f;
+ private static final float MIN_ALPHA_SLIDE = 0.35f;
+
+ public void transformPage(View page, float position) {
+ final float alpha;
+ final float scale;
+ final float translationX;
+
+ switch (mTransformType) {
+ case FLOW:
+ page.setRotationY(position * -30f);
+ return;
+
+ case SLIDE_OVER:
+ if (position < 0 && position > -1) {
+ // this is the page to the left
+ scale = Math.abs(Math.abs(position) - 1) * (1.0f - SCALE_FACTOR_SLIDE) + SCALE_FACTOR_SLIDE;
+ alpha = Math.max(MIN_ALPHA_SLIDE, 1 - Math.abs(position));
+ int pageWidth = page.getWidth();
+ float translateValue = position * -pageWidth;
+ if (translateValue > -pageWidth) {
+ translationX = translateValue;
+ } else {
+ translationX = 0;
+ }
+ } else {
+ alpha = 1;
+ scale = 1;
+ translationX = 0;
+ }
+ break;
+
+ case DEPTH:
+ if (position > 0 && position < 1) {
+ // moving to the right
+ alpha = (1 - position);
+ scale = MIN_SCALE_DEPTH + (1 - MIN_SCALE_DEPTH) * (1 - Math.abs(position));
+ translationX = (page.getWidth() * -position);
+ } else {
+ // use default for all other cases
+ alpha = 1;
+ scale = 1;
+ translationX = 0;
+ }
+ break;
+
+ case ZOOM:
+ if (position >= -1 && position <= 1) {
+ scale = Math.max(MIN_SCALE_ZOOM, 1 - Math.abs(position));
+ alpha = MIN_ALPHA_ZOOM +
+ (scale - MIN_SCALE_ZOOM) / (1 - MIN_SCALE_ZOOM) * (1 - MIN_ALPHA_ZOOM);
+ float vMargin = page.getHeight() * (1 - scale) / 2;
+ float hMargin = page.getWidth() * (1 - scale) / 2;
+ if (position < 0) {
+ translationX = (hMargin - vMargin / 2);
+ } else {
+ translationX = (-hMargin + vMargin / 2);
+ }
+ } else {
+ alpha = 1;
+ scale = 1;
+ translationX = 0;
+ }
+ break;
+
+ default:
+ return;
+ }
+
+ page.setAlpha(alpha);
+ page.setTranslationX(translationX);
+ page.setScaleX(scale);
+ page.setScaleY(scale);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java
new file mode 100644
index 000000000..5dd01edef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.reader.actions;
+
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderComment;
+
+/**
+ * classes in this package serve as a middleman between local data and server data - used by
+ * reader activities/fragments/adapters that wish to perform actions on posts, blogs & topics,
+ * or wish to get the latest data from the server.
+ *
+ * methods in this package which change state (like, follow, etc.) are generally optimistic
+ * and work like this:
+ *
+ * 1. caller asks method to send a network request which changes state
+ * 2. method changes state in local data and returns to caller *before* network request completes
+ * 3. caller can access local state change without waiting for the network request
+ * 4. if the network request fails, the method restores the previous state of the local data
+ * 5. if caller passes a listener, it can be alerted to the actual success/failure of the request
+ *
+ * note that all methods MUST be called from the UI thread in order to guarantee that listeners
+ * are alerted on the UI thread
+ */
+public class ReaderActions {
+
+ private ReaderActions() {
+ throw new AssertionError();
+ }
+
+ /*
+ * listener when a specific action is performed (liking a post, etc.)
+ */
+ public interface ActionListener {
+ void onActionResult(boolean succeeded);
+ }
+
+ /*
+ * helper routine for telling an action listener the call succeeded or failed w/o having to null check
+ */
+ public static void callActionListener(ActionListener actionListener, boolean succeeded) {
+ if (actionListener != null) {
+ actionListener.onActionResult(succeeded);
+ }
+ }
+
+ /*
+ * listener when the failure status code is required
+ */
+ public interface OnRequestListener {
+ void onSuccess();
+ void onFailure(int statusCode);
+ }
+
+ /*
+ * listener when submitting a comment
+ */
+ public interface CommentActionListener {
+ void onActionResult(boolean succeeded, ReaderComment newComment);
+ }
+
+ /*
+ * result when updating data (getting latest posts or comments for a post, etc.)
+ */
+ public enum UpdateResult {
+ HAS_NEW, // new posts/comments/etc. have been retrieved
+ CHANGED, // no new posts/comments, but existing ones have changed
+ UNCHANGED, // no new or changed posts/comments
+ FAILED; // request failed
+ public boolean isNewOrChanged() {
+ return (this == HAS_NEW || this == CHANGED);
+ }
+ }
+ public interface UpdateResultListener {
+ void onUpdateResult(UpdateResult result);
+ }
+
+ /*
+ * used by adapters to notify when more data should be loaded
+ */
+ public interface DataRequestedListener {
+ void onRequestData();
+ }
+
+ /*
+ * used by blog preview when requesting latest info about a blog
+ */
+ public interface UpdateBlogInfoListener {
+ void onResult(ReaderBlog blogInfo);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java
new file mode 100644
index 000000000..6398b1e07
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java
@@ -0,0 +1,477 @@
+package org.wordpress.android.ui.reader.actions;
+
+import android.text.TextUtils;
+
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.StringRequest;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.ui.reader.actions.ReaderActions.ActionListener;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateBlogInfoListener;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.net.HttpURLConnection;
+
+public class ReaderBlogActions {
+
+ public static class BlockedBlogResult {
+ public long blogId;
+ public ReaderPostList deletedPosts;
+ public boolean wasFollowing;
+ }
+
+ private static String jsonToString(JSONObject json) {
+ return (json != null ? json.toString() : "");
+ }
+
+ public static boolean followBlogById(final long blogId,
+ final boolean isAskingToFollow,
+ final ActionListener actionListener) {
+ if (blogId == 0) {
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ return false;
+ }
+
+ ReaderBlogTable.setIsFollowedBlogId(blogId, isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInBlog(blogId, isAskingToFollow);
+
+ if (isAskingToFollow) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_FOLLOWED, blogId);
+ } else {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_UNFOLLOWED, blogId);
+ }
+
+ final String actionName = (isAskingToFollow ? "follow" : "unfollow");
+ final String path = "sites/" + blogId + "/follows/" + (isAskingToFollow ? "new" : "mine/delete");
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = isFollowActionSuccessful(jsonObject, isAskingToFollow);
+ if (success) {
+ AppLog.d(T.READER, "blog " + actionName + " succeeded");
+ } else {
+ AppLog.w(T.READER, "blog " + actionName + " failed - " + jsonToString(jsonObject) + " - " + path);
+ localRevertFollowBlogId(blogId, isAskingToFollow);
+ }
+ if (actionListener != null) {
+ actionListener.onActionResult(success);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.w(T.READER, "blog " + actionName + " failed with error");
+ AppLog.e(T.READER, volleyError);
+ localRevertFollowBlogId(blogId, isAskingToFollow);
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ };
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ public static boolean followFeedById(final long feedId,
+ final boolean isAskingToFollow,
+ final ActionListener actionListener) {
+ ReaderBlog blogInfo = ReaderBlogTable.getFeedInfo(feedId);
+ if (blogInfo != null) {
+ return internalFollowFeed(blogInfo.feedId, blogInfo.getFeedUrl(), isAskingToFollow, actionListener);
+ }
+
+ updateFeedInfo(feedId, null, new UpdateBlogInfoListener() {
+ @Override
+ public void onResult(ReaderBlog blogInfo) {
+ if (blogInfo != null) {
+ internalFollowFeed(
+ blogInfo.feedId,
+ blogInfo.getFeedUrl(),
+ isAskingToFollow,
+ actionListener);
+ } else if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ });
+
+ return true;
+ }
+
+ public static void followFeedByUrl(final String feedUrl,
+ final ActionListener actionListener) {
+ if (TextUtils.isEmpty(feedUrl)) {
+ ReaderActions.callActionListener(actionListener, false);
+ return;
+ }
+
+ // use existing blog info if we can
+ ReaderBlog blogInfo = ReaderBlogTable.getFeedInfo(ReaderBlogTable.getFeedIdFromUrl(feedUrl));
+ if (blogInfo != null) {
+ internalFollowFeed(blogInfo.feedId, blogInfo.getFeedUrl(), true, actionListener);
+ return;
+ }
+
+ // otherwise, look it up via the endpoint
+ updateFeedInfo(0, feedUrl, new UpdateBlogInfoListener() {
+ @Override
+ public void onResult(ReaderBlog blogInfo) {
+ // note we attempt to follow even when the look up fails (blogInfo = null) because that
+ // endpoint doesn't perform feed discovery, whereas the endpoint to follow a feed does
+ long feedIdToFollow = blogInfo != null ? blogInfo.feedId : 0;
+ String feedUrlToFollow = (blogInfo != null && blogInfo.hasFeedUrl()) ? blogInfo.getFeedUrl() : feedUrl;
+ internalFollowFeed(
+ feedIdToFollow,
+ feedUrlToFollow,
+ true,
+ actionListener);
+ }
+ });
+ }
+
+ private static boolean internalFollowFeed(
+ final long feedId,
+ final String feedUrl,
+ final boolean isAskingToFollow,
+ final ActionListener actionListener)
+ {
+ // feedUrl is required
+ if (TextUtils.isEmpty(feedUrl)) {
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ return false;
+ }
+
+ if (feedId != 0) {
+ ReaderBlogTable.setIsFollowedFeedId(feedId, isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInFeed(feedId, isAskingToFollow);
+ }
+
+ if (isAskingToFollow) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_FOLLOWED);
+ } else {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_UNFOLLOWED);
+ }
+
+ final String actionName = (isAskingToFollow ? "follow" : "unfollow");
+ final String path = "read/following/mine/"
+ + (isAskingToFollow ? "new" : "delete")
+ + "?url=" + UrlUtils.urlEncode(feedUrl);
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = isFollowActionSuccessful(jsonObject, isAskingToFollow);
+ if (success) {
+ AppLog.d(T.READER, "feed " + actionName + " succeeded");
+ } else {
+ AppLog.w(T.READER, "feed " + actionName + " failed - " + jsonToString(jsonObject) + " - " + path);
+ localRevertFollowFeedId(feedId, isAskingToFollow);
+ }
+ if (actionListener != null) {
+ actionListener.onActionResult(success);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.w(T.READER, "feed " + actionName + " failed with error");
+ AppLog.e(T.READER, volleyError);
+ localRevertFollowFeedId(feedId, isAskingToFollow);
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ };
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ /*
+ * helper routine when following a blog from a post view
+ */
+ public static boolean followBlogForPost(ReaderPost post,
+ boolean isAskingToFollow,
+ ActionListener actionListener) {
+ if (post == null) {
+ AppLog.w(T.READER, "follow action performed with null post");
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ return false;
+ }
+ if (post.feedId != 0) {
+ return followFeedById(post.feedId, isAskingToFollow, actionListener);
+ } else {
+ return followBlogById(post.blogId, isAskingToFollow, actionListener);
+ }
+ }
+
+ /*
+ * called when a follow/unfollow fails, restores local data to previous state
+ */
+ private static void localRevertFollowBlogId(long blogId, boolean isAskingToFollow) {
+ ReaderBlogTable.setIsFollowedBlogId(blogId, !isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInBlog(blogId, !isAskingToFollow);
+ }
+ private static void localRevertFollowFeedId(long feedId, boolean isAskingToFollow) {
+ ReaderBlogTable.setIsFollowedFeedId(feedId, !isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInFeed(feedId, !isAskingToFollow);
+ }
+
+ /*
+ * returns whether a follow/unfollow was successful based on the response to:
+ * read/follows/new
+ * read/follows/delete
+ * site/$site/follows/new
+ * site/$site/follows/mine/delete
+ */
+ private static boolean isFollowActionSuccessful(JSONObject json, boolean isAskingToFollow) {
+ if (json == null) {
+ return false;
+ }
+
+ boolean isSubscribed;
+ if (json.has("subscribed")) {
+ // read/follows/
+ isSubscribed = json.optBoolean("subscribed", false);
+ } else {
+ // site/$site/follows/
+ isSubscribed = json.has("is_following") && json.optBoolean("is_following", false);
+ }
+ return (isSubscribed == isAskingToFollow);
+ }
+
+ /*
+ * request info about a specific blog
+ */
+ public static void updateBlogInfo(long blogId,
+ final String blogUrl,
+ final UpdateBlogInfoListener infoListener) {
+ // must pass either a valid id or url
+ final boolean hasBlogId = (blogId != 0);
+ final boolean hasBlogUrl = !TextUtils.isEmpty(blogUrl);
+ if (!hasBlogId && !hasBlogUrl) {
+ AppLog.w(T.READER, "cannot get blog info without either id or url");
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ return;
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateBlogInfoResponse(jsonObject, infoListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ // authentication error may indicate that API access has been disabled for this blog
+ int statusCode = VolleyUtils.statusCodeFromVolleyError(volleyError);
+ boolean isAuthErr = (statusCode == HttpURLConnection.HTTP_FORBIDDEN);
+ // if we failed to get the blog info using the id and this isn't an authentication
+ // error, try again using just the domain
+ if (!isAuthErr && hasBlogId && hasBlogUrl) {
+ AppLog.w(T.READER, "failed to get blog info by id, retrying with url");
+ updateBlogInfo(0, blogUrl, infoListener);
+ } else {
+ AppLog.e(T.READER, volleyError);
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ }
+ }
+ };
+
+ if (hasBlogId) {
+ WordPress.getRestClientUtilsV1_1().get("read/sites/" + blogId, listener, errorListener);
+ } else {
+ WordPress.getRestClientUtilsV1_1().get("read/sites/" + UrlUtils.urlEncode(UrlUtils.getHost(blogUrl)), listener, errorListener);
+ }
+ }
+ public static void updateFeedInfo(long feedId, String feedUrl, final UpdateBlogInfoListener infoListener) {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateBlogInfoResponse(jsonObject, infoListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ }
+ };
+ String path;
+ if (feedId != 0) {
+ path = "read/feed/" + feedId;
+ } else {
+ path = "read/feed/" + UrlUtils.urlEncode(feedUrl);
+ }
+ WordPress.getRestClientUtilsV1_1().get(path, listener, errorListener);
+ }
+ private static void handleUpdateBlogInfoResponse(JSONObject jsonObject, UpdateBlogInfoListener infoListener) {
+ if (jsonObject == null) {
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ return;
+ }
+
+ ReaderBlog blogInfo = ReaderBlog.fromJson(jsonObject);
+ ReaderBlogTable.addOrUpdateBlog(blogInfo);
+
+ if (infoListener != null) {
+ infoListener.onResult(blogInfo);
+ }
+ }
+
+ /*
+ * tests whether the passed url can be reached - does NOT use authentication, and does not
+ * account for 404 replacement pages used by ISPs such as Charter
+ */
+ public static void checkUrlReachable(final String blogUrl,
+ final ReaderActions.OnRequestListener requestListener) {
+ // listener is required
+ if (requestListener == null) return;
+
+ Response.Listener<String> listener = new Response.Listener<String>() {
+ @Override
+ public void onResponse(String response) {
+ requestListener.onSuccess();
+ }
+ };
+ Response.ErrorListener errorListener = new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ int statusCode;
+ // check specifically for auth failure class rather than relying on status code
+ // since a redirect to an unauthorized url may return a 301 rather than a 401
+ if (volleyError instanceof com.android.volley.AuthFailureError) {
+ statusCode = 401;
+ } else {
+ statusCode = VolleyUtils.statusCodeFromVolleyError(volleyError);
+ }
+ // Volley treats a 301 redirect as a failure here, we should treat it as
+ // success since it means the blog url is reachable
+ if (statusCode == 301) {
+ requestListener.onSuccess();
+ } else {
+ requestListener.onFailure(statusCode);
+ }
+ }
+ };
+
+ // TODO: this should be a HEAD request, but even though Volley supposedly supports HEAD
+ // using it results in "java.lang.IllegalStateException: Unknown method type"
+ StringRequest request = new StringRequest(
+ Request.Method.GET,
+ blogUrl,
+ listener,
+ errorListener);
+ WordPress.requestQueue.add(request);
+ }
+
+ /*
+ * block a blog - result includes the list of posts that were deleted by the block so they
+ * can be restored if the user undoes the block
+ */
+ public static BlockedBlogResult blockBlogFromReader(final long blogId, final ActionListener actionListener) {
+ final BlockedBlogResult blockResult = new BlockedBlogResult();
+ blockResult.blogId = blogId;
+ blockResult.deletedPosts = ReaderPostTable.getPostsInBlog(blogId, 0, false);
+ blockResult.wasFollowing = ReaderBlogTable.isFollowedBlog(blogId);
+
+ ReaderPostTable.deletePostsInBlog(blogId);
+ ReaderBlogTable.setIsFollowedBlogId(blogId, false);
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (actionListener != null) {
+ actionListener.onActionResult(true);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ ReaderPostTable.addOrUpdatePosts(null, blockResult.deletedPosts);
+ if (blockResult.wasFollowing) {
+ ReaderBlogTable.setIsFollowedBlogId(blogId, true);
+ }
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ };
+
+ AppLog.i(T.READER, "blocking blog " + blogId);
+ String path = "me/block/sites/" + Long.toString(blogId) + "/new";
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return blockResult;
+ }
+
+ public static void undoBlockBlogFromReader(final BlockedBlogResult blockResult) {
+ if (blockResult == null) {
+ return;
+ }
+ if (blockResult.deletedPosts != null) {
+ ReaderPostTable.addOrUpdatePosts(null, blockResult.deletedPosts);
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = (jsonObject != null && jsonObject.optBoolean("success"));
+ // re-follow the blog if it was being followed prior to the block
+ if (success && blockResult.wasFollowing) {
+ followBlogById(blockResult.blogId, true, null);
+ } else if (!success) {
+ AppLog.w(T.READER, "failed to unblock blog " + blockResult.blogId);
+ }
+
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ }
+ };
+
+ AppLog.i(T.READER, "unblocking blog " + blockResult.blogId);
+ String path = "me/block/sites/" + Long.toString(blockResult.blogId) + "/delete";
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java
new file mode 100644
index 000000000..1674102ba
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java
@@ -0,0 +1,181 @@
+package org.wordpress.android.ui.reader.actions;
+
+import android.text.TextUtils;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderUser;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ReaderCommentActions {
+ /*
+ * used by post detail to generate a temporary "fake" comment id (see below)
+ */
+ public static long generateFakeCommentId() {
+ return System.currentTimeMillis();
+ }
+
+ /*
+ * add the passed comment text to the passed post - caller must pass a unique "fake" comment id
+ * to give the comment that's generated locally
+ */
+ public static ReaderComment submitPostComment(final ReaderPost post,
+ final long fakeCommentId,
+ final String commentText,
+ final long replyToCommentId,
+ final ReaderActions.CommentActionListener actionListener) {
+ if (post == null || TextUtils.isEmpty(commentText)) {
+ return null;
+ }
+
+ // determine which page this new comment should be assigned to
+ final int pageNumber;
+ if (replyToCommentId != 0) {
+ pageNumber = ReaderCommentTable.getPageNumberForComment(post.blogId, post.postId, replyToCommentId);
+ } else {
+ pageNumber = ReaderCommentTable.getLastPageNumberForPost(post.blogId, post.postId);
+ }
+
+ // create a "fake" comment that's added to the db so it can be shown right away - will be
+ // replaced with actual comment if it succeeds to be posted, or deleted if comment fails
+ // to be posted
+ ReaderComment newComment = new ReaderComment();
+ newComment.commentId = fakeCommentId;
+ newComment.postId = post.postId;
+ newComment.blogId = post.blogId;
+ newComment.parentId = replyToCommentId;
+ newComment.pageNumber = pageNumber;
+ newComment.setText(commentText);
+
+ Date dtPublished = DateTimeUtils.nowUTC();
+ newComment.setPublished(DateTimeUtils.iso8601FromDate(dtPublished));
+ newComment.timestamp = dtPublished.getTime();
+
+ ReaderUser currentUser = ReaderUserTable.getCurrentUser();
+ if (currentUser != null) {
+ newComment.setAuthorAvatar(currentUser.getAvatarUrl());
+ newComment.setAuthorName(currentUser.getDisplayName());
+ }
+
+ ReaderCommentTable.addOrUpdateComment(newComment);
+
+ // different endpoint depending on whether the new comment is a reply to another comment
+ final String path;
+ if (replyToCommentId == 0) {
+ path = "sites/" + post.blogId + "/posts/" + post.postId + "/replies/new";
+ } else {
+ path = "sites/" + post.blogId + "/comments/" + Long.toString(replyToCommentId) + "/replies/new";
+ }
+
+ Map<String, String> params = new HashMap<>();
+ params.put("content", commentText);
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ ReaderCommentTable.deleteComment(post, fakeCommentId);
+ AppLog.i(T.READER, "comment succeeded");
+ ReaderComment newComment = ReaderComment.fromJson(jsonObject, post.blogId);
+ newComment.pageNumber = pageNumber;
+ ReaderCommentTable.addOrUpdateComment(newComment);
+ if (actionListener != null) {
+ actionListener.onActionResult(true, newComment);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ ReaderCommentTable.deleteComment(post, fakeCommentId);
+ AppLog.w(T.READER, "comment failed");
+ AppLog.e(T.READER, volleyError);
+ if (actionListener != null) {
+ actionListener.onActionResult(false, null);
+ }
+ }
+ };
+
+ AppLog.i(T.READER, "submitting comment");
+ WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener);
+
+ return newComment;
+ }
+
+ /*
+ * like or unlike the passed comment
+ */
+ public static boolean performLikeAction(final ReaderComment comment, boolean isAskingToLike) {
+ if (comment == null) {
+ return false;
+ }
+
+ // make sure like status is changing
+ boolean isCurrentlyLiked = ReaderCommentTable.isCommentLikedByCurrentUser(comment);
+ if (isCurrentlyLiked == isAskingToLike) {
+ AppLog.w(T.READER, "comment like unchanged");
+ return false;
+ }
+
+ // update like status and like count in local db
+ int newNumLikes = (isAskingToLike ? comment.numLikes + 1 : comment.numLikes - 1);
+ ReaderCommentTable.setLikesForComment(comment, newNumLikes, isAskingToLike);
+ ReaderLikeTable.setCurrentUserLikesComment(comment, isAskingToLike);
+
+ // sites/$site/comments/$comment_ID/likes/new
+ final String actionName = isAskingToLike ? "like" : "unlike";
+ String path = "sites/" + comment.blogId + "/comments/" + comment.commentId + "/likes/";
+ if (isAskingToLike) {
+ path += "new";
+ } else {
+ path += "mine/delete";
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = (jsonObject != null && JSONUtils.getBool(jsonObject, "success"));
+ if (success) {
+ AppLog.d(T.READER, String.format("comment %s succeeded", actionName));
+ } else {
+ AppLog.w(T.READER, String.format("comment %s failed", actionName));
+ ReaderCommentTable.setLikesForComment(comment, comment.numLikes, comment.isLikedByCurrentUser);
+ ReaderLikeTable.setCurrentUserLikesComment(comment, comment.isLikedByCurrentUser);
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (TextUtils.isEmpty(error)) {
+ AppLog.w(T.READER, String.format("comment %s failed", actionName));
+ } else {
+ AppLog.w(T.READER, String.format("comment %s failed (%s)", actionName, error));
+ }
+ AppLog.e(T.READER, volleyError);
+ ReaderCommentTable.setLikesForComment(comment, comment.numLikes, comment.isLikedByCurrentUser);
+ ReaderLikeTable.setCurrentUserLikesComment(comment, comment.isLikedByCurrentUser);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java
new file mode 100644
index 000000000..fa772de60
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java
@@ -0,0 +1,359 @@
+package org.wordpress.android.ui.reader.actions;
+
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.StringRequest;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderUserIdList;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderPostActions {
+
+ private static final String TRACKING_REFERRER = "https://wordpress.com/";
+ private static final Random mRandom = new Random();
+
+ private ReaderPostActions() {
+ throw new AssertionError();
+ }
+
+ /**
+ * like/unlike the passed post
+ */
+ public static boolean performLikeAction(final ReaderPost post,
+ final boolean isAskingToLike) {
+ // do nothing if post's like state is same as passed
+ boolean isCurrentlyLiked = ReaderPostTable.isPostLikedByCurrentUser(post);
+ if (isCurrentlyLiked == isAskingToLike) {
+ AppLog.w(T.READER, "post like unchanged");
+ return false;
+ }
+
+ // update like status and like count in local db
+ int numCurrentLikes = ReaderPostTable.getNumLikesForPost(post.blogId, post.postId);
+ int newNumLikes = (isAskingToLike ? numCurrentLikes + 1 : numCurrentLikes - 1);
+ if (newNumLikes < 0) {
+ newNumLikes = 0;
+ }
+ ReaderPostTable.setLikesForPost(post, newNumLikes, isAskingToLike);
+ ReaderLikeTable.setCurrentUserLikesPost(post, isAskingToLike);
+
+ final String actionName = isAskingToLike ? "like" : "unlike";
+ String path = "sites/" + post.blogId + "/posts/" + post.postId + "/likes/";
+ if (isAskingToLike) {
+ path += "new";
+ } else {
+ path += "mine/delete";
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.d(T.READER, String.format("post %s succeeded", actionName));
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (TextUtils.isEmpty(error)) {
+ AppLog.w(T.READER, String.format("post %s failed", actionName));
+ } else {
+ AppLog.w(T.READER, String.format("post %s failed (%s)", actionName, error));
+ }
+ AppLog.e(T.READER, volleyError);
+ ReaderPostTable.setLikesForPost(post, post.numLikes, post.isLikedByCurrentUser);
+ ReaderLikeTable.setCurrentUserLikesPost(post, post.isLikedByCurrentUser);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ return true;
+ }
+
+ /*
+ * get the latest version of this post - note that the post is only considered changed if the
+ * like/comment count has changed, or if the current user's like/follow status has changed
+ */
+ public static void updatePost(final ReaderPost localPost,
+ final UpdateResultListener resultListener) {
+ String path = "read/sites/" + localPost.blogId + "/posts/" + localPost.postId + "/?meta=site,likes";
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdatePostResponse(localPost, jsonObject, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ if (resultListener != null) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ }
+ };
+ AppLog.d(T.READER, "updating post");
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void handleUpdatePostResponse(final ReaderPost localPost,
+ final JSONObject jsonObject,
+ final UpdateResultListener resultListener) {
+ if (jsonObject == null) {
+ if (resultListener != null) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPost serverPost = ReaderPost.fromJson(jsonObject);
+
+ // TODO: this temporary fix was added 25-Apr-2016 as a workaround for the fact that
+ // the read/sites/{blogId}/posts/{postId} endpoint doesn't contain the feedId or
+ // feedItemId of the post. because of this, we need to copy them from the local post
+ // before calling isSamePost (since the difference in those IDs causes it to return false)
+ if (serverPost.feedId == 0 && localPost.feedId != 0) {
+ serverPost.feedId = localPost.feedId;
+ }
+
+ if (serverPost.feedItemId == 0 && localPost.feedItemId != 0) {
+ serverPost.feedItemId = localPost.feedItemId;
+ }
+
+ boolean hasChanges = !serverPost.isSamePost(localPost);
+
+ if (hasChanges) {
+ AppLog.d(T.READER, "post updated");
+ // copy changes over to the local post - this is done instead of simply overwriting
+ // the local post with the server post because the server post was retrieved using
+ // the read/sites/$siteId/posts/$postId endpoint which is missing some information
+ // https://github.com/wordpress-mobile/WordPress-Android/issues/3164
+ localPost.numReplies = serverPost.numReplies;
+ localPost.numLikes = serverPost.numLikes;
+ localPost.isFollowedByCurrentUser = serverPost.isFollowedByCurrentUser;
+ localPost.isLikedByCurrentUser = serverPost.isLikedByCurrentUser;
+ localPost.isCommentsOpen = serverPost.isCommentsOpen;
+ localPost.setTitle(serverPost.getTitle());
+ localPost.setText(serverPost.getText());
+ ReaderPostTable.addOrUpdatePost(localPost);
+ }
+
+ // always update liking users regardless of whether changes were detected - this
+ // ensures that the liking avatars are immediately available to post detail
+ if (handlePostLikes(serverPost, jsonObject)) {
+ hasChanges = true;
+ }
+
+ if (resultListener != null) {
+ final UpdateResult result = (hasChanges ? UpdateResult.CHANGED : UpdateResult.UNCHANGED);
+ handler.post(new Runnable() {
+ public void run() {
+ resultListener.onUpdateResult(result);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /*
+ * updates local liking users based on the "likes" meta section of the post's json - requires
+ * using the /sites/ endpoint with ?meta=likes - returns true if likes have changed
+ */
+ private static boolean handlePostLikes(final ReaderPost post, JSONObject jsonPost) {
+ if (post == null || jsonPost == null) {
+ return false;
+ }
+
+ JSONObject jsonLikes = JSONUtils.getJSONChild(jsonPost, "meta/data/likes");
+ if (jsonLikes == null) {
+ return false;
+ }
+
+ ReaderUserList likingUsers = ReaderUserList.fromJsonLikes(jsonLikes);
+ ReaderUserIdList likingUserIds = likingUsers.getUserIds();
+
+ ReaderUserIdList existingIds = ReaderLikeTable.getLikesForPost(post);
+ if (likingUserIds.isSameList(existingIds)) {
+ return false;
+ }
+
+ ReaderUserTable.addOrUpdateUsers(likingUsers);
+ ReaderLikeTable.setLikesForPost(post, likingUserIds);
+ return true;
+ }
+
+ /**
+ * similar to updatePost, but used when post doesn't already exist in local db
+ **/
+ public static void requestPost(final long blogId,
+ final long postId,
+ final ReaderActions.OnRequestListener requestListener) {
+ String path = "read/sites/" + blogId + "/posts/" + postId + "/?meta=site,likes";
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ ReaderPost post = ReaderPost.fromJson(jsonObject);
+ ReaderPostTable.addOrUpdatePost(post);
+ handlePostLikes(post, jsonObject);
+ if (requestListener != null) {
+ requestListener.onSuccess();
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ if (requestListener != null) {
+ int statusCode = 0;
+ // first try to get the error code from the JSON response, example:
+ // {"code":403,"headers":[{"name":"Content-Type","value":"application\/json"}],
+ // "body":{"error":"unauthorized","message":"User cannot access this private blog."}}
+ JSONObject jsonObject = VolleyUtils.volleyErrorToJSON(volleyError);
+ if (jsonObject != null && jsonObject.has("code")) {
+ statusCode = jsonObject.optInt("code");
+ }
+ if (statusCode == 0) {
+ statusCode = VolleyUtils.statusCodeFromVolleyError(volleyError);
+ }
+ requestListener.onFailure(statusCode);
+ }
+ }
+ };
+ AppLog.d(T.READER, "requesting post");
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static String getTrackingPixelForPost(@NonNull ReaderPost post) {
+ return "https://pixel.wp.com/g.gif?v=wpcom&reader=1"
+ + "&blog=" + post.blogId
+ + "&post=" + post.postId
+ + "&host=" + UrlUtils.urlEncode(UrlUtils.getHost(post.getBlogUrl()))
+ + "&ref=" + UrlUtils.urlEncode(TRACKING_REFERRER)
+ + "&t=" + mRandom.nextInt();
+ }
+
+ public static void bumpPageViewForPost(long blogId, long postId) {
+ bumpPageViewForPost(ReaderPostTable.getPost(blogId, postId, true));
+ }
+ public static void bumpPageViewForPost(ReaderPost post) {
+ if (post == null) {
+ return;
+ }
+
+ // don't bump stats for posts in blogs the current user is an admin of, unless
+ // this is a private post since we count views for private posts from admins
+ if (!post.isPrivate && WordPress.wpDB.isCurrentUserAdminOfRemoteBlogId(post.blogId)) {
+ AppLog.d(T.READER, "skipped bump page view - user is admin");
+ return;
+ }
+
+ Response.Listener<String> listener = new Response.Listener<String>() {
+ @Override
+ public void onResponse(String response) {
+ AppLog.d(T.READER, "bump page view succeeded");
+ }
+ };
+ Response.ErrorListener errorListener = new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ AppLog.w(T.READER, "bump page view failed");
+ }
+ };
+
+ Request request = new StringRequest(
+ Request.Method.GET,
+ getTrackingPixelForPost(post),
+ listener,
+ errorListener) {
+ @Override
+ public Map<String, String> getHeaders() throws AuthFailureError {
+ // call will fail without correct refer(r)er
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Referer", TRACKING_REFERRER);
+ return headers;
+ }
+ };
+
+ WordPress.requestQueue.add(request);
+ }
+
+ /*
+ * request posts related to the passed one
+ */
+ public static void requestRelatedPosts(final ReaderPost sourcePost) {
+ if (sourcePost == null) return;
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleRelatedPostsResponse(sourcePost, jsonObject);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.w(T.READER, "updateRelatedPosts failed");
+ AppLog.e(T.READER, volleyError);
+
+ }
+ };
+
+ String path = "/read/site/" + sourcePost.blogId + "/post/" + sourcePost.postId + "/related";
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void handleRelatedPostsResponse(final ReaderPost sourcePost, final JSONObject jsonObject) {
+ if (jsonObject == null) return;
+
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPostList relatedPosts = ReaderPostList.fromJson(jsonObject);
+ if (relatedPosts != null && relatedPosts.size() > 0) {
+ ReaderPostTable.addOrUpdatePosts(null, relatedPosts);
+ EventBus.getDefault().post(new ReaderEvents.RelatedPostsUpdated(sourcePost, relatedPosts));
+ }
+ }
+ }.start();
+
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java
new file mode 100644
index 000000000..6732c49fa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java
@@ -0,0 +1,168 @@
+package org.wordpress.android.ui.reader.actions;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+public class ReaderTagActions {
+ private ReaderTagActions() {
+ throw new AssertionError();
+ }
+
+ public static boolean deleteTag(final ReaderTag tag,
+ final ReaderActions.ActionListener actionListener) {
+ if (tag == null) {
+ ReaderActions.callActionListener(actionListener, false);
+ return false;
+ }
+
+ final String tagNameForApi = ReaderUtils.sanitizeWithDashes(tag.getTagSlug());
+ final String path = "read/tags/" + tagNameForApi + "/mine/delete";
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.i(T.READER, "delete tag succeeded");
+ ReaderActions.callActionListener(actionListener, true);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ // treat it as a success if the error says the user isn't following the deleted tag
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (error.equals("not_subscribed")) {
+ AppLog.w(T.READER, "delete tag succeeded with error " + error);
+ ReaderActions.callActionListener(actionListener, true);
+ return;
+ }
+
+ AppLog.w(T.READER, " delete tag failed");
+ AppLog.e(T.READER, volleyError);
+
+ // add back original tag
+ ReaderTagTable.addOrUpdateTag(tag);
+
+ ReaderActions.callActionListener(actionListener, false);
+ }
+ };
+
+ ReaderTagTable.deleteTag(tag);
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ public static boolean addTag(final ReaderTag tag,
+ final ReaderActions.ActionListener actionListener) {
+ if (tag == null) {
+ ReaderActions.callActionListener(actionListener, false);
+ return false;
+ }
+
+ final String tagNameForApi = ReaderUtils.sanitizeWithDashes(tag.getTagSlug());
+ final String path = "read/tags/" + tagNameForApi + "/mine/new";
+ String endpoint = "/read/tags/" + tagNameForApi + "/posts";
+
+ ReaderTag newTag = new ReaderTag(
+ tag.getTagSlug(),
+ tag.getTagDisplayName(),
+ tag.getTagTitle(),
+ endpoint,
+ ReaderTagType.FOLLOWED);
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.i(T.READER, "add tag succeeded");
+ // the response will contain the list of the user's followed tags
+ ReaderTagList tags = parseFollowedTags(jsonObject);
+ ReaderTagTable.replaceFollowedTags(tags);
+ ReaderActions.callActionListener(actionListener, true);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ // treat is as a success if we're adding a tag and the error says the user is
+ // already following it
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (error.equals("already_subscribed")) {
+ AppLog.w(T.READER, "add tag succeeded with error " + error);
+ ReaderActions.callActionListener(actionListener, true);
+ return;
+ }
+
+ AppLog.w(T.READER, "add tag failed");
+ AppLog.e(T.READER, volleyError);
+
+ // revert on failure
+ ReaderTagTable.deleteTag(tag);
+
+ ReaderActions.callActionListener(actionListener, false);
+ }
+ };
+
+ ReaderTagTable.addOrUpdateTag(newTag);
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ /*
+ * return the user's followed tags from the response to read/tags/{tag}/mine/new
+ */
+ /*
+ {
+ "added_tag": "84776",
+ "subscribed": true,
+ "tags": [
+ {
+ "display_name": "fitness",
+ "ID": "5189",
+ "slug": "fitness",
+ "title": "Fitness",
+ "URL": "https://public-api.wordpress.com/rest/v1.1/read/tags/fitness/posts"
+ },
+ ...
+ }
+ */
+ private static ReaderTagList parseFollowedTags(JSONObject jsonObject) {
+ if (jsonObject == null) {
+ return null;
+ }
+
+ JSONArray jsonTags = jsonObject.optJSONArray(ReaderConstants.JSON_TAG_TAGS_ARRAY);
+ if (jsonTags == null || jsonTags.length() == 0) {
+ return null;
+ }
+
+ ReaderTagList tags = new ReaderTagList();
+ for (int i=0; i < jsonTags.length(); i++) {
+ JSONObject jsonThisTag = jsonTags.optJSONObject(i);
+ String tagTitle = JSONUtils.getStringDecoded(jsonThisTag, ReaderConstants.JSON_TAG_TITLE);
+ String tagDisplayName = JSONUtils.getStringDecoded(jsonThisTag, ReaderConstants.JSON_TAG_DISPLAY_NAME);
+ String tagSlug = JSONUtils.getStringDecoded(jsonThisTag, ReaderConstants.JSON_TAG_SLUG);
+ String endpoint = JSONUtils.getString(jsonThisTag, ReaderConstants.JSON_TAG_URL);
+ tags.add(new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, ReaderTagType.FOLLOWED));
+ }
+
+ return tags;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java
new file mode 100644
index 000000000..6c6257a86
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java
@@ -0,0 +1,263 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderBlogList;
+import org.wordpress.android.models.ReaderRecommendBlogList;
+import org.wordpress.android.models.ReaderRecommendedBlog;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+/*
+ * adapter which shows either recommended or followed blogs - used by ReaderBlogFragment
+ */
+public class ReaderBlogAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private static final int VIEW_TYPE_ITEM = 0;
+
+ public enum ReaderBlogType {RECOMMENDED, FOLLOWED}
+
+ public interface BlogClickListener {
+ void onBlogClicked(Object blog);
+ }
+
+ private final ReaderBlogType mBlogType;
+ private BlogClickListener mClickListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+
+ private ReaderRecommendBlogList mRecommendedBlogs = new ReaderRecommendBlogList();
+ private ReaderBlogList mFollowedBlogs = new ReaderBlogList();
+
+ @SuppressWarnings("UnusedParameters")
+ public ReaderBlogAdapter(Context context, ReaderBlogType blogType) {
+ super();
+ setHasStableIds(false);
+ mBlogType = blogType;
+ }
+
+ public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ public void setBlogClickListener(BlogClickListener listener) {
+ mClickListener = listener;
+ }
+
+ public void refresh() {
+ if (mIsTaskRunning) {
+ AppLog.w(T.READER, "load blogs task is already running");
+ return;
+ }
+ new LoadBlogsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private ReaderBlogType getBlogType() {
+ return mBlogType;
+ }
+
+ public boolean isEmpty() {
+ return (getItemCount() == 0);
+ }
+
+ @Override
+ public int getItemCount() {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ return mRecommendedBlogs.size();
+ case FOLLOWED:
+ return mFollowedBlogs.size();
+ default:
+ return 0;
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return VIEW_TYPE_ITEM;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM:
+ View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_blog, parent, false);
+ return new BlogViewHolder(itemView);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof BlogViewHolder) {
+ final BlogViewHolder blogHolder = (BlogViewHolder) holder;
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ final ReaderRecommendedBlog blog = mRecommendedBlogs.get(position);
+ blogHolder.txtTitle.setText(blog.getTitle());
+ blogHolder.txtDescription.setText(blog.getReason());
+ blogHolder.txtUrl.setText(UrlUtils.getHost(blog.getBlogUrl()));
+ blogHolder.imgBlog.setImageUrl(blog.getImageUrl(), WPNetworkImageView.ImageType.BLAVATAR);
+ break;
+
+ case FOLLOWED:
+ final ReaderBlog blogInfo = mFollowedBlogs.get(position);
+ if (blogInfo.hasName()) {
+ blogHolder.txtTitle.setText(blogInfo.getName());
+ } else {
+ blogHolder.txtTitle.setText(R.string.reader_untitled_post);
+ }
+ if (blogInfo.hasUrl()) {
+ blogHolder.txtUrl.setText(UrlUtils.getHost(blogInfo.getUrl()));
+ } else if (blogInfo.hasFeedUrl()) {
+ blogHolder.txtUrl.setText(UrlUtils.getHost(blogInfo.getFeedUrl()));
+ } else {
+ blogHolder.txtUrl.setText("");
+ }
+ blogHolder.imgBlog.setImageUrl(blogInfo.getImageUrl(), WPNetworkImageView.ImageType.BLAVATAR);
+ break;
+ }
+
+ if (mClickListener != null) {
+ blogHolder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int clickedPosition = blogHolder.getAdapterPosition();
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ mClickListener.onBlogClicked(mRecommendedBlogs.get(clickedPosition));
+ break;
+ case FOLLOWED:
+ mClickListener.onBlogClicked(mFollowedBlogs.get(clickedPosition));
+ break;
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /*
+ * holder used for followed/recommended blogs
+ */
+ class BlogViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtTitle;
+ private final TextView txtDescription;
+ private final TextView txtUrl;
+ private final WPNetworkImageView imgBlog;
+
+ public BlogViewHolder(View view) {
+ super(view);
+
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtDescription = (TextView) view.findViewById(R.id.text_description);
+ txtUrl = (TextView) view.findViewById(R.id.text_url);
+ imgBlog = (WPNetworkImageView) view.findViewById(R.id.image_blog);
+
+ // followed blogs don't have a description
+ switch (getBlogType()) {
+ case FOLLOWED:
+ txtDescription.setVisibility(View.GONE);
+ break;
+ case RECOMMENDED:
+ txtDescription.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+ }
+
+ private boolean mIsTaskRunning = false;
+ private class LoadBlogsTask extends AsyncTask<Void, Void, Boolean> {
+ ReaderRecommendBlogList tmpRecommendedBlogs;
+ ReaderBlogList tmpFollowedBlogs;
+
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ tmpRecommendedBlogs = ReaderBlogTable.getRecommendedBlogs();
+ return !mRecommendedBlogs.isSameList(tmpRecommendedBlogs);
+
+ case FOLLOWED:
+ tmpFollowedBlogs = ReaderBlogTable.getFollowedBlogs();
+ return !mFollowedBlogs.isSameList(tmpFollowedBlogs);
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ mRecommendedBlogs = (ReaderRecommendBlogList) tmpRecommendedBlogs.clone();
+ break;
+ case FOLLOWED:
+ mFollowedBlogs = (ReaderBlogList) tmpFollowedBlogs.clone();
+ // sort followed blogs by name/domain to match display
+ Collections.sort(mFollowedBlogs, new Comparator<ReaderBlog>() {
+ @Override
+ public int compare(ReaderBlog thisBlog, ReaderBlog thatBlog) {
+ String thisName = getBlogNameForComparison(thisBlog);
+ String thatName = getBlogNameForComparison(thatBlog);
+ return thisName.compareToIgnoreCase(thatName);
+ }
+ });
+ break;
+ }
+ notifyDataSetChanged();
+ }
+
+ mIsTaskRunning = false;
+
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+
+ private String getBlogNameForComparison(ReaderBlog blog) {
+ if (blog == null) {
+ return "";
+ } else if (blog.hasName()) {
+ return blog.getName();
+ } else if (blog.hasUrl()) {
+ return StringUtils.notNullStr(UrlUtils.getHost(blog.getUrl()));
+ } else {
+ return "";
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java
new file mode 100644
index 000000000..8039d13f8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java
@@ -0,0 +1,497 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderCommentList;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.ui.comments.CommentUtils;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderAnim;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderCommentActions;
+import org.wordpress.android.ui.reader.utils.ReaderLinkMovementMethod;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.views.ReaderCommentsPostHeaderView;
+import org.wordpress.android.ui.reader.views.ReaderIconCountView;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+public class ReaderCommentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ private ReaderPost mPost;
+ private boolean mMoreCommentsExist;
+
+ private static final int MAX_INDENT_LEVEL = 2;
+ private final int mIndentPerLevel;
+ private final int mAvatarSz;
+ private final int mContentWidth;
+
+ private long mHighlightCommentId = 0;
+ private boolean mShowProgressForHighlightedComment = false;
+ private final boolean mIsPrivatePost;
+ private final boolean mIsLoggedOutReader;
+ private boolean mIsHeaderClickEnabled;
+
+ private final int mColorAuthor;
+ private final int mColorNotAuthor;
+ private final int mColorHighlight;
+
+ private static final int VIEW_TYPE_HEADER = 1;
+ private static final int VIEW_TYPE_COMMENT = 2;
+
+ private static final long ID_HEADER = -1L;
+
+ private static final int NUM_HEADERS = 1;
+
+ public interface RequestReplyListener {
+ void onRequestReply(long commentId);
+ }
+
+ private ReaderCommentList mComments = new ReaderCommentList();
+ private RequestReplyListener mReplyListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+ private ReaderActions.DataRequestedListener mDataRequestedListener;
+
+ class CommentHolder extends RecyclerView.ViewHolder {
+ private final ViewGroup container;
+ private final TextView txtAuthor;
+ private final TextView txtText;
+ private final TextView txtDate;
+
+ private final WPNetworkImageView imgAvatar;
+ private final View spacerIndent;
+ private final ProgressBar progress;
+
+ private final TextView txtReply;
+ private final ImageView imgReply;
+
+ private final ReaderIconCountView countLikes;
+
+ public CommentHolder(View view) {
+ super(view);
+
+ container = (ViewGroup) view.findViewById(R.id.layout_container);
+
+ txtAuthor = (TextView) view.findViewById(R.id.text_comment_author);
+ txtText = (TextView) view.findViewById(R.id.text_comment_text);
+ txtDate = (TextView) view.findViewById(R.id.text_comment_date);
+
+ txtReply = (TextView) view.findViewById(R.id.text_comment_reply);
+ imgReply = (ImageView) view.findViewById(R.id.image_comment_reply);
+
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.image_comment_avatar);
+ spacerIndent = view.findViewById(R.id.spacer_comment_indent);
+ progress = (ProgressBar) view.findViewById(R.id.progress_comment);
+
+ countLikes = (ReaderIconCountView) view.findViewById(R.id.count_likes);
+
+ txtText.setLinksClickable(true);
+ txtText.setMovementMethod(ReaderLinkMovementMethod.getInstance(mIsPrivatePost));
+ }
+ }
+
+ class PostHeaderHolder extends RecyclerView.ViewHolder {
+ private final ReaderCommentsPostHeaderView mHeaderView;
+
+ public PostHeaderHolder(View view) {
+ super(view);
+ mHeaderView = (ReaderCommentsPostHeaderView) view;
+ }
+ }
+
+ public ReaderCommentAdapter(Context context, ReaderPost post) {
+ mPost = post;
+ mIsPrivatePost = (post != null && post.isPrivate);
+ mIsLoggedOutReader = ReaderUtils.isLoggedOutReader();
+
+ mIndentPerLevel = context.getResources().getDimensionPixelSize(R.dimen.reader_comment_indent_per_level);
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_extra_small);
+
+ // calculate the max width of comment content
+ int displayWidth = DisplayUtils.getDisplayPixelWidth(context);
+ int cardMargin = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin);
+ int contentPadding = context.getResources().getDimensionPixelSize(R.dimen.reader_card_content_padding);
+ int mediumMargin = context.getResources().getDimensionPixelSize(R.dimen.margin_medium);
+ mContentWidth = displayWidth - (cardMargin * 2) - (contentPadding * 2) - (mediumMargin * 2);
+
+ mColorAuthor = ContextCompat.getColor(context, R.color.blue_medium);
+ mColorNotAuthor = ContextCompat.getColor(context, R.color.grey_dark);
+ mColorHighlight = ContextCompat.getColor(context, R.color.grey_lighten_30);
+
+ setHasStableIds(true);
+ }
+
+ public void setReplyListener(RequestReplyListener replyListener) {
+ mReplyListener = replyListener;
+ }
+
+ public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener dataLoadedListener) {
+ mDataLoadedListener = dataLoadedListener;
+ }
+
+ public void setDataRequestedListener(ReaderActions.DataRequestedListener dataRequestedListener) {
+ mDataRequestedListener = dataRequestedListener;
+ }
+
+ public void enableHeaderClicks() {
+ mIsHeaderClickEnabled = true;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position == 0 ? VIEW_TYPE_HEADER : VIEW_TYPE_COMMENT;
+ }
+
+ public void refreshComments() {
+ if (mIsTaskRunning) {
+ AppLog.w(T.READER, "reader comment adapter > Load comments task already running");
+ }
+ new LoadCommentsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mComments.size() + NUM_HEADERS;
+ }
+
+ public boolean isEmpty() {
+ return mComments.size() == 0;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_HEADER:
+ View headerView = new ReaderCommentsPostHeaderView(parent.getContext());
+ headerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ return new PostHeaderHolder(headerView);
+ default:
+ View commentView = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_comment, parent, false);
+ return new CommentHolder(commentView);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof PostHeaderHolder) {
+ PostHeaderHolder headerHolder = (PostHeaderHolder) holder;
+ headerHolder.mHeaderView.setPost(mPost);
+ if (mIsHeaderClickEnabled) {
+ headerHolder.mHeaderView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderPostDetail(view.getContext(), mPost.blogId, mPost.postId);
+ }
+ });
+ }
+ return;
+ }
+
+ final ReaderComment comment = getItem(position);
+ if (comment == null) {
+ return;
+ }
+
+ CommentHolder commentHolder = (CommentHolder) holder;
+ commentHolder.txtAuthor.setText(comment.getAuthorName());
+
+ java.util.Date dtPublished = DateTimeUtils.dateFromIso8601(comment.getPublished());
+ commentHolder.txtDate.setText(DateTimeUtils.javaDateToTimeSpan(dtPublished, WordPress.getContext()));
+
+ if (comment.hasAuthorAvatar()) {
+ String avatarUrl = GravatarUtils.fixGravatarUrl(comment.getAuthorAvatar(), mAvatarSz);
+ commentHolder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ commentHolder.imgAvatar.showDefaultGravatarImage();
+ }
+
+ // tapping avatar or author name opens blog preview
+ if (comment.hasAuthorBlogId()) {
+ View.OnClickListener authorListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ view.getContext(),
+ comment.authorBlogId
+ );
+ }
+ };
+ commentHolder.imgAvatar.setOnClickListener(authorListener);
+ commentHolder.txtAuthor.setOnClickListener(authorListener);
+ } else {
+ commentHolder.imgAvatar.setOnClickListener(null);
+ commentHolder.txtAuthor.setOnClickListener(null);
+ }
+
+ // author name uses different color for comments from the post's author
+ if (comment.authorId == mPost.authorId) {
+ commentHolder.txtAuthor.setTextColor(mColorAuthor);
+ } else {
+ commentHolder.txtAuthor.setTextColor(mColorNotAuthor);
+ }
+
+ // show indentation spacer for comments with parents and indent it based on comment level
+ int indentWidth;
+ if (comment.parentId != 0 && comment.level > 0) {
+ indentWidth = Math.min(MAX_INDENT_LEVEL, comment.level) * mIndentPerLevel;
+ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) commentHolder.spacerIndent.getLayoutParams();
+ params.width = indentWidth;
+ commentHolder.spacerIndent.setVisibility(View.VISIBLE);
+ } else {
+ indentWidth = 0;
+ commentHolder.spacerIndent.setVisibility(View.GONE);
+ }
+
+ int maxImageWidth = mContentWidth - indentWidth;
+ CommentUtils.displayHtmlComment(commentHolder.txtText, comment.getText(), maxImageWidth);
+
+ // different background for highlighted comment, with optional progress bar
+ if (mHighlightCommentId != 0 && mHighlightCommentId == comment.commentId) {
+ commentHolder.container.setBackgroundColor(mColorHighlight);
+ commentHolder.progress.setVisibility(mShowProgressForHighlightedComment ? View.VISIBLE : View.GONE);
+ } else {
+ commentHolder.container.setBackgroundColor(Color.WHITE);
+ commentHolder.progress.setVisibility(View.GONE);
+ }
+
+ if (mIsLoggedOutReader) {
+ commentHolder.txtReply.setVisibility(View.GONE);
+ commentHolder.imgReply.setVisibility(View.GONE);
+ } else if (mReplyListener != null) {
+ // tapping reply icon tells activity to show reply box
+ View.OnClickListener replyClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mReplyListener.onRequestReply(comment.commentId);
+ }
+ };
+ commentHolder.txtReply.setOnClickListener(replyClickListener);
+ commentHolder.imgReply.setOnClickListener(replyClickListener);
+ }
+
+ showLikeStatus(commentHolder, position);
+
+ // if we're nearing the end of the comments and we know more exist on the server,
+ // fire request to load more
+ if (mMoreCommentsExist && mDataRequestedListener != null && (position >= getItemCount() - NUM_HEADERS)) {
+ mDataRequestedListener.onRequestData();
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_HEADER:
+ return ID_HEADER;
+ default:
+ ReaderComment comment = getItem(position);
+ return comment != null ? comment.commentId : 0;
+ }
+ }
+
+ private ReaderComment getItem(int position) {
+ return position == 0 ? null : mComments.get(position - NUM_HEADERS);
+ }
+
+ private void showLikeStatus(final CommentHolder holder, int position) {
+ ReaderComment comment = getItem(position);
+ if (comment == null) {
+ return;
+ }
+
+ if (mPost.canLikePost()) {
+ holder.countLikes.setVisibility(View.VISIBLE);
+ holder.countLikes.setSelected(comment.isLikedByCurrentUser);
+ holder.countLikes.setCount(comment.numLikes);
+
+ if (mIsLoggedOutReader) {
+ holder.countLikes.setEnabled(false);
+ } else {
+ holder.countLikes.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int clickedPosition = holder.getAdapterPosition();
+ toggleLike(v.getContext(), holder, clickedPosition);
+ }
+ });
+ }
+ } else {
+ holder.countLikes.setVisibility(View.GONE);
+ holder.countLikes.setOnClickListener(null);
+ }
+ }
+
+ private void toggleLike(Context context, CommentHolder holder, int position) {
+ if (!NetworkUtils.checkConnection(context)) {
+ return;
+ }
+
+ ReaderComment comment = getItem(position);
+ if (comment == null) {
+ ToastUtils.showToast(context, R.string.reader_toast_err_generic);
+ return;
+ }
+
+ boolean isAskingToLike = !comment.isLikedByCurrentUser;
+ ReaderAnim.animateLikeButton(holder.countLikes.getImageView(), isAskingToLike);
+
+ if (!ReaderCommentActions.performLikeAction(comment, isAskingToLike)) {
+ ToastUtils.showToast(context, R.string.reader_toast_err_generic);
+ return;
+ }
+
+ ReaderComment updatedComment = ReaderCommentTable.getComment(comment.blogId, comment.postId, comment.commentId);
+ if (updatedComment != null) {
+ mComments.set(position - NUM_HEADERS, updatedComment);
+ showLikeStatus(holder, position);
+ }
+ }
+
+ /*
+ * called from post detail activity when user submits a comment
+ */
+ public void addComment(ReaderComment comment) {
+ if (comment == null) {
+ return;
+ }
+
+ // if the comment doesn't have a parent we can just add it to the list of existing
+ // comments - but if it does have a parent, we need to reload the list so that it
+ // appears under its parent and is correctly indented
+ if (comment.parentId == 0) {
+ mComments.add(comment);
+ notifyDataSetChanged();
+ } else {
+ refreshComments();
+ }
+ }
+
+ /*
+ * called from post detail when submitted a comment fails - this removes the "fake" comment
+ * that was inserted while the API call was still being processed
+ */
+ public void removeComment(long commentId) {
+ if (commentId == mHighlightCommentId) {
+ setHighlightCommentId(0, false);
+ }
+
+ int index = mComments.indexOfCommentId(commentId);
+ if (index > -1) {
+ mComments.remove(index);
+ notifyDataSetChanged();
+ }
+ }
+
+ /*
+ * replace the comment that has the passed commentId with another comment
+ */
+ public void replaceComment(long commentId, ReaderComment comment) {
+ int position = positionOfCommentId(commentId);
+ if (position > -1 && mComments.replaceComment(commentId, comment)) {
+ notifyItemChanged(position);
+ }
+ }
+
+ /*
+ * sets the passed comment as highlighted with a different background color and an optional
+ * progress bar (used when posting new comments) - note that we don't call notifyDataSetChanged()
+ * here since in most cases it's unnecessary, so we leave it up to the caller to do that
+ */
+ public void setHighlightCommentId(long commentId, boolean showProgress) {
+ mHighlightCommentId = commentId;
+ mShowProgressForHighlightedComment = showProgress;
+ }
+
+ /*
+ * returns the position of the passed comment in the adapter, taking the header into account
+ */
+ public int positionOfCommentId(long commentId) {
+ int index = mComments.indexOfCommentId(commentId);
+ return index == -1 ? -1 : index + NUM_HEADERS;
+ }
+
+ /*
+ * AsyncTask to load comments for this post
+ */
+ private boolean mIsTaskRunning = false;
+
+ private class LoadCommentsTask extends AsyncTask<Void, Void, Boolean> {
+ private ReaderCommentList tmpComments;
+ private boolean tmpMoreCommentsExist;
+
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ if (mPost == null) {
+ return false;
+ }
+
+ // determine whether more comments can be downloaded by comparing the number of
+ // comments the post says it has with the number of comments actually stored
+ // locally for this post
+ int numServerComments = ReaderPostTable.getNumCommentsForPost(mPost);
+ int numLocalComments = ReaderCommentTable.getNumCommentsForPost(mPost);
+ tmpMoreCommentsExist = (numServerComments > numLocalComments);
+
+ tmpComments = ReaderCommentTable.getCommentsForPost(mPost);
+ return !mComments.isSameList(tmpComments);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mMoreCommentsExist = tmpMoreCommentsExist;
+
+ if (result) {
+ // assign the comments with children sorted under their parents and indent levels applied
+ mComments = ReaderCommentList.getLevelList(tmpComments);
+ notifyDataSetChanged();
+ }
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ mIsTaskRunning = false;
+ }
+ }
+
+ /*
+ * Set a post to adapter and update relevant information in the post header
+ */
+ public void setPost(ReaderPost post) {
+ if (post != null) {
+ mPost = post;
+ notifyItemChanged(0); //notify header to update itself
+ }
+
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java
new file mode 100644
index 000000000..85bfab181
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java
@@ -0,0 +1,103 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * adapter for the popup menu that appears when clicking "..." in the reader
+ */
+public class ReaderMenuAdapter extends BaseAdapter {
+
+ private final LayoutInflater mInflater;
+ private final List<Integer> mMenuItems = new ArrayList<>();
+
+ public static final int ITEM_FOLLOW = 0;
+ public static final int ITEM_UNFOLLOW = 1;
+ public static final int ITEM_BLOCK = 2;
+
+ public ReaderMenuAdapter(Context context, @NonNull List<Integer> menuItems) {
+ super();
+ mInflater = LayoutInflater.from(context);
+ mMenuItems.addAll(menuItems);
+ }
+
+ @Override
+ public int getCount() {
+ return mMenuItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ReaderMenuHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.reader_popup_menu_item, parent, false);
+ holder = new ReaderMenuHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (ReaderMenuHolder) convertView.getTag();
+ }
+
+ int textRes;
+ int textColorRes;
+ int iconRes;
+ switch (mMenuItems.get(position)) {
+ case ITEM_FOLLOW:
+ textRes = R.string.reader_btn_follow;
+ textColorRes = R.color.reader_follow;
+ iconRes = R.drawable.reader_follow;
+ break;
+ case ITEM_UNFOLLOW:
+ textRes = R.string.reader_btn_unfollow;
+ textColorRes = R.color.reader_following;
+ iconRes = R.drawable.reader_following;
+ break;
+ case ITEM_BLOCK:
+ textRes = R.string.reader_menu_block_blog;
+ textColorRes = R.color.grey_dark;
+ iconRes = 0;
+ break;
+ default:
+ return convertView;
+ }
+
+ holder.text.setText(textRes);
+ holder.text.setTextColor(convertView.getContext().getResources().getColor(textColorRes));
+
+ if (iconRes != 0) {
+ holder.icon.setImageResource(iconRes);
+ }
+
+ return convertView;
+ }
+
+ class ReaderMenuHolder {
+ private final TextView text;
+ private final ImageView icon;
+
+ ReaderMenuHolder(View view) {
+ text = (TextView) view.findViewById(R.id.text);
+ icon = (ImageView) view.findViewById(R.id.image);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java
new file mode 100644
index 000000000..e650bc22b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java
@@ -0,0 +1,962 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostDiscoverData;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderAnim;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.ui.reader.ReaderTypes;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderBlogActions;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.utils.ReaderXPostUtils;
+import org.wordpress.android.ui.reader.views.ReaderFollowButton;
+import org.wordpress.android.ui.reader.views.ReaderGapMarkerView;
+import org.wordpress.android.ui.reader.views.ReaderIconCountView;
+import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView;
+import org.wordpress.android.ui.reader.views.ReaderTagHeaderView;
+import org.wordpress.android.ui.reader.views.ReaderThumbnailStrip;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.HashSet;
+
+public class ReaderPostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ private ReaderTag mCurrentTag;
+ private long mCurrentBlogId;
+ private long mCurrentFeedId;
+ private int mGapMarkerPosition = -1;
+
+ private final int mPhotonWidth;
+ private final int mPhotonHeight;
+ private final int mAvatarSzMedium;
+ private final int mAvatarSzSmall;
+ private final int mAvatarSzTiny;
+ private final int mMarginLarge;
+
+ private boolean mCanRequestMorePosts;
+ private final boolean mIsLoggedOutReader;
+
+ private final ReaderTypes.ReaderPostListType mPostListType;
+ private final ReaderPostList mPosts = new ReaderPostList();
+ private final HashSet<String> mRenderedIds = new HashSet<>();
+
+ private ReaderInterfaces.OnPostSelectedListener mPostSelectedListener;
+ private ReaderInterfaces.OnTagSelectedListener mOnTagSelectedListener;
+ private ReaderInterfaces.OnPostPopupListener mOnPostPopupListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+ private ReaderActions.DataRequestedListener mDataRequestedListener;
+ private ReaderSiteHeaderView.OnBlogInfoLoadedListener mBlogInfoLoadedListener;
+
+ // the large "tbl_posts.text" column is unused here, so skip it when querying
+ private static final boolean EXCLUDE_TEXT_COLUMN = true;
+ private static final int MAX_ROWS = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY;
+
+ private static final int VIEW_TYPE_POST = 0;
+ private static final int VIEW_TYPE_XPOST = 1;
+ private static final int VIEW_TYPE_SITE_HEADER = 2;
+ private static final int VIEW_TYPE_TAG_HEADER = 3;
+ private static final int VIEW_TYPE_GAP_MARKER = 4;
+
+ private static final long ITEM_ID_CUSTOM_VIEW = -1L;
+
+ /*
+ * cross-post
+ */
+ class ReaderXPostViewHolder extends RecyclerView.ViewHolder {
+ private final CardView cardView;
+ private final WPNetworkImageView imgAvatar;
+ private final WPNetworkImageView imgBlavatar;
+ private final TextView txtTitle;
+ private final TextView txtSubtitle;
+
+ public ReaderXPostViewHolder(View itemView) {
+ super(itemView);
+ cardView = (CardView) itemView.findViewById(R.id.card_view);
+ imgAvatar = (WPNetworkImageView) itemView.findViewById(R.id.image_avatar);
+ imgBlavatar = (WPNetworkImageView) itemView.findViewById(R.id.image_blavatar);
+ txtTitle = (TextView) itemView.findViewById(R.id.text_title);
+ txtSubtitle = (TextView) itemView.findViewById(R.id.text_subtitle);
+ }
+ }
+
+ /*
+ * full post
+ */
+ class ReaderPostViewHolder extends RecyclerView.ViewHolder {
+ private final CardView cardView;
+
+ private final TextView txtTitle;
+ private final TextView txtText;
+ private final TextView txtBlogName;
+ private final TextView txtDomain;
+ private final TextView txtDateline;
+ private final TextView txtTag;
+
+ private final ReaderIconCountView commentCount;
+ private final ReaderIconCountView likeCount;
+
+ private final ImageView imgMore;
+
+ private final WPNetworkImageView imgFeatured;
+ private final WPNetworkImageView imgAvatar;
+ private final WPNetworkImageView imgBlavatar;
+
+ private final ViewGroup layoutPostHeader;
+ private final ReaderFollowButton followButton;
+
+ private final ViewGroup layoutDiscover;
+ private final WPNetworkImageView imgDiscoverAvatar;
+ private final TextView txtDiscover;
+
+ private final ReaderThumbnailStrip thumbnailStrip;
+
+ public ReaderPostViewHolder(View itemView) {
+ super(itemView);
+
+ cardView = (CardView) itemView.findViewById(R.id.card_view);
+
+ txtTitle = (TextView) itemView.findViewById(R.id.text_title);
+ txtText = (TextView) itemView.findViewById(R.id.text_excerpt);
+ txtBlogName = (TextView) itemView.findViewById(R.id.text_blog_name);
+ txtDomain = (TextView) itemView.findViewById(R.id.text_domain);
+ txtDateline = (TextView) itemView.findViewById(R.id.text_dateline);
+ txtTag = (TextView) itemView.findViewById(R.id.text_tag);
+
+ commentCount = (ReaderIconCountView) itemView.findViewById(R.id.count_comments);
+ likeCount = (ReaderIconCountView) itemView.findViewById(R.id.count_likes);
+
+ imgFeatured = (WPNetworkImageView) itemView.findViewById(R.id.image_featured);
+ imgBlavatar = (WPNetworkImageView) itemView.findViewById(R.id.image_blavatar);
+ imgAvatar = (WPNetworkImageView) itemView.findViewById(R.id.image_avatar);
+ imgMore = (ImageView) itemView.findViewById(R.id.image_more);
+
+ layoutDiscover = (ViewGroup) itemView.findViewById(R.id.layout_discover);
+ imgDiscoverAvatar = (WPNetworkImageView) layoutDiscover.findViewById(R.id.image_discover_avatar);
+ txtDiscover = (TextView) layoutDiscover.findViewById(R.id.text_discover);
+
+ thumbnailStrip = (ReaderThumbnailStrip) itemView.findViewById(R.id.thumbnail_strip);
+
+ layoutPostHeader = (ViewGroup) itemView.findViewById(R.id.layout_post_header);
+ followButton = (ReaderFollowButton) layoutPostHeader.findViewById(R.id.follow_button);
+
+ // post header isn't shown when there's a site header, so add a bit more
+ // padding above the title
+ if (hasSiteHeader()) {
+ int extraPadding = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.margin_medium);
+ txtTitle.setPadding(
+ txtTitle.getPaddingLeft(),
+ txtTitle.getPaddingTop() + extraPadding,
+ txtTitle.getPaddingRight(),
+ txtTitle.getPaddingBottom());
+ }
+
+ ReaderUtils.setBackgroundToRoundRipple(imgMore);
+ }
+ }
+
+ class SiteHeaderViewHolder extends RecyclerView.ViewHolder {
+ private final ReaderSiteHeaderView mSiteHeaderView;
+ public SiteHeaderViewHolder(View itemView) {
+ super(itemView);
+ mSiteHeaderView = (ReaderSiteHeaderView) itemView;
+ }
+ }
+
+ class TagHeaderViewHolder extends RecyclerView.ViewHolder {
+ private final ReaderTagHeaderView mTagHeaderView;
+ public TagHeaderViewHolder(View itemView) {
+ super(itemView);
+ mTagHeaderView = (ReaderTagHeaderView) itemView;
+ }
+ }
+
+ class GapMarkerViewHolder extends RecyclerView.ViewHolder {
+ private final ReaderGapMarkerView mGapMarkerView;
+ public GapMarkerViewHolder(View itemView) {
+ super(itemView);
+ mGapMarkerView = (ReaderGapMarkerView) itemView;
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == 0 && hasSiteHeader()) {
+ // first item is a ReaderSiteHeaderView
+ return VIEW_TYPE_SITE_HEADER;
+ } else if (position == 0 && hasTagHeader()) {
+ // first item is a ReaderTagHeaderView
+ return VIEW_TYPE_TAG_HEADER;
+ } else if (position == mGapMarkerPosition) {
+ return VIEW_TYPE_GAP_MARKER;
+ } else if (getItem(position).isXpost()) {
+ return VIEW_TYPE_XPOST;
+ } else {
+ return VIEW_TYPE_POST;
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ Context context = parent.getContext();
+ switch (viewType) {
+ case VIEW_TYPE_SITE_HEADER:
+ return new SiteHeaderViewHolder(new ReaderSiteHeaderView(context));
+
+ case VIEW_TYPE_TAG_HEADER:
+ return new TagHeaderViewHolder(new ReaderTagHeaderView(context));
+
+ case VIEW_TYPE_GAP_MARKER:
+ return new GapMarkerViewHolder(new ReaderGapMarkerView(context));
+
+ case VIEW_TYPE_XPOST:
+ View xpostView = LayoutInflater.from(context).inflate(R.layout.reader_cardview_xpost, parent, false);
+ return new ReaderXPostViewHolder(xpostView);
+
+ default:
+ View postView = LayoutInflater.from(context).inflate(R.layout.reader_cardview_post, parent, false);
+ return new ReaderPostViewHolder(postView);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof ReaderPostViewHolder) {
+ renderPost(position, (ReaderPostViewHolder) holder);
+ } else if (holder instanceof ReaderXPostViewHolder) {
+ renderXPost(position, (ReaderXPostViewHolder) holder);
+ } else if (holder instanceof SiteHeaderViewHolder) {
+ SiteHeaderViewHolder siteHolder = (SiteHeaderViewHolder) holder;
+ siteHolder.mSiteHeaderView.setOnBlogInfoLoadedListener(mBlogInfoLoadedListener);
+ if (isDiscover()) {
+ siteHolder.mSiteHeaderView.loadBlogInfo(ReaderConstants.DISCOVER_SITE_ID, 0);
+ } else {
+ siteHolder.mSiteHeaderView.loadBlogInfo(mCurrentBlogId, mCurrentFeedId);
+ }
+ } else if (holder instanceof TagHeaderViewHolder) {
+ TagHeaderViewHolder tagHolder = (TagHeaderViewHolder) holder;
+ tagHolder.mTagHeaderView.setCurrentTag(mCurrentTag);
+ } else if (holder instanceof GapMarkerViewHolder) {
+ GapMarkerViewHolder gapHolder = (GapMarkerViewHolder) holder;
+ gapHolder.mGapMarkerView.setCurrentTag(mCurrentTag);
+ }
+ }
+
+ private void renderXPost(int position, ReaderXPostViewHolder holder) {
+ final ReaderPost post = getItem(position);
+
+ if (post.hasPostAvatar()) {
+ holder.imgAvatar.setImageUrl(
+ post.getPostAvatarForDisplay(mAvatarSzSmall), WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ holder.imgAvatar.showDefaultGravatarImage();
+ }
+
+ if (post.hasBlogUrl()) {
+ holder.imgBlavatar.setImageUrl(
+ post.getPostBlavatarForDisplay(mAvatarSzMedium), WPNetworkImageView.ImageType.BLAVATAR);
+ } else {
+ holder.imgBlavatar.showDefaultBlavatarImage();
+ }
+
+ holder.txtTitle.setText(ReaderXPostUtils.getXPostTitle(post));
+ holder.txtSubtitle.setText(ReaderXPostUtils.getXPostSubtitleHtml(post));
+
+ holder.cardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPostSelectedListener != null) {
+ mPostSelectedListener.onPostSelected(post);
+ }
+ }
+ });
+
+ checkLoadMore(position);
+ }
+
+ private void renderPost(int position, ReaderPostViewHolder holder) {
+ final ReaderPost post = getItem(position);
+ ReaderTypes.ReaderPostListType postListType = getPostListType();
+
+ holder.txtTitle.setText(post.getTitle());
+
+ String timestamp = DateTimeUtils.javaDateToTimeSpan(post.getDisplayDate(), WordPress.getContext());
+ if (post.hasAuthorName()) {
+ holder.txtDateline.setText(post.getAuthorName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp);
+ } else if (post.hasBlogName()) {
+ holder.txtDateline.setText(post.getBlogName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp);
+ } else {
+ holder.txtDateline.setText(timestamp);
+ }
+
+ if (post.hasPostAvatar()) {
+ holder.imgAvatar.setImageUrl(
+ post.getPostAvatarForDisplay(mAvatarSzTiny), WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ holder.imgAvatar.showDefaultGravatarImage();
+ }
+
+ // post header isn't show when there's a site header
+ if (hasSiteHeader()) {
+ holder.layoutPostHeader.setVisibility(View.GONE);
+ } else {
+ holder.layoutPostHeader.setVisibility(View.VISIBLE);
+ // show blog preview when post header is tapped
+ holder.layoutPostHeader.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderBlogPreview(view.getContext(), post);
+ }
+ });
+ }
+
+ if (post.hasBlogUrl()) {
+ String imageUrl = GravatarUtils.blavatarFromUrl(post.getBlogUrl(), mAvatarSzMedium);
+ holder.imgBlavatar.setImageUrl(imageUrl, WPNetworkImageView.ImageType.BLAVATAR);
+ holder.txtDomain.setText(UrlUtils.getHost(post.getBlogUrl()));
+ } else {
+ holder.imgBlavatar.showDefaultBlavatarImage();
+ holder.txtDomain.setText(null);
+ }
+ if (post.hasBlogName()) {
+ holder.txtBlogName.setText(post.getBlogName());
+ } else if (post.hasAuthorName()) {
+ holder.txtBlogName.setText(post.getAuthorName());
+ } else {
+ holder.txtBlogName.setText(null);
+ }
+
+ if (post.hasExcerpt()) {
+ holder.txtText.setVisibility(View.VISIBLE);
+ holder.txtText.setText(post.getExcerpt());
+ } else {
+ holder.txtText.setVisibility(View.GONE);
+ }
+
+ final int titleMargin;
+ if (post.hasFeaturedImage()) {
+ final String imageUrl = post.getFeaturedImageForDisplay(mPhotonWidth, mPhotonHeight);
+ holder.imgFeatured.setImageUrl(imageUrl, WPNetworkImageView.ImageType.PHOTO);
+ holder.imgFeatured.setVisibility(View.VISIBLE);
+ titleMargin = mMarginLarge;
+ } else if (post.hasFeaturedVideo() && WPNetworkImageView.canShowVideoThumbnail(post.getFeaturedVideo())) {
+ holder.imgFeatured.setVideoUrl(post.postId, post.getFeaturedVideo());
+ holder.imgFeatured.setVisibility(View.VISIBLE);
+ titleMargin = mMarginLarge;
+ } else {
+ holder.imgFeatured.setVisibility(View.GONE);
+ titleMargin = (holder.layoutPostHeader.getVisibility() == View.VISIBLE ? 0 : mMarginLarge);
+ }
+
+ // set the top margin of the title based on whether there's a featured image and post header
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) holder.txtTitle.getLayoutParams();
+ params.topMargin = titleMargin;
+
+ // show the best tag for this post
+ final String tagToDisplay = (mCurrentTag != null ? post.getTagForDisplay(mCurrentTag.getTagSlug()) : null);
+ if (!TextUtils.isEmpty(tagToDisplay)) {
+ holder.txtTag.setText(ReaderUtils.makeHashTag(tagToDisplay));
+ holder.txtTag.setVisibility(View.VISIBLE);
+ holder.txtTag.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnTagSelectedListener != null) {
+ mOnTagSelectedListener.onTagSelected(tagToDisplay);
+ }
+ }
+ });
+ } else {
+ holder.txtTag.setVisibility(View.GONE);
+ holder.txtTag.setOnClickListener(null);
+ }
+
+ showLikes(holder, post);
+ showComments(holder, post);
+
+ // more menu only shows for followed tags
+ if (!mIsLoggedOutReader && postListType == ReaderTypes.ReaderPostListType.TAG_FOLLOWED) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mOnPostPopupListener != null) {
+ mOnPostPopupListener.onShowPostPopup(view, post);
+ }
+ }
+ });
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setOnClickListener(null);
+ }
+
+ // follow button doesn't show for "Followed Sites" or when there's a site header (Discover, site preview)
+ boolean showFollowButton = !hasSiteHeader()
+ && !mIsLoggedOutReader
+ && !isFollowedSites();
+ if (showFollowButton) {
+ holder.followButton.setIsFollowed(post.isFollowedByCurrentUser);
+ holder.followButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleFollow(view.getContext(), view, post);
+ }
+ });
+ holder.followButton.setVisibility(View.VISIBLE);
+ } else {
+ holder.followButton.setVisibility(View.GONE);
+ }
+
+ // attribution section for discover posts
+ if (post.isDiscoverPost()) {
+ showDiscoverData(holder, post);
+ } else {
+ holder.layoutDiscover.setVisibility(View.GONE);
+ }
+
+ // if this post has attachments or contains a gallery, scan it for images and show a
+ // thumbnail strip of them - note that the thumbnail strip will take care of making
+ // itself visible
+ if (post.hasAttachments() || post.isGallery()) {
+ holder.thumbnailStrip.loadThumbnails(post.blogId, post.postId, post.isPrivate);
+ } else {
+ holder.thumbnailStrip.setVisibility(View.GONE);
+ }
+
+ holder.cardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPostSelectedListener != null) {
+ mPostSelectedListener.onPostSelected(post);
+ }
+ }
+ });
+
+ checkLoadMore(position);
+
+ // if we haven't already rendered this post and it has a "railcar" attached to it, add it
+ // to the rendered list and record the TrainTracks render event
+ if (post.hasRailcar() && !mRenderedIds.contains(post.getPseudoId())) {
+ mRenderedIds.add(post.getPseudoId());
+ AnalyticsUtils.trackRailcarRender(post.getRailcarJson());
+ }
+ }
+
+ /*
+ * if we're nearing the end of the posts, fire request to load more
+ */
+ private void checkLoadMore(int position) {
+ if (mCanRequestMorePosts
+ && mDataRequestedListener != null
+ && (position >= getItemCount() - 1)) {
+ mDataRequestedListener.onRequestData();
+ }
+ }
+
+ private void showDiscoverData(final ReaderPostViewHolder postHolder,
+ final ReaderPost post) {
+ final ReaderPostDiscoverData discoverData = post.getDiscoverData();
+ if (discoverData == null) {
+ postHolder.layoutDiscover.setVisibility(View.GONE);
+ return;
+ }
+
+ postHolder.layoutDiscover.setVisibility(View.VISIBLE);
+ postHolder.txtDiscover.setText(discoverData.getAttributionHtml());
+
+ switch (discoverData.getDiscoverType()) {
+ case EDITOR_PICK:
+ if (discoverData.hasAvatarUrl()) {
+ postHolder.imgDiscoverAvatar.setImageUrl(GravatarUtils.fixGravatarUrl(discoverData.getAvatarUrl(), mAvatarSzSmall), WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ postHolder.imgDiscoverAvatar.showDefaultGravatarImage();
+ }
+ // tapping an editor pick opens the source post, which is handled by the existing
+ // post selection handler
+ postHolder.layoutDiscover.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPostSelectedListener != null) {
+ mPostSelectedListener.onPostSelected(post);
+ }
+ }
+ });
+ break;
+
+ case SITE_PICK:
+ if (discoverData.hasAvatarUrl()) {
+ postHolder.imgDiscoverAvatar.setImageUrl(
+ GravatarUtils.fixGravatarUrl(discoverData.getAvatarUrl(), mAvatarSzSmall), WPNetworkImageView.ImageType.BLAVATAR);
+ } else {
+ postHolder.imgDiscoverAvatar.showDefaultBlavatarImage();
+ }
+ // site picks show "Visit [BlogName]" link - tapping opens the blog preview if
+ // we have the blogId, if not show blog in internal webView
+ postHolder.layoutDiscover.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (discoverData.getBlogId() != 0) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ v.getContext(),
+ discoverData.getBlogId());
+ } else if (discoverData.hasBlogUrl()) {
+ ReaderActivityLauncher.openUrl(v.getContext(), discoverData.getBlogUrl());
+ }
+ }
+ });
+ break;
+
+ default:
+ // something else, so hide discover section
+ postHolder.layoutDiscover.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ // ********************************************************************************************
+
+ public ReaderPostAdapter(Context context, ReaderTypes.ReaderPostListType postListType) {
+ super();
+
+ mPostListType = postListType;
+ mAvatarSzMedium = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium);
+ mAvatarSzSmall = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small);
+ mAvatarSzTiny = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_tiny);
+ mMarginLarge = context.getResources().getDimensionPixelSize(R.dimen.margin_large);
+ mIsLoggedOutReader = ReaderUtils.isLoggedOutReader();
+
+ int displayWidth = DisplayUtils.getDisplayPixelWidth(context);
+ int cardMargin = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin);
+ mPhotonWidth = displayWidth - (cardMargin * 2);
+ mPhotonHeight = context.getResources().getDimensionPixelSize(R.dimen.reader_featured_image_height);
+
+ setHasStableIds(true);
+ }
+
+ private boolean hasCustomFirstItem() {
+ return hasSiteHeader() || hasTagHeader();
+ }
+
+ private boolean hasSiteHeader() {
+ return isDiscover() || getPostListType() == ReaderTypes.ReaderPostListType.BLOG_PREVIEW;
+ }
+
+ private boolean hasTagHeader() {
+ return getPostListType() == ReaderTypes.ReaderPostListType.TAG_PREVIEW;
+ }
+
+ private boolean isDiscover() {
+ return mCurrentTag != null && mCurrentTag.isDiscover();
+ }
+
+ private boolean isFollowedSites() {
+ return mCurrentTag != null && mCurrentTag.isFollowedSites();
+ }
+
+ public void setOnPostSelectedListener(ReaderInterfaces.OnPostSelectedListener listener) {
+ mPostSelectedListener = listener;
+ }
+
+ public void setOnDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ public void setOnDataRequestedListener(ReaderActions.DataRequestedListener listener) {
+ mDataRequestedListener = listener;
+ }
+
+ public void setOnPostPopupListener(ReaderInterfaces.OnPostPopupListener onPostPopupListener) {
+ mOnPostPopupListener = onPostPopupListener;
+ }
+
+ public void setOnBlogInfoLoadedListener(ReaderSiteHeaderView.OnBlogInfoLoadedListener listener) {
+ mBlogInfoLoadedListener = listener;
+ }
+
+ /*
+ * called when user clicks a tag
+ */
+ public void setOnTagSelectedListener(ReaderInterfaces.OnTagSelectedListener listener) {
+ mOnTagSelectedListener = listener;
+ }
+
+ private ReaderTypes.ReaderPostListType getPostListType() {
+ return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE);
+ }
+
+ // used when the viewing tagged posts
+ public void setCurrentTag(ReaderTag tag) {
+ if (!ReaderTag.isSameTag(tag, mCurrentTag)) {
+ mCurrentTag = tag;
+ mRenderedIds.clear();
+ reload();
+ }
+ }
+
+ public boolean isCurrentTag(ReaderTag tag) {
+ return ReaderTag.isSameTag(tag, mCurrentTag);
+ }
+
+ // used when the list type is ReaderPostListType.BLOG_PREVIEW
+ public void setCurrentBlogAndFeed(long blogId, long feedId) {
+ if (blogId != mCurrentBlogId || feedId != mCurrentFeedId) {
+ mCurrentBlogId = blogId;
+ mCurrentFeedId = feedId;
+ mRenderedIds.clear();
+ reload();
+ }
+ }
+
+ public void clear() {
+ if (!mPosts.isEmpty()) {
+ mPosts.clear();
+ notifyDataSetChanged();
+ }
+ }
+
+ public void refresh() {
+ loadPosts();
+ }
+
+ /*
+ * same as refresh() above but first clears the existing posts
+ */
+ public void reload() {
+ clear();
+ loadPosts();
+ }
+
+ public void removePostsInBlog(long blogId) {
+ int numRemoved = 0;
+ ReaderPostList postsInBlog = mPosts.getPostsInBlog(blogId);
+ for (ReaderPost post : postsInBlog) {
+ int index = mPosts.indexOfPost(post);
+ if (index > -1) {
+ numRemoved++;
+ mPosts.remove(index);
+ }
+ }
+ if (numRemoved > 0) {
+ notifyDataSetChanged();
+ }
+ }
+
+ private void loadPosts() {
+ if (mIsTaskRunning) {
+ AppLog.w(AppLog.T.READER, "reader posts task already running");
+ return;
+ }
+ new LoadPostsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private ReaderPost getItem(int position) {
+ if (position == 0 && hasCustomFirstItem()) {
+ return null;
+ }
+ if (position == mGapMarkerPosition) {
+ return null;
+ }
+
+ int arrayPos = hasCustomFirstItem() ? position - 1 : position;
+
+ if (mGapMarkerPosition > -1 && position > mGapMarkerPosition) {
+ arrayPos--;
+ }
+
+ return mPosts.get(arrayPos);
+ }
+
+ @Override
+ public int getItemCount() {
+ if (hasCustomFirstItem()) {
+ return mPosts.size() + 1;
+ }
+ return mPosts.size();
+ }
+
+ public boolean isEmpty() {
+ return (mPosts == null || mPosts.size() == 0);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (getItemViewType(position) == VIEW_TYPE_POST) {
+ return getItem(position).getStableId();
+ } else {
+ return ITEM_ID_CUSTOM_VIEW;
+ }
+ }
+
+ private void showLikes(final ReaderPostViewHolder holder, final ReaderPost post) {
+ boolean canShowLikes;
+ if (post.isDiscoverPost()) {
+ canShowLikes = false;
+ } else if (mIsLoggedOutReader) {
+ canShowLikes = post.numLikes > 0;
+ } else {
+ canShowLikes = post.canLikePost();
+ }
+
+ if (canShowLikes) {
+ holder.likeCount.setCount(post.numLikes);
+ holder.likeCount.setSelected(post.isLikedByCurrentUser);
+ holder.likeCount.setVisibility(View.VISIBLE);
+ // can't like when logged out
+ if (!mIsLoggedOutReader) {
+ holder.likeCount.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleLike(v.getContext(), holder, post);
+ }
+ });
+ }
+ } else {
+ holder.likeCount.setVisibility(View.GONE);
+ holder.likeCount.setOnClickListener(null);
+ }
+ }
+
+ private void showComments(final ReaderPostViewHolder holder, final ReaderPost post) {
+ boolean canShowComments;
+ if (post.isDiscoverPost()) {
+ canShowComments = false;
+ } else if (mIsLoggedOutReader) {
+ canShowComments = post.numReplies > 0;
+ } else {
+ canShowComments = post.isWP() && !post.isJetpack && (post.isCommentsOpen || post.numReplies > 0);
+ }
+
+ if (canShowComments) {
+ holder.commentCount.setCount(post.numReplies);
+ holder.commentCount.setVisibility(View.VISIBLE);
+ holder.commentCount.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderActivityLauncher.showReaderComments(v.getContext(), post.blogId, post.postId);
+ }
+ });
+ } else {
+ holder.commentCount.setVisibility(View.GONE);
+ holder.commentCount.setOnClickListener(null);
+ }
+ }
+
+ /*
+ * triggered when user taps the like button (textView)
+ */
+ private void toggleLike(Context context, ReaderPostViewHolder holder, ReaderPost post) {
+ if (post == null || !NetworkUtils.checkConnection(context)) {
+ return;
+ }
+
+ boolean isCurrentlyLiked = ReaderPostTable.isPostLikedByCurrentUser(post);
+ boolean isAskingToLike = !isCurrentlyLiked;
+ ReaderAnim.animateLikeButton(holder.likeCount.getImageView(), isAskingToLike);
+
+ if (!ReaderPostActions.performLikeAction(post, isAskingToLike)) {
+ ToastUtils.showToast(context, R.string.reader_toast_err_generic);
+ return;
+ }
+
+ if (isAskingToLike) {
+ AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, post);
+ // Consider a like to be enough to push a page view - solves a long-standing question
+ // from folks who ask 'why do I have more likes than page views?'.
+ ReaderPostActions.bumpPageViewForPost(post);
+ } else {
+ AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, post);
+ }
+
+ // update post in array and on screen
+ int position = mPosts.indexOfPost(post);
+ ReaderPost updatedPost = ReaderPostTable.getPost(post.blogId, post.postId, true);
+ if (updatedPost != null && position > -1) {
+ mPosts.set(position, updatedPost);
+ showLikes(holder, updatedPost);
+ }
+ }
+
+ /*
+ * triggered when user taps the follow button on a post
+ */
+ private void toggleFollow(final Context context, final View followButton, final ReaderPost post) {
+ if (post == null || !NetworkUtils.checkConnection(context)) {
+ return;
+ }
+
+ boolean isCurrentlyFollowed = ReaderPostTable.isPostFollowed(post);
+ final boolean isAskingToFollow = !isCurrentlyFollowed;
+
+ ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ followButton.setEnabled(true);
+ if (!succeeded) {
+ int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog);
+ ToastUtils.showToast(context, resId);
+ setFollowStatusForBlog(post.blogId, !isAskingToFollow);
+ }
+ }
+ };
+
+ if (!ReaderBlogActions.followBlogForPost(post, isAskingToFollow, actionListener)) {
+ ToastUtils.showToast(context, R.string.reader_toast_err_generic);
+ return;
+ }
+
+ followButton.setEnabled(false);
+ setFollowStatusForBlog(post.blogId, isAskingToFollow);
+ }
+
+ public void setFollowStatusForBlog(long blogId, boolean isFollowing) {
+ ReaderPost post;
+ for (int i = 0; i < mPosts.size(); i++) {
+ post = mPosts.get(i);
+ if (post.blogId == blogId && post.isFollowedByCurrentUser != isFollowing) {
+ post.isFollowedByCurrentUser = isFollowing;
+ mPosts.set(i, post);
+ notifyItemChanged(i);
+ }
+ }
+ }
+
+ public void removeGapMarker() {
+ if (mGapMarkerPosition == -1) return;
+
+ int position = mGapMarkerPosition;
+ mGapMarkerPosition = -1;
+ if (position < getItemCount()) {
+ notifyItemRemoved(position);
+ }
+ }
+
+ /*
+ * AsyncTask to load posts in the current tag
+ */
+ private boolean mIsTaskRunning = false;
+
+ private class LoadPostsTask extends AsyncTask<Void, Void, Boolean> {
+ ReaderPostList allPosts;
+
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ int numExisting;
+ switch (getPostListType()) {
+ case TAG_PREVIEW:
+ case TAG_FOLLOWED:
+ case SEARCH_RESULTS:
+ allPosts = ReaderPostTable.getPostsWithTag(mCurrentTag, MAX_ROWS, EXCLUDE_TEXT_COLUMN);
+ numExisting = ReaderPostTable.getNumPostsWithTag(mCurrentTag);
+ break;
+ case BLOG_PREVIEW:
+ if (mCurrentFeedId != 0) {
+ allPosts = ReaderPostTable.getPostsInFeed(mCurrentFeedId, MAX_ROWS, EXCLUDE_TEXT_COLUMN);
+ numExisting = ReaderPostTable.getNumPostsInFeed(mCurrentFeedId);
+ } else {
+ allPosts = ReaderPostTable.getPostsInBlog(mCurrentBlogId, MAX_ROWS, EXCLUDE_TEXT_COLUMN);
+ numExisting = ReaderPostTable.getNumPostsInBlog(mCurrentBlogId);
+ }
+ break;
+ default:
+ return false;
+ }
+
+ if (mPosts.isSameList(allPosts)) {
+ return false;
+ }
+
+ // if we're not already displaying the max # posts, enable requesting more when
+ // the user scrolls to the end of the list
+ mCanRequestMorePosts = (numExisting < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY);
+
+ // determine whether a gap marker exists - only applies to tagged posts
+ mGapMarkerPosition = getGapMarkerPosition();
+
+ return true;
+ }
+
+ private int getGapMarkerPosition() {
+ if (!getPostListType().isTagType()) {
+ return -1;
+ }
+
+ ReaderBlogIdPostId gapMarkerIds = ReaderPostTable.getGapMarkerIdsForTag(mCurrentTag);
+ if (gapMarkerIds == null) {
+ return -1;
+ }
+
+ // find the position of the gap marker post
+ int gapPosition = allPosts.indexOfIds(gapMarkerIds);
+ if (gapPosition > -1) {
+ // increment it because we want the gap marker to appear *below* this post
+ gapPosition++;
+ // increment it again if there's a custom first item
+ if (hasCustomFirstItem()) {
+ gapPosition++;
+ }
+ // remove the gap marker if it's on the last post (edge case but
+ // it can happen following a purge)
+ if (gapPosition >= allPosts.size() - 1) {
+ gapPosition = -1;
+ AppLog.w(AppLog.T.READER, "gap marker at/after last post, removed");
+ ReaderPostTable.removeGapMarkerForTag(mCurrentTag);
+ } else {
+ AppLog.d(AppLog.T.READER, "gap marker at position " + gapPosition);
+ }
+ }
+ return gapPosition;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ mPosts.clear();
+ mPosts.addAll(allPosts);
+ notifyDataSetChanged();
+ }
+
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+
+ mIsTaskRunning = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java
new file mode 100644
index 000000000..44228bb58
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java
@@ -0,0 +1,184 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.CursorAdapter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderSearchTable;
+
+public class ReaderSearchSuggestionAdapter extends CursorAdapter {
+ private static final int MAX_SUGGESTIONS = 5;
+ private static final int CLEAR_ALL_ROW_ID = -1;
+
+ private static final int NUM_VIEW_TYPES = 2;
+ private static final int VIEW_TYPE_QUERY = 0;
+ private static final int VIEW_TYPE_CLEAR = 1;
+
+ private String mCurrentFilter;
+ private final Object[] mClearAllRow;
+ private final int mClearAllBgColor;
+ private final int mSuggestionBgColor;
+
+ public ReaderSearchSuggestionAdapter(Context context) {
+ super(context, null, false);
+ String clearAllText = context.getString(R.string.label_clear_search_history);
+ mClearAllRow = new Object[]{CLEAR_ALL_ROW_ID, clearAllText};
+ mClearAllBgColor = ContextCompat.getColor(context, R.color.grey_lighten_30);
+ mSuggestionBgColor = ContextCompat.getColor(context, R.color.filtered_list_suggestions);
+ }
+
+ public void setFilter(String filter) {
+ // skip if unchanged
+ if (isCurrentFilter(filter) && getCursor() != null) {
+ return;
+ }
+
+ // get db cursor containing matching query strings
+ Cursor sqlCursor = ReaderSearchTable.getQueryStringCursor(filter, MAX_SUGGESTIONS);
+
+ // create a MatrixCursor which will be the actual cursor behind this adapter
+ MatrixCursor matrixCursor = new MatrixCursor(
+ new String[]{
+ ReaderSearchTable.COL_ID,
+ ReaderSearchTable.COL_QUERY});
+
+ if (sqlCursor.moveToFirst()) {
+ // first populate the matrix from the db cursor...
+ do {
+ long id = sqlCursor.getLong(sqlCursor.getColumnIndex(ReaderSearchTable.COL_ID));
+ String query = sqlCursor.getString(sqlCursor.getColumnIndex(ReaderSearchTable.COL_QUERY));
+ matrixCursor.addRow(new Object[]{id, query});
+ } while (sqlCursor.moveToNext());
+
+ // ...then add our custom item
+ matrixCursor.addRow(mClearAllRow);
+ }
+
+ mCurrentFilter = filter;
+ swapCursor(matrixCursor);
+ }
+
+ /*
+ * forces setFilter() to always repopulate by skipping the isCurrentFilter() check
+ */
+ private void reload() {
+ String newFilter = mCurrentFilter;
+ mCurrentFilter = null;
+ setFilter(newFilter);
+ }
+
+ private boolean isCurrentFilter(String filter) {
+ if (TextUtils.isEmpty(filter) && TextUtils.isEmpty(mCurrentFilter)) {
+ return true;
+ }
+ return filter != null && filter.equalsIgnoreCase(mCurrentFilter);
+ }
+
+ public String getSuggestion(int position) {
+ Cursor cursor = (Cursor) getItem(position);
+ if (cursor != null) {
+ return cursor.getString(cursor.getColumnIndex(ReaderSearchTable.COL_QUERY));
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ // use a different view type for the "clear" row so it doesn't get recycled and used
+ // as a query row
+ if (getItemId(position) == CLEAR_ALL_ROW_ID) {
+ return VIEW_TYPE_CLEAR;
+ }
+ return VIEW_TYPE_QUERY;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return NUM_VIEW_TYPES;
+ }
+
+ private class SuggestionViewHolder {
+ private final TextView txtSuggestion;
+ private final ImageView imgDelete;
+
+ SuggestionViewHolder(View view) {
+ txtSuggestion = (TextView) view.findViewById(R.id.text_suggestion);
+ imgDelete = (ImageView) view.findViewById(R.id.image_delete);
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = LayoutInflater.from(context).inflate(R.layout.reader_listitem_suggestion, parent, false);
+
+ SuggestionViewHolder holder = new SuggestionViewHolder(view);
+ view.setTag(holder);
+
+ long id = cursor.getLong(cursor.getColumnIndex(ReaderSearchTable.COL_ID));
+ if (id == CLEAR_ALL_ROW_ID) {
+ view.setBackgroundColor(mClearAllBgColor);
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ confirmClearSavedSearches(v.getContext());
+ }
+ });
+ holder.imgDelete.setVisibility(View.GONE);
+ } else {
+ view.setBackgroundColor(mSuggestionBgColor);
+ }
+
+ return view;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ SuggestionViewHolder holder = (SuggestionViewHolder) view.getTag();
+
+ final String query = cursor.getString(cursor.getColumnIndex(ReaderSearchTable.COL_QUERY));
+ holder.txtSuggestion.setText(query);
+
+ long id = cursor.getLong(cursor.getColumnIndex(ReaderSearchTable.COL_ID));
+ if (id != CLEAR_ALL_ROW_ID) {
+ holder.imgDelete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderSearchTable.deleteQueryString(query);
+ reload();
+ }
+ });
+ }
+ }
+
+ private void confirmClearSavedSearches(Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.dlg_confirm_clear_search_history)
+ .setCancelable(true)
+ .setNegativeButton(R.string.no, null)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ clearSavedSearches();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ private void clearSavedSearches() {
+ ReaderSearchTable.deleteAllQueries();
+ swapCursor(null);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java
new file mode 100644
index 000000000..de203e0a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java
@@ -0,0 +1,174 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderTagActions;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.lang.ref.WeakReference;
+
+public class ReaderTagAdapter extends RecyclerView.Adapter<ReaderTagAdapter.TagViewHolder> {
+
+ public interface TagDeletedListener {
+ void onTagDeleted(ReaderTag tag);
+ }
+
+ private final WeakReference<Context> mWeakContext;
+ private final ReaderTagList mTags = new ReaderTagList();
+ private TagDeletedListener mTagDeletedListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+
+ public ReaderTagAdapter(Context context) {
+ super();
+ setHasStableIds(true);
+ mWeakContext = new WeakReference<>(context);
+ }
+
+ public void setTagDeletedListener(TagDeletedListener listener) {
+ mTagDeletedListener = listener;
+ }
+
+ public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ private boolean hasContext() {
+ return (getContext() != null);
+ }
+
+ private Context getContext() {
+ return mWeakContext.get();
+ }
+
+ public void refresh() {
+ if (mIsTaskRunning) {
+ AppLog.w(T.READER, "tag task is already running");
+ return;
+ }
+ new LoadTagsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mTags.size();
+ }
+
+ public boolean isEmpty() {
+ return (getItemCount() == 0);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mTags.get(position).getTagSlug().hashCode();
+ }
+
+ @Override
+ public TagViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_tag, parent, false);
+ return new TagViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(TagViewHolder holder, int position) {
+ final ReaderTag tag = mTags.get(position);
+ holder.txtTagName.setText(tag.getLabel());
+ holder.btnRemove.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performDeleteTag(tag);
+
+ }
+ });
+ }
+
+ private void performDeleteTag(@NonNull ReaderTag tag) {
+ if (!NetworkUtils.checkConnection(getContext())) {
+ return;
+ }
+
+ ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (!succeeded && hasContext()) {
+ ToastUtils.showToast(getContext(), R.string.reader_toast_err_remove_tag);
+ refresh();
+ }
+ }
+ };
+
+ boolean success = ReaderTagActions.deleteTag(tag, actionListener);
+
+ if (success) {
+ int index = mTags.indexOfTagName(tag.getTagSlug());
+ if (index > -1) {
+ mTags.remove(index);
+ notifyItemRemoved(index);
+ }
+ if (mTagDeletedListener != null) {
+ mTagDeletedListener.onTagDeleted(tag);
+ }
+ }
+ }
+
+ class TagViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtTagName;
+ private final ImageButton btnRemove;
+
+ public TagViewHolder(View view) {
+ super(view);
+ txtTagName = (TextView) view.findViewById(R.id.text_topic);
+ btnRemove = (ImageButton) view.findViewById(R.id.btn_remove);
+ ReaderUtils.setBackgroundToRoundRipple(btnRemove);
+ }
+ }
+
+ /*
+ * AsyncTask to load tags
+ */
+ private boolean mIsTaskRunning = false;
+ private class LoadTagsTask extends AsyncTask<Void, Void, ReaderTagList> {
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+ @Override
+ protected ReaderTagList doInBackground(Void... params) {
+ return ReaderTagTable.getFollowedTags();
+ }
+ @Override
+ protected void onPostExecute(ReaderTagList tagList) {
+ if (tagList != null && !tagList.isSameList(mTags)) {
+ mTags.clear();
+ mTags.addAll(tagList);
+ notifyDataSetChanged();
+ }
+ mIsTaskRunning = false;
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java
new file mode 100644
index 000000000..00e6ce023
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java
@@ -0,0 +1,112 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.ReaderUser;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderInterfaces.DataLoadedListener;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+/**
+ * owner must call setUsers() with the list of
+ * users to display
+ */
+public class ReaderUserAdapter extends RecyclerView.Adapter<ReaderUserAdapter.UserViewHolder> {
+ private final ReaderUserList mUsers = new ReaderUserList();
+ private DataLoadedListener mDataLoadedListener;
+ private final int mAvatarSz;
+
+ public ReaderUserAdapter(Context context) {
+ super();
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small);
+ setHasStableIds(true);
+ }
+
+ public void setDataLoadedListener(DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mUsers.size();
+ }
+
+ private boolean isEmpty() {
+ return (getItemCount() == 0);
+ }
+
+ @Override
+ public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_user, parent, false);
+ return new UserViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(UserViewHolder holder, int position) {
+ final ReaderUser user = mUsers.get(position);
+
+ holder.txtName.setText(user.getDisplayName());
+ if (user.hasUrl()) {
+ holder.txtUrl.setVisibility(View.VISIBLE);
+ holder.txtUrl.setText(user.getUrlDomain());
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (user.hasBlogId()) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ v.getContext(),
+ user.blogId);
+ }
+ }
+ });
+ } else {
+ holder.txtUrl.setVisibility(View.GONE);
+ holder.itemView.setOnClickListener(null);
+ }
+
+ if (user.hasAvatarUrl()) {
+ holder.imgAvatar.setImageUrl(
+ GravatarUtils.fixGravatarUrl(user.getAvatarUrl(), mAvatarSz),
+ WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ holder.imgAvatar.showDefaultGravatarImage();
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mUsers.get(position).userId;
+ }
+
+ class UserViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtName;
+ private final TextView txtUrl;
+ private final WPNetworkImageView imgAvatar;
+
+ public UserViewHolder(View view) {
+ super(view);
+ txtName = (TextView) view.findViewById(R.id.text_name);
+ txtUrl = (TextView) view.findViewById(R.id.text_url);
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.image_avatar);
+ }
+ }
+
+ public void setUsers(final ReaderUserList users) {
+ mUsers.clear();
+ if (users != null && users.size() > 0) {
+ mUsers.addAll(users);
+ }
+ notifyDataSetChanged();
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostId.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostId.java
new file mode 100644
index 000000000..a058ff20a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostId.java
@@ -0,0 +1,23 @@
+package org.wordpress.android.ui.reader.models;
+
+import java.io.Serializable;
+
+public class ReaderBlogIdPostId implements Serializable {
+ private static final long serialVersionUID = 0L;
+
+ private final long blogId;
+ private final long postId;
+
+ public ReaderBlogIdPostId(long blogId, long postId) {
+ this.blogId = blogId;
+ this.postId = postId;
+ }
+
+ public long getBlogId() {
+ return blogId;
+ }
+
+ public long getPostId() {
+ return postId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostIdList.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostIdList.java
new file mode 100644
index 000000000..a19eec5be
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderBlogIdPostIdList.java
@@ -0,0 +1,40 @@
+package org.wordpress.android.ui.reader.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+public class ReaderBlogIdPostIdList extends ArrayList<ReaderBlogIdPostId>
+ implements Serializable {
+
+ private static final long serialVersionUID = 0L;
+
+ public ReaderBlogIdPostIdList() {
+ super();
+ }
+
+ /*
+ * when Android serializes any ArrayList descendant, it does so as an ArrayList
+ * rather than its actual class - use this to convert the serialized list back
+ * into a ReaderBlogIdPostIdList
+ */
+ @SuppressWarnings("unused")
+ public ReaderBlogIdPostIdList(Serializable serializedList) {
+ super();
+ if (serializedList != null && serializedList instanceof ArrayList) {
+ //noinspection unchecked
+ ArrayList<ReaderBlogIdPostId> list = (ArrayList<ReaderBlogIdPostId>) serializedList;
+ for (ReaderBlogIdPostId idPair: list) {
+ this.add(idPair);
+ }
+ }
+ }
+
+ public int indexOf(long blogId, long postId) {
+ for (int i = 0; i < this.size(); i++) {
+ if (this.get(i).getBlogId() == blogId && this.get(i).getPostId() == postId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderImageList.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderImageList.java
new file mode 100644
index 000000000..0fdbe14a4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderImageList.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.ui.reader.models;
+
+import org.wordpress.android.util.UrlUtils;
+
+import java.util.ArrayList;
+
+/*
+ * used by ReaderImageScanner to compile a list of images in a specific post
+ */
+
+public class ReaderImageList extends ArrayList<String> {
+ private final boolean mIsPrivate;
+
+ public ReaderImageList(boolean isPrivate) {
+ mIsPrivate = isPrivate;
+ }
+
+ // image urls are always added normalized and without query params for easier matching, and
+ // to ensure there are no hard-coded sizes in the query
+ private static String fixImageUrl(final String imageUrl) {
+ if (imageUrl == null) {
+ return null;
+ }
+ return UrlUtils.normalizeUrl(UrlUtils.removeQuery(imageUrl));
+ }
+
+ public int indexOfImageUrl(final String imageUrl) {
+ if (imageUrl == null || this.isEmpty()) {
+ return -1;
+ }
+ String fixedUrl = fixImageUrl(imageUrl);
+ for (int i = 0; i < this.size(); i++) {
+ if (fixedUrl.equals(this.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public boolean hasImageUrl(final String imageUrl) {
+ return (indexOfImageUrl(imageUrl) > -1);
+ }
+
+ public void addImageUrl(String imageUrl) {
+ if (imageUrl != null && imageUrl.startsWith("http")) {
+ this.add(fixImageUrl(imageUrl));
+ }
+ }
+
+ public void addImageUrl(@SuppressWarnings("SameParameterValue") int index,
+ String imageUrl) {
+ if (imageUrl != null && imageUrl.startsWith("http")) {
+ this.add(index, fixImageUrl(imageUrl));
+ }
+ }
+
+ public boolean isPrivate() {
+ return mIsPrivate;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java
new file mode 100644
index 000000000..e95968908
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java
@@ -0,0 +1,86 @@
+package org.wordpress.android.ui.reader.models;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.util.UrlUtils;
+
+/**
+ * simplified version of a reader post that contains only the fields necessary for a related post
+ */
+public class ReaderRelatedPost {
+ private final long mPostId;
+ private final long mBlogId;
+ private final String mTitle;
+ private final String mByline;
+ private final String mFeaturedImage;
+
+ public ReaderRelatedPost(@NonNull ReaderPost post) {
+ mPostId = post.postId;
+ mBlogId = post.blogId;
+
+ mTitle = post.getTitle();
+ mFeaturedImage = post.getFeaturedImage();
+
+ /*
+ * we want to include the blog name in the byline when it's available, and most sites
+ * will have a name, but in rare cases there isn't one so we show the domain instead
+ */
+ String blogNameOrDomain;
+ boolean hasBlogNameOrDomain;
+ if (post.hasBlogName()) {
+ blogNameOrDomain = post.getBlogName();
+ hasBlogNameOrDomain = true;
+ } else if (post.hasBlogUrl()) {
+ blogNameOrDomain = UrlUtils.getHost(post.getBlogUrl());
+ hasBlogNameOrDomain = true;
+ } else {
+ blogNameOrDomain = null;
+ hasBlogNameOrDomain = false;
+ }
+
+ /*
+ * The byline should show the author name and blog name if both are available, but if
+ * they're the same (which happens frequently) we only need to show the blog name.
+ * Otherwise, show either the blog name or author name depending on which is available.
+ */
+ if (post.hasAuthorName() && hasBlogNameOrDomain) {
+ if (post.getAuthorName().equalsIgnoreCase(blogNameOrDomain)) {
+ mByline = blogNameOrDomain;
+ } else {
+ mByline = post.getAuthorName() + ", " + blogNameOrDomain;
+ }
+ } else if (post.hasAuthorName()) {
+ mByline = post.getAuthorName();
+ } else if (hasBlogNameOrDomain) {
+ mByline = blogNameOrDomain;
+ } else {
+ mByline = "";
+ }
+ }
+
+ public long getPostId() {
+ return mPostId;
+ }
+
+ public long getBlogId() {
+ return mBlogId;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getByline() {
+ return mByline;
+ }
+
+ public String getFeaturedImage() {
+ return mFeaturedImage;
+ }
+
+ public boolean hasFeaturedImage() {
+ return !TextUtils.isEmpty(mFeaturedImage);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java
new file mode 100644
index 000000000..94c1d5b5a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.ui.reader.models;
+
+import android.support.annotation.NonNull;
+
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+
+import java.util.ArrayList;
+
+public class ReaderRelatedPostList extends ArrayList<ReaderRelatedPost> {
+
+ public ReaderRelatedPostList(@NonNull ReaderPostList posts) {
+ for (ReaderPost post: posts) {
+ add(new ReaderRelatedPost(post));
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java
new file mode 100644
index 000000000..63e58564b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java
@@ -0,0 +1,206 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderDatabase;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderCommentList;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderCommentService extends Service {
+
+ private static final String ARG_POST_ID = "post_id";
+ private static final String ARG_BLOG_ID = "blog_id";
+ private static final String ARG_COMMENT_ID = "comment_id";
+ private static final String ARG_NEXT_PAGE = "next_page";
+
+ private static int mCurrentPage;
+
+ public static void startService(Context context, long blogId, long postId, boolean requestNextPage) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderCommentService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_POST_ID, postId);
+ intent.putExtra(ARG_NEXT_PAGE, requestNextPage);
+ context.startService(intent);
+ }
+
+ // Requests comments until the passed commentId is found
+ public static void startServiceForComment(Context context, long blogId, long postId, long commentId) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderCommentService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_POST_ID, postId);
+ intent.putExtra(ARG_COMMENT_ID, commentId);
+ context.startService(intent);
+ }
+
+ public static void stopService(Context context) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderCommentService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader comment service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader comment service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ EventBus.getDefault().post(new ReaderEvents.UpdateCommentsStarted());
+
+ final long blogId = intent.getLongExtra(ARG_BLOG_ID, 0);
+ final long postId = intent.getLongExtra(ARG_POST_ID, 0);
+ final long commentId = intent.getLongExtra(ARG_COMMENT_ID, 0);
+ boolean requestNextPage = intent.getBooleanExtra(ARG_NEXT_PAGE, false);
+
+ if (requestNextPage) {
+ int prevPage = ReaderCommentTable.getLastPageNumberForPost(blogId, postId);
+ mCurrentPage = prevPage + 1;
+ } else {
+ mCurrentPage = 1;
+ }
+
+ updateCommentsForPost(blogId, postId, mCurrentPage, new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ if (commentId > 0) {
+ if (ReaderCommentTable.commentExists(blogId, postId, commentId) || !result.isNewOrChanged()) {
+ EventBus.getDefault().post(new ReaderEvents.UpdateCommentsEnded(result));
+ stopSelf();
+ } else {
+ // Comment not found yet, request the next page
+ mCurrentPage++;
+ updateCommentsForPost(blogId, postId, mCurrentPage, this);
+ }
+ } else {
+ EventBus.getDefault().post(new ReaderEvents.UpdateCommentsEnded(result));
+ stopSelf();
+ }
+ }
+ });
+
+ return START_NOT_STICKY;
+ }
+
+ private static void updateCommentsForPost(final long blogId,
+ final long postId,
+ final int pageNumber,
+ final ReaderActions.UpdateResultListener resultListener) {
+ String path = "sites/" + blogId + "/posts/" + postId + "/replies/"
+ + "?number=" + Integer.toString(ReaderConstants.READER_MAX_COMMENTS_TO_REQUEST)
+ + "&meta=likes"
+ + "&hierarchical=true"
+ + "&order=ASC"
+ + "&page=" + pageNumber;
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateCommentsResponse(jsonObject, blogId, pageNumber, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED);
+ }
+ };
+ AppLog.d(AppLog.T.READER, "updating comments");
+ WordPress.getRestClientUtilsV1_1().get(path, null, null, listener, errorListener);
+ }
+ private static void handleUpdateCommentsResponse(final JSONObject jsonObject,
+ final long blogId,
+ final int pageNumber,
+ final ReaderActions.UpdateResultListener resultListener) {
+ if (jsonObject == null) {
+ resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED);
+ return;
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ final boolean hasNewComments;
+
+ ReaderDatabase.getWritableDb().beginTransaction();
+ try {
+ ReaderCommentList serverComments = new ReaderCommentList();
+ JSONArray jsonCommentList = jsonObject.optJSONArray("comments");
+ if (jsonCommentList != null) {
+ for (int i = 0; i < jsonCommentList.length(); i++) {
+ JSONObject jsonComment = jsonCommentList.optJSONObject(i);
+
+ // extract this comment and add it to the list
+ ReaderComment comment = ReaderComment.fromJson(jsonComment, blogId);
+ comment.pageNumber = pageNumber;
+ serverComments.add(comment);
+
+ // extract and save likes for this comment
+ JSONObject jsonLikes = JSONUtils.getJSONChild(jsonComment, "meta/data/likes");
+ if (jsonLikes != null) {
+ ReaderUserList likingUsers = ReaderUserList.fromJsonLikes(jsonLikes);
+ ReaderUserTable.addOrUpdateUsers(likingUsers);
+ ReaderLikeTable.setLikesForComment(comment, likingUsers.getUserIds());
+ }
+ }
+ }
+
+ hasNewComments = (serverComments.size() > 0);
+
+ // save to db regardless of whether any are new so changes to likes are stored
+ ReaderCommentTable.addOrUpdateComments(serverComments);
+ ReaderDatabase.getWritableDb().setTransactionSuccessful();
+ } finally {
+ ReaderDatabase.getWritableDb().endTransaction();
+ }
+
+ ReaderActions.UpdateResult result =
+ (hasNewComments ? ReaderActions.UpdateResult.HAS_NEW : ReaderActions.UpdateResult.UNCHANGED);
+ resultListener.onUpdateResult(result);
+ }
+ }.start();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java
new file mode 100644
index 000000000..a0cf613bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java
@@ -0,0 +1,391 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which updates posts with specific tags or in specific blogs/feeds - relies on
+ * EventBus to alert of update status
+ */
+
+public class ReaderPostService extends Service {
+
+ private static final String ARG_TAG = "tag";
+ private static final String ARG_ACTION = "action";
+ private static final String ARG_BLOG_ID = "blog_id";
+ private static final String ARG_FEED_ID = "feed_id";
+
+ public enum UpdateAction {
+ REQUEST_NEWER, // request the newest posts for this tag/blog/feed
+ REQUEST_OLDER, // request posts older than the oldest existing one for this tag/blog/feed
+ REQUEST_OLDER_THAN_GAP // request posts older than the one with the gap marker for this tag (not supported for blog/feed)
+ }
+
+ /*
+ * update posts with the passed tag
+ */
+ public static void startServiceForTag(Context context, ReaderTag tag, UpdateAction action) {
+ Intent intent = new Intent(context, ReaderPostService.class);
+ intent.putExtra(ARG_TAG, tag);
+ intent.putExtra(ARG_ACTION, action);
+ context.startService(intent);
+ }
+
+ /*
+ * update posts in the passed blog
+ */
+ public static void startServiceForBlog(Context context, long blogId, UpdateAction action) {
+ Intent intent = new Intent(context, ReaderPostService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_ACTION, action);
+ context.startService(intent);
+ }
+
+ /*
+ * update posts in the passed feed
+ */
+ public static void startServiceForFeed(Context context, long feedId, UpdateAction action) {
+ Intent intent = new Intent(context, ReaderPostService.class);
+ intent.putExtra(ARG_FEED_ID, feedId);
+ intent.putExtra(ARG_ACTION, action);
+ context.startService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader post service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader post service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ UpdateAction action;
+ if (intent.hasExtra(ARG_ACTION)) {
+ action = (UpdateAction) intent.getSerializableExtra(ARG_ACTION);
+ } else {
+ action = UpdateAction.REQUEST_NEWER;
+ }
+
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsStarted(action));
+
+ if (intent.hasExtra(ARG_TAG)) {
+ ReaderTag tag = (ReaderTag) intent.getSerializableExtra(ARG_TAG);
+ updatePostsWithTag(tag, action);
+ } else if (intent.hasExtra(ARG_BLOG_ID)) {
+ long blogId = intent.getLongExtra(ARG_BLOG_ID, 0);
+ updatePostsInBlog(blogId, action);
+ } else if (intent.hasExtra(ARG_FEED_ID)) {
+ long feedId = intent.getLongExtra(ARG_FEED_ID, 0);
+ updatePostsInFeed(feedId, action);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void updatePostsWithTag(final ReaderTag tag, final UpdateAction action) {
+ requestPostsWithTag(
+ tag,
+ action,
+ new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsEnded(tag, result, action));
+ stopSelf();
+ }
+ });
+ }
+
+ private void updatePostsInBlog(long blogId, final UpdateAction action) {
+ UpdateResultListener listener = new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsEnded(result, action));
+ stopSelf();
+ }
+ };
+ requestPostsForBlog(blogId, action, listener);
+ }
+
+ private void updatePostsInFeed(long feedId, final UpdateAction action) {
+ UpdateResultListener listener = new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsEnded(result, action));
+ stopSelf();
+ }
+ };
+ requestPostsForFeed(feedId, action, listener);
+ }
+
+ private static void requestPostsWithTag(final ReaderTag tag,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ String path = getRelativeEndpointForTag(tag);
+ if (TextUtils.isEmpty(path)) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder(path);
+
+ // append #posts to retrieve
+ sb.append("?number=").append(ReaderConstants.READER_MAX_POSTS_TO_REQUEST);
+
+ // return newest posts first (this is the default, but make it explicit since it's important)
+ sb.append("&order=DESC");
+
+ String beforeDate;
+ switch (updateAction) {
+ case REQUEST_OLDER:
+ // request posts older than the oldest existing post with this tag
+ beforeDate = ReaderPostTable.getOldestDateWithTag(tag);
+ break;
+ case REQUEST_OLDER_THAN_GAP:
+ // request posts older than the post with the gap marker for this tag
+ beforeDate = ReaderPostTable.getGapMarkerDateForTag(tag);
+ break;
+ default:
+ beforeDate = null;
+ break;
+ }
+ if (!TextUtils.isEmpty(beforeDate)) {
+ sb.append("&before=").append(UrlUtils.urlEncode(beforeDate));
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ // remember when this tag was updated if newer posts were requested
+ if (updateAction == UpdateAction.REQUEST_NEWER) {
+ ReaderTagTable.setTagLastUpdated(tag);
+ }
+ handleUpdatePostsResponse(tag, jsonObject, updateAction, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_2().get(sb.toString(), null, null, listener, errorListener);
+ }
+
+ private static void requestPostsForBlog(final long blogId,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ String path = "read/sites/" + blogId + "/posts/?meta=site,likes";
+
+ // append the date of the oldest cached post in this blog when requesting older posts
+ if (updateAction == UpdateAction.REQUEST_OLDER) {
+ String dateOldest = ReaderPostTable.getOldestDateInBlog(blogId);
+ if (!TextUtils.isEmpty(dateOldest)) {
+ path += "&before=" + UrlUtils.urlEncode(dateOldest);
+ }
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ };
+ AppLog.d(AppLog.T.READER, "updating posts in blog " + blogId);
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void requestPostsForFeed(final long feedId,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ String path = "read/feed/" + feedId + "/posts/?meta=site,likes";
+ if (updateAction == UpdateAction.REQUEST_OLDER) {
+ String dateOldest = ReaderPostTable.getOldestDateInFeed(feedId);
+ if (!TextUtils.isEmpty(dateOldest)) {
+ path += "&before=" + UrlUtils.urlEncode(dateOldest);
+ }
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "updating posts in feed " + feedId);
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ /*
+ * called after requesting posts with a specific tag or in a specific blog/feed
+ */
+ private static void handleUpdatePostsResponse(final ReaderTag tag,
+ final JSONObject jsonObject,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ if (jsonObject == null) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ return;
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPostList serverPosts = ReaderPostList.fromJson(jsonObject);
+ UpdateResult updateResult = ReaderPostTable.comparePosts(serverPosts);
+ if (updateResult.isNewOrChanged()) {
+ // gap detection - only applies to posts with a specific tag
+ ReaderPost postWithGap = null;
+ if (tag != null) {
+ switch (updateAction) {
+ case REQUEST_NEWER:
+ // if there's no overlap between server and local (ie: all server
+ // posts are new), assume there's a gap between server and local
+ // provided that local posts exist
+ int numServerPosts = serverPosts.size();
+ if (numServerPosts >= 2
+ && ReaderPostTable.getNumPostsWithTag(tag) > 0
+ && !ReaderPostTable.hasOverlap(serverPosts)) {
+ // treat the second to last server post as having a gap
+ postWithGap = serverPosts.get(numServerPosts - 2);
+ // remove the last server post to deal with the edge case of
+ // there actually not being a gap between local & server
+ serverPosts.remove(numServerPosts - 1);
+ AppLog.d(AppLog.T.READER, "added gap marker to tag " + tag.getTagNameForLog());
+ }
+ ReaderPostTable.removeGapMarkerForTag(tag);
+ break;
+ case REQUEST_OLDER_THAN_GAP:
+ // if service was started as a request to fill a gap, delete existing posts
+ // before the one with the gap marker, then remove the existing gap marker
+ ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag);
+ ReaderPostTable.removeGapMarkerForTag(tag);
+ break;
+ }
+ }
+
+ ReaderPostTable.addOrUpdatePosts(tag, serverPosts);
+
+ // gap marker must be set after saving server posts
+ if (postWithGap != null) {
+ ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, tag);
+ }
+ } else if (updateResult == UpdateResult.UNCHANGED && updateAction == UpdateAction.REQUEST_OLDER_THAN_GAP) {
+ // edge case - request to fill gap returned nothing new, so remove the gap marker
+ ReaderPostTable.removeGapMarkerForTag(tag);
+ AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new");
+ }
+ AppLog.d(AppLog.T.READER, "requested posts response = " + updateResult.toString());
+ resultListener.onUpdateResult(updateResult);
+ }
+ }.start();
+ }
+
+ /*
+ * returns the endpoint to use when requesting posts with the passed tag
+ */
+ private static String getRelativeEndpointForTag(ReaderTag tag) {
+ if (tag == null) {
+ return null;
+ }
+
+ // if passed tag has an assigned endpoint, return it and be done
+ if (!TextUtils.isEmpty(tag.getEndpoint())) {
+ return getRelativeEndpoint(tag.getEndpoint());
+ }
+
+ // check the db for the endpoint
+ String endpoint = ReaderTagTable.getEndpointForTag(tag);
+ if (!TextUtils.isEmpty(endpoint)) {
+ return getRelativeEndpoint(endpoint);
+ }
+
+ // never hand craft the endpoint for default tags, since these MUST be updated
+ // using their stored endpoints
+ if (tag.tagType == ReaderTagType.DEFAULT) {
+ return null;
+ }
+
+ return String.format("read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tag.getTagSlug()));
+ }
+
+ /*
+ * returns the passed endpoint without the unnecessary path - this is
+ * needed because as of 20-Feb-2015 the /read/menu/ call returns the
+ * full path but we don't want to use the full path since it may change
+ * between API versions (as it did when we moved from v1 to v1.1)
+ *
+ * ex: https://public-api.wordpress.com/rest/v1/read/tags/fitness/posts
+ * becomes just read/tags/fitness/posts
+ */
+ private static String getRelativeEndpoint(final String endpoint) {
+ if (endpoint != null && endpoint.startsWith("http")) {
+ int pos = endpoint.indexOf("/read/");
+ if (pos > -1) {
+ return endpoint.substring(pos + 1, endpoint.length());
+ }
+ pos = endpoint.indexOf("/v1/");
+ if (pos > -1) {
+ return endpoint.substring(pos + 4, endpoint.length());
+ }
+ }
+ return StringUtils.notNullStr(endpoint);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java
new file mode 100644
index 000000000..457a7b69d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.annotation.NonNull;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which searches for reader posts on wordpress.com
+ */
+
+public class ReaderSearchService extends Service {
+
+ private static final String ARG_QUERY = "query";
+ private static final String ARG_OFFSET = "offset";
+
+ public static void startService(Context context, @NonNull String query, int offset) {
+ Intent intent = new Intent(context, ReaderSearchService.class);
+ intent.putExtra(ARG_QUERY, query);
+ intent.putExtra(ARG_OFFSET, offset);
+ context.startService(intent);
+ }
+
+ public static void stopService(Context context) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderSearchService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader search service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader search service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ String query = intent.getStringExtra(ARG_QUERY);
+ int offset = intent.getIntExtra(ARG_OFFSET, 0);
+ startSearch(query, offset);
+
+ return START_NOT_STICKY;
+ }
+
+ private void startSearch(final String query, final int offset) {
+ String path = "read/search?q="
+ + UrlUtils.urlEncode(query)
+ + "&number=" + ReaderConstants.READER_MAX_SEARCH_POSTS_TO_REQUEST
+ + "&offset=" + offset
+ + "&meta=site,likes";
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ handleSearchResponse(query, offset, jsonObject);
+ } else {
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, false));
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, false));
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "reader search service > starting search for " + query);
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsStarted(query, offset));
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void handleSearchResponse(final String query, final int offset, final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPostList serverPosts = ReaderPostList.fromJson(jsonObject);
+
+ // we want search results to be sorted based on their offset - this works because
+ // ReaderPostTable.getPostsWithTag() sorts by sort_index in descending order
+ int sortIndex = -offset - 1;
+ for (ReaderPost post: serverPosts) {
+ post.sortIndex = sortIndex;
+ sortIndex--;
+ }
+
+ ReaderPostTable.addOrUpdatePosts(getTagForSearchQuery(query), serverPosts);
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, true));
+ }
+ }.start();
+ }
+
+ /*
+ * used when storing search results in the reader post table
+ */
+ public static ReaderTag getTagForSearchQuery(@NonNull String query) {
+ String trimQuery = query != null ? query.trim() : "";
+ String slug = ReaderUtils.sanitizeWithDashes(trimQuery);
+ return new ReaderTag(slug, trimQuery, trimQuery, null, ReaderTagType.SEARCH);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java
new file mode 100644
index 000000000..f3590ac78
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java
@@ -0,0 +1,331 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.IBinder;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.datasets.ReaderDatabase;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderBlogList;
+import org.wordpress.android.models.ReaderRecommendBlogList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.EnumSet;
+import java.util.Iterator;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderUpdateService extends Service {
+
+ /***
+ * service which updates followed/recommended tags and blogs for the Reader, relies
+ * on EventBus to notify of changes
+ */
+
+ public enum UpdateTask {
+ TAGS,
+ FOLLOWED_BLOGS,
+ RECOMMENDED_BLOGS
+ }
+
+ private EnumSet<UpdateTask> mCurrentTasks;
+ private static final String ARG_UPDATE_TASKS = "update_tasks";
+
+ public static void startService(Context context, EnumSet<UpdateTask> tasks) {
+ if (context == null || tasks == null || tasks.size() == 0) {
+ return;
+ }
+ Intent intent = new Intent(context, ReaderUpdateService.class);
+ intent.putExtra(ARG_UPDATE_TASKS, tasks);
+ context.startService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.hasExtra(ARG_UPDATE_TASKS)) {
+ //noinspection unchecked
+ EnumSet<UpdateTask> tasks = (EnumSet<UpdateTask>) intent.getSerializableExtra(ARG_UPDATE_TASKS);
+ performTasks(tasks);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void performTasks(EnumSet<UpdateTask> tasks) {
+ mCurrentTasks = EnumSet.copyOf(tasks);
+
+ // perform in priority order - we want to update tags first since without them
+ // the Reader can't show anything
+ if (tasks.contains(UpdateTask.TAGS)) {
+ updateTags();
+ }
+ if (tasks.contains(UpdateTask.FOLLOWED_BLOGS)) {
+ updateFollowedBlogs();
+ }
+ if (tasks.contains(UpdateTask.RECOMMENDED_BLOGS)) {
+ updateRecommendedBlogs();
+ }
+ }
+
+ private void taskCompleted(UpdateTask task) {
+ mCurrentTasks.remove(task);
+ if (mCurrentTasks.isEmpty()) {
+ allTasksCompleted();
+ }
+ }
+
+ private void allTasksCompleted() {
+ AppLog.i(AppLog.T.READER, "reader service > all tasks completed");
+ stopSelf();
+ }
+
+ /***
+ * update the tags the user is followed - also handles recommended (popular) tags since
+ * they're included in the response
+ */
+ private void updateTags() {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateTagsResponse(jsonObject);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ taskCompleted(UpdateTask.TAGS);
+ }
+ };
+ AppLog.d(AppLog.T.READER, "reader service > updating tags");
+ WordPress.getRestClientUtilsV1_2().get("read/menu", null, null, listener, errorListener);
+ }
+
+ private void handleUpdateTagsResponse(final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ // get server topics, both default & followed - but use "recommended" for logged-out
+ // reader since user won't have any followed tags
+ ReaderTagList serverTopics = new ReaderTagList();
+ serverTopics.addAll(parseTags(jsonObject, "default", ReaderTagType.DEFAULT));
+ if (ReaderUtils.isLoggedOutReader()) {
+ serverTopics.addAll(parseTags(jsonObject, "recommended", ReaderTagType.FOLLOWED));
+ } else {
+ serverTopics.addAll(parseTags(jsonObject, "subscribed", ReaderTagType.FOLLOWED));
+ }
+
+ // parse topics from the response, detect whether they're different from local
+ ReaderTagList localTopics = new ReaderTagList();
+ localTopics.addAll(ReaderTagTable.getDefaultTags());
+ localTopics.addAll(ReaderTagTable.getFollowedTags());
+ localTopics.addAll(ReaderTagTable.getCustomListTags());
+
+ if (!localTopics.isSameList(serverTopics)) {
+ AppLog.d(AppLog.T.READER, "reader service > followed topics changed");
+ // if any local topics have been removed from the server, make sure to delete
+ // them locally (including their posts)
+ deleteTags(localTopics.getDeletions(serverTopics));
+ // now replace local topics with the server topics
+ ReaderTagTable.replaceTags(serverTopics);
+ // broadcast the fact that there are changes
+ EventBus.getDefault().post(new ReaderEvents.FollowedTagsChanged());
+ }
+
+ // save changes to recommended topics
+ if (!ReaderUtils.isLoggedOutReader()) {
+ ReaderTagList serverRecommended = parseTags(jsonObject, "recommended", ReaderTagType.RECOMMENDED);
+ ReaderTagList localRecommended = ReaderTagTable.getRecommendedTags(false);
+ if (!serverRecommended.isSameList(localRecommended)) {
+ AppLog.d(AppLog.T.READER, "reader service > recommended topics changed");
+ ReaderTagTable.setRecommendedTags(serverRecommended);
+ EventBus.getDefault().post(new ReaderEvents.RecommendedTagsChanged());
+ }
+ }
+
+ taskCompleted(UpdateTask.TAGS);
+ }
+ }.start();
+ }
+
+ /*
+ * parse a specific topic section from the topic response
+ */
+ private static ReaderTagList parseTags(JSONObject jsonObject, String name, ReaderTagType tagType) {
+ ReaderTagList topics = new ReaderTagList();
+
+ if (jsonObject == null) {
+ return topics;
+ }
+
+ JSONObject jsonTopics = jsonObject.optJSONObject(name);
+ if (jsonTopics == null) {
+ return topics;
+ }
+
+ Iterator<String> it = jsonTopics.keys();
+ while (it.hasNext()) {
+ String internalName = it.next();
+ JSONObject jsonTopic = jsonTopics.optJSONObject(internalName);
+ if (jsonTopic != null) {
+ String tagTitle = JSONUtils.getStringDecoded(jsonTopic, ReaderConstants.JSON_TAG_TITLE);
+ String tagDisplayName = JSONUtils.getStringDecoded(jsonTopic, ReaderConstants.JSON_TAG_DISPLAY_NAME);
+ String tagSlug = JSONUtils.getStringDecoded(jsonTopic, ReaderConstants.JSON_TAG_SLUG);
+ String endpoint = JSONUtils.getString(jsonTopic, ReaderConstants.JSON_TAG_URL);
+
+ // if the endpoint contains `read/list` then this is a custom list - these are
+ // included in the response as default tags
+ if (tagType == ReaderTagType.DEFAULT && endpoint.contains("/read/list/")) {
+ topics.add(new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, ReaderTagType.CUSTOM_LIST));
+ } else {
+ topics.add(new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, tagType));
+ }
+ }
+ }
+
+ return topics;
+ }
+
+ private static void deleteTags(ReaderTagList tagList) {
+ if (tagList == null || tagList.size() == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ try {
+ for (ReaderTag tag: tagList) {
+ ReaderTagTable.deleteTag(tag);
+ ReaderPostTable.deletePostsWithTag(tag);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ /***
+ * request the list of blogs the current user is following
+ */
+ private void updateFollowedBlogs() {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleFollowedBlogsResponse(jsonObject);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ taskCompleted(UpdateTask.FOLLOWED_BLOGS);
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "reader service > updating followed blogs");
+ // request using ?meta=site,feed to get extra info
+ WordPress.getRestClientUtilsV1_1().get("read/following/mine?meta=site%2Cfeed", listener, errorListener);
+ }
+
+ private void handleFollowedBlogsResponse(final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderBlogList serverBlogs = ReaderBlogList.fromJson(jsonObject);
+ ReaderBlogList localBlogs = ReaderBlogTable.getFollowedBlogs();
+
+ if (!localBlogs.isSameList(serverBlogs)) {
+ // always update the list of followed blogs if there are *any* changes between
+ // server and local (including subscription count, description, etc.)
+ ReaderBlogTable.setFollowedBlogs(serverBlogs);
+ // ...but only update the follow status and alert that followed blogs have
+ // changed if the server list doesn't have the same blogs as the local list
+ // (ie: a blog has been followed/unfollowed since local was last updated)
+ if (!localBlogs.hasSameBlogs(serverBlogs)) {
+ ReaderPostTable.updateFollowedStatus();
+ AppLog.i(AppLog.T.READER, "reader blogs service > followed blogs changed");
+ EventBus.getDefault().post(new ReaderEvents.FollowedBlogsChanged());
+ }
+ }
+
+ taskCompleted(UpdateTask.FOLLOWED_BLOGS);
+ }
+ }.start();
+ }
+
+ /***
+ * request the latest recommended blogs, replaces all local ones
+ */
+ private void updateRecommendedBlogs() {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleRecommendedBlogsResponse(jsonObject);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ taskCompleted(UpdateTask.RECOMMENDED_BLOGS);
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "reader service > updating recommended blogs");
+ String path = "read/recommendations/mine/"
+ + "?source=mobile"
+ + "&number=" + Integer.toString(ReaderConstants.READER_MAX_RECOMMENDED_TO_REQUEST);
+ WordPress.getRestClientUtilsV1_1().get(path, listener, errorListener);
+ }
+ private void handleRecommendedBlogsResponse(final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderRecommendBlogList serverBlogs = ReaderRecommendBlogList.fromJson(jsonObject);
+ ReaderRecommendBlogList localBlogs = ReaderBlogTable.getRecommendedBlogs();
+
+ if (!localBlogs.isSameList(serverBlogs)) {
+ ReaderBlogTable.setRecommendedBlogs(serverBlogs);
+ EventBus.getDefault().post(new ReaderEvents.RecommendedBlogsChanged());
+ }
+
+ taskCompleted(UpdateTask.RECOMMENDED_BLOGS);
+ }
+ }.start();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java
new file mode 100644
index 000000000..fdb883bb1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java
@@ -0,0 +1,86 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * hash map of sizes of attachments in a reader post - created from the json "attachments" section
+ * of the post endpoints
+ */
+public class ImageSizeMap extends HashMap<String, ImageSizeMap.ImageSize> {
+ private static final String EMPTY_JSON = "{}";
+ public ImageSizeMap(String jsonString) {
+ if (TextUtils.isEmpty(jsonString) || jsonString.equals(EMPTY_JSON)) {
+ return;
+ }
+
+ try {
+ JSONObject json = new JSONObject(jsonString);
+ Iterator<String> it = json.keys();
+ if (!it.hasNext()) {
+ return;
+ }
+
+ while (it.hasNext()) {
+ JSONObject jsonAttach = json.optJSONObject(it.next());
+ if (jsonAttach != null && JSONUtils.getString(jsonAttach, "mime_type").startsWith("image")) {
+ String normUrl = UrlUtils.normalizeUrl(UrlUtils.removeQuery(JSONUtils.getString(jsonAttach, "URL")));
+ int width = jsonAttach.optInt("width");
+ int height = jsonAttach.optInt("height");
+
+ // chech if data-orig-size is present and use it
+ String originalSize = jsonAttach.optString("data-orig-size", null);
+ if (originalSize != null) {
+ String[] sizes = originalSize.split(",");
+ if (sizes != null && sizes.length == 2) {
+ width = Integer.parseInt(sizes[0]);
+ height = Integer.parseInt(sizes[1]);
+ }
+ }
+
+ this.put(normUrl, new ImageSize(width, height));
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+
+ public ImageSize getImageSize(final String imageUrl) {
+ if (imageUrl == null) {
+ return null;
+ } else {
+ return super.get(UrlUtils.normalizeUrl(UrlUtils.removeQuery(imageUrl)));
+ }
+ }
+
+ public String getLargestImageUrl(int minImageWidth) {
+ String currentImageUrl = null;
+ int currentMaxWidth = minImageWidth;
+ for (Entry<String, ImageSize> attach: this.entrySet()) {
+ if (attach.getValue().width > currentMaxWidth) {
+ currentImageUrl = attach.getKey();
+ currentMaxWidth = attach.getValue().width;
+ }
+ }
+
+ return currentImageUrl;
+ }
+
+ public static class ImageSize {
+ public final int width;
+ public final int height;
+ public ImageSize(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java
new file mode 100644
index 000000000..7f980b490
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java
@@ -0,0 +1,131 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.net.Uri;
+
+import org.wordpress.android.util.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReaderHtmlUtils {
+
+ public interface HtmlScannerListener {
+ void onTagFound(String tag, String src);
+ }
+
+ // regex for matching oriwidth attributes in tags
+ private static final Pattern ORIGINAL_WIDTH_ATTR_PATTERN = Pattern.compile(
+ "data-orig-size\\s*=\\s*(?:'|\")(.*?),.*?(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern ORIGINAL_HEIGHT_ATTR_PATTERN = Pattern.compile(
+ "data-orig-size\\s*=\\s*(?:'|\").*?,(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ // regex for matching width attributes in tags
+ private static final Pattern WIDTH_ATTR_PATTERN = Pattern.compile(
+ "width\\s*=\\s*(?:'|\")(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ // regex for matching height attributes in tags
+ private static final Pattern HEIGHT_ATTR_PATTERN = Pattern.compile(
+ "height\\s*=\\s*(?:'|\")(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ // regex for matching src attributes in tags
+ private static final Pattern SRC_ATTR_PATTERN = Pattern.compile(
+ "src\\s*=\\s*(?:'|\")(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+
+ /*
+ * returns the integer value from the data-orig-size attribute in the passed html tag
+ */
+ public static int getOriginalWidthAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ Matcher matcher = ORIGINAL_WIDTH_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ return StringUtils.stringToInt(matcher.group(1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ public static int getOriginalHeightAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ Matcher matcher = ORIGINAL_HEIGHT_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ return StringUtils.stringToInt(matcher.group(1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ /*
+ * returns the integer value from the width attribute in the passed html tag
+ */
+ public static int getWidthAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ Matcher matcher = WIDTH_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ // remove "width=" and quotes from the result
+ return StringUtils.stringToInt(tag.substring(matcher.start() + 7, matcher.end() - 1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ public static int getHeightAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+ Matcher matcher = HEIGHT_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ return StringUtils.stringToInt(tag.substring(matcher.start() + 8, matcher.end() - 1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ /*
+ * returns the value from the src attribute in the passed html tag
+ */
+ public static String getSrcAttrValue(final String tag) {
+ if (tag == null) {
+ return null;
+ }
+
+ Matcher matcher = SRC_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ // remove "src=" and quotes from the result
+ return tag.substring(matcher.start() + 5, matcher.end() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * returns the integer value of the passed query param in the passed url - returns zero
+ * if the url is invalid, or the param doesn't exist, or the param value could not be
+ * converted to an int
+ */
+ public static int getIntQueryParam(final String url,
+ @SuppressWarnings("SameParameterValue") final String param) {
+ if (url == null
+ || param == null
+ || !url.startsWith("http")
+ || !url.contains(param + "=")) {
+ return 0;
+ }
+ return StringUtils.stringToInt(Uri.parse(url).getQueryParameter(param));
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java
new file mode 100644
index 000000000..4b9eb93fb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java
@@ -0,0 +1,34 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.text.TextUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReaderIframeScanner {
+
+ private final String mContent;
+
+ private static final Pattern IFRAME_TAG_PATTERN = Pattern.compile(
+ "<iframe(\\s+.*?)(?:src\\s*=\\s*(?:'|\")(.*?)(?:'|\"))(.*?)>",
+ Pattern.DOTALL| Pattern.CASE_INSENSITIVE);
+
+ public ReaderIframeScanner(String contentOfPost) {
+ mContent = contentOfPost;
+ }
+
+ public void beginScan(ReaderHtmlUtils.HtmlScannerListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("HtmlScannerListener is required");
+ }
+
+ Matcher matcher = IFRAME_TAG_PATTERN.matcher(mContent);
+ while (matcher.find()) {
+ String tag = mContent.substring(matcher.start(), matcher.end());
+ String src = ReaderHtmlUtils.getSrcAttrValue(tag);
+ if (!TextUtils.isEmpty(src)) {
+ listener.onTagFound(tag, src);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java
new file mode 100644
index 000000000..d1708302f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java
@@ -0,0 +1,117 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.text.TextUtils;
+
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.models.ReaderImageList;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReaderImageScanner {
+ private final String mContent;
+ private final boolean mIsPrivate;
+ private final boolean mContentContainsImages;
+
+ private static final Pattern IMG_TAG_PATTERN = Pattern.compile(
+ "<img(\\s+.*?)(?:src\\s*=\\s*(?:'|\")(.*?)(?:'|\"))(.*?)>",
+ Pattern.DOTALL| Pattern.CASE_INSENSITIVE);
+
+ public ReaderImageScanner(String contentOfPost, boolean isPrivate) {
+ mContent = contentOfPost;
+ mIsPrivate = isPrivate;
+ mContentContainsImages = mContent != null && mContent.contains("<img");
+ }
+
+ /*
+ * start scanning the content for images and notify the passed listener about each one
+ */
+ public void beginScan(ReaderHtmlUtils.HtmlScannerListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("HtmlScannerListener is required");
+ }
+
+ if (!mContentContainsImages) {
+ return;
+ }
+
+ Matcher imgMatcher = IMG_TAG_PATTERN.matcher(mContent);
+ while (imgMatcher.find()) {
+ String imageTag = mContent.substring(imgMatcher.start(), imgMatcher.end());
+ String imageUrl = ReaderHtmlUtils.getSrcAttrValue(imageTag);
+ if (!TextUtils.isEmpty(imageUrl)) {
+ listener.onTagFound(imageTag, imageUrl);
+ }
+ }
+ }
+
+ /*
+ * returns a list of all image URLs in the content above a certain width - pass zero
+ * for the min to include all images regardless of size
+ */
+ public ReaderImageList getImageList() {
+ return getImageList(0);
+ }
+ public ReaderImageList getGalleryImageList() {
+ return getImageList(ReaderConstants.MIN_GALLERY_IMAGE_WIDTH);
+ }
+ public ReaderImageList getImageList(int minImageWidth) {
+ ReaderImageList imageList = new ReaderImageList(mIsPrivate);
+
+ if (!mContentContainsImages) {
+ return imageList;
+ }
+
+ Matcher imgMatcher = IMG_TAG_PATTERN.matcher(mContent);
+ while (imgMatcher.find()) {
+ String imgTag = mContent.substring(imgMatcher.start(), imgMatcher.end());
+ String imageUrl = ReaderHtmlUtils.getSrcAttrValue(imgTag);
+
+ if (minImageWidth == 0) {
+ imageList.addImageUrl(imageUrl);
+ } else {
+ int width = Math.max(ReaderHtmlUtils.getWidthAttrValue(imgTag), ReaderHtmlUtils.getIntQueryParam(imageUrl, "w"));
+ if (width >= minImageWidth) {
+ imageList.addImageUrl(imageUrl);
+ }
+ }
+ }
+
+ return imageList;
+ }
+
+ /*
+ * used when a post doesn't have a featured image assigned, searches post's content
+ * for an image that may be large enough to be suitable as a featured image
+ */
+ public String getLargestImage(int minImageWidth) {
+ if (!mContentContainsImages) {
+ return null;
+ }
+
+ String currentImageUrl = null;
+ int currentMaxWidth = minImageWidth;
+
+ Matcher imgMatcher = IMG_TAG_PATTERN.matcher(mContent);
+ while (imgMatcher.find()) {
+ String imgTag = mContent.substring(imgMatcher.start(), imgMatcher.end());
+ String imageUrl = ReaderHtmlUtils.getSrcAttrValue(imgTag);
+
+ int width = Math.max(ReaderHtmlUtils.getWidthAttrValue(imgTag), ReaderHtmlUtils.getIntQueryParam(imageUrl, "w"));
+ if (width > currentMaxWidth) {
+ currentImageUrl = imageUrl;
+ currentMaxWidth = width;
+ }
+ }
+
+ return currentImageUrl;
+ }
+
+ /*
+ * same as above, but doesn't enforce the max width - will return the first image found if
+ * no images have their width set
+ */
+ public String getLargestImage() {
+ return getLargestImage(-1);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java
new file mode 100644
index 000000000..f6dd7659c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java
@@ -0,0 +1,103 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.content.ActivityNotFoundException;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ImageSpan;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.EnumSet;
+
+/*
+ * custom LinkMovementMethod which shows photo viewer when an image span is tapped
+ */
+public class ReaderLinkMovementMethod extends LinkMovementMethod {
+ private static ReaderLinkMovementMethod mMovementMethod;
+ private static ReaderLinkMovementMethod mMovementMethodPrivate;
+
+ private final boolean mIsPrivate;
+
+ /*
+ * note that separate instances are returned depending on whether we're showing
+ * content from a private blog
+ */
+ public static ReaderLinkMovementMethod getInstance(boolean isPrivate) {
+ if (isPrivate) {
+ if (mMovementMethodPrivate == null) {
+ mMovementMethodPrivate = new ReaderLinkMovementMethod(true);
+ }
+ return mMovementMethodPrivate;
+ } else {
+ if (mMovementMethod == null) {
+ mMovementMethod = new ReaderLinkMovementMethod(false);
+ }
+ return mMovementMethod;
+ }
+ }
+
+ /*
+ * override MovementMethod.getInstance() to ensure our getInstance(false) is used
+ */
+ @SuppressWarnings("unused")
+ public static ReaderLinkMovementMethod getInstance() {
+ return getInstance(false);
+ }
+
+ private ReaderLinkMovementMethod(boolean isPrivate) {
+ super();
+ mIsPrivate = isPrivate;
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull TextView textView,
+ @NonNull Spannable buffer,
+ @NonNull MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= textView.getTotalPaddingLeft();
+ y -= textView.getTotalPaddingTop();
+
+ x += textView.getScrollX();
+ y += textView.getScrollY();
+
+ Layout layout = textView.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ImageSpan[] images = buffer.getSpans(off, off, ImageSpan.class);
+ if (images != null && images.length > 0) {
+ EnumSet<PhotoViewerOption> options = EnumSet.noneOf(PhotoViewerOption.class);
+ if (mIsPrivate) {
+ options.add(ReaderActivityLauncher.PhotoViewerOption.IS_PRIVATE_IMAGE);
+ }
+ String imageUrl = StringUtils.notNullStr(images[0].getSource());
+ ReaderActivityLauncher.showReaderPhotoViewer(
+ textView.getContext(),
+ imageUrl,
+ null,
+ textView,
+ options,
+ (int) event.getX(),
+ (int) event.getY());
+ return true;
+ }
+ }
+
+ try {
+ return super.onTouchEvent(textView, buffer, event);
+ } catch (ActivityNotFoundException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java
new file mode 100644
index 000000000..bdb79a907
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java
@@ -0,0 +1,221 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+public class ReaderUtils {
+
+ public static String getResizedImageUrl(final String imageUrl, int width, int height, boolean isPrivate) {
+ return getResizedImageUrl(imageUrl, width, height, isPrivate, PhotonUtils.Quality.MEDIUM);
+ }
+ public static String getResizedImageUrl(final String imageUrl,
+ int width,
+ int height,
+ boolean isPrivate,
+ PhotonUtils.Quality quality) {
+
+ final String unescapedUrl = HtmlUtils.fastUnescapeHtml(imageUrl);
+ if (isPrivate) {
+ return getPrivateImageForDisplay(unescapedUrl, width, height);
+ } else {
+ return PhotonUtils.getPhotonImageUrl(unescapedUrl, width, height, quality);
+ }
+ }
+
+ /*
+ * use this to request a reduced size image from a private post - images in private posts can't
+ * use photon but these are usually wp images so they support the h= and w= query params
+ */
+ private static String getPrivateImageForDisplay(final String imageUrl, int width, int height) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ final String query;
+ if (width > 0 && height > 0) {
+ query = "?w=" + width + "&h=" + height;
+ } else if (width > 0) {
+ query = "?w=" + width;
+ } else if (height > 0) {
+ query = "?h=" + height;
+ } else {
+ query = "";
+ }
+ // remove the existing query string, add the new one, and make sure the url is https:
+ return UrlUtils.removeQuery(UrlUtils.makeHttps(imageUrl)) + query;
+ }
+
+ /*
+ * returns the passed string formatted for use with our API - see sanitize_title_with_dashes
+ * https://github.com/WordPress/WordPress/blob/master/wp-includes/formatting.php#L1258
+ * http://stackoverflow.com/a/1612015/1673548
+ */
+ public static String sanitizeWithDashes(final String title) {
+ if (title == null) {
+ return "";
+ }
+
+ return title.trim()
+ .replaceAll("&[^\\s]*;", "") // remove html entities
+ .replaceAll("[\\.\\s]+", "-") // replace periods and whitespace with a dash
+ .replaceAll("[^\\p{L}\\p{Nd}\\-]+", "") // remove remaining non-alphanum/non-dash chars (Unicode aware)
+ .replaceAll("--", "-"); // reduce double dashes potentially added above
+ }
+
+ /*
+ * returns the long text to use for a like label ("Liked by 3 people", etc.)
+ */
+ public static String getLongLikeLabelText(Context context, int numLikes, boolean isLikedByCurrentUser) {
+ if (isLikedByCurrentUser) {
+ switch (numLikes) {
+ case 1:
+ return context.getString(R.string.reader_likes_only_you);
+ case 2:
+ return context.getString(R.string.reader_likes_you_and_one);
+ default:
+ String youAndMultiLikes = context.getString(R.string.reader_likes_you_and_multi);
+ return String.format(youAndMultiLikes, numLikes - 1);
+ }
+ } else {
+ if (numLikes == 1) {
+ return context.getString(R.string.reader_likes_one);
+ } else {
+ String likes = context.getString(R.string.reader_likes_multi);
+ return String.format(likes, numLikes);
+ }
+ }
+ }
+
+ /*
+ * short like text ("1 like," "5 likes," etc.)
+ */
+ public static String getShortLikeLabelText(Context context, int numLikes) {
+ switch (numLikes) {
+ case 0:
+ return context.getString(R.string.reader_short_like_count_none);
+ case 1:
+ return context.getString(R.string.reader_short_like_count_one);
+ default:
+ String count = FormatUtils.formatInt(numLikes);
+ return String.format(context.getString(R.string.reader_short_like_count_multi), count);
+ }
+ }
+
+ public static String getShortCommentLabelText(Context context, int numComments) {
+ switch (numComments) {
+ case 1:
+ return context.getString(R.string.reader_short_comment_count_one);
+ default:
+ String count = FormatUtils.formatInt(numComments);
+ return String.format(context.getString(R.string.reader_short_comment_count_multi), count);
+ }
+ }
+
+ /*
+ * returns true if the reader should provide a "logged out" experience - no likes,
+ * comments, or anything else that requires a wp.com account
+ */
+ public static boolean isLoggedOutReader() {
+ return !AccountHelper.isSignedInWordPressDotCom();
+ }
+
+ /*
+ * returns true if a ReaderPost and ReaderComment exist for the passed Ids
+ */
+ public static boolean postAndCommentExists(long blogId, long postId, long commentId) {
+ return ReaderPostTable.postExists(blogId, postId) &&
+ ReaderCommentTable.commentExists(blogId, postId, commentId);
+ }
+
+ /*
+ * used by Discover site picks to add a "Visit [BlogName]" link which shows the
+ * native blog preview for that blog
+ */
+ public static String makeBlogPreviewUrl(long blogId) {
+ return "wordpress://blogpreview?blogId=" + Long.toString(blogId);
+ }
+
+ public static boolean isBlogPreviewUrl(String url) {
+ return (url != null && url.startsWith("wordpress://blogpreview"));
+ }
+
+ public static long getBlogIdFromBlogPreviewUrl(String url) {
+ if (isBlogPreviewUrl(url)) {
+ String strBlogId = Uri.parse(url).getQueryParameter("blogId");
+ return StringUtils.stringToLong(strBlogId);
+ } else {
+ return 0;
+ }
+ }
+
+ /*
+ * returns the passed string prefixed with a "#" if it's non-empty and isn't already
+ * prefixed with a "#"
+ */
+ public static String makeHashTag(String tagName) {
+ if (TextUtils.isEmpty(tagName)) {
+ return "";
+ } else if (tagName.startsWith("#")) {
+ return tagName;
+ } else {
+ return "#" + tagName;
+ }
+ }
+
+ /*
+ * set the background of the passed view to the round ripple drawable - only works on
+ * Lollipop or later, does nothing on earlier Android versions
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static void setBackgroundToRoundRipple(View view) {
+ if (view != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ view.setBackgroundResource(R.drawable.ripple_oval);
+ }
+ }
+
+ /*
+ * returns a tag object from the passed tag name - first checks for it in the tag db
+ * (so we can also get its title & endpoint), returns a new tag if that fails
+ */
+ public static ReaderTag getTagFromTagName(String tagName, ReaderTagType tagType) {
+ ReaderTag tag = ReaderTagTable.getTag(tagName, tagType);
+ if (tag != null) {
+ return tag;
+ } else {
+ return createTagFromTagName(tagName, tagType);
+ }
+ }
+
+ public static ReaderTag createTagFromTagName(String tagName, ReaderTagType tagType) {
+ String tagSlug = sanitizeWithDashes(tagName).toLowerCase();
+ String tagDisplayName = tagType == ReaderTagType.DEFAULT ? tagName : tagSlug;
+ return new ReaderTag(tagSlug, tagDisplayName, tagName, null, tagType);
+ }
+
+ /*
+ * returns the default tag, which is the one selected by default in the reader when
+ * the user hasn't already chosen one
+ */
+ public static ReaderTag getDefaultTag() {
+ return getTagFromTagName(ReaderTag.TAG_TITLE_DEFAULT, ReaderTagType.DEFAULT);
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java
new file mode 100644
index 000000000..94ad8ece0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java
@@ -0,0 +1,163 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.JsonArrayRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+
+public class ReaderVideoUtils {
+ private ReaderVideoUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * returns the url to get the full-size (480x360) thumbnail url for the passed video
+ * see http://www.reelseo.com/youtube-thumbnail-image/ for other sizes
+ */
+ public static String getYouTubeThumbnailUrl(final String videoUrl) {
+ String videoId = getYouTubeVideoId(videoUrl);
+ if (TextUtils.isEmpty(videoId))
+ return "";
+ // note that this *must* use https rather than http - ex: https://img.youtube.com/vi/ClbE019cLNI/0.jpg
+ return "https://img.youtube.com/vi/" + videoId + "/0.jpg";
+ }
+
+ /*
+ * returns true if the passed url is a link to a YouTube video
+ */
+ public static boolean isYouTubeVideoLink(final String link) {
+ return (!TextUtils.isEmpty(getYouTubeVideoId(link)));
+ }
+
+ /*
+ * extract the video id from the passed YouTube url
+ */
+ private static String getYouTubeVideoId(final String link) {
+ if (link==null)
+ return "";
+
+ Uri uri = Uri.parse(link);
+ try {
+ String host = uri.getHost();
+ if (host==null)
+ return "";
+
+ // youtube.com links
+ if (host.equals("youtube.com") || host.equals("www.youtube.com")) {
+ // if link contains "watch" in the path, then the id is in the "v=" query param
+ if (link.contains("watch"))
+ return uri.getQueryParameter("v");
+ // if the link contains "embed" in the path, then the id is the last path segment
+ // ex: https://www.youtube.com/embed/fw3w68YrKwc?version=3&#038;rel=1&#038;
+ if (link.contains("/embed/"))
+ return uri.getLastPathSegment();
+ return "";
+ }
+
+ // youtu.be urls have the videoId as the path - ex: http://youtu.be/pEnXclbO9jg
+ if (host.equals("youtu.be")) {
+ String path = uri.getPath();
+ if (path==null)
+ return "";
+ // remove the leading slash
+ return path.replace("/", "");
+ }
+
+ // YouTube mobile urls include video id in fragment, ex: http://m.youtube.com/?dc=organic&source=mog#/watch?v=t77Vlme_pf8
+ if (host.equals("m.youtube.com")) {
+ String fragment = uri.getFragment();
+ if (fragment==null)
+ return "";
+ int index = fragment.lastIndexOf("v=");
+ if (index!=-1)
+ return fragment.substring(index+2, fragment.length());
+ }
+
+ return "";
+ } catch (UnsupportedOperationException e) {
+ AppLog.e(T.READER, e);
+ return "";
+ } catch (IndexOutOfBoundsException e) {
+ // thrown by substring
+ AppLog.e(T.READER, e);
+ return "";
+ }
+ }
+
+ /*
+ * returns true if the passed url is a link to a Vimeo video
+ */
+ public static boolean isVimeoLink(final String link) {
+ return (!TextUtils.isEmpty(getVimeoVideoId(link)));
+ }
+
+ /*
+ * extract the video id from the passed Vimeo url
+ * ex: http://player.vimeo.com/video/72386905 -> 72386905
+ */
+ private static String getVimeoVideoId(final String link) {
+ if (link==null)
+ return "";
+ if (!link.contains("player.vimeo.com"))
+ return "";
+
+ Uri uri = Uri.parse(link);
+ return uri.getLastPathSegment();
+ }
+
+ /*
+ * unlike YouTube thumbnails, Vimeo thumbnails require network request
+ */
+ public static void requestVimeoThumbnail(final String videoUrl, final VideoThumbnailListener thumbListener) {
+ // useless without a listener
+ if (thumbListener==null)
+ return;
+
+ String id = getVimeoVideoId(videoUrl);
+ if (TextUtils.isEmpty(id)) {
+ thumbListener.onResponse(false, null);
+ return;
+ }
+
+ Response.Listener<JSONArray> listener = new Response.Listener<JSONArray>() {
+ public void onResponse(JSONArray response) {
+ String thumbnailUrl = null;
+ if (response!=null && response.length() > 0) {
+ JSONObject json = response.optJSONObject(0);
+ if (json!=null && json.has("thumbnail_large"))
+ thumbnailUrl = JSONUtils.getString(json, "thumbnail_large");
+ }
+ if (TextUtils.isEmpty(thumbnailUrl)) {
+ thumbListener.onResponse(false, null);
+ } else {
+ thumbListener.onResponse(true, thumbnailUrl);
+ }
+ }
+ };
+ Response.ErrorListener errorListener = new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ thumbListener.onResponse(false, null);
+ }
+ };
+
+ String url = "http://vimeo.com/api/v2/video/" + id + ".json";
+ JsonArrayRequest request = new JsonArrayRequest(url, listener, errorListener);
+
+ WordPress.requestQueue.add(request);
+ }
+
+ public interface VideoThumbnailListener {
+ void onResponse(boolean successful, String thumbnailUrl);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java
new file mode 100644
index 000000000..75e6908b8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.Spanned;
+
+import org.wordpress.android.models.ReaderPost;
+
+/**
+ * Reader cross-post utility routines
+ */
+
+public class ReaderXPostUtils {
+
+ // note that these strings don't need to be localized due to the intended audience
+ private static final String UNKNOWN_SITE = "(unknown)";
+ private static final String FMT_SITE_XPOST = "%1$s cross-posted from %2$s to %3$s";
+ private static final String FMT_COMMENT_XPOST = "%1$s commented on %2$s, cross-posted to %3$s";
+
+ /*
+ * returns the title to display for this xpost, which is simply the post's title
+ * without the "X-post: " prefix
+ */
+ public static String getXPostTitle(@NonNull ReaderPost post) {
+ if (post.getTitle().startsWith("X-post: ")) {
+ return post.getTitle().substring(8);
+ } else {
+ return post.getTitle();
+ }
+ }
+
+ /*
+ * returns the html subtitle to display for this xpost
+ * ex: "Nick cross-posted from +blog1 to +blog2"
+ * ex: "Nick commented on +blog1, cross-posted to +blog2"
+ */
+ public static Spanned getXPostSubtitleHtml(@NonNull ReaderPost post) {
+ boolean isCommentXPost = post.getExcerpt().startsWith("X-comment");
+
+ String name = post.hasAuthorFirstName() ? post.getAuthorFirstName() : post.getAuthorName();
+ String subtitle = String.format(
+ isCommentXPost ? FMT_COMMENT_XPOST : FMT_SITE_XPOST,
+ "<strong>" + name + "</strong>",
+ getFromSiteName(post),
+ getToSiteName(post));
+
+ return Html.fromHtml(subtitle);
+ }
+
+ // origin site name can be extracted from the excerpt,
+ // example excerpt: "<p>X-post from +blog2: I have a request..."
+ private static String getFromSiteName(@NonNull ReaderPost post) {
+ String excerpt = post.getExcerpt();
+ int plusPos = excerpt.indexOf("+");
+ int colonPos = excerpt.indexOf(":", plusPos);
+ if (plusPos > 0 && colonPos > 0) {
+ return excerpt.substring(plusPos, colonPos);
+ } else {
+ return UNKNOWN_SITE;
+ }
+ }
+
+ // destination site name is the subdomain of the blog url
+ private static String getToSiteName(@NonNull ReaderPost post) {
+ Uri uri = Uri.parse(post.getBlogUrl());
+ String domain = uri.getHost();
+ if (domain == null || !domain.contains(".")) {
+ return "+" + UNKNOWN_SITE;
+ }
+
+ return "+" + domain.substring(0, domain.indexOf("."));
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderCommentsPostHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderCommentsPostHeaderView.java
new file mode 100644
index 000000000..cefb3c285
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderCommentsPostHeaderView.java
@@ -0,0 +1,75 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+/**
+ * topmost view in reader comment adapter - show info about the post
+ */
+public class ReaderCommentsPostHeaderView extends LinearLayout {
+
+ public ReaderCommentsPostHeaderView(Context context) {
+ super(context);
+ initView(context);
+ }
+
+ public ReaderCommentsPostHeaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public ReaderCommentsPostHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView(context);
+ }
+
+ private void initView(Context context) {
+ inflate(context, R.layout.reader_comments_post_header_view, this);
+ }
+
+ public void setPost(final ReaderPost post) {
+ if (post == null) return;
+
+ TextView txtTitle = (TextView) findViewById(R.id.text_post_title);
+ TextView txtBlogName = (TextView) findViewById(R.id.text_blog_name);
+ TextView txtDateline = (TextView) findViewById(R.id.text_post_dateline);
+ WPNetworkImageView imgAvatar = (WPNetworkImageView) findViewById(R.id.image_post_avatar);
+
+ txtTitle.setText(post.getTitle());
+ if (post.hasBlogName()) {
+ txtBlogName.setText(post.getBlogName());
+ } else {
+ txtBlogName.setText(R.string.reader_untitled_post);
+ }
+
+ java.util.Date dtPost = DateTimeUtils.dateFromIso8601(post.getDate());
+ String dateLine = DateTimeUtils.javaDateToTimeSpan(dtPost, WordPress.getContext());
+ if (post.isCommentsOpen || post.numReplies > 0) {
+ dateLine += " \u2022 " + ReaderUtils.getShortCommentLabelText(getContext(), post.numReplies);
+ }
+ if (post.canLikePost() || post.numLikes > 0) {
+ dateLine += " \u2022 " + ReaderUtils.getShortLikeLabelText(getContext(), post.numLikes);
+ }
+ txtDateline.setText(dateLine);
+
+ int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_extra_small);
+ String avatarUrl;
+ if (post.hasBlogUrl()) {
+ avatarUrl = GravatarUtils.blavatarFromUrl(post.getBlogUrl(), avatarSz);
+ imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.BLAVATAR);
+ } else {
+ avatarUrl = post.getPostAvatarForDisplay(avatarSz);
+ imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java
new file mode 100644
index 000000000..efd5484b8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java
@@ -0,0 +1,96 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+/**
+ * Follow button used in reader detail
+ */
+public class ReaderFollowButton extends LinearLayout {
+ private TextView mTextFollow;
+ private ImageView mImageFollow;
+ private boolean mIsFollowed;
+
+ public ReaderFollowButton(Context context){
+ super(context);
+ initView(context);
+ }
+
+ public ReaderFollowButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public ReaderFollowButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initView(context);
+ }
+
+ private void initView(Context context) {
+ inflate(context, R.layout.reader_follow_button, this);
+ mTextFollow = (TextView) findViewById(R.id.text_follow_button);
+ mImageFollow = (ImageView) findViewById(R.id.image_follow_button);
+ }
+
+ private void updateFollowText() {
+ mTextFollow.setSelected(mIsFollowed);
+ mTextFollow.setText(mIsFollowed ? R.string.reader_btn_unfollow : R.string.reader_btn_follow);
+ mImageFollow.setImageResource(mIsFollowed ? R.drawable.reader_following : R.drawable.reader_follow);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mTextFollow.setEnabled(enabled);
+ mImageFollow.setEnabled(enabled);
+ }
+
+ public void setIsFollowed(boolean isFollowed) {
+ setIsFollowed(isFollowed, false);
+ }
+ public void setIsFollowedAnimated(boolean isFollowed) {
+ setIsFollowed(isFollowed, true);
+ }
+ private void setIsFollowed(boolean isFollowed, boolean animateChanges) {
+ if (isFollowed == mIsFollowed && mTextFollow.isSelected() == isFollowed) {
+ return;
+ }
+
+ mIsFollowed = isFollowed;
+
+ if (animateChanges) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, View.SCALE_Y, 1f, 0f);
+ anim.setRepeatMode(ValueAnimator.REVERSE);
+ anim.setRepeatCount(1);
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ updateFollowText();
+ }
+ });
+
+ long duration = getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
+ AnimatorSet set = new AnimatorSet();
+ set.play(anim);
+ set.setDuration(duration);
+ set.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ set.start();
+ } else {
+ updateFollowText();
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderGapMarkerView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderGapMarkerView.java
new file mode 100644
index 000000000..db5a63703
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderGapMarkerView.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.services.ReaderPostService;
+import org.wordpress.android.ui.reader.services.ReaderPostService.UpdateAction;
+import org.wordpress.android.util.NetworkUtils;
+
+/**
+ * marker view between posts indicating a gap in time between them that can be filled in - designed
+ * for use inside ReaderPostAdapter
+ */
+public class ReaderGapMarkerView extends RelativeLayout {
+ private TextView mText;
+ private ProgressBar mProgress;
+ private ReaderTag mCurrentTag;
+
+ public ReaderGapMarkerView(Context context) {
+ super(context);
+ initView(context);
+ }
+
+ public ReaderGapMarkerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public ReaderGapMarkerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView(context);
+ }
+
+ private void initView(Context context) {
+ View view = inflate(context, R.layout.reader_gap_marker_view, this);
+ mText = (TextView) view.findViewById(R.id.text_gap_marker);
+ mProgress = (ProgressBar) view.findViewById(R.id.progress_gap_marker);
+
+ mText.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ fillTheGap();
+ }
+ });
+ }
+
+ public void setCurrentTag(ReaderTag tag) {
+ mCurrentTag = tag;
+ hideProgress();
+ }
+
+ private void fillTheGap() {
+ if (mCurrentTag == null
+ || !NetworkUtils.checkConnection(getContext())) {
+ return;
+ }
+
+ // start service to fill the gap - EventBus will notify the owning fragment of new posts,
+ // and will take care of hiding this view
+ ReaderPostService.startServiceForTag(getContext(), mCurrentTag, UpdateAction.REQUEST_OLDER_THAN_GAP);
+ showProgress();
+ }
+
+ private void showProgress() {
+ mText.setVisibility(View.INVISIBLE);
+ mProgress.setVisibility(View.VISIBLE);
+ }
+
+ private void hideProgress() {
+ mText.setVisibility(View.VISIBLE);
+ mProgress.setVisibility(View.GONE);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java
new file mode 100644
index 000000000..9e2ff2fd2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java
@@ -0,0 +1,91 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.FormatUtils;
+
+/*
+ * used when showing comment + comment count, like + like count
+ */
+public class ReaderIconCountView extends LinearLayout {
+ private ImageView mImageView;
+ private TextView mTextCount;
+ private int mIconType;
+
+ // these must match the same values in attrs.xml
+ private static final int ICON_LIKE = 0;
+ private static final int ICON_COMMENT = 1;
+
+ public ReaderIconCountView(Context context){
+ super(context);
+ initView(context, null);
+ }
+
+ public ReaderIconCountView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context, attrs);
+ }
+
+ public ReaderIconCountView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initView(context, attrs);
+ }
+
+ private void initView(Context context, AttributeSet attrs) {
+ inflate(context, R.layout.reader_icon_count_view, this);
+
+ mImageView = (ImageView) findViewById(R.id.image_count);
+ mTextCount = (TextView) findViewById(R.id.text_count);
+
+ if (attrs != null) {
+ TypedArray a = context.getTheme().obtainStyledAttributes(
+ attrs,
+ R.styleable.ReaderIconCountView,
+ 0, 0);
+ try {
+ mIconType = a.getInteger(R.styleable.ReaderIconCountView_readerIcon, ICON_LIKE);
+ switch (mIconType) {
+ case ICON_LIKE :
+ mImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.reader_button_like));
+ break;
+ case ICON_COMMENT :
+ mImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.reader_button_comment));
+ break;
+ }
+
+ } finally {
+ a.recycle();
+ }
+ }
+
+ ReaderUtils.setBackgroundToRoundRipple(mImageView);
+ }
+
+ public ImageView getImageView() {
+ return mImageView;
+ }
+
+ public void setSelected(boolean selected) {
+ mImageView.setSelected(selected);
+ mTextCount.setSelected(selected);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mImageView.setEnabled(enabled);
+ mTextCount.setEnabled(enabled);
+ }
+
+ public void setCount(int count) {
+ mTextCount.setText(FormatUtils.formatInt(count));
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderLikingUsersView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderLikingUsersView.java
new file mode 100644
index 000000000..99da9256d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderLikingUsersView.java
@@ -0,0 +1,105 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderUserIdList;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+
+/*
+ * LinearLayout which shows liking users - used by ReaderPostDetailFragment
+ */
+public class ReaderLikingUsersView extends LinearLayout {
+ private final int mLikeAvatarSz;
+
+ public ReaderLikingUsersView(Context context) {
+ this(context, null);
+ }
+
+ public ReaderLikingUsersView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setOrientation(HORIZONTAL);
+ setGravity(Gravity.CENTER_VERTICAL);
+
+ mLikeAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small);
+ }
+
+ public void showLikingUsers(final ReaderPost post) {
+ if (post == null) {
+ return;
+ }
+
+ final Handler handler = new Handler();
+ new Thread() {
+ @Override
+ public void run() {
+ // get avatar URLs of liking users up to the max, sized to fit
+ int maxAvatars = getMaxAvatars();
+ ReaderUserIdList avatarIds = ReaderLikeTable.getLikesForPost(post);
+ final ArrayList<String> avatars = ReaderUserTable.getAvatarUrls(avatarIds, maxAvatars, mLikeAvatarSz);
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ showLikingAvatars(avatars);
+ }
+ });
+ }
+ }.start();
+ }
+
+ /*
+ * returns count of avatars that can fit the current space
+ */
+ private int getMaxAvatars() {
+ int marginAvatar = getResources().getDimensionPixelSize(R.dimen.margin_extra_small);
+ int marginReader = getResources().getDimensionPixelSize(R.dimen.reader_detail_margin);
+ int likeAvatarSizeWithMargin = mLikeAvatarSz + (marginAvatar * 2);
+ int spaceForAvatars = getWidth() - (marginReader * 2);
+ return spaceForAvatars / likeAvatarSizeWithMargin;
+ }
+
+ /*
+ * note that the passed list of avatar urls has already been Photon-ized,
+ * so there's no need to do that here
+ */
+ private void showLikingAvatars(final ArrayList<String> avatarUrls) {
+ if (avatarUrls == null || avatarUrls.size() == 0) {
+ removeAllViews();
+ return;
+ }
+
+ // remove excess existing views
+ int numExistingViews = getChildCount();
+ if (numExistingViews > avatarUrls.size()) {
+ int numToRemove = numExistingViews - avatarUrls.size();
+ removeViews(numExistingViews - numToRemove, numToRemove);
+ }
+
+ int index = 0;
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ for (String url : avatarUrls) {
+ WPNetworkImageView imgAvatar;
+ // reuse existing view when possible, otherwise inflate a new one
+ if (index < numExistingViews) {
+ imgAvatar = (WPNetworkImageView) getChildAt(index);
+ } else {
+ imgAvatar = (WPNetworkImageView) inflater.inflate(R.layout.reader_like_avatar, this, false);
+ addView(imgAvatar);
+ }
+ imgAvatar.setImageUrl(url, WPNetworkImageView.ImageType.AVATAR);
+ index++;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPhotoView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPhotoView.java
new file mode 100644
index 000000000..ad5182785
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPhotoView.java
@@ -0,0 +1,271 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+import com.android.volley.toolbox.ImageLoader.ImageContainer;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.PhotonUtils;
+
+import uk.co.senab.photoview.PhotoViewAttacher;
+
+/**
+ * used by ReaderPhotoViewerActivity to show full-width images - based on Volley's ImageView
+ * but adds pinch/zoom and the ability to first load a lo-res version of the image
+ */
+public class ReaderPhotoView extends RelativeLayout {
+
+ public interface PhotoViewListener {
+ void onTapPhotoView();
+ }
+
+ private PhotoViewListener mPhotoViewListener;
+ private String mLoResImageUrl;
+ private String mHiResImageUrl;
+
+ private ImageContainer mLoResContainer;
+ private ImageContainer mHiResContainer;
+
+ private final ImageView mImageView;
+ private final ProgressBar mProgress;
+ private final TextView mTxtError;
+ private boolean mIsInitialLayout = true;
+
+ public ReaderPhotoView(Context context) {
+ this(context, null);
+ }
+
+ public ReaderPhotoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ inflate(context, R.layout.reader_photo_view, this);
+
+ // ImageView which contains the downloaded image
+ mImageView = (ImageView) findViewById(R.id.image_photo);
+
+ // error text that appears when download fails
+ mTxtError = (TextView) findViewById(R.id.text_error);
+
+ // progress bar which appears while downloading
+ mProgress = (ProgressBar) findViewById(R.id.progress_loading);
+ }
+
+ /**
+ * @param imageUrl the url of the image to load
+ * @param hiResWidth maximum width of the full-size image
+ * @param isPrivate whether this is an image from a private blog
+ * @param listener listener for taps on this view
+ */
+ public void setImageUrl(String imageUrl,
+ int hiResWidth,
+ boolean isPrivate,
+ PhotoViewListener listener) {
+ int loResWidth = (int) (hiResWidth * 0.10f);
+ mLoResImageUrl = ReaderUtils.getResizedImageUrl(imageUrl, loResWidth, 0, isPrivate, PhotonUtils.Quality.LOW);
+ mHiResImageUrl = ReaderUtils.getResizedImageUrl(imageUrl, hiResWidth, 0, isPrivate, PhotonUtils.Quality.MEDIUM);
+
+ mPhotoViewListener = listener;
+ loadLoResImage();
+ }
+
+ private boolean isRequestingUrl(ImageContainer container, String url) {
+ return (container != null
+ && container.getRequestUrl() != null
+ && container.getRequestUrl().equals(url));
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean hasLayout() {
+ // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
+ // view, hold off on loading the image.
+ if (getWidth() == 0 && getHeight() == 0) {
+ boolean isFullyWrapContent = getLayoutParams() != null
+ && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT
+ && getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT;
+ if (!isFullyWrapContent) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void loadLoResImage() {
+ if (!hasLayout() || TextUtils.isEmpty(mLoResImageUrl)) {
+ return;
+ }
+
+ // skip if this same image url is already being loaded
+ if (isRequestingUrl(mLoResContainer, mLoResImageUrl)) {
+ AppLog.d(AppLog.T.READER, "reader photo > already requesting lo-res");
+ return;
+ }
+
+ Point pt = DisplayUtils.getDisplayPixelSize(this.getContext());
+ int maxSize = Math.min(pt.x, pt.y);
+
+ showProgress();
+
+ mLoResContainer = WordPress.imageLoader.get(mLoResImageUrl,
+ new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.e(AppLog.T.READER, error);
+ hideProgress();
+ showError();
+ }
+
+ @Override
+ public void onResponse(final ImageContainer response, boolean isImmediate) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ handleResponse(response.getBitmap(), true);
+ }
+ });
+ }
+ }, maxSize, maxSize);
+ }
+
+ private void loadHiResImage() {
+ if (!hasLayout() || TextUtils.isEmpty(mHiResImageUrl)) {
+ return;
+ }
+
+ if (isRequestingUrl(mHiResContainer, mHiResImageUrl)) {
+ AppLog.d(AppLog.T.READER, "reader photo > already requesting hi-res");
+ return;
+ }
+
+ Point pt = DisplayUtils.getDisplayPixelSize(this.getContext());
+ int maxSize = Math.max(pt.x, pt.y);
+
+ mHiResContainer = WordPress.imageLoader.get(mHiResImageUrl,
+ new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.e(AppLog.T.READER, error);
+ }
+
+ @Override
+ public void onResponse(final ImageContainer response, boolean isImmediate) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ handleResponse(response.getBitmap(), false);
+ }
+ });
+ }
+ }, maxSize, maxSize);
+ }
+
+ private void handleResponse(Bitmap bitmap, boolean isLoRes) {
+ if (bitmap != null) {
+ hideProgress();
+
+ // show the bitmap and attach the pinch/zoom handler
+ mImageView.setImageBitmap(bitmap);
+ setAttacher();
+
+ // load hi-res image if this was the lo-res one
+ if (isLoRes && !mLoResImageUrl.equals(mHiResImageUrl)) {
+ loadHiResImage();
+ }
+ }
+ }
+
+ private void setAttacher() {
+ PhotoViewAttacher attacher = new PhotoViewAttacher(mImageView);
+ attacher.setOnPhotoTapListener(new PhotoViewAttacher.OnPhotoTapListener() {
+ @Override
+ public void onPhotoTap(View view, float v, float v2) {
+ if (mPhotoViewListener != null) {
+ mPhotoViewListener.onTapPhotoView();
+ }
+ }
+ });
+ attacher.setOnViewTapListener(new PhotoViewAttacher.OnViewTapListener() {
+ @Override
+ public void onViewTap(View view, float v, float v2) {
+ if (mPhotoViewListener != null) {
+ mPhotoViewListener.onTapPhotoView();
+ }
+ }
+ });
+ }
+
+ private void showError() {
+ hideProgress();
+ if (mTxtError != null) {
+ mTxtError.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void showProgress() {
+ if (mProgress != null) {
+ mProgress.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideProgress() {
+ if (mProgress != null) {
+ mProgress.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!isInEditMode()) {
+ if (mIsInitialLayout) {
+ mIsInitialLayout = false;
+ AppLog.d(AppLog.T.READER, "reader photo > initial layout");
+ post(new Runnable() {
+ @Override
+ public void run() {
+ loadLoResImage();
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mLoResContainer != null || mHiResContainer != null) {
+ mImageView.setImageDrawable(null);
+ }
+ if (mLoResContainer != null) {
+ mLoResContainer.cancelRequest();
+ mLoResContainer = null;
+ }
+ if (mHiResContainer != null) {
+ mHiResContainer.cancelRequest();
+ mHiResContainer = null;
+ }
+ mIsInitialLayout = true;
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRecyclerView.java
new file mode 100644
index 000000000..ab6713f74
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRecyclerView.java
@@ -0,0 +1,30 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+
+public class ReaderRecyclerView extends RecyclerView {
+
+ public ReaderRecyclerView(Context context) {
+ super(context);
+ initialize(context);
+ }
+
+ public ReaderRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context);
+ }
+
+ public ReaderRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialize(context);
+ }
+
+ private void initialize(Context context) {
+ if (!isInEditMode()) {
+ setLayoutManager(new LinearLayoutManager(context));
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java
new file mode 100644
index 000000000..2a7535a9b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java
@@ -0,0 +1,205 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderBlogActions;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+import org.wordpress.android.widgets.WPNetworkImageView.ImageType;
+
+/**
+ * topmost view in post adapter when showing blog preview - displays description, follower
+ * count, and follow button
+ */
+public class ReaderSiteHeaderView extends LinearLayout {
+
+ public interface OnBlogInfoLoadedListener {
+ void onBlogInfoLoaded(ReaderBlog blogInfo);
+ }
+
+ private long mBlogId;
+ private long mFeedId;
+ private ReaderFollowButton mFollowButton;
+ private ReaderBlog mBlogInfo;
+ private OnBlogInfoLoadedListener mBlogInfoListener;
+
+ public ReaderSiteHeaderView(Context context) {
+ super(context);
+ initView(context);
+ }
+
+ public ReaderSiteHeaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public ReaderSiteHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView(context);
+ }
+
+ private void initView(Context context) {
+ View view = inflate(context, R.layout.reader_site_header_view, this);
+ mFollowButton = (ReaderFollowButton) view.findViewById(R.id.follow_button);
+ }
+
+ public void setOnBlogInfoLoadedListener(OnBlogInfoLoadedListener listener) {
+ mBlogInfoListener = listener;
+ }
+
+ public void loadBlogInfo(long blogId, long feedId) {
+ mBlogId = blogId;
+ mFeedId = feedId;
+
+ // first get info from local db
+ final ReaderBlog localBlogInfo;
+ if (mBlogId != 0) {
+ localBlogInfo = ReaderBlogTable.getBlogInfo(mBlogId);
+ } else if (mFeedId != 0) {
+ localBlogInfo = ReaderBlogTable.getFeedInfo(mFeedId);
+ } else {
+ ToastUtils.showToast(getContext(), R.string.reader_toast_err_get_blog_info);
+ return;
+ }
+ if (localBlogInfo != null) {
+ showBlogInfo(localBlogInfo);
+ }
+
+ // then get from server if doesn't exist locally or is time to update it
+ if (localBlogInfo == null || ReaderBlogTable.isTimeToUpdateBlogInfo(localBlogInfo)) {
+ ReaderActions.UpdateBlogInfoListener listener = new ReaderActions.UpdateBlogInfoListener() {
+ @Override
+ public void onResult(ReaderBlog serverBlogInfo) {
+ showBlogInfo(serverBlogInfo);
+ }
+ };
+ if (mFeedId != 0) {
+ ReaderBlogActions.updateFeedInfo(mFeedId, null, listener);
+ } else {
+ ReaderBlogActions.updateBlogInfo(mBlogId, null, listener);
+ }
+ }
+ }
+
+ private void showBlogInfo(ReaderBlog blogInfo) {
+ // do nothing if unchanged
+ if (blogInfo == null || blogInfo.isSameAs(mBlogInfo)) {
+ return;
+ }
+
+ mBlogInfo = blogInfo;
+
+ ViewGroup layoutInfo = (ViewGroup) findViewById(R.id.layout_blog_info);
+ TextView txtBlogName = (TextView) layoutInfo.findViewById(R.id.text_blog_name);
+ TextView txtDomain = (TextView) layoutInfo.findViewById(R.id.text_domain);
+ TextView txtDescription = (TextView) layoutInfo.findViewById(R.id.text_blog_description);
+ TextView txtFollowCount = (TextView) layoutInfo.findViewById(R.id.text_blog_follow_count);
+ WPNetworkImageView imgBlavatar = (WPNetworkImageView) layoutInfo.findViewById(R.id.image_blavatar);
+
+ if (blogInfo.hasName()) {
+ txtBlogName.setText(blogInfo.getName());
+ } else {
+ txtBlogName.setText(R.string.reader_untitled_post);
+ }
+
+ if (blogInfo.hasUrl()) {
+ txtDomain.setText(UrlUtils.getHost(blogInfo.getUrl()));
+ txtDomain.setVisibility(View.VISIBLE);
+ } else {
+ txtDomain.setVisibility(View.GONE);
+ }
+
+ if (blogInfo.hasDescription()) {
+ txtDescription.setText(blogInfo.getDescription());
+ txtDescription.setVisibility(View.VISIBLE);
+ } else {
+ txtDescription.setVisibility(View.GONE);
+ }
+
+ txtFollowCount.setText(String.format(getContext().getString(R.string.reader_label_follow_count), blogInfo.numSubscribers));
+
+ if (ReaderUtils.isLoggedOutReader()) {
+ mFollowButton.setVisibility(View.GONE);
+ } else {
+ mFollowButton.setVisibility(View.VISIBLE);
+ mFollowButton.setIsFollowed(blogInfo.isFollowing);
+ mFollowButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleFollowStatus();
+ }
+ });
+ }
+
+ if (blogInfo.hasImageUrl()) {
+ int imageSize = getContext().getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium);
+ String imageUrl = PhotonUtils.getPhotonImageUrl(blogInfo.getImageUrl(), imageSize, imageSize);
+ imgBlavatar.setImageUrl(imageUrl, ImageType.BLAVATAR);
+ } else {
+ imgBlavatar.showDefaultBlavatarImage();
+ }
+
+ if (layoutInfo.getVisibility() != View.VISIBLE) {
+ layoutInfo.setVisibility(View.VISIBLE);
+ }
+
+ if (mBlogInfoListener != null) {
+ mBlogInfoListener.onBlogInfoLoaded(blogInfo);
+ }
+ }
+
+ private void toggleFollowStatus() {
+ if (!NetworkUtils.checkConnection(getContext())) {
+ return;
+ }
+
+ final boolean isAskingToFollow;
+ if (mFeedId != 0) {
+ isAskingToFollow = !ReaderBlogTable.isFollowedFeed(mFeedId);
+ } else {
+ isAskingToFollow = !ReaderBlogTable.isFollowedBlog(mBlogId);
+ }
+
+ ReaderActions.ActionListener listener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (getContext() == null) {
+ return;
+ }
+ mFollowButton.setEnabled(true);
+ if (!succeeded) {
+ int errResId = isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog;
+ ToastUtils.showToast(getContext(), errResId);
+ mFollowButton.setIsFollowed(!isAskingToFollow);
+ }
+ }
+ };
+
+ // disable follow button until API call returns
+ mFollowButton.setEnabled(false);
+
+ boolean result;
+ if (mFeedId != 0) {
+ result = ReaderBlogActions.followFeedById(mFeedId, isAskingToFollow, listener);
+ } else {
+ result = ReaderBlogActions.followBlogById(mBlogId, isAskingToFollow, listener);
+ }
+
+ if (result) {
+ mFollowButton.setIsFollowedAnimated(isAskingToFollow);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.java
new file mode 100644
index 000000000..40d798ede
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.java
@@ -0,0 +1,103 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderTagActions;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+
+/**
+ * topmost view in post adapter when showing tag preview - displays tag name and follow button
+ */
+public class ReaderTagHeaderView extends LinearLayout {
+
+ private ReaderFollowButton mFollowButton;
+ private ReaderTag mCurrentTag;
+
+ public ReaderTagHeaderView(Context context) {
+ super(context);
+ initView(context);
+ }
+
+ public ReaderTagHeaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public ReaderTagHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView(context);
+ }
+
+ private void initView(Context context) {
+ View view = inflate(context, R.layout.reader_tag_header_view, this);
+ mFollowButton = (ReaderFollowButton) view.findViewById(R.id.follow_button);
+ }
+
+ public void setCurrentTag(final ReaderTag tag) {
+ if (tag == null) return;
+
+ mCurrentTag = tag;
+
+ TextView txtTagName = (TextView) findViewById(R.id.text_tag);
+ txtTagName.setText(ReaderUtils.makeHashTag(tag.getTagSlug()));
+
+ if (ReaderUtils.isLoggedOutReader()) {
+ mFollowButton.setVisibility(View.GONE);
+ } else {
+ mFollowButton.setVisibility(View.VISIBLE);
+ mFollowButton.setIsFollowed(ReaderTagTable.isFollowedTagName(tag.getTagSlug()));
+ mFollowButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleFollowStatus();
+ }
+ });
+ }
+ }
+
+ private void toggleFollowStatus() {
+ if (mCurrentTag == null || !NetworkUtils.checkConnection(getContext())) {
+ return;
+ }
+
+ final boolean isAskingToFollow = !ReaderTagTable.isFollowedTagName(mCurrentTag.getTagSlug());
+
+ ReaderActions.ActionListener listener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (getContext() == null) {
+ return;
+ }
+ mFollowButton.setEnabled(true);
+ if (!succeeded) {
+ int errResId = isAskingToFollow ? R.string.reader_toast_err_add_tag : R.string.reader_toast_err_remove_tag;
+ ToastUtils.showToast(getContext(), errResId);
+ mFollowButton.setIsFollowed(!isAskingToFollow);
+ }
+ }
+ };
+
+ mFollowButton.setEnabled(false);
+
+ boolean success;
+ if (isAskingToFollow) {
+ success = ReaderTagActions.addTag(mCurrentTag, listener);
+ } else {
+ success = ReaderTagActions.deleteTag(mCurrentTag, listener);
+ }
+
+ if (success) {
+ mFollowButton.setIsFollowedAnimated(isAskingToFollow);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderThumbnailStrip.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderThumbnailStrip.java
new file mode 100644
index 000000000..042b1cf19
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderThumbnailStrip.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption;
+import org.wordpress.android.ui.reader.models.ReaderImageList;
+import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.EnumSet;
+
+/**
+ * displays a row of image thumbnails from a reader post - only shows when two or more images
+ * of a minimum size are found
+ */
+public class ReaderThumbnailStrip extends LinearLayout {
+
+ private static final int MIN_IMAGE_COUNT = 2;
+
+ private View mView;
+ private LinearLayout mContainer;
+ private int mThumbnailSize;
+ private int mMaxImageCount;
+ private String mCountStr;
+
+ public ReaderThumbnailStrip(Context context) {
+ super(context);
+ initView(context);
+ }
+
+ public ReaderThumbnailStrip(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context);
+ }
+
+ public ReaderThumbnailStrip(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView(context);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ReaderThumbnailStrip(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initView(context);
+ }
+
+ private void initView(Context context) {
+ mView = inflate(context, R.layout.reader_thumbnail_strip, this);
+ mContainer = (LinearLayout) mView.findViewById(R.id.thumbnail_strip_container);
+ mThumbnailSize = context.getResources().getDimensionPixelSize(R.dimen.reader_thumbnail_strip_image_size);
+ mCountStr = context.getResources().getString(R.string.reader_label_image_count_multi);
+
+ // base max image count on display width
+ int displayWidth = DisplayUtils.getDisplayPixelWidth(context);
+ if (displayWidth <= 800) {
+ mMaxImageCount = 2;
+ } else if (displayWidth <= 1024) {
+ mMaxImageCount = 3;
+ } else if (displayWidth <= 1440) {
+ mMaxImageCount = 4;
+ } else {
+ mMaxImageCount = 5;
+ }
+ }
+
+ public void loadThumbnails(long blogId, long postId, final boolean isPrivate) {
+ // get rid of any views already added
+ mContainer.removeAllViews();
+
+ // get this post's content and scan it for images suitable in a gallery
+ final String content = ReaderPostTable.getPostText(blogId, postId);
+ final ReaderImageList imageList =
+ new ReaderImageScanner(content, isPrivate).getGalleryImageList();
+ if (imageList.size() < MIN_IMAGE_COUNT) {
+ mView.setVisibility(View.GONE);
+ return;
+ }
+
+ // add a separate imageView for each image up to the max
+ int numAdded = 0;
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ for (final String imageUrl: imageList) {
+ View view = inflater.inflate(R.layout.reader_thumbnail_strip_image, mContainer, false);
+ WPNetworkImageView imageView = (WPNetworkImageView) view.findViewById(R.id.thumbnail_strip_image);
+ mContainer.addView(view);
+
+ String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, mThumbnailSize, mThumbnailSize);
+ imageView.setImageUrl(photonUrl, WPNetworkImageView.ImageType.PHOTO);
+
+ numAdded++;
+ if (numAdded >= mMaxImageCount) {
+ break;
+ }
+ }
+
+ // add the labels which include the image count
+ View labelView = inflater.inflate(R.layout.reader_thumbnail_strip_labels, mContainer, false);
+ TextView txtCount = (TextView) labelView.findViewById(R.id.text_gallery_count);
+ txtCount.setText(String.format(mCountStr, imageList.size()));
+ mContainer.addView(labelView);
+
+ // tapping anywhere opens the first image
+ mView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ EnumSet<PhotoViewerOption> options = EnumSet.of(PhotoViewerOption.IS_GALLERY_IMAGE);
+ if (isPrivate) {
+ options.add(PhotoViewerOption.IS_PRIVATE_IMAGE);
+ }
+ ReaderActivityLauncher.showReaderPhotoViewer(
+ view.getContext(),
+ imageList.get(0),
+ content,
+ view,
+ options,
+ 0,
+ 0);
+ }
+ });
+
+ if (mView.getVisibility() != View.VISIBLE) {
+ AniUtils.fadeIn(mView, AniUtils.Duration.SHORT);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java
new file mode 100644
index 000000000..d6358f997
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java
@@ -0,0 +1,368 @@
+package org.wordpress.android.ui.reader.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.CookieManager;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPRestClient;
+import org.wordpress.android.util.WPUrlUtils;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/*
+ * WebView descendant used by ReaderPostDetailFragment - handles
+ * displaying fullscreen video and detecting url/image clicks
+ */
+public class ReaderWebView extends WebView {
+
+ public interface ReaderWebViewUrlClickListener {
+ @SuppressWarnings("SameReturnValue")
+ boolean onUrlClick(String url);
+ boolean onImageUrlClick(String imageUrl, View view, int x, int y);
+ }
+
+ public interface ReaderCustomViewListener {
+ void onCustomViewShown();
+ void onCustomViewHidden();
+ ViewGroup onRequestCustomView();
+ ViewGroup onRequestContentView();
+ }
+
+ public interface ReaderWebViewPageFinishedListener {
+ void onPageFinished(WebView view, String url);
+ }
+
+ private ReaderWebChromeClient mReaderChromeClient;
+ private ReaderCustomViewListener mCustomViewListener;
+ private ReaderWebViewUrlClickListener mUrlClickListener;
+ private ReaderWebViewPageFinishedListener mPageFinishedListener;
+
+ private static String mToken;
+ private static boolean mIsPrivatePost;
+ private static boolean mBlogSchemeIsHttps;
+
+ private boolean mIsDestroyed;
+
+ public ReaderWebView(Context context) {
+ super(context);
+
+ init();
+ }
+
+ @Override
+ public void destroy() {
+ mIsDestroyed = true;
+ super.destroy();
+ }
+
+ public boolean isDestroyed() {
+ return mIsDestroyed;
+ }
+
+ public ReaderWebView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init();
+ }
+
+ public ReaderWebView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ init();
+ }
+
+ @SuppressLint("NewApi")
+ private void init() {
+ if (!isInEditMode()) {
+ mToken = AccountHelper.getDefaultAccount().getAccessToken();
+
+ mReaderChromeClient = new ReaderWebChromeClient(this);
+ this.setWebChromeClient(mReaderChromeClient);
+ this.setWebViewClient(new ReaderWebViewClient(this));
+ this.getSettings().setUserAgentString(WordPress.getUserAgent());
+
+ // Adjust content font size on APIs 19 and below as those do not do it automatically.
+ // If fontScale is close to 1, just let it be 1.
+ final float fontScale = getResources().getConfiguration().fontScale;
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT && ((int) (fontScale * 10000)) != 10000) {
+
+ this.getSettings().setDefaultFontSize((int) (this.getSettings().getDefaultFontSize() * fontScale));
+ this.getSettings().setDefaultFixedFontSize(
+ (int) (this.getSettings().getDefaultFixedFontSize() * fontScale));
+ }
+
+ // Lollipop disables third-party cookies by default, but we need them in order
+ // to support authenticated images
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ CookieManager.getInstance().setAcceptThirdPartyCookies(this, true);
+ }
+ }
+ }
+
+ public void clearContent() {
+ loadUrl("about:blank");
+ }
+
+ private ReaderWebViewUrlClickListener getUrlClickListener() {
+ return mUrlClickListener;
+ }
+
+ public void setUrlClickListener(ReaderWebViewUrlClickListener listener) {
+ mUrlClickListener = listener;
+ }
+
+ private boolean hasUrlClickListener() {
+ return (mUrlClickListener != null);
+ }
+
+ private ReaderWebViewPageFinishedListener getPageFinishedListener() {
+ return mPageFinishedListener;
+ }
+
+ public void setPageFinishedListener(ReaderWebViewPageFinishedListener listener) {
+ mPageFinishedListener = listener;
+ }
+
+ private boolean hasPageFinishedListener() {
+ return (mPageFinishedListener != null);
+ }
+
+ public void setCustomViewListener(ReaderCustomViewListener listener) {
+ mCustomViewListener = listener;
+ }
+
+ private boolean hasCustomViewListener() {
+ return (mCustomViewListener != null);
+ }
+
+ private ReaderCustomViewListener getCustomViewListener() {
+ return mCustomViewListener;
+ }
+
+ public void setIsPrivatePost(boolean isPrivatePost) {
+ mIsPrivatePost = isPrivatePost;
+ }
+
+ public void setBlogSchemeIsHttps(boolean blogSchemeIsHttps) {
+ mBlogSchemeIsHttps = blogSchemeIsHttps;
+ }
+
+ private static boolean isValidClickedUrl(String url) {
+ // only return true for http(s) urls so we avoid file: and data: clicks
+ return (url != null && (url.startsWith("http") || url.startsWith("wordpress:")));
+ }
+
+ public boolean isCustomViewShowing() {
+ return mReaderChromeClient.isCustomViewShowing();
+ }
+
+ public void hideCustomView() {
+ if (isCustomViewShowing()) {
+ mReaderChromeClient.onHideCustomView();
+ }
+ }
+
+ /*
+ * detect when a link is tapped
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP && mUrlClickListener != null) {
+ HitTestResult hr = getHitTestResult();
+ if (hr != null && isValidClickedUrl(hr.getExtra())) {
+ if (UrlUtils.isImageUrl(hr.getExtra())) {
+ return mUrlClickListener.onImageUrlClick(
+ hr.getExtra(),
+ this,
+ (int) event.getX(),
+ (int) event.getY());
+ } else {
+ return mUrlClickListener.onUrlClick(hr.getExtra());
+ }
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private static class ReaderWebViewClient extends WebViewClient {
+ private final ReaderWebView mReaderWebView;
+
+ ReaderWebViewClient(ReaderWebView readerWebView) {
+ if (readerWebView == null) {
+ throw new IllegalArgumentException("ReaderWebViewClient requires readerWebView");
+ }
+ mReaderWebView = readerWebView;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ if (mReaderWebView.hasPageFinishedListener()) {
+ mReaderWebView.getPageFinishedListener().onPageFinished(view, url);
+ }
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ // fire the url click listener, but only do this when webView has
+ // loaded (is visible) - have seen some posts containing iframes
+ // automatically try to open urls (without being clicked)
+ // before the page has loaded
+ return view.getVisibility() == View.VISIBLE
+ && mReaderWebView.hasUrlClickListener()
+ && isValidClickedUrl(url)
+ && mReaderWebView.getUrlClickListener().onUrlClick(url);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ URL imageUrl = null;
+ if (mIsPrivatePost && mBlogSchemeIsHttps && UrlUtils.isImageUrl(url)) {
+ try {
+ imageUrl = new URL(UrlUtils.makeHttps(url));
+ } catch (MalformedURLException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+ // Intercept requests for private images and add the WP.com authorization header
+ if (imageUrl != null && WPUrlUtils.safeToAddWordPressComAuthToken(imageUrl) &&
+ !TextUtils.isEmpty(mToken)) {
+ try {
+ HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();
+ conn.setRequestProperty("Authorization", "Bearer " + mToken);
+ conn.setReadTimeout(WPRestClient.REST_TIMEOUT_MS);
+ conn.setConnectTimeout(WPRestClient.REST_TIMEOUT_MS);
+ conn.setRequestProperty("User-Agent", WordPress.getUserAgent());
+ conn.setRequestProperty("Connection", "Keep-Alive");
+ return new WebResourceResponse(conn.getContentType(),
+ conn.getContentEncoding(),
+ conn.getInputStream());
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+
+ return super.shouldInterceptRequest(view, url);
+ }
+ }
+
+ private static class ReaderWebChromeClient extends WebChromeClient {
+ private final ReaderWebView mReaderWebView;
+ private View mCustomView;
+ private CustomViewCallback mCustomViewCallback;
+
+ ReaderWebChromeClient(ReaderWebView readerWebView) {
+ if (readerWebView == null) {
+ throw new IllegalArgumentException("ReaderWebChromeClient requires readerWebView");
+ }
+ mReaderWebView = readerWebView;
+ }
+
+ /*
+ * request the view that will host the fullscreen video
+ */
+ private ViewGroup getTargetView() {
+ if (mReaderWebView.hasCustomViewListener()) {
+ return mReaderWebView.getCustomViewListener().onRequestCustomView();
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * request the view that should be hidden when showing fullscreen video
+ */
+ private ViewGroup getContentView() {
+ if (mReaderWebView.hasCustomViewListener()) {
+ return mReaderWebView.getCustomViewListener().onRequestContentView();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onShowCustomView(View view, CustomViewCallback callback) {
+ AppLog.i(AppLog.T.READER, "onShowCustomView");
+
+ if (mCustomView != null) {
+ AppLog.w(AppLog.T.READER, "customView already showing");
+ onHideCustomView();
+ return;
+ }
+
+ // hide the post detail content
+ ViewGroup contentView = getContentView();
+ if (contentView != null) {
+ contentView.setVisibility(View.INVISIBLE);
+ }
+
+ // show the full screen view
+ ViewGroup targetView = getTargetView();
+ if (targetView != null) {
+ targetView.addView(view);
+ targetView.setVisibility(View.VISIBLE);
+ }
+
+ if (mReaderWebView.hasCustomViewListener()) {
+ mReaderWebView.getCustomViewListener().onCustomViewShown();
+ }
+
+ mCustomView = view;
+ mCustomViewCallback = callback;
+ }
+
+ @Override
+ public void onHideCustomView() {
+ AppLog.i(AppLog.T.READER, "onHideCustomView");
+
+ if (mCustomView == null) {
+ AppLog.w(AppLog.T.READER, "customView does not exist");
+ return;
+ }
+
+ // hide the target view
+ ViewGroup targetView = getTargetView();
+ if (targetView != null) {
+ targetView.removeView(mCustomView);
+ targetView.setVisibility(View.GONE);
+ }
+
+ // redisplay the post detail content
+ ViewGroup contentView = getContentView();
+ if (contentView != null) {
+ contentView.setVisibility(View.VISIBLE);
+ }
+
+ if (mCustomViewCallback != null) {
+ mCustomViewCallback.onCustomViewHidden();
+ }
+ if (mReaderWebView.hasCustomViewListener()) {
+ mReaderWebView.getCustomViewListener().onCustomViewHidden();
+ }
+
+ mCustomView = null;
+ mCustomViewCallback = null;
+ }
+
+ boolean isCustomViewShowing() {
+ return (mCustomView != null);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java
new file mode 100644
index 000000000..cb6c20cd5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/FollowHelper.java
@@ -0,0 +1,118 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.PopupMenu;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+import java.lang.ref.WeakReference;
+
+class FollowHelper {
+
+ private final WeakReference<Activity> mActivityRef;
+
+ public FollowHelper(Activity activity) {
+ mActivityRef = new WeakReference<>(activity);
+ }
+
+
+ public void showPopup(View anchor, final FollowDataModel followData) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ final String workingText = followData.getFollowingText();
+ final String followText = followData.getFollowText();
+ final String unfollowText = followData.getFollowingHoverText();
+
+ final PopupMenu popup = new PopupMenu(mActivityRef.get(), anchor);
+ final MenuItem menuItem;
+
+ if (followData.isRestCallInProgress) {
+ menuItem = popup.getMenu().add(workingText);
+ } else {
+ menuItem = popup.getMenu().add(followData.isFollowing() ? unfollowText : followText);
+ }
+
+ menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ item.setTitle(workingText);
+ item.setOnMenuItemClickListener(null);
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtils();
+ final String restPath;
+ if (!followData.isFollowing()) {
+ restPath = String.format("/sites/%s/follows/new", followData.getSiteID());
+ } else {
+ restPath = String.format("/sites/%s/follows/mine/delete", followData.getSiteID());
+ }
+
+ followData.isRestCallInProgress = true;
+ FollowRestListener vListener = new FollowRestListener(mActivityRef.get(), followData);
+ restClientUtils.post(restPath, vListener, vListener);
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following REST request " + restPath);
+ return true;
+ }
+ });
+
+ popup.show();
+
+ }
+
+
+ private class FollowRestListener implements RestRequest.Listener, RestRequest.ErrorListener {
+ private final WeakReference<Activity> mActivityRef;
+ private final FollowDataModel mFollowData;
+
+ public FollowRestListener(Activity activity, final FollowDataModel followData) {
+ this.mActivityRef = new WeakReference<>(activity);
+ this.mFollowData = followData;
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mFollowData.isRestCallInProgress = false;
+ if (response!= null) {
+ try {
+ boolean isFollowing = response.getBoolean("is_following");
+ mFollowData.setIsFollowing(isFollowing);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ if (volleyError != null) {
+ AppLog.e(AppLog.T.STATS, "Error while following a blog "
+ + volleyError.getMessage(), volleyError);
+ }
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mFollowData.isRestCallInProgress = false;
+ ToastUtils.showToast(mActivityRef.get(),
+ mActivityRef.get().getString(R.string.reader_toast_err_follow_blog),
+ ToastUtils.Duration.LONG);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java
new file mode 100644
index 000000000..f601cdb38
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/NestedScrollViewExt.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.support.v4.widget.NestedScrollView;
+import android.util.AttributeSet;
+
+public class NestedScrollViewExt extends NestedScrollView {
+ private ScrollViewListener mScrollViewListener = null;
+ public NestedScrollViewExt(Context context) {
+ super(context);
+ }
+
+ public NestedScrollViewExt(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public NestedScrollViewExt(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setScrollViewListener(ScrollViewListener scrollViewListener) {
+ this.mScrollViewListener = scrollViewListener;
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (mScrollViewListener != null) {
+ mScrollViewListener.onScrollChanged(this, l, t, oldl, oldt);
+ }
+ }
+
+ public interface ScrollViewListener {
+ void onScrollChanged(NestedScrollViewExt scrollView,
+ int x, int y, int oldx, int oldy);
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java
new file mode 100644
index 000000000..c8738cd9b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/ReferrerSpamHelper.java
@@ -0,0 +1,159 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.PopupMenu;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.ui.stats.models.ReferrerGroupModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.lang.ref.WeakReference;
+
+class ReferrerSpamHelper {
+
+ private final WeakReference<Activity> mActivityRef;
+
+ public ReferrerSpamHelper(Activity activity) {
+ mActivityRef = new WeakReference<>(activity);
+ }
+
+ // return the domain of the passed ReferrerGroupModel or null.
+ private static String getDomain(ReferrerGroupModel group) {
+ // Use the URL value given in the JSON response, or use the groupID that doesn't contain the schema.
+ final String spamDomain = group.getUrl() != null ? group.getUrl() : "http://" + group.getGroupId();
+ return UrlUtils.isValidUrlAndHostNotNull(spamDomain) ? UrlUtils.getHost(spamDomain) : null;
+ }
+
+ public static boolean isSpamActionAvailable(ReferrerGroupModel group) {
+ String domain = getDomain(group);
+ return !TextUtils.isEmpty(domain) && !domain.equals("wordpress.com");
+ }
+
+ public void showPopup(View anchor, final ReferrerGroupModel referrerGroup) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ final PopupMenu popup = new PopupMenu(mActivityRef.get(), anchor);
+ final MenuItem menuItem;
+
+ if (referrerGroup.isRestCallInProgress) {
+ menuItem = popup.getMenu().add(
+ referrerGroup.isMarkedAsSpam ?
+ mActivityRef.get().getString(R.string.stats_referrers_marking_not_spam) :
+ mActivityRef.get().getString(R.string.stats_referrers_marking_spam)
+ );
+ } else {
+ menuItem = popup.getMenu().add(
+ referrerGroup.isMarkedAsSpam ?
+ mActivityRef.get().getString(R.string.stats_referrers_unspam) :
+ mActivityRef.get().getString(R.string.stats_referrers_spam)
+ );
+ }
+
+ menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ item.setTitle(
+ referrerGroup.isMarkedAsSpam ?
+ mActivityRef.get().getString(R.string.stats_referrers_marking_not_spam) :
+ mActivityRef.get().getString(R.string.stats_referrers_marking_spam)
+ );
+ item.setOnMenuItemClickListener(null);
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtilsV1_1();
+ final String restPath;
+ final boolean isMarkingAsSpamInProgress;
+ if (referrerGroup.isMarkedAsSpam) {
+ restPath = String.format("/sites/%s/stats/referrers/spam/delete/?domain=%s", referrerGroup.getBlogId(), getDomain(referrerGroup));
+ isMarkingAsSpamInProgress = false;
+ } else {
+ restPath = String.format("/sites/%s/stats/referrers/spam/new/?domain=%s", referrerGroup.getBlogId(), getDomain(referrerGroup));
+ isMarkingAsSpamInProgress = true;
+ }
+
+ referrerGroup.isRestCallInProgress = true;
+ ReferrerSpamRestListener vListener = new ReferrerSpamRestListener(mActivityRef.get(), referrerGroup, isMarkingAsSpamInProgress);
+ restClientUtils.post(restPath, vListener, vListener);
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following REST request " + restPath);
+ return true;
+ }
+ });
+
+ popup.show();
+ }
+
+
+ private class ReferrerSpamRestListener implements RestRequest.Listener, RestRequest.ErrorListener {
+ private final WeakReference<Activity> mActivityRef;
+ private final ReferrerGroupModel mReferrerGroup;
+ private final boolean isMarkingAsSpamInProgress;
+
+ public ReferrerSpamRestListener(Activity activity, final ReferrerGroupModel referrerGroup, final boolean isMarkingAsSpamInProgress) {
+ this.mActivityRef = new WeakReference<>(activity);
+ this.mReferrerGroup = referrerGroup;
+ this.isMarkingAsSpamInProgress = isMarkingAsSpamInProgress;
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mReferrerGroup.isRestCallInProgress = false;
+ if (response!= null) {
+ boolean success = response.optBoolean("success");
+ if (success) {
+ mReferrerGroup.isMarkedAsSpam = isMarkingAsSpamInProgress;
+ int localBlogID = StatsUtils.getLocalBlogIdFromRemoteBlogId(
+ Integer.parseInt(mReferrerGroup.getBlogId())
+ );
+ StatsTable.deleteStatsForBlog(mActivityRef.get(), localBlogID, StatsService.StatsEndpointsEnum.REFERRERS);
+ } else {
+ // It's not a success. Something went wrong on the server
+ String errorMessage = null;
+ if (response.has("error")) {
+ errorMessage = response.optString("message");
+ }
+
+ if (TextUtils.isEmpty(errorMessage)) {
+ errorMessage = mActivityRef.get().getString(R.string.stats_referrers_spam_generic_error);
+ }
+
+ ToastUtils.showToast(mActivityRef.get(), errorMessage, ToastUtils.Duration.LONG);
+ }
+ }
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ if (volleyError != null) {
+ AppLog.e(AppLog.T.STATS, "Error while marking the referrer " + getDomain(mReferrerGroup) + " as "
+ + (isMarkingAsSpamInProgress ? " spam " : " unspam ") +
+ volleyError.getMessage(), volleyError);
+ }
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+
+ mReferrerGroup.isRestCallInProgress = false;
+ ToastUtils.showToast(mActivityRef.get(),
+ mActivityRef.get().getString(R.string.stats_referrers_spam_generic_error),
+ ToastUtils.Duration.LONG);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java
new file mode 100644
index 000000000..4b734adef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/ScrollViewExt.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+public class ScrollViewExt extends ScrollView {
+ private ScrollViewListener mScrollViewListener = null;
+ public ScrollViewExt(Context context) {
+ super(context);
+ }
+
+ public ScrollViewExt(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public ScrollViewExt(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setScrollViewListener(ScrollViewListener scrollViewListener) {
+ this.mScrollViewListener = scrollViewListener;
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (mScrollViewListener != null) {
+ mScrollViewListener.onScrollChanged(this, l, t, oldl, oldt);
+ }
+ }
+
+ public interface ScrollViewListener {
+ void onScrollChanged(ScrollViewExt scrollView,
+ int x, int y, int oldx, int oldy);
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java
new file mode 100644
index 000000000..1503a3631
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/SparseBooleanArrayParcelable.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseBooleanArray;
+
+public class SparseBooleanArrayParcelable extends SparseBooleanArray implements Parcelable {
+ public static Parcelable.Creator<SparseBooleanArrayParcelable> CREATOR = new Parcelable.Creator<SparseBooleanArrayParcelable>() {
+ @Override
+ public SparseBooleanArrayParcelable createFromParcel(Parcel source) {
+ SparseBooleanArrayParcelable read = new SparseBooleanArrayParcelable();
+ int size = source.readInt();
+
+ int[] keys = new int[size];
+ boolean[] values = new boolean[size];
+
+ source.readIntArray(keys);
+ source.readBooleanArray(values);
+
+ for (int i = 0; i < size; i++) {
+ read.put(keys[i], values[i]);
+ }
+
+ return read;
+ }
+
+ @Override
+ public SparseBooleanArrayParcelable[] newArray(int size) {
+ return new SparseBooleanArrayParcelable[size];
+ }
+ };
+
+ public SparseBooleanArrayParcelable() {
+
+ }
+
+ public SparseBooleanArrayParcelable(SparseBooleanArray sparseBooleanArray) {
+ for (int i = 0; i < sparseBooleanArray.size(); i++) {
+ this.put(sparseBooleanArray.keyAt(i), sparseBooleanArray.valueAt(i));
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ int[] keys = new int[size()];
+ boolean[] values = new boolean[size()];
+
+ for (int i = 0; i < size(); i++) {
+ keys[i] = keyAt(i);
+ values[i] = valueAt(i);
+ }
+
+ dest.writeInt(size());
+ dest.writeIntArray(keys);
+ dest.writeBooleanArray(values);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java
new file mode 100644
index 000000000..a6d761b9c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractFragment.java
@@ -0,0 +1,361 @@
+package org.wordpress.android.ui.stats;
+
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.volley.NoConnectionError;
+import com.android.volley.VolleyError;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+
+import de.greenrobot.event.EventBus;
+
+
+public abstract class StatsAbstractFragment extends Fragment {
+ public static final String TAG = StatsAbstractFragment.class.getSimpleName();
+
+ public static final String ARGS_VIEW_TYPE = "ARGS_VIEW_TYPE";
+ public static final String ARGS_TIMEFRAME = "ARGS_TIMEFRAME";
+ public static final String ARGS_SELECTED_DATE = "ARGS_SELECTED_DATE";
+ static final String ARG_REST_RESPONSE = "ARG_REST_RESPONSE";
+ static final String ARGS_IS_SINGLE_VIEW = "ARGS_IS_SINGLE_VIEW";
+
+ // The number of results to return for NON Paged REST endpoints.
+ private static final int MAX_RESULTS_REQUESTED = 100;
+
+ private String mDate;
+ private StatsTimeframe mStatsTimeframe = StatsTimeframe.DAY;
+
+ protected abstract StatsService.StatsEndpointsEnum[] sectionsToUpdate();
+ protected abstract void showPlaceholderUI();
+ protected abstract void updateUI();
+ protected abstract void showErrorUI(String label);
+
+ /**
+ * Wheter or not previous data is available.
+ * @return True if previous data is already available in the fragment
+ */
+ protected abstract boolean hasDataAvailable();
+
+ /**
+ * Called in onSaveIstance. Fragments should persist data here.
+ * @param outState Bundle in which to place fragment saved state.
+ */
+ protected abstract void saveStatsData(Bundle outState);
+
+ /**
+ * Called in OnCreate. Fragment should restore here previous saved data.
+ * @param savedInstanceState If the fragment is being re-created from a previous saved state, this is the state.
+ */
+ protected abstract void restoreStatsData(Bundle savedInstanceState); // called in onCreate
+
+ protected StatsResourceVars mResourceVars;
+
+ public void refreshStats() {
+ refreshStats(-1, null);
+ }
+ // call an update for the stats shown in the fragment
+ void refreshStats(int pageNumberRequested, StatsService.StatsEndpointsEnum[] sections) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // if no sections to update is passed to the method, default to fragment
+ if (sections == null) {
+ sections = sectionsToUpdate();
+ }
+
+ //AppLog.d(AppLog.T.STATS, this.getClass().getCanonicalName() + " > refreshStats");
+
+ final Blog currentBlog = WordPress.getBlog(getLocalTableBlogID());
+ if (currentBlog == null) {
+ AppLog.w(AppLog.T.STATS, "Current blog is null. This should never happen here.");
+ return;
+ }
+
+ final String blogId = currentBlog.getDotComBlogId();
+ // Make sure the blogId is available.
+ if (blogId == null) {
+ AppLog.e(AppLog.T.STATS, "remote blogID is null: " + currentBlog.getHomeURL());
+ return;
+ }
+
+ // Check credentials for jetpack blogs first
+ if (!currentBlog.isDotcomFlag()
+ && !currentBlog.hasValidJetpackCredentials() && !AccountHelper.isSignedInWordPressDotCom()) {
+ AppLog.w(AppLog.T.STATS, "Current blog is a Jetpack blog without valid .com credentials stored");
+ return;
+ }
+
+ // Do not pass the array of StatsEndpointsEnum to the Service. Otherwise we get
+ // java.lang.RuntimeException: Unable to start service org.wordpress.android.ui.stats.service.StatsService
+ // with Intent { cmp=org.wordpress.android/.ui.stats.service.StatsService (has extras) }: java.lang.ClassCastException:
+ // java.lang.Object[] cannot be cast to org.wordpress.android.ui.stats.service.StatsService$StatsEndpointsEnum[]
+ // on older devices.
+ // We should use Enumset, or array of int. Going for the latter, since we have an array and cannot create an Enumset easily.
+ int[] sectionsForTheService = new int[sections.length];
+ for (int i=0; i < sections.length; i++){
+ sectionsForTheService[i] = sections[i].ordinal();
+ }
+
+ // start service to get stats
+ Intent intent = new Intent(getActivity(), StatsService.class);
+ intent.putExtra(StatsService.ARG_BLOG_ID, blogId);
+ intent.putExtra(StatsService.ARG_PERIOD, mStatsTimeframe);
+ intent.putExtra(StatsService.ARG_DATE, mDate);
+ if (isSingleView()) {
+ // Single Item screen: request 20 items per page on paged requests. Default to the first 100 items otherwise.
+ int maxElementsToRetrieve = pageNumberRequested > 0 ? StatsService.MAX_RESULTS_REQUESTED_PER_PAGE : MAX_RESULTS_REQUESTED;
+ intent.putExtra(StatsService.ARG_MAX_RESULTS, maxElementsToRetrieve);
+ }
+ if (pageNumberRequested > 0) {
+ intent.putExtra(StatsService.ARG_PAGE_REQUESTED, pageNumberRequested);
+ }
+ intent.putExtra(StatsService.ARG_SECTION, sectionsForTheService);
+ getActivity().startService(intent);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // AppLog.d(AppLog.T.STATS, this.getClass().getCanonicalName() + " > onCreate");
+
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(ARGS_TIMEFRAME)) {
+ mStatsTimeframe = (StatsTimeframe) savedInstanceState.getSerializable(ARGS_TIMEFRAME);
+ }
+ if (savedInstanceState.containsKey(ARGS_SELECTED_DATE)) {
+ mDate = savedInstanceState.getString(ARGS_SELECTED_DATE);
+ }
+ restoreStatsData(savedInstanceState); // Each fragment will override this to restore fragment dependant data
+ }
+
+ // AppLog.d(AppLog.T.STATS, "mStatsTimeframe: " + mStatsTimeframe.getLabel());
+ // AppLog.d(AppLog.T.STATS, "mDate: " + mDate);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mResourceVars = new StatsResourceVars(activity);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ /* AppLog.d(AppLog.T.STATS, this.getClass().getCanonicalName() + " > saving instance state");
+ AppLog.d(AppLog.T.STATS, "mStatsTimeframe: " + mStatsTimeframe.getLabel());
+ AppLog.d(AppLog.T.STATS, "mDate: " + mDate); */
+
+ outState.putString(ARGS_SELECTED_DATE, mDate);
+ outState.putSerializable(ARGS_TIMEFRAME, mStatsTimeframe);
+ saveStatsData(outState); // Each fragment will override this
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Init the UI
+ if (hasDataAvailable()) {
+ updateUI();
+ } else {
+ showPlaceholderUI();
+ refreshStats();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ public boolean shouldUpdateFragmentOnUpdateEvent(StatsEvents.SectionUpdatedAbstract event) {
+ if (!isAdded()) {
+ return false;
+ }
+
+ if (!getDate().equals(event.mDate)) {
+ return false;
+ }
+
+ if (!isSameBlog(event)) {
+ return false;
+ }
+
+ if (!event.mTimeframe.equals(getTimeframe())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ boolean isSameBlog(StatsEvents.SectionUpdatedAbstract event) {
+ final Blog currentBlog = WordPress.getBlog(getLocalTableBlogID());
+ if (currentBlog != null && currentBlog.getDotComBlogId() != null) {
+ return event.mRequestBlogId.equals(currentBlog.getDotComBlogId());
+ }
+ return false;
+ }
+
+ protected void showErrorUI(VolleyError error) {
+ if (!isAdded()) {
+ return;
+ }
+
+ String label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+
+ if (error instanceof NoConnectionError) {
+ label += "<br/>" + getString(R.string.no_network_message);
+ }
+
+ if (StatsUtils.isRESTDisabledError(error)) {
+ label += "<br/>" + getString(R.string.stats_enable_rest_api_in_jetpack);
+ }
+
+ showErrorUI(label);
+ }
+
+ protected void showErrorUI() {
+ String label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+ showErrorUI(label);
+ }
+
+ public boolean shouldUpdateFragmentOnErrorEvent(StatsEvents.SectionUpdateError errorEvent) {
+ if (!shouldUpdateFragmentOnUpdateEvent(errorEvent)) {
+ return false;
+ }
+
+ StatsService.StatsEndpointsEnum sectionToUpdate = errorEvent.mEndPointName;
+ StatsService.StatsEndpointsEnum[] sectionsToUpdate = sectionsToUpdate();
+
+ for (int i = 0; i < sectionsToUpdate().length; i++) {
+ if (sectionToUpdate == sectionsToUpdate[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static StatsAbstractFragment newVisitorsAndViewsInstance(StatsViewType viewType, int localTableBlogID,
+ StatsTimeframe timeframe, String date, StatsVisitorsAndViewsFragment.OverviewLabel itemToSelect) {
+ StatsVisitorsAndViewsFragment fragment = (StatsVisitorsAndViewsFragment) newInstance(viewType, localTableBlogID, timeframe, date);
+ fragment.setSelectedOverviewItem(itemToSelect);
+ return fragment;
+ }
+
+ public static StatsAbstractFragment newInstance(StatsViewType viewType, int localTableBlogID,
+ StatsTimeframe timeframe, String date ) {
+ StatsAbstractFragment fragment = null;
+
+ switch (viewType) {
+ //case TIMEFRAME_SELECTOR:
+ // fragment = new StatsDateSelectorFragment();
+ // break;
+ case GRAPH_AND_SUMMARY:
+ fragment = new StatsVisitorsAndViewsFragment();
+ break;
+ case TOP_POSTS_AND_PAGES:
+ fragment = new StatsTopPostsAndPagesFragment();
+ break;
+ case REFERRERS:
+ fragment = new StatsReferrersFragment();
+ break;
+ case CLICKS:
+ fragment = new StatsClicksFragment();
+ break;
+ case GEOVIEWS:
+ fragment = new StatsGeoviewsFragment();
+ break;
+ case AUTHORS:
+ fragment = new StatsAuthorsFragment();
+ break;
+ case VIDEO_PLAYS:
+ fragment = new StatsVideoplaysFragment();
+ break;
+ case COMMENTS:
+ fragment = new StatsCommentsFragment();
+ break;
+ case TAGS_AND_CATEGORIES:
+ fragment = new StatsTagsAndCategoriesFragment();
+ break;
+ case PUBLICIZE:
+ fragment = new StatsPublicizeFragment();
+ break;
+ case FOLLOWERS:
+ fragment = new StatsFollowersFragment();
+ break;
+ case SEARCH_TERMS:
+ fragment = new StatsSearchTermsFragment();
+ break;
+ case INSIGHTS_MOST_POPULAR:
+ fragment = new StatsInsightsMostPopularFragment();
+ break;
+ case INSIGHTS_ALL_TIME:
+ fragment = new StatsInsightsAllTimeFragment();
+ break;
+ case INSIGHTS_TODAY:
+ fragment = new StatsInsightsTodayFragment();
+ break;
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ fragment = new StatsInsightsLatestPostSummaryFragment();
+ break;
+ }
+
+ fragment.setTimeframe(timeframe);
+ fragment.setDate(date);
+
+ Bundle args = new Bundle();
+ args.putSerializable(ARGS_VIEW_TYPE, viewType);
+ args.putInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, localTableBlogID);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ public void setDate(String newDate) {
+ mDate = newDate;
+ }
+
+ String getDate() {
+ return mDate;
+ }
+
+ public void setTimeframe(StatsTimeframe newTimeframe) {
+ mStatsTimeframe = newTimeframe;
+ }
+
+ StatsTimeframe getTimeframe() {
+ return mStatsTimeframe;
+ }
+
+ StatsViewType getViewType() {
+ return (StatsViewType) getArguments().getSerializable(ARGS_VIEW_TYPE);
+ }
+
+ int getLocalTableBlogID() {
+ return getArguments().getInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID);
+ }
+
+ boolean isSingleView() {
+ return getArguments().getBoolean(ARGS_IS_SINGLE_VIEW, false);
+ }
+
+ protected abstract String getTitle();
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java
new file mode 100644
index 000000000..8f24efd8f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractInsightsFragment.java
@@ -0,0 +1,85 @@
+package org.wordpress.android.ui.stats;
+
+
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+
+public abstract class StatsAbstractInsightsFragment extends StatsAbstractFragment {
+ public static final String TAG = StatsAbstractInsightsFragment.class.getSimpleName();
+
+ private TextView mErrorLabel;
+ private LinearLayout mEmptyModulePlaceholder;
+ LinearLayout mResultContainer;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.stats_insights_generic_fragment, container, false);
+ TextView moduleTitleTextView = (TextView) view.findViewById(R.id.stats_module_title);
+ moduleTitleTextView.setText(getTitle());
+
+ mEmptyModulePlaceholder = (LinearLayout) view.findViewById(R.id.stats_empty_module_placeholder);
+ mResultContainer = (LinearLayout) view.findViewById(R.id.stats_module_result_container);
+ mErrorLabel = (TextView) view.findViewById(R.id.stats_error_text);
+ return view;
+ }
+
+ @Override
+ protected void showPlaceholderUI() {
+ mErrorLabel.setVisibility(View.GONE);
+ mResultContainer.setVisibility(View.GONE);
+ mEmptyModulePlaceholder.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ protected void showErrorUI(String label) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Use the generic error message when the string passed to this method is null.
+ if (TextUtils.isEmpty(label)) {
+ label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+ }
+
+ if (label.contains("<")) {
+ mErrorLabel.setText(Html.fromHtml(label));
+ } else {
+ mErrorLabel.setText(label);
+ }
+ mErrorLabel.setVisibility(View.VISIBLE);
+ mResultContainer.setVisibility(View.GONE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+ }
+
+ /**
+ * Insights module all have the same basic implementation of updateUI. Let's provide a common code here.
+ */
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Another check that the data is available. At this point it should be available.
+ if (!hasDataAvailable()) {
+ showErrorUI();
+ return;
+ }
+
+ // not an error - update the module UI here
+ mErrorLabel.setVisibility(View.GONE);
+ mResultContainer.setVisibility(View.VISIBLE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+
+ mResultContainer.removeAllViews();
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java
new file mode 100644
index 000000000..d78e1f4c4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAbstractListFragment.java
@@ -0,0 +1,297 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.SparseBooleanArray;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CheckedTextView;
+import android.widget.LinearLayout;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.widgets.TypefaceCache;
+
+public abstract class StatsAbstractListFragment extends StatsAbstractFragment {
+
+ // Used when the fragment has 2 pages/kind of stats in it. Not meaning the bottom pagination.
+ static final String ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX = "ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX";
+ private static final String ARGS_EXPANDED_ROWS = "ARGS_EXPANDED_ROWS";
+ private static final int MAX_NUM_OF_ITEMS_DISPLAYED_IN_SINGLE_VIEW_LIST = 1000;
+ static final int MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST = 10;
+
+ private static final int NO_STRING_ID = -1;
+
+ private TextView mModuleTitleTextView;
+ private TextView mEmptyLabel;
+ TextView mTotalsLabel;
+ private LinearLayout mListContainer;
+ LinearLayout mList;
+ private Button mViewAll;
+
+ LinearLayout mTopPagerContainer;
+ int mTopPagerSelectedButtonIndex = 0;
+
+ // Bottom and Top Pagination for modules that has pagination enabled.
+ LinearLayout mBottomPaginationContainer;
+ Button mBottomPaginationGoBackButton;
+ Button mBottomPaginationGoForwardButton;
+ TextView mBottomPaginationText;
+ LinearLayout mTopPaginationContainer;
+ Button mTopPaginationGoBackButton;
+ Button mTopPaginationGoForwardButton;
+ TextView mTopPaginationText;
+
+ private LinearLayout mEmptyModulePlaceholder;
+
+ SparseBooleanArray mGroupIdToExpandedMap;
+
+ protected abstract int getEntryLabelResId();
+ protected abstract int getTotalsLabelResId();
+ protected abstract int getEmptyLabelTitleResId();
+ protected abstract int getEmptyLabelDescResId();
+ protected abstract boolean isExpandableList();
+ protected abstract boolean isViewAllOptionAvailable();
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view;
+ if (isExpandableList()) {
+ view = inflater.inflate(R.layout.stats_expandable_list_fragment, container, false);
+ } else {
+ view = inflater.inflate(R.layout.stats_list_fragment, container, false);
+ }
+
+ mEmptyModulePlaceholder = (LinearLayout) view.findViewById(R.id.stats_empty_module_placeholder);
+ mModuleTitleTextView = (TextView) view.findViewById(R.id.stats_module_title);
+ mModuleTitleTextView.setText(getTitle());
+
+ TextView entryLabel = (TextView) view.findViewById(R.id.stats_list_entry_label);
+ entryLabel.setText(getEntryLabelResId());
+ TextView totalsLabel = (TextView) view.findViewById(R.id.stats_list_totals_label);
+ totalsLabel.setText(getTotalsLabelResId());
+
+ mEmptyLabel = (TextView) view.findViewById(R.id.stats_list_empty_text);
+ mTotalsLabel = (TextView) view.findViewById(R.id.stats_module_totals_label);
+ mList = (LinearLayout) view.findViewById(R.id.stats_list_linearlayout);
+ mListContainer = (LinearLayout) view.findViewById(R.id.stats_list_container);
+ mViewAll = (Button) view.findViewById(R.id.btnViewAll);
+ mTopPagerContainer = (LinearLayout) view.findViewById(R.id.stats_pager_tabs);
+
+ // Load pagination items
+ mBottomPaginationContainer = (LinearLayout) view.findViewById(R.id.stats_bottom_pagination_container);
+ mBottomPaginationGoBackButton = (Button) mBottomPaginationContainer.findViewById(R.id.stats_pagination_go_back);
+ mBottomPaginationGoForwardButton = (Button) mBottomPaginationContainer.findViewById(R.id.stats_pagination_go_forward);
+ mBottomPaginationText = (TextView) mBottomPaginationContainer.findViewById(R.id.stats_pagination_text);
+ mTopPaginationContainer = (LinearLayout) view.findViewById(R.id.stats_top_pagination_container);
+ mTopPaginationContainer.setBackgroundResource(R.drawable.stats_pagination_item_background);
+ mTopPaginationGoBackButton = (Button) mTopPaginationContainer.findViewById(R.id.stats_pagination_go_back);
+ mTopPaginationGoForwardButton = (Button) mTopPaginationContainer.findViewById(R.id.stats_pagination_go_forward);
+ mTopPaginationText = (TextView) mTopPaginationContainer.findViewById(R.id.stats_pagination_text);
+
+ return view;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mGroupIdToExpandedMap = new SparseBooleanArray();
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(ARGS_EXPANDED_ROWS)) {
+ mGroupIdToExpandedMap = savedInstanceState.getParcelable(ARGS_EXPANDED_ROWS);
+ }
+ mTopPagerSelectedButtonIndex = savedInstanceState.getInt(ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mGroupIdToExpandedMap.size() > 0) {
+ outState.putParcelable(ARGS_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mGroupIdToExpandedMap));
+ }
+ outState.putInt(ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mTopPagerSelectedButtonIndex);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void showPlaceholderUI() {
+ mTopPagerContainer.setVisibility(View.GONE);
+ mEmptyLabel.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.GONE);
+ mList.setVisibility(View.GONE);
+ mViewAll.setVisibility(View.GONE);
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTopPaginationContainer.setVisibility(View.GONE);
+ mEmptyModulePlaceholder.setVisibility(View.VISIBLE);
+ }
+
+ void showHideNoResultsUI(boolean showNoResultsUI) {
+ mModuleTitleTextView.setVisibility(View.VISIBLE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+
+ if (showNoResultsUI) {
+ mGroupIdToExpandedMap.clear();
+ String label;
+ if (getEmptyLabelDescResId() == NO_STRING_ID) {
+ label = "<b>" + getString(getEmptyLabelTitleResId()) + "</b><br/><br/>";
+ } else {
+ label = "<b>" + getString(getEmptyLabelTitleResId()) + "</b><br/><br/>" + getString(getEmptyLabelDescResId());
+ }
+ if (label.contains("<")) {
+ mEmptyLabel.setText(Html.fromHtml(label));
+ } else {
+ mEmptyLabel.setText(label);
+ }
+ mEmptyLabel.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.GONE);
+ mList.setVisibility(View.GONE);
+ mViewAll.setVisibility(View.GONE);
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTopPaginationContainer.setVisibility(View.GONE);
+ } else {
+ mEmptyLabel.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.VISIBLE);
+ mList.setVisibility(View.VISIBLE);
+
+ if (!isSingleView() && isViewAllOptionAvailable()) {
+ // No view all button if already in single view
+ configureViewAllButton();
+ } else {
+ mViewAll.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void showErrorUI(String label) {
+ if (!isAdded()) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mModuleTitleTextView.setVisibility(View.VISIBLE);
+ mEmptyModulePlaceholder.setVisibility(View.GONE);
+
+ // Use the generic error message when the string passed to this method is null.
+ if (TextUtils.isEmpty(label)) {
+ label = "<b>" + getString(R.string.error_refresh_stats) + "</b>";
+ }
+
+ if (label.contains("<")) {
+ mEmptyLabel.setText(Html.fromHtml(label));
+ } else {
+ mEmptyLabel.setText(label);
+ }
+ mEmptyLabel.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.GONE);
+ mList.setVisibility(View.GONE);
+ }
+
+ private void configureViewAllButton() {
+ if (isSingleView()) {
+ // No view all button if you're already in single view
+ mViewAll.setVisibility(View.GONE);
+ return;
+ }
+ mViewAll.setVisibility(View.VISIBLE);
+ mViewAll.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (isSingleView()) {
+ return; // already in single view
+ }
+
+ if (!hasDataAvailable()) {
+ return;
+ }
+
+ Intent viewAllIntent = new Intent(getActivity(), StatsViewAllActivity.class);
+ viewAllIntent.putExtra(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, getLocalTableBlogID());
+ viewAllIntent.putExtra(StatsAbstractFragment.ARGS_TIMEFRAME, getTimeframe());
+ viewAllIntent.putExtra(StatsAbstractFragment.ARGS_VIEW_TYPE, getViewType());
+ viewAllIntent.putExtra(StatsAbstractFragment.ARGS_SELECTED_DATE, getDate());
+ viewAllIntent.putExtra(ARGS_IS_SINGLE_VIEW, true);
+ if (mTopPagerContainer.getVisibility() == View.VISIBLE) {
+ viewAllIntent.putExtra(ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mTopPagerSelectedButtonIndex);
+ }
+ //viewAllIntent.putExtra(StatsAbstractFragment.ARG_REST_RESPONSE, mDatamodels[mTopPagerSelectedButtonIndex]);
+ getActivity().startActivity(viewAllIntent);
+ }
+ });
+ }
+
+ int getMaxNumberOfItemsToShowInList() {
+ return isSingleView() ? MAX_NUM_OF_ITEMS_DISPLAYED_IN_SINGLE_VIEW_LIST : MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ void setupTopModulePager(LayoutInflater inflater, ViewGroup container, View view, String[] buttonTitles) {
+ int dp4 = DisplayUtils.dpToPx(view.getContext(), 4);
+ int dp80 = DisplayUtils.dpToPx(view.getContext(), 80);
+
+ for (int i = 0; i < buttonTitles.length; i++) {
+ CheckedTextView rb = (CheckedTextView) inflater.inflate(R.layout.stats_top_module_pager_button, container, false);
+ RadioGroup.LayoutParams params = new RadioGroup.LayoutParams(RadioGroup.LayoutParams.MATCH_PARENT,
+ RadioGroup.LayoutParams.WRAP_CONTENT);
+ params.weight = 1;
+ rb.setTypeface((TypefaceCache.getTypeface(view.getContext())));
+ if (i == 0) {
+ params.setMargins(0, 0, dp4, 0);
+ } else {
+ params.setMargins(dp4, 0, 0, 0);
+ }
+ rb.setMinimumWidth(dp80);
+ rb.setGravity(Gravity.CENTER);
+ rb.setLayoutParams(params);
+ rb.setText(buttonTitles[i]);
+ rb.setChecked(i == mTopPagerSelectedButtonIndex);
+ rb.setOnClickListener(TopModulePagerOnClickListener);
+ mTopPagerContainer.addView(rb);
+ }
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ }
+
+ private final View.OnClickListener TopModulePagerOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ CheckedTextView ctv = (CheckedTextView) v;
+ if (ctv.isChecked()) {
+ // already checked. Do nothing
+ return;
+ }
+
+ int numberOfButtons = mTopPagerContainer.getChildCount();
+ int checkedId = -1;
+ for (int i = 0; i < numberOfButtons; i++) {
+ CheckedTextView currentCheckedTextView = (CheckedTextView)mTopPagerContainer.getChildAt(i);
+ if (ctv == currentCheckedTextView) {
+ checkedId = i;
+ currentCheckedTextView.setChecked(true);
+ } else {
+ currentCheckedTextView.setChecked(false);
+ }
+ }
+
+ if (checkedId == -1)
+ return;
+
+ mTopPagerSelectedButtonIndex = checkedId;
+
+ TextView entryLabel = (TextView) getView().findViewById(R.id.stats_list_entry_label);
+ if (entryLabel != null) {
+ entryLabel.setText(getEntryLabelResId());
+ }
+ updateUI();
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java
new file mode 100644
index 000000000..c3c390e5d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java
@@ -0,0 +1,1034 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.apache.commons.lang.StringUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.ui.posts.PromoDialog;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.RateLimitedTask;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * The native stats activity
+ * <p>
+ * By pressing a spinner on the action bar, the user can select which timeframe they wish to see.
+ * </p>
+ */
+public class StatsActivity extends AppCompatActivity
+ implements NestedScrollViewExt.ScrollViewListener,
+ StatsVisitorsAndViewsFragment.OnDateChangeListener,
+ StatsVisitorsAndViewsFragment.OnOverviewItemChangeListener,
+ StatsInsightsTodayFragment.OnInsightsTodayClickListener {
+
+ private static final String SAVED_WP_LOGIN_STATE = "SAVED_WP_LOGIN_STATE";
+ private static final String SAVED_STATS_TIMEFRAME = "SAVED_STATS_TIMEFRAME";
+ private static final String SAVED_STATS_REQUESTED_DATE = "SAVED_STATS_REQUESTED_DATE";
+ private static final String SAVED_STATS_SCROLL_POSITION = "SAVED_STATS_SCROLL_POSITION";
+ private static final String SAVED_THERE_WAS_AN_ERROR_LOADING_STATS = "SAVED_THERE_WAS_AN_ERROR_LOADING_STATS";
+
+ private Spinner mSpinner;
+ private NestedScrollViewExt mOuterScrollView;
+
+ private static final int REQUEST_JETPACK = 7000;
+ public static final String ARG_LOCAL_TABLE_BLOG_ID = "ARG_LOCAL_TABLE_BLOG_ID";
+ public static final String ARG_LAUNCHED_FROM = "ARG_LAUNCHED_FROM";
+ public static final String ARG_DESIRED_TIMEFRAME = "ARG_DESIRED_TIMEFRAME";
+
+ public enum StatsLaunchedFrom {
+ STATS_WIDGET,
+ NOTIFICATIONS
+ }
+
+ private int mResultCode = -1;
+ private boolean mIsInFront;
+ private int mLocalBlogID = -1;
+ private StatsTimeframe mCurrentTimeframe = StatsTimeframe.INSIGHTS;
+ private String mRequestedDate;
+ private boolean mIsUpdatingStats;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private TimeframeSpinnerAdapter mTimeframeSpinnerAdapter;
+ private final StatsTimeframe[] timeframes = {StatsTimeframe.INSIGHTS, StatsTimeframe.DAY, StatsTimeframe.WEEK,
+ StatsTimeframe.MONTH, StatsTimeframe.YEAR};
+ private StatsVisitorsAndViewsFragment.OverviewLabel mTabToSelectOnGraph = StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS;
+
+ private boolean mThereWasAnErrorLoadingStats = false;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (WordPress.wpDB == null) {
+ Toast.makeText(this, R.string.fatal_db_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.stats_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setElevation(0);
+ actionBar.setTitle(R.string.stats);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+
+ if (mIsUpdatingStats) {
+ AppLog.w(T.STATS, "stats are already updating, refresh cancelled");
+ return;
+ }
+
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ if (checkCredentials()) {
+ updateTimeframeAndDateAndStartRefreshOfFragments(true);
+ }
+ }
+ });
+
+ setTitle(R.string.stats);
+
+ mOuterScrollView = (NestedScrollViewExt) findViewById(R.id.scroll_view_stats);
+ mOuterScrollView.setScrollViewListener(this);
+
+ if (savedInstanceState != null) {
+ mResultCode = savedInstanceState.getInt(SAVED_WP_LOGIN_STATE);
+ mLocalBlogID = savedInstanceState.getInt(ARG_LOCAL_TABLE_BLOG_ID);
+ mCurrentTimeframe = (StatsTimeframe) savedInstanceState.getSerializable(SAVED_STATS_TIMEFRAME);
+ mRequestedDate = savedInstanceState.getString(SAVED_STATS_REQUESTED_DATE);
+ mThereWasAnErrorLoadingStats = savedInstanceState.getBoolean(SAVED_THERE_WAS_AN_ERROR_LOADING_STATS);
+ final int yScrollPosition = savedInstanceState.getInt(SAVED_STATS_SCROLL_POSITION);
+ if(yScrollPosition != 0) {
+ mOuterScrollView.postDelayed(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ mOuterScrollView.scrollTo(0, yScrollPosition);
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ } else if (getIntent() != null) {
+ mLocalBlogID = getIntent().getIntExtra(ARG_LOCAL_TABLE_BLOG_ID, -1);
+ if (getIntent().hasExtra(SAVED_STATS_TIMEFRAME)) {
+ mCurrentTimeframe = (StatsTimeframe) getIntent().getSerializableExtra(SAVED_STATS_TIMEFRAME);
+ } else if (getIntent().hasExtra(ARG_DESIRED_TIMEFRAME)) {
+ mCurrentTimeframe = (StatsTimeframe) getIntent().getSerializableExtra(ARG_DESIRED_TIMEFRAME);
+ } else {
+ // Read the value from app preferences here. Default to 0 - Insights
+ mCurrentTimeframe = AppPrefs.getStatsTimeframe();
+ }
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+
+ if (getIntent().hasExtra(ARG_LAUNCHED_FROM)) {
+ StatsLaunchedFrom from = (StatsLaunchedFrom) getIntent().getSerializableExtra(ARG_LAUNCHED_FROM);
+ if (from == StatsLaunchedFrom.STATS_WIDGET) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_WIDGET_TAPPED, WordPress.getBlog(mLocalBlogID));
+ }
+ }
+
+ }
+
+ //Make sure the blog_id passed to this activity is valid and the blog is available within the app
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ // create the fragments without forcing the re-creation. If the activity is restarted fragments can already
+ // be there, and ready to be displayed without making any network connections. A fragment calls the stats service
+ // if its internal datamodel is empty.
+ createFragments(false);
+
+ if (mSpinner == null) {
+ mSpinner = (Spinner) findViewById(R.id.filter_spinner);
+
+ mTimeframeSpinnerAdapter = new TimeframeSpinnerAdapter(this, timeframes);
+
+ mSpinner.setAdapter(mTimeframeSpinnerAdapter);
+ mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (isFinishing()) {
+ return;
+ }
+ final StatsTimeframe selectedTimeframe = (StatsTimeframe) mTimeframeSpinnerAdapter.getItem(position);
+
+ if (mCurrentTimeframe == selectedTimeframe) {
+ AppLog.d(T.STATS, "The selected TIME FRAME is already active: " + selectedTimeframe.getLabel());
+ return;
+ }
+
+ AppLog.d(T.STATS, "NEW TIME FRAME : " + selectedTimeframe.getLabel());
+ mCurrentTimeframe = selectedTimeframe;
+ AppPrefs.setStatsTimeframe(mCurrentTimeframe);
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ createFragments(true); // Need to recreate fragment here, since a new timeline was selected.
+ mSpinner.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ scrollToTop();
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+
+ trackStatsAnalytics();
+ }
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // nop
+ }
+ });
+
+ Toolbar spinnerToolbar = (Toolbar) findViewById(R.id.toolbar_filter);
+ spinnerToolbar.setBackgroundColor(getResources().getColor(R.color.blue_medium));
+
+ }
+
+ selectCurrentTimeframeInActionBar();
+
+ TextView otherRecentStatsMovedLabel = (TextView) findViewById(R.id.stats_other_recent_stats_moved);
+ otherRecentStatsMovedLabel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ for (int i = 0; i < timeframes.length; i++) {
+ if (timeframes[i] == StatsTimeframe.INSIGHTS) {
+ mSpinner.setSelection(i);
+ break;
+ }
+ }
+
+ mSpinner.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ scrollToTop();
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ });
+
+ // Track usage here
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_ACCESSED, currentBlog);
+ trackStatsAnalytics();
+ }
+ }
+
+ private void trackStatsAnalytics() {
+ // Track usage here
+ Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ switch (mCurrentTimeframe) {
+ case INSIGHTS:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_INSIGHTS_ACCESSED, currentBlog);
+ break;
+ case DAY:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_DAYS_ACCESSED, currentBlog);
+ break;
+ case WEEK:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_WEEKS_ACCESSED, currentBlog);
+ break;
+ case MONTH:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_MONTHS_ACCESSED, currentBlog);
+ break;
+ case YEAR:
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_PERIOD_YEARS_ACCESSED, currentBlog);
+ break;
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsInFront = true;
+ if (NetworkUtils.checkConnection(this)) {
+ checkCredentials();
+ } else {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+ ActivityId.trackLastActivity(ActivityId.STATS);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsInFront = false;
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(SAVED_WP_LOGIN_STATE, mResultCode);
+ outState.putInt(ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ outState.putSerializable(SAVED_STATS_TIMEFRAME, mCurrentTimeframe);
+ outState.putString(SAVED_STATS_REQUESTED_DATE, mRequestedDate);
+ outState.putBoolean(SAVED_THERE_WAS_AN_ERROR_LOADING_STATS, mThereWasAnErrorLoadingStats);
+ if (mOuterScrollView.getScrollY() != 0) {
+ outState.putInt(SAVED_STATS_SCROLL_POSITION, mOuterScrollView.getScrollY());
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ private void createFragments(boolean forceRecreationOfFragments) {
+ if (isFinishing()) {
+ return;
+ }
+
+ // Make the labels invisible see: https://github.com/wordpress-mobile/WordPress-Android/issues/3279
+ findViewById(R.id.stats_other_recent_stats_label_insights).setVisibility(View.INVISIBLE);
+ findViewById(R.id.stats_other_recent_stats_label_timeline).setVisibility(View.INVISIBLE);
+ findViewById(R.id.stats_other_recent_stats_moved).setVisibility(View.INVISIBLE);
+
+ FragmentManager fm = getFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+
+ StatsAbstractFragment fragment;
+
+ if (mCurrentTimeframe != StatsTimeframe.INSIGHTS) {
+ findViewById(R.id.stats_timeline_fragments_container).setVisibility(View.VISIBLE);
+ findViewById(R.id.stats_insights_fragments_container).setVisibility(View.GONE);
+
+ if (fm.findFragmentByTag(StatsVisitorsAndViewsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newVisitorsAndViewsInstance(StatsViewType.GRAPH_AND_SUMMARY, mLocalBlogID, mCurrentTimeframe, mRequestedDate,
+ mTabToSelectOnGraph);
+ ft.replace(R.id.stats_visitors_and_views_container, fragment, StatsVisitorsAndViewsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsTopPostsAndPagesFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.TOP_POSTS_AND_PAGES, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_top_posts_container, fragment, StatsTopPostsAndPagesFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsReferrersFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.REFERRERS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_referrers_container, fragment, StatsReferrersFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsClicksFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.CLICKS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_clicks_container, fragment, StatsClicksFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsGeoviewsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.GEOVIEWS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_geoviews_container, fragment, StatsGeoviewsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsAuthorsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.AUTHORS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_top_authors_container, fragment, StatsAuthorsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsVideoplaysFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.VIDEO_PLAYS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_video_container, fragment, StatsVideoplaysFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsSearchTermsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.SEARCH_TERMS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_search_terms_container, fragment, StatsSearchTermsFragment.TAG);
+ }
+
+ } else {
+ findViewById(R.id.stats_timeline_fragments_container).setVisibility(View.GONE);
+ findViewById(R.id.stats_insights_fragments_container).setVisibility(View.VISIBLE);
+
+ if (fm.findFragmentByTag(StatsInsightsMostPopularFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_MOST_POPULAR, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_insights_most_popular_container, fragment, StatsInsightsMostPopularFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsInsightsAllTimeFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_ALL_TIME, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_insights_all_time_container, fragment, StatsInsightsAllTimeFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsInsightsTodayFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_TODAY, mLocalBlogID, StatsTimeframe.DAY, mRequestedDate);
+ ft.replace(R.id.stats_insights_today_container, fragment, StatsInsightsTodayFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsInsightsLatestPostSummaryFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.INSIGHTS_LATEST_POST_SUMMARY, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_insights_latest_post_summary_container, fragment, StatsInsightsLatestPostSummaryFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsCommentsFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.COMMENTS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_comments_container, fragment, StatsCommentsFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsTagsAndCategoriesFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.TAGS_AND_CATEGORIES, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_tags_and_categories_container, fragment, StatsTagsAndCategoriesFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsPublicizeFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.PUBLICIZE, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_publicize_container, fragment, StatsPublicizeFragment.TAG);
+ }
+
+ if (fm.findFragmentByTag(StatsFollowersFragment.TAG) == null || forceRecreationOfFragments) {
+ fragment = StatsAbstractFragment.newInstance(StatsViewType.FOLLOWERS, mLocalBlogID, mCurrentTimeframe, mRequestedDate);
+ ft.replace(R.id.stats_followers_container, fragment, StatsFollowersFragment.TAG);
+ }
+ }
+
+ ft.commitAllowingStateLoss();
+
+ // Slightly delayed labels setup: see https://github.com/wordpress-mobile/WordPress-Android/issues/3279
+ mOuterScrollView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isFinishing()) {
+ return;
+ }
+ boolean isInsights = mCurrentTimeframe == StatsTimeframe.INSIGHTS;
+ findViewById(R.id.stats_other_recent_stats_label_insights).setVisibility(isInsights ? View.VISIBLE : View.GONE);
+ findViewById(R.id.stats_other_recent_stats_label_timeline).setVisibility(isInsights ? View.GONE : View.VISIBLE);
+ findViewById(R.id.stats_other_recent_stats_moved).setVisibility(isInsights ? View.GONE : View.VISIBLE);
+ }
+ }, StatsConstants.STATS_LABELS_SETUP_DELAY);
+ }
+
+ private void updateTimeframeAndDateAndStartRefreshOfFragments(boolean includeGraph) {
+ if (isFinishing()) {
+ return;
+ }
+ FragmentManager fm = getFragmentManager();
+
+ if (mCurrentTimeframe != StatsTimeframe.INSIGHTS) {
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsTopPostsAndPagesFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsReferrersFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsClicksFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsGeoviewsFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsAuthorsFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsVideoplaysFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsSearchTermsFragment.TAG);
+ if (includeGraph) {
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsVisitorsAndViewsFragment.TAG);
+ }
+ } else {
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsTodayFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsAllTimeFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsMostPopularFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsInsightsLatestPostSummaryFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsCommentsFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsTagsAndCategoriesFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsPublicizeFragment.TAG);
+ updateTimeframeAndDateAndStartRefreshInFragment(fm, StatsFollowersFragment.TAG);
+ }
+ }
+
+ private boolean updateTimeframeAndDateAndStartRefreshInFragment(FragmentManager fm , String fragmentTAG) {
+ StatsAbstractFragment fragment = (StatsAbstractFragment) fm.findFragmentByTag(fragmentTAG);
+ if (fragment != null) {
+ fragment.setDate(mRequestedDate);
+ fragment.setTimeframe(mCurrentTimeframe);
+ fragment.refreshStats();
+ return true;
+ }
+ return false;
+ }
+
+ private void startWPComLoginActivity() {
+ mResultCode = RESULT_CANCELED;
+ Intent signInIntent = new Intent(this, SignInActivity.class);
+ signInIntent.putExtra(SignInActivity.EXTRA_JETPACK_SITE_AUTH, mLocalBlogID);
+ signInIntent.putExtra(
+ SignInActivity.EXTRA_JETPACK_MESSAGE_AUTH,
+ getString(R.string.stats_sign_in_jetpack_different_com_account)
+ );
+ startActivityForResult(signInIntent, SignInActivity.REQUEST_CODE);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == SignInActivity.REQUEST_CODE) {
+ if (resultCode == RESULT_CANCELED) {
+ finish();
+ }
+ mResultCode = resultCode;
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (resultCode == RESULT_OK && currentBlog != null && !currentBlog.isDotcomFlag()) {
+ if (currentBlog.getDotComBlogId() == null) {
+ final Handler handler = new Handler();
+ // Attempt to get the Jetpack blog ID
+ XMLRPCClientInterface xmlrpcClient = XMLRPCFactory.instantiate(currentBlog.getUri(), "", "");
+ Map<String, String> args = ApiHelper.blogOptionsXMLRPCParameters;
+ Object[] params = {
+ currentBlog.getRemoteBlogId(), currentBlog.getUsername(), currentBlog.getPassword(), args
+ };
+ xmlrpcClient.callAsync(new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, Object result) {
+ if (result != null && (result instanceof HashMap)) {
+ Map<?, ?> blogOptions = (HashMap<?, ?>) result;
+ ApiHelper.updateBlogOptions(currentBlog, blogOptions);
+ AnalyticsUtils.refreshMetadata();
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.SIGNED_INTO_JETPACK, currentBlog);
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN, currentBlog);
+ if (isFinishing()) {
+ return;
+ }
+ // We have the blogID now, but we need to re-check if the network connection is available
+ if (NetworkUtils.checkConnection(StatsActivity.this)) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSwipeToRefreshHelper.setRefreshing(true);
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ createFragments(true); // Recreate the fragment and start a refresh of Stats
+ }
+ });
+ }
+ }
+ }
+ @Override
+ public void onFailure(long id, Exception error) {
+ AppLog.e(T.STATS,
+ "Cannot load blog options (wp.getOptions failed) "
+ + "and no jetpack_client_id is then available",
+ error);
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ ToastUtils.showToast(StatsActivity.this,
+ StatsActivity.this.getString(R.string.error_refresh_stats),
+ Duration.LONG);
+ }
+ });
+ }
+ }, Method.GET_OPTIONS, params);
+ } else {
+ mRequestedDate = StatsUtils.getCurrentDateTZ(mLocalBlogID);
+ createFragments(true); // Recreate the fragment and start a refresh of Stats
+ }
+ mSwipeToRefreshHelper.setRefreshing(true);
+ }
+ }
+ }
+
+ private class VerifyJetpackSettingsCallback implements ApiHelper.GenericCallback {
+ // AsyncTasks are bound to the Activity that launched it. If the user rotate the device StatsActivity is restarted.
+ // Use the event bus to fix this issue.
+
+ @Override
+ public void onSuccess() {
+ EventBus.getDefault().post(new StatsEvents.JetpackSettingsCompleted(false));
+ }
+
+ @Override
+ public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) {
+ EventBus.getDefault().post(new StatsEvents.JetpackSettingsCompleted(true));
+ }
+ }
+
+ private void showJetpackNonConnectedAlert() {
+ if (isFinishing()) {
+ return;
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (currentBlog.isAdmin()) {
+ builder.setMessage(getString(R.string.jetpack_not_connected_message))
+ .setTitle(getString(R.string.jetpack_not_connected));
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String stringToLoad = currentBlog.getAdminUrl();
+ String jetpackConnectPageAdminPath = "admin.php?page=jetpack";
+ stringToLoad = stringToLoad.endsWith("/") ? stringToLoad + jetpackConnectPageAdminPath :
+ stringToLoad + "/" + jetpackConnectPageAdminPath;
+ String authURL = WPWebViewActivity.getBlogLoginUrl(currentBlog);
+ Intent jetpackIntent = new Intent(StatsActivity.this, WPWebViewActivity.class);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, currentBlog.getUsername());
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_PASSWD, currentBlog.getPassword());
+ jetpackIntent.putExtra(WPWebViewActivity.URL_TO_LOAD, stringToLoad);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, authURL);
+ startActivityForResult(jetpackIntent, REQUEST_JETPACK);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_SELECTED_CONNECT_JETPACK);
+ }
+ });
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User cancelled the dialog. Hide Stats.
+ finish();
+ }
+ });
+ } else {
+ builder.setMessage(getString(R.string.jetpack_message_not_admin))
+ .setTitle(getString(R.string.jetpack_not_found));
+ builder.setPositiveButton(R.string.yes, null);
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // User pressed the back key Hide Stats.
+ finish();
+ }
+ });
+ dialog.show();
+ }
+
+ private void showJetpackMissingAlert() {
+ if (isFinishing()) {
+ return;
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (currentBlog.isAdmin()) {
+ builder.setMessage(getString(R.string.jetpack_message))
+ .setTitle(getString(R.string.jetpack_not_found));
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String stringToLoad = currentBlog.getAdminUrl()
+ + "plugin-install.php?tab=search&s=jetpack+by+wordpress.com"
+ + "&plugin-search-input=Search+Plugins";
+ String authURL = WPWebViewActivity.getBlogLoginUrl(currentBlog);
+ Intent jetpackIntent = new Intent(StatsActivity.this, WPWebViewActivity.class);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, currentBlog.getUsername());
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_PASSWD, currentBlog.getPassword());
+ jetpackIntent.putExtra(WPWebViewActivity.URL_TO_LOAD, stringToLoad);
+ jetpackIntent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, authURL);
+ startActivityForResult(jetpackIntent, REQUEST_JETPACK);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_SELECTED_INSTALL_JETPACK);
+ }
+ });
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User cancelled the dialog. Hide Stats.
+ finish();
+ }
+ });
+ } else {
+ builder.setMessage(getString(R.string.jetpack_message_not_admin))
+ .setTitle(getString(R.string.jetpack_not_found));
+ builder.setPositiveButton(R.string.yes, null);
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // User pressed the back key Hide Stats.
+ finish();
+ }
+ });
+ dialog.show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int i = item.getItemId();
+ if (i == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void scrollToTop() {
+ mOuterScrollView.fullScroll(ScrollView.FOCUS_UP);
+ }
+
+ // StatsInsightsTodayFragment calls this when the user taps on a item in Today's Stats
+ @Override
+ public void onInsightsTodayClicked(final StatsVisitorsAndViewsFragment.OverviewLabel item) {
+ mTabToSelectOnGraph = item;
+ for (int i = 0; i < timeframes.length; i++) {
+ if (timeframes[i] == StatsTimeframe.DAY) {
+ mSpinner.setSelection(i);
+ break;
+ }
+ }
+ }
+
+ // StatsVisitorsAndViewsFragment calls this when the user taps on a bar in the graph
+ @Override
+ public void onDateChanged(String blogID, StatsTimeframe timeframe, String date) {
+ if (isFinishing()) {
+ return;
+ }
+ mRequestedDate = date;
+ updateTimeframeAndDateAndStartRefreshOfFragments(false);
+ if (NetworkUtils.checkConnection(StatsActivity.this)) {
+ mSwipeToRefreshHelper.setRefreshing(true);
+ } else {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+ }
+
+ // StatsVisitorsAndViewsFragment calls this when the user taps on the tab bar to change the type of the graph
+ @Override
+ public void onOverviewItemChanged(StatsVisitorsAndViewsFragment.OverviewLabel newItem) {
+ mTabToSelectOnGraph = newItem;
+ }
+
+ private boolean checkCredentials() {
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ AppLog.w(AppLog.T.STATS, "StatsActivity > cannot check credentials since no internet connection available");
+ return false;
+ }
+
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ return false;
+ }
+
+ final String blogId = currentBlog.getDotComBlogId();
+
+ // blogId is always available for dotcom blogs. It could be null on Jetpack blogs...
+ if (blogId != null) {
+ // for self-hosted sites; launch the user into an activity where they can provide their credentials
+ if (!currentBlog.isDotcomFlag()
+ && !currentBlog.hasValidJetpackCredentials() && mResultCode != RESULT_CANCELED) {
+ if (AccountHelper.isSignedInWordPressDotCom()) {
+ // Let's try the global wpcom credentials them first
+ String username = AccountHelper.getDefaultAccount().getUserName();
+ currentBlog.setDotcom_username(username);
+ WordPress.wpDB.saveBlog(currentBlog);
+ createFragments(true);
+ } else {
+ startWPComLoginActivity();
+ return false;
+ }
+ }
+ } else {
+ // blogId is null at this point.
+ if (!currentBlog.isDotcomFlag()) {
+ // Refresh blog settings/options that includes 'jetpack_client_id' needed here
+ mSwipeToRefreshHelper.setRefreshing(true);
+ new ApiHelper.RefreshBlogContentTask(currentBlog,
+ new VerifyJetpackSettingsCallback()).execute(false);
+ return false;
+ } else {
+ // blodID cannot be null on dotcom blogs.
+ Toast.makeText(this, R.string.error_refresh_stats, Toast.LENGTH_LONG).show();
+ AppLog.e(T.STATS, "blogID is null for a wpcom blog!! " + currentBlog.getHomeURL());
+ finish();
+ }
+ }
+
+ return true;
+ }
+
+ private void bumpPromoAnaylticsAndShowPromoDialogIfNecessary() {
+ if (mIsUpdatingStats || mThereWasAnErrorLoadingStats) {
+ // Do nothing in case of errors or when it's still loading
+ return;
+ }
+
+ if (!StringUtils.isEmpty(AppPrefs.getStatsWidgetsKeys())) {
+ // Stats widgets already used!!
+ return;
+ }
+
+ // Bump analytics that drives the Promo widget when the loading is completed without errors.
+ AppPrefs.bumpAnalyticsForStatsWidgetPromo();
+
+ // Should we display the widget promo?
+ int counter = AppPrefs.getAnalyticsForStatsWidgetPromo();
+ if (counter == 3 || counter == 1000 || counter == 10000) {
+ DialogFragment newFragment = PromoDialog.newInstance(R.drawable.stats_widget_promo_header,
+ R.string.stats_widget_promo_title, R.string.stats_widget_promo_desc,
+ R.string.stats_widget_promo_ok_btn_label);
+ newFragment.show(getFragmentManager(), "promote_widget_dialog");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.UpdateStatusChanged event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+ mSwipeToRefreshHelper.setRefreshing(event.mUpdating);
+ mIsUpdatingStats = event.mUpdating;
+
+ if (!mIsUpdatingStats && !mThereWasAnErrorLoadingStats) {
+ // Do not bump promo analytics or show the dialog in case of errors or when it's still loading
+ bumpPromoAnaylticsAndShowPromoDialogIfNecessary();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.JetpackSettingsCompleted event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+ mSwipeToRefreshHelper.setRefreshing(false);
+
+ if (!event.isError) {
+ final Blog currentBlog = WordPress.getBlog(mLocalBlogID);
+ if (currentBlog == null) {
+ AppLog.e(T.STATS, "The blog with local_blog_id " + mLocalBlogID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (currentBlog.getDotComBlogId() == null) {
+ if (TextUtils.isEmpty(currentBlog.getJetpackVersion())) {
+ // jetpack_version option is available, but not the jetpack_client_id ----> Jetpack available but not connected.
+ showJetpackNonConnectedAlert();
+ } else {
+ // Blog has not returned jetpack_version/jetpack_client_id.
+ showJetpackMissingAlert();
+ }
+ } else {
+ checkCredentials();
+ }
+ } else {
+ Toast.makeText(StatsActivity.this, R.string.error_refresh_stats, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ // There was an error loading Stats. Don't bump stats for promo widget.
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+
+ // There was an error loading Stats. Don't bump stats for promo widget.
+ mThereWasAnErrorLoadingStats = true;
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.JetpackAuthError event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+
+ // There was an error loading Stats. Don't bump stats for promo widget.
+ mThereWasAnErrorLoadingStats = true;
+
+ if (event.mLocalBlogId != mLocalBlogID) {
+ // The user has changed blog
+ return;
+ }
+
+ mSwipeToRefreshHelper.setRefreshing(false);
+ startWPComLoginActivity();
+ }
+
+ /*
+ * make sure the passed timeframe is the one selected in the actionbar
+ */
+ private void selectCurrentTimeframeInActionBar() {
+ if (isFinishing()) {
+ return;
+ }
+
+ if (mTimeframeSpinnerAdapter == null || mSpinner == null) {
+ return;
+ }
+
+ int position = mTimeframeSpinnerAdapter.getIndexOfTimeframe(mCurrentTimeframe);
+
+ if (position > -1 && position != mSpinner.getSelectedItemPosition()) {
+ mSpinner.setSelection(position);
+ }
+ }
+
+ /*
+ * adapter used by the timeframe spinner
+ */
+ private class TimeframeSpinnerAdapter extends BaseAdapter {
+ private final StatsTimeframe[] mTimeframes;
+ private final LayoutInflater mInflater;
+
+ TimeframeSpinnerAdapter(Context context, StatsTimeframe[] timeframeNames) {
+ super();
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mTimeframes = timeframeNames;
+ }
+
+ @Override
+ public int getCount() {
+ return (mTimeframes != null ? mTimeframes.length : 0);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ if (position < 0 || position >= getCount())
+ return "";
+ return mTimeframes[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view;
+ if (convertView == null) {
+ view = mInflater.inflate(R.layout.filter_spinner_item, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ final TextView text = (TextView) view.findViewById(R.id.text);
+ StatsTimeframe selectedTimeframe = (StatsTimeframe)getItem(position);
+ text.setText(selectedTimeframe.getLabel());
+ return view;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ StatsTimeframe selectedTimeframe = (StatsTimeframe)getItem(position);
+ final TagViewHolder holder;
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.toolbar_spinner_dropdown_item, parent, false);
+ holder = new TagViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (TagViewHolder) convertView.getTag();
+ }
+
+ holder.textView.setText(selectedTimeframe.getLabel());
+ return convertView;
+ }
+
+ private class TagViewHolder {
+ private final TextView textView;
+ TagViewHolder(View view) {
+ textView = (TextView) view.findViewById(R.id.text);
+ }
+ }
+
+ public int getIndexOfTimeframe(StatsTimeframe tm) {
+ int pos = 0;
+ for (int i = 0; i < mTimeframes.length; i++) {
+ if (mTimeframes[i] == tm) {
+ pos = i;
+ return pos;
+ }
+ }
+ return pos;
+ }
+ }
+
+ @Override
+ public void onScrollChanged(NestedScrollViewExt scrollView, int x, int y, int oldx, int oldy) {
+ // We take the last son in the scrollview
+ View view = scrollView.getChildAt(scrollView.getChildCount() - 1);
+ if (view == null) {
+ return;
+ }
+ int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY() + view.getTop()));
+
+ // if diff is zero, then the bottom has been reached
+ if (diff == 0) {
+ sTrackBottomReachedStats.runIfNotLimited();
+ }
+ }
+
+ private static final RateLimitedTask sTrackBottomReachedStats = new RateLimitedTask(2) {
+ protected boolean run() {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_SCROLLED_TO_BOTTOM);
+ return true;
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java
new file mode 100644
index 000000000..65b55cc5c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsAuthorsFragment.java
@@ -0,0 +1,282 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.apache.commons.lang.StringUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.AuthorModel;
+import org.wordpress.android.ui.stats.models.AuthorsModel;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+public class StatsAuthorsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsAuthorsFragment.class.getSimpleName();
+
+ private AuthorsModel mAuthors;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mAuthors != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mAuthors);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mAuthors = (AuthorsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.AuthorsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mAuthors = event.mAuthors;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mAuthors = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!hasAuthors()) {
+ showHideNoResultsUI(true);
+ return;
+ }
+
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), mAuthors.getAuthors());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ }
+
+ private boolean hasAuthors() {
+ return mAuthors != null
+ && mAuthors.getAuthors() != null
+ && mAuthors.getAuthors().size() > 0;
+ }
+
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return (hasAuthors()
+ && mAuthors.getAuthors().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.AUTHORS
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_authors;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_top_posts_title;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_top_authors_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ public final Activity activity;
+ private final List<AuthorModel> authors;
+
+ public MyExpandableListAdapter(Activity act, List<AuthorModel> authors) {
+ this.activity = act;
+ this.authors = authors;
+ this.inflater = act.getLayoutInflater();
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ AuthorModel currentGroup = authors.get(groupPosition);
+ List<PostModel> posts = currentGroup.getPosts();
+ return posts.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final PostModel children = (PostModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ // The link icon
+ holder.showLinkIcon();
+
+ // name, url
+ holder.setEntryTextOpenDetailsPage(children);
+
+ // Setup the more button
+ holder.setMoreButtonOpenInReader(children);
+
+ // totals
+ int total = children.getTotals();
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ AuthorModel currentGroup = authors.get(groupPosition);
+ List<PostModel> posts = currentGroup.getPosts();
+ if (posts == null) {
+ return 0;
+ } else {
+ return posts.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return authors.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return authors.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ convertView.setTag(new StatsViewHolder(convertView));
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ AuthorModel group = (AuthorModel) getGroup(groupPosition);
+
+ String name = group.getName();
+ if (StringUtils.isBlank(name)) {
+ // Jetpack case: articles published before the activation of Jetpack.
+ name = getString(R.string.stats_unknown_author);
+ }
+ int total = group.getViews();
+ String icon = group.getAvatar();
+ int children = getChildrenCount(groupPosition);
+
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // icon
+ //holder.showNetworkImage(icon);
+ holder.networkImageView.setImageUrl(GravatarUtils.fixGravatarUrl(icon, mResourceVars.headerAvatarSizePx), WPNetworkImageView.ImageType.AVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ final FollowDataModel followData = group.getFollowData();
+ if (followData == null) {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ } else {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FollowHelper fh = new FollowHelper(activity);
+ fh.showPopup(holder.imgMore, followData);
+ }
+ });
+ }
+
+ if (children == 0) {
+ holder.showLinkIcon();
+ } else {
+ holder.showChevronIcon();
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_authors);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java
new file mode 100644
index 000000000..342c45821
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsBarGraph.java
@@ -0,0 +1,335 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Shader;
+import android.support.v4.view.GestureDetectorCompat;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import com.jjoe64.graphview.CustomLabelFormatter;
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.GraphViewDataInterface;
+import com.jjoe64.graphview.GraphViewSeries.GraphViewSeriesStyle;
+import com.jjoe64.graphview.GraphViewStyle;
+import com.jjoe64.graphview.IndexDependentColor;
+
+import org.wordpress.android.R;
+import org.wordpress.android.widgets.TypefaceCache;
+
+import java.text.NumberFormat;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A Bar graph depicting the view and visitors.
+ * Based on BarGraph from the GraphView library.
+ */
+class StatsBarGraph extends GraphView {
+
+ private static final int DEFAULT_MAX_Y = 10;
+
+ // Keep tracks of every bar drawn on the graph.
+ private final List<List<BarChartRect>> mSeriesRectsDrawedOnScreen = (List<List<BarChartRect>>) new LinkedList();
+ private int mBarPositionToHighlight = -1;
+ private boolean[] mWeekendDays;
+
+ private final GestureDetectorCompat mDetector;
+ private OnGestureListener mGestureListener;
+
+ public StatsBarGraph(Context context) {
+ super(context, "");
+
+ int width = LayoutParams.MATCH_PARENT;
+ int height = getResources().getDimensionPixelSize(R.dimen.stats_barchart_height);
+ setLayoutParams(new LayoutParams(width, height));
+
+ setProperties();
+
+ paint.setTypeface(TypefaceCache.getTypeface(getContext()));
+
+ mDetector = new GestureDetectorCompat(getContext(), new MyGestureListener());
+ mDetector.setIsLongpressEnabled(false);
+ }
+
+ public void setGestureListener(OnGestureListener listener) {
+ this.mGestureListener = listener;
+ }
+
+ class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onDown(MotionEvent event) {
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent event) {
+ highlightBarAndBroadcastDate();
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ highlightBarAndBroadcastDate();
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ private void highlightBarAndBroadcastDate() {
+ int tappedBar = getTappedBar();
+ //AppLog.d(AppLog.T.STATS, this.getClass().getName() + " Tapped bar " + tappedBar);
+ if (tappedBar >= 0) {
+ highlightBar(tappedBar);
+ if (mGestureListener != null) {
+ mGestureListener.onBarTapped(tappedBar);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent (MotionEvent event) {
+ boolean handled = super.onTouchEvent(event);
+ if (mDetector != null && handled) {
+ this.mDetector.onTouchEvent(event);
+ }
+ return handled;
+ }
+
+ private class HorizontalLabelsColor implements IndexDependentColor {
+ public int get(int index) {
+ if (mBarPositionToHighlight == index) {
+ return getResources().getColor(R.color.orange_jazzy);
+ } else {
+ return getResources().getColor(R.color.grey_darken_30);
+ }
+ }
+ }
+
+ private void setProperties() {
+ GraphViewStyle gStyle = getGraphViewStyle();
+ gStyle.setHorizontalLabelsIndexDependentColor(new HorizontalLabelsColor());
+ gStyle.setHorizontalLabelsColor(getResources().getColor(R.color.grey_darken_30));
+ gStyle.setVerticalLabelsColor(getResources().getColor(R.color.grey_darken_10));
+ gStyle.setTextSize(getResources().getDimensionPixelSize(R.dimen.text_sz_extra_small));
+ gStyle.setGridXColor(Color.TRANSPARENT);
+ gStyle.setGridYColor(getResources().getColor(R.color.grey_lighten_30));
+ gStyle.setNumVerticalLabels(3);
+
+ setCustomLabelFormatter(new CustomLabelFormatter() {
+ private NumberFormat numberFormatter;
+
+ @Override
+ public String formatLabel(double value, boolean isValueX) {
+ if (isValueX) {
+ return null;
+ }
+ if (numberFormatter == null) {
+ numberFormatter = NumberFormat.getNumberInstance();
+ numberFormatter.setMaximumFractionDigits(0);
+ }
+ return numberFormatter.format(value);
+ }
+ });
+ }
+
+ @Override
+ protected void onBeforeDrawSeries() {
+ mSeriesRectsDrawedOnScreen.clear();
+ }
+
+ @Override
+ public void drawSeries(Canvas canvas, GraphViewDataInterface[] values,
+ float graphwidth, float graphheight, float border, double minX,
+ double minY, double diffX, double diffY, float horstart,
+ GraphViewSeriesStyle style) {
+ float colwidth = graphwidth / values.length;
+ int maxColumnSize = getGraphViewStyle().getMaxColumnWidth();
+ if (maxColumnSize > 0 && colwidth > maxColumnSize) {
+ colwidth = maxColumnSize;
+ }
+
+ paint.setStrokeWidth(style.thickness);
+ paint.setColor(style.color);
+
+ // Bar chart position of this series on the canvas
+ List<BarChartRect> barChartRects = new LinkedList<>();
+
+ // draw data
+ for (int i = 0; i < values.length; i++) {
+ float valY = (float) (values[i].getY() - minY);
+ float ratY = (float) (valY / diffY);
+ float y = graphheight * ratY;
+
+ // hook for value dependent color
+ if (style.getValueDependentColor() != null) {
+ paint.setColor(style.getValueDependentColor().get(values[i]));
+ }
+
+ float pad = style.padding;
+
+ float left = (i * colwidth) + horstart;
+ float top = (border - y) + graphheight;
+ float right = left + colwidth;
+ float bottom = graphheight + border - 1;
+
+ // Draw the orange selection behind the selected bar
+ if (style.outerhighlightColor != 0x00ffffff && mBarPositionToHighlight == i) {
+ paint.setColor(style.outerhighlightColor);
+ canvas.drawRect(left, 10f, right, bottom, paint);
+ }
+
+ // Draw the grey background color on weekend days
+ if (style.outerColor != 0x00ffffff
+ && mBarPositionToHighlight != i
+ && mWeekendDays != null && mWeekendDays[i]) {
+ paint.setColor(style.outerColor);
+ canvas.drawRect(left, 10f, right, bottom, paint);
+ }
+
+ if ((top - bottom) == 1) {
+ // draw a placeholder
+ if (mBarPositionToHighlight != i) {
+ paint.setColor(style.color);
+ paint.setAlpha(25);
+ Shader shader = new LinearGradient(left + pad, bottom - 50, left + pad, bottom, Color.WHITE, Color.BLACK, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ canvas.drawRect(left + pad, bottom - 50, right - pad, bottom, paint);
+ paint.setShader(null);
+ }
+ } else {
+ // draw a real bar
+ paint.setAlpha(255);
+ if (mBarPositionToHighlight == i) {
+ paint.setColor(style.highlightColor);
+ } else {
+ paint.setColor(style.color);
+ }
+ canvas.drawRect(left + pad, top, right - pad, bottom, paint);
+ }
+
+ barChartRects.add(new BarChartRect(left + pad, top, right - pad, bottom));
+ }
+ mSeriesRectsDrawedOnScreen.add(barChartRects);
+ }
+
+ private int getTappedBar() {
+ float[] lastBarChartTouchedPoint = this.getLastTouchedPointOnCanvasAndReset();
+ if (lastBarChartTouchedPoint[0] == 0f && lastBarChartTouchedPoint[1] == 0f) {
+ return -1;
+ }
+ for (List<BarChartRect> currentSerieChartRects : mSeriesRectsDrawedOnScreen) {
+ int i = 0;
+ for (BarChartRect barChartRect : currentSerieChartRects) {
+ if (barChartRect.isPointInside(lastBarChartTouchedPoint[0], lastBarChartTouchedPoint[1])) {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+ }
+/*
+ public float getMiddlePointOfTappedBar(int tappedBar) {
+ if (tappedBar == -1 || mSeriesRectsDrawedOnScreen == null || mSeriesRectsDrawedOnScreen.size() == 0) {
+ return -1;
+ }
+ BarChartRect rect = mSeriesRectsDrawedOnScreen.get(0).get(tappedBar);
+
+ return ((rect.mLeft + rect.mRight) / 2) + getCanvasLeft();
+ }
+
+ public void highlightAndDismissBar(int barPosition) {
+ mBarPositionToHighlight = barPosition;
+ if (mBarPositionToHighlight == -1) {
+ return;
+ }
+ this.redrawAll();
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mBarPositionToHighlight = -1;
+ redrawAll();
+ }
+ }, 500);
+ }
+*/
+
+ public void setWeekendDays(boolean[] days) {
+ mWeekendDays = days;
+ }
+
+ public void highlightBar(int barPosition) {
+ mBarPositionToHighlight = barPosition;
+ this.redrawAll();
+ }
+
+ public int getHighlightBar() {
+ return mBarPositionToHighlight;
+ }
+
+ public void resetHighlightBar() {
+ mBarPositionToHighlight = -1;
+ }
+
+ @Override
+ protected double getMinY() {
+ return 0;
+ }
+
+ // Make sure the highest number is always even, so the halfway mark is correctly balanced in the middle of the graph
+ // Also make sure to display a default value when there is no activity in the period.
+ @Override
+ protected double getMaxY() {
+ double maxY = super.getMaxY();
+ if (maxY == 0) {
+ return DEFAULT_MAX_Y;
+ }
+
+ return maxY + (maxY % 2);
+ }
+
+ /**
+ * Private class that is used to hold the local (to the canvas) coordinate on the screen
+ * of every single bar in the graph
+ */
+ private class BarChartRect {
+ final float mLeft;
+ final float mTop;
+ final float mRight;
+ final float mBottom;
+
+ BarChartRect(float left, float top, float right, float bottom) {
+ this.mLeft = left;
+ this.mTop = top;
+ this.mRight = right;
+ this.mBottom = bottom;
+ }
+
+ /**
+ * Check if the tap happens on a bar in the graph.
+ *
+ * @return true if the tap point falls within the bar for the X coordinate, and within the full canvas
+ * height for the Y coordinate. This is a fix to make very small bars tappable.
+ */
+ public boolean isPointInside(float x, float y) {
+ return x >= this.mLeft
+ && x <= this.mRight;
+ }
+ }
+
+ interface OnGestureListener {
+ void onBarTapped(int tappedBar);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java
new file mode 100644
index 000000000..edb4ef953
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsClicksFragment.java
@@ -0,0 +1,266 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.ClickGroupModel;
+import org.wordpress.android.ui.stats.models.ClicksModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+public class StatsClicksFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsClicksFragment.class.getSimpleName();
+
+ private ClicksModel mClicks;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mClicks != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mClicks);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mClicks = (ClicksModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.ClicksUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mClicks = event.mClicks;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mClicks = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasClicks()) {
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), mClicks.getClickGroups());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasClicks() {
+ return mClicks != null
+ && mClicks.getClickGroups() != null
+ && mClicks.getClickGroups().size() > 0;
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return (hasClicks() && mClicks.getClickGroups().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.CLICKS
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_clicks_link;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_clicks;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_clicks_title;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_clicks_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ private final List<ClickGroupModel> clickGroups;
+
+ public MyExpandableListAdapter(Context context, List<ClickGroupModel> clickGroups) {
+ this.clickGroups = clickGroups;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ ClickGroupModel currentGroup = clickGroups.get(groupPosition);
+ List<SingleItemModel> results = currentGroup.getClicks();
+ return results.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final SingleItemModel children = (SingleItemModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ // The link icon
+ holder.showLinkIcon();
+
+ // name, url
+ holder.setEntryTextOrLink(children.getUrl(), children.getTitle());
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(
+ children.getTotals()
+ ));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ ClickGroupModel currentGroup = clickGroups.get(groupPosition);
+ List<SingleItemModel> clicks = currentGroup.getClicks();
+ if (clicks == null) {
+ return 0;
+ } else {
+ return clicks.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return clickGroups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return clickGroups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ ClickGroupModel group = (ClickGroupModel) getGroup(groupPosition);
+
+ String name = group.getName();
+ int total = group.getViews();
+ String url = group.getUrl();
+ String icon = group.getIcon();
+ int children = getChildrenCount(groupPosition);
+
+ if (children > 0) {
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ holder.setEntryTextOrLink(url, name);
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // Site icon
+ holder.networkImageView.setVisibility(View.GONE);
+ if (!TextUtils.isEmpty(icon)) {
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(icon, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.GONE_UNTIL_AVAILABLE
+ );
+ }
+
+ if (children == 0) {
+ holder.showLinkIcon();
+ } else {
+ holder.showChevronIcon();
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_clicks);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java
new file mode 100644
index 000000000..bb13d37f3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsCommentsFragment.java
@@ -0,0 +1,280 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.adapters.PostsAndPagesAdapter;
+import org.wordpress.android.ui.stats.models.AuthorModel;
+import org.wordpress.android.ui.stats.models.CommentFollowersModel;
+import org.wordpress.android.ui.stats.models.CommentsModel;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsCommentsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsCommentsFragment.class.getSimpleName();
+ static final String ARG_REST_RESPONSE_FOLLOWERS = "ARG_REST_RESPONSE_FOLLOWERS";
+
+ private CommentsModel mCommentsModel;
+ private CommentFollowersModel mCommentFollowersModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mCommentsModel != null && mCommentFollowersModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mCommentsModel != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mCommentsModel);
+ }
+ if (mCommentFollowersModel != null) {
+ outState.putSerializable(ARG_REST_RESPONSE_FOLLOWERS, mCommentFollowersModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mCommentsModel = (CommentsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE_FOLLOWERS)) {
+ mCommentFollowersModel = (CommentFollowersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE_FOLLOWERS);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.CommentsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mCommentsModel = event.mComments;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.CommentFollowersUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mCommentFollowersModel = event.mCommentFollowers;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mCommentsModel = null;
+ mCommentFollowersModel = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ Resources res = container.getContext().getResources();
+ String[] titles = {
+ res.getString(R.string.stats_comments_by_authors),
+ res.getString(R.string.stats_comments_by_posts_and_pages),
+ };
+
+ setupTopModulePager(inflater, container, view, titles);
+
+ return view;
+ }
+
+ @Override
+ protected void updateUI() {
+ // This module is a kind of exception to the normal way we build page interface.
+ // In this module only the first rest endpoint StatsService.StatsEndpointsEnum.COMMENTS
+ // is used to populate 99% of the UI even if there is a tab on the top.
+ // Switching to a different tab on the UI doesn't switch the underlying datamodel index as in all other modules.
+
+ if (!isAdded()) {
+ return;
+ }
+
+ if (mCommentsModel == null && mCommentFollowersModel == null) {
+ showHideNoResultsUI(true);
+ mTotalsLabel.setVisibility(View.GONE);
+ mTopPagerContainer.setVisibility(View.GONE);
+ return;
+ }
+
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+
+ if (mCommentFollowersModel != null) { // check if comment-followers is already here
+ mTotalsLabel.setVisibility(View.VISIBLE);
+ int totalNumberOfFollowers = mCommentFollowersModel.getTotal();
+ String totalCommentsFollowers = getString(R.string.stats_comments_total_comments_followers);
+ mTotalsLabel.setText(
+ String.format(totalCommentsFollowers, FormatUtils.formatDecimal(totalNumberOfFollowers))
+ );
+ }
+
+ ArrayAdapter adapter = null;
+
+ if (mTopPagerSelectedButtonIndex == 0 && hasAuthors()) {
+ adapter = new AuthorsAdapter(getActivity(), getAuthors());
+ } else if (mTopPagerSelectedButtonIndex == 1 && hasPosts()) {
+ adapter = new PostsAndPagesAdapter(getActivity(), getPosts());
+ }
+
+ if (adapter != null) {
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasAuthors() {
+ return mCommentsModel != null
+ && mCommentsModel.getAuthors() != null
+ && mCommentsModel.getAuthors().size() > 0;
+ }
+
+ private List<AuthorModel> getAuthors() {
+ if (!hasAuthors()) {
+ return new ArrayList<AuthorModel>(0);
+ }
+ return mCommentsModel.getAuthors();
+ }
+
+ private boolean hasPosts() {
+ return mCommentsModel != null
+ && mCommentsModel.getPosts() != null
+ && mCommentsModel.getPosts().size() > 0;
+ }
+
+ private List<PostModel> getPosts() {
+ if (!hasPosts()) {
+ return new ArrayList<PostModel>(0);
+ }
+ return mCommentsModel.getPosts();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ if (mTopPagerSelectedButtonIndex == 0 && hasAuthors() && getAuthors().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST) {
+ return true;
+ } else if (mTopPagerSelectedButtonIndex == 1 && hasPosts() && getPosts().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class AuthorsAdapter extends ArrayAdapter<AuthorModel> {
+
+ private final List<AuthorModel> list;
+ private final Activity context;
+ private final LayoutInflater inflater;
+
+ public AuthorsAdapter(Activity context, List<AuthorModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.context = context;
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final AuthorModel currentRowData = list.get(position);
+ final StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ // entries
+ holder.setEntryText(currentRowData.getName(), getResources().getColor(R.color.stats_text_color));
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getViews()));
+
+ // avatar
+ holder.networkImageView.setImageUrl(GravatarUtils.fixGravatarUrl(currentRowData.getAvatar(), mResourceVars.headerAvatarSizePx), WPNetworkImageView.ImageType.AVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ final FollowDataModel followData = currentRowData.getFollowData();
+ if (followData == null) {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ } else {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FollowHelper fh = new FollowHelper(context);
+ fh.showPopup(holder.imgMore, followData);
+ }
+ });
+ }
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ if (mTopPagerSelectedButtonIndex == 0) {
+ return R.string.stats_entry_top_commenter;
+ } else {
+ return R.string.stats_entry_posts_and_pages;
+ }
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_comments;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_comments;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_comments_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.COMMENTS, StatsService.StatsEndpointsEnum.COMMENT_FOLLOWERS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_comments);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java
new file mode 100644
index 000000000..fd1a456d8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConstants.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.ui.stats;
+
+public class StatsConstants {
+
+ // Date formatting constants
+ public static final String STATS_INPUT_DATE_FORMAT = "yyyy-MM-dd";
+ public static final String STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT = "MMM d";
+ public static final String STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT = "MMMM d";
+ public static final String STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT = "MMMM dd";
+ public static final String STATS_OUTPUT_DATE_MONTH_LONG_FORMAT = "MMMM";
+ public static final String STATS_OUTPUT_DATE_YEAR_FORMAT = "yyyy";
+
+ public static final int STATS_GRAPH_BAR_MAX_COLUMN_WIDTH_DP = 100;
+
+ public static final long STATS_SCROLL_TO_DELAY = 75L;
+ public static final long STATS_LABELS_SETUP_DELAY = 75L;
+
+ public static final String ITEM_TYPE_POST = "post";
+ public static final String ITEM_TYPE_PAGE = "page";
+ public static final String ITEM_TYPE_HOME_PAGE = "homepage";
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java
new file mode 100644
index 000000000..b5b604303
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsEvents.java
@@ -0,0 +1,276 @@
+package org.wordpress.android.ui.stats;
+
+import com.android.volley.VolleyError;
+
+import org.wordpress.android.ui.stats.models.AuthorsModel;
+import org.wordpress.android.ui.stats.models.ClicksModel;
+import org.wordpress.android.ui.stats.models.CommentFollowersModel;
+import org.wordpress.android.ui.stats.models.CommentsModel;
+import org.wordpress.android.ui.stats.models.FollowersModel;
+import org.wordpress.android.ui.stats.models.GeoviewsModel;
+import org.wordpress.android.ui.stats.models.InsightsAllTimeModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostDetailsModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostModel;
+import org.wordpress.android.ui.stats.models.InsightsPopularModel;
+import org.wordpress.android.ui.stats.models.PublicizeModel;
+import org.wordpress.android.ui.stats.models.ReferrersModel;
+import org.wordpress.android.ui.stats.models.SearchTermsModel;
+import org.wordpress.android.ui.stats.models.TagsContainerModel;
+import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel;
+import org.wordpress.android.ui.stats.models.VideoPlaysModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.ui.stats.service.StatsService.StatsEndpointsEnum;
+
+public class StatsEvents {
+ public static class UpdateStatusChanged {
+ public final boolean mUpdating;
+ public UpdateStatusChanged(boolean updating) {
+ mUpdating = updating;
+ }
+ }
+
+ public abstract static class SectionUpdatedAbstract {
+ public final String mRequestBlogId; // This is the remote blog ID
+ public final StatsTimeframe mTimeframe;
+ public final String mDate;
+ public final int mMaxResultsRequested, mPageRequested;
+
+ public SectionUpdatedAbstract(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested) {
+ mRequestBlogId = blogId;
+ mDate = date;
+ mTimeframe = timeframe;
+ mMaxResultsRequested = maxResultsRequested;
+ mPageRequested = pageRequested;
+ }
+ }
+
+ public static class SectionUpdateError extends SectionUpdatedAbstract {
+
+ public final VolleyError mError;
+ public final StatsEndpointsEnum mEndPointName;
+
+ public SectionUpdateError(StatsEndpointsEnum endPointName, String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, VolleyError error) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mEndPointName = endPointName;
+ mError = error;
+ }
+ }
+
+ public static class VisitorsAndViewsUpdated extends SectionUpdatedAbstract {
+
+ public final VisitsModel mVisitsAndViews;
+
+ public VisitorsAndViewsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, VisitsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mVisitsAndViews = responseObjectModel;
+ }
+ }
+
+ public static class TopPostsUpdated extends SectionUpdatedAbstract {
+
+ public final TopPostsAndPagesModel mTopPostsAndPagesModel;
+
+ public TopPostsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, TopPostsAndPagesModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mTopPostsAndPagesModel = responseObjectModel;
+ }
+ }
+
+ public static class ReferrersUpdated extends SectionUpdatedAbstract {
+
+ public final ReferrersModel mReferrers;
+
+ public ReferrersUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, ReferrersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mReferrers = responseObjectModel;
+ }
+ }
+
+ public static class ClicksUpdated extends SectionUpdatedAbstract {
+
+ public final ClicksModel mClicks;
+
+ public ClicksUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, ClicksModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mClicks = responseObjectModel;
+ }
+ }
+
+
+ public static class AuthorsUpdated extends SectionUpdatedAbstract {
+
+ public final AuthorsModel mAuthors;
+
+ public AuthorsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, AuthorsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mAuthors = responseObjectModel;
+ }
+ }
+
+ public static class CountriesUpdated extends SectionUpdatedAbstract {
+
+ public final GeoviewsModel mCountries;
+
+ public CountriesUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, GeoviewsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mCountries = responseObjectModel;
+ }
+ }
+
+ public static class VideoPlaysUpdated extends SectionUpdatedAbstract {
+
+ public final VideoPlaysModel mVideos;
+
+ public VideoPlaysUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, VideoPlaysModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mVideos = responseObjectModel;
+ }
+ }
+
+ public static class SearchTermsUpdated extends SectionUpdatedAbstract {
+
+ public final SearchTermsModel mSearchTerms;
+
+ public SearchTermsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, SearchTermsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mSearchTerms = responseObjectModel;
+ }
+ }
+
+ public static class CommentsUpdated extends SectionUpdatedAbstract {
+
+ public final CommentsModel mComments;
+
+ public CommentsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, CommentsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mComments = responseObjectModel;
+ }
+ }
+
+ public static class CommentFollowersUpdated extends SectionUpdatedAbstract {
+
+ public final CommentFollowersModel mCommentFollowers;
+
+ public CommentFollowersUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, CommentFollowersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mCommentFollowers = responseObjectModel;
+ }
+ }
+
+ public static class TagsUpdated extends SectionUpdatedAbstract {
+
+ public final TagsContainerModel mTagsContainer;
+
+ public TagsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, TagsContainerModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mTagsContainer = responseObjectModel;
+ }
+ }
+
+ public static class PublicizeUpdated extends SectionUpdatedAbstract {
+
+ public final PublicizeModel mPublicizeModel;
+
+ public PublicizeUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, PublicizeModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mPublicizeModel = responseObjectModel;
+ }
+ }
+
+ public static class FollowersWPCOMUdated extends SectionUpdatedAbstract {
+
+ public final FollowersModel mFollowers;
+
+ public FollowersWPCOMUdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, FollowersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mFollowers = responseObjectModel;
+ }
+ }
+
+ public static class FollowersEmailUdated extends SectionUpdatedAbstract {
+
+ public final FollowersModel mFollowers;
+
+ public FollowersEmailUdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, FollowersModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mFollowers = responseObjectModel;
+ }
+ }
+
+ public static class InsightsAllTimeUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsAllTimeModel mInsightsAllTimeModel;
+
+ public InsightsAllTimeUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, InsightsAllTimeModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsAllTimeModel = responseObjectModel;
+ }
+ }
+
+ public static class InsightsPopularUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsPopularModel mInsightsPopularModel;
+
+ public InsightsPopularUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested, InsightsPopularModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsPopularModel = responseObjectModel;
+ }
+ }
+
+ public static class InsightsLatestPostSummaryUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsLatestPostModel mInsightsLatestPostModel;
+
+ public InsightsLatestPostSummaryUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested,
+ InsightsLatestPostModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsLatestPostModel = responseObjectModel;
+ }
+ }
+
+ public static class InsightsLatestPostDetailsUpdated extends SectionUpdatedAbstract {
+
+ public final InsightsLatestPostDetailsModel mInsightsLatestPostDetailsModel;
+
+ public InsightsLatestPostDetailsUpdated(String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested,
+ InsightsLatestPostDetailsModel responseObjectModel) {
+ super(blogId, timeframe, date, maxResultsRequested, pageRequested);
+ mInsightsLatestPostDetailsModel = responseObjectModel;
+ }
+ }
+
+ public static class JetpackSettingsCompleted {
+ public final boolean isError;
+ public JetpackSettingsCompleted(boolean isError) {
+ this.isError = isError;
+ }
+ }
+
+ public static class JetpackAuthError {
+ public final int mLocalBlogId; // This is the local blogID
+
+ public JetpackAuthError(int blogId) {
+ mLocalBlogId = blogId;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java
new file mode 100644
index 000000000..dc15eab17
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsFollowersFragment.java
@@ -0,0 +1,449 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.stats.models.FollowDataModel;
+import org.wordpress.android.ui.stats.models.FollowerModel;
+import org.wordpress.android.ui.stats.models.FollowersModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+
+public class StatsFollowersFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsFollowersFragment.class.getSimpleName();
+
+ private static final String ARG_REST_RESPONSE_FOLLOWERS_EMAIL = "ARG_REST_RESPONSE_FOLLOWERS_EMAIL";
+ private final Map<String, Integer> userBlogs = new HashMap<>();
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ Resources res = container.getContext().getResources();
+
+ String[] titles = {
+ res.getString(R.string.stats_followers_wpcom_selector),
+ res.getString(R.string.stats_followers_email_selector),
+ };
+
+
+ setupTopModulePager(inflater, container, view, titles);
+
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ mTotalsLabel.setVisibility(View.VISIBLE);
+ mTotalsLabel.setText("");
+
+ return view;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Single background thread used to create the blogs list in BG
+ ThreadPoolExecutor blogsListCreatorExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
+ blogsListCreatorExecutor.submit(new Thread() {
+ @Override
+ public void run() {
+ // Read all the dotcomBlog blogs and get the list of home URLs.
+ // This will be used later to check if the user is a member of followers blog marked as private.
+ List<Map<String, Object>> dotComUserBlogs = WordPress.wpDB.getBlogsBy("dotcomFlag=1",
+ new String[]{"homeURL"});
+ for (Map<String, Object> blog : dotComUserBlogs) {
+ if (blog != null && blog.get("homeURL") != null && blog.get("blogId") != null) {
+ String normURL = normalizeAndRemoveScheme(blog.get("homeURL").toString());
+ Integer blogID = (Integer) blog.get("blogId");
+ userBlogs.put(normURL, blogID);
+ }
+ }
+ }
+ });
+ }
+
+ private FollowersModel mFollowersWPCOM;
+ private FollowersModel mFollowersEmail;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mFollowersWPCOM != null || mFollowersEmail != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mFollowersWPCOM != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mFollowersWPCOM);
+ }
+ if (mFollowersEmail != null) {
+ outState.putSerializable(ARG_REST_RESPONSE_FOLLOWERS_EMAIL, mFollowersEmail);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mFollowersWPCOM = (FollowersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE_FOLLOWERS_EMAIL)) {
+ mFollowersEmail = (FollowersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE_FOLLOWERS_EMAIL);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.FollowersWPCOMUdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mFollowersWPCOM = event.mFollowers;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.FollowersEmailUdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mFollowersEmail = event.mFollowers;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mFollowersWPCOM = null;
+ mFollowersEmail = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!hasDataAvailable()) {
+ showHideNoResultsUI(true);
+ mTotalsLabel.setText(getTotalFollowersLabel(0));
+ return;
+ }
+
+ mTotalsLabel.setVisibility(View.VISIBLE);
+
+ final FollowersModel followersModel = getCurrentDataModel();
+
+ if (followersModel != null && followersModel.getFollowers() != null &&
+ followersModel.getFollowers().size() > 0) {
+ ArrayAdapter adapter = new DotComFollowerAdapter(getActivity(), followersModel.getFollowers());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+
+ if (mTopPagerSelectedButtonIndex == 0) {
+ mTotalsLabel.setText(getTotalFollowersLabel(followersModel.getTotalWPCom()));
+ } else {
+ mTotalsLabel.setText(getTotalFollowersLabel(followersModel.getTotalEmail()));
+ }
+
+ if (isSingleView()) {
+ if (followersModel.getPages() > 1) {
+ mBottomPaginationContainer.setVisibility(View.VISIBLE);
+ mTopPaginationContainer.setVisibility(View.VISIBLE);
+ String paginationLabel = String.format(
+ getString(R.string.stats_pagination_label),
+ FormatUtils.formatDecimal(followersModel.getPage()),
+ FormatUtils.formatDecimal(followersModel.getPages())
+ );
+ mBottomPaginationText.setText(paginationLabel);
+ mTopPaginationText.setText(paginationLabel);
+ setNavigationButtonsEnabled(true);
+
+ // Setting up back buttons
+ if (followersModel.getPage() == 1) {
+ // first page. No go back buttons
+ setNavigationBackButtonsVisibility(false);
+ } else {
+ setNavigationBackButtonsVisibility(true);
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setNavigationButtonsEnabled(false);
+ refreshStats(
+ followersModel.getPage() - 1,
+ new StatsService.StatsEndpointsEnum[]{sectionsToUpdate()[mTopPagerSelectedButtonIndex]}
+ );
+ }
+ };
+ mBottomPaginationGoBackButton.setOnClickListener(clickListener);
+ mTopPaginationGoBackButton.setOnClickListener(clickListener);
+ }
+
+ // Setting up forward buttons
+ if (followersModel.getPage() == followersModel.getPages()) {
+ // last page. No go forward buttons
+ setNavigationForwardButtonsVisibility(false);
+ } else {
+ setNavigationForwardButtonsVisibility(true);
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setNavigationButtonsEnabled(false);
+ refreshStats(
+ followersModel.getPage() + 1,
+ new StatsService.StatsEndpointsEnum[]{sectionsToUpdate()[mTopPagerSelectedButtonIndex]}
+ );
+ }
+ };
+ mBottomPaginationGoForwardButton.setOnClickListener(clickListener);
+ mTopPaginationGoForwardButton.setOnClickListener(clickListener);
+ }
+
+ // Change the total number of followers label by adding the current paging info
+ int startIndex = followersModel.getPage() * StatsService.MAX_RESULTS_REQUESTED_PER_PAGE - StatsService.MAX_RESULTS_REQUESTED_PER_PAGE + 1;
+ int endIndex = startIndex + followersModel.getFollowers().size() - 1;
+ String pagedLabel = getString(
+ mTopPagerSelectedButtonIndex == 0 ? R.string.stats_followers_total_wpcom_paged : R.string.stats_followers_total_email_paged,
+ startIndex,
+ endIndex,
+ FormatUtils.formatDecimal(mTopPagerSelectedButtonIndex == 0 ? followersModel.getTotalWPCom() : followersModel.getTotalEmail())
+ );
+ mTotalsLabel.setText(pagedLabel);
+ } else {
+ // No paging required. Hide the controls.
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTopPaginationContainer.setVisibility(View.GONE);
+ }
+ }
+ } else {
+ showHideNoResultsUI(true);
+ mBottomPaginationContainer.setVisibility(View.GONE);
+ mTotalsLabel.setText(getTotalFollowersLabel(0));
+ }
+
+ // Always visible. Even if the current tab is empty, otherwise the user can't switch tab
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ }
+
+ private FollowersModel getCurrentDataModel() {
+ return mTopPagerSelectedButtonIndex == 0 ? mFollowersWPCOM : mFollowersEmail;
+ }
+
+ private void setNavigationBackButtonsVisibility(boolean visible) {
+ mBottomPaginationGoBackButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ mTopPaginationGoBackButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void setNavigationForwardButtonsVisibility(boolean visible) {
+ mBottomPaginationGoForwardButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ mTopPaginationGoForwardButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void setNavigationButtonsEnabled(boolean enable) {
+ mBottomPaginationGoBackButton.setEnabled(enable);
+ mBottomPaginationGoForwardButton.setEnabled(enable);
+ mTopPaginationGoBackButton.setEnabled(enable);
+ mTopPaginationGoForwardButton.setEnabled(enable);
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ if (!hasDataAvailable()) {
+ return false;
+ }
+ FollowersModel followersModel = getCurrentDataModel();
+ return !(followersModel == null || followersModel.getFollowers() == null
+ || followersModel.getFollowers().size() < MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+
+ }
+
+ private String getTotalFollowersLabel(int total) {
+ final String totalFollowersLabel;
+
+ if (mTopPagerSelectedButtonIndex == 0) {
+ totalFollowersLabel = getString(R.string.stats_followers_total_wpcom);
+ } else {
+ totalFollowersLabel = getString(R.string.stats_followers_total_email);
+ }
+
+ return String.format(totalFollowersLabel, FormatUtils.formatDecimal(total));
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class DotComFollowerAdapter extends ArrayAdapter<FollowerModel> {
+
+ private final List<FollowerModel> list;
+ private final Activity context;
+ private final LayoutInflater inflater;
+
+ public DotComFollowerAdapter(Activity context, List<FollowerModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.context = context;
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // set a min-width value that is large enough to contains the "since" string
+ LinearLayout totalContainer = (LinearLayout) rowView.findViewById(R.id.stats_list_cell_total_container);
+ int dp64 = DisplayUtils.dpToPx(rowView.getContext(), 64);
+ totalContainer.setMinimumWidth(dp64);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final FollowerModel currentRowData = list.get(position);
+ final StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ holder.entryTextView.setTextColor(context.getResources().getColor(R.color.stats_text_color));
+ holder.rowContent.setClickable(false);
+
+ final FollowDataModel followData = currentRowData.getFollowData();
+
+ // entries
+ if (mTopPagerSelectedButtonIndex == 0 && !(TextUtils.isEmpty(currentRowData.getURL()) && followData == null)) {
+ // WPCOM followers with no empty URL or empty follow data
+
+ final int blogID;
+ if (followData == null) {
+ // If follow data is empty, we cannot follow the blog, or access it in the reader.
+ // We need to check if the user is a member of this blog.
+ // If so, we can launch open the reader, otherwise open the blog in the in-app browser.
+ String normURL = normalizeAndRemoveScheme(currentRowData.getURL());
+ blogID = userBlogs.containsKey(normURL) ? userBlogs.get(normURL) : Integer.MIN_VALUE;
+ } else {
+ blogID = followData.getSiteID();
+ }
+
+ if (blogID > Integer.MIN_VALUE) {
+ // Open the Reader
+ holder.entryTextView.setText(currentRowData.getLabel());
+ holder.rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ context,
+ blogID
+ );
+ }
+ });
+ } else {
+ // Open the in-app web browser
+ holder.setEntryTextOrLink(currentRowData.getURL(), currentRowData.getLabel());
+ }
+ holder.entryTextView.setTextColor(context.getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ // Email followers, or wpcom followers with empty URL and no blogID
+ holder.setEntryText(currentRowData.getLabel());
+ }
+
+ // since date
+ holder.totalsTextView.setText(
+ StatsUtils.getSinceLabel(
+ context,
+ currentRowData.getDateSubscribed()
+ )
+ );
+
+ // Avatar
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(currentRowData.getAvatar(), mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.AVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ if (followData == null) {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ } else {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FollowHelper fh = new FollowHelper(context);
+ fh.showPopup(holder.imgMore, followData);
+ }
+ });
+ }
+
+ return rowView;
+ }
+
+
+ }
+
+ private static String normalizeAndRemoveScheme(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return "";
+ }
+ String normURL = UrlUtils.normalizeUrl(url.toLowerCase());
+ int pos = normURL.indexOf("://");
+ if (pos > -1) {
+ return normURL.substring(pos + 3);
+ } else {
+ return normURL;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_followers;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_followers;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_followers;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_followers_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.FOLLOWERS_WPCOM, StatsService.StatsEndpointsEnum.FOLLOWERS_EMAIL
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_followers);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java
new file mode 100644
index 000000000..3622c817a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsGeoviewsFragment.java
@@ -0,0 +1,299 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.GeoviewModel;
+import org.wordpress.android.ui.stats.models.GeoviewsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+
+public class StatsGeoviewsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsGeoviewsFragment.class.getSimpleName();
+
+ private GeoviewsModel mCountries;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mCountries != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mCountries);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mCountries = (GeoviewsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.CountriesUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mCountries = event.mCountries;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mCountries = null;
+ showErrorUI(event.mError);
+ }
+
+ private void hideMap() {
+ if (!isAdded()) {
+ return;
+ }
+
+ mTopPagerContainer.setVisibility(View.GONE);
+ }
+
+ private void showMap(final List<GeoviewModel> countries) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // setting up different margins for the map. We're basically remove left margins since the
+ // chart service produce a map that's slightly shifted on the right. See the Web version.
+ int dp4 = DisplayUtils.dpToPx(mTopPagerContainer.getContext(), 4);
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ layoutParams.setMargins(0, 0, dp4, 0);
+ mTopPagerContainer.setLayoutParams(layoutParams);
+
+ mTopPagerContainer.removeAllViews();
+
+ // must wait for mTopPagerContainer to be fully laid out (ie: measured). Then we can read the width and
+ // calculate the right height for the map div
+ mTopPagerContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mTopPagerContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ if (!isAdded()) {
+ return;
+ }
+
+ StringBuilder dataToLoad = new StringBuilder();
+
+ for (int i = 0; i < countries.size(); i++) {
+ final GeoviewModel currentCountry = countries.get(i);
+ dataToLoad.append("['").append(currentCountry.getCountryFullName()).append("',")
+ .append(currentCountry.getViews()).append("],");
+ }
+
+ // This is the label that is shown when the user taps on a region
+ String label = getResources().getString(getTotalsLabelResId());
+
+ // See: https://developers.google.com/chart/interactive/docs/gallery/geochart
+ // Loading the v42 of the Google Charts API, since the latest stable version has a problem with the legend. https://github.com/wordpress-mobile/WordPress-Android/issues/4131
+ // https://developers.google.com/chart/interactive/docs/release_notes#release-candidate-details
+ String htmlPage = "<html>" +
+ "<head>" +
+ "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>" +
+ "<script type=\"text/javascript\" src=\"https://www.google.com/jsapi\"></script>" +
+ "<script type=\"text/javascript\">" +
+ "google.charts.load('42', {'packages':['geochart']});" +
+ "google.charts.setOnLoadCallback(drawRegionsMap);" +
+ "function drawRegionsMap() {" +
+ "var data = google.visualization.arrayToDataTable(" +
+ "[" +
+ "['Country', '" + label + "']," +
+ dataToLoad +
+ "]);" +
+ "var options = {keepAspectRatio: true, region: 'world', colorAxis: { colors: [ '#FFF088', '#F34605' ] }, enableRegionInteractivity: true};" +
+ "var chart = new google.visualization.GeoChart(document.getElementById('regions_div'));" +
+ "chart.draw(data, options);" +
+ "}" +
+ "</script>" +
+ "</head>" +
+ "<body>" +
+ "<div id=\"regions_div\" style=\"width: 100%; height: 100%;\"></div>" +
+ "</body>" +
+ "</html>";
+
+ WebView webView = new WebView(getActivity());
+ mTopPagerContainer.addView(webView);
+
+ int width = mTopPagerContainer.getWidth();
+ int height = width * 3 / 4;
+
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) webView.getLayoutParams();
+ params.width = WebView.LayoutParams.MATCH_PARENT;
+ params.height = height;
+
+ webView.setLayoutParams(params);
+
+ webView.setWebViewClient(new MyWebViewClient()); // Hide map in case of unrecoverable errors
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
+ webView.loadData(htmlPage, "text/html", "UTF-8");
+
+ }
+ });
+ mTopPagerContainer.setVisibility(View.VISIBLE);
+ }
+
+ // Hide the Map in case of errors
+ private class MyWebViewClient extends WebViewClient {
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ super.onReceivedError(view, errorCode, description, failingUrl);
+ mTopPagerContainer.setVisibility(View.GONE);
+ AppLog.e(AppLog.T.STATS, "Cannot load geochart."
+ + " ErrorCode: " + errorCode
+ + " Description: " + description
+ + " Failing URL: " + failingUrl);
+ }
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ super.onReceivedSslError(view, handler, error);
+ mTopPagerContainer.setVisibility(View.GONE);
+ AppLog.e(AppLog.T.STATS, "Cannot load geochart. SSL ERROR. " + error.toString());
+ }
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasCountries()) {
+ List<GeoviewModel> countries = getCountries();
+ ArrayAdapter adapter = new GeoviewsAdapter(getActivity(), countries);
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ showMap(countries);
+ } else {
+ showHideNoResultsUI(true);
+ hideMap();
+ }
+ }
+
+ private boolean hasCountries() {
+ return mCountries != null && mCountries.getCountries() != null;
+ }
+
+ private List<GeoviewModel> getCountries() {
+ if (!hasCountries()) {
+ return null;
+ }
+ return mCountries.getCountries();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return (hasCountries()
+ && mCountries.getCountries().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST);
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class GeoviewsAdapter extends ArrayAdapter<GeoviewModel> {
+
+ private final List<GeoviewModel> list;
+ private final Activity context;
+ private final LayoutInflater inflater;
+
+ public GeoviewsAdapter(Activity context, List<GeoviewModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.context = context;
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final GeoviewModel currentRowData = list.get(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+ // fill data
+ String entry = currentRowData.getCountryFullName();
+ String imageUrl = currentRowData.getFlatFlagIconURL();
+ int total = currentRowData.getViews();
+
+ holder.setEntryText(entry);
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // image (country flag)
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(imageUrl, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.BLAVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_country;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_geoviews;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_geoviews_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.GEO_VIEWS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_countries);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java
new file mode 100644
index 000000000..8fa6a17f5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsAllTimeFragment.java
@@ -0,0 +1,99 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.InsightsAllTimeModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+
+public class StatsInsightsAllTimeFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsAllTimeFragment.class.getSimpleName();
+
+ private InsightsAllTimeModel mInsightsAllTimeModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mInsightsAllTimeModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mInsightsAllTimeModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mInsightsAllTimeModel = (InsightsAllTimeModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsAllTimeUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mInsightsAllTimeModel = event.mInsightsAllTimeModel;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mInsightsAllTimeModel = null;
+ showErrorUI(event.mError);
+ }
+
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_all_time_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ TextView postsTextView = (TextView) ll.findViewById(R.id.stats_all_time_posts);
+ TextView viewsTextView = (TextView) ll.findViewById(R.id.stats_all_time_views);
+ TextView visitorsTextView = (TextView) ll.findViewById(R.id.stats_all_time_visitors);
+ TextView besteverTextView = (TextView) ll.findViewById(R.id.stats_all_time_bestever);
+ TextView besteverDateTextView = (TextView) ll.findViewById(R.id.stats_all_time_bestever_date);
+
+
+ postsTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getPosts()));
+ viewsTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getViews()));
+ visitorsTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getVisitors()));
+
+ besteverTextView.setText(FormatUtils.formatDecimal(mInsightsAllTimeModel.getViewsBestDayTotal()));
+ besteverDateTextView.setText(
+ StatsUtils.parseDate(mInsightsAllTimeModel.getViewsBestDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, "MMMM dd, yyyy")
+ );
+
+ mResultContainer.addView(ll);
+ }
+
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.INSIGHTS_ALL_TIME
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_all_time);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java
new file mode 100644
index 000000000..c65b2053c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsLatestPostSummaryFragment.java
@@ -0,0 +1,280 @@
+package org.wordpress.android.ui.stats;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostDetailsModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+public class StatsInsightsLatestPostSummaryFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsLatestPostSummaryFragment.class.getSimpleName();
+
+ private static final String ARG_REST_RESPONSE_DETAILS = "ARG_REST_RESPONSE_DETAILS";
+
+ private InsightsLatestPostModel mInsightsLatestPostModel;
+ private InsightsLatestPostDetailsModel mInsightsLatestPostDetailsModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mInsightsLatestPostModel != null && mInsightsLatestPostDetailsModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mInsightsLatestPostModel);
+ outState.putSerializable(ARG_REST_RESPONSE_DETAILS, mInsightsLatestPostDetailsModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mInsightsLatestPostModel = (InsightsLatestPostModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE_DETAILS)) {
+ mInsightsLatestPostDetailsModel = (InsightsLatestPostDetailsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE_DETAILS);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsLatestPostSummaryUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ if (event.mInsightsLatestPostModel == null) {
+ showErrorUI();
+ return;
+ }
+
+ mInsightsLatestPostModel = event.mInsightsLatestPostModel;
+
+ // check if there is a post "published" on the blog
+ View mainView = getView();
+ if (mainView != null) {
+ mainView.setVisibility(mInsightsLatestPostModel.isLatestPostAvailable() ? View.VISIBLE : View.GONE);
+ }
+ if (!mInsightsLatestPostModel.isLatestPostAvailable()) {
+ // No need to go further into UI updating. There are no posts on this blog and the
+ // entire fragment is hidden.
+ return;
+ }
+
+ // Check if we already have the number of "views" for the latest post
+ if (mInsightsLatestPostModel.getPostViewsCount() == Integer.MIN_VALUE) {
+ // we don't have the views count. Need to call the service again here
+ refreshStats(mInsightsLatestPostModel.getPostID(),
+ new StatsService.StatsEndpointsEnum[]{StatsService.StatsEndpointsEnum.INSIGHTS_LATEST_POST_VIEWS});
+ showPlaceholderUI();
+ } else {
+ updateUI();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsLatestPostDetailsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ if (mInsightsLatestPostModel == null || event.mInsightsLatestPostDetailsModel == null) {
+ showErrorUI();
+ return;
+ }
+
+ mInsightsLatestPostDetailsModel = event.mInsightsLatestPostDetailsModel;
+ mInsightsLatestPostModel.setPostViewsCount(mInsightsLatestPostDetailsModel.getPostViewsCount());
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+
+ if (!shouldUpdateFragmentOnErrorEvent(event)
+ && event.mEndPointName != StatsService.StatsEndpointsEnum.INSIGHTS_LATEST_POST_VIEWS ) {
+ return;
+ }
+
+ mInsightsLatestPostDetailsModel = null;
+ mInsightsLatestPostModel = null;
+ showErrorUI(event.mError);
+ }
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ // check if there are posts "published" on the blog
+ if (!mInsightsLatestPostModel.isLatestPostAvailable()) {
+ // No need to go further into UI updating. There are no posts on this blog and the
+ // entire fragment is hidden.
+ return;
+ }
+
+ TextView moduleTitle = (TextView) getView().findViewById(R.id.stats_module_title);
+ moduleTitle.setOnClickListener(ViewsTabOnClickListener);
+ moduleTitle.setTextColor(getResources().getColor(R.color.stats_link_text_color));
+
+ // update the tabs and the text now
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_latest_post_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ String trendLabel = getString(R.string.stats_insights_latest_post_trend);
+ String sinceLabel = StatsUtils.getSinceLabel(
+ getActivity(),
+ mInsightsLatestPostModel.getPostDate()
+ ).toLowerCase();
+
+ String postTitle = StringEscapeUtils.unescapeHtml(mInsightsLatestPostModel.getPostTitle());
+ if (TextUtils.isEmpty(postTitle)) {
+ postTitle = getString(R.string.stats_insights_latest_post_no_title);
+ }
+
+ final String trendLabelFormatted = String.format(
+ trendLabel, sinceLabel, postTitle);
+
+ int startIndex, endIndex;
+ startIndex = trendLabelFormatted.indexOf(postTitle);
+ endIndex = startIndex + postTitle.length() +1;
+
+ Spannable descriptionTextToSpan = new SpannableString(trendLabelFormatted);
+ descriptionTextToSpan.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.stats_link_text_color)),
+ startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ TextView trendLabelTextField = (TextView) ll.findViewById(R.id.stats_post_trend_label);
+ trendLabelTextField.setText(descriptionTextToSpan);
+ trendLabelTextField.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ StatsUtils.openPostInReaderOrInAppWebview(getActivity(),
+ mInsightsLatestPostModel.getBlogID(),
+ String.valueOf(mInsightsLatestPostModel.getPostID()),
+ StatsConstants.ITEM_TYPE_POST,
+ mInsightsLatestPostModel.getPostURL());
+ }
+ });
+
+ LinearLayout tabs = (LinearLayout) ll.findViewById(R.id.stats_latest_post_tabs);
+
+ for (int i = 0; i < tabs.getChildCount(); i++) {
+ LinearLayout currentTab = (LinearLayout) tabs.getChildAt(i);
+ switch (i) {
+ case 0:
+ setupTab(currentTab, FormatUtils.formatDecimal(mInsightsLatestPostModel.getPostViewsCount()), StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS);
+ break;
+ case 1:
+ setupTab(currentTab, FormatUtils.formatDecimal(mInsightsLatestPostModel.getPostLikeCount()), StatsVisitorsAndViewsFragment.OverviewLabel.LIKES);
+ break;
+ case 2:
+ setupTab(currentTab, FormatUtils.formatDecimal(mInsightsLatestPostModel.getPostCommentCount()), StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS);
+ break;
+ }
+ }
+
+ mResultContainer.addView(ll);
+ }
+
+ private void setupTab(LinearLayout currentTab, String total, final StatsVisitorsAndViewsFragment.OverviewLabel itemType) {
+ final TextView label;
+ final TextView value;
+ final ImageView icon;
+
+ currentTab.setTag(itemType);
+ // Only Views is clickable here
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS) {
+ currentTab.setOnClickListener(ViewsTabOnClickListener);
+ } else {
+ currentTab.setClickable(false);
+ }
+
+ label = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_label);
+ label.setText(itemType.getLabel());
+ value = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_value);
+ value.setText(total);
+ if (total.equals("0")) {
+ value.setTextColor(getResources().getColor(R.color.grey));
+ } else {
+ // Only Views is clickable here.
+ // Likes and Comments shouldn't link anywhere because they don't have summaries
+ // so their color should be Gray Darken 30 or #3d596d
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS) {
+ value.setTextColor(getResources().getColor(R.color.blue_wordpress));
+ } else {
+ value.setTextColor(getResources().getColor(R.color.grey_darken_30));
+ }
+ }
+ icon = (ImageView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_icon);
+ icon.setImageDrawable(getTabIcon(itemType));
+
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS) {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_white);
+ } else {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_white);
+ }
+ }
+
+ private final View.OnClickListener ViewsTabOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Another check that the data is available
+ if (mInsightsLatestPostModel == null) {
+ showErrorUI();
+ return;
+ }
+
+ PostModel postModel = new PostModel(
+ mInsightsLatestPostModel.getBlogID(),
+ String.valueOf(mInsightsLatestPostModel.getPostID()),
+ mInsightsLatestPostModel.getPostTitle(),
+ mInsightsLatestPostModel.getPostURL(),
+ StatsConstants.ITEM_TYPE_POST);
+ ActivityLauncher.viewStatsSinglePostDetails(getActivity(), postModel);
+ }
+ };
+
+ private Drawable getTabIcon(final StatsVisitorsAndViewsFragment.OverviewLabel labelItem) {
+ switch (labelItem) {
+ case VISITORS:
+ return getResources().getDrawable(R.drawable.stats_icon_visitors);
+ case COMMENTS:
+ return getResources().getDrawable(R.drawable.stats_icon_comments);
+ case LIKES:
+ return getResources().getDrawable(R.drawable.stats_icon_likes);
+ default:
+ // Views and when no prev match
+ return getResources().getDrawable(R.drawable.stats_icon_views);
+ }
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.INSIGHTS_LATEST_POST_SUMMARY,
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_latest_post_summary);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java
new file mode 100644
index 000000000..247ca80b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsMostPopularFragment.java
@@ -0,0 +1,149 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.InsightsPopularModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+
+public class StatsInsightsMostPopularFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsMostPopularFragment.class.getSimpleName();
+
+ private InsightsPopularModel mInsightsPopularModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mInsightsPopularModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mInsightsPopularModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mInsightsPopularModel = (InsightsPopularModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.InsightsPopularUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mInsightsPopularModel = event.mInsightsPopularModel;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mInsightsPopularModel = null;
+ showErrorUI(event.mError);
+ }
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_most_popular_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ int dayOfTheWeek = mInsightsPopularModel.getHighestDayOfWeek();
+
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ c.setTimeInMillis(System.currentTimeMillis());
+ switch (dayOfTheWeek) {
+ case 0:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ break;
+ case 1:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);
+ break;
+ case 2:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);
+ break;
+ case 3:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY);
+ break;
+ case 4:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+ break;
+ case 5:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
+ break;
+ case 6:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+ break;
+ }
+
+ DateFormat formatter = new SimpleDateFormat("EEEE");
+ final TextView mostPopularDayTextView = (TextView) ll.findViewById(R.id.stats_most_popular_day);
+ mostPopularDayTextView.setText(formatter.format(c.getTime()));
+ final TextView mostPopularDayPercentTextView = (TextView) ll.findViewById(R.id.stats_most_popular_day_percent);
+ mostPopularDayPercentTextView.setText(
+ String.format(
+ getString(R.string.stats_insights_most_popular_percent_views),
+ roundToInteger(mInsightsPopularModel.getHighestDayPercent())
+ )
+ );
+
+ TextView mostPopularHourTextView = (TextView) ll.findViewById(R.id.stats_most_popular_hour);
+ DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(getActivity());
+ c.set(Calendar.HOUR_OF_DAY, mInsightsPopularModel.getHighestHour());
+ c.set(Calendar.MINUTE, 0);
+ mostPopularHourTextView.setText(timeFormat.format(c.getTime()));
+ final TextView mostPopularHourPercentTextView = (TextView) ll.findViewById(R.id.stats_most_popular_hour_percent);
+ mostPopularHourPercentTextView.setText(
+ String.format(
+ getString(R.string.stats_insights_most_popular_percent_views),
+ roundToInteger(mInsightsPopularModel.getHighestHourPercent())
+ )
+ );
+
+ mResultContainer.addView(ll);
+ }
+
+ /*
+ * Round a double to the closest integer
+ *
+ * If the decimal part is less than 0.5, the integer part stays the same,
+ * and truncation gives the right result.
+ * If the decimal part is more that 0.5, the integer part increments,
+ * and again truncation gives what we want.
+ *
+ */
+ private int roundToInteger(double inputValue) {
+ return (int) Math.floor(inputValue + 0.5);
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.INSIGHTS_POPULAR
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_popular);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java
new file mode 100644
index 000000000..0f1ae466b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsInsightsTodayFragment.java
@@ -0,0 +1,200 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.List;
+
+
+public class StatsInsightsTodayFragment extends StatsAbstractInsightsFragment {
+ public static final String TAG = StatsInsightsTodayFragment.class.getSimpleName();
+
+ // Container Activity must implement this interface
+ public interface OnInsightsTodayClickListener {
+ void onInsightsTodayClicked(StatsVisitorsAndViewsFragment.OverviewLabel item);
+ }
+
+ private OnInsightsTodayClickListener mListener;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mListener = (OnInsightsTodayClickListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnInsightsTodayClickListener");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ TextView moduleTitle = (TextView) view.findViewById(R.id.stats_module_title);
+ moduleTitle.setTag(StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS);
+ moduleTitle.setOnClickListener(ButtonsOnClickListener);
+ moduleTitle.setTextColor(getResources().getColor(R.color.stats_link_text_color));
+ return view;
+ }
+
+
+ private VisitsModel mVisitsModel;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mVisitsModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mVisitsModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mVisitsModel = (VisitsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.VisitorsAndViewsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mVisitsModel = event.mVisitsAndViews;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mVisitsModel = null;
+ showErrorUI(event.mError);
+ }
+
+ protected void updateUI() {
+ super.updateUI();
+
+ if (!isAdded() || !hasDataAvailable()) {
+ return;
+ }
+
+ if (mVisitsModel.getVisits() == null || mVisitsModel.getVisits().size() == 0) {
+ showErrorUI();
+ return;
+ }
+
+ List<VisitModel> visits = mVisitsModel.getVisits();
+ VisitModel data = visits.get(visits.size() - 1);
+
+ LinearLayout ll = (LinearLayout) getActivity().getLayoutInflater()
+ .inflate(R.layout.stats_insights_today_item, (ViewGroup) mResultContainer.getRootView(), false);
+
+ LinearLayout tabs = (LinearLayout) ll.findViewById(R.id.stats_post_tabs);
+
+ for (int i = 0; i < tabs.getChildCount(); i++) {
+ LinearLayout currentTab = (LinearLayout) tabs.getChildAt(i);
+ switch (i) {
+ case 0:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getViews()), StatsVisitorsAndViewsFragment.OverviewLabel.VIEWS);
+ break;
+ case 1:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getVisitors()), StatsVisitorsAndViewsFragment.OverviewLabel.VISITORS );
+ break;
+ case 2:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getLikes()), StatsVisitorsAndViewsFragment.OverviewLabel.LIKES );
+ break;
+ case 3:
+ setupTab(currentTab, FormatUtils.formatDecimal(data.getComments()), StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS );
+ break;
+ }
+ }
+
+ mResultContainer.addView(ll);
+ }
+
+ private void setupTab(LinearLayout currentTab, String total, final StatsVisitorsAndViewsFragment.OverviewLabel itemType) {
+ final TextView label;
+ final TextView value;
+ final ImageView icon;
+
+ currentTab.setTag(itemType);
+ currentTab.setOnClickListener(ButtonsOnClickListener);
+
+ label = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_label);
+ label.setText(itemType.getLabel());
+ value = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_value);
+ value.setText(total);
+ if (total.equals("0")) {
+ value.setTextColor(getResources().getColor(R.color.grey));
+ } else {
+ value.setTextColor(getResources().getColor(R.color.blue_wordpress));
+ }
+ icon = (ImageView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_icon);
+ icon.setImageDrawable(getTabIcon(itemType));
+
+ if (itemType == StatsVisitorsAndViewsFragment.OverviewLabel.COMMENTS) {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_white);
+ } else {
+ currentTab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_white);
+ }
+ }
+
+ private final View.OnClickListener ButtonsOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+ if (mListener == null) {
+ return;
+ }
+ StatsVisitorsAndViewsFragment.OverviewLabel tag = (StatsVisitorsAndViewsFragment.OverviewLabel) v.getTag();
+ mListener.onInsightsTodayClicked(tag);
+ }
+ };
+
+ private Drawable getTabIcon(final StatsVisitorsAndViewsFragment.OverviewLabel labelItem) {
+ switch (labelItem) {
+ case VISITORS:
+ return getResources().getDrawable(R.drawable.stats_icon_visitors);
+ case COMMENTS:
+ return getResources().getDrawable(R.drawable.stats_icon_comments);
+ case LIKES:
+ return getResources().getDrawable(R.drawable.stats_icon_likes);
+ default:
+ // Views and when no prev match
+ return getResources().getDrawable(R.drawable.stats_icon_views);
+ }
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.VISITS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_insights_today);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java
new file mode 100644
index 000000000..84522cf08
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsPublicizeFragment.java
@@ -0,0 +1,238 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.PublicizeModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.List;
+
+
+public class StatsPublicizeFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsPublicizeFragment.class.getSimpleName();
+
+ private PublicizeModel mPublicizeData;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mPublicizeData != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mPublicizeData != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mPublicizeData);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mPublicizeData = (PublicizeModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.PublicizeUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mPublicizeData = event.mPublicizeModel;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mPublicizeData = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasPublicize()) {
+ ArrayAdapter adapter = new PublicizeAdapter(getActivity(), getPublicize());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasPublicize() {
+ return mPublicizeData != null
+ && mPublicizeData.getServices() != null
+ && mPublicizeData.getServices().size() > 0;
+ }
+
+ private List<SingleItemModel> getPublicize() {
+ if (!hasPublicize()) {
+ return null;
+ }
+ return mPublicizeData.getServices();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return false;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class PublicizeAdapter extends ArrayAdapter<SingleItemModel> {
+
+ private final List<SingleItemModel> list;
+ private final LayoutInflater inflater;
+
+ public PublicizeAdapter(Activity context, List<SingleItemModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ final StatsViewHolder holder;
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ holder = new StatsViewHolder(rowView);
+ holder.networkImageView.setErrorImageResId(R.drawable.stats_icon_default_site_avatar);
+ holder.networkImageView.setDefaultImageResId(R.drawable.stats_icon_default_site_avatar);
+ rowView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) rowView.getTag();
+ }
+
+ final SingleItemModel currentRowData = list.get(position);
+
+ String serviceName = currentRowData.getTitle();
+
+ // entries
+ holder.setEntryText(getServiceName(serviceName));
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // image
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(getServiceImage(serviceName), mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.BLAVATAR);
+ holder.networkImageView.setVisibility(View.VISIBLE);
+
+ return rowView;
+ }
+ }
+
+ private String getServiceImage(String service) {
+ String serviceIconURL;
+
+ switch (service) {
+ case "facebook":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/2343ec78a04c6ea9d80806345d31fd78?s=";
+ break;
+ case "twitter":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/7905d1c4e12c54933a44d19fcd5f9356?s=";
+ break;
+ case "tumblr":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/84314f01e87cb656ba5f382d22d85134?s=";
+ break;
+ case "google_plus":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/4a4788c1dfc396b1f86355b274cc26b3?s=";
+ break;
+ case "linkedin":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/f54db463750940e0e7f7630fe327845e?s=";
+ break;
+ case "path":
+ serviceIconURL = "https://secure.gravatar.com/blavatar/3a03c8ce5bf1271fb3760bb6e79b02c1?s=";
+ break;
+ default:
+ return null;
+ }
+
+ return serviceIconURL + mResourceVars.headerAvatarSizePx;
+ }
+
+ private String getServiceName(String service) {
+ if (service.equals("facebook")) {
+ return "Facebook";
+ }
+
+ if (service.equals("twitter")) {
+ return "Twitter";
+ }
+
+ if (service.equals("tumblr")) {
+ return "Tumblr";
+ }
+
+ if (service.equals("google_plus")) {
+ return "Google+";
+ }
+
+ if (service.equals("linkedin")) {
+ return "LinkedIn";
+ }
+
+ if (service.equals("path")) {
+ return "Path";
+ }
+
+ return StringUtils.capitalize(service);
+ }
+
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_publicize;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_publicize;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_publicize;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_publicize_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.PUBLICIZE
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_publicize);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java
new file mode 100644
index 000000000..c94293c7b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsReferrersFragment.java
@@ -0,0 +1,340 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.ReferrerGroupModel;
+import org.wordpress.android.ui.stats.models.ReferrerResultModel;
+import org.wordpress.android.ui.stats.models.ReferrersModel;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StatsReferrersFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsReferrersFragment.class.getSimpleName();
+
+ private ReferrersModel mReferrers;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mReferrers != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mReferrers);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mReferrers = (ReferrersModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.ReferrersUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mReferrers = event.mReferrers;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mReferrers = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasReferrers()) {
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), getReferrersGroups());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasReferrers() {
+ return mReferrers != null
+ && mReferrers.getGroups() != null
+ && mReferrers.getGroups().size() > 0;
+ }
+
+ private List<ReferrerGroupModel> getReferrersGroups() {
+ if (!hasReferrers()) {
+ return new ArrayList<ReferrerGroupModel>(0);
+ }
+ return mReferrers.getGroups();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasReferrers() && getReferrersGroups().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.REFERRERS
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_referrers;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_referrers_title;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_referrers_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ public final Activity act;
+ private final List<ReferrerGroupModel> groups;
+ private final List<List<MyChildModel>> children;
+
+ public MyExpandableListAdapter(Activity act, List<ReferrerGroupModel> groups) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(act);
+ this.act = act;
+
+ // The code below flattens the 3-levels tree of children to a 2-levels structure
+ // that will be used later to populate the UI
+ this.children = new ArrayList<>(groups.size());
+ // pre-populate the structure with null values
+ for (int i = 0; i < groups.size(); i++) {
+ this.children.add(null);
+ }
+
+ for (int i = 0; i < groups.size(); i++) {
+ ReferrerGroupModel currentGroup = groups.get(i);
+ List<MyChildModel> currentGroupChildren = new ArrayList<>();
+ List<ReferrerResultModel> childrenOfLevelOne = currentGroup.getResults();
+ if (childrenOfLevelOne != null) {
+ // Children at first level could be a single item or another tree
+ // Levels 2 children are skipped in the UI.
+ for (ReferrerResultModel singleLevelOneChild : childrenOfLevelOne) {
+ // Use all the info given in the first level child.
+ MyChildModel myChild = new MyChildModel();
+ myChild.icon = singleLevelOneChild.getIcon();
+ myChild.url = singleLevelOneChild.getUrl();
+ myChild.name = singleLevelOneChild.getName();
+ myChild.views = singleLevelOneChild.getViews();
+
+ // read the URL from the first second-level child if available.
+ List<SingleItemModel> secondLevelChildren = singleLevelOneChild.getChildren();
+ if (secondLevelChildren != null && secondLevelChildren.size() > 0) {
+ SingleItemModel firstThirdLevelChild = secondLevelChildren.get(0);
+ myChild.url = firstThirdLevelChild.getUrl();
+ }
+ currentGroupChildren.add(myChild);
+ }
+ }
+ this.children.set(i, currentGroupChildren);
+ }
+ }
+
+ private final class MyChildModel {
+ String name;
+ int views;
+ String url;
+ String icon;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ List<MyChildModel> currentGroupChildren = children.get(groupPosition);
+ return currentGroupChildren.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final MyChildModel currentChild = (MyChildModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ String name = currentChild.name;
+ int views = currentChild.views;
+
+ holder.chevronImageView.setVisibility(View.GONE);
+ holder.linkImageView.setVisibility(TextUtils.isEmpty(currentChild.url) ? View.GONE : View.VISIBLE);
+ holder.setEntryTextOrLink(currentChild.url, name);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(views));
+
+ // site icon
+ holder.networkImageView.setVisibility(View.GONE);
+ if (!TextUtils.isEmpty(currentChild.icon)) {
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(currentChild.icon, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.GONE_UNTIL_AVAILABLE);
+ }
+
+ // no more btm
+ holder.imgMore.setVisibility(View.GONE);
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ List<MyChildModel> currentGroupChildren = children.get(groupPosition);
+ if (currentGroupChildren == null) {
+ return 0;
+ } else {
+ return currentGroupChildren.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ final ReferrerGroupModel group = (ReferrerGroupModel) getGroup(groupPosition);
+
+ String name = group.getName();
+ int total = group.getTotal();
+ String url = group.getUrl();
+ String icon = group.getIcon();
+ int children = getChildrenCount(groupPosition);
+
+ if (children > 0) {
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ holder.setEntryTextOrLink(url, name);
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // Site icon
+ holder.networkImageView.setVisibility(View.GONE);
+ if (!TextUtils.isEmpty(icon)) {
+ holder.networkImageView.setImageUrl(
+ GravatarUtils.fixGravatarUrl(icon, mResourceVars.headerAvatarSizePx),
+ WPNetworkImageView.ImageType.GONE_UNTIL_AVAILABLE);
+ }
+
+ if (children == 0) {
+ holder.showLinkIcon();
+ } else {
+ holder.showChevronIcon();
+ }
+
+ // Setup the spam button
+ if (ReferrerSpamHelper.isSpamActionAvailable(group)) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReferrerSpamHelper rp = new ReferrerSpamHelper(act);
+ rp.showPopup(holder.imgMore, group);
+ }
+ });
+
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ holder.imgMore.setClickable(false);
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_referrers);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java
new file mode 100644
index 000000000..5bdbda53e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsResourceVars.java
@@ -0,0 +1,19 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.wordpress.android.R;
+
+/*
+ * class which holds all resource-based variables used in Stats
+ */
+class StatsResourceVars {
+ final int headerAvatarSizePx;
+
+ StatsResourceVars(Context context) {
+ Resources resources = context.getResources();
+
+ headerAvatarSizePx = resources.getDimensionPixelSize(R.dimen.avatar_sz_small);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java
new file mode 100644
index 000000000..5289b2e0d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSearchTermsFragment.java
@@ -0,0 +1,238 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.SearchTermModel;
+import org.wordpress.android.ui.stats.models.SearchTermsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsSearchTermsFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsSearchTermsFragment.class.getSimpleName();
+
+ private final static String UNKNOWN_SEARCH_TERMS_HELP_PAGE = "http://en.support.wordpress.com/stats/#search-engine-terms";
+
+ private SearchTermsModel mSearchTerms;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mSearchTerms != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mSearchTerms);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mSearchTerms = (SearchTermsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SearchTermsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mSearchTerms = event.mSearchTerms;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mSearchTerms = null;
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasSearchTerms()) {
+
+ /**
+ * At this point we can have:
+ * - A list of search terms
+ * - A list of search terms + Encrypted item
+ * - Encrypted item only
+ *
+ * We want to display max 10 items regardless the kind of the items, AND Encrypted
+ * must be present if available.
+ *
+ * We need to do some counts then...
+ */
+
+ List<SearchTermModel> originalSearchTermList = mSearchTerms.getSearchTerms();
+ List<SearchTermModel> mySearchTermList;
+ if (originalSearchTermList == null) {
+ // No clear-text search terms. we know we have the encrypted search terms item available
+ mySearchTermList = new ArrayList<>(0);
+ } else {
+ // Make sure the list has MAX 9 items if the "Encrypted" is available
+ // we want to show exactly 10 items per module
+ if (mSearchTerms.getEncryptedSearchTerms() > 0 && originalSearchTermList.size() > getMaxNumberOfItemsToShowInList() - 1) {
+ mySearchTermList = new ArrayList<>();
+ int minIndex = Math.min(originalSearchTermList.size(), getMaxNumberOfItemsToShowInList() - 1);
+ for (int i = 0; i < minIndex; i++) {
+ mySearchTermList.add(originalSearchTermList.get(i));
+ }
+ } else {
+ mySearchTermList = originalSearchTermList;
+ }
+ }
+ ArrayAdapter adapter = new SearchTermsAdapter(getActivity(), mySearchTermList, mSearchTerms.getEncryptedSearchTerms());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasSearchTerms() {
+ return mSearchTerms != null
+ && ((mSearchTerms.getSearchTerms() != null && mSearchTerms.getSearchTerms().size() > 0)
+ || mSearchTerms.getEncryptedSearchTerms() > 0
+ );
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ if (!hasSearchTerms()) {
+ return false;
+ }
+
+
+ int total = mSearchTerms.getSearchTerms() != null ? mSearchTerms.getSearchTerms().size() : 0;
+ // If "Encrypted" is available we only have 9 items of clear text terms in the list
+ if (mSearchTerms.getEncryptedSearchTerms() > 0) {
+ return total > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST - 1;
+ } else {
+ return total > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class SearchTermsAdapter extends ArrayAdapter<SearchTermModel> {
+
+ private final List<SearchTermModel> list;
+ private final LayoutInflater inflater;
+ private final int encryptedSearchTerms;
+
+ public SearchTermsAdapter(Activity context, List<SearchTermModel> list, int encryptedSearchTerms) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ this.encryptedSearchTerms = encryptedSearchTerms;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount() + (encryptedSearchTerms > 0 ? 1 : 0);
+ }
+
+ @Override
+ public SearchTermModel getItem(int position) {
+ // If it's an element in the list returns it, otherwise it's the position of "Encrypted"
+ if (position < super.getCount()) {
+ return super.getItem(position);
+ }
+
+ return new SearchTermModel("", null, "Unknown Search Terms", encryptedSearchTerms, true);
+ }
+
+ @Override
+ public int getPosition(SearchTermModel item) {
+ if (item.isEncriptedTerms()) {
+ return super.getCount(); // "Encrypted" is always at the end of the list
+ }
+
+ return super.getPosition(item);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final SearchTermModel currentRowData = this.getItem(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ String term = currentRowData.getTitle();
+
+ if (currentRowData.isEncriptedTerms()) {
+ holder.setEntryTextOrLink(UNKNOWN_SEARCH_TERMS_HELP_PAGE, getString(R.string.stats_search_terms_unknown_search_terms));
+ } else {
+ holder.setEntryText(term, getResources().getColor(R.color.stats_text_color));
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // image
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_search_terms;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_search_terms;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_search_terms_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.SEARCH_TERMS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_search_terms);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java
new file mode 100644
index 000000000..0ec8970cd
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsSingleItemDetailsActivity.java
@@ -0,0 +1,907 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.volley.NoConnectionError;
+import com.android.volley.VolleyError;
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.GraphViewSeries;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.stats.models.PostViewsModel;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+
+/**
+ * Single item details activity.
+ */
+public class StatsSingleItemDetailsActivity extends AppCompatActivity
+ implements StatsBarGraph.OnGestureListener{
+
+ public static final String ARG_REMOTE_BLOG_ID = "ARG_REMOTE_BLOG_ID";
+ public static final String ARG_REMOTE_ITEM_ID = "ARG_REMOTE_ITEM_ID";
+ public static final String ARG_REMOTE_ITEM_TYPE = "ARG_REMOTE_ITEM_TYPE";
+ public static final String ARG_ITEM_TITLE = "ARG_ITEM_TITLE";
+ public static final String ARG_ITEM_URL = "ARG_ITEM_URL";
+ private static final String ARG_REST_RESPONSE = "ARG_REST_RESPONSE";
+ private static final String ARG_SELECTED_GRAPH_BAR = "ARG_SELECTED_GRAPH_BAR";
+ private static final String ARG_PREV_NUMBER_OF_BARS = "ARG_PREV_NUMBER_OF_BARS";
+ private static final String SAVED_STATS_SCROLL_POSITION = "SAVED_STATS_SCROLL_POSITION";
+
+ private boolean mIsUpdatingStats;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private ScrollViewExt mOuterScrollView;
+
+ private final Handler mHandler = new Handler();
+
+ private LinearLayout mGraphContainer;
+ private TextView mStatsViewsLabel;
+ private TextView mStatsViewsTotals;
+
+ private LinearLayout mMonthsAndYearsModule;
+ private LinearLayout mMonthsAndYearsList;
+ private RelativeLayout mMonthsAndYearsHeader;
+ private LinearLayout mMonthsAndYearsEmptyPlaceholder;
+
+ private LinearLayout mAveragesModule;
+ private LinearLayout mAveragesList;
+ private RelativeLayout mAveragesHeader;
+ private LinearLayout mAveragesEmptyPlaceholder;
+
+ private LinearLayout mRecentWeeksModule;
+ private LinearLayout mRecentWeeksList;
+ private RelativeLayout mRecentWeeksHeader;
+ private LinearLayout mRecentWeeksEmptyPlaceholder;
+ private String mRemoteBlogID, mRemoteItemID, mRemoteItemType, mItemTitle, mItemURL;
+ private PostViewsModel mRestResponseParsed;
+ private int mSelectedBarGraphIndex = -1;
+ private int mPrevNumberOfBarsGraph = -1;
+
+ private SparseBooleanArray mYearsIdToExpandedMap;
+ private SparseBooleanArray mAveragesIdToExpandedMap;
+ private SparseBooleanArray mRecentWeeksIdToExpandedMap;
+
+ private static final String ARG_YEARS_EXPANDED_ROWS = "ARG_YEARS_EXPANDED_ROWS";
+ private static final String ARG_AVERAGES_EXPANDED_ROWS = "ARG_AVERAGES_EXPANDED_ROWS";
+ private static final String ARG_RECENT_EXPANDED_ROWS = "ARG_RECENT_EXPANDED_ROWS";
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.stats_activity_single_post_details);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ // pull to refresh setup
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+ refreshStats();
+ }
+ }
+ );
+
+ TextView mStatsForLabel = (TextView) findViewById(R.id.stats_summary_title);
+ mGraphContainer = (LinearLayout) findViewById(R.id.stats_bar_chart_fragment_container);
+ mStatsViewsLabel = (TextView) findViewById(R.id.stats_views_label);
+ mStatsViewsTotals = (TextView) findViewById(R.id.stats_views_totals);
+
+ mMonthsAndYearsModule = (LinearLayout) findViewById(R.id.stats_months_years_module);
+ mMonthsAndYearsHeader = (RelativeLayout) findViewById(R.id.stats_months_years_header);
+ mMonthsAndYearsList = (LinearLayout) findViewById(R.id.stats_months_years_list_linearlayout);
+ mMonthsAndYearsEmptyPlaceholder = (LinearLayout) findViewById(R.id.stats_months_years_empty_module_placeholder);
+
+ mAveragesModule = (LinearLayout) findViewById(R.id.stats_averages_module);
+ mAveragesHeader = (RelativeLayout) findViewById(R.id.stats_averages_list_header);
+ mAveragesList = (LinearLayout) findViewById(R.id.stats_averages_list_linearlayout);
+ mAveragesEmptyPlaceholder = (LinearLayout) findViewById(R.id.stats_averages_empty_module_placeholder);
+
+ mRecentWeeksModule = (LinearLayout) findViewById(R.id.stats_recent_weeks_module);
+ mRecentWeeksHeader = (RelativeLayout) findViewById(R.id.stats_recent_weeks_list_header);
+ mRecentWeeksList = (LinearLayout) findViewById(R.id.stats_recent_weeks_list_linearlayout);
+ mRecentWeeksEmptyPlaceholder = (LinearLayout) findViewById(R.id.stats_recent_weeks_empty_module_placeholder);
+
+ mYearsIdToExpandedMap = new SparseBooleanArray();
+ mAveragesIdToExpandedMap = new SparseBooleanArray();
+ mRecentWeeksIdToExpandedMap = new SparseBooleanArray();
+
+ setTitle(R.string.stats);
+ mOuterScrollView = (ScrollViewExt) findViewById(R.id.scroll_view_stats);
+
+ if (savedInstanceState != null) {
+ mRemoteItemID = savedInstanceState.getString(ARG_REMOTE_ITEM_ID);
+ mRemoteBlogID = savedInstanceState.getString(ARG_REMOTE_BLOG_ID);
+ mRemoteItemType = savedInstanceState.getString(ARG_REMOTE_ITEM_TYPE);
+ mItemTitle = savedInstanceState.getString(ARG_ITEM_TITLE);
+ mItemURL = savedInstanceState.getString(ARG_ITEM_URL);
+ mRestResponseParsed = (PostViewsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ mSelectedBarGraphIndex = savedInstanceState.getInt(ARG_SELECTED_GRAPH_BAR, -1);
+ mPrevNumberOfBarsGraph = savedInstanceState.getInt(ARG_PREV_NUMBER_OF_BARS, -1);
+
+ final int yScrollPosition = savedInstanceState.getInt(SAVED_STATS_SCROLL_POSITION);
+ if(yScrollPosition != 0) {
+ mOuterScrollView.postDelayed(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ mOuterScrollView.scrollTo(0, yScrollPosition);
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ if (savedInstanceState.containsKey(ARG_AVERAGES_EXPANDED_ROWS)) {
+ mAveragesIdToExpandedMap = savedInstanceState.getParcelable(ARG_AVERAGES_EXPANDED_ROWS);
+ }
+ if (savedInstanceState.containsKey(ARG_RECENT_EXPANDED_ROWS)) {
+ mRecentWeeksIdToExpandedMap = savedInstanceState.getParcelable(ARG_RECENT_EXPANDED_ROWS);
+ }
+ if (savedInstanceState.containsKey(ARG_YEARS_EXPANDED_ROWS)) {
+ mYearsIdToExpandedMap = savedInstanceState.getParcelable(ARG_YEARS_EXPANDED_ROWS);
+ }
+ } else if (getIntent() != null && getIntent().getExtras() != null) {
+ Bundle extras = getIntent().getExtras();
+ mRemoteItemID = extras.getString(ARG_REMOTE_ITEM_ID);
+ mRemoteBlogID = extras.getString(ARG_REMOTE_BLOG_ID);
+ mRemoteItemType = extras.getString(ARG_REMOTE_ITEM_TYPE);
+ mItemTitle = extras.getString(ARG_ITEM_TITLE);
+ mItemURL = extras.getString(ARG_ITEM_URL);
+ mRestResponseParsed = (PostViewsModel) extras.getSerializable(ARG_REST_RESPONSE);
+ mSelectedBarGraphIndex = extras.getInt(ARG_SELECTED_GRAPH_BAR, -1);
+ }
+
+ if (mRemoteBlogID == null || mRemoteItemID == null) {
+ Toast.makeText(this, R.string.stats_generic_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.STATS_SINGLE_POST_ACCESSED,
+ mRemoteBlogID
+ );
+ }
+
+ // Setup the main top label that opens the post in the Reader where possible
+ if (mItemTitle != null || mItemURL != null) {
+ mStatsForLabel.setVisibility(View.VISIBLE);
+ mStatsForLabel.setText(mItemTitle != null ? mItemTitle : mItemURL );
+ // make the label clickable if the URL is available
+ if (mItemURL != null) {
+ mStatsForLabel.setTextColor(getResources().getColor(R.color.stats_link_text_color));
+ mStatsForLabel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Context ctx = v.getContext();
+ StatsUtils.openPostInReaderOrInAppWebview(ctx,
+ mRemoteBlogID,
+ mRemoteItemID,
+ mRemoteItemType,
+ mItemURL);
+ }
+ });
+ } else {
+ mStatsForLabel.setTextColor(getResources().getColor(R.color.grey_darken_20));
+ }
+ } else {
+ mStatsForLabel.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(ARG_SELECTED_GRAPH_BAR, mSelectedBarGraphIndex);
+ outState.putInt(ARG_PREV_NUMBER_OF_BARS, mPrevNumberOfBarsGraph);
+ outState.putString(ARG_REMOTE_BLOG_ID, mRemoteBlogID);
+ outState.putString(ARG_REMOTE_ITEM_ID, mRemoteItemID);
+ outState.putString(ARG_REMOTE_ITEM_TYPE, mRemoteItemType);
+ outState.putString(ARG_ITEM_TITLE, mItemTitle);
+ outState.putString(ARG_ITEM_URL, mItemURL);
+
+ outState.putSerializable(ARG_REST_RESPONSE, mRestResponseParsed);
+ if (mOuterScrollView.getScrollY() != 0) {
+ outState.putInt(SAVED_STATS_SCROLL_POSITION, mOuterScrollView.getScrollY());
+ }
+
+ if (mAveragesIdToExpandedMap.size() > 0){
+ outState.putParcelable(ARG_AVERAGES_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mAveragesIdToExpandedMap));
+ }
+ if (mRecentWeeksIdToExpandedMap.size() > 0) {
+ outState.putParcelable(ARG_RECENT_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mRecentWeeksIdToExpandedMap));
+ }
+ if (mYearsIdToExpandedMap.size() > 0) {
+ outState.putParcelable(ARG_YEARS_EXPANDED_ROWS, new SparseBooleanArrayParcelable(mYearsIdToExpandedMap));
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mRestResponseParsed == null) {
+ // check if network is available, if not shows the empty UI immediately
+ if (!NetworkUtils.checkConnection(this)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ setupEmptyUI();
+ } else {
+ setupEmptyGraph("");
+ showHideEmptyModulesIndicator(true);
+ refreshStats();
+ }
+ } else {
+ updateUI();
+ }
+ ActivityId.trackLastActivity(ActivityId.STATS_POST_DETAILS);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void refreshStats() {
+
+ if (mIsUpdatingStats) {
+ AppLog.w(AppLog.T.STATS, "stats details are already updating for the following postID "
+ + mRemoteItemID + ", refresh cancelled.");
+ return;
+ }
+
+ if (!NetworkUtils.checkConnection(this)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ return;
+ }
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtilsV1_1();
+
+
+ // View and visitor counts for a site
+ final String singlePostRestPath = String.format(
+ "/sites/%s/stats/post/%s", mRemoteBlogID, mRemoteItemID);
+
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following Stats request " + singlePostRestPath);
+
+ RestBatchCallListener vListener = new RestBatchCallListener(this);
+ restClientUtils.get(singlePostRestPath, vListener, vListener);
+
+ mIsUpdatingStats = true;
+ mSwipeToRefreshHelper.setRefreshing(true);
+ }
+
+ private void showHideEmptyModulesIndicator(boolean show) {
+ if (isFinishing()) {
+ return;
+ }
+
+ mMonthsAndYearsModule.setVisibility(View.VISIBLE);
+ mRecentWeeksModule.setVisibility(View.VISIBLE);
+ mAveragesModule.setVisibility(View.VISIBLE);
+
+ mMonthsAndYearsHeader.setVisibility(show ? View.GONE : View.VISIBLE);
+ mRecentWeeksHeader.setVisibility(show ? View.GONE : View.VISIBLE);
+ mAveragesHeader.setVisibility(show ? View.GONE : View.VISIBLE);
+
+ mMonthsAndYearsList.setVisibility(show ? View.GONE : View.VISIBLE);
+ mAveragesList.setVisibility(show ? View.GONE : View.VISIBLE);
+ mRecentWeeksList.setVisibility(show ? View.GONE : View.VISIBLE);
+
+ mMonthsAndYearsEmptyPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE);
+ mRecentWeeksEmptyPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE);
+ mAveragesEmptyPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ private void setupEmptyUI() {
+ if (isFinishing()) {
+ return;
+ }
+
+ setupEmptyGraph(null);
+
+ mMonthsAndYearsModule.setVisibility(View.GONE);
+ mRecentWeeksModule.setVisibility(View.GONE);
+ mAveragesModule.setVisibility(View.GONE);
+
+ mRecentWeeksIdToExpandedMap.clear();
+ mAveragesIdToExpandedMap.clear();
+ mYearsIdToExpandedMap.clear();
+ }
+
+ private void setupEmptyGraph(String emptyLabel) {
+ if (isFinishing()) {
+ return;
+ }
+ Context context = mGraphContainer.getContext();
+ if (context != null) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View emptyBarGraphView = inflater.inflate(R.layout.stats_bar_graph_empty, mGraphContainer, false);
+ if (emptyLabel != null) {
+ final TextView emptyLabelField = (TextView) emptyBarGraphView.findViewById(R.id.stats_bar_graph_empty_label);
+ emptyLabelField.setText(emptyLabel);
+ }
+ mGraphContainer.removeAllViews();
+ mGraphContainer.addView(emptyBarGraphView);
+ }
+ mStatsViewsLabel.setText("");
+ mStatsViewsTotals.setText("");
+ }
+
+ private VisitModel[] getDataToShowOnGraph () {
+ if (mRestResponseParsed == null) {
+ return new VisitModel[0];
+ }
+
+ final VisitModel[] dayViews = mRestResponseParsed.getDayViews();
+ if (dayViews == null) {
+ return new VisitModel[0];
+ }
+
+ int numPoints = Math.min(StatsUIHelper.getNumOfBarsToShow(), dayViews.length);
+ int currentPointIndex = numPoints - 1;
+ VisitModel[] visitModels = new VisitModel[numPoints];
+
+ for (int i = dayViews.length - 1; i >= 0 && currentPointIndex >= 0; i--) {
+ visitModels[currentPointIndex] = dayViews[i];
+ currentPointIndex--;
+ }
+
+ return visitModels;
+ }
+
+ private void updateUI() {
+ if (isFinishing()) {
+ return;
+ }
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph();
+
+ if (dataToShowOnGraph == null || dataToShowOnGraph.length == 0) {
+ setupEmptyUI();
+ return;
+ }
+
+ final String[] horLabels = new String[dataToShowOnGraph.length];
+ String[] mStatsDate = new String[dataToShowOnGraph.length];
+ GraphView.GraphViewData[] views = new GraphView.GraphViewData[dataToShowOnGraph.length];
+
+ for (int i = 0; i < dataToShowOnGraph.length; i++) {
+ int currentItemValue = dataToShowOnGraph[i].getViews();
+ views[i] = new GraphView.GraphViewData(i, currentItemValue);
+
+ String currentItemStatsDate = dataToShowOnGraph[i].getPeriod();
+ horLabels[i] = StatsUtils.parseDate(
+ currentItemStatsDate,
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT
+ );
+ mStatsDate[i] = currentItemStatsDate;
+ }
+
+ GraphViewSeries mCurrentSeriesOnScreen = new GraphViewSeries(views);
+ mCurrentSeriesOnScreen.getStyle().color = getResources().getColor(R.color.stats_bar_graph_main_series);
+ mCurrentSeriesOnScreen.getStyle().highlightColor = getResources().getColor(R.color.stats_bar_graph_main_series_highlight);
+ mCurrentSeriesOnScreen.getStyle().outerhighlightColor = getResources().getColor(R.color.stats_bar_graph_outer_highlight);
+ mCurrentSeriesOnScreen.getStyle().padding = DisplayUtils.dpToPx(this, 5);
+
+ StatsBarGraph mGraphView;
+ if (mGraphContainer.getChildCount() >= 1 && mGraphContainer.getChildAt(0) instanceof GraphView) {
+ mGraphView = (StatsBarGraph) mGraphContainer.getChildAt(0);
+ } else {
+ mGraphContainer.removeAllViews();
+ mGraphView = new StatsBarGraph(this);
+ mGraphContainer.addView(mGraphView);
+ }
+
+
+ mGraphView.removeAllSeries();
+ mGraphView.addSeries(mCurrentSeriesOnScreen);
+ //mGraphView.getGraphViewStyle().setNumHorizontalLabels(getNumOfHorizontalLabels(dataToShowOnGraph.length));
+ mGraphView.getGraphViewStyle().setNumHorizontalLabels(dataToShowOnGraph.length);
+ mGraphView.getGraphViewStyle().setMaxColumnWidth(
+ DisplayUtils.dpToPx(this, StatsConstants.STATS_GRAPH_BAR_MAX_COLUMN_WIDTH_DP)
+ );
+ mGraphView.setHorizontalLabels(horLabels);
+ mGraphView.setGestureListener(this);
+
+ // Reset the bar selected upon rotation of the device when the no. of bars can change with orientation.
+ // Only happens on 720DP tablets
+ if (mPrevNumberOfBarsGraph != -1 && mPrevNumberOfBarsGraph != dataToShowOnGraph.length) {
+ mSelectedBarGraphIndex = dataToShowOnGraph.length - 1;
+ } else {
+ mSelectedBarGraphIndex = (mSelectedBarGraphIndex != -1) ? mSelectedBarGraphIndex : dataToShowOnGraph.length - 1;
+ }
+
+ mGraphView.highlightBar(mSelectedBarGraphIndex);
+ mPrevNumberOfBarsGraph = dataToShowOnGraph.length;
+
+ setMainViewsLabel(
+ StatsUtils.parseDate(
+ mStatsDate[mSelectedBarGraphIndex],
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT
+ ),
+ dataToShowOnGraph[mSelectedBarGraphIndex].getViews()
+ );
+
+ showHideEmptyModulesIndicator(false);
+
+ mMonthsAndYearsList.setVisibility(View.VISIBLE);
+ List<PostViewsModel.Year> years = mRestResponseParsed.getYears();
+ MonthsAndYearsListAdapter monthsAndYearsListAdapter = new MonthsAndYearsListAdapter(this, years, mRestResponseParsed.getHighestMonth());
+ StatsUIHelper.reloadGroupViews(this, monthsAndYearsListAdapter, mYearsIdToExpandedMap, mMonthsAndYearsList);
+
+ mAveragesList.setVisibility(View.VISIBLE);
+ List<PostViewsModel.Year> averages = mRestResponseParsed.getAverages();
+ MonthsAndYearsListAdapter averagesListAdapter = new MonthsAndYearsListAdapter(this, averages, mRestResponseParsed.getHighestDayAverage());
+ StatsUIHelper.reloadGroupViews(this, averagesListAdapter, mAveragesIdToExpandedMap, mAveragesList);
+
+ mRecentWeeksList.setVisibility(View.VISIBLE);
+ List<PostViewsModel.Week> recentWeeks = mRestResponseParsed.getWeeks();
+ RecentWeeksListAdapter recentWeeksListAdapter = new RecentWeeksListAdapter(this, recentWeeks, mRestResponseParsed.getHighestWeekAverage());
+ StatsUIHelper.reloadGroupViews(this, recentWeeksListAdapter, mRecentWeeksIdToExpandedMap, mRecentWeeksList);
+ }
+
+
+ private void setMainViewsLabel(String dateFormatted, int totals) {
+ mStatsViewsLabel.setText(getString(R.string.stats_views) + ": "
+ + dateFormatted);
+ mStatsViewsTotals.setText(FormatUtils.formatDecimal(totals));
+ }
+
+
+ private class RecentWeeksListAdapter extends BaseExpandableListAdapter {
+ public static final String GROUP_DATE_FORMAT = "MMM dd";
+ public final LayoutInflater inflater;
+ private final List<PostViewsModel.Week> groups;
+ private final int maxReachedValue;
+
+ public RecentWeeksListAdapter(Context context, List<PostViewsModel.Week> groups, int maxReachedValue) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(context);
+ this.maxReachedValue = maxReachedValue;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ PostViewsModel.Week currentWeek = groups.get(groupPosition);
+ return currentWeek.getDays().get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final PostViewsModel.Day currentDay = (PostViewsModel.Day) getChild(groupPosition, childPosition);
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ holder.setEntryText(StatsUtils.parseDate(currentDay.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, "EEE, MMM dd"));
+
+ // Intercept clicks at row level and eat the event. We don't want to show the ripple here.
+ holder.rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+
+ }
+ });
+ holder.rowContent.setBackgroundColor(Color.TRANSPARENT);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentDay.getCount()));
+
+ // show the trophy indicator if the value is the maximum reached
+ if (currentDay.getCount() == maxReachedValue && maxReachedValue > 0) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ holder.networkImageView.setVisibility(View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ PostViewsModel.Week week = groups.get(groupPosition);
+ return week.getDays().size();
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ PostViewsModel.Week week = (PostViewsModel.Week) getGroup(groupPosition);
+
+ int total = week.getTotal();
+
+ // change the color of the text if one of its childs has reached maximum value
+ int numberOfChilds = getChildrenCount(groupPosition);
+ boolean shouldShowTheTrophyIcon = false;
+ if (maxReachedValue > 0) {
+ for (int i = 0; i < numberOfChilds; i++) {
+ PostViewsModel.Day currentChild = (PostViewsModel.Day) getChild(groupPosition, i);
+ if (currentChild.getCount() == maxReachedValue) {
+ shouldShowTheTrophyIcon = true;
+ }
+ }
+ }
+
+ // Build the label to show on the group
+ String name;
+ PostViewsModel.Day firstChild = (PostViewsModel.Day) getChild(groupPosition, 0);
+ if (numberOfChilds > 1) {
+ PostViewsModel.Day lastChild = (PostViewsModel.Day) getChild(groupPosition, getChildrenCount(groupPosition) - 1);
+ name = StatsUtils.parseDate(firstChild.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, GROUP_DATE_FORMAT)
+ + " - " + StatsUtils.parseDate(lastChild.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, GROUP_DATE_FORMAT);
+ } else {
+ name = StatsUtils.parseDate(firstChild.getDay(), StatsConstants.STATS_INPUT_DATE_FORMAT, GROUP_DATE_FORMAT);
+ }
+
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+
+ holder.networkImageView.setVisibility(View.GONE);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+ if (shouldShowTheTrophyIcon) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ // expand/collapse chevron
+ holder.chevronImageView.setVisibility(numberOfChilds > 0 ? View.VISIBLE : View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+
+ private class MonthsAndYearsListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ private final List<PostViewsModel.Year> groups;
+ private final int maxReachedValue;
+
+ public MonthsAndYearsListAdapter(Context context, List<PostViewsModel.Year> groups, int maxReachedValue) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(context);
+ this.maxReachedValue = maxReachedValue;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ PostViewsModel.Year currentYear = groups.get(groupPosition);
+ return currentYear.getMonths().get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final PostViewsModel.Month currentMonth = (PostViewsModel.Month) getChild(groupPosition, childPosition);
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ holder.setEntryText(StatsUtils.parseDate(currentMonth.getMonth(), "MM", StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_FORMAT));
+
+ // Intercept clicks at row level and eat the event. We don't want to show the ripple here.
+ holder.rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+
+ }
+ });
+ holder.rowContent.setBackgroundColor(Color.TRANSPARENT);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentMonth.getCount()));
+
+ // show the trophy indicator if the value is the maximum reached
+ if (currentMonth.getCount() == maxReachedValue && maxReachedValue > 0) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ holder.networkImageView.setVisibility(View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ PostViewsModel.Year currentYear = groups.get(groupPosition);
+ return currentYear.getMonths().size();
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final StatsViewHolder holder;
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ holder = new StatsViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (StatsViewHolder) convertView.getTag();
+ }
+
+ PostViewsModel.Year year = (PostViewsModel.Year) getGroup(groupPosition);
+
+ String name = year.getLabel();
+ int total = year.getTotal();
+
+ // change the color of the text if one of its childs has reached maximum value
+ int numberOfChilds = getChildrenCount(groupPosition);
+ boolean shouldShowTheTrophyIcon = false;
+ if (maxReachedValue > 0) {
+ for (int i = 0; i < numberOfChilds; i++) {
+ PostViewsModel.Month currentChild = (PostViewsModel.Month) getChild(groupPosition, i);
+ if (currentChild.getCount() == maxReachedValue) {
+ shouldShowTheTrophyIcon = true;
+ break;
+ }
+ }
+ }
+
+ holder.setEntryText(name, getResources().getColor(R.color.stats_link_text_color));
+
+ if (shouldShowTheTrophyIcon) {
+ holder.imgMore.setVisibility(View.VISIBLE);
+ holder.imgMore.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_trophy));
+ holder.imgMore.setBackgroundColor(Color.TRANSPARENT); // Hide the default click indicator
+ } else {
+ holder.imgMore.setVisibility(View.GONE);
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ holder.networkImageView.setVisibility(View.GONE);
+
+ // expand/collapse chevron
+ holder.chevronImageView.setVisibility(numberOfChilds > 0 ? View.VISIBLE : View.GONE);
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+
+ private class RestBatchCallListener implements RestRequest.Listener, RestRequest.ErrorListener {
+
+ private final WeakReference<Activity> mActivityRef;
+
+ public RestBatchCallListener(Activity activity) {
+ mActivityRef = new WeakReference<>(activity);
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ // single background thread used to parse the response in BG.
+ ThreadPoolExecutor parseResponseExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
+ parseResponseExecutor.submit(new Thread() {
+ @Override
+ public void run() {
+ //AppLog.d(AppLog.T.STATS, "The REST response: " + response.toString());
+ mSelectedBarGraphIndex = -1;
+ try {
+ mRestResponseParsed = new PostViewsModel(response);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the JSON response", e);
+ resetModelVariables();
+ }
+
+ // Update the UI
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateUI();
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ StatsUtils.logVolleyErrorDetails(volleyError);
+ if (mActivityRef.get() == null || mActivityRef.get().isFinishing()) {
+ return;
+ }
+ resetModelVariables();
+
+ String label = mActivityRef.get().getString(R.string.error_refresh_stats);
+ if (volleyError instanceof NoConnectionError) {
+ label += "\n" + mActivityRef.get().getString(R.string.no_network_message);
+ }
+
+ ToastUtils.showToast(mActivityRef.get(), label, ToastUtils.Duration.LONG);
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+
+ // Update the UI
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateUI();
+ }
+ });
+ }
+ }
+
+ private void resetModelVariables() {
+ mRestResponseParsed = null;
+ mSelectedBarGraphIndex = -1;
+ mAveragesIdToExpandedMap.clear();
+ mYearsIdToExpandedMap.clear();
+ }
+
+ @Override
+ public void onBarTapped(int tappedBar) {
+ mSelectedBarGraphIndex = tappedBar;
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph();
+ String currentItemStatsDate = dataToShowOnGraph[mSelectedBarGraphIndex].getPeriod();
+ currentItemStatsDate = StatsUtils.parseDate(
+ currentItemStatsDate,
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT
+ );
+ setMainViewsLabel(currentItemStatsDate, dataToShowOnGraph[mSelectedBarGraphIndex].getViews());
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java
new file mode 100644
index 000000000..9f838ea9d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTagsAndCategoriesFragment.java
@@ -0,0 +1,282 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.TagModel;
+import org.wordpress.android.ui.stats.models.TagsContainerModel;
+import org.wordpress.android.ui.stats.models.TagsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StatsTagsAndCategoriesFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsTagsAndCategoriesFragment.class.getSimpleName();
+
+ private TagsContainerModel mTagsContainer;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mTagsContainer != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (mTagsContainer != null) {
+ outState.putSerializable(ARG_REST_RESPONSE, mTagsContainer);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mTagsContainer = (TagsContainerModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.TagsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mTagsContainer = event.mTagsContainer;
+ mGroupIdToExpandedMap.clear();
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mTagsContainer = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasTags()) {
+ BaseExpandableListAdapter adapter = new MyExpandableListAdapter(getActivity(), getTags());
+ StatsUIHelper.reloadGroupViews(getActivity(), adapter, mGroupIdToExpandedMap, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasTags() {
+ return mTagsContainer != null
+ && mTagsContainer.getTags() != null
+ && mTagsContainer.getTags().size() > 0;
+ }
+
+ private List<TagsModel> getTags() {
+ if (!hasTags()) {
+ return new ArrayList<TagsModel>(0);
+ }
+ return mTagsContainer.getTags();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasTags() && getTags().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return true;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.TAGS_AND_CATEGORIES
+ };
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_tags_and_categories;
+ }
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_tags_and_categories;
+ }
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_tags_and_categories_desc;
+ }
+
+ private class MyExpandableListAdapter extends BaseExpandableListAdapter {
+ public final LayoutInflater inflater;
+ private final List<TagsModel> groups;
+
+ public MyExpandableListAdapter(Context context, List<TagsModel> groups) {
+ this.groups = groups;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ TagsModel currentGroup = groups.get(groupPosition);
+ List<TagModel> results = currentGroup.getTags();
+ return results.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final TagModel children = (TagModel) getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+
+ //Make the picture smaller (same size of the chevron) only for tag
+ ViewGroup.LayoutParams params = viewHolder.networkImageView.getLayoutParams();
+ params.width = DisplayUtils.dpToPx(convertView.getContext(), 12);
+ params.height = params.width;
+ viewHolder.networkImageView.setLayoutParams(params);
+
+ convertView.setTag(viewHolder);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ // name, url
+ holder.setEntryTextOrLink(children.getLink(), children.getName());
+
+ // totals
+ holder.totalsTextView.setText("");
+
+ // icon.
+ holder.networkImageView.setVisibility(View.VISIBLE);
+ holder.networkImageView.setImageDrawable(getResources().getDrawable(R.drawable.stats_icon_tags));
+
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ TagsModel currentGroup = groups.get(groupPosition);
+ List<TagModel> tags = currentGroup.getTags();
+ if (tags == null || tags.size() == 1 ) {
+ return 0;
+ } else {
+ return tags.size();
+ }
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return groups.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(convertView);
+ convertView.setTag(viewHolder);
+
+ //Make the picture smaller (same size of the chevron) only for tag
+ ViewGroup.LayoutParams params = viewHolder.networkImageView.getLayoutParams();
+ params.width = DisplayUtils.dpToPx(convertView.getContext(), 12);
+ params.height = params.width;
+ viewHolder.networkImageView.setLayoutParams(params);
+ }
+
+ final StatsViewHolder holder = (StatsViewHolder) convertView.getTag();
+
+ TagsModel group = (TagsModel) getGroup(groupPosition);
+ StringBuilder groupName = new StringBuilder();
+ List<TagModel> tags = group.getTags();
+ for (int i = 0; i < tags.size(); i++) {
+ TagModel currentTag = tags.get(i);
+ groupName.append(currentTag.getName());
+ if ( i < (tags.size() - 1)) {
+ groupName.append(" | ");
+ }
+ }
+ int total = group.getViews();
+ int children = getChildrenCount(groupPosition);
+
+ if (children > 0) {
+ holder.setEntryText(groupName.toString(), getResources().getColor(R.color.stats_link_text_color));
+ } else {
+ holder.setEntryTextOrLink(tags.get(0).getLink(), groupName.toString());
+ }
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(total));
+
+ // expand/collapse chevron
+ holder.chevronImageView.setVisibility(children > 0 ? View.VISIBLE : View.GONE);
+
+
+ // icon
+ if ( children == 0 ) {
+ holder.networkImageView.setVisibility(View.VISIBLE);
+ int drawableResource = groupName.toString().equalsIgnoreCase("uncategorized") ? R.drawable.stats_icon_categories
+ : R.drawable.stats_icon_tags;
+ holder.networkImageView.setImageDrawable(getResources().getDrawable(drawableResource));
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_tags_and_categories);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java
new file mode 100644
index 000000000..e9cae5b63
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTimeframe.java
@@ -0,0 +1,39 @@
+package org.wordpress.android.ui.stats;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+/**
+ * Timeframes for the stats pages.
+ */
+public enum StatsTimeframe {
+ INSIGHTS(R.string.stats_insights),
+ DAY(R.string.stats_timeframe_days),
+ WEEK(R.string.stats_timeframe_weeks),
+ MONTH(R.string.stats_timeframe_months),
+ YEAR(R.string.stats_timeframe_years),
+ ;
+
+ private final int mLabelResId;
+
+ StatsTimeframe(int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+
+ public String getLabelForRestCall() {
+ switch (this) {
+ case WEEK:
+ return "week";
+ case MONTH:
+ return "month";
+ case YEAR:
+ return "year";
+ default:
+ return "day";
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java
new file mode 100644
index 000000000..32a3742fa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsTopPostsAndPagesFragment.java
@@ -0,0 +1,129 @@
+package org.wordpress.android.ui.stats;
+
+import android.os.Bundle;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.adapters.PostsAndPagesAdapter;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsTopPostsAndPagesFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsTopPostsAndPagesFragment.class.getSimpleName();
+
+ private TopPostsAndPagesModel mTopPostsAndPagesModel = null;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mTopPostsAndPagesModel != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mTopPostsAndPagesModel);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mTopPostsAndPagesModel = (TopPostsAndPagesModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.TopPostsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mGroupIdToExpandedMap.clear();
+ mTopPostsAndPagesModel = event.mTopPostsAndPagesModel;
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mTopPostsAndPagesModel = null;
+ mGroupIdToExpandedMap.clear();
+ showErrorUI(event.mError);
+ }
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasTopPostsAndPages()) {
+ List<PostModel> postViews = mTopPostsAndPagesModel.getTopPostsAndPages();
+ ArrayAdapter adapter = new PostsAndPagesAdapter(getActivity(), postViews);
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasTopPostsAndPages() {
+ return mTopPostsAndPagesModel != null && mTopPostsAndPagesModel.hasTopPostsAndPages();
+ }
+
+ private List<PostModel> getTopPostsAndPages() {
+ if (!hasTopPostsAndPages()) {
+ return new ArrayList<PostModel>(0);
+ }
+ return mTopPostsAndPagesModel.getTopPostsAndPages();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasTopPostsAndPages() && getTopPostsAndPages().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_posts_and_pages;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_views;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_top_posts_title;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_top_posts_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.TOP_POSTS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_top_posts_and_pages);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java
new file mode 100644
index 000000000..441ed3050
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUIHelper.java
@@ -0,0 +1,344 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.text.Spannable;
+import android.text.style.URLSpan;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.RotateAnimation;
+import android.view.animation.ScaleAnimation;
+import android.widget.ExpandableListAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.DisplayUtils;
+
+class StatsUIHelper {
+ // Max number of rows to show in a stats fragment
+ private static final int STATS_GROUP_MAX_ITEMS = 10;
+ private static final int STATS_CHILD_MAX_ITEMS = 50;
+ private static final int ANIM_DURATION = 150;
+
+ // Used for tablet UI
+ private static final int TABLET_720DP = 720;
+ private static final int TABLET_600DP = 600;
+
+ private static boolean isInLandscape(Activity act) {
+ Display display = act.getWindowManager().getDefaultDisplay();
+ Point point = new Point();
+ display.getSize(point);
+ return (point.y < point.x);
+ }
+
+ // Load more bars for 720DP tablets
+ private static boolean shouldLoadMoreBars() {
+ return (StatsUtils.getSmallestWidthDP() >= TABLET_720DP);
+ }
+
+ public static void reloadLinearLayout(Context ctx, ListAdapter adapter, LinearLayout linearLayout, int maxNumberOfItemsToshow) {
+ if (ctx == null || linearLayout == null || adapter == null) {
+ return;
+ }
+
+ // limit number of items to show otherwise it would cause performance issues on the LinearLayout
+ int count = Math.min(adapter.getCount(), maxNumberOfItemsToshow);
+
+ if (count == 0) {
+ linearLayout.removeAllViews();
+ return;
+ }
+
+ int numExistingViews = linearLayout.getChildCount();
+ // remove excess views
+ if (count < numExistingViews) {
+ int numToRemove = numExistingViews - count;
+ linearLayout.removeViews(count, numToRemove);
+ numExistingViews = count;
+ }
+
+ int bgColor = Color.TRANSPARENT;
+ for (int i = 0; i < count; i++) {
+ final View view;
+ // reuse existing view when possible
+ if (i < numExistingViews) {
+ View convertView = linearLayout.getChildAt(i);
+ view = adapter.getView(i, convertView, linearLayout);
+ view.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(view, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ } else {
+ view = adapter.getView(i, null, linearLayout);
+ view.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(view, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ linearLayout.addView(view);
+ }
+ }
+ linearLayout.invalidate();
+ }
+
+ /**
+ *
+ * Padding information are reset when changing the background Drawable on a View.
+ * The reason why setting an image resets the padding is because 9-patch images can encode padding.
+ *
+ * See http://stackoverflow.com/a/10469121 and
+ * http://www.mail-archive.com/android-developers@googlegroups.com/msg09595.html
+ *
+ * @param v The view to apply the background resource
+ * @param backgroundResId The resource ID
+ */
+ private static void setViewBackgroundWithoutResettingPadding(final View v, final int backgroundResId) {
+ final int paddingBottom = v.getPaddingBottom(), paddingLeft = v.getPaddingLeft();
+ final int paddingRight = v.getPaddingRight(), paddingTop = v.getPaddingTop();
+ v.setBackgroundResource(backgroundResId);
+ v.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+ }
+
+ public static void reloadLinearLayout(Context ctx, ListAdapter adapter, LinearLayout linearLayout) {
+ reloadLinearLayout(ctx, adapter, linearLayout, STATS_GROUP_MAX_ITEMS);
+ }
+
+ public static void reloadGroupViews(final Context ctx,
+ final ExpandableListAdapter mAdapter,
+ final SparseBooleanArray mGroupIdToExpandedMap,
+ final LinearLayout mLinearLayout) {
+ reloadGroupViews(ctx, mAdapter, mGroupIdToExpandedMap, mLinearLayout, STATS_GROUP_MAX_ITEMS);
+ }
+
+ public static void reloadGroupViews(final Context ctx,
+ final ExpandableListAdapter mAdapter,
+ final SparseBooleanArray mGroupIdToExpandedMap,
+ final LinearLayout mLinearLayout,
+ final int maxNumberOfItemsToshow) {
+ if (ctx == null || mLinearLayout == null || mAdapter == null || mGroupIdToExpandedMap == null) {
+ return;
+ }
+
+ int groupCount = Math.min(mAdapter.getGroupCount(), maxNumberOfItemsToshow);
+ if (groupCount == 0) {
+ mLinearLayout.removeAllViews();
+ return;
+ }
+
+ int numExistingGroupViews = mLinearLayout.getChildCount();
+
+ // remove excess views
+ if (groupCount < numExistingGroupViews) {
+ int numToRemove = numExistingGroupViews - groupCount;
+ mLinearLayout.removeViews(groupCount, numToRemove);
+ numExistingGroupViews = groupCount;
+ }
+
+ int bgColor = Color.TRANSPARENT;
+
+ // add each group
+ for (int i = 0; i < groupCount; i++) {
+ boolean isExpanded = mGroupIdToExpandedMap.get(i);
+
+ // reuse existing view when possible
+ final View groupView;
+ if (i < numExistingGroupViews) {
+ View convertView = mLinearLayout.getChildAt(i);
+ groupView = mAdapter.getGroupView(i, isExpanded, convertView, mLinearLayout);
+ groupView.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(groupView, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ } else {
+ groupView = mAdapter.getGroupView(i, isExpanded, null, mLinearLayout);
+ groupView.setBackgroundColor(bgColor);
+ setViewBackgroundWithoutResettingPadding(groupView, i == 0 ? 0 : R.drawable.stats_list_item_background);
+ mLinearLayout.addView(groupView);
+ }
+
+ // groupView is recycled, we need to reset it to the original state.
+ ViewGroup childContainer = (ViewGroup) groupView.findViewById(R.id.layout_child_container);
+ if (childContainer != null) {
+ childContainer.setVisibility(View.GONE);
+ }
+ // Remove any other prev animations set on the chevron
+ final ImageView chevron = (ImageView) groupView.findViewById(R.id.stats_list_cell_chevron);
+ if (chevron != null) {
+ chevron.clearAnimation();
+ chevron.setImageResource(R.drawable.stats_chevron_right);
+ }
+
+ // add children if this group is expanded
+ if (isExpanded) {
+ StatsUIHelper.showChildViews(mAdapter, mLinearLayout, i, groupView, false);
+ }
+
+ // toggle expand/collapse when group view is tapped
+ final int groupPosition = i;
+ groupView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mAdapter.getChildrenCount(groupPosition) == 0) {
+ return;
+ }
+ boolean shouldExpand = !mGroupIdToExpandedMap.get(groupPosition);
+ mGroupIdToExpandedMap.put(groupPosition, shouldExpand);
+ if (shouldExpand) {
+ StatsUIHelper.showChildViews(mAdapter, mLinearLayout, groupPosition, groupView, true);
+ } else {
+ StatsUIHelper.hideChildViews(groupView, groupPosition, true);
+ }
+ }
+ });
+ }
+ }
+
+ /*
+ * interpolator for all expand/collapse animations
+ */
+ private static Interpolator getInterpolator() {
+ return new AccelerateInterpolator();
+ }
+
+ private static void hideChildViews(View groupView, int groupPosition, boolean animate) {
+ final ViewGroup childContainer = (ViewGroup) groupView.findViewById(R.id.layout_child_container);
+ if (childContainer == null) {
+ return;
+ }
+
+ if (childContainer.getVisibility() != View.GONE) {
+ if (animate) {
+ Animation expand = new ScaleAnimation(1.0f, 1.0f, 1.0f, 0.0f);
+ expand.setDuration(ANIM_DURATION);
+ expand.setInterpolator(getInterpolator());
+ expand.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ childContainer.setVisibility(View.GONE);
+ }
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ });
+ childContainer.startAnimation(expand);
+ } else {
+ childContainer.setVisibility(View.GONE);
+ }
+ }
+ StatsUIHelper.setGroupChevron(false, groupView, groupPosition, animate);
+ }
+
+ /*
+ * shows the correct up/down chevron for the passed group
+ */
+ private static void setGroupChevron(final boolean isGroupExpanded, View groupView, int groupPosition, boolean animate) {
+ final ImageView chevron = (ImageView) groupView.findViewById(R.id.stats_list_cell_chevron);
+ if (chevron == null) {
+ return;
+ }
+ if (isGroupExpanded) {
+ // change the background of the parent
+ setViewBackgroundWithoutResettingPadding(groupView, R.drawable.stats_list_item_expanded_background);
+ } else {
+ setViewBackgroundWithoutResettingPadding(groupView, groupPosition == 0 ? 0 : R.drawable.stats_list_item_background);
+ }
+
+ chevron.clearAnimation(); // Remove any other prev animations set on the chevron
+ if (animate) {
+ // make sure we start with the correct chevron for the prior state before animating it
+ chevron.setImageResource(isGroupExpanded ? R.drawable.stats_chevron_right : R.drawable.stats_chevron_down);
+ float start = (isGroupExpanded ? 0.0f : 0.0f);
+ float end = (isGroupExpanded ? 90.0f : -90.0f);
+ Animation rotate = new RotateAnimation(start, end, Animation.RELATIVE_TO_SELF, 0.5f,
+ Animation.RELATIVE_TO_SELF, 0.5f);
+ rotate.setDuration(ANIM_DURATION);
+ rotate.setInterpolator(getInterpolator());
+ rotate.setFillAfter(true);
+ chevron.startAnimation(rotate);
+ } else {
+ chevron.setImageResource(isGroupExpanded ? R.drawable.stats_chevron_down : R.drawable.stats_chevron_right);
+ }
+ }
+
+ private static void showChildViews(ExpandableListAdapter mAdapter, LinearLayout mLinearLayout,
+ int groupPosition, View groupView, boolean animate) {
+ int childCount = Math.min(mAdapter.getChildrenCount(groupPosition), STATS_CHILD_MAX_ITEMS);
+ if (childCount == 0) {
+ return;
+ }
+
+ final ViewGroup childContainer = (ViewGroup) groupView.findViewById(R.id.layout_child_container);
+ if (childContainer == null) {
+ return;
+ }
+
+ int numExistingViews = childContainer.getChildCount();
+ if (childCount < numExistingViews) {
+ int numToRemove = numExistingViews - childCount;
+ childContainer.removeViews(childCount, numToRemove);
+ numExistingViews = childCount;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ boolean isLastChild = (i == childCount - 1);
+ if (i < numExistingViews) {
+ View convertView = childContainer.getChildAt(i);
+ mAdapter.getChildView(groupPosition, i, isLastChild, convertView, mLinearLayout);
+ } else {
+ View childView = mAdapter.getChildView(groupPosition, i, isLastChild, null, mLinearLayout);
+ // remove the right/left padding so the child total aligns to left
+ childView.setPadding(0,
+ childView.getPaddingTop(),
+ 0,
+ isLastChild ? 0 : childView.getPaddingBottom()); // No padding bottom on last child
+ setViewBackgroundWithoutResettingPadding(childView, R.drawable.stats_list_item_child_background);
+ childContainer.addView(childView);
+ }
+ }
+
+ if (childContainer.getVisibility() != View.VISIBLE) {
+ if (animate) {
+ Animation expand = new ScaleAnimation(1.0f, 1.0f, 0.0f, 1.0f);
+ expand.setDuration(ANIM_DURATION);
+ expand.setInterpolator(getInterpolator());
+ childContainer.startAnimation(expand);
+ }
+ childContainer.setVisibility(View.VISIBLE);
+ }
+
+ StatsUIHelper.setGroupChevron(true, groupView, groupPosition, animate);
+ }
+
+ /**
+ * Removes URL underlines in a string by replacing URLSpan occurrences by
+ * URLSpanNoUnderline objects.
+ *
+ * @param pText A Spannable object. For example, a TextView casted as
+ * Spannable.
+ */
+ public static void removeUnderlines(Spannable pText) {
+ URLSpan[] spans = pText.getSpans(0, pText.length(), URLSpan.class);
+
+ for(URLSpan span:spans) {
+ int start = pText.getSpanStart(span);
+ int end = pText.getSpanEnd(span);
+ pText.removeSpan(span);
+ span = new URLSpanNoUnderline(span.getURL());
+ pText.setSpan(span, start, end, 0);
+ }
+ }
+
+ public static int getNumOfBarsToShow() {
+ if (StatsUtils.getSmallestWidthDP() >= TABLET_720DP && DisplayUtils.isLandscape(WordPress.getContext())) {
+ return 15;
+ } else if (StatsUtils.getSmallestWidthDP() >= TABLET_600DP) {
+ return 10;
+ } else {
+ return 7;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java
new file mode 100644
index 000000000..15467db20
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsUtils.java
@@ -0,0 +1,558 @@
+package org.wordpress.android.ui.stats;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyError;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.stats.exceptions.StatsError;
+import org.wordpress.android.ui.stats.models.AuthorsModel;
+import org.wordpress.android.ui.stats.models.BaseStatsModel;
+import org.wordpress.android.ui.stats.models.ClicksModel;
+import org.wordpress.android.ui.stats.models.CommentFollowersModel;
+import org.wordpress.android.ui.stats.models.CommentsModel;
+import org.wordpress.android.ui.stats.models.FollowersModel;
+import org.wordpress.android.ui.stats.models.GeoviewsModel;
+import org.wordpress.android.ui.stats.models.InsightsAllTimeModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostDetailsModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostModel;
+import org.wordpress.android.ui.stats.models.InsightsPopularModel;
+import org.wordpress.android.ui.stats.models.InsightsTodayModel;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.ui.stats.models.PublicizeModel;
+import org.wordpress.android.ui.stats.models.ReferrersModel;
+import org.wordpress.android.ui.stats.models.SearchTermsModel;
+import org.wordpress.android.ui.stats.models.TagsContainerModel;
+import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel;
+import org.wordpress.android.ui.stats.models.VideoPlaysModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+public class StatsUtils {
+ @SuppressLint("SimpleDateFormat")
+ private static long toMs(String date, String pattern) {
+ if (date == null || date.equals("null")) {
+ AppLog.w(T.UTILS, "Trying to parse a 'null' Stats Date.");
+ return -1;
+ }
+
+ if (pattern == null) {
+ AppLog.w(T.UTILS, "Trying to parse a Stats date with a null pattern");
+ return -1;
+ }
+
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+ try {
+ return sdf.parse(date).getTime();
+ } catch (ParseException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ return -1;
+ }
+
+ /**
+ * Converts date in the form of 2013-07-18 to ms *
+ */
+ public static long toMs(String date) {
+ return toMs(date, StatsConstants.STATS_INPUT_DATE_FORMAT);
+ }
+
+ public static String msToString(long ms, String format) {
+ SimpleDateFormat sdf = new SimpleDateFormat(format);
+ return sdf.format(new Date(ms));
+ }
+
+ /**
+ * Get the current date of the blog in the form of yyyy-MM-dd (EX: 2013-07-18) *
+ */
+ public static String getCurrentDateTZ(int localTableBlogID) {
+ String timezone = StatsUtils.getBlogTimezone(WordPress.getBlog(localTableBlogID));
+ if (timezone == null) {
+ AppLog.w(T.UTILS, "Timezone is null. Returning the device time!!");
+ return getCurrentDate();
+ }
+
+ return getCurrentDateTimeTZ(timezone, StatsConstants.STATS_INPUT_DATE_FORMAT);
+ }
+
+ /**
+ * Get the current datetime of the blog *
+ */
+ public static String getCurrentDateTimeTZ(int localTableBlogID) {
+ String timezone = StatsUtils.getBlogTimezone(WordPress.getBlog(localTableBlogID));
+ if (timezone == null) {
+ AppLog.w(T.UTILS, "Timezone is null. Returning the device time!!");
+ return getCurrentDatetime();
+ }
+ String pattern = "yyyy-MM-dd HH:mm:ss"; // precision to seconds
+ return getCurrentDateTimeTZ(timezone, pattern);
+ }
+
+ /**
+ * Get the current datetime of the blog in Ms *
+ */
+ public static long getCurrentDateTimeMsTZ(int localTableBlogID) {
+ String timezone = StatsUtils.getBlogTimezone(WordPress.getBlog(localTableBlogID));
+ if (timezone == null) {
+ AppLog.w(T.UTILS, "Timezone is null. Returning the device time!!");
+ return new Date().getTime();
+ }
+ String pattern = "yyyy-MM-dd HH:mm:ss"; // precision to seconds
+ return toMs(getCurrentDateTimeTZ(timezone, pattern), pattern);
+ }
+
+ /**
+ * Get the current date in the form of yyyy-MM-dd (EX: 2013-07-18) *
+ */
+ public static String getCurrentDate() {
+ SimpleDateFormat sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ return sdf.format(new Date());
+ }
+
+ /**
+ * Get the current date in the form of "yyyy-MM-dd HH:mm:ss"
+ */
+ private static String getCurrentDatetime() {
+ String pattern = "yyyy-MM-dd HH:mm:ss"; // precision to seconds
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+ return sdf.format(new Date());
+ }
+
+ private static String getBlogTimezone(Blog blog) {
+ if (blog == null) {
+ AppLog.w(T.UTILS, "Blog object is null!! Can't read timezone opt then.");
+ return null;
+ }
+
+ JSONObject jsonOptions = blog.getBlogOptionsJSONObject();
+ String timezone = null;
+ if (jsonOptions != null && jsonOptions.has("time_zone")) {
+ try {
+ timezone = jsonOptions.getJSONObject("time_zone").getString("value");
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load time_zone from options: " + jsonOptions, e);
+ }
+ } else {
+ AppLog.w(T.UTILS, "Blog options are null, or doesn't contain time_zone");
+ }
+ return timezone;
+ }
+
+ private static String getCurrentDateTimeTZ(String blogTimeZoneOption, String pattern) {
+ Date date = new Date();
+ SimpleDateFormat gmtDf = new SimpleDateFormat(pattern);
+
+ if (blogTimeZoneOption == null) {
+ AppLog.w(T.UTILS, "blogTimeZoneOption is null. getCurrentDateTZ() will return the device time!");
+ return gmtDf.format(date);
+ }
+
+ /*
+ Convert the timezone to a form that is compatible with Java TimeZone class
+ WordPress returns something like the following:
+ UTC+0:30 ----> 0.5
+ UTC+1 ----> 1.0
+ UTC-0:30 ----> -1.0
+ */
+
+ AppLog.v(T.STATS, "Parsing the following Timezone received from WP: " + blogTimeZoneOption);
+ String timezoneNormalized;
+ if (blogTimeZoneOption.equals("0") || blogTimeZoneOption.equals("0.0")) {
+ timezoneNormalized = "GMT";
+ } else {
+ String[] timezoneSplitted = org.apache.commons.lang.StringUtils.split(blogTimeZoneOption, ".");
+ timezoneNormalized = timezoneSplitted[0];
+ if(timezoneSplitted.length > 1 && timezoneSplitted[1].equals("5")){
+ timezoneNormalized += ":30";
+ }
+ if (timezoneNormalized.startsWith("-")) {
+ timezoneNormalized = "GMT" + timezoneNormalized;
+ } else {
+ if (timezoneNormalized.startsWith("+")) {
+ timezoneNormalized = "GMT" + timezoneNormalized;
+ } else {
+ timezoneNormalized = "GMT+" + timezoneNormalized;
+ }
+ }
+ }
+
+ AppLog.v(T.STATS, "Setting the following Timezone: " + timezoneNormalized);
+ gmtDf.setTimeZone(TimeZone.getTimeZone(timezoneNormalized));
+ return gmtDf.format(date);
+ }
+
+ public static String parseDate(String timestamp, String fromFormat, String toFormat) {
+ SimpleDateFormat from = new SimpleDateFormat(fromFormat);
+ SimpleDateFormat to = new SimpleDateFormat(toFormat);
+ try {
+ Date date = from.parse(timestamp);
+ return to.format(date);
+ } catch (ParseException e) {
+ AppLog.e(T.STATS, e);
+ }
+ return "";
+ }
+
+ /**
+ * Get a diff between two dates
+ * @param date1 the oldest date in Ms
+ * @param date2 the newest date in Ms
+ * @param timeUnit the unit in which you want the diff
+ * @return the diff value, in the provided unit
+ */
+ public static long getDateDiff(Date date1, Date date2, TimeUnit timeUnit) {
+ long diffInMillies = date2.getTime() - date1.getTime();
+ return timeUnit.convert(diffInMillies, TimeUnit.MILLISECONDS);
+ }
+
+
+ //Calculate the correct start/end date for the selected period
+ public static String getPublishedEndpointPeriodDateParameters(StatsTimeframe timeframe, String date) {
+ if (date == null) {
+ AppLog.w(AppLog.T.STATS, "Can't calculate start and end period without a reference date");
+ return null;
+ }
+
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ Date parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+
+
+ final String after;
+ final String before;
+ switch (timeframe) {
+ case DAY:
+ after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ c.add(Calendar.DAY_OF_YEAR, +1);
+ before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case WEEK:
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+ c.add(Calendar.DAY_OF_YEAR, +1);
+ before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case MONTH:
+ //first day of the next month
+ c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH));
+ c.add(Calendar.DAY_OF_YEAR, +1);
+ before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+
+ //last day of the prev month
+ c.setTime(parsedDate);
+ c.set(Calendar.DAY_OF_MONTH, c.getActualMinimum(Calendar.DAY_OF_MONTH));
+ after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case YEAR:
+ //first day of the next year
+ c.set(Calendar.MONTH, Calendar.DECEMBER);
+ c.set(Calendar.DAY_OF_MONTH, 31);
+ c.add(Calendar.DAY_OF_YEAR, +1);
+ before = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+
+ c.setTime(parsedDate);
+ c.set(Calendar.MONTH, Calendar.JANUARY);
+ c.set(Calendar.DAY_OF_MONTH, 1);
+ after = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ default:
+ AppLog.w(AppLog.T.STATS, "Can't calculate start and end period without a reference timeframe");
+ return null;
+ }
+ return "&after=" + after + "&before=" + before;
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return null;
+ }
+ }
+
+ public static int getSmallestWidthDP() {
+ return WordPress.getContext().getResources().getInteger(R.integer.smallest_width_dp);
+ }
+
+ public static int getLocalBlogIdFromRemoteBlogId(int remoteBlogID) {
+ // workaround: There are 2 entries in the DB for each Jetpack blog linked with
+ // the current wpcom account. We need to load the correct localID here, otherwise options are
+ // blank
+ int localId = WordPress.wpDB.getLocalTableBlogIdForJetpackRemoteID(
+ remoteBlogID,
+ null);
+ if (localId == 0) {
+ localId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(
+ remoteBlogID
+ );
+ }
+
+ return localId;
+ }
+
+ public static synchronized void logVolleyErrorDetails(final VolleyError volleyError) {
+ if (volleyError == null) {
+ AppLog.e(T.STATS, "Tried to log a VolleyError, but the error obj was null!");
+ return;
+ }
+ if (volleyError.networkResponse != null) {
+ NetworkResponse networkResponse = volleyError.networkResponse;
+ AppLog.e(T.STATS, "Network status code: " + networkResponse.statusCode);
+ if (networkResponse.data != null) {
+ AppLog.e(T.STATS, "Network data: " + new String(networkResponse.data));
+ }
+ }
+ AppLog.e(T.STATS, "Volley Error Message: " + volleyError.getMessage(), volleyError);
+ }
+
+ public static synchronized boolean isRESTDisabledError(final Serializable error) {
+ if (error == null || !(error instanceof com.android.volley.AuthFailureError)) {
+ return false;
+ }
+ com.android.volley.AuthFailureError volleyError = (com.android.volley.AuthFailureError) error;
+ if (volleyError.networkResponse != null && volleyError.networkResponse.data != null) {
+ String errorMessage = new String(volleyError.networkResponse.data).toLowerCase();
+ return errorMessage.contains("api calls") && errorMessage.contains("disabled");
+ } else {
+ AppLog.e(T.STATS, "Network response is null in Volley. Can't check if it is a Rest Disabled error.");
+ return false;
+ }
+ }
+
+ public static synchronized BaseStatsModel parseResponse(StatsService.StatsEndpointsEnum endpointName, String blogID, JSONObject response)
+ throws JSONException {
+ BaseStatsModel model = null;
+ switch (endpointName) {
+ case VISITS:
+ model = new VisitsModel(blogID, response);
+ break;
+ case TOP_POSTS:
+ model = new TopPostsAndPagesModel(blogID, response);
+ break;
+ case REFERRERS:
+ model = new ReferrersModel(blogID, response);
+ break;
+ case CLICKS:
+ model = new ClicksModel(blogID, response);
+ break;
+ case GEO_VIEWS:
+ model = new GeoviewsModel(blogID, response);
+ break;
+ case AUTHORS:
+ model = new AuthorsModel(blogID, response);
+ break;
+ case VIDEO_PLAYS:
+ model = new VideoPlaysModel(blogID, response);
+ break;
+ case COMMENTS:
+ model = new CommentsModel(blogID, response);
+ break;
+ case FOLLOWERS_WPCOM:
+ model = new FollowersModel(blogID, response);
+ break;
+ case FOLLOWERS_EMAIL:
+ model = new FollowersModel(blogID, response);
+ break;
+ case COMMENT_FOLLOWERS:
+ model = new CommentFollowersModel(blogID, response);
+ break;
+ case TAGS_AND_CATEGORIES:
+ model = new TagsContainerModel(blogID, response);
+ break;
+ case PUBLICIZE:
+ model = new PublicizeModel(blogID, response);
+ break;
+ case SEARCH_TERMS:
+ model = new SearchTermsModel(blogID, response);
+ break;
+ case INSIGHTS_ALL_TIME:
+ model = new InsightsAllTimeModel(blogID, response);
+ break;
+ case INSIGHTS_POPULAR:
+ model = new InsightsPopularModel(blogID, response);
+ break;
+ case INSIGHTS_TODAY:
+ model = new InsightsTodayModel(blogID, response);
+ break;
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ model = new InsightsLatestPostModel(blogID, response);
+ break;
+ case INSIGHTS_LATEST_POST_VIEWS:
+ model = new InsightsLatestPostDetailsModel(blogID, response);
+ break;
+ }
+ return model;
+ }
+
+ public static void openPostInReaderOrInAppWebview(Context ctx, final String remoteBlogID,
+ final String remoteItemID,
+ final String itemType,
+ final String itemURL) {
+ final long blogID = Long.parseLong(remoteBlogID);
+ final long itemID = Long.parseLong(remoteItemID);
+ if (itemType == null) {
+ // If we don't know the type of the item, open it with the browser.
+ AppLog.d(AppLog.T.UTILS, "Type of the item is null. Opening it in the in-app browser: " + itemURL);
+ WPWebViewActivity.openURL(ctx, itemURL);
+ } else if (itemType.equals(StatsConstants.ITEM_TYPE_POST)
+ || itemType.equals(StatsConstants.ITEM_TYPE_PAGE)) {
+ // If the post/page has ID == 0 is the home page, and we need to load the blog preview,
+ // otherwise 404 is returned if we try to show the post in the reader
+ if (itemID == 0) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ ctx,
+ blogID
+ );
+ } else {
+ ReaderActivityLauncher.showReaderPostDetail(
+ ctx,
+ blogID,
+ itemID
+ );
+ }
+ } else if (itemType.equals(StatsConstants.ITEM_TYPE_HOME_PAGE)) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ ctx,
+ blogID
+ );
+ } else {
+ AppLog.d(AppLog.T.UTILS, "Opening the in-app browser: " + itemURL);
+ WPWebViewActivity.openURL(ctx, itemURL);
+ }
+ }
+
+ public static void openPostInReaderOrInAppWebview(Context ctx, final PostModel post) {
+ final String postType = post.getPostType();
+ final String url = post.getUrl();
+ final String blogID = post.getBlogID();
+ final String itemID = post.getItemID();
+ openPostInReaderOrInAppWebview(ctx, blogID, itemID, postType, url);
+ }
+
+ /*
+ * This function rewrites a VolleyError into a simple Stats Error by getting the error message.
+ * This is a FIX for https://github.com/wordpress-mobile/WordPress-Android/issues/2228 where
+ * VolleyErrors cannot be serializable.
+ */
+ public static StatsError rewriteVolleyError(VolleyError volleyError, String defaultErrorString) {
+ if (volleyError != null && volleyError.getMessage() != null) {
+ return new StatsError(volleyError.getMessage());
+ }
+
+ if (defaultErrorString != null) {
+ return new StatsError(defaultErrorString);
+ }
+
+ // Error string should be localized here, but don't want to pass a context
+ return new StatsError("Stats couldn't be refreshed at this time");
+ }
+
+
+ private static int roundUp(double num, double divisor) {
+ double unrounded = num / divisor;
+ //return (int) Math.ceil(unrounded);
+ return (int) (unrounded + 0.5);
+ }
+
+ public static String getSinceLabel(Context ctx, String dataSubscribed) {
+
+ Date currentDateTime = new Date();
+
+ try {
+ SimpleDateFormat from = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+ Date date = from.parse(dataSubscribed);
+
+ // See http://momentjs.com/docs/#/displaying/fromnow/
+ long currentDifference = Math.abs(
+ StatsUtils.getDateDiff(date, currentDateTime, TimeUnit.SECONDS)
+ );
+
+ if (currentDifference <= 45 ) {
+ return ctx.getString(R.string.stats_followers_seconds_ago);
+ }
+ if (currentDifference < 90 ) {
+ return ctx.getString(R.string.stats_followers_a_minute_ago);
+ }
+
+ // 90 seconds to 45 minutes
+ if (currentDifference <= 2700 ) {
+ long minutes = StatsUtils.roundUp(currentDifference, 60);
+ String followersMinutes = ctx.getString(R.string.stats_followers_minutes);
+ return String.format(followersMinutes, minutes);
+ }
+
+ // 45 to 90 minutes
+ if (currentDifference <= 5400 ) {
+ return ctx.getString(R.string.stats_followers_an_hour_ago);
+ }
+
+ // 90 minutes to 22 hours
+ if (currentDifference <= 79200 ) {
+ long hours = StatsUtils.roundUp(currentDifference, 60 * 60);
+ String followersHours = ctx.getString(R.string.stats_followers_hours);
+ return String.format(followersHours, hours);
+ }
+
+ // 22 to 36 hours
+ if (currentDifference <= 129600 ) {
+ return ctx.getString(R.string.stats_followers_a_day);
+ }
+
+ // 36 hours to 25 days
+ // 86400 secs in a day - 2160000 secs in 25 days
+ if (currentDifference <= 2160000 ) {
+ long days = StatsUtils.roundUp(currentDifference, 86400);
+ String followersDays = ctx.getString(R.string.stats_followers_days);
+ return String.format(followersDays, days);
+ }
+
+ // 25 to 45 days
+ // 3888000 secs in 45 days
+ if (currentDifference <= 3888000 ) {
+ return ctx.getString(R.string.stats_followers_a_month);
+ }
+
+ // 45 to 345 days
+ // 2678400 secs in a month - 29808000 secs in 345 days
+ if (currentDifference <= 29808000 ) {
+ long months = StatsUtils.roundUp(currentDifference, 2678400);
+ String followersMonths = ctx.getString(R.string.stats_followers_months);
+ return String.format(followersMonths, months);
+ }
+
+ // 345 to 547 days (1.5 years)
+ if (currentDifference <= 47260800 ) {
+ return ctx.getString(R.string.stats_followers_a_year);
+ }
+
+ // 548 days+
+ // 31536000 secs in a year
+ long years = StatsUtils.roundUp(currentDifference, 31536000);
+ String followersYears = ctx.getString(R.string.stats_followers_years);
+ return String.format(followersYears, years);
+
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+
+ return "";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java
new file mode 100644
index 000000000..3ca853a8f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVideoplaysFragment.java
@@ -0,0 +1,170 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.models.SingleItemModel;
+import org.wordpress.android.ui.stats.models.VideoPlaysModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class StatsVideoplaysFragment extends StatsAbstractListFragment {
+ public static final String TAG = StatsVideoplaysFragment.class.getSimpleName();
+
+ private VideoPlaysModel mVideos;
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mVideos != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mVideos);
+ }
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mVideos = (VideoPlaysModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.VideoPlaysUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mVideos = event.mVideos;
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mVideos = null;
+ showErrorUI(event.mError);
+ }
+
+
+ @Override
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (hasVideoplays()) {
+ ArrayAdapter adapter = new TopPostsAndPagesAdapter(getActivity(), getVideoplays());
+ StatsUIHelper.reloadLinearLayout(getActivity(), adapter, mList, getMaxNumberOfItemsToShowInList());
+ showHideNoResultsUI(false);
+ } else {
+ showHideNoResultsUI(true);
+ }
+ }
+
+ private boolean hasVideoplays() {
+ return mVideos != null
+ && mVideos.getPlays() != null
+ && mVideos.getPlays().size() > 0;
+ }
+
+ private List<SingleItemModel> getVideoplays() {
+ if (!hasVideoplays()) {
+ return new ArrayList<SingleItemModel>(0);
+ }
+ return mVideos.getPlays();
+ }
+
+ @Override
+ protected boolean isViewAllOptionAvailable() {
+ return hasVideoplays() && getVideoplays().size() > MAX_NUM_OF_ITEMS_DISPLAYED_IN_LIST;
+ }
+
+ @Override
+ protected boolean isExpandableList() {
+ return false;
+ }
+
+ private class TopPostsAndPagesAdapter extends ArrayAdapter<SingleItemModel> {
+
+ private final List<SingleItemModel> list;
+ private final LayoutInflater inflater;
+
+ public TopPostsAndPagesAdapter(Context context, List<SingleItemModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final SingleItemModel currentRowData = list.get(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+ // fill data
+ // entries
+ holder.setEntryTextOrLink(currentRowData.getUrl(), currentRowData.getTitle());
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected int getEntryLabelResId() {
+ return R.string.stats_entry_video_plays;
+ }
+
+ @Override
+ protected int getTotalsLabelResId() {
+ return R.string.stats_totals_plays;
+ }
+
+ @Override
+ protected int getEmptyLabelTitleResId() {
+ return R.string.stats_empty_video;
+ }
+
+ @Override
+ protected int getEmptyLabelDescResId() {
+ return R.string.stats_empty_video_desc;
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.VIDEO_PLAYS
+ };
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.stats_view_videos);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java
new file mode 100644
index 000000000..0f0c795b5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewAllActivity.java
@@ -0,0 +1,318 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Single item details activity.
+ */
+public class StatsViewAllActivity extends AppCompatActivity {
+
+ public static final String ARG_STATS_VIEW_ALL_TITLE = "arg_stats_view_all_title";
+ private static final String SAVED_STATS_SCROLL_POSITION = "SAVED_STATS_SCROLL_POSITION";
+
+ private boolean mIsInFront;
+ private boolean mIsUpdatingStats;
+ private SwipeToRefreshHelper mSwipeToRefreshHelper;
+ private ScrollViewExt mOuterScrollView;
+
+ private StatsAbstractListFragment mFragment;
+
+ private int mLocalBlogID = -1;
+ private StatsTimeframe mTimeframe;
+ private StatsViewType mStatsViewType;
+ private String mDate;
+ private Serializable[] mRestResponse;
+ private int mOuterPagerSelectedButtonIndex = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.stats_activity_view_all);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ setTitle(R.string.stats);
+
+ mOuterScrollView = (ScrollViewExt) findViewById(R.id.scroll_view_stats);
+
+ // pull to refresh setup
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(this, (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout),
+ new SwipeToRefreshHelper.RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!NetworkUtils.checkConnection(getBaseContext())) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ mIsUpdatingStats = false;
+ return;
+ }
+
+ if (mIsUpdatingStats) {
+ AppLog.w(AppLog.T.STATS, "stats are already updating, refresh cancelled");
+ return;
+ }
+
+ refreshStats();
+ }
+ }
+ );
+
+ if (savedInstanceState != null) {
+ mLocalBlogID = savedInstanceState.getInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, -1);
+ Serializable oldData = savedInstanceState.getSerializable(StatsAbstractFragment.ARG_REST_RESPONSE);
+ if (oldData != null && oldData instanceof Serializable[]) {
+ mRestResponse = (Serializable[]) oldData;
+ }
+ mTimeframe = (StatsTimeframe) savedInstanceState.getSerializable(StatsAbstractFragment.ARGS_TIMEFRAME);
+ mDate = savedInstanceState.getString(StatsAbstractFragment.ARGS_SELECTED_DATE);
+ mStatsViewType = (StatsViewType) savedInstanceState.getSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE);
+ mOuterPagerSelectedButtonIndex = savedInstanceState.getInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, 0);
+ final int yScrollPosition = savedInstanceState.getInt(SAVED_STATS_SCROLL_POSITION);
+ if(yScrollPosition != 0) {
+ mOuterScrollView.postDelayed(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ mOuterScrollView.scrollTo(0, yScrollPosition);
+ }
+ }
+ }, StatsConstants.STATS_SCROLL_TO_DELAY);
+ }
+ } else if (getIntent() != null) {
+ Bundle extras = getIntent().getExtras();
+ mLocalBlogID = extras.getInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, -1);
+ mTimeframe = (StatsTimeframe) extras.getSerializable(StatsAbstractFragment.ARGS_TIMEFRAME);
+ mDate = extras.getString(StatsAbstractFragment.ARGS_SELECTED_DATE);
+ mStatsViewType = (StatsViewType) extras.getSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE);
+ mOuterPagerSelectedButtonIndex = extras.getInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, 0);
+
+ // Set a custom activity title if one was passed
+ if (!TextUtils.isEmpty(extras.getString(ARG_STATS_VIEW_ALL_TITLE))) {
+ setTitle(extras.getString(ARG_STATS_VIEW_ALL_TITLE));
+ }
+ }
+
+ if (mStatsViewType == null || mTimeframe == null || mDate == null) {
+ Toast.makeText(this, getResources().getText(R.string.stats_generic_error),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ // Setup the top date label. It's available on those fragments that are affected by the top date selector.
+ TextView dateTextView = (TextView) findViewById(R.id.stats_summary_date);
+ switch (mStatsViewType) {
+ case TOP_POSTS_AND_PAGES:
+ case REFERRERS:
+ case CLICKS:
+ case GEOVIEWS:
+ case AUTHORS:
+ case VIDEO_PLAYS:
+ case SEARCH_TERMS:
+ dateTextView.setText(getDateForDisplayInLabels(mDate, mTimeframe));
+ dateTextView.setVisibility(View.VISIBLE);
+ break;
+ default:
+ dateTextView.setVisibility(View.GONE);
+ break;
+ }
+
+ FragmentManager fm = getFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+ mFragment = (StatsAbstractListFragment) fm.findFragmentByTag("ViewAll-Fragment");
+ if (mFragment == null) {
+ mFragment = getInnerFragment();
+ ft.replace(R.id.stats_single_view_fragment, mFragment, "ViewAll-Fragment");
+ ft.commitAllowingStateLoss();
+ }
+
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_VIEW_ALL_ACCESSED, WordPress.getBlog(mLocalBlogID));
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.UpdateStatusChanged event) {
+ if (isFinishing() || !mIsInFront) {
+ return;
+ }
+ mSwipeToRefreshHelper.setRefreshing(event.mUpdating);
+ mIsUpdatingStats = event.mUpdating;
+ }
+
+ private String getDateForDisplayInLabels(String date, StatsTimeframe timeframe) {
+ String prefix = getString(R.string.stats_for);
+ switch (timeframe) {
+ case DAY:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT));
+ case WEEK:
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ final Date parsedDate = sdf.parse(date);
+ Calendar c = Calendar.getInstance();
+ c.setTime(parsedDate);
+ String endDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ // last day of this week
+ c.add(Calendar.DAY_OF_WEEK, - 6);
+ String startDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ return String.format(prefix, startDateLabel + " - " + endDateLabel);
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return "";
+ }
+ case MONTH:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_FORMAT));
+ case YEAR:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_YEAR_FORMAT));
+ }
+ return "";
+ }
+
+ private StatsAbstractListFragment getInnerFragment() {
+ StatsAbstractListFragment fragment = null;
+ switch (mStatsViewType) {
+ case TOP_POSTS_AND_PAGES:
+ fragment = new StatsTopPostsAndPagesFragment();
+ break;
+ case REFERRERS:
+ fragment = new StatsReferrersFragment();
+ break;
+ case CLICKS:
+ fragment = new StatsClicksFragment();
+ break;
+ case GEOVIEWS:
+ fragment = new StatsGeoviewsFragment();
+ break;
+ case AUTHORS:
+ fragment = new StatsAuthorsFragment();
+ break;
+ case VIDEO_PLAYS:
+ fragment = new StatsVideoplaysFragment();
+ break;
+ case COMMENTS:
+ fragment = new StatsCommentsFragment();
+ break;
+ case TAGS_AND_CATEGORIES:
+ fragment = new StatsTagsAndCategoriesFragment();
+ break;
+ case PUBLICIZE:
+ fragment = new StatsPublicizeFragment();
+ break;
+ case FOLLOWERS:
+ fragment = new StatsFollowersFragment();
+ break;
+ case SEARCH_TERMS:
+ fragment = new StatsSearchTermsFragment();
+ break;
+ }
+
+ fragment.setTimeframe(mTimeframe);
+ fragment.setDate(mDate);
+
+ Bundle args = new Bundle();
+ args.putInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ args.putSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE, mStatsViewType);
+ args.putBoolean(StatsAbstractListFragment.ARGS_IS_SINGLE_VIEW, true); // Always true here
+ args.putInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mOuterPagerSelectedButtonIndex);
+ args.putSerializable(StatsAbstractFragment.ARG_REST_RESPONSE, mRestResponse);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putInt(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, mLocalBlogID);
+ outState.putSerializable(StatsAbstractFragment.ARG_REST_RESPONSE, mRestResponse);
+ outState.putSerializable(StatsAbstractFragment.ARGS_TIMEFRAME, mTimeframe);
+ outState.putString(StatsAbstractFragment.ARGS_SELECTED_DATE, mDate);
+ outState.putSerializable(StatsAbstractFragment.ARGS_VIEW_TYPE, mStatsViewType);
+ outState.putInt(StatsAbstractListFragment.ARGS_TOP_PAGER_SELECTED_BUTTON_INDEX, mOuterPagerSelectedButtonIndex);
+ if (mOuterScrollView.getScrollY() != 0) {
+ outState.putInt(SAVED_STATS_SCROLL_POSITION, mOuterScrollView.getScrollY());
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsInFront = true;
+ NetworkUtils.checkConnection(this); // show the error toast if the network is offline
+ ActivityId.trackLastActivity(ActivityId.STATS_VIEW_ALL);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsInFront = false;
+ mIsUpdatingStats = false;
+ mSwipeToRefreshHelper.setRefreshing(false);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void refreshStats() {
+ if (mIsUpdatingStats) {
+ return;
+ }
+ if (!NetworkUtils.isNetworkAvailable(this)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ AppLog.w(AppLog.T.STATS, "ViewAll on "+ mFragment.getTag() + " > no connection, update canceled");
+ return;
+ }
+
+ if (mFragment != null) {
+ mFragment.refreshStats();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java
new file mode 100644
index 000000000..0230dec77
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewHolder.java
@@ -0,0 +1,170 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.R;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+/**
+ * View holder for stats_list_cell layout
+ */
+public class StatsViewHolder {
+ public final TextView entryTextView;
+ public final TextView totalsTextView;
+ public final WPNetworkImageView networkImageView;
+ public final ImageView chevronImageView;
+ public final ImageView linkImageView;
+ public final ImageView imgMore;
+ public final LinearLayout rowContent;
+
+ public StatsViewHolder(View view) {
+ rowContent = (LinearLayout) view.findViewById(R.id.layout_content);
+ entryTextView = (TextView) view.findViewById(R.id.stats_list_cell_entry);
+ totalsTextView = (TextView) view.findViewById(R.id.stats_list_cell_total);
+ chevronImageView = (ImageView) view.findViewById(R.id.stats_list_cell_chevron);
+ linkImageView = (ImageView) view.findViewById(R.id.stats_list_cell_link);
+ networkImageView = (WPNetworkImageView) view.findViewById(R.id.stats_list_cell_image);
+
+ imgMore = (ImageView) view.findViewById(R.id.image_more);
+ }
+
+ /*
+ * used by stats fragments to set the entry text, making it a clickable link if a url is passed
+ */
+ public void setEntryTextOrLink(final String linkURL, String linkName) {
+ if (entryTextView == null) {
+ return;
+ }
+
+ entryTextView.setText(linkName);
+ if (TextUtils.isEmpty(linkURL)) {
+ entryTextView.setTextColor(entryTextView.getContext().getResources().getColor(R.color.stats_text_color));
+ rowContent.setClickable(false);
+ return;
+ }
+
+ rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String url = linkURL;
+ AppLog.d(AppLog.T.UTILS, "Tapped on the Link: " + url);
+ if (url.startsWith("https://wordpress.com/my-stats")
+ || url.startsWith("http://wordpress.com/my-stats")) {
+ // make sure to load the no-chrome version of Stats over https
+ url = UrlUtils.makeHttps(url);
+ if (url.contains("?")) {
+ // add the no chrome parameters if not available
+ if (!url.contains("?no-chrome") && !url.contains("&no-chrome")) {
+ url += "&no-chrome";
+ }
+ } else {
+ url += "?no-chrome";
+ }
+ AppLog.d(AppLog.T.UTILS, "Opening the Authenticated in-app browser : " + url);
+ // Let's try the global wpcom credentials
+ String statsAuthenticatedUser = AccountHelper.getDefaultAccount().getUserName();
+ if (org.apache.commons.lang.StringUtils.isEmpty(statsAuthenticatedUser)) {
+ // Still empty. Do not eat the event, but let's open the default Web Browser.
+
+ }
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(view.getContext(),
+ url, statsAuthenticatedUser);
+
+ } else if (url.startsWith("https") || url.startsWith("http")) {
+ AppLog.d(AppLog.T.UTILS, "Opening the in-app browser: " + url);
+ WPWebViewActivity.openURL(view.getContext(), url);
+ }
+
+ }
+ }
+ );
+
+ entryTextView.setTextColor(entryTextView.getContext().getResources().getColor(R.color.stats_link_text_color));
+ }
+
+ public void setEntryText(String text) {
+ entryTextView.setText(text);
+ rowContent.setClickable(false);
+ }
+
+ public void setEntryText(String text, int color) {
+ entryTextView.setTextColor(color);
+ setEntryText(text);
+ }
+
+
+ /*
+ * Used by stats fragments to set the entry text, opening the stats details page.
+ */
+ public void setEntryTextOpenDetailsPage(final PostModel currentItem) {
+ if (entryTextView == null) {
+ return;
+ }
+
+ String name = StringEscapeUtils.unescapeHtml(currentItem.getTitle());
+ entryTextView.setText(name);
+ rowContent.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ActivityLauncher.viewStatsSinglePostDetails(view.getContext(), currentItem);
+ }
+ });
+ entryTextView.setTextColor(entryTextView.getContext().getResources().getColor(R.color.stats_link_text_color));
+ }
+
+ /*
+ * Used by stats fragments to create the more btn context menu with the "View" option in it.
+ * Opening it with reader if possible.
+ *
+ */
+ public void setMoreButtonOpenInReader(final PostModel currentItem) {
+ if (imgMore == null) {
+ return;
+ }
+
+ imgMore.setVisibility(View.VISIBLE);
+ imgMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ final Context ctx = view.getContext();
+ PopupMenu popup = new PopupMenu(ctx, view);
+ MenuItem menuItem = popup.getMenu().add(ctx.getString(R.string.stats_view));
+ menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ StatsUtils.openPostInReaderOrInAppWebview(ctx, currentItem);
+ return true;
+ }
+ });
+ popup.show();
+ }
+ });
+ }
+
+
+ public void showChevronIcon() {
+ linkImageView.setVisibility(View.GONE);
+ chevronImageView.setVisibility(View.VISIBLE);
+ }
+
+ public void showLinkIcon() {
+ linkImageView.setVisibility(View.VISIBLE);
+ chevronImageView.setVisibility(View.GONE);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java
new file mode 100644
index 000000000..2510d61e0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsViewType.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.ui.stats;
+
+/**
+ * An enum of the different view types to appear on the stats view.
+ */
+
+public enum StatsViewType {
+ // TIMEFRAME_SELECTOR,
+ GRAPH_AND_SUMMARY,
+ TOP_POSTS_AND_PAGES,
+ REFERRERS,
+ CLICKS,
+ GEOVIEWS,
+ AUTHORS,
+ VIDEO_PLAYS,
+ COMMENTS,
+ TAGS_AND_CATEGORIES,
+ PUBLICIZE,
+ FOLLOWERS,
+ SEARCH_TERMS,
+ INSIGHTS_MOST_POPULAR,
+ INSIGHTS_ALL_TIME,
+ INSIGHTS_TODAY,
+ INSIGHTS_LATEST_POST_SUMMARY,
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java
new file mode 100644
index 000000000..230064f6f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsVisitorsAndViewsFragment.java
@@ -0,0 +1,846 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CheckedTextView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.jjoe64.graphview.GraphView;
+import com.jjoe64.graphview.GraphViewSeries;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+public class StatsVisitorsAndViewsFragment extends StatsAbstractFragment
+ implements StatsBarGraph.OnGestureListener {
+
+ public static final String TAG = StatsVisitorsAndViewsFragment.class.getSimpleName();
+ private static final String ARG_SELECTED_GRAPH_BAR = "ARG_SELECTED_GRAPH_BAR";
+ private static final String ARG_PREV_NUMBER_OF_BARS = "ARG_PREV_NUMBER_OF_BARS";
+ private static final String ARG_SELECTED_OVERVIEW_ITEM = "ARG_SELECTED_OVERVIEW_ITEM";
+ private static final String ARG_CHECKBOX_SELECTED = "ARG_CHECKBOX_SELECTED";
+
+
+ private LinearLayout mGraphContainer;
+ private LinearLayout mNoActivtyThisPeriodContainer;
+ private StatsBarGraph mGraphView;
+ private LinearLayout mModuleButtonsContainer;
+ private TextView mDateTextView;
+ private String[] mStatsDate;
+
+ private LinearLayout mLegendContainer;
+ private CheckedTextView mLegendLabel;
+ private LinearLayout mVisitorsCheckboxContainer;
+ private CheckBox mVisitorsCheckbox;
+ private boolean mIsCheckboxChecked = true;
+
+ private OnDateChangeListener mListener;
+ private OnOverviewItemChangeListener mOverviewItemChangeListener;
+
+ private final OverviewLabel[] overviewItems = {OverviewLabel.VIEWS, OverviewLabel.VISITORS, OverviewLabel.LIKES,
+ OverviewLabel.COMMENTS};
+
+ // Restore the following variables on restart
+ private VisitsModel mVisitsData;
+ private int mSelectedOverviewItemIndex = 0;
+ private int mSelectedBarGraphBarIndex = -1;
+ private int mPrevNumberOfBarsGraph = -1;
+
+ // Container Activity must implement this interface
+ public interface OnDateChangeListener {
+ void onDateChanged(String blogID, StatsTimeframe timeframe, String newDate);
+ }
+
+ // Container Activity must implement this interface
+ public interface OnOverviewItemChangeListener {
+ void onOverviewItemChanged(OverviewLabel newItem);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mListener = (OnDateChangeListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnDateChangeListener");
+ }
+ try {
+ mOverviewItemChangeListener = (OnOverviewItemChangeListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnOverviewItemChangeListener");
+ }
+ }
+
+ void setSelectedOverviewItem(OverviewLabel itemToSelect) {
+ for (int i = 0; i < overviewItems.length; i++) {
+ if (overviewItems[i] == itemToSelect) {
+ mSelectedOverviewItemIndex = i;
+ return;
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.stats_visitors_and_views_fragment, container, false);
+
+ mDateTextView = (TextView) view.findViewById(R.id.stats_summary_date);
+ mGraphContainer = (LinearLayout) view.findViewById(R.id.stats_bar_chart_fragment_container);
+ mModuleButtonsContainer = (LinearLayout) view.findViewById(R.id.stats_pager_tabs);
+ mNoActivtyThisPeriodContainer = (LinearLayout) view.findViewById(R.id.stats_bar_chart_no_activity);
+
+ mLegendContainer = (LinearLayout) view.findViewById(R.id.stats_legend_container);
+ mLegendLabel = (CheckedTextView) view.findViewById(R.id.stats_legend_label);
+ mLegendLabel.setCheckMarkDrawable(null); // Make sure to set a null drawable here. Otherwise the touching area is the same of a TextView
+ mVisitorsCheckboxContainer = (LinearLayout) view.findViewById(R.id.stats_checkbox_visitors_container);
+ mVisitorsCheckbox = (CheckBox) view.findViewById(R.id.stats_checkbox_visitors);
+ mVisitorsCheckbox.setOnClickListener(onCheckboxClicked);
+
+ // Fix an issue on devices with 4.1 or lower, where the Checkbox already uses padding by default internally and overriding it with paddingLeft
+ // causes the issue report here https://github.com/wordpress-mobile/WordPress-Android/pull/2377#issuecomment-77067993
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ mVisitorsCheckbox.setPadding(getResources().getDimensionPixelSize(R.dimen.margin_medium), 0, 0, 0);
+ }
+
+ // Make sure we've all the info to build the tab correctly. This is ALWAYS true
+ if (mModuleButtonsContainer.getChildCount() == overviewItems.length) {
+ for (int i = 0; i < mModuleButtonsContainer.getChildCount(); i++) {
+ LinearLayout currentTab = (LinearLayout) mModuleButtonsContainer.getChildAt(i);
+ boolean isLastItem = i == (overviewItems.length - 1);
+ boolean isChecked = i == mSelectedOverviewItemIndex;
+ TabViewHolder currentTabViewHolder = new TabViewHolder(currentTab, overviewItems[i], isChecked, isLastItem);
+ currentTab.setOnClickListener(TopButtonsOnClickListener);
+ currentTab.setTag(currentTabViewHolder);
+ }
+ mModuleButtonsContainer.setVisibility(View.VISIBLE);
+ }
+
+ return view;
+ }
+
+ private class TabViewHolder {
+ final LinearLayout tab;
+ final LinearLayout innerContainer;
+ final TextView label;
+ final TextView value;
+ final ImageView icon;
+ final OverviewLabel labelItem;
+ boolean isChecked = false;
+ boolean isLastItem = false;
+
+ public TabViewHolder(LinearLayout currentTab, OverviewLabel labelItem, boolean checked, boolean isLastItem) {
+ tab = currentTab;
+ innerContainer = (LinearLayout) currentTab.findViewById(R.id.stats_visitors_and_views_tab_inner_container);
+ label = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_label);
+ label.setText(labelItem.getLabel());
+ value = (TextView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_value);
+ icon = (ImageView) currentTab.findViewById(R.id.stats_visitors_and_views_tab_icon);
+ this.labelItem = labelItem;
+ this.isChecked = checked;
+ this.isLastItem = isLastItem;
+ updateBackGroundAndIcon(0);
+ }
+
+ private Drawable getTabIcon() {
+ switch (labelItem) {
+ case VISITORS:
+ return getResources().getDrawable(R.drawable.stats_icon_visitors);
+ case COMMENTS:
+ return getResources().getDrawable(R.drawable.stats_icon_comments);
+ case LIKES:
+ return getResources().getDrawable(R.drawable.stats_icon_likes);
+ default:
+ // Views and when no prev match
+ return getResources().getDrawable(R.drawable.stats_icon_views);
+ }
+ }
+
+ public void updateBackGroundAndIcon(int currentValue) {
+ if (isChecked) {
+ value.setTextColor(getResources().getColor(R.color.orange_jazzy));
+ } else {
+ if (currentValue == 0) {
+ value.setTextColor(getResources().getColor(R.color.grey));
+ } else {
+ value.setTextColor(getResources().getColor(R.color.blue_wordpress));
+ }
+ }
+
+ icon.setImageDrawable(getTabIcon());
+
+ if (isLastItem) {
+ if (isChecked) {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_white);
+ } else {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_latest_blue_light);
+ }
+ } else {
+ if (isChecked) {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_white);
+ } else {
+ tab.setBackgroundResource(R.drawable.stats_visitors_and_views_button_blue_light);
+ }
+ }
+ }
+
+ public void setChecked(boolean checked) {
+ this.isChecked = checked;
+ }
+ }
+
+ private final View.OnClickListener TopButtonsOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ //LinearLayout tab = (LinearLayout) v;
+ TabViewHolder tabViewHolder = (TabViewHolder) v.getTag();
+
+ if (tabViewHolder.isChecked) {
+ // already checked. Do nothing
+ return;
+ }
+
+ int numberOfTabs = mModuleButtonsContainer.getChildCount();
+ int checkedId = -1;
+ for (int i = 0; i < numberOfTabs; i++) {
+ LinearLayout currentTab = (LinearLayout) mModuleButtonsContainer.getChildAt(i);
+ TabViewHolder currentTabViewHolder = (TabViewHolder) currentTab.getTag();
+ if (tabViewHolder == currentTab.getTag()) {
+ checkedId = i;
+ currentTabViewHolder.setChecked(true);
+ } else {
+ currentTabViewHolder.setChecked(false);
+ }
+ }
+
+ if (checkedId == -1)
+ return;
+
+ mSelectedOverviewItemIndex = checkedId;
+ if (mOverviewItemChangeListener != null) {
+ mOverviewItemChangeListener.onOverviewItemChanged(
+ overviewItems[mSelectedOverviewItemIndex]
+ );
+ }
+ updateUI();
+ }
+ };
+
+
+ private final View.OnClickListener onCheckboxClicked = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Is the view now checked?
+ mIsCheckboxChecked = ((CheckBox) view).isChecked();
+ updateUI();
+ }
+ };
+
+
+ @Override
+ protected boolean hasDataAvailable() {
+ return mVisitsData != null;
+ }
+ @Override
+ protected void saveStatsData(Bundle outState) {
+ if (hasDataAvailable()) {
+ outState.putSerializable(ARG_REST_RESPONSE, mVisitsData);
+ }
+ outState.putInt(ARG_SELECTED_GRAPH_BAR, mSelectedBarGraphBarIndex);
+ outState.putInt(ARG_PREV_NUMBER_OF_BARS, mPrevNumberOfBarsGraph);
+ outState.putInt(ARG_SELECTED_OVERVIEW_ITEM, mSelectedOverviewItemIndex);
+ outState.putBoolean(ARG_CHECKBOX_SELECTED, mVisitorsCheckbox.isChecked());
+ }
+ @Override
+ protected void restoreStatsData(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(ARG_REST_RESPONSE)) {
+ mVisitsData = (VisitsModel) savedInstanceState.getSerializable(ARG_REST_RESPONSE);
+ }
+ if (savedInstanceState.containsKey(ARG_SELECTED_OVERVIEW_ITEM)) {
+ mSelectedOverviewItemIndex = savedInstanceState.getInt(ARG_SELECTED_OVERVIEW_ITEM, 0);
+ }
+ if (savedInstanceState.containsKey(ARG_SELECTED_GRAPH_BAR)) {
+ mSelectedBarGraphBarIndex = savedInstanceState.getInt(ARG_SELECTED_GRAPH_BAR, -1);
+ }
+ if (savedInstanceState.containsKey(ARG_PREV_NUMBER_OF_BARS)) {
+ mPrevNumberOfBarsGraph = savedInstanceState.getInt(ARG_PREV_NUMBER_OF_BARS, -1);
+ }
+
+ mIsCheckboxChecked = savedInstanceState.getBoolean(ARG_CHECKBOX_SELECTED, true);
+ }
+ }
+
+ @Override
+ protected void showErrorUI(String label) {
+ setupNoResultsUI(false);
+ }
+
+ @Override
+ protected void showPlaceholderUI() {
+ setupNoResultsUI(true);
+ }
+
+ private VisitModel[] getDataToShowOnGraph(VisitsModel visitsData) {
+ List<VisitModel> visitModels = visitsData.getVisits();
+ int numPoints = Math.min(StatsUIHelper.getNumOfBarsToShow(), visitModels.size());
+ int currentPointIndex = numPoints - 1;
+ VisitModel[] visitModelsToShow = new VisitModel[numPoints];
+
+ for (int i = visitModels.size() -1; i >= 0 && currentPointIndex >= 0; i--) {
+ VisitModel currentVisitModel = visitModels.get(i);
+ visitModelsToShow[currentPointIndex] = currentVisitModel;
+ currentPointIndex--;
+ }
+ return visitModelsToShow;
+ }
+
+ protected void updateUI() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (mVisitsData == null) {
+ setupNoResultsUI(false);
+ return;
+ }
+
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph(mVisitsData);
+ if (dataToShowOnGraph == null || dataToShowOnGraph.length == 0) {
+ setupNoResultsUI(false);
+ return;
+ }
+
+ // Hide the "no-activity this period" message
+ mNoActivtyThisPeriodContainer.setVisibility(View.GONE);
+
+ // Read the selected Tab in the UI
+ OverviewLabel selectedStatsType = overviewItems[mSelectedOverviewItemIndex];
+
+ // Update the Legend and enable/disable the visitors checkboxes
+ mLegendContainer.setVisibility(View.VISIBLE);
+ mLegendLabel.setText(StringUtils.capitalize(selectedStatsType.getLabel().toLowerCase()));
+ switch(selectedStatsType) {
+ case VIEWS:
+ mVisitorsCheckboxContainer.setVisibility(View.VISIBLE);
+ mVisitorsCheckbox.setEnabled(true);
+ mVisitorsCheckbox.setChecked(mIsCheckboxChecked);
+ break;
+ default:
+ mVisitorsCheckboxContainer.setVisibility(View.GONE);
+ break;
+ }
+
+ // Setting Up labels and prepare variables that hold series
+ final String[] horLabels = new String[dataToShowOnGraph.length];
+ mStatsDate = new String[dataToShowOnGraph.length];
+ GraphView.GraphViewData[] mainSeriesItems = new GraphView.GraphViewData[dataToShowOnGraph.length];
+
+ GraphView.GraphViewData[] secondarySeriesItems = null;
+ if (mIsCheckboxChecked && selectedStatsType == OverviewLabel.VIEWS) {
+ secondarySeriesItems = new GraphView.GraphViewData[dataToShowOnGraph.length];
+ }
+
+ // index of days that should be XXX on the graph
+ final boolean[] weekendDays;
+ if (getTimeframe() == StatsTimeframe.DAY) {
+ weekendDays = new boolean[dataToShowOnGraph.length];
+ } else {
+ weekendDays = null;
+ }
+
+ // Check we have at least one result in the current section.
+ boolean atLeastOneResultIsAvailable = false;
+
+ // Fill series variables with data
+ for (int i = 0; i < dataToShowOnGraph.length; i++) {
+ int currentItemValue = 0;
+ switch(selectedStatsType) {
+ case VIEWS:
+ currentItemValue = dataToShowOnGraph[i].getViews();
+ break;
+ case VISITORS:
+ currentItemValue = dataToShowOnGraph[i].getVisitors();
+ break;
+ case LIKES:
+ currentItemValue = dataToShowOnGraph[i].getLikes();
+ break;
+ case COMMENTS:
+ currentItemValue = dataToShowOnGraph[i].getComments();
+ break;
+ }
+ mainSeriesItems[i] = new GraphView.GraphViewData(i, currentItemValue);
+
+ if (currentItemValue > 0) {
+ atLeastOneResultIsAvailable = true;
+ }
+
+ if (mIsCheckboxChecked && secondarySeriesItems != null) {
+ secondarySeriesItems[i] = new GraphView.GraphViewData(i, dataToShowOnGraph[i].getVisitors());
+ }
+
+ String currentItemStatsDate = dataToShowOnGraph[i].getPeriod();
+ horLabels[i] = getDateLabelForBarInGraph(currentItemStatsDate);
+ mStatsDate[i] = currentItemStatsDate;
+
+ if (weekendDays != null) {
+ SimpleDateFormat from = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ try {
+ Date date = from.parse(currentItemStatsDate);
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ c.setTimeInMillis(date.getTime());
+ weekendDays[i] = c.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY || c.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY;
+ } catch (ParseException e) {
+ weekendDays[i] = false;
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+ }
+
+ if (mGraphContainer.getChildCount() >= 1 && mGraphContainer.getChildAt(0) instanceof GraphView) {
+ mGraphView = (StatsBarGraph) mGraphContainer.getChildAt(0);
+ } else {
+ mGraphContainer.removeAllViews();
+ mGraphView = new StatsBarGraph(getActivity());
+ mGraphContainer.addView(mGraphView);
+ }
+
+ mGraphView.removeAllSeries();
+
+ GraphViewSeries mainSeriesOnScreen = new GraphViewSeries(mainSeriesItems);
+ mainSeriesOnScreen.getStyle().color = getResources().getColor(R.color.stats_bar_graph_main_series);
+ mainSeriesOnScreen.getStyle().outerColor = getResources().getColor(R.color.translucent_grey_lighten_30);
+ mainSeriesOnScreen.getStyle().highlightColor = getResources().getColor(R.color.stats_bar_graph_main_series_highlight);
+ mainSeriesOnScreen.getStyle().outerhighlightColor = getResources().getColor(R.color.stats_bar_graph_outer_highlight);
+ mainSeriesOnScreen.getStyle().padding = DisplayUtils.dpToPx(getActivity(), 5);
+ mGraphView.addSeries(mainSeriesOnScreen);
+
+ // Add the Visitors series if it's checked in the legend
+ if (mIsCheckboxChecked && secondarySeriesItems != null && selectedStatsType == OverviewLabel.VIEWS) {
+ GraphViewSeries secondarySeries = new GraphViewSeries(secondarySeriesItems);
+ secondarySeries.getStyle().padding = DisplayUtils.dpToPx(getActivity(), 10);
+ secondarySeries.getStyle().color = getResources().getColor(R.color.stats_bar_graph_secondary_series);
+ secondarySeries.getStyle().highlightColor = getResources().getColor(R.color.orange_fire);
+ mGraphView.addSeries(secondarySeries);
+ }
+
+ // Setup the Y-axis on Visitors and Views Tabs.
+ // Views and Visitors tabs have the exact same Y-axis as shifting from one Y-axis to another defeats
+ // the purpose of making these bars visually easily to compare.
+ switch(selectedStatsType) {
+ case VISITORS:
+ double maxYValue = getMaxYValueForVisitorsAndView(dataToShowOnGraph);
+ mGraphView.setManualYAxisBounds(maxYValue, 0d);
+ break;
+ default:
+ mGraphView.setManualYAxis(false);
+ break;
+ }
+
+ // Set the Graph Style
+ mGraphView.getGraphViewStyle().setNumHorizontalLabels(dataToShowOnGraph.length);
+ // Set the maximum size a column can get on the screen in PX
+ mGraphView.getGraphViewStyle().setMaxColumnWidth(
+ DisplayUtils.dpToPx(getActivity(), StatsConstants.STATS_GRAPH_BAR_MAX_COLUMN_WIDTH_DP)
+ );
+ mGraphView.setHorizontalLabels(horLabels);
+ mGraphView.setGestureListener(this);
+
+ // If zero results in the current section disable clicks on the graph and show the dialog.
+ mNoActivtyThisPeriodContainer.setVisibility(atLeastOneResultIsAvailable ? View.GONE : View.VISIBLE);
+ mGraphView.setClickable(atLeastOneResultIsAvailable);
+
+ // Draw the background on weekend days
+ mGraphView.setWeekendDays(weekendDays);
+
+ // Reset the bar selected upon rotation of the device when the no. of bars can change with orientation.
+ // Only happens on 720DP tablets
+ if (mPrevNumberOfBarsGraph != -1 && mPrevNumberOfBarsGraph != dataToShowOnGraph.length) {
+ mSelectedBarGraphBarIndex = -1;
+ mPrevNumberOfBarsGraph = dataToShowOnGraph.length;
+ onBarTapped(dataToShowOnGraph.length - 1);
+ mGraphView.highlightBar(dataToShowOnGraph.length - 1);
+ return;
+ }
+
+ mPrevNumberOfBarsGraph = dataToShowOnGraph.length;
+ int barSelectedOnGraph;
+ if (mSelectedBarGraphBarIndex == -1) {
+ // No previous bar was highlighted, highlight the most recent one
+ barSelectedOnGraph = dataToShowOnGraph.length - 1;
+ } else if (mSelectedBarGraphBarIndex < dataToShowOnGraph.length) {
+ barSelectedOnGraph = mSelectedBarGraphBarIndex;
+ } else {
+ // A previous bar was highlighted, but it's out of the screen now. This should never happen atm.
+ barSelectedOnGraph = dataToShowOnGraph.length - 1;
+ mSelectedBarGraphBarIndex = barSelectedOnGraph;
+ }
+
+ updateUIBelowTheGraph(barSelectedOnGraph);
+ mGraphView.highlightBar(barSelectedOnGraph);
+ }
+
+ // Find the max value in Visitors and Views data.
+ // Only checks the Views data, since Visitors is for sure less-equals than Views.
+ private double getMaxYValueForVisitorsAndView(final VisitModel[] dataToShowOnGraph) {
+ if (dataToShowOnGraph == null || dataToShowOnGraph.length == 0) {
+ return 0d;
+ }
+ double largest = Integer.MIN_VALUE;
+
+ for (VisitModel aDataToShowOnGraph : dataToShowOnGraph) {
+ int currentItemValue = aDataToShowOnGraph.getViews();
+ if (currentItemValue > largest) {
+ largest = currentItemValue;
+ }
+ }
+ return largest;
+ }
+
+ //update the area right below the graph
+ private void updateUIBelowTheGraph(int itemPosition) {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (mVisitsData == null) {
+ setupNoResultsUI(false);
+ return;
+ }
+
+ final VisitModel[] dataToShowOnGraph = getDataToShowOnGraph(mVisitsData);
+
+ // Make sure we've data to show on the screen
+ if (dataToShowOnGraph.length == 0) {
+ return;
+ }
+
+ // This check should never be true, since we put a check on the index in the calling function updateUI()
+ if (dataToShowOnGraph.length <= itemPosition || itemPosition == -1) {
+ // Make sure we're not highlighting
+ itemPosition = dataToShowOnGraph.length -1;
+ }
+
+ String date = mStatsDate[itemPosition];
+ if (date == null) {
+ AppLog.w(AppLog.T.STATS, "Cannot update the area below the graph if a null date is passed!!");
+ return;
+ }
+
+ mDateTextView.setText(getDateForDisplayInLabels(date, getTimeframe()));
+
+ VisitModel modelTapped = dataToShowOnGraph[itemPosition];
+ for (int i=0 ; i < mModuleButtonsContainer.getChildCount(); i++) {
+ View o = mModuleButtonsContainer.getChildAt(i);
+ if (o instanceof LinearLayout && o.getTag() instanceof TabViewHolder) {
+ TabViewHolder tabViewHolder = (TabViewHolder)o.getTag();
+ int currentValue = 0;
+ switch (tabViewHolder.labelItem) {
+ case VIEWS:
+ currentValue = modelTapped.getViews();
+ break;
+ case VISITORS:
+ currentValue = modelTapped.getVisitors();
+ break;
+ case LIKES:
+ currentValue = modelTapped.getLikes();
+ break;
+ case COMMENTS:
+ currentValue = modelTapped.getComments();
+ break;
+ }
+ tabViewHolder.value.setText(FormatUtils.formatDecimal(currentValue));
+ tabViewHolder.updateBackGroundAndIcon(currentValue);
+ }
+ }
+ }
+
+ private String getDateForDisplayInLabels(String date, StatsTimeframe timeframe) {
+ String prefix = getString(R.string.stats_for);
+ switch (timeframe) {
+ case DAY:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_SHORT_FORMAT));
+ case WEEK:
+ try {
+ SimpleDateFormat sdf;
+ Calendar c;
+ final Date parsedDate;
+ // Used in bar graph
+ // first four digits are the year
+ // followed by Wxx where xx is the month
+ // followed by Wxx where xx is the day of the month
+ // ex: 2013W07W22 = July 22, 2013
+ sdf = new SimpleDateFormat("yyyy'W'MM'W'dd");
+ //Calculate the end of the week
+ parsedDate = sdf.parse(date);
+ c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ c.setTime(parsedDate);
+ // first day of this week
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY );
+ String startDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ // last day of this week
+ c.add(Calendar.DAY_OF_WEEK, + 6);
+ String endDateLabel = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_DAY_LONG_FORMAT);
+ return String.format(prefix, startDateLabel + " - " + endDateLabel);
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return "";
+ }
+ case MONTH:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_MONTH_LONG_FORMAT));
+ case YEAR:
+ return String.format(prefix, StatsUtils.parseDate(date, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_YEAR_FORMAT));
+ }
+ return "";
+ }
+
+ /**
+ * Return the date string that is displayed under each bar in the graph
+ */
+ private String getDateLabelForBarInGraph(String dateToFormat) {
+ switch (getTimeframe()) {
+ case DAY:
+ return StatsUtils.parseDate(
+ dateToFormat,
+ StatsConstants.STATS_INPUT_DATE_FORMAT,
+ StatsConstants.STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT
+ );
+ case WEEK:
+ // first four digits are the year
+ // followed by Wxx where xx is the month
+ // followed by Wxx where xx is the day of the month
+ // ex: 2013W07W22 = July 22, 2013
+ return StatsUtils.parseDate(dateToFormat, "yyyy'W'MM'W'dd", StatsConstants.STATS_OUTPUT_DATE_MONTH_SHORT_DAY_SHORT_FORMAT);
+ case MONTH:
+ return StatsUtils.parseDate(dateToFormat, "yyyy-MM", "MMM");
+ case YEAR:
+ return StatsUtils.parseDate(dateToFormat, StatsConstants.STATS_INPUT_DATE_FORMAT, StatsConstants.STATS_OUTPUT_DATE_YEAR_FORMAT);
+ default:
+ return dateToFormat;
+ }
+ }
+
+ private void setupNoResultsUI(boolean isLoading) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Hide the legend
+ mLegendContainer.setVisibility(View.GONE);
+ mVisitorsCheckboxContainer.setVisibility(View.GONE);
+
+ mSelectedBarGraphBarIndex = -1;
+ Context context = mGraphContainer.getContext();
+ if (context != null) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View emptyBarGraphView = inflater.inflate(R.layout.stats_bar_graph_empty, mGraphContainer, false);
+
+ final TextView emptyLabel = (TextView) emptyBarGraphView.findViewById(R.id.stats_bar_graph_empty_label);
+ emptyLabel.setText("");
+ if (!isLoading) {
+ mNoActivtyThisPeriodContainer.setVisibility(View.VISIBLE);
+ }
+
+ if (emptyBarGraphView != null) {
+ mGraphContainer.removeAllViews();
+ mGraphContainer.addView(emptyBarGraphView);
+ }
+ }
+ mDateTextView.setText("");
+
+ for (int i=0 ; i < mModuleButtonsContainer.getChildCount(); i++) {
+ View o = mModuleButtonsContainer.getChildAt(i);
+ if (o instanceof CheckedTextView) {
+ CheckedTextView currentBtm = (CheckedTextView)o;
+ OverviewLabel overviewItem = (OverviewLabel)currentBtm.getTag();
+ String labelPrefix = overviewItem.getLabel() + "\n 0" ;
+ currentBtm.setText(labelPrefix);
+ }
+ }
+ }
+
+ @Override
+ protected String getTitle() {
+ return getString(R.string.stats_view_visitors_and_views);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.VisitorsAndViewsUpdated event) {
+ if (!shouldUpdateFragmentOnUpdateEvent(event)) {
+ return;
+ }
+
+ mVisitsData = event.mVisitsAndViews;
+ mSelectedBarGraphBarIndex = -1;
+
+ // Reset the bar to highlight
+ if (mGraphView != null) {
+ mGraphView.resetHighlightBar();
+ }
+
+ updateUI();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(StatsEvents.SectionUpdateError event) {
+ if (!shouldUpdateFragmentOnErrorEvent(event)) {
+ return;
+ }
+
+ mVisitsData = null;
+ mSelectedBarGraphBarIndex = -1;
+
+ // Reset the bar to highlight
+ if (mGraphView != null) {
+ mGraphView.resetHighlightBar();
+ }
+
+ updateUI();
+ }
+
+ @Override
+ public void onBarTapped(int tappedBar) {
+ if (!isAdded()) {
+ return;
+ }
+ //AppLog.d(AppLog.T.STATS, " Tapped bar date " + mStatsDate[tappedBar]);
+ mSelectedBarGraphBarIndex = tappedBar;
+ updateUIBelowTheGraph(tappedBar);
+
+ if (!NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ // Update Stats here
+ String date = mStatsDate[tappedBar];
+ if (date == null) {
+ AppLog.w(AppLog.T.STATS, "A bar was tapped but a null date is received!!");
+ return;
+ }
+
+ //Calculate the correct end date for the selected period
+ String calculatedDate = null;
+
+ try {
+ SimpleDateFormat sdf;
+ Calendar c = Calendar.getInstance();
+ c.setFirstDayOfWeek(Calendar.MONDAY);
+ final Date parsedDate;
+ switch (getTimeframe()) {
+ case DAY:
+ calculatedDate = date;
+ break;
+ case WEEK:
+ // first four digits are the year
+ // followed by Wxx where xx is the month
+ // followed by Wxx where xx is the day of the month
+ // ex: 2013W07W22 = July 22, 2013
+ sdf = new SimpleDateFormat("yyyy'W'MM'W'dd");
+ //Calculate the end of the week
+ parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+ // first day of this week
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ // last day of this week
+ c.add(Calendar.DAY_OF_WEEK, +6);
+ calculatedDate = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case MONTH:
+ sdf = new SimpleDateFormat("yyyy-MM");
+ //Calculate the end of the month
+ parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+ // last day of this month
+ c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH));
+ calculatedDate = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ case YEAR:
+ sdf = new SimpleDateFormat(StatsConstants.STATS_INPUT_DATE_FORMAT);
+ //Calculate the end of the week
+ parsedDate = sdf.parse(date);
+ c.setTime(parsedDate);
+ c.set(Calendar.MONTH, Calendar.DECEMBER);
+ c.set(Calendar.DAY_OF_MONTH, 31);
+ calculatedDate = StatsUtils.msToString(c.getTimeInMillis(), StatsConstants.STATS_INPUT_DATE_FORMAT);
+ break;
+ }
+ } catch (ParseException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ if (calculatedDate == null) {
+ AppLog.w(AppLog.T.STATS, "A call to request new stats stats is made but date received cannot be parsed!! " + date);
+ return;
+ }
+
+ // Update the data below the graph
+ if (mListener!= null) {
+ // Should never be null
+ final Blog currentBlog = WordPress.getBlog(getLocalTableBlogID());
+ if (currentBlog != null && currentBlog.getDotComBlogId() != null) {
+ mListener.onDateChanged(currentBlog.getDotComBlogId(), getTimeframe(), calculatedDate);
+ }
+ }
+
+ AnalyticsUtils.trackWithBlogDetails(
+ AnalyticsTracker.Stat.STATS_TAPPED_BAR_CHART,
+ WordPress.getBlog(getLocalTableBlogID())
+ );
+ }
+
+ public enum OverviewLabel {
+ VIEWS(R.string.stats_views),
+ VISITORS(R.string.stats_visitors),
+ LIKES(R.string.stats_likes),
+ COMMENTS(R.string.stats_comments),
+ ;
+
+ private final int mLabelResId;
+
+ OverviewLabel(int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId).toUpperCase();
+ }
+ }
+
+ @Override
+ protected StatsService.StatsEndpointsEnum[] sectionsToUpdate() {
+ return new StatsService.StatsEndpointsEnum[]{
+ StatsService.StatsEndpointsEnum.VISITS
+ };
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java
new file mode 100644
index 000000000..30f271981
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWPLinkMovementMethod.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.ui.stats;
+
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.style.URLSpan;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPLinkMovementMethod;
+
+class StatsWPLinkMovementMethod extends WPLinkMovementMethod {
+ public static WPLinkMovementMethod getInstance() {
+ if (mMovementMethod == null) {
+ mMovementMethod = new StatsWPLinkMovementMethod();
+ }
+ return mMovementMethod;
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ if (layout == null) {
+ return super.onTouchEvent(widget, buffer, event);
+ }
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
+ if (link.length != 0) {
+ String url = link[0].getURL();
+ AppLog.d(AppLog.T.UTILS, "Tapped on the Link: " + url);
+ if (url.startsWith("https://wordpress.com/my-stats")
+ || url.startsWith("http://wordpress.com/my-stats")) {
+ // make sure to load the no-chrome version of Stats over https
+ url = UrlUtils.makeHttps(url);
+ if (url.contains("?")) {
+ // add the no chrome parameters if not available
+ if (!url.contains("?no-chrome") && !url.contains("&no-chrome")) {
+ url += "&no-chrome";
+ }
+ } else {
+ url += "?no-chrome";
+ }
+ AppLog.d(AppLog.T.UTILS, "Opening the Authenticated in-app browser : " + url);
+ // Let's try the global wpcom credentials
+ String statsAuthenticatedUser = AccountHelper.getDefaultAccount().getUserName();
+ if (org.apache.commons.lang.StringUtils.isEmpty(statsAuthenticatedUser)) {
+ // Still empty. Do not eat the event, but let's open the default Web Browser.
+ return super.onTouchEvent(widget, buffer, event);
+ }
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(widget.getContext(),
+ url, statsAuthenticatedUser);
+ return true;
+ } else if (url.startsWith("https") || url.startsWith("http")) {
+ AppLog.d(AppLog.T.UTILS, "Opening the in-app browser: " + url);
+ WPWebViewActivity.openURL(widget.getContext(), url);
+ return true;
+ }
+ }
+ }
+ return super.onTouchEvent(widget, buffer, event);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java
new file mode 100644
index 000000000..cdef4f32a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureActivity.java
@@ -0,0 +1,161 @@
+package org.wordpress.android.ui.stats;
+
+/**
+ * The configuration screen for the StatsWidgetProvider widget.
+ */
+
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.List;
+import java.util.Map;
+
+public class StatsWidgetConfigureActivity extends AppCompatActivity
+ implements StatsWidgetConfigureAdapter.OnSiteClickListener {
+
+ private StatsWidgetConfigureAdapter mAdapter;
+ private RecyclerView mRecycleView;
+ private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Find the widget id from the intent.
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ mAppWidgetId = extras.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
+ }
+
+ // Set the result to CANCELED. This will cause the widget host to cancel out of the widget
+ // placement if they press the back button.
+ setResult(RESULT_CANCELED, new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
+
+ // Intent without the widget id, just bail.
+ if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish();
+ return;
+ }
+
+ // If not signed into WordPress inform the user
+ if (!AccountHelper.isSignedIn()) {
+ ToastUtils.showToast(getBaseContext(), R.string.stats_widget_error_no_account, ToastUtils.Duration.LONG);
+ finish();
+ return;
+ }
+
+ // If no visible blogs
+ List<Map<String, Object>> accounts = WordPress.wpDB.getBlogsBy("isHidden = 0", null);
+ if (accounts.size() == 0) {
+ ToastUtils.showToast(getBaseContext(), R.string.stats_widget_error_no_visible_blog, ToastUtils.Duration.LONG);
+ finish();
+ return;
+ }
+
+ // If one blog only, skip config
+ if (accounts.size() == 1) {
+ Map<String, Object> account = accounts.get(0);
+ Integer localID = (Integer) account.get("id");
+ addWidgetToScreenAndFinish(localID);
+ return;
+ }
+
+ setContentView(R.layout.stats_widget_config_activity);
+ setNewAdapter();
+ setupActionBar();
+ setupRecycleView();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ private void setupRecycleView() {
+ mRecycleView = (RecyclerView) findViewById(R.id.recycler_view);
+ mRecycleView.setLayoutManager(new LinearLayoutManager(this));
+ mRecycleView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+ mRecycleView.setItemAnimator(null);
+ mRecycleView.setAdapter(getAdapter());
+ }
+
+ private void setupActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ actionBar.setHomeButtonEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ private StatsWidgetConfigureAdapter getAdapter() {
+ if (mAdapter == null) {
+ setNewAdapter();
+ }
+ return mAdapter;
+ }
+
+ private void setNewAdapter() {
+ Blog blog = WordPress.getCurrentBlog();
+ int localBlogId = (blog != null ? blog.getLocalTableBlogId() : 0);
+ mAdapter = new StatsWidgetConfigureAdapter(this, localBlogId);
+ mAdapter.setOnSiteClickListener(this);
+ }
+
+ @Override
+ public void onSiteClick(StatsWidgetConfigureAdapter.SiteRecord site) {
+ addWidgetToScreenAndFinish(site.localId);
+ }
+
+ private void addWidgetToScreenAndFinish(int localID) {
+ final Blog currentBlog = WordPress.getBlog(localID);
+
+ if (currentBlog == null) {
+ AppLog.e(AppLog.T.STATS, "The blog with local_blog_id " + localID + " cannot be loaded from the DB.");
+ Toast.makeText(this, R.string.stats_no_blog, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ if (currentBlog.getDotComBlogId() == null) {
+ // The blog could be a self-hosted blog with NO Jetpack installed on it
+ // Or a Jetpack blog whose options are not yet synched in the app
+ // In both of these cases show a generic message that encourages the user to refresh
+ // the blog within the app. There are so many different paths here that's better to handle them in the app.
+ Toast.makeText(this, R.string.stats_widget_error_jetpack_no_blogid, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ final Context context = StatsWidgetConfigureActivity.this;
+ StatsWidgetProvider.setupNewWidget(context, mAppWidgetId, localID);
+ // Make sure we pass back the original appWidgetId
+ Intent resultValue = new Intent();
+ resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+ setResult(RESULT_OK, resultValue);
+ finish();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java
new file mode 100644
index 000000000..cb18d7393
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetConfigureAdapter.java
@@ -0,0 +1,300 @@
+package org.wordpress.android.ui.stats;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class StatsWidgetConfigureAdapter extends RecyclerView.Adapter<StatsWidgetConfigureAdapter.SiteViewHolder> {
+
+ interface OnSiteClickListener {
+ void onSiteClick(SiteRecord site);
+ }
+
+ private final int mTextColorNormal;
+ private final int mTextColorHidden;
+
+ private static int mBlavatarSz;
+
+ private SiteList mSites = new SiteList();
+ private final int mCurrentLocalId;
+
+ private final Drawable mSelectedItemBackground;
+
+ private final LayoutInflater mInflater;
+
+ private boolean mShowHiddenSites = false;
+ private boolean mShowSelfHostedSites = true;
+
+ private OnSiteClickListener mSiteSelectedListener;
+
+ class SiteViewHolder extends RecyclerView.ViewHolder {
+ private final ViewGroup layoutContainer;
+ private final TextView txtTitle;
+ private final TextView txtDomain;
+ private final WPNetworkImageView imgBlavatar;
+ private final View divider;
+ private Boolean isSiteHidden;
+
+ public SiteViewHolder(View view) {
+ super(view);
+ layoutContainer = (ViewGroup) view.findViewById(R.id.layout_container);
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtDomain = (TextView) view.findViewById(R.id.text_domain);
+ imgBlavatar = (WPNetworkImageView) view.findViewById(R.id.image_blavatar);
+ divider = view.findViewById(R.id.divider);
+ isSiteHidden = null;
+
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mSiteSelectedListener != null) {
+ int clickedPosition = getAdapterPosition();
+ mSiteSelectedListener.onSiteClick(getItem(clickedPosition));
+ }
+ }
+ });
+ }
+ }
+
+ public StatsWidgetConfigureAdapter(Context context, int currentLocalBlogId) {
+ super();
+
+ setHasStableIds(true);
+
+ mCurrentLocalId = currentLocalBlogId;
+ mInflater = LayoutInflater.from(context);
+
+ mBlavatarSz = context.getResources().getDimensionPixelSize(R.dimen.blavatar_sz);
+ mTextColorNormal = context.getResources().getColor(R.color.grey_dark);
+ mTextColorHidden = context.getResources().getColor(R.color.grey);
+
+ mSelectedItemBackground = new ColorDrawable(context.getResources().getColor(R.color.translucent_grey_lighten_20));
+
+ loadSites();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSites.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).localId;
+ }
+
+ private SiteRecord getItem(int position) {
+ return mSites.get(position);
+ }
+
+ public void setOnSiteClickListener(OnSiteClickListener listener) {
+ mSiteSelectedListener = listener;
+ }
+
+ @Override
+ public SiteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = mInflater.inflate(R.layout.site_picker_listitem, parent, false);
+ return new SiteViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(SiteViewHolder holder, int position) {
+ SiteRecord site = getItem(position);
+
+ holder.txtTitle.setText(site.getBlogNameOrHomeURL());
+ holder.txtDomain.setText(site.homeURL);
+ holder.imgBlavatar.setImageUrl(site.blavatarUrl, WPNetworkImageView.ImageType.BLAVATAR);
+
+ if (site.localId == mCurrentLocalId) {
+ holder.layoutContainer.setBackgroundDrawable(mSelectedItemBackground);
+ } else {
+ holder.layoutContainer.setBackgroundDrawable(null);
+ }
+
+ // different styling for visible/hidden sites
+ if (holder.isSiteHidden == null || holder.isSiteHidden != site.isHidden) {
+ holder.isSiteHidden = site.isHidden;
+ holder.txtTitle.setTextColor(site.isHidden ? mTextColorHidden : mTextColorNormal);
+ holder.txtTitle.setTypeface(holder.txtTitle.getTypeface(), site.isHidden ? Typeface.NORMAL : Typeface.BOLD);
+ holder.imgBlavatar.setAlpha(site.isHidden ? 0.5f : 1f);
+ }
+
+ // hide the divider for the last item
+ boolean isLastItem = (position == getItemCount() - 1);
+ holder.divider.setVisibility(isLastItem ? View.INVISIBLE : View.VISIBLE);
+ }
+
+
+ private void loadSites() {
+ if (mIsTaskRunning) {
+ AppLog.w(AppLog.T.UTILS, "site picker > already loading sites");
+ } else {
+ new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ /*
+ * AsyncTask which loads sites from database and populates the adapter
+ */
+ private boolean mIsTaskRunning;
+ private class LoadSitesTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ List<Map<String, Object>> blogs;
+ String[] extraFields = {"isHidden", "dotcomFlag", "homeURL"};
+
+ blogs = getBlogsForCurrentView(extraFields);
+ SiteList sites = new SiteList(blogs);
+
+ // sort by blog/host
+ final long primaryBlogId = AccountHelper.getDefaultAccount().getPrimaryBlogId();
+ Collections.sort(sites, new Comparator<SiteRecord>() {
+ public int compare(SiteRecord site1, SiteRecord site2) {
+ if (primaryBlogId > 0) {
+ if (site1.blogId == primaryBlogId) {
+ return -1;
+ } else if (site2.blogId == primaryBlogId) {
+ return 1;
+ }
+ }
+ return site1.getBlogNameOrHomeURL().compareToIgnoreCase(site2.getBlogNameOrHomeURL());
+ }
+ });
+
+ if (mSites == null || !mSites.isSameList(sites)) {
+ mSites = sites;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void results) {
+ notifyDataSetChanged();
+ mIsTaskRunning = false;
+ }
+
+ private List<Map<String, Object>> getBlogsForCurrentView(String[] extraFields) {
+ if (mShowHiddenSites) {
+ if (mShowSelfHostedSites) {
+ // all self-hosted blogs and all wp.com blogs
+ return WordPress.wpDB.getBlogsBy(null, extraFields);
+ } else {
+ // only wp.com blogs
+ return WordPress.wpDB.getBlogsBy("dotcomFlag=1", extraFields);
+ }
+ } else {
+ if (mShowSelfHostedSites) {
+ // all self-hosted blogs plus visible wp.com blogs
+ return WordPress.wpDB.getBlogsBy("dotcomFlag=0 OR (isHidden=0 AND dotcomFlag=1) ", extraFields);
+ } else {
+ // only visible wp.com blogs
+ return WordPress.wpDB.getBlogsBy("isHidden=0 AND dotcomFlag=1", extraFields);
+ }
+ }
+ }
+ }
+
+ /**
+ * SiteRecord is a simplified version of the full account (blog) record
+ */
+ static class SiteRecord {
+ final int localId;
+ final int blogId;
+ final String blogName;
+ final String homeURL;
+ final String url;
+ final String blavatarUrl;
+ final boolean isDotCom;
+ final boolean isHidden;
+
+ SiteRecord(Map<String, Object> account) {
+ localId = MapUtils.getMapInt(account, "id");
+ blogId = MapUtils.getMapInt(account, "blogId");
+ blogName = BlogUtils.getBlogNameOrHomeURLFromAccountMap(account);
+ homeURL = BlogUtils.getHomeURLOrHostNameFromAccountMap(account);
+ url = MapUtils.getMapStr(account, "url");
+ blavatarUrl = GravatarUtils.blavatarFromUrl(url, mBlavatarSz);
+ isDotCom = MapUtils.getMapBool(account, "dotcomFlag");
+ isHidden = MapUtils.getMapBool(account, "isHidden");
+ }
+
+ String getBlogNameOrHomeURL() {
+ if (TextUtils.isEmpty(blogName)) {
+ return homeURL;
+ }
+ return blogName;
+ }
+ }
+
+ static class SiteList extends ArrayList<SiteRecord> {
+ SiteList() { }
+ SiteList(List<Map<String, Object>> accounts) {
+ if (accounts != null) {
+ for (Map<String, Object> account : accounts) {
+ add(new SiteRecord(account));
+ }
+ }
+ }
+
+ boolean isSameList(SiteList sites) {
+ if (sites == null || sites.size() != this.size()) {
+ return false;
+ }
+ int i;
+ for (SiteRecord site: sites) {
+ i = indexOfSite(site);
+ if (i == -1 || this.get(i).isHidden != site.isHidden) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ int indexOfSite(SiteRecord site) {
+ if (site != null && site.blogId > 0) {
+ for (int i = 0; i < size(); i++) {
+ if (site.blogId == this.get(i).blogId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java
new file mode 100644
index 000000000..41efa62fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsWidgetProvider.java
@@ -0,0 +1,541 @@
+package org.wordpress.android.ui.stats;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.util.SparseArray;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.android.volley.VolleyError;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.main.WPMainActivity;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.stats.exceptions.StatsError;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.service.StatsService;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+
+import java.util.ArrayList;
+
+public class StatsWidgetProvider extends AppWidgetProvider {
+
+ private static void showMessage(Context context, int[] allWidgets, String message){
+ if (allWidgets.length == 0){
+ return;
+ }
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+
+ for (int widgetId : allWidgets) {
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.stats_widget_layout);
+ int remoteBlogID = getRemoteBlogIDFromWidgetID(widgetId);
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ String name;
+ if (blog != null) {
+ name = context.getString(R.string.stats_widget_name_for_blog);
+ name = String.format(name, StringEscapeUtils.unescapeHtml(blog.getNameOrHostUrl()));
+ } else {
+ name = context.getString(R.string.stats_widget_name);
+ }
+ remoteViews.setTextViewText(R.id.blog_title, name);
+
+ remoteViews.setViewVisibility(R.id.stats_widget_error_container, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.stats_widget_values_container, View.GONE);
+ remoteViews.setTextViewText(R.id.stats_widget_error_text, message);
+
+ Intent intent = new Intent(context, WPMainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.setAction("android.intent.action.MAIN");
+ intent.addCategory("android.intent.category.LAUNCHER");
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ remoteViews.setOnClickPendingIntent(R.id.stats_widget_outer_container, pendingIntent);
+
+ appWidgetManager.updateAppWidget(widgetId, remoteViews);
+ }
+ }
+
+ private static void updateTabValue(Context context, RemoteViews remoteViews, int viewId, String text) {
+ remoteViews.setTextViewText(viewId, text);
+ if (text.equals("0")) {
+ remoteViews.setTextColor(viewId, context.getResources().getColor(R.color.grey));
+ }
+ }
+
+ private static void showStatsData(Context context, int[] allWidgets, Blog blog, JSONObject data) {
+ if (allWidgets.length == 0){
+ return;
+ }
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+
+ String name = context.getString(R.string.stats_widget_name_for_blog);
+ name = String.format(name, StringEscapeUtils.unescapeHtml(blog.getNameOrHostUrl()));
+
+ for (int widgetId : allWidgets) {
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.stats_widget_layout);
+ remoteViews.setTextViewText(R.id.blog_title, name);
+
+ remoteViews.setViewVisibility(R.id.stats_widget_error_container, View.GONE);
+ remoteViews.setViewVisibility(R.id.stats_widget_values_container, View.VISIBLE);
+
+ // Update Views
+ updateTabValue(context, remoteViews, R.id.stats_widget_views, data.optString("views", " 0"));
+
+ // Update Visitors
+ updateTabValue(context, remoteViews, R.id.stats_widget_visitors, data.optString("visitors", " 0"));
+
+ // Update Comments
+ updateTabValue(context, remoteViews, R.id.stats_widget_comments, data.optString("comments", " 0"));
+
+ // Update Likes
+ updateTabValue(context, remoteViews, R.id.stats_widget_likes, data.optString("likes", " 0"));
+
+ Intent intent = new Intent(context, StatsActivity.class);
+ intent.putExtra(StatsActivity.ARG_LOCAL_TABLE_BLOG_ID, blog.getLocalTableBlogId());
+ intent.putExtra(StatsActivity.ARG_LAUNCHED_FROM, StatsActivity.StatsLaunchedFrom.STATS_WIDGET);
+ intent.putExtra(StatsActivity.ARG_DESIRED_TIMEFRAME, StatsTimeframe.DAY);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, blog.getLocalTableBlogId(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ remoteViews.setOnClickPendingIntent(R.id.stats_widget_outer_container, pendingIntent);
+ appWidgetManager.updateAppWidget(widgetId, remoteViews);
+ }
+ }
+
+ private static void ShowCacheIfAvailableOrGenericError(Context context, int remoteBlogID) {
+ int[] widgetIDs = getWidgetIDsFromRemoteBlogID(remoteBlogID);
+ if (widgetIDs.length == 0){
+ return;
+ }
+
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (blog == null) {
+ AppLog.e(AppLog.T.STATS, "No blog found in the db!");
+ return;
+ }
+
+ String currentDate = StatsUtils.getCurrentDateTZ(localId);
+
+ // Show cached data if available
+ JSONObject cache = getCacheDataForBlog(remoteBlogID, currentDate);
+ if (cache != null) {
+ showStatsData(context, widgetIDs, blog, cache);
+ } else {
+ showMessage(context, widgetIDs, context.getString(R.string.stats_widget_error_generic));
+ }
+ }
+
+ public static void updateWidgetsOnLogout(Context context) {
+ refreshAllWidgets(context);
+ }
+
+ public static void updateWidgetsOnLogin(Context context) {
+ refreshAllWidgets(context);
+ }
+
+ // This is called by the Stats service in case of error
+ public static void updateWidgets(Context context, int remoteBlogID, VolleyError error) {
+ if (error == null) {
+ AppLog.e(AppLog.T.STATS, "Widget received a VolleyError that is null!");
+ return;
+ }
+
+ // If it's an auth error, show it in the widget UI
+ if (error instanceof com.android.volley.AuthFailureError) {
+ int[] widgetIDs = getWidgetIDsFromRemoteBlogID(remoteBlogID);
+ if (widgetIDs.length == 0){
+ return;
+ }
+
+ // Check if Jetpack or .com
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (blog == null) {
+ return;
+ }
+
+ if (blog.isDotcomFlag()) {
+ // User cannot access stats for this .com blog
+ showMessage(context, widgetIDs, context.getString(R.string.stats_widget_error_no_permissions));
+ } else {
+ // Not logged into wpcom, or the main .com account of the app is not linked with this blog
+ showMessage(context, widgetIDs, context.getString(R.string.stats_sign_in_jetpack_different_com_account));
+ }
+ return;
+ }
+
+ ShowCacheIfAvailableOrGenericError(context, remoteBlogID);
+ }
+
+ // This is called by the Stats service in case of error
+ public static void updateWidgets(Context context, int remoteBlogID, StatsError error) {
+ if (error == null) {
+ AppLog.e(AppLog.T.STATS, "Widget received a StatsError that is null!");
+ return;
+ }
+
+ ShowCacheIfAvailableOrGenericError(context, remoteBlogID);
+ }
+
+ // This is called by the Stats service to keep widgets updated
+ public static void updateWidgets(Context context, int remoteBlogID, VisitModel data) {
+ AppLog.d(AppLog.T.STATS, "updateWidgets called for the blogID " + remoteBlogID);
+
+ int[] widgetIDs = getWidgetIDsFromRemoteBlogID(remoteBlogID);
+ if (widgetIDs.length == 0){
+ return;
+ }
+
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (blog == null) {
+ AppLog.e(AppLog.T.STATS, "No blog found in the db!");
+ return;
+ }
+
+ try {
+ String currentDate = StatsUtils.getCurrentDateTZ(blog.getLocalTableBlogId());
+ JSONObject newData = new JSONObject();
+ newData.put("blog_id", data.getBlogID());
+ newData.put("date", currentDate);
+ newData.put("views", data.getViews());
+ newData.put("visitors", data.getVisitors());
+ newData.put("comments", data.getComments());
+ newData.put("likes", data.getLikes());
+
+ // Store new data in cache
+ String prevDataAsString = AppPrefs.getStatsWidgetsData();
+ JSONObject prevData = null;
+ if (!StringUtils.isEmpty(prevDataAsString)) {
+ try {
+ prevData = new JSONObject(prevDataAsString);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+ try {
+ if (prevData == null) {
+ prevData = new JSONObject();
+ }
+ prevData.put(data.getBlogID(), newData);
+ AppPrefs.setStatsWidgetsData(prevData.toString());
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+
+ // Show data on the screen now!
+ showStatsData(context, widgetIDs, blog, newData);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ // This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute in the AppWidgetProviderInfo.
+ // Also called at booting time!
+ // This method is NOT called when the user adds the App Widget.
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ AppLog.d(AppLog.T.STATS, "onUpdate called");
+ refreshWidgets(context, appWidgetIds);
+ }
+
+ /**
+ * This is called when an instance the App Widget is created for the first time.
+ * For example, if the user adds two instances of your App Widget, this is only called the first time.
+ */
+ @Override
+ public void onEnabled(Context context) {
+ AppLog.d(AppLog.T.STATS, "onEnabled called");
+ // Note: don't erase prefs here, since for some reasons this method is called after the booting of the device.
+ }
+
+ /**
+ * This is called when the last instance of your App Widget is deleted from the App Widget host.
+ * This is where you should clean up any work done in onEnabled(Context), such as delete a temporary database.
+ * @param context The Context in which this receiver is running.
+ */
+ @Override
+ public void onDisabled(Context context) {
+ AppLog.d(AppLog.T.STATS, "onDisabled called");
+ AnalyticsTracker.track(AnalyticsTracker.Stat.STATS_WIDGET_REMOVED);
+ AnalyticsTracker.flush();
+ AppPrefs.resetStatsWidgetsKeys();
+ AppPrefs.resetStatsWidgetsData();
+ }
+
+ /**
+ * This is called every time an App Widget is deleted from the App Widget host.
+ * @param context The Context in which this receiver is running.
+ * @param widgetIDs Widget IDs to set blank. We cannot remove widget from home screen.
+ */
+ @Override
+ public void onDeleted(Context context, int[] widgetIDs) {
+ setRemoteBlogIDForWidgetIDs(widgetIDs, null);
+ }
+
+ public static void enqueueStatsRequestForBlog(Context context, String remoteBlogID, String date) {
+ // start service to get stats
+ Intent intent = new Intent(context, StatsService.class);
+ intent.putExtra(StatsService.ARG_BLOG_ID, remoteBlogID);
+ intent.putExtra(StatsService.ARG_PERIOD, StatsTimeframe.DAY);
+ intent.putExtra(StatsService.ARG_DATE, date);
+ intent.putExtra(StatsService.ARG_SECTION, new int[]{StatsService.StatsEndpointsEnum.VISITS.ordinal()});
+ context.startService(intent);
+ }
+
+ private static synchronized JSONObject getCacheDataForBlog(int remoteBlogID, String date) {
+ String prevDataAsString = AppPrefs.getStatsWidgetsData();
+ if (StringUtils.isEmpty(prevDataAsString)) {
+ AppLog.i(AppLog.T.STATS, "No cache found for the widgets");
+ return null;
+ }
+
+ try {
+ JSONObject prevData = new JSONObject(prevDataAsString);
+ if (!prevData.has(String.valueOf(remoteBlogID))) {
+ AppLog.i(AppLog.T.STATS, "No cache found for the blog ID " + remoteBlogID);
+ return null;
+ }
+
+ JSONObject cache = prevData.getJSONObject(String.valueOf(remoteBlogID));
+ String dateStoredInCache = cache.optString("date");
+ if (date.equals(dateStoredInCache)) {
+ AppLog.i(AppLog.T.STATS, "Cache found for the blog ID " + remoteBlogID);
+ return cache;
+ } else {
+ AppLog.i(AppLog.T.STATS, "Cache found for the blog ID " + remoteBlogID + " but the date value doesn't match!!");
+ return null;
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ return null;
+ }
+ }
+
+ public static synchronized boolean isBlogDisplayedInWidget(int remoteBlogID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ if (StringUtils.isEmpty(prevWidgetKeysString)) {
+ return false;
+ }
+ try {
+ JSONObject prevKeys = new JSONObject(prevWidgetKeysString);
+ JSONArray allKeys = prevKeys.names();
+ if (allKeys == null) {
+ return false;
+ }
+ for (int i=0; i < allKeys.length(); i ++) {
+ String currentKey = allKeys.getString(i);
+ int currentBlogID = prevKeys.getInt(currentKey);
+ if (currentBlogID == remoteBlogID) {
+ return true;
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ return false;
+ }
+
+ private static synchronized int[] getWidgetIDsFromRemoteBlogID(int remoteBlogID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ if (StringUtils.isEmpty(prevWidgetKeysString)) {
+ return new int[0];
+ }
+ ArrayList<Integer> widgetIDs = new ArrayList<>();
+
+ try {
+ JSONObject prevKeys = new JSONObject(prevWidgetKeysString);
+ JSONArray allKeys = prevKeys.names();
+ if (allKeys == null) {
+ return new int[0];
+ }
+ for (int i=0; i < allKeys.length(); i ++) {
+ String currentKey = allKeys.getString(i);
+ int currentBlogID = prevKeys.getInt(currentKey);
+ if (currentBlogID == remoteBlogID) {
+ AppLog.d(AppLog.T.STATS, "The blog with remoteID " + remoteBlogID + " is displayed in the widget " + currentKey);
+ widgetIDs.add(Integer.parseInt(currentKey));
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ return ArrayUtils.toPrimitive(widgetIDs.toArray(new Integer[widgetIDs.size()]));
+ }
+
+ private static synchronized int getRemoteBlogIDFromWidgetID(int widgetID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ if (StringUtils.isEmpty(prevWidgetKeysString)) {
+ return 0;
+ }
+ try {
+ JSONObject prevKeys = new JSONObject(prevWidgetKeysString);
+ return prevKeys.optInt(String.valueOf(widgetID), 0);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ return 0;
+ }
+
+
+ // Store the association between widgetIDs and the remote blog id into prefs.
+ private static void setRemoteBlogIDForWidgetIDs(int[] widgetIDs, String remoteBlogID) {
+ String prevWidgetKeysString = AppPrefs.getStatsWidgetsKeys();
+ JSONObject prevKeys = null;
+ if (!StringUtils.isEmpty(prevWidgetKeysString)) {
+ try {
+ prevKeys = new JSONObject(prevWidgetKeysString);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ if (prevKeys == null) {
+ prevKeys = new JSONObject();
+ }
+
+ for (int widgetID : widgetIDs) {
+ try {
+ prevKeys.put(String.valueOf(widgetID), remoteBlogID);
+ AppPrefs.setStatsWidgetsKeys(prevKeys.toString());
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+ }
+
+ // This is called by the Widget config activity at the end if the process
+ static void setupNewWidget(Context context, int widgetID, int localBlogID) {
+ AppLog.d(AppLog.T.STATS, "setupNewWidget called");
+
+ Blog blog = WordPress.getBlog(localBlogID);
+ if (blog == null) {
+ // it's unlikely that blog is null here.
+ // This method is called from config activity which has loaded the blog fine.
+ showMessage(context, new int[]{widgetID},
+ context.getString(R.string.stats_widget_error_readd_widget));
+ AppLog.e(AppLog.T.STATS, "setupNewWidget: No blog found in the db!");
+ return;
+ }
+
+ // At this point the remote ID cannot be null.
+ String remoteBlogID = blog.getDotComBlogId();
+ // Add the following check just to be safe
+ if (remoteBlogID == null) {
+ showMessage(context, new int[]{widgetID},
+ context.getString(R.string.stats_widget_error_readd_widget));
+ return;
+ }
+
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.STATS_WIDGET_ADDED, remoteBlogID);
+ AnalyticsTracker.flush();
+
+ // Store the association between the widget ID and the remote blog id into prefs.
+ setRemoteBlogIDForWidgetIDs(new int[] {widgetID}, remoteBlogID);
+
+ String currentDate = StatsUtils.getCurrentDateTZ(localBlogID);
+
+ // Load cached data if available and show it immediately
+ JSONObject cache = getCacheDataForBlog(Integer.parseInt(remoteBlogID), currentDate);
+ if (cache != null) {
+ showStatsData(context, new int[] {widgetID}, blog, cache);
+ return;
+ }
+
+ if (!NetworkUtils.isNetworkAvailable(context)) {
+ showMessage(context, new int[] {widgetID}, context.getString(R.string.no_network_title));
+ } else {
+ showMessage(context, new int[] {widgetID}, context.getString(R.string.stats_widget_loading_data));
+ enqueueStatsRequestForBlog(context, remoteBlogID, currentDate);
+ }
+ }
+
+
+ private static void refreshWidgets(Context context, int[] appWidgetIds) {
+ // If not signed into WordPress inform the user
+ if (!AccountHelper.isSignedIn()) {
+ showMessage(context, appWidgetIds, context.getString(R.string.stats_widget_error_no_account));
+ return;
+ }
+
+ SparseArray<ArrayList<Integer>> blogsToWidgetIDs = new SparseArray<>();
+ for (int widgetId : appWidgetIds) {
+ int remoteBlogID = getRemoteBlogIDFromWidgetID(widgetId);
+ if (remoteBlogID == 0) {
+ // This could happen on logout when prefs are erased completely since we cannot remove
+ // widgets programmatically from the screen, or during the configuration of new widgets!!!
+ AppLog.e(AppLog.T.STATS, "No remote blog ID for widget ID " + widgetId);
+ showMessage(context, new int[] {widgetId}, context.getString(R.string.stats_widget_error_readd_widget));
+ continue;
+ }
+
+ ArrayList<Integer> widgetIDs = blogsToWidgetIDs.get(remoteBlogID, new ArrayList<Integer>());
+ widgetIDs.add(widgetId);
+ blogsToWidgetIDs.append(remoteBlogID, widgetIDs);
+ }
+
+ // we now have an optimized data structure for our needs. BlogId -> widgetIDs list
+ for(int i = 0; i < blogsToWidgetIDs.size(); i++) {
+ int remoteBlogID = blogsToWidgetIDs.keyAt(i);
+ // get the object by the key.
+ ArrayList<Integer> widgetsList = blogsToWidgetIDs.get(remoteBlogID);
+ int[] currentWidgets = ArrayUtils.toPrimitive(widgetsList.toArray(new Integer[widgetsList.size()]));
+
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(remoteBlogID);
+ Blog blog = WordPress.getBlog(localId);
+ if (localId == 0 || blog == null) {
+ // No blog in the app
+ showMessage(context, currentWidgets, context.getString(R.string.stats_widget_error_readd_widget));
+ continue;
+ }
+ String currentDate = StatsUtils.getCurrentDateTZ(localId);
+
+ // Load cached data if available and show it immediately
+ JSONObject cache = getCacheDataForBlog(remoteBlogID, currentDate);
+ if (cache != null) {
+ showStatsData(context, currentWidgets, blog, cache);
+ }
+
+ // If network is not available check if NO cache, and show the generic error
+ // If network is available always start a refresh, and show prev data or the loading in progress message.
+ if (!NetworkUtils.isNetworkAvailable(context)) {
+ if (cache == null) {
+ showMessage(context, currentWidgets, context.getString(R.string.stats_widget_error_generic));
+ }
+ } else {
+ if (cache == null) {
+ showMessage(context, currentWidgets, context.getString(R.string.stats_widget_loading_data));
+ }
+ // Make sure to refresh widget data now.
+ enqueueStatsRequestForBlog(context, String.valueOf(remoteBlogID), currentDate);
+ }
+ }
+ }
+
+ private static void refreshAllWidgets(Context context) {
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ ComponentName thisWidget = new ComponentName(context, StatsWidgetProvider.class);
+ int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
+ refreshWidgets(context, allWidgetIds);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java
new file mode 100644
index 000000000..2b3e04c6f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/URLSpanNoUnderline.java
@@ -0,0 +1,15 @@
+package org.wordpress.android.ui.stats;
+
+import android.text.TextPaint;
+import android.text.style.URLSpan;
+
+public class URLSpanNoUnderline extends URLSpan {
+ public URLSpanNoUnderline(String url) {
+ super(url);
+ }
+
+ public void updateDrawState(TextPaint drawState) {
+ super.updateDrawState(drawState);
+ drawState.setUnderlineText(false);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java
new file mode 100644
index 000000000..e49cbaf67
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/adapters/PostsAndPagesAdapter.java
@@ -0,0 +1,55 @@
+package org.wordpress.android.ui.stats.adapters;
+
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.stats.StatsViewHolder;
+import org.wordpress.android.ui.stats.models.PostModel;
+import org.wordpress.android.util.FormatUtils;
+
+import java.util.List;
+
+public class PostsAndPagesAdapter extends ArrayAdapter<PostModel> {
+
+ private final List<PostModel> list;
+ private final LayoutInflater inflater;
+
+ public PostsAndPagesAdapter(Context context, List<PostModel> list) {
+ super(context, R.layout.stats_list_cell, list);
+ this.list = list;
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // reuse views
+ if (rowView == null) {
+ rowView = inflater.inflate(R.layout.stats_list_cell, parent, false);
+ // configure view holder
+ StatsViewHolder viewHolder = new StatsViewHolder(rowView);
+ rowView.setTag(viewHolder);
+ }
+
+ final PostModel currentRowData = list.get(position);
+ StatsViewHolder holder = (StatsViewHolder) rowView.getTag();
+
+ // Entry
+ holder.setEntryTextOpenDetailsPage(currentRowData);
+
+ // Setup the more button
+ holder.setMoreButtonOpenInReader(currentRowData);
+
+ // totals
+ holder.totalsTextView.setText(FormatUtils.formatDecimal(currentRowData.getTotals()));
+
+ // no icon
+ holder.networkImageView.setVisibility(View.GONE);
+ return rowView;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java
new file mode 100644
index 000000000..03670cda8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsDatabaseHelper.java
@@ -0,0 +1,130 @@
+package org.wordpress.android.ui.stats.datasets;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import org.wordpress.android.util.AppLog;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * database for all tracks information
+ */
+public class StatsDatabaseHelper extends SQLiteOpenHelper {
+ private static final String DB_NAME = "stats.db";
+ private static final int DB_VERSION = 1;
+
+ /*
+ * database singleton
+ */
+ private static StatsDatabaseHelper mDatabaseHelper;
+ private final static Object mDbLock = new Object();
+ private final Context mContext;
+
+ public static StatsDatabaseHelper getDatabase(Context ctx) {
+ if (mDatabaseHelper == null) {
+ synchronized(mDbLock) {
+ if (mDatabaseHelper == null) {
+ mDatabaseHelper = new StatsDatabaseHelper(ctx);
+ // this ensures that onOpen() is called with a writable database (open will fail if app calls getReadableDb() first)
+ mDatabaseHelper.getWritableDatabase();
+ }
+ }
+ }
+ return mDatabaseHelper;
+ }
+
+ private StatsDatabaseHelper(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ mContext = context;
+ }
+
+
+ public static SQLiteDatabase getReadableDb(Context ctx) {
+ return getDatabase(ctx).getReadableDatabase();
+ }
+ public static SQLiteDatabase getWritableDb(Context ctx) {
+ return getDatabase(ctx).getWritableDatabase();
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ super.onOpen(db);
+ // Used during development to copy database to external storage and read its content.
+ // copyDatabase(db);
+ }
+
+ /*
+ * drop & recreate all tables (essentially clears the db of all data)
+ */
+ public void reset() {
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ dropAllTables(db);
+ createAllTables(db);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createAllTables(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // for now just reset the db when upgrading, future versions may want to avoid this
+ // and modify table structures, etc., on upgrade while preserving data
+ AppLog.i(AppLog.T.STATS, "Upgrading database from version " + oldVersion + " to version " + newVersion);
+ reset();
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // IMPORTANT: do NOT call super() here - doing so throws a SQLiteException
+ AppLog.w(AppLog.T.STATS, "Downgrading database from version " + oldVersion + " to version " + newVersion);
+ reset();
+ }
+
+ private void createAllTables(SQLiteDatabase db) {
+ StatsTable.createTables(db);
+ }
+
+ private void dropAllTables(SQLiteDatabase db) {
+ StatsTable.dropTables(db);
+ }
+
+ /*
+ * used during development to copy database to external storage so we can access it via DDMS
+ */
+ @SuppressWarnings("unused")
+ private void copyDatabase(SQLiteDatabase db) {
+ String copyFrom = db.getPath();
+ String copyTo = mContext.getExternalFilesDir(null).getAbsolutePath() + "/" + DB_NAME;
+
+ try {
+ InputStream input = new FileInputStream(copyFrom);
+ OutputStream output = new FileOutputStream(copyTo);
+
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+
+ output.flush();
+ output.close();
+ input.close();
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.STATS, "failed to copy stats database", e);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java
new file mode 100644
index 000000000..27cad108c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/datasets/StatsTable.java
@@ -0,0 +1,226 @@
+package org.wordpress.android.ui.stats.datasets;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.ui.stats.StatsTimeframe;
+import org.wordpress.android.ui.stats.service.StatsService.StatsEndpointsEnum;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.SqlUtils;
+
+public class StatsTable {
+
+ private static final String TABLE_NAME = "tbl_stats";
+ public static final int CACHE_TTL_MINUTES = 10;
+ private static final int MAX_RESPONSE_LEN = (int) (1024 * 1024 * 1.8); // 1.8 MB Approx
+
+ static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_NAME + " ("
+ + " id INTEGER PRIMARY KEY ASC," // Also alias for the built-in rowid: "rowid", "oid", or "_rowid_"
+ + " blogID INTEGER NOT NULL," // The local blog_id as stored in the WPDB
+ + " type INTEGER DEFAULT 0," // The type of the stats. TopPost, followers, etc..
+ + " timeframe INTEGER DEFAULT 0," // This could be days, week, years - It's an enum
+ + " date TEXT NOT NULL,"
+ + " jsonData TEXT NOT NULL,"
+ + " maxResult INTEGER DEFAULT 0,"
+ + " page INTEGER DEFAULT 0,"
+ + " timestamp INTEGER NOT NULL," // The unix timestamp of the response
+ + " UNIQUE (blogID, type, timeframe, date) ON CONFLICT REPLACE"
+ + ")");
+ }
+
+ static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+ }
+
+ protected static void reset(SQLiteDatabase db) {
+ dropTables(db);
+ createTables(db);
+ }
+
+
+ public static String getStats(final Context ctx, final int blogId, final StatsTimeframe timeframe, final String date,
+ final StatsEndpointsEnum sectionToUpdate, final int maxResultsRequested, final int pageRequested) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot insert a null stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return null;
+ }
+
+ String sql = "SELECT * FROM " + TABLE_NAME + " WHERE blogID = ? "
+ + " AND type=?"
+ + " AND timeframe=?"
+ + " AND date=?"
+ + " AND page=?"
+ + " AND maxResult >=?"
+ + " ORDER BY timestamp DESC"
+ + " LIMIT 1";
+
+ String[] args = {
+ Integer.toString(blogId),
+ Integer.toString(sectionToUpdate.ordinal()),
+ Integer.toString(timeframe.ordinal()),
+ date,
+ Integer.toString(pageRequested),
+ Integer.toString(maxResultsRequested),
+ };
+
+ Cursor cursor = StatsDatabaseHelper.getReadableDb(ctx).rawQuery(sql, args);
+
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ long timestamp = cursor.getLong(cursor.getColumnIndex("timestamp"));
+ long currentTime = System.currentTimeMillis();
+ long deltaMS = currentTime - timestamp;
+ if (deltaMS < 0) {
+ // current date is in the past respect to stats date?? Uhhh!
+ return null;
+ }
+
+ deltaMS = deltaMS / 1000; // seconds
+ // check if the cache is fresh
+ if ((deltaMS / 60) > CACHE_TTL_MINUTES) {
+ return null; // cache is expired
+ }
+
+ return cursor.getString(cursor.getColumnIndex("jsonData"));
+ } else {
+ return null;
+ }
+ } catch (IllegalStateException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ } finally {
+ SqlUtils.closeCursor(cursor);
+ }
+
+ return null;
+ }
+
+ public static void insertStats(final Context ctx, final int blogId, final StatsTimeframe timeframe, final String date,
+ final StatsEndpointsEnum sectionToUpdate, final int maxResultsRequested, final int pageRequested,
+ final String jsonResponse, final long responseTimestamp) {
+
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot insert a null stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return;
+ }
+
+ /*
+ * Android's CursorWindow has a max size of 2MB per row which can be exceeded
+ * with a very large text column, causing an IllegalStateException when the
+ * row is read - prevent this by limiting the amount of text that's stored in
+ * the text column - note that this situation very rarely occurs
+ * https://github.com/android/platform_frameworks_base/blob/master/core/res/res/values/config.xml#L1268
+ * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java/android/database/CursorWindow.java#L103
+ */
+
+ //Check if the response document from the server is less than 1.8MB. getBytes uses UTF-8 on Android.
+ if (jsonResponse.getBytes().length > MAX_RESPONSE_LEN) {
+ AppLog.w(AppLog.T.STATS, "Stats JSON response length > max allowed length of 1.8MB. Current response will not be stored in cache.");
+ return;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ db.beginTransaction();
+ SQLiteStatement stmt = db.compileStatement("INSERT INTO " + TABLE_NAME + " (blogID, type, timeframe, date, " +
+ "jsonData, maxResult, page, timestamp) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)");
+ try {
+ stmt.bindLong(1, blogId);
+ stmt.bindLong(2, sectionToUpdate.ordinal());
+ stmt.bindLong(3, timeframe.ordinal());
+ stmt.bindString(4, date);
+ stmt.bindString(5, jsonResponse);
+ stmt.bindLong(6, maxResultsRequested);
+ stmt.bindLong(7, pageRequested);
+ stmt.bindLong(8, responseTimestamp);
+ stmt.execute();
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ /**
+ * Delete expired Stats data from StatsDB
+ */
+ public static boolean deleteOldStats(final Context ctx, final long timestamp) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot delete stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return false;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ try {
+ db.beginTransaction();
+ int rowDeleted = db.delete(TABLE_NAME, "timestamp <= ?", new String[] { Long.toString(timestamp) });
+ db.setTransactionSuccessful();
+ AppLog.d(AppLog.T.STATS, "Number of old stats deleted : " + rowDeleted);
+ return rowDeleted > 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public static boolean deleteStatsForBlog(final Context ctx, final int blogId) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot delete stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return false;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ try {
+ db.beginTransaction();
+ int rowDeleted = db.delete(TABLE_NAME, "blogID=?", new String[] {Integer.toString(blogId)});
+ db.setTransactionSuccessful();
+ AppLog.d(AppLog.T.STATS, "Stats deleted for localBlogID " + blogId);
+ return rowDeleted > 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public static boolean deleteStatsForBlog(final Context ctx, final int blogId, final StatsEndpointsEnum sectionToUpdate ) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot delete stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return false;
+ }
+
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ try {
+ db.beginTransaction();
+ int rowDeleted = db.delete(TABLE_NAME, "blogID=? AND type=?",
+ new String[] {Integer.toString(blogId), Integer.toString(sectionToUpdate.ordinal())}
+ );
+ db.setTransactionSuccessful();
+ AppLog.d(AppLog.T.STATS, "Stats deleted for localBlogID " + blogId + " and type " + sectionToUpdate.getRestEndpointPath());
+ return rowDeleted > 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ public static void purgeAll(Context ctx) {
+ if (ctx == null) {
+ AppLog.e(AppLog.T.STATS, "Cannot purgeAll stats since the passed context is null. Context is required " +
+ "to access the DB.");
+ return;
+ }
+ SQLiteDatabase db = StatsDatabaseHelper.getWritableDb(ctx);
+ db.beginTransaction();
+ try {
+ db.execSQL("DELETE FROM " + TABLE_NAME);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java
new file mode 100644
index 000000000..8784ff95f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/exceptions/StatsError.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.ui.stats.exceptions;
+
+import java.io.Serializable;
+
+public class StatsError extends Exception implements Serializable {
+ public StatsError(String errorMessage) {
+ super(errorMessage);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java
new file mode 100644
index 000000000..51ba25535
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorModel.java
@@ -0,0 +1,119 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A model to represent a Author
+ */
+public class AuthorModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+ private String mGroupId;
+ private String mName;
+ private String mAvatar;
+ private int mViews;
+ private FollowDataModel mFollowData;
+ private List<PostModel> mPosts;
+
+ public AuthorModel(String mBlogId, String date, String mGroupId, String mName, String mAvatar, int mViews, JSONObject followData) throws JSONException {
+ this.mBlogId = mBlogId;
+ setDate(StatsUtils.toMs(date));
+ this.mGroupId = mGroupId;
+ this.mName = mName;
+ this.mAvatar = mAvatar;
+ this.mViews = mViews;
+ if (followData != null) {
+ this.mFollowData = new FollowDataModel(followData);
+ }
+ }
+
+ public AuthorModel(String blogId, String date, JSONObject authorJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setGroupId(authorJSON.getString("name"));
+ setName(authorJSON.getString("name"));
+ setViews(authorJSON.getInt("views"));
+ setAvatar(JSONUtils.getString(authorJSON, "avatar"));
+
+ // Follow data could return a boolean false
+ JSONObject followData = authorJSON.optJSONObject("follow_data");
+ if (followData != null) {
+ this.mFollowData = new FollowDataModel(followData);
+ }
+
+ JSONArray postsJSON = authorJSON.getJSONArray("posts");
+ mPosts = new ArrayList<>(authorJSON.length());
+ for (int i = 0; i < postsJSON.length(); i++) {
+ JSONObject currentPostJSON = postsJSON.getJSONObject(i);
+ String postId = String.valueOf(currentPostJSON.getInt("id"));
+ String title = currentPostJSON.getString("title");
+ int views = currentPostJSON.getInt("views");
+ String url = currentPostJSON.getString("url");
+ PostModel currentPost = new PostModel(mBlogId, mDate, postId, title, views, url);
+ mPosts.add(currentPost);
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ private void setGroupId(String groupId) {
+ this.mGroupId = groupId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ private void setViews(int total) {
+ this.mViews = total;
+ }
+
+ public FollowDataModel getFollowData() {
+ return mFollowData;
+ }
+
+ public String getAvatar() {
+ return mAvatar;
+ }
+
+ private void setAvatar(String icon) {
+ this.mAvatar = icon;
+ }
+
+ public List<PostModel> getPosts() { return mPosts; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java
new file mode 100644
index 000000000..92b5f6a47
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/AuthorsModel.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class AuthorsModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherViews;
+ private List<AuthorModel> mAuthors;
+
+ public AuthorsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ JSONArray authorsJSONArray;
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherViews = firstDayObject.optInt("other_views");
+ authorsJSONArray = firstDayObject.optJSONArray("authors");
+
+ if (authorsJSONArray != null) {
+ mAuthors = new ArrayList<>(authorsJSONArray.length());
+ for (int i = 0; i < authorsJSONArray.length(); i++) {
+ try {
+ JSONObject currentAuthorJSON = authorsJSONArray.getJSONObject(i);
+ AuthorModel currentAuthor = new AuthorModel(blogID, mDate, currentAuthorJSON);
+ mAuthors.add(currentAuthor);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected Author object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<AuthorModel> getAuthors() {
+ return this.mAuthors;
+ }
+
+ public int getOtherViews() {
+ return mOtherViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java
new file mode 100644
index 000000000..814feefd6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/BaseStatsModel.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+public class BaseStatsModel implements Serializable{
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java
new file mode 100644
index 000000000..660c30767
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClickGroupModel.java
@@ -0,0 +1,113 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A model to represent a click group stat
+ */
+public class ClickGroupModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+
+ private String mGroupId;
+ private String mName;
+ private String mIcon;
+ private int mViews;
+ private String mUrl;
+ private List<SingleItemModel> mClicks;
+
+ public ClickGroupModel(String blogId, String date, JSONObject clickGroupJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setGroupId(clickGroupJSON.getString("name"));
+ setName(clickGroupJSON.getString("name"));
+ setViews(clickGroupJSON.getInt("views"));
+ setIcon(JSONUtils.getString(clickGroupJSON, "icon"));
+
+ // if URL is set in the response there is one result only. No need to unfold "results"
+ if (!TextUtils.isEmpty(JSONUtils.getString(clickGroupJSON, "url"))) {
+ setUrl(JSONUtils.getString(clickGroupJSON, "url"));
+ } else {
+ JSONArray childrenJSON = clickGroupJSON.getJSONArray("children");
+ mClicks = new ArrayList<>(childrenJSON.length());
+ for (int i = 0; i < childrenJSON.length(); i++) {
+ JSONObject currentResultJSON = childrenJSON.getJSONObject(i);
+ String name = currentResultJSON.getString("name");
+ int totals = currentResultJSON.getInt("views");
+ String icon = currentResultJSON.optString("icon");
+ String url = currentResultJSON.optString("url");
+ SingleItemModel rm = new SingleItemModel(blogId, date, null, name, totals, url, icon);
+ mClicks.add(rm);
+ }
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ private void setGroupId(String groupId) {
+ this.mGroupId = groupId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ private void setViews(int total) {
+ this.mViews = total;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ private void setUrl(String url) {
+ this.mUrl = url;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ private void setIcon(String icon) {
+ this.mIcon = icon;
+ }
+
+ public List<SingleItemModel> getClicks() { return mClicks; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java
new file mode 100644
index 000000000..ef60320aa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ClicksModel.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class ClicksModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherClicks;
+ private int mTotalClicks;
+ private List<ClickGroupModel> mClickGroups;
+
+ public ClicksModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ JSONArray jClickGroupsArray;
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherClicks = firstDayObject.getInt("other_clicks");
+ this.mTotalClicks = firstDayObject.getInt("total_clicks");
+ jClickGroupsArray = firstDayObject.optJSONArray("clicks");
+
+ if (jClickGroupsArray != null) {
+ mClickGroups = new ArrayList<>(jClickGroupsArray.length());
+ for (int i = 0; i < jClickGroupsArray.length(); i++) {
+ try {
+ JSONObject currentGroupJSON = jClickGroupsArray.getJSONObject(i);
+ ClickGroupModel currentGroupModel = new ClickGroupModel(blogID, mDate, currentGroupJSON);
+ mClickGroups.add(currentGroupModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected ClickGroupModel object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<ClickGroupModel> getClickGroups() {
+ return this.mClickGroups;
+ }
+
+ public int getOtherClicks() {
+ return mOtherClicks;
+ }
+
+ public int getTotalClicks() {
+ return mTotalClicks;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java
new file mode 100644
index 000000000..a970b44bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentFollowersModel.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class CommentFollowersModel extends BaseStatsModel {
+ private String mBlogID;
+ private int mPage;
+ private int mPages;
+ private int mTotal;
+ private List<SingleItemModel> mPosts;
+
+ public CommentFollowersModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPage = response.getInt("page");
+ this.mPages = response.getInt("pages");
+ this.mTotal = response.getInt("total");
+
+ JSONArray postsJSONArray = response.optJSONArray("posts");
+ if (postsJSONArray != null) {
+ mPosts = new ArrayList<>(postsJSONArray.length());
+ for (int i = 0; i < postsJSONArray.length(); i++) {
+ JSONObject currentPostJSON = postsJSONArray.getJSONObject(i);
+ String postId = String.valueOf(currentPostJSON.getInt("id"));
+ String title = currentPostJSON.getString("title");
+ int followers = currentPostJSON.getInt("followers");
+ String url = currentPostJSON.getString("url");
+ SingleItemModel currentPost = new SingleItemModel(blogID, null, postId, title, followers, url, null);
+ mPosts.add(currentPost);
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public List<SingleItemModel> getPosts() {
+ return this.mPosts;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ public int getPages() {
+ return mPages;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java
new file mode 100644
index 000000000..ef7bfe6ac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/CommentsModel.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsConstants;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class CommentsModel extends BaseStatsModel {
+ private String mDate;
+ private String mBlogID;
+ private int mMonthlyComments;
+ private int mTotalComments;
+ private String mMostActiveDay;
+ private String mMostActiveTime;
+ private SingleItemModel mMostCommentedPost;
+
+ private List<PostModel> mPosts;
+ private List<AuthorModel> mAuthors;
+
+ public CommentsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mDate = response.getString("date");
+
+ this.mMonthlyComments = response.getInt("monthly_comments");
+ this.mTotalComments = response.getInt("total_comments");
+ this.mMostActiveDay = response.getString("most_active_day");
+ this.mMostActiveTime = response.getString("most_active_time");
+
+
+ JSONArray postsJSONArray = response.optJSONArray("posts");
+ if (postsJSONArray != null) {
+ mPosts = new ArrayList<>(postsJSONArray.length());
+ for (int i = 0; i < postsJSONArray.length(); i++) {
+ JSONObject currentPostJSON = postsJSONArray.getJSONObject(i);
+ String itemID = String.valueOf(currentPostJSON.getInt("id"));
+ String name = currentPostJSON.getString("name");
+ int totals = currentPostJSON.getInt("comments");
+ String link = currentPostJSON.getString("link");
+ PostModel currentPost = new PostModel(blogID, mDate, itemID, name, totals, link, StatsConstants.ITEM_TYPE_POST);
+ mPosts.add(currentPost);
+ }
+ }
+
+ JSONArray authorsJSONArray = response.optJSONArray("authors");
+ if (authorsJSONArray != null) {
+ mAuthors = new ArrayList<>(authorsJSONArray.length());
+ for (int i = 0; i < authorsJSONArray.length(); i++) {
+ JSONObject currentAuthorJSON = authorsJSONArray.getJSONObject(i);
+ String name = currentAuthorJSON.getString("name");
+ int comments = currentAuthorJSON.getInt("comments");
+ String url = currentAuthorJSON.getString("link");
+ String gravatar = currentAuthorJSON.getString("gravatar");
+ JSONObject followData = currentAuthorJSON.optJSONObject("follow_data");
+ AuthorModel currentAuthor = new AuthorModel(blogID, mDate, url, name, gravatar, comments, followData);
+ mAuthors.add(currentAuthor);
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public List<PostModel> getPosts() {
+ return this.mPosts;
+ }
+
+ public List<AuthorModel> getAuthors() {
+ return this.mAuthors;
+ }
+
+ public int getTotalComments() {
+ return mTotalComments;
+ }
+
+ public int getMonthlyComments() {
+ return mMonthlyComments;
+ }
+
+ public String getMostActiveDay() {
+ return mMostActiveDay;
+ }
+
+ public String getMostActiveTime() {
+ return mMostActiveTime;
+ }
+
+ public SingleItemModel getMostCommentedPost() {
+ return mMostCommentedPost;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java
new file mode 100644
index 000000000..2c8acb829
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowDataModel.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+public class FollowDataModel implements Serializable {
+
+ /*
+ "following-text": "Following",
+ "is_following": false,
+ "following-hover-text": "Unfollow",
+ "blog_id": 6098762,
+ "blog_url": "http://ilpostodellefragole.wordpress.com",
+ "blog_title": "Il posto delle fragole",
+ "site_id": 6098762,
+ "stat-source": "stats_comments",
+ "follow-text": "Follow",
+ "blog_domain": "ilpostodellefragole.wordpress.com"
+ */
+
+ private String type;
+ private String followText;
+ private String followingText;
+ private String followingHoverText;
+ private boolean isFollowing;
+ private int blogID;
+ private int siteID;
+ private String statsSource;
+ private String blogDomain;
+
+ public transient boolean isRestCallInProgress = false;
+
+ public FollowDataModel(JSONObject followDataJSON) throws JSONException {
+ this.type = followDataJSON.getString("type");
+ JSONObject paramsJSON = followDataJSON.getJSONObject("params");
+ this.followText = paramsJSON.getString("follow-text");
+ this.followingText = paramsJSON.getString("following-text");
+ this.followingHoverText = paramsJSON.getString("following-hover-text");
+ this.isFollowing = paramsJSON.getBoolean("is_following");
+ this.blogID = paramsJSON.getInt("blog_id");
+ this.siteID = paramsJSON.getInt("site_id");
+ this.statsSource = paramsJSON.getString("stat-source");
+ this.blogDomain = paramsJSON.getString("blog_domain");
+ }
+
+ public boolean isFollowing() {
+ return isFollowing;
+ }
+
+ public void setIsFollowing(boolean following) {
+ isFollowing = following;
+ }
+
+ public int getBlogID() {
+ return blogID;
+ }
+
+ public int getSiteID() {
+ return siteID;
+ }
+
+ public String getFollowText() {
+ return followText;
+ }
+
+ public String getFollowingHoverText() {
+ return followingHoverText;
+ }
+
+ public String getFollowingText() {
+ return followingText;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getStatsSource() {
+ return statsSource;
+ }
+
+ public String getBlogDomain() {
+ return blogDomain;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java
new file mode 100644
index 000000000..4efc3bb8c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowerModel.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.io.Serializable;
+
+public class FollowerModel implements Serializable {
+ private String mBlogId;
+ private String mLabel;
+ private String mAvatar;
+ private String mUrl;
+ private FollowDataModel mFollowData;
+ private String mDateSubscribed;
+
+ public FollowerModel(String mBlogId, JSONObject followerJSONData) throws JSONException{
+ this.mBlogId = mBlogId;
+ this.mLabel = followerJSONData.getString("label");
+
+ setAvatar(JSONUtils.getString(followerJSONData, "avatar"));
+ setURL(JSONUtils.getString(followerJSONData, "url"));
+
+ this.mDateSubscribed = followerJSONData.getString("date_subscribed");
+
+ JSONObject followData = followerJSONData.optJSONObject("follow_data");
+ if (followData != null) {
+ this.mFollowData = new FollowDataModel(followData);
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public String getURL() {
+ return mUrl;
+ }
+
+ private boolean setURL(String URL) {
+ if (!TextUtils.isEmpty(URL) && UrlUtils.isValidUrlAndHostNotNull(URL)) {
+ this.mUrl = URL;
+ return true;
+ }
+ return false;
+ }
+
+ public FollowDataModel getFollowData() {
+ return mFollowData;
+ }
+
+ public String getAvatar() {
+ return mAvatar;
+ }
+
+
+
+
+ private void setAvatar(String icon) {
+ this.mAvatar = icon;
+ }
+
+ public String getDateSubscribed() {
+ return mDateSubscribed;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java
new file mode 100644
index 000000000..ec7826cc0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/FollowersModel.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class FollowersModel extends BaseStatsModel {
+ private String mBlogID;
+ private int mPage;
+ private int mPages;
+ private int mTotal;
+ private int mTotalEmail;
+ private int mTotalWPCom;
+ private List<FollowerModel> mSubscribers;
+
+ public FollowersModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPage = response.getInt("page");
+ this.mPages = response.getInt("pages");
+ this.mTotal = response.getInt("total");
+ this.mTotalEmail = response.getInt("total_email");
+ this.mTotalWPCom = response.getInt("total_wpcom");
+
+ JSONArray subscribersJSONArray = response.optJSONArray("subscribers");
+ if (subscribersJSONArray != null) {
+ mSubscribers = new ArrayList<>(subscribersJSONArray.length());
+ for (int i = 0; i < subscribersJSONArray.length(); i++) {
+ JSONObject currentAuthorJSON = subscribersJSONArray.getJSONObject(i);
+ try {
+ FollowerModel currentFollower = new FollowerModel(mBlogID, currentAuthorJSON);
+ mSubscribers.add(currentFollower);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected Follower object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public List<FollowerModel> getFollowers() {
+ return this.mSubscribers;
+ }
+
+ public int getTotalEmail() {
+ return mTotalEmail;
+ }
+
+ public int getTotalWPCom() {
+ return mTotalWPCom;
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ public int getPages() {
+ return mPages;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java
new file mode 100644
index 000000000..bab8f4541
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewModel.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+/**
+ * A model to represent a geoview stat.
+ */
+public class GeoviewModel implements Serializable {
+ private final String mCountryShortName;
+ private final String mCountryFullName;
+ private int mViews;
+ private final String mFlagIconURL;
+ private final String mFlatFlagIconURL;
+
+ public GeoviewModel(String countryShortName, String countryFullName, int views, String flagIcon, String flatFlagIcon) {
+ this.mCountryShortName = countryShortName;
+ this.mCountryFullName = countryFullName;
+ this.mViews = views;
+ this.mFlagIconURL = flagIcon;
+ this.mFlatFlagIconURL = flatFlagIcon;
+ }
+
+ public String getCountryFullName() {
+ return mCountryFullName;
+ }
+
+ public String getCountryShortName() {
+ return mCountryShortName;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public void setViews(int views) {
+ this.mViews = views;
+ }
+
+ public String getFlagIconURL() {
+ return mFlagIconURL;
+ }
+
+ public String getFlatFlagIconURL() {
+ return mFlatFlagIconURL;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java
new file mode 100644
index 000000000..589147bc0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/GeoviewsModel.java
@@ -0,0 +1,96 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class GeoviewsModel extends BaseStatsModel {
+ private String mDate;
+ private String mBlogID;
+ private int otherViews;
+ private int totalViews;
+ private List<GeoviewModel> countries;
+
+ public GeoviewsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String firstDayKey = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(firstDayKey);
+ this.otherViews = firstDayObject.getInt("other_views");
+ this.totalViews = firstDayObject.getInt("total_views");
+
+ JSONObject countryInfoJSON = response.optJSONObject("country-info");
+ JSONArray viewsJSON = firstDayObject.optJSONArray("views");
+
+ if (viewsJSON != null && countryInfoJSON != null) {
+ countries = new ArrayList<>(viewsJSON.length());
+ for (int i = 0; i < viewsJSON.length(); i++) {
+ JSONObject currentCountryJSON = viewsJSON.getJSONObject(i);
+ String currentCountryCode = currentCountryJSON.getString("country_code");
+ int currentCountryViews = currentCountryJSON.getInt("views");
+ String flagIcon = null;
+ String flatFlagIcon = null;
+ String countryFullName = null;
+ JSONObject currentCountryDetails = countryInfoJSON.optJSONObject(currentCountryCode);
+ if (currentCountryDetails != null) {
+ flagIcon = currentCountryDetails.optString("flag_icon");
+ flatFlagIcon = currentCountryDetails.optString("flat_flag_icon");
+ countryFullName = currentCountryDetails.optString("country_full");
+ }
+ GeoviewModel m = new GeoviewModel(currentCountryCode, countryFullName, currentCountryViews, flagIcon, flatFlagIcon);
+ countries.add(m);
+
+ }
+
+ // Sort the countries by views.
+ Collections.sort(countries, new java.util.Comparator<GeoviewModel>() {
+ public int compare(GeoviewModel o1, GeoviewModel o2) {
+ // descending order
+ return o2.getViews() - o1.getViews();
+ }
+ });
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public List<GeoviewModel> getCountries() {
+ return this.countries;
+ }
+
+ public int getOtherViews() {
+ return otherViews;
+ }
+
+ public int getTotalViews() {
+ return totalViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java
new file mode 100644
index 000000000..fd0b02e7b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsAllTimeModel.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class InsightsAllTimeModel extends BaseStatsModel {
+
+ private String mBlogID;
+ private String mDate;
+ private int mVisitors;
+ private int mViews;
+ private int mPosts;
+ private String mViewsBestDay;
+ private int mViewsBestDayTotal;
+
+
+ public InsightsAllTimeModel(String blogID, JSONObject response) throws JSONException {
+ this.setBlogID(blogID);
+ this.mDate = response.getString("date");
+ JSONObject stats = response.getJSONObject("stats");
+ this.mPosts = stats.optInt("posts");
+ this.mVisitors = stats.optInt("visitors");
+ this.mViews = stats.optInt("views");
+ this.mViewsBestDay = stats.getString("views_best_day");
+ this.mViewsBestDayTotal = stats.optInt("views_best_day_total");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public int getVisitors() {
+ return mVisitors;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public int getPosts() {
+ return mPosts;
+ }
+
+ public String getViewsBestDay() {
+ return mViewsBestDay;
+ }
+
+ public int getViewsBestDayTotal() {
+ return mViewsBestDayTotal;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java
new file mode 100644
index 000000000..7a70e9e89
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostDetailsModel.java
@@ -0,0 +1,23 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+public class InsightsLatestPostDetailsModel extends BaseStatsModel {
+ private String mBlogID;
+ private int mViews;
+
+ public InsightsLatestPostDetailsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mViews = response.getInt("views");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public int getPostViewsCount() {
+ return mViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java
new file mode 100644
index 000000000..534348c37
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsLatestPostModel.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+public class InsightsLatestPostModel extends BaseStatsModel {
+ private String mBlogID;
+ private String mPostTitle;
+ private String mPostURL;
+ private String mPostDate;
+ private int mPostID;
+ private int mPostViewsCount = Integer.MIN_VALUE;
+ private int mPostCommentCount;
+ private int mPostLikeCount;
+ private int mPostsFound; // if 0 there are no posts on the blog.
+
+ public InsightsLatestPostModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+
+ mPostsFound = response.optInt("found", 0);
+ if (mPostsFound == 0) {
+ // No latest post found!
+ return;
+ }
+
+ JSONArray postsObject = response.getJSONArray("posts");
+ if (postsObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ // Read the first post
+ JSONObject firstPostObject = postsObject.getJSONObject(0);
+
+ this.mPostID = firstPostObject.getInt("ID");
+ this.mPostTitle = firstPostObject.getString("title");
+ this.mPostDate = firstPostObject.getString("date");
+ this.mPostURL = firstPostObject.getString("URL");
+ this.mPostLikeCount = firstPostObject.getInt("like_count");
+
+ JSONObject discussionObject = firstPostObject.optJSONObject("discussion");
+ if (discussionObject != null) {
+ this.mPostCommentCount = discussionObject.optInt("comment_count", 0);
+ }
+ }
+
+ public boolean isLatestPostAvailable() {
+ return mPostsFound > 0;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public String getPostDate() {
+ return mPostDate;
+ }
+
+ public String getPostTitle() {
+ return mPostTitle;
+ }
+
+ public String getPostURL() {
+ return mPostURL;
+ }
+
+ public int getPostID() {
+ return mPostID;
+ }
+
+ public int getPostViewsCount() {
+ return mPostViewsCount;
+ }
+
+ public void setPostViewsCount(int postViewsCount) {
+ this.mPostViewsCount = postViewsCount;
+ }
+
+ public int getPostCommentCount() {
+ return mPostCommentCount;
+ }
+
+ public int getPostLikeCount() {
+ return mPostLikeCount;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java
new file mode 100644
index 000000000..640c96ffa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsPopularModel.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONObject;
+
+public class InsightsPopularModel extends BaseStatsModel {
+ private final int mHighestHour;
+ private final int mHighestDayOfWeek;
+ private final Double mHighestDayPercent;
+ private final Double mHighestHourPercent;
+ private String mBlogID;
+
+ public InsightsPopularModel(String blogID, JSONObject response) {
+ this.setBlogID(blogID);
+ this.mHighestDayOfWeek = response.optInt("highest_day_of_week");
+ this.mHighestHour = response.optInt("highest_hour");
+ this.mHighestDayPercent = response.optDouble("highest_day_percent");
+ this.mHighestHourPercent = response.optDouble("highest_hour_percent");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public int getHighestHour() {
+ return mHighestHour;
+ }
+
+ public int getHighestDayOfWeek() {
+ return mHighestDayOfWeek;
+ }
+
+ public Double getHighestDayPercent() {
+ return mHighestDayPercent;
+ }
+
+ public Double getHighestHourPercent() {
+ return mHighestHourPercent;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java
new file mode 100644
index 000000000..c633bc2e6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/InsightsTodayModel.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class InsightsTodayModel extends BaseStatsModel {
+
+ private String mBlogID;
+ private String mDate;
+ private String mPeriod;
+ private int mVisitors;
+ private int mViews;
+ private int mLikes;
+ private int mReblogs;
+ private int mComments;
+ private int mFollowers;
+
+ public InsightsTodayModel(String blogID, JSONObject response) throws JSONException {
+ this.setBlogID(blogID);
+ this.mDate = response.getString("date");
+ this.mPeriod = response.getString("period");
+ this.mViews = response.optInt("views");
+ this.mVisitors = response.optInt("visitors");
+ this.mLikes = response.optInt("likes");
+ this.mReblogs = response.optInt("reblogs");
+ this.mComments = response.optInt("comments");
+ this.mFollowers = response.optInt("followers");
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public int getReblogs() {
+ return mReblogs;
+ }
+
+ public int getComments() {
+ return mComments;
+ }
+
+ public int getFollowers() {
+ return mFollowers;
+ }
+
+ public int getLikes() {
+ return mLikes;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public int getVisitors() {
+ return mVisitors;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java
new file mode 100644
index 000000000..40a37b8e1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostModel.java
@@ -0,0 +1,30 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.wordpress.android.ui.stats.StatsConstants;
+import org.wordpress.android.ui.stats.StatsUtils;
+
+import java.io.Serializable;
+
+public class PostModel extends SingleItemModel implements Serializable {
+
+ private final String mPostType;
+
+ public PostModel(String blogId, String date, String itemID, String title, int totals, String url, String postType) {
+ super(blogId, date, itemID, title, totals, url, null);
+ this.mPostType = postType;
+ }
+
+ public PostModel(String blogId, long date, String itemID, String title, int totals, String url) {
+ super(blogId, date, itemID, title, totals, url, null);
+ this.mPostType = StatsConstants.ITEM_TYPE_POST;
+ }
+
+ public PostModel(String blogId, String itemID, String title, String url, String postType) {
+ super(blogId, StatsUtils.getCurrentDate(), itemID, title, 0, url, null);
+ this.mPostType = postType;
+ }
+
+ public String getPostType() {
+ return mPostType;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java
new file mode 100644
index 000000000..5beb0df95
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PostViewsModel.java
@@ -0,0 +1,370 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class PostViewsModel implements Serializable {
+ private String mOriginalResponse;
+
+ private int mHighestMonth, mHighestDayAverage, mHighestWeekAverage;
+ private String mDate;
+ private VisitModel[] mDayViews; //Used to build the graph
+ private List<Year> mYears;
+ private List<Year> mAverages;
+ private List<Week> mWeeks;
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public String getOriginalResponse() {
+ return mOriginalResponse;
+ }
+
+ public VisitModel[] getDayViews() {
+ return mDayViews;
+ }
+
+ public int getHighestMonth() {
+ return mHighestMonth;
+ }
+
+ public int getHighestDayAverage() {
+ return mHighestDayAverage;
+ }
+
+ public int getHighestWeekAverage() {
+ return mHighestWeekAverage;
+ }
+
+ public List<Year> getYears() {
+ return mYears;
+ }
+
+ public List<Year> getAverages() {
+ return mAverages;
+ }
+
+ public List<Week> getWeeks() {
+ return mWeeks;
+ }
+
+
+ public PostViewsModel(String response) throws JSONException {
+ this.mOriginalResponse = response;
+ JSONObject responseObj = new JSONObject(response);
+ parseResponseObject(responseObj);
+ }
+
+ public PostViewsModel(JSONObject response) throws JSONException {
+ if (response == null) {
+ return;
+ }
+ this.mOriginalResponse = response.toString();
+ parseResponseObject(response);
+ }
+
+ private void parseResponseObject(JSONObject response) throws JSONException {
+
+ mDate = response.getString("date");
+ mHighestDayAverage = response.getInt("highest_day_average");
+ mHighestWeekAverage = response.getInt("highest_week_average");
+ mHighestMonth = response.getInt("highest_month");
+ mYears = new LinkedList<>();
+ mAverages = new LinkedList<>();
+ mWeeks = new LinkedList<>();
+
+ JSONArray dataJSON = response.getJSONArray("data");
+ if (dataJSON != null) {
+ // Read the position/index of each field in the response
+ JSONArray fieldsJSON = response.getJSONArray("fields");
+ HashMap<String, Integer> fieldColumnsMapping;
+ try {
+ fieldColumnsMapping = new HashMap<>(2);
+ for (int i = 0; i < fieldsJSON.length(); i++) {
+ final String field = fieldsJSON.getString(i);
+ fieldColumnsMapping.put(field, i);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot read the fields indexes from the JSON response", e);
+ throw e;
+ }
+
+ VisitModel[] visitModels = new VisitModel[dataJSON.length()];
+ int viewsColumnIndex = fieldColumnsMapping.get("views");
+ int periodColumnIndex = fieldColumnsMapping.get("period");
+
+ for (int i = 0; i < dataJSON.length(); i++) {
+ try {
+ JSONArray currentDayData = dataJSON.getJSONArray(i);
+ VisitModel currentVisitModel = new VisitModel();
+ currentVisitModel.setPeriod(currentDayData.getString(periodColumnIndex));
+ currentVisitModel.setViews(currentDayData.getInt(viewsColumnIndex));
+ visitModels[i] = currentVisitModel;
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot create the Visit at index " + i, e);
+ }
+ }
+ mDayViews = visitModels;
+ } else {
+ mDayViews = null;
+ }
+
+ parseYears(response);
+ parseAverages(response);
+ parseWeeks(response);
+ }
+
+ private String[] orderKeys(Iterator keys, int numberOfKeys) {
+ // Keys could not be ordered fine. Reordering them.
+ String[] orderedKeys = new String[numberOfKeys];
+ int i = 0;
+ while (keys.hasNext()) {
+ orderedKeys[i] = (String)keys.next();
+ i++;
+ }
+ Arrays.sort(orderedKeys);
+ return orderedKeys;
+ }
+
+ private void parseYears(JSONObject response) {
+ // Parse the Years section
+ try {
+ JSONObject yearsJSON = response.getJSONObject("years");
+ // Keys could not be ordered fine. Reordering them.
+ String[] orderedKeys = orderKeys(yearsJSON.keys(), yearsJSON.length());
+
+ for (String currentYearKey : orderedKeys) {
+ Year currentYear = new Year();
+ currentYear.setLabel(currentYearKey);
+
+ JSONObject currentYearObj = yearsJSON.getJSONObject(currentYearKey);
+ int total = currentYearObj.getInt("total");
+ currentYear.setTotal(total);
+
+ JSONObject monthsJSON = currentYearObj.getJSONObject("months");
+ Iterator<String> monthsKeys = monthsJSON.keys();
+ List<Month> monthsList = new ArrayList<>(monthsJSON.length());
+ while (monthsKeys.hasNext()) {
+ String currentMonthKey = monthsKeys.next();
+ int currentMonthVisits = monthsJSON.getInt(currentMonthKey);
+ monthsList.add(new Month(currentMonthKey, currentMonthVisits));
+ }
+
+ Collections.sort(monthsList, new Comparator<Month>() {
+ public int compare(Month o1, Month o2) {
+ int v1 = Integer.parseInt(o1.getMonth());
+ int v2 = Integer.parseInt(o2.getMonth());
+ // ascending order
+ return v1 - v2;
+ }
+ });
+
+ currentYear.setMonths(monthsList);
+ mYears.add(currentYear);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the Years section", e);
+ }
+ }
+
+ private void parseAverages(JSONObject response) {
+ // Parse the Averages section
+ try {
+ JSONObject averagesJSON = response.getJSONObject("averages");
+ // Keys could not be ordered fine. Reordering them.
+ String[] orderedKeys = orderKeys(averagesJSON.keys(), averagesJSON.length());
+
+ for (String currentJSONKey : orderedKeys) {
+ Year currentAverage = new Year();
+ currentAverage.setLabel(currentJSONKey);
+
+ JSONObject currentAverageJSONObj = averagesJSON.getJSONObject(currentJSONKey);
+ currentAverage.setTotal(currentAverageJSONObj.getInt("overall"));
+
+ JSONObject monthsJSON = currentAverageJSONObj.getJSONObject("months");
+ Iterator<String> monthsKeys = monthsJSON.keys();
+ List<Month> monthsList = new ArrayList<>(monthsJSON.length());
+ while (monthsKeys.hasNext()) {
+ String currentMonthKey = monthsKeys.next();
+ int currentMonthVisits = monthsJSON.getInt(currentMonthKey);
+ monthsList.add(new Month(currentMonthKey, currentMonthVisits));
+ }
+ Collections.sort(monthsList, new java.util.Comparator<Month>() {
+ public int compare(Month o1, Month o2) {
+ int v1 = Integer.parseInt(o1.getMonth());
+ int v2 = Integer.parseInt(o2.getMonth());
+ // ascending order
+ return v1 - v2;
+ }
+ });
+
+ currentAverage.setMonths(monthsList);
+ mAverages.add(currentAverage);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the Averages section", e);
+ }
+ }
+
+ private void parseWeeks(JSONObject response) {
+ // Parse the Weeks section
+ try {
+ JSONArray weeksJSON = response.getJSONArray("weeks");
+ for (int i = 0; i < weeksJSON.length(); i++) {
+ Week currentWeek = new Week();
+ JSONObject currentWeekJSON = weeksJSON.getJSONObject(i);
+
+ currentWeek.setTotal(currentWeekJSON.getInt("total"));
+ currentWeek.setAverage(currentWeekJSON.getInt("average"));
+ try {
+ if (i == 0 ) {
+ currentWeek.setChange(0);
+ } else {
+ currentWeek.setChange(currentWeekJSON.getInt("change"));
+ }
+ } catch (JSONException e){
+ AppLog.w(AppLog.T.STATS, "Cannot parse the change value in weeks section. Trying to understand the meaning: 42!!");
+ // if i == 0 is the first week. if not it could mean infinity
+ String aProblematicValue = currentWeekJSON.get("change").toString();
+ if (aProblematicValue.contains("infinity")) {
+ currentWeek.setChange(Integer.MAX_VALUE);
+ } else {
+ currentWeek.setChange(0);
+ }
+ }
+
+ JSONArray daysJSON = currentWeekJSON.getJSONArray("days");
+ for (int j = 0; j < daysJSON.length(); j++) {
+ Day currentDay = new Day();
+ JSONObject dayJSON = daysJSON.getJSONObject(j);
+ currentDay.setCount(dayJSON.getInt("count"));
+ currentDay.setDay(dayJSON.getString("day"));
+ currentWeek.getDays().add(currentDay);
+ }
+ mWeeks.add(currentWeek);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot parse the Weeks section", e);
+ }
+ }
+
+ public class Day implements Serializable {
+ private int mCount;
+ private String mDay;
+
+ public String getDay() {
+ return mDay;
+ }
+
+ public void setDay(String day) {
+ this.mDay = day;
+ }
+
+ public int getCount() {
+ return mCount;
+ }
+
+ public void setCount(int count) {
+ this.mCount = count;
+ }
+ }
+
+ public class Week implements Serializable {
+ int mChange;
+ int mTotal;
+ int mAverage;
+ List<Day> mDays = new LinkedList<>();
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ public void setTotal(int total) {
+ this.mTotal = total;
+ }
+
+ public int getAverage() {
+ return mAverage;
+ }
+
+ public void setAverage(int average) {
+ this.mAverage = average;
+ }
+
+ public int getChange() {
+ return mChange;
+ }
+
+ public void setChange(int change) {
+ this.mChange = change;
+ }
+
+ public List<Day> getDays() {
+ return mDays;
+ }
+
+ public void setDays(List<Day> days) {
+ this.mDays = days;
+ }
+ }
+
+ public class Year implements Serializable {
+ private String mLabel;
+ private int mTotal;
+ private List<Month> mMonths;
+
+ public List<Month> getMonths() {
+ return mMonths;
+ }
+
+ public void setMonths(List<Month> months) {
+ mMonths = months;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ this.mLabel = label;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ public void setTotal(int total) {
+ this.mTotal = total;
+ }
+ }
+
+ public class Month implements Serializable {
+ private final int mCount;
+ private final String mMonth;
+
+ Month(String label, int count) {
+ this.mMonth = label;
+ this.mCount = count;
+ }
+
+ public String getMonth() {
+ return mMonth;
+ }
+ public int getCount() {
+ return mCount;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java
new file mode 100644
index 000000000..295a28061
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/PublicizeModel.java
@@ -0,0 +1,40 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PublicizeModel extends BaseStatsModel {
+ private String mBlogID;
+ private List<SingleItemModel> mServices;
+
+ public PublicizeModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ JSONArray services = response.getJSONArray("services");
+ if (services.length() > 0) {
+ mServices = new ArrayList<>(services.length());
+ for (int i = 0; i < services.length(); i++) {
+ JSONObject current = services.getJSONObject(i);
+ String serviceName = current.getString("service");
+ int followers = current.getInt("followers");
+ SingleItemModel currentItem = new SingleItemModel(blogID, null, null, serviceName, followers, null, null);
+ mServices.add(currentItem);
+ }
+ }
+ }
+
+ public List<SingleItemModel> getServices() {
+ return mServices;
+ }
+
+ public String getBlogId() {
+ return mBlogID;
+ }
+
+ public void setBlogId(String blogId) {
+ this.mBlogID = blogId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java
new file mode 100644
index 000000000..e093c0006
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerGroupModel.java
@@ -0,0 +1,124 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model to represent a referrer group stat
+ */
+public class ReferrerGroupModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+
+ private String mGroupId;
+ private String mName;
+ private String mIcon;
+ private int mTotal;
+ private String mUrl;
+ private List<ReferrerResultModel> mResults;
+
+ public transient boolean isRestCallInProgress = false;
+ public transient boolean isMarkedAsSpam = false;
+
+ public ReferrerGroupModel(String blogId, String date, JSONObject groupJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setGroupId(groupJSON.getString("group"));
+ setName(groupJSON.getString("name"));
+ setTotal(groupJSON.getInt("total"));
+ setIcon(JSONUtils.getString(groupJSON, "icon"));
+
+ // if URL is set in the response there is one result only.
+ if (!TextUtils.isEmpty(JSONUtils.getString(groupJSON, "url"))) {
+ setUrl(JSONUtils.getString(groupJSON, "url"));
+ }
+
+ // results is an array when there are results, otherwise it's an object.
+ JSONArray resultsArray = groupJSON.optJSONArray("results");
+ if (resultsArray != null) {
+ mResults = new ArrayList<>();
+ for (int i = 0; i < resultsArray.length(); i++) {
+ JSONObject currentResultJSON = resultsArray.getJSONObject(i);
+ ReferrerResultModel currentResultModel = new ReferrerResultModel(blogId,
+ date, currentResultJSON);
+ mResults.add(currentResultModel);
+ }
+ // Sort the results by views.
+ Collections.sort(mResults, new java.util.Comparator<ReferrerResultModel>() {
+ public int compare(ReferrerResultModel o1, ReferrerResultModel o2) {
+ // descending order
+ return o2.getViews() - o1.getViews();
+ }
+ });
+ }
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ private void setGroupId(String groupId) {
+ this.mGroupId = groupId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getTotal() {
+ return mTotal;
+ }
+
+ private void setTotal(int total) {
+ this.mTotal = total;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ private void setUrl(String url) {
+ this.mUrl = url;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ private void setIcon(String icon) {
+ this.mIcon = icon;
+ }
+
+ public List<ReferrerResultModel> getResults() { return mResults; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java
new file mode 100644
index 000000000..a2cb1fdc4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrerResultModel.java
@@ -0,0 +1,116 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.util.JSONUtils;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model to represent a referrer result in stat
+ */
+public class ReferrerResultModel implements Serializable {
+ private String mBlogId;
+ private long mDate;
+
+ private String mName;
+ private String mIcon;
+ private int mViews;
+ private String mUrl;
+ private List<SingleItemModel> mChildren;
+
+ public ReferrerResultModel(String blogId, String date, JSONObject resultJSON) throws JSONException {
+ setBlogId(blogId);
+ setDate(StatsUtils.toMs(date));
+
+ setName(resultJSON.getString("name"));
+ setViews(resultJSON.getInt("views"));
+ setIcon(JSONUtils.getString(resultJSON, "icon"));
+
+ if (!TextUtils.isEmpty(JSONUtils.getString(resultJSON, "url"))) {
+ setUrl(JSONUtils.getString(resultJSON, "url"));
+ }
+
+ if (resultJSON.has("children")) {
+ JSONArray childrenJSON = resultJSON.getJSONArray("children");
+ mChildren = new ArrayList<>();
+ for (int i = 0; i < childrenJSON.length(); i++) {
+ JSONObject currentChild = childrenJSON.getJSONObject(i);
+ mChildren.add(getChildren(blogId, date, currentChild));
+ }
+
+ //Sort the children by views.
+ Collections.sort(mChildren, new java.util.Comparator<SingleItemModel>() {
+ public int compare(SingleItemModel o1, SingleItemModel o2) {
+ // descending order
+ return o2.getTotals() - o1.getTotals();
+ }
+ });
+ }
+ }
+
+ private SingleItemModel getChildren(String blogId, String date, JSONObject child) throws JSONException {
+ String name = child.getString("name");
+ int totals = child.getInt("views");
+ String icon = JSONUtils.getString(child, "icon");
+ String url = child.optString("url");
+ return new SingleItemModel(blogId, date, null, name, totals, url, icon);
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ private void setBlogId(String blogId) {
+ this.mBlogId = blogId;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+ private void setDate(long date) {
+ this.mDate = date;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ private void setName(String name) {
+ this.mName = name;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ private void setViews(int total) {
+ this.mViews = total;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ private void setIcon(String icon) {
+ this.mIcon = icon;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ private void setUrl(String url) {
+ this.mUrl = url;
+ }
+
+ public List<SingleItemModel> getChildren() { return mChildren; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java
new file mode 100644
index 000000000..62f5f32fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/ReferrersModel.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class ReferrersModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherViews;
+ private int mTotalViews;
+ private List<ReferrerGroupModel> mGroups;
+
+ public ReferrersModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ JSONArray jGroupsArray;
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherViews = firstDayObject.optInt("other_views");
+ this.mTotalViews = firstDayObject.optInt("total_views");
+ jGroupsArray = firstDayObject.optJSONArray("groups");
+
+ if (jGroupsArray != null) {
+ mGroups = new ArrayList<>(jGroupsArray.length());
+ for (int i = 0; i < jGroupsArray.length(); i++) {
+ try {
+ JSONObject currentGroupJSON = jGroupsArray.getJSONObject(i);
+ ReferrerGroupModel currentGroupModel = new ReferrerGroupModel(blogID, mDate, currentGroupJSON);
+ mGroups.add(currentGroupModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected ReferrerGroupModel object " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<ReferrerGroupModel> getGroups() {
+ return this.mGroups;
+ }
+
+ public int getOtherViews() {
+ return mOtherViews;
+ }
+
+ public int getTotalViews() {
+ return mTotalViews;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java
new file mode 100644
index 000000000..b83b71b01
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermModel.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+public class SearchTermModel extends SingleItemModel implements Serializable {
+
+ private final boolean mIsEncriptedTerms;
+
+ public SearchTermModel(String blogId, String date, String title, int totals, boolean isEncriptedTerms) {
+ super(blogId, date, null, title, totals, null, null);
+ this.mIsEncriptedTerms = isEncriptedTerms;
+ }
+
+ public boolean isEncriptedTerms() {
+ return mIsEncriptedTerms;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java
new file mode 100644
index 000000000..49fde8f04
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SearchTermsModel.java
@@ -0,0 +1,108 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class SearchTermsModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private List<SearchTermModel> mSearchTerms;
+ private int mEncryptedSearchTerms, mOtherSearchTerms, mTotalSearchTerms;
+
+ public SearchTermsModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONArray searchTermsArray = null;
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ Iterator<String> keys = jDaysObject.keys();
+ if (keys.hasNext()) {
+ String key = keys.next();
+ JSONObject jDateObject = jDaysObject.optJSONObject(key); // This could be an empty array on site with low traffic
+ searchTermsArray = null;
+ if (jDateObject != null) {
+ searchTermsArray = jDateObject.getJSONArray("search_terms");
+ this.mEncryptedSearchTerms = jDateObject.optInt("encrypted_search_terms");
+ this.mOtherSearchTerms = jDateObject.optInt("other_search_terms");
+ this.mTotalSearchTerms = jDateObject.optInt("total_search_terms");
+ }
+ }
+
+ if (searchTermsArray == null) {
+ searchTermsArray = new JSONArray();
+ }
+
+ ArrayList<SearchTermModel> list = new ArrayList<>(searchTermsArray.length());
+ for (int i=0; i < searchTermsArray.length(); i++) {
+ try {
+ JSONObject postObject = searchTermsArray.getJSONObject(i);
+ String term = postObject.getString("term");
+ int total = postObject.getInt("views");
+ SearchTermModel currentModel = new SearchTermModel(blogID, mDate, term, total, false);
+ list.add(currentModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected SearchTerm object in searchterms array" +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+
+ this.mSearchTerms = list;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<SearchTermModel> getSearchTerms() {
+ return mSearchTerms;
+ }
+
+ public boolean hasSearchTerms() {
+ return mSearchTerms != null && mSearchTerms.size() > 0;
+ }
+
+ public int getEncryptedSearchTerms() {
+ return mEncryptedSearchTerms;
+ }
+
+ public int getOtherSearchTerms() {
+ return mOtherSearchTerms;
+ }
+
+ public int getTotalSearchTerms() {
+ return mTotalSearchTerms;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java
new file mode 100644
index 000000000..0f3b2ef23
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/SingleItemModel.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.webkit.URLUtil;
+
+import org.wordpress.android.ui.stats.StatsUtils;
+
+import java.io.Serializable;
+
+/*
+* A model to represent a SINGLE stats item
+*/
+public class SingleItemModel implements Serializable {
+ private final String mBlogID;
+ private final String mItemID;
+ private final long mDate;
+ private final String mTitle;
+ private final int mTotals;
+ private final String mUrl;
+ private final String mIcon;
+
+ public SingleItemModel(String blogId, String date, String itemID, String title, int totals, String url, String icon) {
+ this(blogId, StatsUtils.toMs(date), itemID, title, totals, url, icon);
+ }
+
+ SingleItemModel(String blogId, long date, String itemID, String title, int totals, String url, String icon) {
+ this.mBlogID = blogId;
+ this.mItemID = itemID;
+ this.mTitle = title;
+ this.mTotals = totals;
+
+ // We could get invalid data back from the server. Check that URL is OK.
+ if (!URLUtil.isValidUrl(url)) {
+ this.mUrl = "";
+ } else {
+ this.mUrl = url;
+ }
+
+ this.mDate = date;
+ this.mIcon = icon;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public String getItemID() {
+ return mItemID;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public int getTotals() {
+ return mTotals;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public String getIcon() {
+ return mIcon;
+ }
+
+ public long getDate() {
+ return mDate;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java
new file mode 100644
index 000000000..087c3db58
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagModel.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+public class TagModel implements Serializable {
+ private String mName;
+ private String mLink;
+ private String mType;
+
+ public TagModel(JSONObject tagJSON) throws JSONException {
+
+ this.mName = tagJSON.getString("name");
+ this.mType = tagJSON.getString("type");
+ this.mLink = tagJSON.getString("link");
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getLink() {
+ return mLink;
+ }
+
+ public String getType() {
+ return mType;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java
new file mode 100644
index 000000000..4c35ccc1f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsContainerModel.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagsContainerModel extends BaseStatsModel {
+ private String mDate;
+ private String mBlogID;
+ private List<TagsModel> mTags;
+
+ public TagsContainerModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mDate = response.getString("date");
+ JSONArray outerTags = response.getJSONArray("tags");
+ if (outerTags != null) {
+ mTags = new ArrayList<>(outerTags.length());
+ for (int i = 0; i < outerTags.length(); i++) {
+ JSONObject current = outerTags.getJSONObject(i);
+ mTags.add(new TagsModel(current));
+ }
+ }
+ }
+
+ public List<TagsModel> getTags() {
+ return mTags;
+ }
+
+ public String getBlogId() {
+ return mBlogID;
+ }
+
+ public void setBlogId(String blogId) {
+ this.mBlogID = blogId;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java
new file mode 100644
index 000000000..19ce375ef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TagsModel.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagsModel implements Serializable {
+ private int mViews;
+ private List<TagModel> mTags;
+
+ public TagsModel(JSONObject responseJSON) throws JSONException {
+ this.mViews = responseJSON.getInt("views");
+ JSONArray innerTagsJSON = responseJSON.getJSONArray("tags");
+ mTags = new ArrayList<>(innerTagsJSON.length());
+ for (int i = 0; i < innerTagsJSON.length(); i++) {
+ JSONObject currentTagJSON = innerTagsJSON.getJSONObject(i);
+ TagModel currentTag = new TagModel(currentTagJSON);
+ mTags.add(currentTag);
+ }
+ }
+
+
+
+ public List<TagModel> getTags() {
+ return mTags;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java
new file mode 100644
index 000000000..5ccf3bef4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/TopPostsAndPagesModel.java
@@ -0,0 +1,93 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class TopPostsAndPagesModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private List<PostModel> mTopPostsAndPages;
+
+ public TopPostsAndPagesModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+ JSONArray postViewsArray = null;
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ Iterator<String> keys = jDaysObject.keys();
+ if (keys.hasNext()) {
+ String key = keys.next();
+ JSONObject jDateObject = jDaysObject.optJSONObject(key); // This could be an empty array on site with low traffic
+ postViewsArray = (jDateObject != null) ? jDateObject.getJSONArray("postviews") : null;
+ }
+
+ if (postViewsArray == null) {
+ postViewsArray = new JSONArray();
+ }
+
+ ArrayList<PostModel> list = new ArrayList<>(postViewsArray.length());
+
+ for (int i=0; i < postViewsArray.length(); i++) {
+ try {
+ JSONObject postObject = postViewsArray.getJSONObject(i);
+ String itemID = postObject.getString("id");
+ String itemTitle = postObject.getString("title");
+ int itemTotal = postObject.getInt("views");
+ String itemURL = postObject.getString("href");
+ String itemType = postObject.getString("type");
+ String itemDate = postObject.getString("date");
+ PostModel currentModel = new PostModel(blogID, itemDate, itemID, itemTitle,
+ itemTotal, itemURL, itemType);
+ list.add(currentModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Unexpected PostModel object in top posts and pages array " +
+ "at position " + i + " Response: " + response.toString(), e);
+ }
+ }
+ this.mTopPostsAndPages = list;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<PostModel> getTopPostsAndPages() {
+ return mTopPostsAndPages;
+ }
+
+ public boolean hasTopPostsAndPages() {
+ return mTopPostsAndPages != null && mTopPostsAndPages.size() > 0;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java
new file mode 100644
index 000000000..ef31b2f19
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VideoPlaysModel.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.stats.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class VideoPlaysModel extends BaseStatsModel {
+ private String mPeriod;
+ private String mDate;
+ private String mBlogID;
+ private int mOtherPlays;
+ private int mTotalPlays;
+ private List<SingleItemModel> mPlays;
+
+ public VideoPlaysModel(String blogID, JSONObject response) throws JSONException {
+ this.mBlogID = blogID;
+ this.mPeriod = response.getString("period");
+ this.mDate = response.getString("date");
+
+ JSONObject jDaysObject = response.getJSONObject("days");
+ if (jDaysObject.length() == 0) {
+ throw new JSONException("Invalid document returned from the REST API");
+ }
+
+ // Read the first day
+ Iterator<String> keys = jDaysObject.keys();
+ String key = keys.next();
+ JSONObject firstDayObject = jDaysObject.getJSONObject(key);
+ this.mOtherPlays = firstDayObject.getInt("other_plays");
+ this.mTotalPlays = firstDayObject.getInt("total_plays");
+ JSONArray playsJSONArray = firstDayObject.optJSONArray("plays");
+
+ if (playsJSONArray != null) {
+ mPlays = new ArrayList<>(playsJSONArray.length());
+ for (int i = 0; i < playsJSONArray.length(); i++) {
+ JSONObject currentVideoplaysJSON = playsJSONArray.getJSONObject(i);
+ String postId = String.valueOf(currentVideoplaysJSON.getInt("post_id"));
+ String title = currentVideoplaysJSON.getString("title");
+ int views = currentVideoplaysJSON.getInt("plays");
+ String url = currentVideoplaysJSON.getString("url");
+ SingleItemModel currentPost = new SingleItemModel(blogID, mDate, postId, title, views, url, null);
+ mPlays.add(currentPost);
+ }
+ }
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ public void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+ public List<SingleItemModel> getPlays() {
+ return this.mPlays;
+ }
+
+ public int getOtherPlays() {
+ return mOtherPlays;
+ }
+
+ public int getTotalPlays() {
+ return mTotalPlays;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java
new file mode 100644
index 000000000..d1f4ac613
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitModel.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.stats.models;
+
+import java.io.Serializable;
+
+public class VisitModel implements Serializable {
+
+ private int mViews;
+ private int mLikes;
+ private int mVisitors;
+ private int mComments;
+ private String mPeriod;
+ private String mBlogID;
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ public void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public int getViews() {
+ return mViews;
+ }
+
+ public void setViews(int views) {
+ this.mViews = views;
+ }
+
+ public int getLikes() {
+ return mLikes;
+ }
+
+ public void setLikes(int likes) {
+ this.mLikes = likes;
+ }
+
+ public int getVisitors() {
+ return mVisitors;
+ }
+
+ public void setVisitors(int visitors) {
+ this.mVisitors = visitors;
+ }
+
+ public int getComments() {
+ return mComments;
+ }
+
+ public void setComments(int comments) {
+ this.mComments = comments;
+ }
+
+ public String getPeriod() {
+ return mPeriod;
+ }
+
+ public void setPeriod(String period) {
+ this.mPeriod = period;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java
new file mode 100644
index 000000000..4b96bf3a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/models/VisitsModel.java
@@ -0,0 +1,136 @@
+package org.wordpress.android.ui.stats.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class VisitsModel extends BaseStatsModel {
+ private String mFields; // Holds a JSON Object
+ private String mUnit;
+ private String mDate;
+ private String mBlogID;
+ private List<VisitModel> mVisits;
+
+ public VisitsModel(String blogID, JSONObject response) throws JSONException {
+ this.setBlogID(blogID);
+ this.setDate(response.getString("date"));
+ this.setUnit(response.getString("unit"));
+ this.setFields(response.getJSONArray("fields").toString());
+
+ JSONArray dataJSON;
+ try {
+ dataJSON = response.getJSONArray("data");
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, this.getClass().getName() + " cannot convert the data field to a JSON array", e);
+ dataJSON = new JSONArray();
+ }
+
+ if (dataJSON == null || dataJSON.length() == 0) {
+ mVisits = new ArrayList<>(0);
+ } else {
+ // Read the position/index of each field in the response
+ HashMap<String, Integer> columnsMapping = new HashMap<>(6);
+ final JSONArray fieldsJSON = getFieldsJSON();
+ if (fieldsJSON == null || fieldsJSON.length() == 0) {
+ mVisits = new ArrayList<>(0);
+ } else {
+ try {
+ for (int i = 0; i < fieldsJSON.length(); i++) {
+ final String field = fieldsJSON.getString(i);
+ columnsMapping.put(field, i);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot read the parameter fields from the JSON response." +
+ "Response: " + response.toString(), e);
+ mVisits = new ArrayList<>(0);
+ }
+ }
+
+ int viewsColumnIndex = columnsMapping.get("views");
+ int visitorsColumnIndex = columnsMapping.get("visitors");
+ int likesColumnIndex = columnsMapping.get("likes");
+ int commentsColumnIndex = columnsMapping.get("comments");
+ int periodColumnIndex = columnsMapping.get("period");
+
+ int numPoints = dataJSON.length();
+ mVisits = new ArrayList<>(numPoints);
+
+ for (int i = 0; i < numPoints; i++) {
+ try {
+ JSONArray currentDayData = dataJSON.getJSONArray(i);
+ VisitModel currentVisitModel = new VisitModel();
+ currentVisitModel.setBlogID(getBlogID());
+ currentVisitModel.setPeriod(currentDayData.getString(periodColumnIndex));
+ currentVisitModel.setViews(currentDayData.getInt(viewsColumnIndex));
+ currentVisitModel.setVisitors(currentDayData.getInt(visitorsColumnIndex));
+ currentVisitModel.setComments(currentDayData.getInt(commentsColumnIndex));
+ currentVisitModel.setLikes(currentDayData.getInt(likesColumnIndex));
+ mVisits.add(currentVisitModel);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Cannot read the Visit item at index " + i
+ + " Response: " + response.toString(), e);
+ }
+ }
+ }
+ }
+
+ public List<VisitModel> getVisits() {
+ return mVisits;
+ }
+
+ public String getBlogID() {
+ return mBlogID;
+ }
+
+ private void setBlogID(String blogID) {
+ this.mBlogID = blogID;
+ }
+
+ public String getDate() {
+ return mDate;
+ }
+
+ private void setDate(String date) {
+ this.mDate = date;
+ }
+
+ public String getUnit() {
+ return mUnit;
+ }
+
+ private void setUnit(String unit) {
+ this.mUnit = unit;
+ }
+
+ private JSONArray getFieldsJSON() {
+ JSONArray jArray;
+ try {
+ String categories = StringUtils.unescapeHTML(this.getFields() != null ? this.getFields() : "[]");
+ if (TextUtils.isEmpty(categories)) {
+ jArray = new JSONArray();
+ } else {
+ jArray = new JSONArray(categories);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, this.getClass().getName() + " cannot convert the string to JSON", e);
+ return null;
+ }
+ return jArray;
+ }
+
+ private void setFields(String fields) {
+ this.mFields = fields;
+ }
+
+ private String getFields() {
+ return mFields;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java
new file mode 100644
index 000000000..77e42c01a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/service/StatsService.java
@@ -0,0 +1,614 @@
+package org.wordpress.android.ui.stats.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import com.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.RestClientUtils;
+import org.wordpress.android.ui.stats.StatsEvents;
+import org.wordpress.android.ui.stats.StatsTimeframe;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.ui.stats.exceptions.StatsError;
+import org.wordpress.android.ui.stats.models.AuthorsModel;
+import org.wordpress.android.ui.stats.models.BaseStatsModel;
+import org.wordpress.android.ui.stats.models.ClicksModel;
+import org.wordpress.android.ui.stats.models.CommentFollowersModel;
+import org.wordpress.android.ui.stats.models.CommentsModel;
+import org.wordpress.android.ui.stats.models.FollowersModel;
+import org.wordpress.android.ui.stats.models.GeoviewsModel;
+import org.wordpress.android.ui.stats.models.InsightsAllTimeModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostDetailsModel;
+import org.wordpress.android.ui.stats.models.InsightsLatestPostModel;
+import org.wordpress.android.ui.stats.models.InsightsPopularModel;
+import org.wordpress.android.ui.stats.models.PublicizeModel;
+import org.wordpress.android.ui.stats.models.ReferrersModel;
+import org.wordpress.android.ui.stats.models.SearchTermsModel;
+import org.wordpress.android.ui.stats.models.TagsContainerModel;
+import org.wordpress.android.ui.stats.models.TopPostsAndPagesModel;
+import org.wordpress.android.ui.stats.models.VideoPlaysModel;
+import org.wordpress.android.ui.stats.models.VisitModel;
+import org.wordpress.android.ui.stats.models.VisitsModel;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Background service to retrieve Stats.
+ * Parsing of response(s) and submission of new network calls are done by using a ThreadPoolExecutor with a single thread.
+ */
+
+public class StatsService extends Service {
+ public static final String ARG_BLOG_ID = "blog_id";
+ public static final String ARG_PERIOD = "stats_period";
+ public static final String ARG_DATE = "stats_date";
+ public static final String ARG_SECTION = "stats_section";
+ public static final String ARG_MAX_RESULTS = "stats_max_results";
+ public static final String ARG_PAGE_REQUESTED = "stats_page_requested";
+
+ private static final int DEFAULT_NUMBER_OF_RESULTS = 12;
+ // The number of results to return per page for Paged REST endpoints. Numbers larger than 20 will default to 20 on the server.
+ public static final int MAX_RESULTS_REQUESTED_PER_PAGE = 20;
+
+ public enum StatsEndpointsEnum {
+ VISITS,
+ TOP_POSTS,
+ REFERRERS,
+ CLICKS,
+ GEO_VIEWS,
+ AUTHORS,
+ VIDEO_PLAYS,
+ COMMENTS,
+ FOLLOWERS_WPCOM,
+ FOLLOWERS_EMAIL,
+ COMMENT_FOLLOWERS,
+ TAGS_AND_CATEGORIES,
+ PUBLICIZE,
+ SEARCH_TERMS,
+ INSIGHTS_POPULAR,
+ INSIGHTS_ALL_TIME,
+ INSIGHTS_TODAY,
+ INSIGHTS_LATEST_POST_SUMMARY,
+ INSIGHTS_LATEST_POST_VIEWS;
+
+ public String getRestEndpointPath() {
+ switch (this) {
+ case VISITS:
+ return "visits";
+ case TOP_POSTS:
+ return "top-posts";
+ case REFERRERS:
+ return "referrers";
+ case CLICKS:
+ return "clicks";
+ case GEO_VIEWS:
+ return "country-views";
+ case AUTHORS:
+ return "top-authors";
+ case VIDEO_PLAYS:
+ return "video-plays";
+ case COMMENTS:
+ return "comments";
+ case FOLLOWERS_WPCOM:
+ return "followers?type=wpcom";
+ case FOLLOWERS_EMAIL:
+ return "followers?type=email";
+ case COMMENT_FOLLOWERS:
+ return "comment-followers";
+ case TAGS_AND_CATEGORIES:
+ return "tags";
+ case PUBLICIZE:
+ return "publicize";
+ case SEARCH_TERMS:
+ return "search-terms";
+ case INSIGHTS_POPULAR:
+ return "insights";
+ case INSIGHTS_ALL_TIME:
+ return "";
+ case INSIGHTS_TODAY:
+ return "summary";
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ return "posts";
+ case INSIGHTS_LATEST_POST_VIEWS:
+ return "post";
+ default:
+ AppLog.i(T.STATS, "Called an update of Stats of unknown section!?? " + this.name());
+ return "";
+ }
+ }
+
+ public StatsEvents.SectionUpdatedAbstract getEndpointUpdateEvent(final String blogId, final StatsTimeframe timeframe, final String date,
+ final int maxResultsRequested, final int pageRequested, final BaseStatsModel data) {
+ switch (this) {
+ case VISITS:
+ return new StatsEvents.VisitorsAndViewsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (VisitsModel)data);
+ case TOP_POSTS:
+ return new StatsEvents.TopPostsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (TopPostsAndPagesModel)data);
+ case REFERRERS:
+ return new StatsEvents.ReferrersUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (ReferrersModel)data);
+ case CLICKS:
+ return new StatsEvents.ClicksUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (ClicksModel)data);
+ case AUTHORS:
+ return new StatsEvents.AuthorsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (AuthorsModel)data);
+ case GEO_VIEWS:
+ return new StatsEvents.CountriesUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (GeoviewsModel)data);
+ case VIDEO_PLAYS:
+ return new StatsEvents.VideoPlaysUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (VideoPlaysModel)data);
+ case SEARCH_TERMS:
+ return new StatsEvents.SearchTermsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (SearchTermsModel)data);
+ case COMMENTS:
+ return new StatsEvents.CommentsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (CommentsModel)data);
+ case COMMENT_FOLLOWERS:
+ return new StatsEvents.CommentFollowersUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (CommentFollowersModel)data);
+ case TAGS_AND_CATEGORIES:
+ return new StatsEvents.TagsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (TagsContainerModel)data);
+ case PUBLICIZE:
+ return new StatsEvents.PublicizeUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (PublicizeModel)data);
+ case FOLLOWERS_WPCOM:
+ return new StatsEvents.FollowersWPCOMUdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (FollowersModel)data);
+ case FOLLOWERS_EMAIL:
+ return new StatsEvents.FollowersEmailUdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (FollowersModel)data);
+ case INSIGHTS_POPULAR:
+ return new StatsEvents.InsightsPopularUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsPopularModel)data);
+ case INSIGHTS_ALL_TIME:
+ return new StatsEvents.InsightsAllTimeUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsAllTimeModel)data);
+ case INSIGHTS_TODAY:
+ return new StatsEvents.VisitorsAndViewsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (VisitsModel)data);
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ return new StatsEvents.InsightsLatestPostSummaryUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsLatestPostModel)data);
+ case INSIGHTS_LATEST_POST_VIEWS:
+ return new StatsEvents.InsightsLatestPostDetailsUpdated(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, (InsightsLatestPostDetailsModel)data);
+ default:
+ AppLog.e(T.STATS, "Can't find an Update Event that match the current endpoint: " + this.name());
+ }
+
+ return null;
+ }
+ }
+
+ private int mServiceStartId;
+ private final LinkedList<Request<JSONObject>> mStatsNetworkRequests = new LinkedList<>();
+ private final ThreadPoolExecutor singleThreadNetworkHandler = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(T.STATS, "service created");
+ }
+
+ @Override
+ public void onDestroy() {
+ stopRefresh();
+ AppLog.i(T.STATS, "service destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ AppLog.e(T.STATS, "StatsService was killed and restarted with a null intent.");
+ // if this service's process is killed while it is started (after returning from onStartCommand(Intent, int, int)),
+ // then leave it in the started state but don't retain this delivered intent.
+ // Later the system will try to re-create the service.
+ // Because it is in the started state, it will guarantee to call onStartCommand(Intent, int, int) after creating the new service instance;
+ // if there are not any pending start commands to be delivered to the service, it will be called with a null intent object.
+ stopRefresh();
+ return START_NOT_STICKY;
+ }
+
+ final String blogId = intent.getStringExtra(ARG_BLOG_ID);
+ if (TextUtils.isEmpty(blogId)) {
+ AppLog.e(T.STATS, "StatsService was started with a blank blog_id ");
+ return START_NOT_STICKY;
+ }
+
+ final StatsTimeframe period;
+ if (intent.hasExtra(ARG_PERIOD)) {
+ period = (StatsTimeframe) intent.getSerializableExtra(ARG_PERIOD);
+ } else {
+ period = StatsTimeframe.DAY;
+ }
+
+ final String requestedDate;
+ if (intent.getStringExtra(ARG_DATE) == null) {
+ AppLog.w(T.STATS, "StatsService is started with a NULL date on this blogID - "
+ + blogId + ". Using current date!!!");
+ int parsedBlogID = Integer.parseInt(blogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ requestedDate = StatsUtils.getCurrentDateTZ(localTableBlogId);
+ } else {
+ requestedDate = intent.getStringExtra(ARG_DATE);
+ }
+
+ final int maxResultsRequested = intent.getIntExtra(ARG_MAX_RESULTS, DEFAULT_NUMBER_OF_RESULTS);
+ final int pageRequested = intent.getIntExtra(ARG_PAGE_REQUESTED, -1);
+
+ int[] sectionFromIntent = intent.getIntArrayExtra(ARG_SECTION);
+
+ this.mServiceStartId = startId;
+ for (int i=0; i < sectionFromIntent.length; i++){
+ final StatsEndpointsEnum currentSectionsToUpdate = StatsEndpointsEnum.values()[sectionFromIntent[i]];
+ singleThreadNetworkHandler.submit(new Thread() {
+ @Override
+ public void run() {
+ startTasks(blogId, period, requestedDate, currentSectionsToUpdate, maxResultsRequested, pageRequested);
+ }
+ });
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void stopRefresh() {
+ synchronized (mStatsNetworkRequests) {
+ this.mServiceStartId = 0;
+ for (Request<JSONObject> req : mStatsNetworkRequests) {
+ if (req != null && !req.hasHadResponseDelivered() && !req.isCanceled()) {
+ req.cancel();
+ }
+ }
+ mStatsNetworkRequests.clear();
+ }
+ }
+
+ // A fast way to disable caching during develop or when we want to disable it
+ // under some circumstances. Always true for now.
+ private boolean isCacheEnabled() {
+ return true;
+ }
+
+ // Check if we already have Stats
+ private String getCachedStats(final String blogId, final StatsTimeframe timeframe, final String date, final StatsEndpointsEnum sectionToUpdate,
+ final int maxResultsRequested, final int pageRequested) {
+ if (!isCacheEnabled()) {
+ return null;
+ }
+
+ int parsedBlogID = Integer.parseInt(blogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ return StatsTable.getStats(this, localTableBlogId, timeframe, date, sectionToUpdate, maxResultsRequested, pageRequested);
+ }
+
+ private void startTasks(final String blogId, final StatsTimeframe timeframe, final String date, final StatsEndpointsEnum sectionToUpdate,
+ final int maxResultsRequested, final int pageRequested) {
+
+ EventBus.getDefault().post(new StatsEvents.UpdateStatusChanged(true));
+
+ String cachedStats = getCachedStats(blogId, timeframe, date, sectionToUpdate, maxResultsRequested, pageRequested);
+ if (cachedStats != null) {
+ BaseStatsModel mResponseObjectModel;
+ try {
+ JSONObject response = new JSONObject(cachedStats);
+ mResponseObjectModel = StatsUtils.parseResponse(sectionToUpdate, blogId, response);
+
+ EventBus.getDefault().post(
+ sectionToUpdate.getEndpointUpdateEvent(blogId, timeframe, date,
+ maxResultsRequested, pageRequested, mResponseObjectModel)
+ );
+
+ updateWidgetsUI(blogId, sectionToUpdate, timeframe, date, pageRequested, mResponseObjectModel);
+ checkAllRequestsFinished(null);
+ return;
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ final RestClientUtils restClientUtils = WordPress.getRestClientUtilsV1_1();
+
+ String period = timeframe.getLabelForRestCall();
+ /*AppLog.i(T.STATS, "A new Stats network request is required for blogID: " + blogId + " - period: " + period
+ + " - date: " + date + " - StatsType: " + sectionToUpdate.name());
+*/
+
+
+ RestListener vListener = new RestListener(sectionToUpdate, blogId, timeframe, date, maxResultsRequested, pageRequested);
+
+ final String periodDateMaxPlaceholder = "?period=%s&date=%s&max=%s";
+
+ String path = String.format("/sites/%s/stats/" + sectionToUpdate.getRestEndpointPath(), blogId);
+ synchronized (mStatsNetworkRequests) {
+ switch (sectionToUpdate) {
+ case VISITS:
+ path = String.format(path + "?unit=%s&quantity=15&date=%s", period, date);
+ break;
+ case TOP_POSTS:
+ case REFERRERS:
+ case CLICKS:
+ case GEO_VIEWS:
+ case AUTHORS:
+ case VIDEO_PLAYS:
+ case SEARCH_TERMS:
+ path = String.format(path + periodDateMaxPlaceholder, period, date, maxResultsRequested);
+ break;
+ case TAGS_AND_CATEGORIES:
+ case PUBLICIZE:
+ path = String.format(path + "?max=%s", maxResultsRequested);
+ break;
+ case COMMENTS:
+ // No parameters
+ break;
+ case FOLLOWERS_WPCOM:
+ if (pageRequested < 1) {
+ path = String.format(path + "&max=%s", maxResultsRequested);
+ } else {
+ path = String.format(path + "&period=%s&date=%s&max=%s&page=%s",
+ period, date, maxResultsRequested, pageRequested);
+ }
+ break;
+ case FOLLOWERS_EMAIL:
+ if (pageRequested < 1) {
+ path = String.format(path + "&max=%s", maxResultsRequested);
+ } else {
+ path = String.format(path + "&period=%s&date=%s&max=%s&page=%s",
+ period, date, maxResultsRequested, pageRequested);
+ }
+ break;
+ case COMMENT_FOLLOWERS:
+ if (pageRequested < 1) {
+ path = String.format(path + "?max=%s", maxResultsRequested);
+ } else {
+ path = String.format(path + "?period=%s&date=%s&max=%s&page=%s", period,
+ date, maxResultsRequested, pageRequested);
+ }
+ break;
+ case INSIGHTS_ALL_TIME:
+ case INSIGHTS_POPULAR:
+ break;
+ case INSIGHTS_TODAY:
+ path = String.format(path + "?period=day&date=%s", date);
+ break;
+ case INSIGHTS_LATEST_POST_SUMMARY:
+ // This is an edge cases since we're not loading stats but posts
+ path = String.format("/sites/%s/%s", blogId, sectionToUpdate.getRestEndpointPath()
+ + "?order_by=date&number=1&type=post&fields=ID,title,URL,discussion,like_count,date");
+ break;
+ case INSIGHTS_LATEST_POST_VIEWS:
+ // This is a kind of edge case, since we used the pageRequested parameter to request a single postID
+ path = String.format(path + "/%s?fields=views", pageRequested);
+ break;
+ default:
+ AppLog.i(T.STATS, "Called an update of Stats of unknown section!?? " + sectionToUpdate.name());
+ return;
+ }
+
+ // We need to check if we already have the same request in the queue
+ if (checkIfRequestShouldBeEnqueued(restClientUtils, path)) {
+ AppLog.d(AppLog.T.STATS, "Enqueuing the following Stats request " + path);
+ Request<JSONObject> currentRequest = restClientUtils.get(path, vListener, vListener);
+ vListener.currentRequest = currentRequest;
+ currentRequest.setTag("StatsCall");
+ mStatsNetworkRequests.add(currentRequest);
+ } else {
+ AppLog.d(AppLog.T.STATS, "Stats request is already in the queue:" + path);
+ }
+ }
+ }
+
+ /**
+ * This method checks if we already have the same request in the Queue. No need to re-enqueue a new request
+ * if one with the same parameters is there.
+ *
+ * This method is a kind of tricky, since it does the comparison by checking the origin URL of requests.
+ * To do that we had to get the fullURL of the new request by calling a method of the REST client `getAbsoluteURL`.
+ * That's good for now, but could lead to errors if the RestClient changes the way the URL is constructed internally,
+ * by calling `getAbsoluteURL`.
+ *
+ * - Another approach would involve the get of the requests ErrorListener and the check Listener's parameters.
+ * - Cleanest approach is for sure to create a new class that extends Request<JSONObject> and stores parameters for later comparison,
+ * unfortunately we have to change the REST Client and RestClientUtils a lot if we want follow this way...
+ *
+ */
+ private boolean checkIfRequestShouldBeEnqueued(final RestClientUtils restClientUtils, String path) {
+ String absoluteRequestPath = restClientUtils.getRestClient().getAbsoluteURL(path);
+ Iterator<Request<JSONObject>> it = mStatsNetworkRequests.iterator();
+ while (it.hasNext()) {
+ Request<JSONObject> req = it.next();
+ if (!req.hasHadResponseDelivered() && !req.isCanceled() &&
+ absoluteRequestPath.equals(req.getOriginUrl())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Call an updates on the installed widgets if the blog is the primary, the endpoint is Visits
+ // the timeframe is DAY or INSIGHTS, and the date = TODAY
+ private void updateWidgetsUI(String blogId, final StatsEndpointsEnum endpointName,
+ StatsTimeframe timeframe, String date, int pageRequested,
+ Serializable responseObjectModel) {
+ if (pageRequested != -1) {
+ return;
+ }
+ if (endpointName != StatsEndpointsEnum.VISITS) {
+ return;
+ }
+ if (timeframe != StatsTimeframe.DAY && timeframe != StatsTimeframe.INSIGHTS) {
+ return;
+ }
+
+ int parsedBlogID = Integer.parseInt(blogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ // make sure the data is for the current date
+ if (!date.equals(StatsUtils.getCurrentDateTZ(localTableBlogId))) {
+ return;
+ }
+
+ if (responseObjectModel == null) {
+ // TODO What we want to do here?
+ return;
+ }
+
+ if (!StatsWidgetProvider.isBlogDisplayedInWidget(parsedBlogID)) {
+ AppLog.d(AppLog.T.STATS, "The blog with remoteID " + parsedBlogID + " is NOT displayed in any widget. Stats Service doesn't call an update of the widget.");
+ return;
+ }
+
+ if (responseObjectModel instanceof VisitsModel) {
+ VisitsModel visitsModel = (VisitsModel) responseObjectModel;
+ if (visitsModel.getVisits() == null || visitsModel.getVisits().size() == 0) {
+ return;
+ }
+ List<VisitModel> visits = visitsModel.getVisits();
+ VisitModel data = visits.get(visits.size() - 1);
+ StatsWidgetProvider.updateWidgets(getApplicationContext(), parsedBlogID, data);
+ } else if (responseObjectModel instanceof VolleyError) {
+ VolleyError error = (VolleyError) responseObjectModel;
+ StatsWidgetProvider.updateWidgets(getApplicationContext(), parsedBlogID, error);
+ } else if (responseObjectModel instanceof StatsError) {
+ StatsError statsError = (StatsError) responseObjectModel;
+ StatsWidgetProvider.updateWidgets(getApplicationContext(), parsedBlogID, statsError);
+ }
+ }
+
+ private class RestListener implements RestRequest.Listener, RestRequest.ErrorListener {
+ final String mRequestBlogId;
+ private final StatsTimeframe mTimeframe;
+ final StatsEndpointsEnum mEndpointName;
+ private final String mDate;
+ private Request<JSONObject> currentRequest;
+ private final int mMaxResultsRequested, mPageRequested;
+
+ public RestListener(StatsEndpointsEnum endpointName, String blogId, StatsTimeframe timeframe, String date,
+ final int maxResultsRequested, final int pageRequested) {
+ mRequestBlogId = blogId;
+ mTimeframe = timeframe;
+ mEndpointName = endpointName;
+ mDate = date;
+ mMaxResultsRequested = maxResultsRequested;
+ mPageRequested = pageRequested;
+ }
+
+ @Override
+ public void onResponse(final JSONObject response) {
+ singleThreadNetworkHandler.submit(new Thread() {
+ @Override
+ public void run() {
+ // do other stuff here
+ BaseStatsModel mResponseObjectModel = null;
+ if (response != null) {
+ try {
+ //AppLog.d(T.STATS, response.toString());
+ mResponseObjectModel = StatsUtils.parseResponse(mEndpointName, mRequestBlogId, response);
+ if (isCacheEnabled()) {
+ int parsedBlogID = Integer.parseInt(mRequestBlogId);
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(parsedBlogID);
+ StatsTable.insertStats(StatsService.this, localTableBlogId, mTimeframe, mDate, mEndpointName,
+ mMaxResultsRequested, mPageRequested,
+ response.toString(), System.currentTimeMillis());
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, e);
+ }
+ }
+
+ EventBus.getDefault().post(
+ mEndpointName.getEndpointUpdateEvent(mRequestBlogId, mTimeframe, mDate,
+ mMaxResultsRequested, mPageRequested, mResponseObjectModel)
+ );
+
+ updateWidgetsUI(mRequestBlogId, mEndpointName, mTimeframe, mDate, mPageRequested, mResponseObjectModel);
+ checkAllRequestsFinished(currentRequest);
+ }
+ });
+ }
+
+ @Override
+ public void onErrorResponse(final VolleyError volleyError) {
+ singleThreadNetworkHandler.submit(new Thread() {
+ @Override
+ public void run() {
+ AppLog.e(T.STATS, "Error while loading Stats!");
+ StatsUtils.logVolleyErrorDetails(volleyError);
+ BaseStatsModel mResponseObjectModel = null;
+ // Check here if this is an authentication error
+ // .com authentication errors are handled automatically by the app
+ if (volleyError instanceof com.android.volley.AuthFailureError) {
+ int localId = StatsUtils.getLocalBlogIdFromRemoteBlogId(
+ Integer.parseInt(mRequestBlogId)
+ );
+ Blog blog = WordPress.wpDB.instantiateBlogByLocalId(localId);
+ if (blog != null && blog.isJetpackPowered()) {
+ // It's a kind of edge case, but the Jetpack site could have REST Disabled
+ // In that case (only used in insights for now) shows the error in the module that use the REST API
+ if (!StatsUtils.isRESTDisabledError(volleyError)) {
+ EventBus.getDefault().post(new StatsEvents.JetpackAuthError(localId));
+ }
+ }
+ }
+
+
+ EventBus.getDefault().post(new StatsEvents.SectionUpdateError(mEndpointName, mRequestBlogId, mTimeframe, mDate,
+ mMaxResultsRequested, mPageRequested, volleyError));
+
+ updateWidgetsUI(mRequestBlogId, mEndpointName, mTimeframe, mDate, mPageRequested, mResponseObjectModel);
+ checkAllRequestsFinished(currentRequest);
+ }
+ });
+ }
+ }
+
+ private void stopService() {
+ /* Stop the service if this is the current response, or mServiceBlogId is null
+ String currentServiceBlogId = getServiceBlogId();
+ if (currentServiceBlogId == null || currentServiceBlogId.equals(mRequestBlogId)) {
+ stopService();
+ }*/
+ EventBus.getDefault().post(new StatsEvents.UpdateStatusChanged(false));
+ stopSelf(mServiceStartId);
+ }
+
+
+ private void checkAllRequestsFinished(Request<JSONObject> req) {
+ synchronized (mStatsNetworkRequests) {
+ if (req != null) {
+ mStatsNetworkRequests.remove(req);
+ }
+ boolean isStillWorking = mStatsNetworkRequests.size() > 0 || singleThreadNetworkHandler.getQueue().size() > 0;
+ EventBus.getDefault().post(new StatsEvents.UpdateStatusChanged(isStillWorking));
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/SuggestionAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/SuggestionAdapter.java
new file mode 100644
index 000000000..7daa38607
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/SuggestionAdapter.java
@@ -0,0 +1,150 @@
+package org.wordpress.android.ui.suggestion.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SuggestionAdapter extends BaseAdapter implements Filterable {
+ private final LayoutInflater mInflater;
+ private Filter mSuggestionFilter;
+ private List<Suggestion> mSuggestionList;
+ private List<Suggestion> mOrigSuggestionList;
+ private int mAvatarSz;
+
+ public SuggestionAdapter(Context context) {
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small);
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void setSuggestionList(List<Suggestion> suggestionList) {
+ mOrigSuggestionList = suggestionList;
+ }
+
+ @Override
+ public int getCount() {
+ if (mSuggestionList == null) {
+ return 0;
+ }
+ return mSuggestionList.size();
+ }
+
+ @Override
+ public Suggestion getItem(int position) {
+ if (mSuggestionList == null) {
+ return null;
+ }
+ return mSuggestionList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final SuggestionViewHolder holder;
+
+ if (convertView == null || convertView.getTag() == null) {
+ convertView = mInflater.inflate(R.layout.suggestion_list_row, parent, false);
+ holder = new SuggestionViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (SuggestionViewHolder) convertView.getTag();
+ }
+
+ Suggestion suggestion = getItem(position);
+
+ if (suggestion != null) {
+ String avatarUrl = GravatarUtils.fixGravatarUrl(suggestion.getImageUrl(), mAvatarSz);
+ holder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ holder.txtUserLogin.setText("@" + suggestion.getUserLogin());
+ holder.txtDisplayName.setText(suggestion.getDisplayName());
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public Filter getFilter() {
+ if (mSuggestionFilter == null) {
+ mSuggestionFilter = new SuggestionFilter();
+ }
+
+ return mSuggestionFilter;
+ }
+
+ private class SuggestionViewHolder {
+ private final WPNetworkImageView imgAvatar;
+ private final TextView txtUserLogin;
+ private final TextView txtDisplayName;
+
+ SuggestionViewHolder(View row) {
+ imgAvatar = (WPNetworkImageView) row.findViewById(R.id.suggest_list_row_avatar);
+ txtUserLogin = (TextView) row.findViewById(R.id.suggestion_list_row_user_login_label);
+ txtDisplayName = (TextView) row.findViewById(R.id.suggestion_list_row_display_name_label);
+ }
+ }
+
+ private class SuggestionFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults results = new FilterResults();
+
+ if (mOrigSuggestionList == null) {
+ results.values = null;
+ results.count = 0;
+ }
+ else if (constraint == null || constraint.length() == 0) {
+ results.values = mOrigSuggestionList;
+ results.count = mOrigSuggestionList.size();
+ }
+ else {
+ List<Suggestion> nSuggestionList = new ArrayList<Suggestion>();
+
+ for (Suggestion suggestion : mOrigSuggestionList) {
+ String lowerCaseConstraint = constraint.toString().toLowerCase();
+ if (suggestion.getUserLogin().toLowerCase().startsWith(lowerCaseConstraint)
+ || suggestion.getDisplayName().toLowerCase().startsWith(lowerCaseConstraint)
+ || suggestion.getDisplayName().toLowerCase().contains(" " + lowerCaseConstraint))
+ nSuggestionList.add(suggestion);
+ }
+
+ results.values = nSuggestionList;
+ results.count = nSuggestionList.size();
+ }
+ return results;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void publishResults(CharSequence constraint,
+ FilterResults results) {
+ if (results.count == 0)
+ notifyDataSetInvalidated();
+ else {
+ mSuggestionList = (List<Suggestion>) results.values;
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public CharSequence convertResultToString (Object resultValue) {
+ Suggestion suggestion = (Suggestion) resultValue;
+ return suggestion.getUserLogin();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/TagSuggestionAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/TagSuggestionAdapter.java
new file mode 100644
index 000000000..539112c03
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/adapters/TagSuggestionAdapter.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.ui.suggestion.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Tag;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagSuggestionAdapter extends BaseAdapter implements Filterable {
+ private final LayoutInflater mInflater;
+ private Filter mTagFilter;
+ private List<Tag> mTagList;
+ private List<Tag> mOrigTagList;
+
+ public TagSuggestionAdapter(Context context) {
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void setTagList(List<Tag> tagList) {
+ mOrigTagList = tagList;
+ }
+
+ @Override
+ public int getCount() {
+ if (mTagList == null) {
+ return 0;
+ }
+ return mTagList.size();
+ }
+
+ @Override
+ public Tag getItem(int position) {
+ if (mTagList == null) {
+ return null;
+ }
+ return mTagList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final TagViewHolder holder;
+
+ if (convertView == null || convertView.getTag() == null) {
+ convertView = mInflater.inflate(R.layout.tag_list_row, parent, false);
+ holder = new TagViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (TagViewHolder) convertView.getTag();
+ }
+
+ Tag tag = getItem(position);
+
+ if (tag != null) {
+ holder.txtTag.setText(tag.getTag());
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public Filter getFilter() {
+ if (mTagFilter == null) {
+ mTagFilter = new TagFilter();
+ }
+
+ return mTagFilter;
+ }
+
+ private class TagViewHolder {
+ private final TextView txtTag;
+
+ TagViewHolder(View row) {
+ txtTag = (TextView) row.findViewById(R.id.tag_list_row_tag_label);
+ }
+ }
+
+ private class TagFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults results = new FilterResults();
+
+ if (mOrigTagList == null) {
+ results.values = null;
+ results.count = 0;
+ }
+ else if (constraint == null || constraint.length() == 0) {
+ results.values = mOrigTagList;
+ results.count = mOrigTagList.size();
+ }
+ else {
+ List<Tag> nTagList = new ArrayList<Tag>();
+
+ for (Tag tag : mOrigTagList) {
+ String lowerCaseConstraint = constraint.toString().toLowerCase();
+ if (tag.getTag().toLowerCase().startsWith(lowerCaseConstraint)
+ || tag.getTag().toLowerCase().contains(" " + lowerCaseConstraint))
+ nTagList.add(tag);
+ }
+
+ results.values = nTagList;
+ results.count = nTagList.size();
+ }
+ return results;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void publishResults(CharSequence constraint,
+ FilterResults results) {
+ if (results.count == 0)
+ notifyDataSetInvalidated();
+ else {
+ mTagList = (List<Tag>) results.values;
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public CharSequence convertResultToString (Object resultValue) {
+ Tag tag = (Tag) resultValue;
+ return tag.getTag();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionEvents.java
new file mode 100644
index 000000000..77323dbae
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionEvents.java
@@ -0,0 +1,17 @@
+package org.wordpress.android.ui.suggestion.service;
+
+public class SuggestionEvents {
+ public static class SuggestionNameListUpdated {
+ public final int mRemoteBlogId;
+ SuggestionNameListUpdated(int remoteBlogId) {
+ mRemoteBlogId = remoteBlogId;
+ }
+ }
+
+ public static class SuggestionTagListUpdated {
+ public final int mRemoteBlogId;
+ SuggestionTagListUpdated(int remoteBlogId) {
+ mRemoteBlogId = remoteBlogId;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionService.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionService.java
new file mode 100644
index 000000000..b174b14c4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/service/SuggestionService.java
@@ -0,0 +1,162 @@
+package org.wordpress.android.ui.suggestion.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.SuggestionTable;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.models.Tag;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+public class SuggestionService extends Service {
+ private final IBinder mBinder = new SuggestionBinder();
+ private final List<Integer> mCurrentlyRequestingSuggestionsSiteIds = new ArrayList<Integer>();
+ private final List<Integer> mCurrentlyRequestingTagsSiteIds = new ArrayList<Integer>();
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.SUGGESTION, "service created");
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return Service.START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.SUGGESTION, "service destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void updateSuggestions(final int remoteBlogId) {
+ if (mCurrentlyRequestingSuggestionsSiteIds.contains(remoteBlogId)) {
+ return;
+ }
+ mCurrentlyRequestingSuggestionsSiteIds.add(remoteBlogId);
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleSuggestionsUpdatedResponse(remoteBlogId, jsonObject);
+ removeSiteIdFromSuggestionRequestsAndStopServiceIfNecessary(remoteBlogId);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.SUGGESTION, volleyError);
+ removeSiteIdFromSuggestionRequestsAndStopServiceIfNecessary(remoteBlogId);
+ }
+ };
+
+ AppLog.d(AppLog.T.SUGGESTION, "suggestion service > updating suggestions for siteId: " + remoteBlogId);
+ String path = "/users/suggest" + "?site_id=" + remoteBlogId;
+ WordPress.getRestClientUtils().get(path, listener, errorListener);
+ }
+
+ private void handleSuggestionsUpdatedResponse(final int remoteBlogId, final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ if (jsonObject == null) {
+ return;
+ }
+
+ JSONArray jsonSuggestions = jsonObject.optJSONArray("suggestions");
+ List<Suggestion> suggestions = Suggestion.suggestionListFromJSON(jsonSuggestions, remoteBlogId);
+ if (suggestions != null) {
+ SuggestionTable.insertSuggestionsForSite(remoteBlogId, suggestions);
+ EventBus.getDefault().post(new SuggestionEvents.SuggestionNameListUpdated(remoteBlogId));
+ }
+ }
+ }.start();
+ }
+
+ private void removeSiteIdFromSuggestionRequestsAndStopServiceIfNecessary(Integer remoteBlogId) {
+ mCurrentlyRequestingSuggestionsSiteIds.remove(remoteBlogId);
+
+ // if there are no requests being made, we want to stop the service
+ if (mCurrentlyRequestingSuggestionsSiteIds.isEmpty()) {
+ AppLog.d(AppLog.T.SUGGESTION, "stopping suggestion service");
+ stopSelf();
+ }
+ }
+
+ public void updateTags(final int remoteBlogId) {
+ if (mCurrentlyRequestingTagsSiteIds.contains(remoteBlogId)) {
+ return;
+ }
+ mCurrentlyRequestingTagsSiteIds.add(remoteBlogId);
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleTagsUpdatedResponse(remoteBlogId, jsonObject);
+ removeSiteIdFromTagRequestsAndStopServiceIfNecessary(remoteBlogId);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.SUGGESTION, volleyError);
+ removeSiteIdFromTagRequestsAndStopServiceIfNecessary(remoteBlogId);
+ }
+ };
+
+ AppLog.d(AppLog.T.SUGGESTION, "suggestion service > updating tags for siteId: " + remoteBlogId);
+ String path = "/sites/" + remoteBlogId + "/tags";
+ WordPress.getRestClientUtils().get(path, listener, errorListener);
+ }
+
+ private void handleTagsUpdatedResponse(final int remoteBlogId, final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ if (jsonObject == null) {
+ return;
+ }
+
+ JSONArray jsonTags = jsonObject.optJSONArray("tags");
+ List<Tag> tags = Tag.tagListFromJSON(jsonTags, remoteBlogId);
+ if (tags != null) {
+ SuggestionTable.insertTagsForSite(remoteBlogId, tags);
+ EventBus.getDefault().post(new SuggestionEvents.SuggestionTagListUpdated(remoteBlogId));
+ }
+ }
+ }.start();
+ }
+
+ private void removeSiteIdFromTagRequestsAndStopServiceIfNecessary(Integer remoteBlogId) {
+ mCurrentlyRequestingTagsSiteIds.remove(remoteBlogId);
+
+ // if there are no requests being made, we want to stop the service
+ if (mCurrentlyRequestingTagsSiteIds.isEmpty()) {
+ AppLog.d(AppLog.T.SUGGESTION, "stopping suggestion service");
+ stopSelf();
+ }
+ }
+
+ public class SuggestionBinder extends Binder {
+ public SuggestionService getService() {
+ return SuggestionService.this;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionServiceConnectionManager.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionServiceConnectionManager.java
new file mode 100644
index 000000000..bcbdebe30
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionServiceConnectionManager.java
@@ -0,0 +1,55 @@
+package org.wordpress.android.ui.suggestion.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import org.wordpress.android.ui.suggestion.service.SuggestionService;
+
+public class SuggestionServiceConnectionManager implements ServiceConnection {
+
+ private final Context mContext;
+ private final int mRemoteBlogId;
+ private boolean mAttemptingToBind = false;
+ private boolean mBindCalled = false;
+
+ public SuggestionServiceConnectionManager(Context context, int remoteBlogId) {
+ mContext = context;
+ mRemoteBlogId = remoteBlogId;
+ }
+
+ public void bindToService() {
+ if (!mAttemptingToBind) {
+ mAttemptingToBind = true;
+ mBindCalled = true;
+ Intent intent = new Intent(mContext, SuggestionService.class);
+ mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
+ }
+ }
+
+ public void unbindFromService() {
+ mAttemptingToBind = false;
+ if (mBindCalled) {
+ mContext.unbindService(this);
+ mBindCalled = false;
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ SuggestionService.SuggestionBinder b = (SuggestionService.SuggestionBinder) iBinder;
+ SuggestionService suggestionService = b.getService();
+
+ suggestionService.updateSuggestions(mRemoteBlogId);
+ suggestionService.updateTags(mRemoteBlogId);
+
+ mAttemptingToBind = false;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ // noop
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionTokenizer.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionTokenizer.java
new file mode 100644
index 000000000..7dbf8da8e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionTokenizer.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.ui.suggestion.util;
+
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.widget.MultiAutoCompleteTextView;
+
+public class SuggestionTokenizer implements MultiAutoCompleteTextView.Tokenizer {
+
+ @Override
+ public CharSequence terminateToken(CharSequence text) {
+ int i = text.length();
+ while (i > 0 && text.charAt(i - 1) == ' ') {
+ i--;
+ }
+ if (text instanceof Spanned) {
+ SpannableString sp = new SpannableString(text + " ");
+ TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0);
+ return sp;
+ } else {
+ return text + " ";
+ }
+ }
+
+ @Override
+ public int findTokenStart(CharSequence text, int cursor) {
+ int i = cursor;
+ while (i > 0 && text.charAt(i - 1) != '@') {
+ i--;
+ }
+
+ if (i < 1 || text.charAt(i - 1) != '@') {
+ return cursor;
+ }
+
+ return i;
+ }
+
+ @Override
+ public int findTokenEnd(CharSequence text, int cursor) {
+ int i = cursor;
+ int len = text.length();
+ while (i < len) {
+ if (text.charAt(i) == ' ') {
+ return i;
+ } else {
+ i++;
+ }
+ }
+ return len;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionUtils.java
new file mode 100644
index 000000000..be0e7fd00
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/util/SuggestionUtils.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.suggestion.util;
+
+import android.content.Context;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.SuggestionTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.models.Tag;
+import org.wordpress.android.ui.suggestion.adapters.SuggestionAdapter;
+import org.wordpress.android.ui.suggestion.adapters.TagSuggestionAdapter;
+
+import java.util.List;
+
+public class SuggestionUtils {
+
+ public static SuggestionAdapter setupSuggestions(final int remoteBlogId, Context context, SuggestionServiceConnectionManager serviceConnectionManager) {
+ Blog blog = WordPress.wpDB.getBlogForDotComBlogId(Integer.toString(remoteBlogId));
+ boolean isDotComFlag = (blog != null && blog.isDotcomFlag());
+
+ return SuggestionUtils.setupSuggestions(remoteBlogId, context, serviceConnectionManager, isDotComFlag);
+ }
+
+ public static SuggestionAdapter setupSuggestions(final int remoteBlogId, Context context, SuggestionServiceConnectionManager serviceConnectionManager, boolean isDotcomFlag) {
+ if (!isDotcomFlag) {
+ return null;
+ }
+
+ SuggestionAdapter suggestionAdapter = new SuggestionAdapter(context);
+
+ List<Suggestion> suggestions = SuggestionTable.getSuggestionsForSite(remoteBlogId);
+ // if the suggestions are not stored yet, we want to trigger an update for it
+ if (suggestions.isEmpty()) {
+ serviceConnectionManager.bindToService();
+ }
+ suggestionAdapter.setSuggestionList(suggestions);
+ return suggestionAdapter;
+ }
+
+ public static TagSuggestionAdapter setupTagSuggestions(final int remoteBlogId, Context context, SuggestionServiceConnectionManager serviceConnectionManager) {
+ Blog blog = WordPress.wpDB.getBlogForDotComBlogId(Integer.toString(remoteBlogId));
+ boolean isDotComFlag = (blog != null && blog.isDotcomFlag());
+
+ return SuggestionUtils.setupTagSuggestions(remoteBlogId, context, serviceConnectionManager, isDotComFlag);
+ }
+
+ public static TagSuggestionAdapter setupTagSuggestions(final int remoteBlogId, Context context, SuggestionServiceConnectionManager serviceConnectionManager, boolean isDotcomFlag) {
+ if (!isDotcomFlag) {
+ return null;
+ }
+
+ TagSuggestionAdapter tagSuggestionAdapter = new TagSuggestionAdapter(context);
+
+ List<Tag> tags = SuggestionTable.getTagsForSite(remoteBlogId);
+ // if the tags are not stored yet, we want to trigger an update for it
+ if (tags.isEmpty()) {
+ serviceConnectionManager.bindToService();
+ }
+ tagSuggestionAdapter.setTagList(tags);
+ return tagSuggestionAdapter;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java
new file mode 100644
index 000000000..feff3d5a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java
@@ -0,0 +1,546 @@
+package org.wordpress.android.ui.themes;
+
+import android.app.AlertDialog;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest.ErrorListener;
+import com.wordpress.rest.RestRequest.Listener;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Theme;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.themes.ThemeBrowserFragment.ThemeBrowserFragmentCallback;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.widgets.WPAlertDialogFragment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The theme browser.
+ */
+public class ThemeBrowserActivity extends AppCompatActivity implements ThemeBrowserFragmentCallback {
+ public static final int THEME_FETCH_MAX = 100;
+ public static final int ACTIVATE_THEME = 1;
+ public static final String THEME_ID = "theme_id";
+ private static final String IS_IN_SEARCH_MODE = "is_in_search_mode";
+ private static final String ALERT_TAB = "alert";
+
+ private boolean mFetchingThemes = false;
+ private boolean mIsRunning;
+ private ThemeBrowserFragment mThemeBrowserFragment;
+ private ThemeSearchFragment mThemeSearchFragment;
+ private Theme mCurrentTheme;
+ private boolean mIsInSearchMode;
+
+ public static boolean isAccessible() {
+ // themes are only accessible to admin wordpress.com users
+ Blog blog = WordPress.getCurrentBlog();
+ return (blog != null && blog.isAdmin() && blog.isDotcomFlag());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (WordPress.wpDB == null) {
+ Toast.makeText(this, R.string.fatal_db_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.theme_browser_activity);
+ setCurrentThemeFromDB();
+
+ if (savedInstanceState == null) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_ACCESSED_THEMES_BROWSER);
+ mThemeBrowserFragment = new ThemeBrowserFragment();
+ mThemeSearchFragment = new ThemeSearchFragment();
+ addBrowserFragment();
+ }
+ showToolbar();
+ }
+
+ private void setCurrentThemeFromDB() {
+ mCurrentTheme = WordPress.wpDB.getCurrentTheme(getBlogId());
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ showCorrectToolbar();
+ mIsRunning = true;
+ ActivityId.trackLastActivity(ActivityId.THEMES);
+
+ fetchThemesIfNoneAvailable();
+ fetchPurchasedThemes();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsRunning = false;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mIsInSearchMode = savedInstanceState.getBoolean(IS_IN_SEARCH_MODE);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(IS_IN_SEARCH_MODE, mIsInSearchMode);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int i = item.getItemId();
+ if (i == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ FragmentManager fm = getFragmentManager();
+ if (fm.getBackStackEntryCount() > 0) {
+ fm.popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == ACTIVATE_THEME && resultCode == RESULT_OK && data != null) {
+ String themeId = data.getStringExtra(THEME_ID);
+ if (!TextUtils.isEmpty(themeId)) {
+ activateTheme(themeId);
+ }
+ }
+ }
+
+ public void setIsInSearchMode(boolean isInSearchMode) {
+ mIsInSearchMode = isInSearchMode;
+ }
+
+ public void fetchThemes() {
+ if (mFetchingThemes) {
+ return;
+ }
+ String siteId = getBlogId();
+ mFetchingThemes = true;
+ int page = 1;
+ if (mThemeBrowserFragment != null) {
+ page = mThemeBrowserFragment.getPage();
+ }
+ WordPress.getRestClientUtilsV1_2().getFreeThemes(siteId, THEME_FETCH_MAX, page, new Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ new FetchThemesTask().execute(response);
+ }
+ }, new ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError response) {
+ if (response.toString().equals(AuthFailureError.class.getName())) {
+ String errorTitle = getString(R.string.theme_auth_error_title);
+ String errorMsg = getString(R.string.theme_auth_error_message);
+
+ if (mIsRunning) {
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ WPAlertDialogFragment fragment = WPAlertDialogFragment.newAlertDialog(errorMsg,
+ errorTitle);
+ ft.add(fragment, ALERT_TAB);
+ ft.commitAllowingStateLoss();
+ }
+ AppLog.d(T.THEMES, getString(R.string.theme_auth_error_authenticate));
+ } else {
+ Toast.makeText(ThemeBrowserActivity.this, R.string.theme_fetch_failed, Toast.LENGTH_LONG)
+ .show();
+ AppLog.d(T.THEMES, getString(R.string.theme_fetch_failed) + ": " + response.toString());
+ }
+ mFetchingThemes = false;
+ }
+ }
+ );
+ }
+
+ public void searchThemes(String searchTerm) {
+ String siteId = getBlogId();
+ mFetchingThemes = true;
+ int page = 1;
+ if (mThemeSearchFragment != null) {
+ page = mThemeSearchFragment.getPage();
+ }
+
+ WordPress.getRestClientUtilsV1_2().getFreeSearchThemes(siteId, THEME_FETCH_MAX, page, searchTerm, new Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ new FetchThemesTask().execute(response);
+ }
+ }, new ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError response) {
+ if (response.toString().equals(AuthFailureError.class.getName())) {
+ String errorTitle = getString(R.string.theme_auth_error_title);
+ String errorMsg = getString(R.string.theme_auth_error_message);
+
+ if (mIsRunning) {
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ WPAlertDialogFragment fragment = WPAlertDialogFragment.newAlertDialog(errorMsg,
+ errorTitle);
+ ft.add(fragment, ALERT_TAB);
+ ft.commitAllowingStateLoss();
+ }
+ AppLog.d(T.THEMES, getString(R.string.theme_auth_error_authenticate));
+ }
+ mFetchingThemes = false;
+ }
+ }
+ );
+ }
+
+ public void fetchCurrentTheme() {
+ final String siteId = getBlogId();
+
+ WordPress.getRestClientUtilsV1_1().getCurrentTheme(siteId, new Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ try {
+ mCurrentTheme = Theme.fromJSONV1_1(response);
+ if (mCurrentTheme != null) {
+ mCurrentTheme.setIsCurrent(true);
+ mCurrentTheme.save();
+ WordPress.wpDB.setCurrentTheme(siteId, mCurrentTheme.getId());
+ if (mThemeBrowserFragment != null) {
+ mThemeBrowserFragment.setRefreshing(false);
+ if (mThemeBrowserFragment.getCurrentThemeTextView() != null) {
+ mThemeBrowserFragment.getCurrentThemeTextView().setText(mCurrentTheme.getName());
+ mThemeBrowserFragment.setCurrentThemeId(mCurrentTheme.getId());
+ }
+ }
+ if (mThemeSearchFragment != null && mThemeSearchFragment.isVisible()) {
+ mThemeSearchFragment.setRefreshing(false);
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.THEMES, e);
+ }
+ }
+ }, new ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError response) {
+ String themeId = WordPress.wpDB.getCurrentThemeId(siteId);
+ mCurrentTheme = WordPress.wpDB.getTheme(siteId, themeId);
+ if (mCurrentTheme != null && mThemeBrowserFragment != null) {
+ if (mThemeBrowserFragment.getCurrentThemeTextView() != null) {
+ mThemeBrowserFragment.getCurrentThemeTextView().setText(mCurrentTheme.getName());
+ mThemeBrowserFragment.setCurrentThemeId(mCurrentTheme.getId());
+ }
+ }
+ }
+ }
+ );
+ }
+
+ protected Theme getCurrentTheme() {
+ return mCurrentTheme;
+ }
+
+ protected void setThemeBrowserFragment(ThemeBrowserFragment themeBrowserFragment) {
+ mThemeBrowserFragment = themeBrowserFragment;
+ }
+
+ protected void setThemeSearchFragment(ThemeSearchFragment themeSearchFragment) {
+ mThemeSearchFragment = themeSearchFragment;
+ }
+
+ private String getBlogId() {
+ if (WordPress.getCurrentBlog() == null)
+ return "0";
+ return String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+ }
+
+ private void fetchThemesIfNoneAvailable() {
+ if (NetworkUtils.isNetworkAvailable(this) && WordPress.getCurrentBlog() != null
+ && WordPress.wpDB.getThemeCount(getBlogId()) == 0) {
+ fetchThemes();
+
+ //do not interact with theme browser fragment if we are in search mode
+ if (!mIsInSearchMode) {
+ mThemeBrowserFragment.setRefreshing(true);
+ }
+ }
+ }
+
+ private void fetchPurchasedThemes() {
+ if (NetworkUtils.isNetworkAvailable(this) && WordPress.getCurrentBlog() != null) {
+ WordPress.getRestClientUtilsV1_1().getPurchasedThemes(getBlogId(), new Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ new FetchThemesTask().execute(response);
+ }
+ }, new ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.d(T.THEMES, error.getMessage());
+ }
+ });
+
+ //do not interact with theme browser fragment if we are in search mode
+ if (!mIsInSearchMode) {
+ mThemeBrowserFragment.setRefreshing(true);
+ }
+ }
+ }
+
+ protected void showToolbar() {
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(R.string.themes);
+ findViewById(R.id.toolbar).setVisibility(View.VISIBLE);
+ findViewById(R.id.toolbar_search).setVisibility(View.GONE);
+ }
+
+ private void showCorrectToolbar() {
+ if (mIsInSearchMode) {
+ showSearchToolbar();
+ } else {
+ hideSearchToolbar();
+ }
+ }
+
+ private void showSearchToolbar() {
+ Toolbar toolbarSearch = (Toolbar) findViewById(R.id.toolbar_search);
+ setSupportActionBar(toolbarSearch);
+ toolbarSearch.setTitle("");
+ findViewById(R.id.toolbar).setVisibility(View.GONE);
+ findViewById(R.id.toolbar_search).setVisibility(View.VISIBLE);
+ }
+
+ private void hideSearchToolbar() {
+ findViewById(R.id.toolbar).setVisibility(View.VISIBLE);
+ findViewById(R.id.toolbar_search).setVisibility(View.GONE);
+ }
+
+ private void addBrowserFragment() {
+ if (mThemeBrowserFragment == null) {
+ mThemeBrowserFragment = new ThemeBrowserFragment();
+ }
+ showToolbar();
+ FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ fragmentTransaction.add(R.id.theme_browser_container, mThemeBrowserFragment);
+ fragmentTransaction.commit();
+ }
+
+ private void addSearchFragment() {
+ if (mThemeSearchFragment == null) {
+ mThemeSearchFragment = new ThemeSearchFragment();
+ }
+ showSearchToolbar();
+ FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ fragmentTransaction.replace(R.id.theme_browser_container, mThemeSearchFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ }
+
+ private void activateTheme(final String themeId) {
+ final String siteId = getBlogId();
+ final String newThemeId = themeId;
+
+ WordPress.getRestClientUtils().setTheme(siteId, themeId, new Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ WordPress.wpDB.setCurrentTheme(siteId, newThemeId);
+ Theme newTheme = WordPress.wpDB.getTheme(siteId, newThemeId);
+
+ Map<String, Object> themeProperties = new HashMap<>();
+ themeProperties.put(THEME_ID, themeId);
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_CHANGED_THEME, themeProperties);
+
+ if (!isFinishing()) {
+ showAlertDialogOnNewSettingNewTheme(newTheme);
+ fetchCurrentTheme();
+ }
+ }
+ }, new ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ Toast.makeText(getApplicationContext(), R.string.theme_activation_error, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ private void showAlertDialogOnNewSettingNewTheme(Theme newTheme) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+
+ String thanksMessage = String.format(getString(R.string.theme_prompt), newTheme.getName());
+ if (!newTheme.getAuthor().isEmpty()) {
+ thanksMessage = thanksMessage + " " + String.format(getString(R.string.theme_by_author_prompt_append), newTheme.getAuthor());
+ }
+
+ dialogBuilder.setMessage(thanksMessage);
+ dialogBuilder.setNegativeButton(R.string.theme_done, null);
+ dialogBuilder.setPositiveButton(R.string.theme_manage_site, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+
+ AlertDialog alertDialog = dialogBuilder.create();
+ alertDialog.show();
+ }
+
+ private void startWebActivity(String themeId, ThemeWebActivity.ThemeWebActivityType type) {
+ String toastText = getString(R.string.no_network_message);
+
+ if (NetworkUtils.isNetworkAvailable(this)) {
+ if (mCurrentTheme != null && !TextUtils.isEmpty(themeId)) {
+ boolean isCurrentTheme = mCurrentTheme.getId().equals(themeId);
+ Map<String, Object> themeProperties = new HashMap<>();
+ themeProperties.put(THEME_ID, themeId);
+
+ switch (type) {
+ case PREVIEW:
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_PREVIEWED_SITE, themeProperties);
+ break;
+ case DEMO:
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_DEMO_ACCESSED, themeProperties);
+ break;
+ case DETAILS:
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_DETAILS_ACCESSED, themeProperties);
+ break;
+ case SUPPORT:
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_SUPPORT_ACCESSED, themeProperties);
+ break;
+ }
+ ThemeWebActivity.openTheme(this, themeId, type, isCurrentTheme);
+ return;
+ } else {
+ toastText = getString(R.string.could_not_load_theme);
+ }
+ }
+
+ Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onActivateSelected(String themeId) {
+ activateTheme(themeId);
+ }
+
+ @Override
+ public void onTryAndCustomizeSelected(String themeId) {
+ startWebActivity(themeId, ThemeWebActivity.ThemeWebActivityType.PREVIEW);
+ }
+
+ @Override
+ public void onViewSelected(String themeId) {
+ startWebActivity(themeId, ThemeWebActivity.ThemeWebActivityType.DEMO);
+ }
+
+ @Override
+ public void onDetailsSelected(String themeId) {
+ startWebActivity(themeId, ThemeWebActivity.ThemeWebActivityType.DETAILS);
+ }
+
+ @Override
+ public void onSupportSelected(String themeId) {
+ startWebActivity(themeId, ThemeWebActivity.ThemeWebActivityType.SUPPORT);
+ }
+
+ @Override
+ public void onSearchClicked() {
+ mIsInSearchMode = true;
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.THEMES_ACCESSED_SEARCH);
+ addSearchFragment();
+ }
+
+ public class FetchThemesTask extends AsyncTask<JSONObject, Void, ArrayList<Theme>> {
+ @Override
+ protected ArrayList<Theme> doInBackground(JSONObject... args) {
+ JSONObject response = args[0];
+ final ArrayList<Theme> themes = new ArrayList<>();
+
+ if (response != null) {
+ JSONArray array;
+ try {
+ array = response.getJSONArray("themes");
+
+ if (array != null) {
+ int count = array.length();
+ for (int i = 0; i < count; i++) {
+ JSONObject object = array.getJSONObject(i);
+ Theme theme = Theme.fromJSONV1_2(object);
+ if (theme != null) {
+ theme.save();
+ themes.add(theme);
+ }
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.THEMES, e);
+ }
+ }
+
+ fetchCurrentTheme();
+
+ if (themes.size() > 0) {
+ return themes;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(final ArrayList<Theme> result) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mFetchingThemes = false;
+ if (mThemeBrowserFragment != null && mThemeBrowserFragment.isVisible()) {
+ mThemeBrowserFragment.getEmptyTextView().setText(R.string.theme_no_search_result_found);
+ mThemeBrowserFragment.setRefreshing(false);
+ } else if (mThemeSearchFragment != null && mThemeSearchFragment.isVisible()) {
+ mThemeSearchFragment.getEmptyTextView().setText(R.string.theme_no_search_result_found);
+ mThemeSearchFragment.setRefreshing(false);
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java
new file mode 100644
index 000000000..7b04d7c6f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java
@@ -0,0 +1,205 @@
+package org.wordpress.android.ui.themes;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.support.v7.widget.CardView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Theme;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.widgets.HeaderGridView;
+
+/**
+ * Adapter for the {@link ThemeBrowserFragment}'s listview
+ *
+ */
+public class ThemeBrowserAdapter extends CursorAdapter {
+ private static final String THEME_IMAGE_PARAMETER = "?w=";
+ private final LayoutInflater mInflater;
+ private final ThemeBrowserFragment.ThemeBrowserFragmentCallback mCallback;
+ private int mViewWidth;
+
+ public ThemeBrowserAdapter(Context context, Cursor c, boolean autoRequery, ThemeBrowserFragment.ThemeBrowserFragmentCallback callback) {
+ super(context, c, autoRequery);
+ mInflater = LayoutInflater.from(context);
+ mCallback = callback;
+ mViewWidth = AppPrefs.getThemeImageSizeWidth();
+ }
+
+ private static class ThemeViewHolder {
+ private final CardView cardView;
+ private final NetworkImageView imageView;
+ private final TextView nameView;
+ private final TextView activeView;
+ private final TextView priceView;
+ private final ImageButton imageButton;
+ private final FrameLayout frameLayout;
+ private final RelativeLayout detailsView;
+
+ ThemeViewHolder(View view) {
+ cardView = (CardView) view.findViewById(R.id.theme_grid_card);
+ imageView = (NetworkImageView) view.findViewById(R.id.theme_grid_item_image);
+ nameView = (TextView) view.findViewById(R.id.theme_grid_item_name);
+ priceView = (TextView) view.findViewById(R.id.theme_grid_item_price);
+ activeView = (TextView) view.findViewById(R.id.theme_grid_item_active);
+ imageButton = (ImageButton) view.findViewById(R.id.theme_grid_item_image_button);
+ frameLayout = (FrameLayout) view.findViewById(R.id.theme_grid_item_image_layout);
+ detailsView = (RelativeLayout) view.findViewById(R.id.theme_grid_item_details);
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.theme_grid_item, parent, false);
+
+ configureThemeImageSize(parent);
+ ThemeViewHolder themeViewHolder = new ThemeViewHolder(view);
+ view.setTag(themeViewHolder);
+
+ return view;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final ThemeViewHolder themeViewHolder = (ThemeViewHolder) view.getTag();
+
+ final String screenshotURL = cursor.getString(cursor.getColumnIndex(Theme.SCREENSHOT));
+ final String name = cursor.getString(cursor.getColumnIndex(Theme.NAME));
+ final String price = cursor.getString(cursor.getColumnIndex(Theme.PRICE));
+ final String themeId = cursor.getString(cursor.getColumnIndex(Theme.ID));
+ final boolean isCurrent = cursor.getInt(cursor.getColumnIndex(Theme.IS_CURRENT)) == 1;
+ final boolean isPremium = !price.isEmpty();
+
+ themeViewHolder.nameView.setText(name);
+ themeViewHolder.priceView.setText(price);
+
+ configureImageView(themeViewHolder, screenshotURL, themeId, isCurrent);
+ configureImageButton(context, themeViewHolder, themeId, isPremium, isCurrent);
+ configureCardView(context, themeViewHolder, isCurrent);
+ }
+
+ private void configureCardView(Context context, ThemeViewHolder themeViewHolder, boolean isCurrent) {
+ Resources resources = context.getResources();
+ if (isCurrent) {
+ themeViewHolder.detailsView.setBackgroundColor(resources.getColor(R.color.blue_wordpress));
+ themeViewHolder.nameView.setTextColor(resources.getColor(R.color.white));
+ themeViewHolder.activeView.setVisibility(View.VISIBLE);
+ themeViewHolder.cardView.setCardBackgroundColor(resources.getColor(R.color.blue_wordpress));
+ } else {
+ themeViewHolder.detailsView.setBackgroundColor(resources.getColor(
+ android.support.v7.cardview.R.color.cardview_light_background));
+ themeViewHolder.nameView.setTextColor(resources.getColor(R.color.black));
+ themeViewHolder.activeView.setVisibility(View.GONE);
+ themeViewHolder.cardView.setCardBackgroundColor(resources.getColor(
+ android.support.v7.cardview.R.color.cardview_light_background));
+ }
+ }
+
+ private void configureImageView(ThemeViewHolder themeViewHolder, String screenshotURL, final String themeId, final boolean isCurrent) {
+ String requestURL = (String) themeViewHolder.imageView.getTag();
+ if (requestURL == null) {
+ requestURL = screenshotURL;
+ themeViewHolder.imageView.setDefaultImageResId(R.drawable.theme_loading);
+ themeViewHolder.imageView.setTag(requestURL);
+ }
+
+ if (!requestURL.equals(screenshotURL)) {
+ requestURL = screenshotURL;
+ }
+
+ themeViewHolder.imageView.setImageUrl(requestURL + THEME_IMAGE_PARAMETER + mViewWidth, WordPress.imageLoader);
+ themeViewHolder.frameLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (isCurrent) {
+ mCallback.onTryAndCustomizeSelected(themeId);
+ } else {
+ mCallback.onViewSelected(themeId);
+ }
+ }
+ });
+ }
+
+ private void configureImageButton(Context context, ThemeViewHolder themeViewHolder, final String themeId, final boolean isPremium, boolean isCurrent) {
+ final PopupMenu popupMenu = new PopupMenu(context, themeViewHolder.imageButton);
+ popupMenu.getMenuInflater().inflate(R.menu.theme_more, popupMenu.getMenu());
+
+ configureMenuForTheme(popupMenu.getMenu(), isCurrent);
+
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ int i = item.getItemId();
+ if (i == R.id.menu_activate) {
+ if (isPremium) {
+ mCallback.onDetailsSelected(themeId);
+ } else {
+ mCallback.onActivateSelected(themeId);
+ }
+ } else if (i == R.id.menu_try_and_customize) {
+ mCallback.onTryAndCustomizeSelected(themeId);
+ } else if (i == R.id.menu_view) {
+ mCallback.onViewSelected(themeId);
+ } else if (i == R.id.menu_details) {
+ mCallback.onDetailsSelected(themeId);
+ } else {
+ mCallback.onSupportSelected(themeId);
+ }
+
+ return true;
+ }
+ });
+ themeViewHolder.imageButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ popupMenu.show();
+ }
+ });
+ }
+
+ private void configureMenuForTheme(Menu menu, boolean isCurrent) {
+ MenuItem activate = menu.findItem(R.id.menu_activate);
+ MenuItem customize = menu.findItem(R.id.menu_try_and_customize);
+ MenuItem view = menu.findItem(R.id.menu_view);
+
+ if (activate != null) {
+ activate.setVisible(!isCurrent);
+ }
+ if (customize != null) {
+ if (isCurrent) {
+ customize.setTitle(R.string.customize);
+ } else {
+ customize.setTitle(R.string.theme_try_and_customize);
+ }
+ }
+ if (view != null) {
+ view.setVisible(!isCurrent);
+ }
+ }
+
+ private void configureThemeImageSize(ViewGroup parent) {
+ HeaderGridView gridView = (HeaderGridView) parent.findViewById(R.id.theme_listview);
+ int numColumns = gridView.getNumColumns();
+ int screenWidth = gridView.getWidth();
+ int imageWidth = screenWidth / numColumns;
+ if (imageWidth > mViewWidth) {
+ mViewWidth = imageWidth;
+ AppPrefs.setThemeImageSizeWidth(mViewWidth);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.java
new file mode 100644
index 000000000..10df605cc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.java
@@ -0,0 +1,383 @@
+package org.wordpress.android.ui.themes;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.RecyclerListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader.ImageContainer;
+import com.android.volley.toolbox.ImageLoader.ImageListener;
+import com.android.volley.toolbox.NetworkImageView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Theme;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+import org.wordpress.android.widgets.HeaderGridView;
+
+/**
+ * A fragment display the themes on a grid view.
+ */
+public class ThemeBrowserFragment extends Fragment implements RecyclerListener, AdapterView.OnItemSelectedListener, AbsListView.OnScrollListener {
+ public interface ThemeBrowserFragmentCallback {
+ void onActivateSelected(String themeId);
+ void onTryAndCustomizeSelected(String themeId);
+ void onViewSelected(String themeId);
+ void onDetailsSelected(String themeId);
+ void onSupportSelected(String themeId);
+ void onSearchClicked();
+ }
+
+ protected static final String BUNDLE_PAGE = "BUNDLE_PAGE";
+ protected static final int THEME_FILTER_ALL_INDEX = 0;
+ protected static final int THEME_FILTER_FREE_INDEX = 1;
+ protected static final int THEME_FILTER_PREMIUM_INDEX = 2;
+
+ protected SwipeToRefreshHelper mSwipeToRefreshHelper;
+ protected ThemeBrowserActivity mThemeBrowserActivity;
+ private String mCurrentThemeId;
+ private HeaderGridView mGridView;
+ private RelativeLayout mEmptyView;
+ private TextView mNoResultText;
+ private TextView mCurrentThemeTextView;
+ private ThemeBrowserAdapter mAdapter;
+ private Spinner mSpinner;
+ private ThemeBrowserFragmentCallback mCallback;
+ private int mPage = 1;
+ private boolean mShouldRefreshOnStart;
+ private TextView mEmptyTextView;
+ private ProgressBar mProgressBar;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mCallback = (ThemeBrowserFragmentCallback) activity;
+ mThemeBrowserActivity = (ThemeBrowserActivity) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement ThemeBrowserFragmentCallback");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallback = null;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.theme_browser_fragment, container, false);
+
+ setRetainInstance(true);
+ mNoResultText = (TextView) view.findViewById(R.id.theme_no_search_result_text);
+ mEmptyTextView = (TextView) view.findViewById(R.id.text_empty);
+ mEmptyView = (RelativeLayout) view.findViewById(R.id.empty_view);
+ mProgressBar = (ProgressBar) view.findViewById(R.id.theme_loading_progress_bar);
+
+ configureGridView(inflater, view);
+ configureSwipeToRefresh(view);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (this instanceof ThemeSearchFragment) {
+ mThemeBrowserActivity.setThemeSearchFragment((ThemeSearchFragment) this);
+ } else {
+ mThemeBrowserActivity.setThemeBrowserFragment(this);
+ }
+ Cursor cursor = fetchThemes(getSpinnerPosition());
+
+ if (cursor == null) {
+ return;
+ }
+
+ mAdapter = new ThemeBrowserAdapter(mThemeBrowserActivity, cursor, false, mCallback);
+ setEmptyViewVisible(mAdapter.getCount() == 0);
+ mGridView.setAdapter(mAdapter);
+ restoreState(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ mThemeBrowserActivity.fetchCurrentTheme();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mGridView != null) {
+ outState.putInt(BUNDLE_PAGE, mPage);
+ }
+ }
+
+ public TextView getEmptyTextView() {
+ return mEmptyTextView;
+ }
+
+ public TextView getCurrentThemeTextView() {
+ return mCurrentThemeTextView;
+ }
+
+ public void setCurrentThemeId(String currentThemeId) {
+ mCurrentThemeId = currentThemeId;
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ protected void addHeaderViews(LayoutInflater inflater) {
+ addMainHeader(inflater);
+ configureAndAddSearchHeader(inflater);
+ }
+
+ protected void configureSwipeToRefresh(View view) {
+ mSwipeToRefreshHelper = new SwipeToRefreshHelper(mThemeBrowserActivity, (CustomSwipeRefreshLayout) view.findViewById(
+ R.id.ptr_layout), new RefreshListener() {
+ @Override
+ public void onRefreshStarted() {
+ if (!isAdded()) {
+ return;
+ }
+ if (!NetworkUtils.checkConnection(mThemeBrowserActivity)) {
+ mSwipeToRefreshHelper.setRefreshing(false);
+ mEmptyTextView.setText(R.string.no_network_title);
+ return;
+ }
+ mThemeBrowserActivity.fetchThemes();
+ }
+ });
+ mSwipeToRefreshHelper.setRefreshing(mShouldRefreshOnStart);
+ }
+
+ private void configureGridView(LayoutInflater inflater, View view) {
+ mGridView = (HeaderGridView) view.findViewById(R.id.theme_listview);
+ addHeaderViews(inflater);
+ mGridView.setRecyclerListener(this);
+ mGridView.setOnScrollListener(this);
+ }
+
+ private void addMainHeader(LayoutInflater inflater) {
+ View header = inflater.inflate(R.layout.theme_grid_cardview_header, null);
+ mCurrentThemeTextView = (TextView) header.findViewById(R.id.header_theme_text);
+
+ setThemeNameIfAlreadyAvailable();
+ mThemeBrowserActivity.fetchCurrentTheme();
+ LinearLayout customize = (LinearLayout) header.findViewById(R.id.customize);
+ customize.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.onTryAndCustomizeSelected(mCurrentThemeId);
+ }
+ });
+
+ LinearLayout details = (LinearLayout) header.findViewById(R.id.details);
+ details.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.onDetailsSelected(mCurrentThemeId);
+ }
+ });
+
+ LinearLayout support = (LinearLayout) header.findViewById(R.id.support);
+ support.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.onSupportSelected(mCurrentThemeId);
+ }
+ });
+
+ mGridView.addHeaderView(header);
+ }
+
+ private void setThemeNameIfAlreadyAvailable() {
+ Theme currentTheme = mThemeBrowserActivity.getCurrentTheme();
+ if (currentTheme != null) {
+ mCurrentThemeTextView.setText(currentTheme.getName());
+ }
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mShouldRefreshOnStart = refreshing;
+ if (mSwipeToRefreshHelper != null) {
+ mSwipeToRefreshHelper.setRefreshing(refreshing);
+ if (!refreshing) {
+ refreshView(getSpinnerPosition());
+ }
+ }
+ }
+
+ private void configureAndAddSearchHeader(LayoutInflater inflater) {
+ View headerSearch = inflater.inflate(R.layout.theme_grid_cardview_header_search, null);
+ headerSearch.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.onSearchClicked();
+ }
+ });
+ configureFilterSpinner(headerSearch);
+ ImageButton searchButton = (ImageButton) headerSearch.findViewById(R.id.theme_search);
+ searchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.onSearchClicked();
+ }
+ });
+ }
+
+ private void configureFilterSpinner(View headerSearch) {
+ mSpinner = (Spinner) headerSearch.findViewById(R.id.theme_filter_spinner);
+ ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mThemeBrowserActivity, R.array.themes_filter_array, android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mSpinner.setAdapter(adapter);
+ mGridView.addHeaderView(headerSearch);
+ mSpinner.setOnItemSelectedListener(this);
+ }
+
+ private void restoreState(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mPage = savedInstanceState.getInt(BUNDLE_PAGE, 1);
+ }
+ }
+
+ private void setEmptyViewVisible(boolean visible) {
+ if (getView() == null || !isAdded()) {
+ return;
+ }
+ mEmptyView.setVisibility(visible ? RelativeLayout.VISIBLE : RelativeLayout.GONE);
+ mGridView.setVisibility(visible ? View.GONE : View.VISIBLE);
+ if (visible && !NetworkUtils.isNetworkAvailable(mThemeBrowserActivity)) {
+ mEmptyTextView.setText(R.string.no_network_title);
+ }
+ }
+
+ /**
+ * Fetch themes for a given ThemeFilterType.
+ *
+ * @return a db Cursor or null if current blog is null
+ */
+ protected Cursor fetchThemes(int position) {
+ if (WordPress.getCurrentBlog() == null) {
+ return null;
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+ switch (position) {
+ case THEME_FILTER_PREMIUM_INDEX:
+ return WordPress.wpDB.getThemesPremium(blogId);
+ case THEME_FILTER_ALL_INDEX:
+ return WordPress.wpDB.getThemesAll(blogId);
+ case THEME_FILTER_FREE_INDEX:
+ default:
+ return WordPress.wpDB.getThemesFree(blogId);
+ }
+ }
+
+ protected void refreshView(int position) {
+ Cursor cursor = fetchThemes(position);
+ if (cursor == null) {
+ return;
+ }
+ if (mAdapter == null) {
+ mAdapter = new ThemeBrowserAdapter(mThemeBrowserActivity, cursor, false, mCallback);
+ }
+ if (mNoResultText.isShown()) {
+ mNoResultText.setVisibility(View.GONE);
+ }
+ mAdapter.changeCursor(cursor);
+ mAdapter.notifyDataSetChanged();
+ setEmptyViewVisible(mAdapter.getCount() == 0);
+ mProgressBar.setVisibility(View.GONE);
+ }
+
+ private boolean shouldFetchThemesOnScroll(int lastVisibleCount, int totalItemCount) {
+ if (totalItemCount < ThemeBrowserActivity.THEME_FETCH_MAX) {
+ return false;
+ } else {
+ int numberOfColumns = mGridView.getNumColumns();
+ return lastVisibleCount >= totalItemCount - numberOfColumns;
+ }
+ }
+
+ protected int getSpinnerPosition() {
+ if (mSpinner != null) {
+ return mSpinner.getSelectedItemPosition();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void onMovedToScrapHeap(View view) {
+ // cancel image fetch requests if the view has been moved to recycler.
+ NetworkImageView niv = (NetworkImageView) view.findViewById(R.id.theme_grid_item_image);
+ if (niv != null) {
+ // this tag is set in the ThemeBrowserAdapter class
+ String requestUrl = (String) niv.getTag();
+ if (requestUrl != null) {
+ // need a listener to cancel request, even if the listener does nothing
+ ImageContainer container = WordPress.imageLoader.get(requestUrl, new ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ }
+
+ @Override
+ public void onResponse(ImageContainer response, boolean isImmediate) {
+ }
+
+ });
+ container.cancelRequest();
+ }
+ }
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (mSpinner != null) {
+ refreshView(position);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (shouldFetchThemesOnScroll(firstVisibleItem + visibleItemCount, totalItemCount) && NetworkUtils.isNetworkAvailable(getActivity())) {
+ mPage++;
+ mThemeBrowserActivity.fetchThemes();
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeSearchFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeSearchFragment.java
new file mode 100644
index 000000000..c781b2ee8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeSearchFragment.java
@@ -0,0 +1,159 @@
+package org.wordpress.android.ui.themes;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.SearchView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.NetworkUtils;
+
+/**
+ * A fragment for display the results of a theme search
+ */
+public class ThemeSearchFragment extends ThemeBrowserFragment implements SearchView.OnQueryTextListener,
+ MenuItemCompat.OnActionExpandListener {
+ public static final String TAG = ThemeSearchFragment.class.getName();
+ private static final String BUNDLE_LAST_SEARCH = "BUNDLE_LAST_SEARCH";
+ public static final int SEARCH_VIEW_MAX_WIDTH = 10000;
+
+ public static ThemeSearchFragment newInstance() {
+ return new ThemeSearchFragment();
+ }
+
+ private String mLastSearch = "";
+ private SearchView mSearchView;
+ private MenuItem mSearchMenuItem;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ if (savedInstanceState != null) {
+ mLastSearch = savedInstanceState.getString(BUNDLE_LAST_SEARCH);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveState(outState);
+ }
+
+ private void saveState(Bundle outState) {
+ outState.putString(BUNDLE_LAST_SEARCH, mLastSearch);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.removeItem(R.id.menu_search);
+
+ mSearchMenuItem = menu.findItem(R.id.menu_theme_search);
+ mSearchMenuItem.expandActionView();
+ MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, this);
+
+ configureSearchView();
+ }
+
+ public void configureSearchView() {
+ mSearchView = (SearchView) MenuItemCompat.getActionView(mSearchMenuItem);
+ mSearchView.setOnQueryTextListener(this);
+ mSearchView.setQuery(mLastSearch, true);
+ mSearchView.setMaxWidth(SEARCH_VIEW_MAX_WIDTH);
+ }
+
+ private void clearFocus(View view) {
+ if (view != null) {
+ view.clearFocus();
+ }
+ }
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ if (item.getItemId() == R.id.menu_theme_search) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ mThemeBrowserActivity.setIsInSearchMode(false);
+ mThemeBrowserActivity.showToolbar();
+ mThemeBrowserActivity.getFragmentManager().popBackStack();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ if (!mLastSearch.equals(query)) {
+ mLastSearch = query;
+ search(query);
+ }
+ clearFocus(mSearchView);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (!mLastSearch.equals(newText) && !newText.equals("")) {
+ mLastSearch = newText;
+ search(newText);
+ }
+ return true;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.theme_search, menu);
+ }
+
+ @Override
+ protected void addHeaderViews(LayoutInflater inflater) {
+ // No header on Search
+ }
+
+ @Override
+ protected void configureSwipeToRefresh(View view) {
+ super.configureSwipeToRefresh(view);
+ mSwipeToRefreshHelper.setEnabled(false);
+ }
+
+ @Override
+ public void setRefreshing(boolean refreshing) {
+ refreshView(getSpinnerPosition());
+ }
+
+ @Override
+ protected Cursor fetchThemes(int position) {
+ if (WordPress.getCurrentBlog() == null) {
+ return null;
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+
+ return WordPress.wpDB.getThemes(blogId, mLastSearch);
+ }
+
+ public void search(String searchTerm) {
+ mLastSearch = searchTerm;
+
+ if (NetworkUtils.isNetworkAvailable(mThemeBrowserActivity)) {
+ mThemeBrowserActivity.searchThemes(searchTerm);
+ } else {
+ refreshView(getSpinnerPosition());
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java
new file mode 100644
index 000000000..d2b76e2f2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java
@@ -0,0 +1,191 @@
+package org.wordpress.android.ui.themes;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Theme;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+public class ThemeWebActivity extends WPWebViewActivity {
+ public static final String IS_CURRENT_THEME = "is_current_theme";
+ public static final String IS_PREMIUM_THEME = "is_premium_theme";
+ public static final String THEME_NAME = "theme_name";
+ private static final String THEME_DOMAIN_PUBLIC = "pub";
+ private static final String THEME_DOMAIN_PREMIUM = "premium";
+ private static final String THEME_URL_PREVIEW = "%s/wp-admin/customize.php?theme=%s/%s&hide_close=true";
+ private static final String THEME_URL_SUPPORT = "https://wordpress.com/themes/%s/support/?preview=true&iframe=true";
+ private static final String THEME_URL_DETAILS = "https://wordpress.com/themes/%s/%s/?preview=true&iframe=true";
+ private static final String THEME_URL_DEMO_PARAMETER = "demo=true&iframe=true&theme_preview=true";
+ private static final String THEME_HTTPS_PREFIX = "https://";
+
+ public enum ThemeWebActivityType {
+ PREVIEW,
+ DEMO,
+ DETAILS,
+ SUPPORT
+ }
+
+ public static void openTheme(Activity activity, String themeId, ThemeWebActivityType type, boolean isCurrentTheme) {
+ String blogId = WordPress.getCurrentBlog().getDotComBlogId();
+ Theme currentTheme = WordPress.wpDB.getTheme(blogId, themeId);
+ if (currentTheme == null) {
+ ToastUtils.showToast(activity, R.string.could_not_load_theme);
+ return;
+ }
+
+ String url = getUrl(currentTheme, type, currentTheme.isPremium());
+ openWPCOMURL(activity, url, currentTheme, WordPress.getCurrentBlog(), isCurrentTheme);
+ }
+
+ /*
+ * opens the current theme for the current blog
+ */
+ public static void openCurrentTheme(Activity activity, ThemeWebActivityType type) {
+ String blogId = WordPress.getCurrentBlog().getDotComBlogId();
+ String themeId = WordPress.wpDB.getCurrentThemeId(blogId);
+ if (themeId.isEmpty()) {
+ requestAndOpenCurrentTheme(activity, blogId);
+ } else {
+ openTheme(activity, themeId, type, true);
+ }
+ }
+
+ private static void requestAndOpenCurrentTheme(final Activity activity, final String blogId) {
+ WordPress.getRestClientUtilsV1_1().getCurrentTheme(blogId, new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ try {
+ Theme currentTheme = Theme.fromJSONV1_1(response);
+ if (currentTheme != null) {
+ currentTheme.setIsCurrent(true);
+ currentTheme.save();
+ WordPress.wpDB.setCurrentTheme(blogId, currentTheme.getId());
+ openTheme(activity, currentTheme.getId(), ThemeWebActivityType.PREVIEW, true);
+ }
+ } catch (JSONException e) {
+ ToastUtils.showToast(activity, R.string.could_not_load_theme);
+ AppLog.e(AppLog.T.THEMES, e);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.e(AppLog.T.THEMES, error);
+ }
+ });
+ }
+
+ private static void openWPCOMURL(Activity activity, String url, Theme currentTheme, Blog blog, Boolean isCurrentTheme) {
+ if (activity == null) {
+ AppLog.e(AppLog.T.UTILS, "Context is null");
+ return;
+ }
+
+ if (TextUtils.isEmpty(url)) {
+ AppLog.e(AppLog.T.UTILS, "Empty or null URL passed to openWPCOMURL");
+ Toast.makeText(activity, activity.getResources().getText(R.string.invalid_site_url_message),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String authURL = ThemeWebActivity.getBlogLoginUrl(blog);
+ Intent intent = new Intent(activity, ThemeWebActivity.class);
+ intent.putExtra(ThemeWebActivity.AUTHENTICATION_USER, blog.getUsername());
+ intent.putExtra(ThemeWebActivity.AUTHENTICATION_PASSWD, blog.getPassword());
+ intent.putExtra(ThemeWebActivity.URL_TO_LOAD, url);
+ intent.putExtra(ThemeWebActivity.AUTHENTICATION_URL, authURL);
+ intent.putExtra(ThemeWebActivity.LOCAL_BLOG_ID, blog.getLocalTableBlogId());
+ intent.putExtra(IS_PREMIUM_THEME, currentTheme.isPremium());
+ intent.putExtra(IS_CURRENT_THEME, isCurrentTheme);
+ intent.putExtra(THEME_NAME, currentTheme.getName());
+ intent.putExtra(ThemeBrowserActivity.THEME_ID, currentTheme.getId());
+
+ activity.startActivityForResult(intent, ThemeBrowserActivity.ACTIVATE_THEME);
+ }
+
+ public static String getUrl(Theme theme, ThemeWebActivityType type, boolean isPremium) {
+ String url = "";
+ String homeURL = WordPress.getCurrentBlog().getHomeURL();
+ String domain = isPremium ? THEME_DOMAIN_PREMIUM : THEME_DOMAIN_PUBLIC;
+
+ switch (type) {
+ case PREVIEW:
+ url = String.format(THEME_URL_PREVIEW, homeURL, domain, theme.getId());
+ break;
+ case DEMO:
+ url = theme.getDemoURI();
+ if (url.contains("?")) {
+ url = url + "&" + THEME_URL_DEMO_PARAMETER;
+ } else {
+ url = url + "?" + THEME_URL_DEMO_PARAMETER;
+ }
+ break;
+ case DETAILS:
+ String currentURL = homeURL.replaceFirst(THEME_HTTPS_PREFIX, "");
+ url = String.format(THEME_URL_DETAILS, currentURL, theme.getId());
+ break;
+ case SUPPORT:
+ url = String.format(THEME_URL_SUPPORT, theme.getId());
+ break;
+ default:
+ break;
+ }
+
+ return url;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.theme_web, menu);
+ Boolean isPremiumTheme = getIntent().getBooleanExtra(IS_PREMIUM_THEME, false);
+ Boolean isCurrentTheme = getIntent().getBooleanExtra(IS_CURRENT_THEME, false);
+
+ if (isPremiumTheme || isCurrentTheme) {
+ menu.findItem(R.id.action_activate).setVisible(false);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_activate) {
+ Intent returnIntent = new Intent();
+ setResult(RESULT_OK, returnIntent);
+ returnIntent.putExtra(ThemeBrowserActivity.THEME_ID,
+ getIntent().getStringExtra(ThemeBrowserActivity.THEME_ID));
+ finish();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void configureView() {
+ setContentView(R.layout.theme_web_activity);
+ setActionBarTitleToThemeName();
+ }
+
+ private void setActionBarTitleToThemeName() {
+ String themeName = getIntent().getStringExtra(THEME_NAME);
+ if (getSupportActionBar() != null && themeName != null) {
+ getSupportActionBar().setTitle(themeName);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/AnalyticsUtils.java b/WordPress/src/main/java/org/wordpress/android/util/AnalyticsUtils.java
new file mode 100644
index 000000000..208605959
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/AnalyticsUtils.java
@@ -0,0 +1,277 @@
+package org.wordpress.android.util;
+
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsMetadata;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTrackerMixpanel;
+import org.wordpress.android.analytics.AnalyticsTrackerNosara;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.ReaderPost;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_COMMENTED_ON;
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_LIKED;
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_OPENED;
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_RELATED_POST_CLICKED;
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_SEARCH_RESULT_TAPPED;
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.TRAIN_TRACKS_INTERACT;
+import static org.wordpress.android.analytics.AnalyticsTracker.Stat.TRAIN_TRACKS_RENDER;
+
+public class AnalyticsUtils {
+ private static String BLOG_ID_KEY = "blog_id";
+ private static String POST_ID_KEY = "post_id";
+ private static String FEED_ID_KEY = "feed_id";
+ private static String FEED_ITEM_ID_KEY = "feed_item_id";
+ private static String IS_JETPACK_KEY = "is_jetpack";
+
+ /**
+ * Utility method to refresh mixpanel metadata.
+ *
+ * @param username WordPress.com username
+ * @param email WordPress.com email address
+ */
+ public static void refreshMetadata(String username, String email) {
+ AnalyticsMetadata metadata = new AnalyticsMetadata();
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
+
+ metadata.setSessionCount(preferences.getInt(AnalyticsTrackerMixpanel.SESSION_COUNT, 0));
+ metadata.setUserConnected(AccountHelper.isSignedIn());
+ metadata.setWordPressComUser(AccountHelper.isSignedInWordPressDotCom());
+ metadata.setJetpackUser(AccountHelper.isJetPackUser());
+ metadata.setNumBlogs(WordPress.wpDB.getNumBlogs());
+ metadata.setUsername(username);
+ metadata.setEmail(email);
+
+ AnalyticsTracker.refreshMetadata(metadata);
+ }
+
+ /**
+ * Utility method to refresh mixpanel metadata.
+ */
+ public static void refreshMetadata() {
+ AnalyticsMetadata metadata = new AnalyticsMetadata();
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
+
+ metadata.setSessionCount(preferences.getInt(AnalyticsTrackerMixpanel.SESSION_COUNT, 0));
+ metadata.setUserConnected(AccountHelper.isSignedIn());
+ metadata.setWordPressComUser(AccountHelper.isSignedInWordPressDotCom());
+ metadata.setJetpackUser(AccountHelper.isJetPackUser());
+ metadata.setNumBlogs(WordPress.wpDB.getNumBlogs());
+ metadata.setUsername(AccountHelper.getDefaultAccount().getUserName());
+ metadata.setEmail(AccountHelper.getDefaultAccount().getEmail());
+
+ AnalyticsTracker.refreshMetadata(metadata);
+ }
+
+ public static int getWordCount(String content) {
+ String text = Html.fromHtml(content.replaceAll("<img[^>]*>", "")).toString();
+ return text.split("\\s+").length;
+ }
+
+ /**
+ * Bump Analytics for the passed Stat and CURRENT blog details into properties.
+ *
+ * @param stat The Stat to bump
+ *
+ */
+ public static void trackWithCurrentBlogDetails(AnalyticsTracker.Stat stat) {
+ trackWithCurrentBlogDetails(stat, null);
+ }
+
+ /**
+ * Bump Analytics for the passed Stat and CURRENT blog details into properties.
+ *
+ * @param stat The Stat to bump
+ * @param properties Properties to attach to the event
+ *
+ */
+ public static void trackWithCurrentBlogDetails(AnalyticsTracker.Stat stat, Map<String, Object> properties) {
+ trackWithBlogDetails(stat, WordPress.getCurrentBlog(), properties);
+ }
+
+ /**
+ * Bump Analytics for the passed Stat and add blog details into properties.
+ *
+ * @param stat The Stat to bump
+ * @param blog The blog object
+ *
+ */
+ public static void trackWithBlogDetails(AnalyticsTracker.Stat stat, Blog blog) {
+ trackWithBlogDetails(stat, blog, null);
+ }
+
+ /**
+ * Bump Analytics for the passed Stat and add blog details into properties.
+ *
+ * @param stat The Stat to bump
+ * @param blog The blog object
+ * @param properties Properties to attach to the event
+ *
+ */
+ public static void trackWithBlogDetails(AnalyticsTracker.Stat stat, Blog blog, Map<String, Object> properties) {
+ if (blog == null || (!blog.isDotcomFlag() && !blog.isJetpackPowered())) {
+ AppLog.w(AppLog.T.STATS, "The passed blog obj is null or it's not a wpcom or Jetpack. Tracking analytics without blog info");
+ AnalyticsTracker.track(stat, properties);
+ return;
+ }
+
+ String blogID = blog.getDotComBlogId();
+ if (blogID != null) {
+ if (properties == null) {
+ properties = new HashMap<>();
+ }
+ properties.put(BLOG_ID_KEY, blogID);
+ properties.put(IS_JETPACK_KEY, blog.isJetpackPowered());
+ } else {
+ // When the blog ID is null here does mean the blog is not hosted on wpcom.
+ // It may be a Jetpack blog still in synch for options, or a self-hosted.
+ // In both of these cases skip adding blog details into properties.
+ }
+
+ if (properties == null) {
+ AnalyticsTracker.track(stat);
+ } else {
+ AnalyticsTracker.track(stat, properties);
+ }
+ }
+
+ /**
+ * Bump Analytics and add blog_id into properties
+ *
+ * @param stat The Stat to bump
+ * @param blogID The REMOTE blog ID.
+ *
+ */
+ public static void trackWithBlogDetails(AnalyticsTracker.Stat stat, Long blogID) {
+ Map<String, Object> properties = new HashMap<>();
+ if (blogID != null) {
+ properties.put(BLOG_ID_KEY, blogID);
+ }
+ AnalyticsTracker.track(stat, properties);
+ }
+
+ /**
+ * Bump Analytics and add blog_id into properties
+ *
+ * @param stat The Stat to bump
+ * @param blogID The REMOTE blog ID.
+ *
+ */
+ public static void trackWithBlogDetails(AnalyticsTracker.Stat stat, String blogID) {
+ try {
+ Long remoteID = Long.parseLong(blogID);
+ trackWithBlogDetails(stat, remoteID);
+ } catch (NumberFormatException err) {
+ AnalyticsTracker.track(stat);
+ }
+ }
+
+ /**
+ * Bump Analytics for a reader post
+ *
+ * @param stat The Stat to bump
+ * @param post The reader post to track
+ *
+ */
+ public static void trackWithReaderPostDetails(AnalyticsTracker.Stat stat, ReaderPost post) {
+ if (post == null) return;
+
+ // wpcom/jetpack posts should pass: feed_id, feed_item_id, blog_id, post_id, is_jetpack
+ // RSS pass should pass: feed_id, feed_item_id, is_jetpack
+ Map<String, Object> properties = new HashMap<>();
+ if (post.isWP() || post.isJetpack) {
+ properties.put(BLOG_ID_KEY, post.blogId);
+ properties.put(POST_ID_KEY, post.postId);
+ }
+ properties.put(FEED_ID_KEY, post.feedId);
+ properties.put(FEED_ITEM_ID_KEY, post.feedItemId);
+ properties.put(IS_JETPACK_KEY, post.isJetpack);
+
+ AnalyticsTracker.track(stat, properties);
+
+ // record a railcar interact event if the post has a railcar and this can be tracked
+ // as an interaction
+ if (canTrackRailcarInteraction(stat) && post.hasRailcar()) {
+ trackRailcarInteraction(stat, post.getRailcarJson());
+ }
+ }
+
+ public static void trackWithReaderPostDetails(AnalyticsTracker.Stat stat, long blogId, long postId) {
+ trackWithReaderPostDetails(stat, ReaderPostTable.getPost(blogId, postId, true));
+ }
+
+ /**
+ * Track when a railcar item has been rendered
+ *
+ * @param post The JSON string of the railcar
+ *
+ */
+ public static void trackRailcarRender(String railcarJson) {
+ if (TextUtils.isEmpty(railcarJson)) return;
+
+ AnalyticsTracker.track(TRAIN_TRACKS_RENDER, railcarJsonToProperties(railcarJson));
+ }
+
+ /**
+ * Track when a railcar item has been interacted with
+ *
+ * @param stat The event that caused the interaction
+ * @param post The JSON string of the railcar
+ *
+ */
+ private static void trackRailcarInteraction(AnalyticsTracker.Stat stat, String railcarJson) {
+ if (TextUtils.isEmpty(railcarJson)) return;
+
+ Map<String, Object> properties = railcarJsonToProperties(railcarJson);
+ properties.put("action", AnalyticsTrackerNosara.getEventNameForStat(stat));
+ AnalyticsTracker.track(TRAIN_TRACKS_INTERACT, properties);
+ }
+
+ /**
+ * @param stat The event that would cause the interaction
+ * @return True if the passed stat event can be recorded as a railcar interaction
+ */
+ private static boolean canTrackRailcarInteraction(AnalyticsTracker.Stat stat) {
+ return stat == READER_ARTICLE_LIKED
+ || stat == READER_ARTICLE_OPENED
+ || stat == READER_SEARCH_RESULT_TAPPED
+ || stat == READER_ARTICLE_COMMENTED_ON
+ || stat == READER_RELATED_POST_CLICKED;
+ }
+
+ /*
+ * Converts the JSON string of a railcar to a properties list using the existing json key names
+ */
+ private static Map<String, Object> railcarJsonToProperties(@NonNull String railcarJson) {
+ Map<String, Object> properties = new HashMap<>();
+ try {
+ JSONObject jsonRailcar = new JSONObject(railcarJson);
+ Iterator<String> iter = jsonRailcar.keys();
+ while (iter.hasNext()) {
+ String key = iter.next();
+ Object value = jsonRailcar.get(key);
+ properties.put(key, value);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+
+ return properties;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/AniUtils.java b/WordPress/src/main/java/org/wordpress/android/util/AniUtils.java
new file mode 100644
index 000000000..57a4f0072
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/AniUtils.java
@@ -0,0 +1,252 @@
+package org.wordpress.android.util;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.TranslateAnimation;
+
+import org.wordpress.android.R;
+
+public class AniUtils {
+
+ public enum Duration {
+ SHORT,
+ MEDIUM,
+ LONG;
+
+ public long toMillis(Context context) {
+ switch (this) {
+ case LONG:
+ return context.getResources().getInteger(android.R.integer.config_longAnimTime);
+ case MEDIUM:
+ return context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+ default:
+ return context.getResources().getInteger(android.R.integer.config_shortAnimTime);
+ }
+ }
+ }
+
+ public interface AnimationEndListener {
+ void onAnimationEnd();
+ }
+
+ private AniUtils() {
+ throw new AssertionError();
+ }
+
+ public static void startAnimation(View target, int aniResId) {
+ startAnimation(target, aniResId, null);
+ }
+
+ public static void startAnimation(View target, int aniResId, int duration) {
+ if (target == null) return;
+
+ Animation animation = AnimationUtils.loadAnimation(target.getContext(), aniResId);
+ if (animation != null) {
+ animation.setDuration(duration);
+ target.startAnimation(animation);
+ }
+ }
+
+ public static void startAnimation(View target, int aniResId, AnimationListener listener) {
+ if (target == null) return;
+
+ Animation animation = AnimationUtils.loadAnimation(target.getContext(), aniResId);
+ if (animation != null) {
+ if (listener != null) {
+ animation.setAnimationListener(listener);
+ }
+ target.startAnimation(animation);
+ }
+ }
+
+ /*
+ * in/out animation for floating action button
+ */
+ public static void showFab(final View view, final boolean show) {
+ if (view == null) return;
+
+ Context context = view.getContext();
+ int fabHeight = context.getResources().getDimensionPixelSize(android.support.design.R.dimen.design_fab_size_normal);
+ int fabMargin = context.getResources().getDimensionPixelSize(R.dimen.fab_margin);
+ int max = (fabHeight + fabMargin) * 2;
+ float fromY = (show ? max : 0f);
+ float toY = (show ? 0f : max);
+
+ ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, fromY, toY);
+ if (show) {
+ anim.setInterpolator(new DecelerateInterpolator());
+ } else {
+ anim.setInterpolator(new AccelerateInterpolator());
+ }
+ anim.setDuration(show ? Duration.LONG.toMillis(context) : Duration.SHORT.toMillis(context));
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ if (view.getVisibility() != View.VISIBLE) {
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!show) {
+ view.setVisibility(View.GONE);
+ }
+ }
+ });
+
+ anim.start();
+ }
+
+ /*
+ * used when animating a toolbar in/out
+ */
+ public static void animateTopBar(View view, boolean show) {
+ animateBar(view, show, true);
+ }
+
+ public static void animateBottomBar(View view, boolean show) {
+ animateBar(view, show, false);
+ }
+
+ private static void animateBar(final View view,
+ final boolean show,
+ final boolean isTopBar) {
+ int newVisibility = (show ? View.VISIBLE : View.GONE);
+ if (view == null || view.getVisibility() == newVisibility) {
+ return;
+ }
+
+ float fromY;
+ float toY;
+ if (isTopBar) {
+ fromY = (show ? -1f : 0f);
+ toY = (show ? 0f : -1f);
+ } else {
+ fromY = (show ? 1f : 0f);
+ toY = (show ? 0f : 1f);
+ }
+ Animation animation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 0.0f,
+ Animation.RELATIVE_TO_SELF, 0.0f,
+ Animation.RELATIVE_TO_SELF, fromY,
+ Animation.RELATIVE_TO_SELF, toY);
+
+ long durationMillis = Duration.SHORT.toMillis(view.getContext());
+ animation.setDuration(durationMillis);
+
+ if (show) {
+ animation.setInterpolator(new DecelerateInterpolator());
+ } else {
+ animation.setInterpolator(new AccelerateInterpolator());
+ }
+
+ view.clearAnimation();
+ view.startAnimation(animation);
+ view.setVisibility(newVisibility);
+ }
+
+ private static ObjectAnimator getFadeInAnim(final View target, Duration duration) {
+ ObjectAnimator fadeIn = ObjectAnimator.ofFloat(target, View.ALPHA, 0.0f, 1.0f);
+ fadeIn.setDuration(duration.toMillis(target.getContext()));
+ fadeIn.setInterpolator(new LinearInterpolator());
+ fadeIn.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ target.setVisibility(View.VISIBLE);
+ }
+ });
+ return fadeIn;
+ }
+
+ private static ObjectAnimator getFadeOutAnim(final View target, Duration duration) {
+ ObjectAnimator fadeOut = ObjectAnimator.ofFloat(target, View.ALPHA, 1.0f, 0.0f);
+ fadeOut.setDuration(duration.toMillis(target.getContext()));
+ fadeOut.setInterpolator(new LinearInterpolator());
+ fadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ target.setVisibility(View.GONE);
+ }
+ });
+ return fadeOut;
+ }
+
+ public static void fadeIn(final View target, Duration duration) {
+ if (target != null && duration != null) {
+ getFadeInAnim(target, duration).start();
+ }
+ }
+
+ public static void fadeOut(final View target, Duration duration) {
+ if (target != null && duration != null) {
+ getFadeOutAnim(target, duration).start();
+ }
+ }
+
+ public static void scaleIn(final View target, Duration duration) {
+ if (target == null || duration == null) {
+ return;
+ }
+
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 0f, 1f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f, 1f);
+
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(target, scaleX, scaleY);
+ animator.setDuration(duration.toMillis(target.getContext()));
+ animator.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ target.setVisibility(View.VISIBLE);
+ }
+ });
+
+ animator.start();
+ }
+
+ public static void scaleOut(final View target, Duration duration) {
+ scaleOut(target, View.GONE, duration, null);
+ }
+ public static void scaleOut(final View target,
+ final int endVisibility,
+ Duration duration,
+ final AnimationEndListener endListener) {
+ if (target == null || duration == null) {
+ return;
+ }
+
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 0f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 0f);
+
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(target, scaleX, scaleY);
+ animator.setDuration(duration.toMillis(target.getContext()));
+ animator.setInterpolator(new AccelerateInterpolator());
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ target.setVisibility(endVisibility);
+ if (endListener != null) {
+ endListener.onAnimationEnd();
+ }
+ }
+ });
+
+ animator.start();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java b/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java
new file mode 100644
index 000000000..5c952f058
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java
@@ -0,0 +1,100 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import com.android.volley.VolleyError;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.accounts.SignInActivity;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.widgets.AuthErrorDialogFragment;
+
+import static org.wordpress.android.util.ToastUtils.showToast;
+
+public class AuthenticationDialogUtils {
+ /**
+ * Shows a toast message, unless there is an authentication issue which will show an alert dialog.
+ */
+ public static void showToastOrAuthAlert(Context context, VolleyError error, String friendlyMessage) {
+ if (context == null)
+ return;
+
+ String message = null;
+ boolean isInvalidTokenError = false;
+ JSONObject errorObj = VolleyUtils.volleyErrorToJSON(error);
+ if (errorObj != null) {
+ try {
+ if (errorObj.has("error_description")) { // OAuth token request error
+ message = (String) errorObj.get("error_description");
+ String error_code = (String) errorObj.get("error");
+ if (error_code != null && error_code.equals("invalid_request") && message.toLowerCase().contains(
+ "incorrect username or password")) {
+ isInvalidTokenError = true;
+ }
+ } else {
+ message = (String) errorObj.get("message");
+ String error_code = (String) errorObj.get("error");
+ if (error_code != null && error_code.equals("invalid_token")) {
+ isInvalidTokenError = true;
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.API, e);
+ }
+ } else {
+ message = error.getMessage();
+ }
+
+ if (isInvalidTokenError && (context instanceof Activity)) {
+ showAuthErrorView((Activity) context);
+ } else {
+ String fallbackErrorMessage = TextUtils.isEmpty(friendlyMessage) ? context.getString(
+ R.string.error_generic) : friendlyMessage;
+ if (message != null && message.contains("Limit reached") ) {
+ message = context.getString(R.string.limit_reached);
+ }
+ String errorMessage = TextUtils.isEmpty(message) ? fallbackErrorMessage : message;
+ showToast(context, errorMessage, Duration.LONG);
+ }
+ }
+
+
+ public static void showAuthErrorView(Activity activity) {
+ showAuthErrorView(activity, AuthErrorDialogFragment.DEFAULT_RESOURCE_ID, AuthErrorDialogFragment.DEFAULT_RESOURCE_ID);
+ }
+
+ public static void showAuthErrorView(Activity activity, int titleResId, int messageResId) {
+ final String ALERT_TAG = "alert_ask_credentials";
+ if (activity.isFinishing()) {
+ return;
+ }
+
+ // WP.com errors will show the sign in activity
+ if (WordPress.getCurrentBlog() == null || (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isDotcomFlag())) {
+ Intent signInIntent = new Intent(activity, SignInActivity.class);
+ signInIntent.putExtra(SignInActivity.EXTRA_IS_AUTH_ERROR, true);
+ signInIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ activity.startActivityForResult(signInIntent, SignInActivity.REQUEST_CODE);
+ return;
+ }
+
+ // abort if the dialog is already visible
+ if (activity.getFragmentManager().findFragmentByTag(ALERT_TAG) != null) {
+ return;
+ }
+
+ FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
+ AuthErrorDialogFragment authAlert = new AuthErrorDialogFragment();
+ authAlert.setWPComTitleMessage(titleResId, messageResId);
+ ft.add(authAlert, ALERT_TAG);
+ ft.commitAllowingStateLoss();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/AutolinkUtils.java b/WordPress/src/main/java/org/wordpress/android/util/AutolinkUtils.java
new file mode 100644
index 000000000..61d812848
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/AutolinkUtils.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.util;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AutolinkUtils {
+ private static final Set<Pattern> PROVIDERS;
+
+ static {
+ PROVIDERS = new HashSet<Pattern>();
+ PROVIDERS.add(Pattern.compile("(http://(www\\.)?youtube\\.com/watch\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https://(www\\.)?youtube\\.com/watch\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://(www\\.)?youtube\\.com/playlist\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https://(www\\.)?youtube\\.com/playlist\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://youtu\\.be/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https://youtu\\.be/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://blip.tv/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(.+\\.)?vimeo\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?dailymotion\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://dai.ly/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?flickr\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://flic\\.kr/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(.+\\.)?smugmug\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?hulu\\.com/watch/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://revision3.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://i*.photobucket.com/albums/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://gi*.photobucket.com/groups/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?scribd\\.com/doc/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://wordpress.tv/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(.+\\.)?polldaddy\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://poll\\.fm/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?funnyordie\\.com/videos/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?twitter\\.com/\\S+?/status(es)?/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?soundcloud\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(.+?\\.)?slideshare\\.net/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(http://instagr(\\.am|am\\.com)/p/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?rdio\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://rd\\.io/x/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(open|play)\\.spotify\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(.+\\.)?imgur\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?meetu(\\.ps|p\\.com)/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?issuu\\.com/\\S+/docs/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?collegehumor\\.com/video/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?mixcloud\\.com/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.|embed\\.)?ted\\.com/talks/\\S+)"));
+ PROVIDERS.add(Pattern.compile("(https?://(www\\.)?(animoto|video214)\\.com/play/\\S+)"));
+ }
+
+ public static String autoCreateLinks(String text) {
+ if (text == null) {
+ return null;
+ }
+ Pattern urlPattern = Pattern.compile("(\\s+|^)((http|https|ftp|mailto):\\S+)");
+ Matcher matcher = urlPattern.matcher(text);
+ StringBuffer stringBuffer = new StringBuffer();
+ while (matcher.find()) {
+ String whitespaces = matcher.group(1);
+ String url = matcher.group(2);
+ boolean blacklisted = false;
+ // Check if the URL is blacklisted
+ for (Pattern providerPattern : PROVIDERS) {
+ Matcher providerMatcher = providerPattern.matcher(url);
+ if (providerMatcher.matches()) {
+ blacklisted = true;
+ }
+ }
+ // Create a <a href> HTML tag for the link
+ if (!blacklisted) {
+ matcher.appendReplacement(stringBuffer, whitespaces + "<a href=\"" + url + "\">" + url + "</a>");
+ } else {
+ matcher.appendReplacement(stringBuffer, whitespaces + url);
+ }
+ }
+ matcher.appendTail(stringBuffer);
+ return stringBuffer.toString();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/BitmapLruCache.java b/WordPress/src/main/java/org/wordpress/android/util/BitmapLruCache.java
new file mode 100644
index 000000000..2c299fb3e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/BitmapLruCache.java
@@ -0,0 +1,42 @@
+package org.wordpress.android.util;
+
+import android.graphics.Bitmap;
+import android.support.v4.util.LruCache;
+
+import com.android.volley.toolbox.ImageLoader.ImageCache;
+
+import java.util.Map;
+
+public class BitmapLruCache extends LruCache<String, Bitmap> implements ImageCache {
+ public BitmapLruCache(int maxSize) {
+ super(maxSize);
+ }
+
+ public void removeSimilar(String keyLike) {
+ Map<String, Bitmap> map = snapshot();
+
+ for (String key : map.keySet()) {
+ if (key.contains(keyLike)) {
+ remove(key);
+ }
+ }
+ }
+
+ @Override
+ protected int sizeOf(String key, Bitmap value) {
+ // The cache size will be measured in kilobytes rather than
+ // number of items.
+ int bytes = (value.getRowBytes() * value.getHeight());
+ return (bytes / 1024); //value.getByteCount() introduced in HONEYCOMB_MR1 or higher.
+ }
+
+ @Override
+ public Bitmap getBitmap(String key) {
+ return this.get(key);
+ }
+
+ @Override
+ public void putBitmap(String key, Bitmap bitmap) {
+ this.put(key, bitmap);
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/util/CoreEvents.java b/WordPress/src/main/java/org/wordpress/android/util/CoreEvents.java
new file mode 100644
index 000000000..d8b60d46f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/CoreEvents.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.util;
+
+public class CoreEvents {
+ public static class BlogListChanged {}
+ public static class RestApiUnauthorized {}
+ public static class UserSignedOutWordPressCom {}
+ public static class UserSignedOutCompletely {}
+ public static class InvalidCredentialsDetected {}
+ public static class InvalidSslCertificateDetected {}
+ public static class LoginLimitDetected {}
+ public static class TwoFactorAuthenticationDetected {}
+ public static class MainViewPagerScrolled {
+ public final float mXOffset;
+ public MainViewPagerScrolled(float xOffset) {
+ mXOffset = xOffset;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/CrashlyticsUtils.java b/WordPress/src/main/java/org/wordpress/android/util/CrashlyticsUtils.java
new file mode 100644
index 000000000..34dc1f0fa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/CrashlyticsUtils.java
@@ -0,0 +1,65 @@
+package org.wordpress.android.util;
+
+import com.crashlytics.android.Crashlytics;
+
+import io.fabric.sdk.android.Fabric;
+
+public class CrashlyticsUtils {
+ final private static String EXCEPTION_KEY = "exception";
+ final private static String TAG_KEY = "tag";
+ final private static String MESSAGE_KEY = "message";
+ public enum ExceptionType {USUAL, SPECIFIC}
+ public enum ExtraKey {IMAGE_ANGLE, IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_RESIZE_SCALE, NOTE_HTMLDATA, ENTERED_URL}
+
+ public static void logException(Throwable tr, ExceptionType exceptionType, AppLog.T tag, String message) {
+ if (!Fabric.isInitialized()) {
+ return;
+ }
+ if (tag != null) {
+ Crashlytics.setString(TAG_KEY, tag.name());
+ }
+ if (message != null) {
+ Crashlytics.setString(MESSAGE_KEY, message);
+ }
+ Crashlytics.setString(EXCEPTION_KEY, exceptionType.name());
+ Crashlytics.logException(tr);
+ }
+
+ public static void logException(Throwable tr, ExceptionType exceptionType, AppLog.T tag) {
+ logException(tr, exceptionType, tag, null);
+ }
+
+ public static void logException(Throwable tr, ExceptionType exceptionType) {
+ logException(tr, exceptionType, null, null);
+ }
+
+ // Utility functions to force us to use and reuse a limited set of keys
+
+ public static void setInt(ExtraKey key, int value) {
+ if (!Fabric.isInitialized()) {
+ return;
+ }
+ Crashlytics.setInt(key.name(), value);
+ }
+
+ public static void setFloat(ExtraKey key, float value) {
+ if (!Fabric.isInitialized()) {
+ return;
+ }
+ Crashlytics.setFloat(key.name(), value);
+ }
+
+ public static void setString(ExtraKey key, String value) {
+ if (!Fabric.isInitialized()) {
+ return;
+ }
+ Crashlytics.setString(key.name(), value);
+ }
+
+ public static void setBool(ExtraKey key, boolean value) {
+ if (!Fabric.isInitialized()) {
+ return;
+ }
+ Crashlytics.setBool(key.name(), value);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/GenericCallback.java b/WordPress/src/main/java/org/wordpress/android/util/GenericCallback.java
new file mode 100644
index 000000000..de9f93760
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/GenericCallback.java
@@ -0,0 +1,5 @@
+package org.wordpress.android.util;
+
+public interface GenericCallback<T> {
+ public void callback(T t);
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/HelpshiftHelper.java b/WordPress/src/main/java/org/wordpress/android/util/HelpshiftHelper.java
new file mode 100644
index 000000000..baf95bc1b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/HelpshiftHelper.java
@@ -0,0 +1,246 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import com.helpshift.Core;
+import com.helpshift.InstallConfig;
+import com.helpshift.exceptions.InstallException;
+import com.helpshift.support.Support;
+import com.helpshift.support.Support.Delegate;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.wordpress.android.BuildConfig;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HelpshiftHelper {
+ public static String ORIGIN_KEY = "ORIGIN_KEY";
+ private static String HELPSHIFT_SCREEN_KEY = "helpshift_screen";
+ private static String HELPSHIFT_ORIGIN_KEY = "origin";
+ private static HelpshiftHelper mInstance = null;
+ private static HashMap<String, Object> mMetadata = new HashMap<String, Object>();
+
+ public enum MetadataKey {
+ USER_ENTERED_URL("user-entered-url"),
+ USER_ENTERED_USERNAME("user-entered-username");
+
+ private final String mStringValue;
+
+ private MetadataKey(final String stringValue) {
+ mStringValue = stringValue;
+ }
+
+ public String toString() {
+ return mStringValue;
+ }
+ }
+
+ public enum Tag {
+ ORIGIN_UNKNOWN("origin:unknown"),
+ ORIGIN_LOGIN_SCREEN_HELP("origin:login-screen-help"),
+ ORIGIN_LOGIN_SCREEN_ERROR("origin:login-screen-error"),
+ ORIGIN_ME_SCREEN_HELP("origin:me-screen-help"),
+ ORIGIN_START_OVER("origin:start-over"),
+ ORIGIN_DELETE_SITE("origin:delete-site");
+
+ private final String mStringValue;
+
+ private Tag(final String stringValue) {
+ mStringValue = stringValue;
+ }
+
+ public String toString() {
+ return mStringValue;
+ }
+
+ public static String[] toString(Tag[] tags) {
+ if (tags == null) {
+ return null;
+ }
+ String[] res = new String[tags.length];
+ for (int i = 0; i < res.length; i++) {
+ res[0] = tags[0].toString();
+ }
+ return res;
+ }
+ }
+
+ private HelpshiftHelper() {
+ }
+
+ public static synchronized HelpshiftHelper getInstance() {
+ if (mInstance == null) {
+ mInstance = new HelpshiftHelper();
+ }
+ return mInstance;
+ }
+
+ public static void init(Application application) {
+ InstallConfig installConfig = new InstallConfig.Builder()
+ .setEnableInAppNotification(true)
+ .build();
+ Core.init(Support.getInstance());
+ try {
+ Core.install(application, BuildConfig.HELPSHIFT_API_KEY, BuildConfig.HELPSHIFT_API_DOMAIN,
+ BuildConfig.HELPSHIFT_API_ID, installConfig);
+ } catch (InstallException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ Support.setDelegate(new Delegate() {
+ @Override
+ public void sessionBegan() {
+ }
+
+ @Override
+ public void sessionEnded() {
+ }
+
+ @Override
+ public void newConversationStarted(String s) {
+ }
+
+ @Override
+ public void userRepliedToConversation(String s) {
+ AnalyticsTracker.track(Stat.SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE);
+ }
+
+ @Override
+ public void userCompletedCustomerSatisfactionSurvey(int i, String s) {
+ }
+
+ @Override
+ public void displayAttachmentFile(File file) {
+ }
+
+ @Override
+ public void didReceiveNotification(int i) {
+ }
+ });
+ }
+
+ /**
+ * Show conversation activity
+ * Automatically add default metadata to this conversation
+ */
+ public void showConversation(Activity activity, Tag origin) {
+ if (origin == null) {
+ origin = Tag.ORIGIN_UNKNOWN;
+ }
+ // track origin and helpshift screen in analytics
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put(HELPSHIFT_SCREEN_KEY, "conversation");
+ properties.put(HELPSHIFT_ORIGIN_KEY, origin.toString());
+ AnalyticsTracker.track(Stat.SUPPORT_OPENED_HELPSHIFT_SCREEN, properties);
+ // Add tags to Helpshift metadata
+ addTags(new Tag[]{origin});
+ HashMap config = getHelpshiftConfig(activity);
+ Support.showConversation(activity, config);
+ }
+
+ /**
+ * Show FAQ activity
+ * Automatically add default metadata to this conversation (users can start a conversation from FAQ screen).
+ */
+ public void showFAQ(Activity activity, Tag origin) {
+ if (origin == null) {
+ origin = Tag.ORIGIN_UNKNOWN;
+ }
+ // track origin and helpshift screen in analytics
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put(HELPSHIFT_SCREEN_KEY, "faq");
+ properties.put(HELPSHIFT_ORIGIN_KEY, origin.toString());
+ AnalyticsTracker.track(Stat.SUPPORT_OPENED_HELPSHIFT_SCREEN, properties);
+ // Add tags to Helpshift metadata
+ addTags(new Tag[]{origin});
+ HashMap config = getHelpshiftConfig(activity);
+ Support.showFAQs(activity, config);
+ }
+
+ /**
+ * Register a GCM device token to Helpshift servers
+ *
+ * @param regId registration id
+ */
+ public void registerDeviceToken(Context context, String regId) {
+ if (!TextUtils.isEmpty(regId)) {
+ Core.registerDeviceToken(context, regId);
+ }
+ }
+
+ public void setTags(Tag[] tags) {
+ mMetadata.put(Support.TagsKey, Tag.toString(tags));
+ }
+
+ public void addTags(Tag[] tags) {
+ String[] oldTags = (String[]) mMetadata.get(Support.TagsKey);
+ // Concatenate arrays
+ mMetadata.put(Support.TagsKey, ArrayUtils.addAll(oldTags, Tag.toString(tags)));
+ }
+
+ /**
+ * Handle push notification
+ */
+ public void handlePush(Context context, Intent intent) {
+ Core.handlePush(context, intent);
+ }
+
+ /**
+ * Add metadata to Helpshift conversations
+ *
+ * @param key map key
+ * @param object to store. Be careful with the type used. Nothing is specified in the documentation. Better to use
+ * String but String[] is needed for specific key like Support.TagsKey
+ */
+ public void addMetaData(MetadataKey key, Object object) {
+ mMetadata.put(key.toString(), object);
+ }
+
+ public Object getMetaData(MetadataKey key) {
+ return mMetadata.get(key.toString());
+ }
+
+ private void addDefaultMetaData(Context context) {
+ // Use plain text log (unfortunately Helpshift can't display this correctly)
+ mMetadata.put("log", AppLog.toPlainText(context));
+
+ // List blogs name and url
+ int counter = 1;
+ for (Map<String, Object> account : WordPress.wpDB.getAllBlogs()) {
+ mMetadata.put("blog-name-" + counter, MapUtils.getMapStr(account, "blogName"));
+ mMetadata.put("blog-url-" + counter, MapUtils.getMapStr(account, "url"));
+ counter += 1;
+ }
+
+ // wpcom user
+ mMetadata.put("wpcom-username", AccountHelper.getDefaultAccount().getUserName());
+ }
+
+ private HashMap getHelpshiftConfig(Context context) {
+ String emailAddress = UserEmailUtils.getPrimaryEmail(context);
+ // Use the user entered username to pre-fill name
+ String name = (String) getMetaData(MetadataKey.USER_ENTERED_USERNAME);
+ // If it's null or empty, use split email address to pre-fill name
+ if (TextUtils.isEmpty(name)) {
+ String[] splitEmail = TextUtils.split(emailAddress, "@");
+ if (splitEmail.length >= 1) {
+ name = splitEmail[0];
+ }
+ }
+ Core.setNameAndEmail(name, emailAddress);
+ addDefaultMetaData(context);
+ HashMap config = new HashMap ();
+ config.put(Support.CustomMetadataKey, mMetadata);
+ return config;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/RateLimitedTask.java b/WordPress/src/main/java/org/wordpress/android/util/RateLimitedTask.java
new file mode 100644
index 000000000..04c3d37b8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/RateLimitedTask.java
@@ -0,0 +1,37 @@
+package org.wordpress.android.util;
+
+import java.util.Date;
+
+public abstract class RateLimitedTask {
+ private Date mLastUpdate;
+ private int mMinRateInSeconds;
+
+ public RateLimitedTask(int minRateInSeconds) {
+ mMinRateInSeconds = minRateInSeconds;
+ }
+
+ public void forceLastUpdate() {
+ mLastUpdate = new Date();
+ }
+
+ public synchronized boolean forceRun() {
+ if (run()) {
+ mLastUpdate = new Date();
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized boolean runIfNotLimited() {
+ Date now = new Date();
+ if (mLastUpdate == null || DateTimeUtils.secondsBetween(now, mLastUpdate) >= mMinRateInSeconds) {
+ if (run()) {
+ mLastUpdate = now;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected abstract boolean run();
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/URLFilteredWebViewClient.java b/WordPress/src/main/java/org/wordpress/android/util/URLFilteredWebViewClient.java
new file mode 100644
index 000000000..82630c3fd
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/URLFilteredWebViewClient.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Toast;
+
+import org.wordpress.android.*;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * WebViewClient that adds the ability of restrict URL loading (navigation) to a list of allowed URLs.
+ * Generally used to disable links and navigation in admin pages.
+ */
+public class URLFilteredWebViewClient extends WebViewClient {
+ private Set<String> allowedURLs = new LinkedHashSet<>();
+ private int linksDisabledMessageResId = org.wordpress.android.R.string.preview_screen_links_disabled;
+
+ public URLFilteredWebViewClient() {
+ }
+
+ public URLFilteredWebViewClient(String url) {
+ allowedURLs.add(url);
+ }
+
+ public URLFilteredWebViewClient(Collection<String> urls) {
+ if (urls == null || urls.size() == 0) {
+ AppLog.w(AppLog.T.UTILS, "No valid URLs passed to URLFilteredWebViewClient! " +
+ "HTTP Links in the page are NOT disabled, and ALL URLs could be loaded by the user!!");
+ return;
+ }
+ allowedURLs.addAll(urls);
+ }
+
+ protected boolean isAllURLsAllowed() {
+ return allowedURLs.size() == 0;
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ // Found a bug on some pages where there is an incorrect
+ // auto-redirect to file:///android_asset/webkit/.
+ if (url.equals("file:///android_asset/webkit/")) {
+ return true;
+ }
+
+ if (isAllURLsAllowed() || allowedURLs.contains(url)) {
+ view.loadUrl(url);
+ } else {
+ // show "links are disabled" message.
+ Context ctx = WordPress.getContext();
+ Toast.makeText(ctx, ctx.getText(linksDisabledMessageResId), Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+
+ public void setLinksDisabledMessageResId(int resId) {
+ linksDisabledMessageResId = resId;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/util/VolleyUtils.java b/WordPress/src/main/java/org/wordpress/android/util/VolleyUtils.java
new file mode 100644
index 000000000..d67ae7a2d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/VolleyUtils.java
@@ -0,0 +1,131 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.HttpStack;
+import com.android.volley.toolbox.ImageRequest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.WPDelayedHurlStack;
+
+import java.io.UnsupportedEncodingException;
+
+public class VolleyUtils {
+ /*
+ * Returns REST API 'error' string code from the response in the passed VolleyError
+ * for example, returns "already_subscribed" from this response:
+ * {
+ * "error": "already_subscribed",
+ * "message": "You are already subscribed to the specified topic."
+ * }
+ */
+ public static String errStringFromVolleyError(VolleyError volleyError) {
+ JSONObject json = volleyErrorToJSON(volleyError);
+ if (json==null)
+ return "";
+ return JSONUtils.getString(json, "error");
+ }
+
+ public static int statusCodeFromVolleyError(VolleyError volleyError) {
+ if (volleyError == null || volleyError.networkResponse == null) {
+ return 0;
+ }
+ return volleyError.networkResponse.statusCode;
+ }
+
+ /*
+ * Returns REST API 'message' string field from the response in the passed VolleyError
+ * for example, returns "You are already subscribed to the specified topic." from this response:
+ * {
+ * "error": "already_subscribed",
+ * "message": "You are already subscribed to the specified topic."
+ * }
+ */
+ public static String messageStringFromVolleyError(VolleyError volleyError) {
+ JSONObject json = volleyErrorToJSON(volleyError);
+ if (json==null)
+ return "";
+ return JSONUtils.getString(json, "message");
+ }
+
+ /*
+ * Attempts to return JSON from a volleyError - useful for WP REST API failures, which often
+ * contain JSON in the response
+ */
+ public static JSONObject volleyErrorToJSON(VolleyError volleyError) {
+ if (volleyError == null || volleyError.networkResponse == null || volleyError.networkResponse.data == null
+ || volleyError.networkResponse.headers == null) {
+ return null;
+ }
+
+ String contentType = volleyError.networkResponse.headers.get("Content-Type");
+ if (contentType == null || !contentType.equals("application/json")) {
+ return null;
+ }
+
+ try {
+ String response = new String(volleyError.networkResponse.data, "UTF-8");
+ JSONObject json = new JSONObject(response);
+ return json;
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ /*
+ * Cancel all Volley requests that aren't for images
+ */
+ public static void cancelAllNonImageRequests(RequestQueue requestQueue) {
+ if (requestQueue==null)
+ return;
+ RequestQueue.RequestFilter filter = new RequestQueue.RequestFilter() {
+ @Override
+ public boolean apply(Request<?> request) {
+ if (request instanceof ImageRequest)
+ return false;
+ return true;
+ }
+ };
+ requestQueue.cancelAll(filter);
+ }
+
+ /*
+ * Cancel all Volley requests
+ */
+ public static void cancelAllRequests(RequestQueue requestQueue) {
+ if (requestQueue==null)
+ return;
+ RequestQueue.RequestFilter filter = new RequestQueue.RequestFilter() {
+ @Override
+ public boolean apply(Request<?> request) {
+ return true;
+ }
+ };
+ requestQueue.cancelAll(filter);
+ }
+
+ /*
+ * Return true if the blog is protected with HTTP Basic Auth
+ */
+ public static boolean isCustomHTTPClientStackNeeded(Blog currentBlog) {
+ if (currentBlog.hasValidHTTPAuthCredentials())
+ return true;
+
+ return false;
+ }
+
+ public static HttpStack getHTTPClientStack(final Context ctx) {
+ return getHTTPClientStack(ctx, null);
+ }
+
+ public static HttpStack getHTTPClientStack(final Context ctx, final Blog currentBlog) {
+ return new WPDelayedHurlStack(ctx, currentBlog);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java
new file mode 100644
index 000000000..8602152aa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java
@@ -0,0 +1,143 @@
+package org.wordpress.android.util;
+
+import android.app.Dialog;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.prefs.AppSettingsFragment;
+
+import java.util.List;
+import java.util.Locale;
+
+public class WPActivityUtils {
+ // Hack! PreferenceScreens don't show the toolbar, so we'll manually add one
+ // See: http://stackoverflow.com/a/27455363/309558
+ public static void addToolbarToDialog(final Fragment context, final Dialog dialog, String title) {
+ if (!context.isAdded() || dialog == null) {
+ return;
+ }
+
+ Toolbar toolbar;
+ if (dialog.findViewById(android.R.id.list) == null) {
+ return;
+ }
+
+ ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.list).getParent();
+ toolbar = (Toolbar) LayoutInflater.from(context.getActivity())
+ .inflate(org.wordpress.android.R.layout.toolbar, root, false);
+ root.addView(toolbar, 0);
+
+ dialog.getWindow().setWindowAnimations(R.style.DialogAnimations);
+
+ TextView titleView = (TextView) toolbar.findViewById(R.id.toolbar_title);
+ titleView.setVisibility(View.VISIBLE);
+ titleView.setText(title);
+
+ toolbar.setTitle("");
+ toolbar.setNavigationIcon(org.wordpress.android.R.drawable.ic_arrow_back_white_24dp);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Checks for a {@link Toolbar} at the first child element of a given {@link Dialog} and
+ * removes it if it exists.
+ *
+ * Originally added to prevent a crash that occurs with nested PreferenceScreens that added
+ * a toolbar via {@link WPActivityUtils#addToolbarToDialog(Fragment, Dialog, String)}. The
+ * crash can be reproduced by turning 'Don't keep activities' on from Developer options.
+ */
+ public static void removeToolbarFromDialog(final Fragment context, final Dialog dialog) {
+ if (dialog == null || !context.isAdded()) return;
+
+ ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.list).getParent();
+ if (root.getChildAt(0) instanceof Toolbar) {
+ root.removeViewAt(0);
+ }
+ }
+
+ public static void setStatusBarColor(Window window, int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ //noinspection deprecation
+ window.setStatusBarColor(window.getContext().getResources().getColor(color));
+ }
+ }
+
+ public static void hideKeyboard(final View view) {
+ InputMethodManager inputMethodManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ public static void applyLocale(Context context) {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ if (sharedPreferences.contains(AppSettingsFragment.LANGUAGE_PREF_KEY)) {
+ Locale contextLocale = context.getResources().getConfiguration().locale;
+ String contextLanguage = contextLocale.getLanguage();
+ contextLanguage = LanguageUtils.patchDeviceLanguageCode(contextLanguage);
+ String contextCountry = contextLocale.getCountry();
+ String locale = sharedPreferences.getString(AppSettingsFragment.LANGUAGE_PREF_KEY, "");
+
+ if (!TextUtils.isEmpty(contextCountry)) {
+ contextLanguage += "-" + contextCountry;
+ }
+
+ if (!locale.equals(contextLanguage)) {
+ Resources resources = context.getResources();
+ Configuration conf = resources.getConfiguration();
+ conf.locale = new Locale(locale);
+ resources.updateConfiguration(conf, resources.getDisplayMetrics());
+ }
+ }
+ }
+
+ public static Context getThemedContext(Context context) {
+ if (context instanceof AppCompatActivity) {
+ ActionBar actionBar = ((AppCompatActivity)context).getSupportActionBar();
+ if (actionBar != null) {
+ return actionBar.getThemedContext();
+ }
+ }
+ return context;
+ }
+
+ public static boolean isEmailClientAvailable(Context context) {
+ if (context == null) {
+ return false;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_APP_EMAIL);
+ PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> emailApps = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+
+ return !emailApps.isEmpty();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java b/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java
new file mode 100644
index 000000000..d44083dee
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java
@@ -0,0 +1,1225 @@
+package org.wordpress.android.util;
+
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+
+import org.ccil.cowan.tagsoup.HTMLSchema;
+import org.ccil.cowan.tagsoup.Parser;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.helpers.MediaGalleryImageSpan;
+import org.wordpress.android.util.helpers.WPImageSpan;
+import org.wordpress.android.util.helpers.WPUnderlineSpan;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.Locale;
+
+/**
+ * This class processes HTML strings into displayable styled text. Not all HTML
+ * tags are supported.
+ */
+public class WPHtml {
+ /**
+ * Retrieves images for HTML &lt;img&gt; tags.
+ */
+ public static interface ImageGetter {
+ /**
+ * This method is called when the HTML parser encounters an &lt;img&gt;
+ * tag. The <code>source</code> argument is the string from the "src"
+ * attribute; the return value should be a Drawable representation of
+ * the image or <code>null</code> for a generic replacement image. Make
+ * sure you call setBounds() on your Drawable if it doesn't already have
+ * its bounds set.
+ */
+ public Drawable getDrawable(String source);
+ }
+
+ /**
+ * Is notified when HTML tags are encountered that the parser does not know
+ * how to interpret.
+ */
+ public static interface TagHandler {
+ /**
+ * This method will be called whenn the HTML parser encounters a tag
+ * that it does not know how to interpret.
+ *
+ * @param mysteryTagContent
+ */
+ public void handleTag(boolean opening, String tag, Editable output,
+ XMLReader xmlReader, String mysteryTagContent);
+ }
+
+ private WPHtml() {
+ }
+
+ /**
+ * Returns displayable styled text from the provided HTML string. Any
+ * &lt;img&gt; tags in the HTML will display as a generic replacement image
+ * which your program can then go through and replace with real images.
+ *
+ * <p>
+ * This uses TagSoup to handle real HTML, including all of the brokenness
+ * found in the wild.
+ */
+ public static Spanned fromHtml(String source, Context ctx, Post post, int maxImageWidth) {
+ return fromHtml(source, null, null, ctx, post, maxImageWidth);
+ }
+
+ /**
+ * Lazy initialization holder for HTML parser. This class will a) be
+ * preloaded by the zygote, or b) not loaded until absolutely necessary.
+ */
+ private static class HtmlParser {
+ private static final HTMLSchema schema = new HTMLSchema();
+ }
+
+ /**
+ * Returns displayable styled text from the provided HTML string. Any
+ * &lt;img&gt; tags in the HTML will use the specified ImageGetter to
+ * request a representation of the image (use null if you don't want this)
+ * and the specified TagHandler to handle unknown tags (specify null if you
+ * don't want this).
+ *
+ * <p>
+ * This uses TagSoup to handle real HTML, including all of the brokenness
+ * found in the wild.
+ */
+ public static Spanned fromHtml(String source, ImageGetter imageGetter,
+ TagHandler tagHandler, Context ctx, Post post, int maxImageWidth) {
+ Parser parser = new Parser();
+ try {
+ parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
+ } catch (org.xml.sax.SAXNotRecognizedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ } catch (org.xml.sax.SAXNotSupportedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+
+ HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source,
+ imageGetter, tagHandler, parser, ctx, post, maxImageWidth);
+ return converter.convert();
+ }
+
+ /**
+ * Returns an HTML representation of the provided Spanned text.
+ */
+ public static String toHtml(Spanned text) {
+ StringBuilder out = new StringBuilder();
+ withinHtml(out, text);
+ return out.toString();
+ }
+
+ private static void withinHtml(StringBuilder out, Spanned text) {
+ int len = text.length();
+
+ int next;
+ for (int i = 0; i < text.length(); i = next) {
+ next = text.nextSpanTransition(i, len, ParagraphStyle.class);
+ /*ParagraphStyle[] style = text.getSpans(i, next,
+ ParagraphStyle.class);
+ String elements = " ";
+ boolean needDiv = false;
+
+ for (int j = 0; j < style.length; j++) {
+ if (style[j] instanceof AlignmentSpan) {
+ Layout.Alignment align = ((AlignmentSpan) style[j])
+ .getAlignment();
+ needDiv = true;
+ if (align == Layout.Alignment.ALIGN_CENTER) {
+ elements = "align=\"center\" " + elements;
+ } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
+ elements = "align=\"right\" " + elements;
+ } else {
+ elements = "align=\"left\" " + elements;
+ }
+ }
+ }
+ if (needDiv) {
+ out.append("<div " + elements + ">");
+ }*/
+
+ withinDiv(out, text, i, next);
+
+ /*if (needDiv) {
+ out.append("</div>");
+ }*/
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void withinDiv(StringBuilder out, Spanned text, int start,
+ int end) {
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = text.nextSpanTransition(i, end, QuoteSpan.class);
+ QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
+
+ for (QuoteSpan quote : quotes) {
+ out.append("<blockquote>");
+ }
+
+ withinBlockquote(out, text, i, next);
+
+ for (QuoteSpan quote : quotes) {
+ out.append("</blockquote>\n");
+ }
+ }
+ }
+
+ private static void withinBlockquote(StringBuilder out, Spanned text,
+ int start, int end) {
+ out.append("<p>");
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = TextUtils.indexOf(text, '\n', i, end);
+ if (next < 0) {
+ next = end;
+ }
+
+ int nl = 0;
+
+ while (next < end && text.charAt(next) == '\n') {
+ nl++;
+ next++;
+ }
+
+ withinParagraph(out, text, i, next - nl, nl, next == end);
+ }
+
+ out.append("</p>\n");
+ }
+
+ private static void withinParagraph(StringBuilder out, Spanned text,
+ int start, int end, int nl, boolean last) {
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = text.nextSpanTransition(i, end, CharacterStyle.class);
+ CharacterStyle[] style = text.getSpans(i, next,
+ CharacterStyle.class);
+
+ for (int j = 0; j < style.length; j++) {
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("<strong>");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("<em>");
+ }
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("<tt>");
+ }
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("<sup>");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("<sub>");
+ }
+ if (style[j] instanceof WPUnderlineSpan) {
+ out.append("<u>");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("<strike>");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("<a href=\"");
+ out.append(((URLSpan) style[j]).getURL());
+ out.append("\">");
+ }
+ if (style[j] instanceof MediaGalleryImageSpan) {
+ out.append(getGalleryShortcode((MediaGalleryImageSpan) style[j]));
+ } else if (style[j] instanceof WPImageSpan && ((WPImageSpan) style[j]).getMediaFile().getMediaId() != null) {
+ out.append(getContent((WPImageSpan) style[j]));
+ } else if (style[j] instanceof WPImageSpan) {
+ out.append("<img src=\"");
+ out.append(((WPImageSpan) style[j]).getSource());
+ out.append("\" android-uri=\""
+ + ((WPImageSpan) style[j]).getImageSource()
+ .toString() + "\"");
+ out.append(" />");
+ // Don't output the dummy character underlying the image.
+ i = next;
+ }
+ if (style[j] instanceof AbsoluteSizeSpan) {
+ out.append("<font size =\"");
+ out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
+ out.append("\">");
+ }
+ if (style[j] instanceof ForegroundColorSpan) {
+ out.append("<font color =\"#");
+ String color = Integer
+ .toHexString(((ForegroundColorSpan) style[j])
+ .getForegroundColor() + 0x01000000);
+ while (color.length() < 6) {
+ color = "0" + color;
+ }
+ out.append(color);
+ out.append("\">");
+ }
+ }
+
+ processWPImage(out, text, i, next);
+
+ for (int j = style.length - 1; j >= 0; j--) {
+ if (style[j] instanceof ForegroundColorSpan) {
+ out.append("</font>");
+ }
+ if (style[j] instanceof AbsoluteSizeSpan) {
+ out.append("</font>");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("</a>");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("</strike>");
+ }
+ if (style[j] instanceof WPUnderlineSpan) {
+ out.append("</u>");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("</sub>");
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("</sup>");
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("</tt>");
+ }
+ }
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("</strong>");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("</em>");
+ }
+ }
+ }
+ }
+
+ String p = last ? "" : "</p>\n<p>";
+
+ if (nl == 1) {
+ out.append("<br>\n");
+ } else if (nl == 2) {
+ out.append(p);
+ } else {
+ for (int i = 2; i < nl; i++) {
+ out.append("<br>");
+ }
+
+ out.append(p);
+ }
+ }
+
+ /** Get gallery shortcode for a MediaGalleryImageSpan */
+ public static String getGalleryShortcode(MediaGalleryImageSpan gallerySpan) {
+ String shortcode = "";
+ MediaGallery gallery = gallerySpan.getMediaGallery();
+ shortcode += "[gallery ";
+ if (gallery.isRandom())
+ shortcode += " orderby=\"rand\"";
+ if (gallery.getType().equals(""))
+ shortcode += " columns=\"" + gallery.getNumColumns() + "\"";
+ else
+ shortcode += " type=\"" + gallery.getType() + "\"";
+ shortcode += " ids=\"" + gallery.getIdsStr() + "\"";
+ shortcode += "]";
+
+ return shortcode;
+ }
+
+ /** Retrieve an image span content for a media file that exists on the server **/
+ public static String getContent(WPImageSpan imageSpan) {
+ // based on PostUploadService
+
+ String content = "";
+ MediaFile mediaFile = imageSpan.getMediaFile();
+ if (mediaFile == null)
+ return content;
+ String mediaId = mediaFile.getMediaId();
+ if (mediaId == null || mediaId.length() == 0)
+ return content;
+
+ boolean isVideo = mediaFile.isVideo();
+ String url = imageSpan.getImageSource().toString();
+
+ if (isVideo) {
+ if (!TextUtils.isEmpty(mediaFile.getVideoPressShortCode())) {
+ content = mediaFile.getVideoPressShortCode();
+ } else {
+ int xRes = mediaFile.getWidth();
+ int yRes = mediaFile.getHeight();
+ String mimeType = mediaFile.getMimeType();
+ content = String.format("<video width=\"%s\" height=\"%s\" controls=\"controls\"><source src=\"%s\" type=\"%s\" /><a href=\"%s\">Click to view video</a>.</video>",
+ xRes, yRes, url, mimeType, url);
+ }
+ } else {
+ String alignment = "";
+ switch (mediaFile.getHorizontalAlignment()) {
+ case 0:
+ alignment = "alignnone";
+ break;
+ case 1:
+ alignment = "alignleft";
+ break;
+ case 2:
+ alignment = "aligncenter";
+ break;
+ case 3:
+ alignment = "alignright";
+ break;
+ }
+ String alignmentCSS = "class=\"" + alignment + " size-full\" ";
+ String title = mediaFile.getTitle();
+ String caption = mediaFile.getCaption();
+ int width = mediaFile.getWidth();
+
+ String inlineCSS = " ";
+ String localBlogID = imageSpan.getMediaFile().getBlogId();
+ Blog currentBlog = WordPress.wpDB.instantiateBlogByLocalId(Integer.parseInt(localBlogID));
+ // If it's not a gif and blog don't keep original size, there is a chance we need to resize
+ if (currentBlog != null && !mediaFile.getMimeType().equals("image/gif")
+ && MediaUtils.getImageWidthSettingFromString(currentBlog.getMaxImageWidth()) != Integer.MAX_VALUE) {
+ width = MediaUtils.getMaximumImageWidth(width, currentBlog.getMaxImageWidth());
+ // Use inline CSS on self-hosted blogs to enforce picture resize settings
+ if (!currentBlog.isDotcomFlag()) {
+ inlineCSS = String.format(Locale.US, " style=\"width:%dpx;max-width:%dpx;\" ", width, width);
+ }
+ }
+ content = content + "<a href=\"" + url + "\"><img" + inlineCSS + "title=\"" + title + "\" "
+ + alignmentCSS + "alt=\"image\" src=\"" + url + "?w=" + width +"\" /></a>";
+
+ if (!caption.equals("")) {
+ content = String.format(Locale.US,
+ "[caption id=\"\" align=\"%s\" width=\"%d\"]%s%s[/caption]",
+ alignment, width, content, TextUtils.htmlEncode(caption));
+ }
+ }
+
+ return content;
+ }
+
+ private static void processWPImage(StringBuilder out, Spanned text,
+ int start, int end) {
+ int next;
+
+ for (int i = start; i < end; i = next) {
+ next = text.nextSpanTransition(i, end, SpannableString.class);
+ SpannableString[] images = text.getSpans(i, next,
+ SpannableString.class);
+
+ for (SpannableString image : images) {
+ out.append(image.toString());
+ }
+
+ withinStyle(out, text, i, next);
+
+ }
+ }
+
+ private static void withinStyle(StringBuilder out, Spanned text, int start,
+ int end) {
+ for (int i = start; i < end; i++) {
+ char c = text.charAt(i);
+
+ /*
+ * if (c == '<') { out.append("&lt;"); } else if (c == '>') {
+ * out.append("&gt;"); } else if (c == '&') { out.append("&amp;");
+ * if (c > 0x7E || c < ' ') { out.append("&#" + ((int) c) + ";"); }
+ * else
+ */
+ if (c == ' ') {
+ while (i + 1 < end && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+ }
+}
+
+class HtmlToSpannedConverter implements ContentHandler {
+ private static final float[] HEADER_SIZES = { 1.5f, 1.4f, 1.3f, 1.2f, 1.1f,
+ 1f, };
+
+ private String mSource;
+ private XMLReader mReader;
+ private SpannableStringBuilder mSpannableStringBuilder;
+ private WPHtml.ImageGetter mImageGetter;
+ private String mysteryTagContent;
+ private boolean mysteryTagFound;
+ private int mMaxImageWidth;
+ private Context mContext;
+ private Post mPost;
+
+ private String mysteryTagName;
+
+ public HtmlToSpannedConverter(String source,
+ WPHtml.ImageGetter imageGetter, WPHtml.TagHandler tagHandler,
+ Parser parser, Context context, Post p, int maxImageWidth) {
+ mSource = source;
+ mSpannableStringBuilder = new SpannableStringBuilder();
+ mImageGetter = imageGetter;
+ mReader = parser;
+ mysteryTagContent = "";
+ mysteryTagName = null;
+ mContext = context;
+ mPost = p;
+ mMaxImageWidth = maxImageWidth;
+ }
+
+ public Spanned convert() {
+ mReader.setContentHandler(this);
+ try {
+ mReader.parse(new InputSource(new StringReader(mSource)));
+ } catch (IOException e) {
+ // We are reading from a string. There should not be IO problems.
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ // TagSoup doesn't throw parse exceptions.
+ throw new RuntimeException(e);
+ }
+
+ // Fix flags and range for paragraph-type markup.
+ Object[] obj = mSpannableStringBuilder.getSpans(0,
+ mSpannableStringBuilder.length(), ParagraphStyle.class);
+ for (int i = 0; i < obj.length; i++) {
+ int start = mSpannableStringBuilder.getSpanStart(obj[i]);
+ int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
+
+ // If the last line of the range is blank, back off by one.
+ if (end - 2 >= 0) {
+ if (mSpannableStringBuilder.charAt(end - 1) == '\n'
+ && mSpannableStringBuilder.charAt(end - 2) == '\n') {
+ end--;
+ }
+ }
+
+ if (end == start) {
+ mSpannableStringBuilder.removeSpan(obj[i]);
+ } else {
+ try {
+ mSpannableStringBuilder.setSpan(obj[i], start, end,
+ Spannable.SPAN_PARAGRAPH);
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ return mSpannableStringBuilder;
+ }
+
+ private void handleStartTag(String tag, Attributes attributes) {
+ if (!mysteryTagFound) {
+ if (mPost != null) {
+ if (!mPost.isLocalDraft()) {
+ if (tag.equalsIgnoreCase("img"))
+ startImg(mSpannableStringBuilder, attributes,
+ mImageGetter);
+
+ return;
+ }
+ }
+
+ if (tag.equalsIgnoreCase("br")) {
+ // We don't need to handle this. TagSoup will ensure that
+ // there's a
+ // </br> for each <br>
+ // so we can safely emite the linebreaks when we handle the
+ // close
+ // tag.
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("b")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("strong")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("cite")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("i")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("big")) {
+ start(mSpannableStringBuilder, new Big());
+ } else if (tag.equalsIgnoreCase("small")) {
+ start(mSpannableStringBuilder, new Small());
+ } else if (tag.equalsIgnoreCase("font")) {
+ startFont(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Blockquote());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ start(mSpannableStringBuilder, new Monospace());
+ } else if (tag.equalsIgnoreCase("a")) {
+ startA(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("u")) {
+ start(mSpannableStringBuilder, new Underline());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ start(mSpannableStringBuilder, new Super());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ start(mSpannableStringBuilder, new Sub());
+ } else if (tag.equalsIgnoreCase("strike")) {
+ start(mSpannableStringBuilder, new Strike());
+ } else if (tag.length() == 2
+ && Character.toLowerCase(tag.charAt(0)) == 'h'
+ && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
+ } else if (tag.equalsIgnoreCase("img")) {
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
+ } else {
+ if (tag.equalsIgnoreCase("html") || tag.equalsIgnoreCase("body")) {
+ return;
+ }
+
+ mysteryTagFound = true;
+ mysteryTagName = tag;
+ }
+ // mTagHandler.handleTag(true, tag, mSpannableStringBuilder,
+ // mReader, mysteryTagContent);
+ }
+ }
+
+ private void handleEndTag(String tag) {
+ if (mPost != null) {
+ if (!mPost.isLocalDraft())
+ return;
+ }
+ if (!mysteryTagFound) {
+ if (tag.equalsIgnoreCase("br")) {
+ handleBr(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(
+ Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("b")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(
+ Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("strong")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(
+ Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("cite")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(
+ Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(
+ Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("i")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(
+ Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("big")) {
+ end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(
+ 1.25f));
+ } else if (tag.equalsIgnoreCase("small")) {
+ end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(
+ 0.8f));
+ } else if (tag.equalsIgnoreCase("font")) {
+ endFont(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan(
+ "monospace"));
+ } else if (tag.equalsIgnoreCase("a")) {
+ endA(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("u")) {
+ end(mSpannableStringBuilder, Underline.class,
+ new WPUnderlineSpan());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
+ } else if (tag.equalsIgnoreCase("strike")) {
+ end(mSpannableStringBuilder, Strike.class,
+ new StrikethroughSpan());
+ } else if (tag.length() == 2
+ && Character.toLowerCase(tag.charAt(0)) == 'h'
+ && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ endHeader(mSpannableStringBuilder);
+ }
+ } else {
+ if (tag.equalsIgnoreCase("html") || tag.equalsIgnoreCase("body")) {
+ return;
+ }
+
+ if (mysteryTagName.equals(tag)) {
+ mysteryTagFound = false;
+ mSpannableStringBuilder.append(mysteryTagContent);
+ }
+ // mTagHandler.handleTag(false, tag, mSpannableStringBuilder,
+ // mReader,
+ // mysteryTagContent);
+ }
+ }
+
+ private static void handleP(SpannableStringBuilder text) {
+ int len = text.length();
+
+ if (len >= 1 && text.charAt(len - 1) == '\n') {
+ if (len >= 2 && text.charAt(len - 2) == '\n') {
+ return;
+ }
+
+ text.append("\n");
+ return;
+ }
+
+ if (len != 0) {
+ text.append("\n\n");
+ }
+ }
+
+ private static void handleBr(SpannableStringBuilder text) {
+ text.append("\n");
+ }
+
+ private static Object getLast(Spanned text, Class<?> kind) {
+ /*
+ * This knows that the last returned object from getSpans() will be the
+ * most recently added.
+ */
+ Object[] objs = text.getSpans(0, text.length(), kind);
+
+ if (objs.length == 0) {
+ return null;
+ } else {
+ return objs[objs.length - 1];
+ }
+ }
+
+ private static void start(SpannableStringBuilder text, Object mark) {
+ int len = text.length();
+ text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void end(SpannableStringBuilder text, Class<?> kind,
+ Object repl) {
+ int len = text.length();
+ Object obj = getLast(text, kind);
+ int where = text.getSpanStart(obj);
+ if (where < 0)
+ where = 0;
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return;
+ }
+
+ private void startImg(SpannableStringBuilder text, Attributes attributes, WPHtml.ImageGetter img) {
+ if (mContext == null) return;
+
+ String src = attributes.getValue("android-uri");
+
+ Bitmap resizedBitmap = null;
+ try {
+ resizedBitmap = ImageUtils.getWPImageSpanThumbnailFromFilePath(mContext, src, mMaxImageWidth);
+ if (resizedBitmap == null && src != null) {
+ if (src.contains("video")) {
+ resizedBitmap = BitmapFactory.decodeResource(mContext.getResources(), org.wordpress.android.editor.R.drawable.media_movieclip);
+ } else {
+ resizedBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.media_image_placeholder);
+ }
+ }
+ } catch (OutOfMemoryError e) {
+ CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC, AppLog.T.UTILS);
+ }
+
+ if (resizedBitmap != null) {
+ int len = text.length();
+ text.append("\uFFFC");
+
+ Uri curStream = Uri.parse(src);
+
+ if (curStream == null) {
+ return;
+ }
+
+ WPImageSpan is = new WPImageSpan(mContext, resizedBitmap, curStream);
+
+ // get the MediaFile data from db
+ MediaFile mf = WordPress.wpDB.getMediaFile(src, mPost);
+ if (mf != null) {
+ is.setMediaFile(mf);
+ is.setImageSource(curStream);
+ text.setSpan(is, len, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ AlignmentSpan.Standard as = new AlignmentSpan.Standard(
+ Layout.Alignment.ALIGN_CENTER);
+ text.setSpan(as, text.getSpanStart(is), text.getSpanEnd(is),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ } else if (mPost != null) {
+ if (mPost.isLocalDraft()) {
+ if (attributes != null) {
+ text.append("<img");
+ for (int i = 0; i < attributes.getLength(); i++) {
+ String aName = attributes.getLocalName(i); // Attr name
+ if ("".equals(aName))
+ aName = attributes.getQName(i);
+ text.append(" ");
+ text.append(aName + "=\"" + attributes.getValue(i) + "\"");
+ }
+ text.append(" />\n");
+ }
+ }
+ } else if (src == null) {
+
+ //get regular src value from <img/> tag's src attribute
+ src = attributes.getValue("", "src");
+ Drawable d = null;
+
+ if (img != null) {
+ d = img.getDrawable(src);
+ }
+
+ if (d != null) {
+ int len = text.length();
+ text.append("\uFFFC");
+
+ text.setSpan(new ImageSpan(d, src), len, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ // noop - we're not showing a default image here
+ }
+
+ }
+ }
+
+ private static void startFont(SpannableStringBuilder text,
+ Attributes attributes) {
+ String color = attributes.getValue("", "color");
+ String face = attributes.getValue("", "face");
+
+ int len = text.length();
+ text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endFont(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Font.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Font f = (Font) obj;
+
+ if (!TextUtils.isEmpty(f.mColor)) {
+ if (f.mColor.startsWith("@")) {
+ Resources res = Resources.getSystem();
+ String name = f.mColor.substring(1);
+ int colorRes = res.getIdentifier(name, "color", "android");
+ if (colorRes != 0) {
+ ColorStateList colors = res.getColorStateList(colorRes);
+ text.setSpan(new TextAppearanceSpan(null, 0, 0, colors,
+ null), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ } else {
+ int c = getHtmlColor(f.mColor);
+ if (c != -1) {
+ text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ if (f.mFace != null) {
+ text.setSpan(new TypefaceSpan(f.mFace), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void startA(SpannableStringBuilder text,
+ Attributes attributes) {
+ String href = attributes.getValue("", "href");
+
+ int len = text.length();
+ text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endA(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Href.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Href h = (Href) obj;
+
+ if (h != null) {
+ if (h.mHref != null) {
+ text.setSpan(new URLSpan(h.mHref), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+ }
+
+ private static void endHeader(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Header.class);
+
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ // Back off not to change only the text, not the blank line.
+ while (len > where && text.charAt(len - 1) == '\n') {
+ len--;
+ }
+
+ if (where != len) {
+ Header h = (Header) obj;
+
+ text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]), where,
+ len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ text.setSpan(new StyleSpan(Typeface.BOLD), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ public void setDocumentLocator(Locator locator) {
+ }
+
+ public void startDocument() throws SAXException {
+ }
+
+ public void endDocument() throws SAXException {
+ }
+
+ public void startPrefixMapping(String prefix, String uri)
+ throws SAXException {
+ }
+
+ public void endPrefixMapping(String prefix) throws SAXException {
+ }
+
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ if (!mysteryTagFound) {
+ mysteryTagContent = "";
+ }
+
+ String eName = localName; // element name
+ if ("".equals(eName))
+ eName = qName; // not namespace-aware
+ mysteryTagContent += "<" + eName;
+ if (attributes != null) {
+ for (int i = 0; i < attributes.getLength(); i++) {
+ String aName = attributes.getLocalName(i); // Attr name
+ if ("".equals(aName))
+ aName = attributes.getQName(i);
+ mysteryTagContent += " ";
+ mysteryTagContent += aName + "=\"" + attributes.getValue(i)
+ + "\"";
+ }
+ }
+ mysteryTagContent += ">";
+
+ handleStartTag(localName, attributes);
+ }
+
+ public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+ if (mysteryTagFound) {
+ mysteryTagContent += "</" + localName + ">" + "\n";
+ }
+ handleEndTag(localName);
+ }
+
+ public void characters(char ch[], int start, int length)
+ throws SAXException {
+ StringBuilder sb = new StringBuilder();
+
+ /*
+ * Ignore whitespace that immediately follows other whitespace; newlines
+ * count as spaces.
+ */
+
+ for (int i = 0; i < length; i++) {
+ char c = ch[i + start];
+
+ if (c == ' ' || c == '\n') {
+ char pred;
+ int len = sb.length();
+
+ if (len == 0) {
+ len = mSpannableStringBuilder.length();
+
+ if (len == 0) {
+ pred = '\n';
+ } else {
+ pred = mSpannableStringBuilder.charAt(len - 1);
+ }
+ } else {
+ pred = sb.charAt(len - 1);
+ }
+
+ if (pred != ' ' && pred != '\n') {
+ sb.append(' ');
+ }
+ } else {
+ sb.append(c);
+ }
+ }
+
+ try {
+ if (mysteryTagFound) {
+ if (sb.length() < length)
+ mysteryTagContent += sb.toString().substring(start,
+ length - 1);
+ else
+ mysteryTagContent += sb.toString().substring(start, length);
+ } else
+ mSpannableStringBuilder.append(sb);
+ } catch (RuntimeException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+
+ public void ignorableWhitespace(char ch[], int start, int length)
+ throws SAXException {
+ }
+
+ public void processingInstruction(String target, String data)
+ throws SAXException {
+ }
+
+ public void skippedEntity(String name) throws SAXException {
+ }
+
+ private static class Bold {
+ }
+
+ private static class Italic {
+ }
+
+ private static class Underline {
+ }
+
+ private static class Big {
+ }
+
+ private static class Small {
+ }
+
+ private static class Monospace {
+ }
+
+ private static class Blockquote {
+ }
+
+ private static class Super {
+ }
+
+ private static class Sub {
+ }
+
+ private static class Strike {
+ }
+
+ private static class Font {
+ public String mColor;
+ public String mFace;
+
+ public Font(String color, String face) {
+ mColor = color;
+ mFace = face;
+ }
+ }
+
+ private static class Href {
+ public String mHref;
+
+ public Href(String href) {
+ mHref = href;
+ }
+ }
+
+ private static class Header {
+ private int mLevel;
+
+ public Header(int level) {
+ mLevel = level;
+ }
+ }
+
+ private static HashMap<String, Integer> COLORS = buildColorMap();
+
+ private static HashMap<String, Integer> buildColorMap() {
+ HashMap<String, Integer> map = new HashMap<String, Integer>();
+ map.put("aqua", 0x00FFFF);
+ map.put("black", 0x000000);
+ map.put("blue", 0x0000FF);
+ map.put("fuchsia", 0xFF00FF);
+ map.put("green", 0x008000);
+ map.put("grey", 0x808080);
+ map.put("lime", 0x00FF00);
+ map.put("maroon", 0x800000);
+ map.put("navy", 0x000080);
+ map.put("olive", 0x808000);
+ map.put("purple", 0x800080);
+ map.put("red", 0xFF0000);
+ map.put("silver", 0xC0C0C0);
+ map.put("teal", 0x008080);
+ map.put("white", 0xFFFFFF);
+ map.put("yellow", 0xFFFF00);
+ return map;
+ }
+
+ /**
+ * Converts an HTML color (named or numeric) to an integer RGB value.
+ *
+ * @param color
+ * Non-null color string.
+ * @return A color value, or {@code -1} if the color string could not be
+ * interpreted.
+ */
+ private static int getHtmlColor(String color) {
+ Integer i = COLORS.get(color.toLowerCase());
+ if (i != null) {
+ return i;
+ } else {
+ try {
+ return convertValueToInt(color, -1);
+ } catch (NumberFormatException nfe) {
+ return -1;
+ }
+ }
+ }
+
+ public static final int convertValueToInt(CharSequence charSeq,
+ int defaultValue) {
+ if (null == charSeq)
+ return defaultValue;
+
+ String nm = charSeq.toString();
+
+ // XXX This code is copied from Integer.decode() so we don't
+ // have to instantiate an Integer!
+
+ int sign = 1;
+ int index = 0;
+ int len = nm.length();
+ int base = 10;
+
+ if ('-' == nm.charAt(0)) {
+ sign = -1;
+ index++;
+ }
+
+ if ('0' == nm.charAt(index)) {
+ // Quick check for a zero by itself
+ if (index == (len - 1))
+ return 0;
+
+ char c = nm.charAt(index + 1);
+
+ if ('x' == c || 'X' == c) {
+ index += 2;
+ base = 16;
+ } else {
+ index++;
+ base = 8;
+ }
+ } else if ('#' == nm.charAt(index)) {
+ index++;
+ base = 16;
+ }
+
+ return Integer.parseInt(nm.substring(index), base) * sign;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPLinkMovementMethod.java b/WordPress/src/main/java/org/wordpress/android/util/WPLinkMovementMethod.java
new file mode 100644
index 000000000..f7069be7e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPLinkMovementMethod.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.util;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.text.style.URLSpan;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+/**
+ * Android's LinkMovementMethod crashes on malformed links, including links that have no
+ * protocol (ex: "example.com" instead of "http://example.com"). This class extends
+ * LinkMovementMethod to catch and ignore the exception.
+ */
+
+public class WPLinkMovementMethod extends LinkMovementMethod {
+ protected static WPLinkMovementMethod mMovementMethod;
+
+ public static WPLinkMovementMethod getInstance() {
+ if (mMovementMethod == null)
+ mMovementMethod = new WPLinkMovementMethod();
+ return mMovementMethod;
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView textView, Spannable buffer, MotionEvent event) {
+ try {
+ return super.onTouchEvent(textView, buffer, event) ;
+ } catch (ActivityNotFoundException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ // attempt to correct the tapped url then launch the intent to display it
+ showTappedUrl(textView.getContext(), fixTappedUrl(buffer));
+ return true;
+ }
+ }
+
+ private static String fixTappedUrl(Spannable buffer) {
+ if (buffer == null)
+ return null;
+
+ URLSpan urlSpans[] = buffer.getSpans(0, buffer.length(), URLSpan.class);
+ if (urlSpans.length == 0)
+ return null;
+
+ // note that there will be only one URLSpan (the one that was tapped)
+ String url = StringUtils.notNullStr(urlSpans[0].getURL());
+ if (Uri.parse(url).getScheme() == null)
+ return "http://" + url.trim();
+
+ return url.trim();
+ }
+
+ private static void showTappedUrl(Context context, String url) {
+ if (context == null || TextUtils.isEmpty(url))
+ return;
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ String readerToastUrlErrorIntent = context.getString(R.string.reader_toast_err_url_intent);
+ ToastUtils.showToast(context, String.format(readerToastUrlErrorIntent, url), ToastUtils.Duration.LONG);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPMeShortlinks.java b/WordPress/src/main/java/org/wordpress/android/util/WPMeShortlinks.java
new file mode 100644
index 000000000..041b988ac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPMeShortlinks.java
@@ -0,0 +1,146 @@
+package org.wordpress.android.util;
+
+/**
+ * Enable WP.me-powered shortlinks for Posts, Pages, and Blogs on WordPress.com or Jetpack powered sites.
+ * <p/>
+ * Shortlinks are a quick way to get short and simple links to your posts, pages, and blogs.
+ * They use the wp.me domain so you can have more space to write on social media sites.
+ * <p/>
+ * See: https://github.com/Automattic/jetpack/blob/master/modules/shortlinks.php
+ */
+
+import android.text.TextUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.util.AppLog.T;
+
+public class WPMeShortlinks {
+ /**
+ * Converts a base-10 number to base-62
+ *
+ * @param num base-10 number
+ * @return String base-62 number
+ */
+ public static String wpme_dec2sixtwo(double num) {
+ if (num == 0) {
+ return "0";
+ }
+
+ StringBuilder out;
+ try {
+ String index = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ out = new StringBuilder();
+
+ if (num < 0) {
+ out.append('-');
+ num = Math.abs(num);
+ }
+
+ double t = Math.floor(Math.log10(num) / Math.log10(62));
+ for (; t >= 0; t--) {
+ int a = (int) Math.floor(num / Math.pow(62, t));
+ out.append(index.substring(a, a + 1));
+ num = num - (a * Math.pow(62, t));
+ }
+ return out.toString();
+ } catch (IndexOutOfBoundsException e) {
+ AppLog.e(T.UTILS, "Connot convert number " + num + " to base 62", e);
+ }
+ return null;
+ }
+
+ /**
+ * Returns The post shortlink
+ *
+ * @param blog Blog that contains the post or the page
+ * @param post Post or page we want calculate the shortlink
+ * @return String The blog shortlink or null (null is returned if the blog object is empty, or it's not a wpcom/jetpack blog, or in case of errors).
+ */
+ public static String getPostShortlink(Blog blog, Post post) {
+ if (post == null || blog == null) {
+ return null;
+ }
+
+ if (!blog.isDotcomFlag() && !blog.isJetpackPowered()) {
+ return null;
+ }
+
+ String postID = post.getRemotePostId();
+ if (postID == null) {
+ return null;
+ }
+
+ String id = null;
+ String type = null;
+
+ String postName = StringUtils.notNullStr(post.getSlug());
+ if (post.getStatusEnum() == PostStatus.PUBLISHED && postName.length() > 0 && postName.length() <= 8 &&
+ !postName.contains("%") && !postName.contains("-")) {
+ id = postName;
+ type = "s";
+ } else {
+ try {
+ id = wpme_dec2sixtwo(Double.parseDouble(postID));
+ } catch (NumberFormatException e) {
+ AppLog.e(T.UTILS, "Remote postID cannot be converted to double" + postID, e);
+ return null;
+ }
+
+ if (post.isPage()) {
+ type = "P";
+ } else {
+ type = "p";
+ }
+ }
+
+ // Calculate the blog shortlink
+ String blogShortlink = null;
+ try {
+ double blogID = blog.isDotcomFlag() ? blog.getRemoteBlogId() : Double.parseDouble(blog.getApi_blogid());
+ blogShortlink = wpme_dec2sixtwo(blogID);
+ } catch (NumberFormatException e) {
+ AppLog.e(T.UTILS, "Remote Blog ID cannot be converted to double", e);
+ return null;
+ }
+
+ if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id) || TextUtils.isEmpty(blogShortlink)) {
+ return null;
+ }
+
+ return "http://wp.me/" + type + blogShortlink + "-" + id;
+ }
+
+ public static String getPostShortlink(Post post) {
+ Blog blog = WordPress.wpDB.instantiateBlogByLocalId(post.getLocalTableBlogId());
+ return getPostShortlink(blog, post);
+ }
+
+ /**
+ * Returns The blog shortlink
+ *
+ * @param blog Blog we want calculate the shortlink
+ * @return String The blog shortlink or null (null is returned if the blog object is empty, or it's not a wpcom/jetpack blog, or in case of errors).
+ */
+ public static String getBlogShortlink(Blog blog) {
+ if (blog == null) {
+ return null;
+ }
+
+ if (!blog.isDotcomFlag() && !blog.isJetpackPowered()) {
+ return null;
+ }
+
+ try {
+ double blogID = blog.isDotcomFlag() ? blog.getRemoteBlogId() : Double.parseDouble(blog.getApi_blogid());
+ String shortlink = wpme_dec2sixtwo(blogID);
+ String shortlinkWithProtocol = (shortlink == null) ? blog.getHomeURL() : "http://wp.me/" + shortlink;
+ return shortlinkWithProtocol;
+ } catch (NumberFormatException e) {
+ AppLog.e(T.UTILS, "Remote Blog ID cannot be converted to double ", e);
+ return blog.getHomeURL();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java
new file mode 100644
index 000000000..be9004005
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java
@@ -0,0 +1,305 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Typeface;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceGroup;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.TypedValue;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.widgets.TypefaceCache;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Design guidelines for Calypso-styled Site Settings (and likely other screens)
+ */
+
+public class WPPrefUtils {
+
+ /**
+ * Length of a {@link String} (representing a language code) when there is no region included.
+ * For example: "en" contains no region, "en_US" contains a region (US)
+ *
+ * Used to parse a language code {@link String} when creating a {@link Locale}.
+ */
+ private static final int NO_REGION_LANG_CODE_LEN = 2;
+
+ /**
+ * Index of a language code {@link String} where the region code begins. The language code
+ * format is cc_rr, where cc is the country code (e.g. en, es, az) and rr is the region code
+ * (e.g. us, au, gb).
+ */
+ private static final int REGION_SUBSTRING_INDEX = 3;
+
+ /**
+ * Gets a preference and sets the {@link android.preference.Preference.OnPreferenceChangeListener}.
+ */
+ public static Preference getPrefAndSetClickListener(PreferenceFragment prefFrag,
+ int id,
+ Preference.OnPreferenceClickListener listener) {
+ Preference pref = prefFrag.findPreference(prefFrag.getString(id));
+ if (pref != null) pref.setOnPreferenceClickListener(listener);
+ return pref;
+ }
+
+ /**
+ * Gets a preference and sets the {@link android.preference.Preference.OnPreferenceChangeListener}.
+ */
+ public static Preference getPrefAndSetChangeListener(PreferenceFragment prefFrag,
+ int id,
+ Preference.OnPreferenceChangeListener listener) {
+ Preference pref = prefFrag.findPreference(prefFrag.getString(id));
+ if (pref != null) pref.setOnPreferenceChangeListener(listener);
+ return pref;
+ }
+
+ /**
+ * Removes a {@link Preference} from the {@link PreferenceCategory} with the given key.
+ */
+ public static void removePreference(PreferenceFragment prefFrag, int parentKey, int prefKey) {
+ String parentName = prefFrag.getString(parentKey);
+ String prefName = prefFrag.getString(prefKey);
+ PreferenceGroup parent = (PreferenceGroup) prefFrag.findPreference(parentName);
+ Preference child = prefFrag.findPreference(prefName);
+
+ if (parent != null && child != null) {
+ parent.removePreference(child);
+ }
+ }
+
+ /**
+ * Font : Default
+ * Style : Normal
+ * Variation : Normal
+ */
+ public static Typeface getNormalTypeface(Context context) {
+ return TypefaceCache.getTypeface(context,
+ TypefaceCache.FAMILY_DEFAULT, Typeface.NORMAL);
+ }
+
+ /**
+ * Font : Default
+ * Style : Bold
+ * Variation : Light
+ */
+ public static Typeface getSemiboldTypeface(Context context) {
+ return TypefaceCache.getTypeface(context,
+ TypefaceCache.FAMILY_DEFAULT_LIGHT, Typeface.BOLD);
+ }
+
+ /**
+ * Styles a {@link TextView} to display a large title against a dark background.
+ */
+ public static void layoutAsLightTitle(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_extra_large);
+ setTextViewAttributes(view, size, R.color.white, getSemiboldTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display a large title against a light background.
+ */
+ public static void layoutAsDarkTitle(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_extra_large);
+ setTextViewAttributes(view, size, R.color.grey_dark, getSemiboldTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display medium sized text as a header with sub-elements.
+ */
+ public static void layoutAsSubhead(TextView view) {
+ int color = view.isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10;
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_large);
+ setTextViewAttributes(view, size, color, getNormalTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display smaller text.
+ */
+ public static void layoutAsBody1(TextView view) {
+ int color = view.isEnabled() ? R.color.grey_darken_10 : R.color.grey_lighten_10;
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium);
+ setTextViewAttributes(view, size, color, getNormalTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display smaller text with a dark grey color.
+ */
+ public static void layoutAsBody2(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium);
+ setTextViewAttributes(view, size, R.color.grey_darken_10, getSemiboldTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display very small helper text.
+ */
+ public static void layoutAsCaption(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_small);
+ setTextViewAttributes(view, size, R.color.grey_darken_10, getNormalTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display text in a button.
+ */
+ public static void layoutAsFlatButton(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium);
+ setTextViewAttributes(view, size, R.color.blue_medium, getSemiboldTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display text in a button.
+ */
+ public static void layoutAsRaisedButton(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium);
+ setTextViewAttributes(view, size, R.color.white, getSemiboldTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display text in an editable text field.
+ */
+ public static void layoutAsInput(EditText view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_large);
+ setTextViewAttributes(view, size, R.color.grey_dark, getNormalTypeface(view.getContext()));
+ view.setHintTextColor(view.getResources().getColor(R.color.grey_lighten_10));
+ view.setTextColor(view.getResources().getColor(R.color.grey_dark));
+ view.setSingleLine(true);
+ }
+
+ /**
+ * Styles a {@link TextView} to display selected numbers in a {@link android.widget.NumberPicker}.
+ */
+ public static void layoutAsNumberPickerSelected(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_triple_extra_large);
+ setTextViewAttributes(view, size, R.color.blue_medium, getSemiboldTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display non-selected numbers in a {@link android.widget.NumberPicker}.
+ */
+ public static void layoutAsNumberPickerPeek(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_large);
+ setTextViewAttributes(view, size, R.color.grey_dark, getNormalTypeface(view.getContext()));
+ }
+
+ /**
+ * Styles a {@link TextView} to display text in a dialog message.
+ */
+ public static void layoutAsDialogMessage(TextView view) {
+ int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_small);
+ setTextViewAttributes(view, size, R.color.grey_darken_10, getNormalTypeface(view.getContext()));
+ }
+
+ public static void setTextViewAttributes(TextView textView, int size, int colorRes, Typeface typeface) {
+ textView.setTypeface(typeface);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
+ textView.setTextColor(textView.getResources().getColor(colorRes));
+ }
+
+ /**
+ * Gets a locale for the given language code.
+ */
+ public static Locale languageLocale(String languageCode) {
+ if (TextUtils.isEmpty(languageCode)) return LanguageUtils.getCurrentDeviceLanguage(WordPress.getContext());
+
+ if (languageCode.length() > NO_REGION_LANG_CODE_LEN) {
+ return new Locale(languageCode.substring(0, NO_REGION_LANG_CODE_LEN),
+ languageCode.substring(REGION_SUBSTRING_INDEX));
+ }
+
+ return new Locale(languageCode);
+ }
+
+ /**
+ * Creates a map from language codes to WordPress language IDs.
+ */
+ public static Map<String, String> generateLanguageMap(Activity activity) {
+ String[] languageIds = activity.getResources().getStringArray(R.array.lang_ids);
+ String[] languageCodes = activity.getResources().getStringArray(R.array.language_codes);
+
+ Map<String, String> languageMap = new HashMap<>();
+ for (int i = 0; i < languageIds.length && i < languageCodes.length; ++i) {
+ languageMap.put(languageCodes[i], languageIds[i]);
+ }
+
+ return languageMap;
+ }
+
+ /**
+ * Generates display strings for given language codes. Used as entries in language preference.
+ */
+ @Nullable
+ public static Pair<String[], String[]> createSortedLanguageDisplayStrings(CharSequence[] languageCodes,
+ Locale locale) {
+ if (languageCodes == null || languageCodes.length < 1) return null;
+
+ ArrayList<String> entryStrings = new ArrayList<>(languageCodes.length);
+ for (int i = 0; i < languageCodes.length; ++i) {
+ // "__" is used to sort the language code with the display string so both arrays are sorted at the same time
+ entryStrings.add(i, StringUtils.capitalize(
+ getLanguageString(languageCodes[i].toString(), locale)) + "__" + languageCodes[i]);
+ }
+
+ Collections.sort(entryStrings, Collator.getInstance(locale));
+
+ String[] sortedEntries = new String[languageCodes.length];
+ String[] sortedValues = new String[languageCodes.length];
+
+ for (int i = 0; i < entryStrings.size(); ++i) {
+ // now, we can split the sorted array to extract the display string and the language code
+ String[] split = entryStrings.get(i).split("__");
+ sortedEntries[i] = split[0];
+ sortedValues[i] = split[1];
+ }
+
+ return new Pair<>(sortedEntries, sortedValues);
+ }
+
+ /**
+ * Generates detail display strings in the currently selected locale. Used as detail text
+ * in language preference dialog.
+ */
+ @Nullable
+ public static String[] createLanguageDetailDisplayStrings(CharSequence[] languageCodes) {
+ if (languageCodes == null || languageCodes.length < 1) return null;
+
+ String[] detailStrings = new String[languageCodes.length];
+ for (int i = 0; i < languageCodes.length; ++i) {
+ detailStrings[i] = StringUtils.capitalize(getLanguageString(
+ languageCodes[i].toString(), WPPrefUtils.languageLocale(languageCodes[i].toString())));
+ }
+
+ return detailStrings;
+ }
+
+ /**
+ * Return a non-null display string for a given language code.
+ */
+ public static String getLanguageString(String languageCode, Locale displayLocale) {
+ if (languageCode == null || languageCode.length() < 2 || languageCode.length() > 6) {
+ return "";
+ }
+
+ Locale languageLocale = WPPrefUtils.languageLocale(languageCode);
+ String displayLanguage = StringUtils.capitalize(languageLocale.getDisplayLanguage(displayLocale));
+ String displayCountry = languageLocale.getDisplayCountry(displayLocale);
+
+ if (!TextUtils.isEmpty(displayCountry)) {
+ return displayLanguage + " (" + displayCountry + ")";
+ }
+ return displayLanguage;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java b/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java
new file mode 100644
index 000000000..8793ccf0c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java
@@ -0,0 +1,430 @@
+/**
+ * Interface to the WordPress.com REST API.
+ */
+package org.wordpress.android.util;
+
+import android.os.AsyncTask;
+
+import com.android.volley.DefaultRetryPolicy;
+import com.android.volley.Request.Method;
+import com.android.volley.RequestQueue;
+import com.android.volley.RetryPolicy;
+import com.android.volley.VolleyError;
+import com.wordpress.rest.Oauth;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+import com.wordpress.rest.RestRequest.ErrorListener;
+import com.wordpress.rest.RestRequest.Listener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Note;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+public class WPRestClient {
+
+ private static final String NOTIFICATION_FIELDS="id,type,unread,body,subject,timestamp";
+ private static final String COMMENT_REPLY_CONTENT_FIELD="content";
+
+ private RestClient mRestClient;
+ private Authenticator mAuthenticator;
+
+ /** Socket timeout in milliseconds for rest requests */
+ public static final int REST_TIMEOUT_MS = 30000;
+
+ /** Default number of retries for POST rest requests */
+ public static final int REST_MAX_RETRIES_POST = 0;
+
+ /** Default number of retries for GET rest requests */
+ public static final int REST_MAX_RETRIES_GET = 3;
+
+ /** Default backoff multiplier for rest requests */
+ public static final float REST_BACKOFF_MULT = 2f;
+
+ public WPRestClient(RequestQueue queue, Authenticator authenticator){
+ this(queue, authenticator, RestClient.REST_CLIENT_VERSIONS.V1);
+ }
+
+ public WPRestClient(RequestQueue queue, Authenticator authenticator, RestClient.REST_CLIENT_VERSIONS version){
+ mAuthenticator = authenticator;
+ mRestClient = new RestClient(queue, version);
+ mRestClient.setUserAgent(WordPress.getUserAgent());
+ }
+
+ /**
+ * Reply to a comment using a Note.Reply object.
+ *
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/
+ */
+ public void replyToComment(Note.Reply reply, Listener listener, ErrorListener errorListener){
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(COMMENT_REPLY_CONTENT_FIELD, reply.getContent());
+ post(reply.getRestPath(), params, null, listener, errorListener);
+ }
+ /**
+ * Reply to a comment.
+ *
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/
+ */
+ public void replyToComment(String siteId, String commentId, String content, Listener listener, ErrorListener errorListener){
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(COMMENT_REPLY_CONTENT_FIELD, content);
+ String path = String.format("sites/%s/comments/%s/replies/new", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+ /**
+ * Follow a site given an ID or domain
+ *
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/new/
+ */
+ public void followSite(String siteId, Listener listener, ErrorListener errorListener){
+ String path = String.format("sites/%s/follows/new", siteId);
+ post(path, listener, errorListener);
+ }
+ /**
+ * Unfollow a site given an ID or domain
+ *
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/mine/delete/
+ */
+ public void unfollowSite(String siteId, Listener listener, ErrorListener errorListener){
+ String path = String.format("sites/%s/follows/mine/delete", siteId);
+ post(path, listener, errorListener);
+ }
+
+ /**
+ * Update the seen timestamp.
+ *
+ * https://developer.wordpress.com/docs/api/1/post/notifications/seen
+ */
+ public void markNotificationsSeen(String timestamp, Listener listener, ErrorListener errorListener){
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("time", timestamp);
+ post("notifications/seen", params, null, listener, errorListener);
+ }
+ /**
+ * Moderate a comment.
+ *
+ * http://developer.wordpress.com/docs/api/1/sites/%24site/comments/%24comment_ID/
+ */
+ public void moderateComment(String siteId, String commentId, String status, Listener listener, ErrorListener errorListener){
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("status", status);
+ String path = String.format("sites/%s/comments/%s/", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Get all a site's themes
+ */
+ public void getThemes(String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes?limit=%d&offset=%d", siteId, limit, offset);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Set a site's theme
+ */
+ public void setTheme(String siteId, String themeId, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("theme", themeId);
+ String path = String.format("sites/%s/themes/mine", siteId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Get a site's current theme
+ */
+ public void getCurrentTheme(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/mine", siteId);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for clicks
+ */
+ public void getStatsClicks(String siteId, String date, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/stats/clicks?date=%s", siteId, date);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for geoviews (views by country)
+ */
+ public void getStatsGeoviews(String siteId, String date, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/stats/country-views?date=%s", siteId, date);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for most commented posts
+ */
+ public void getStatsMostCommented(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = "stats/most_commented";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog", siteId);
+ getXL(path, params, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for top commenters
+ */
+ public void getStatsTopCommenters(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = "stats/top_commenters";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog", siteId);
+ getXL(path, params, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for referrers
+ */
+ public void getStatsReferrers(String siteId, String date, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/stats/referrers?date=%s", siteId, date);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for search engine terms
+ */
+ public void getStatsSearchEngineTerms(String siteId, String date, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/stats/search-terms?date=%s", siteId, date);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for tags and categories
+ */
+ public void getStatsTagsAndCategories(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = "stats/tags_and_categories";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog", siteId);
+ getXL(path, params, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for top authors
+ */
+ public void getStatsTopAuthors(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = "stats/top_authors";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog", siteId);
+ getXL(path, params, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for top posts and pages
+ */
+ public void getStatsTopPosts(String siteId, String date, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/stats/top-posts?date=%s", siteId, date);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats for video plays
+ */
+ public void getStatsVideoPlays(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = "stats/video_plays";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog", siteId);
+ getXL(path, params, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats summary
+ */
+ public void getStatsSummary(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/stats", siteId);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get a site's stats summary for videos
+ */
+ public void getStatsVideoSummary(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = "stats/video_summary";
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("blog", siteId);
+ getXL(path, params, listener, errorListener);
+ }
+
+ public void getSiteDescription(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("rest/v1.1/sites/%s", siteId);
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("fields", "description");
+ get(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * This method is for simulating stats APIs using the XL Studio API simulator. It should be removed once the other APIs are implemented.
+ **/
+ public void getXL(String path, Map<String, String> params, final Listener listener, final ErrorListener errorListener) {
+
+ path = "https://simulator.xlstudio.com/apis/32/" + path;
+
+ final String url_path = path;
+
+ new AsyncTask<Void, Void, JSONObject>() {
+
+ @Override
+ protected JSONObject doInBackground(Void... params) {
+
+ URL url;
+ HttpURLConnection conn;
+ BufferedReader rd;
+ String line;
+ String result = "";
+
+ try {
+ url = new URL(url_path);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.addRequestProperty("X-SIMULATOR-ACCESS-KEY", "bc88864498a705657486edb636196e31");
+ rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ while ((line = rd.readLine()) != null) {
+ result += line;
+ }
+ rd.close();
+
+ return new JSONObject(result);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ protected void onPostExecute(JSONObject result) {
+ if (result != null)
+ listener.onResponse(result);
+ else
+ errorListener.onErrorResponse(new VolleyError("JSONObject null"));
+ };
+
+ }.execute();
+
+ }
+
+ /**
+ *
+ * Make GET request
+ */
+ public void get(String path, Listener listener, ErrorListener errorListener){
+ get(path, null, null, listener, errorListener);
+ }
+ /**
+ * Make GET request with params
+ */
+ public void get(String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener, ErrorListener errorListener){
+ // turn params into querystring
+
+ RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(path, params), null, listener, errorListener);
+ if(retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT);
+ }
+ request.setRetryPolicy(retryPolicy);
+ Request authCheck = new Request(request, errorListener);
+ authCheck.send();
+ }
+ /**
+ * Make POST request
+ */
+ public void post(String path, Listener listener, ErrorListener errorListener){
+ post(path, null, null, listener, errorListener);
+ }
+ /**
+ * Make POST request with params
+ */
+ public void post(final String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener, ErrorListener errorListener){
+ final RestRequest request = mRestClient.makeRequest(Method.POST, mRestClient.getAbsoluteURL(path), params, listener, errorListener);
+ if(retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST, REST_BACKOFF_MULT); //Do not retry on failure
+ }
+ request.setRetryPolicy(retryPolicy);
+ Request authCheck = new Request(request, errorListener);
+ authCheck.send();
+ }
+ /**
+ * Interface that provides a method that should perform the necessary task to make sure
+ * the provided Request will be authenticated.
+ *
+ * The Authenticator must call Request.send() when it has completed its operations. For
+ * convenience the Request class provides Request.setAccessToken so the Authenticator can
+ * easily update the access token.
+ */
+ public interface Authenticator {
+ void authenticate(Request request);
+ }
+
+ /**
+ * Encapsulates the behaviour for asking the Authenticator for an access token. This
+ * allows the request maker to disregard the authentication state when making requests.
+ */
+ public class Request {
+
+ static public final String SITE_PREFIX = "https://public-api.wordpress.com/rest/v1/sites/";
+ RestRequest mRequest;
+ RestRequest.ErrorListener mListener;
+
+ protected Request(RestRequest request, ErrorListener listener){
+ mRequest = request;
+ mListener = listener;
+ }
+
+ public String getSiteId() {
+ // parse out the site id from the url
+ String url = mRequest.getUrl();
+
+ if (url.startsWith(SITE_PREFIX) && !SITE_PREFIX.equals(url)) {
+ int marker = SITE_PREFIX.length();
+ if (url.indexOf("/", marker) < marker)
+ return null;
+ return url.substring(marker, url.indexOf("/", marker));
+ }
+ // not a sites/$siteId request
+ return null;
+ }
+
+ /**
+ * Attempt to send the request, checks to see if we have an access token and if not
+ * asks the Authenticator to authenticate the request.
+ *
+ * If no Authenticator is provided the request is always sent.
+ */
+ protected void send(){
+ if (mAuthenticator == null) {
+ mRestClient.send(mRequest);
+ } else {
+ mAuthenticator.authenticate(this);
+ }
+ }
+
+ public void sendWithAccessToken(String token){
+ mRequest.setAccessToken(token);
+ mRestClient.send(mRequest);
+ }
+
+ public void sendWithAccessToken(Oauth.Token token){
+ sendWithAccessToken(token.toString());
+ }
+
+ /**
+ * If an access token cannot be obtained the request can be aborted and the
+ * handler's onFailure method is called
+ */
+ public void abort(VolleyError error){
+ if (mListener != null) {
+ mListener.onErrorResponse(error);
+ }
+ }
+
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPUrlUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPUrlUtils.java
new file mode 100644
index 000000000..cc77de40b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPUrlUtils.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util;
+
+import java.net.URI;
+import java.net.URL;
+
+public class WPUrlUtils {
+
+ public static boolean safeToAddWordPressComAuthToken(String url) {
+ return UrlUtils.isHttps(url) && isWordPressCom(url);
+ }
+
+ public static boolean safeToAddWordPressComAuthToken(URL url) {
+ return UrlUtils.isHttps(url) && isWordPressCom(url);
+ }
+
+ public static boolean safeToAddWordPressComAuthToken(URI uri) {
+ return UrlUtils.isHttps(uri) && isWordPressCom(uri);
+ }
+
+ public static boolean isWordPressCom(String url) {
+ return UrlUtils.getHost(url).endsWith(".wordpress.com") || UrlUtils.getHost(url).equals("wordpress.com");
+ }
+
+ public static boolean isWordPressCom(URL url) {
+ if (url == null) {
+ return false;
+ }
+ return url.getHost().endsWith(".wordpress.com") || url.getHost().equals("wordpress.com");
+ }
+
+ public static boolean isWordPressCom(URI uri) {
+ if (uri == null || uri.getHost() == null) {
+ return false;
+ }
+ return uri.getHost().endsWith(".wordpress.com") || uri.getHost().equals("wordpress.com");
+ }
+
+ public static boolean isGravatar(URL url) {
+ if (url == null) {
+ return false;
+ }
+ return url.getHost().equals("gravatar.com") || url.getHost().endsWith(".gravatar.com");
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPWebViewClient.java b/WordPress/src/main/java/org/wordpress/android/util/WPWebViewClient.java
new file mode 100644
index 000000000..e81c58041
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPWebViewClient.java
@@ -0,0 +1,121 @@
+package org.wordpress.android.util;
+
+import android.graphics.Bitmap;
+import android.net.http.SslError;
+import android.text.TextUtils;
+import android.webkit.HttpAuthHandler;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.SelfSignedSSLCertsManager;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+/**
+ * WebViewClient that is capable of handling HTTP authentication requests using the HTTP
+ * username and password of the blog configured for this activity.
+ */
+public class WPWebViewClient extends URLFilteredWebViewClient {
+ private final Blog mBlog;
+ private String mToken;
+
+ public WPWebViewClient(Blog blog) {
+ super();
+ this.mBlog = blog;
+ mToken = AccountHelper.getDefaultAccount().getAccessToken();
+ }
+
+ public WPWebViewClient(Blog blog, List<String> urls) {
+ super(urls);
+ this.mBlog = blog;
+ mToken = AccountHelper.getDefaultAccount().getAccessToken();
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ }
+
+ @Override
+ public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
+ if (mBlog != null && mBlog.hasValidHTTPAuthCredentials()) {
+ // Check that the HTTP AUth protected domain is the same of the blog. Do not send current blog's HTTP
+ // AUTH credentials to external site.
+ // NOTE: There is still a small security hole here, since the realm is not considered when getting
+ // the password. Unfortunately the real is not stored when setting up the blog, and we cannot compare it
+ // at this point.
+ String domainFromHttpAuthRequest = UrlUtils.getHost(UrlUtils.addUrlSchemeIfNeeded(host, false));
+ String currentBlogDomain = UrlUtils.getHost(mBlog.getUrl());
+ if (domainFromHttpAuthRequest.equals(currentBlogDomain)) {
+ handler.proceed(mBlog.getHttpuser(), mBlog.getHttppassword());
+ return;
+ }
+ }
+ // TODO: If there is no match show the HTTP Auth dialog here. Like a normal browser usually does...
+ super.onReceivedHttpAuthRequest(view, handler, host, realm);
+ }
+
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ try {
+ if (SelfSignedSSLCertsManager.getInstance(view.getContext()).isCertificateTrusted(error.getCertificate())) {
+ handler.proceed();
+ return;
+ }
+ } catch (GeneralSecurityException e) {
+ // Do nothing
+ } catch (IOException e) {
+ // Do nothing
+ }
+
+ super.onReceivedSslError(view, handler, error);
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String stringUrl) {
+ URL imageUrl = null;
+ if (mBlog != null && mBlog.isPrivate() && UrlUtils.isImageUrl(stringUrl)) {
+ try {
+ imageUrl = new URL(UrlUtils.makeHttps(stringUrl));
+ } catch (MalformedURLException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+
+ // Intercept requests for private images and add the WP.com authorization header
+ if (imageUrl != null &&
+ WPUrlUtils.safeToAddWordPressComAuthToken(imageUrl) &&
+ !TextUtils.isEmpty(mToken)) {
+ try {
+ // Force use of HTTPS for the resource, otherwise the request will fail for private sites
+ HttpURLConnection urlConnection = (HttpURLConnection) imageUrl.openConnection();
+ urlConnection.setRequestProperty("Authorization", "Bearer " + mToken);
+ urlConnection.setReadTimeout(WPRestClient.REST_TIMEOUT_MS);
+ urlConnection.setConnectTimeout(WPRestClient.REST_TIMEOUT_MS);
+ WebResourceResponse response = new WebResourceResponse(urlConnection.getContentType(),
+ urlConnection.getContentEncoding(),
+ urlConnection.getInputStream());
+ return response;
+ } catch (ClassCastException e) {
+ AppLog.e(AppLog.T.POSTS, "Invalid connection type - URL: " + stringUrl);
+ } catch (MalformedURLException e) {
+ AppLog.e(AppLog.T.POSTS, "Malformed URL: " + stringUrl);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, "Invalid post detail request: " + e.getMessage());
+ }
+ }
+ return super.shouldInterceptRequest(view, stringUrl);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AuthErrorDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/widgets/AuthErrorDialogFragment.java
new file mode 100644
index 000000000..65d677c42
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/AuthErrorDialogFragment.java
@@ -0,0 +1,63 @@
+package org.wordpress.android.widgets;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.ActivityLauncher;
+
+/**
+ * An alert dialog fragment for XML-RPC authentication failures
+ */
+public class AuthErrorDialogFragment extends DialogFragment {
+ public static int DEFAULT_RESOURCE_ID = -1;
+
+ private int mMessageId = R.string.incorrect_credentials;
+ private int mTitleId = R.string.connection_error;
+
+ public void setWPComTitleMessage(int titleResourceId, int messageResourceId) {
+ if (titleResourceId != DEFAULT_RESOURCE_ID) {
+ mTitleId = titleResourceId;
+ } else {
+ mTitleId = R.string.connection_error;
+ }
+
+ if (messageResourceId != DEFAULT_RESOURCE_ID) {
+ mMessageId = messageResourceId;
+ } else {
+ mMessageId = R.string.incorrect_credentials;
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.setCancelable(true);
+ int style = DialogFragment.STYLE_NORMAL, theme = 0;
+ setStyle(style, theme);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder b = new AlertDialog.Builder(getActivity());
+ b.setTitle(mTitleId);
+ b.setMessage(mMessageId);
+ b.setCancelable(true);
+ b.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ActivityLauncher.viewBlogSettingsForResult(getActivity(), WordPress.getCurrentBlog());
+ }
+ });
+ b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ });
+ return b.create();
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/CheckedLinearLayout.java b/WordPress/src/main/java/org/wordpress/android/widgets/CheckedLinearLayout.java
new file mode 100644
index 000000000..990274379
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/CheckedLinearLayout.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.CheckedTextView;
+import android.widget.LinearLayout;
+
+public class CheckedLinearLayout extends LinearLayout implements Checkable {
+ private CheckedTextView mCheckbox;
+
+ public CheckedLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ View v = getChildAt(i);
+ if (v instanceof CheckedTextView) {
+ mCheckbox = (CheckedTextView)v;
+ }
+ }
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mCheckbox != null ? mCheckbox.isChecked() : false;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mCheckbox != null) {
+ mCheckbox.setChecked(checked);
+ }
+ }
+
+ @Override
+ public void toggle() {
+ if (mCheckbox != null) {
+ mCheckbox.toggle();
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/DividerItemDecoration.java b/WordPress/src/main/java/org/wordpress/android/widgets/DividerItemDecoration.java
new file mode 100644
index 000000000..c61f9e17e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/DividerItemDecoration.java
@@ -0,0 +1,104 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class DividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.listDivider
+ };
+
+ public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
+
+ public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
+
+ private Drawable mDivider;
+
+ private int mOrientation;
+
+ public DividerItemDecoration(Context context, int orientation) {
+ final TypedArray a = context.obtainStyledAttributes(ATTRS);
+ mDivider = a.getDrawable(0);
+ a.recycle();
+ setOrientation(orientation);
+ }
+
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
+ throw new IllegalArgumentException("invalid orientation");
+ }
+ mOrientation = orientation;
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent) {
+ if (mOrientation == VERTICAL_LIST) {
+ drawVertical(c, parent);
+ } else {
+ drawHorizontal(c, parent);
+ }
+ }
+
+ public void drawVertical(Canvas c, RecyclerView parent) {
+ final int left = parent.getPaddingLeft();
+ final int right = parent.getWidth() - parent.getPaddingRight();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int top = child.getBottom() + params.bottomMargin;
+ final int bottom = top + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ public void drawHorizontal(Canvas c, RecyclerView parent) {
+ final int top = parent.getPaddingTop();
+ final int bottom = parent.getHeight() - parent.getPaddingBottom();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int left = child.getRight() + params.rightMargin;
+ final int right = left + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
+ if (mOrientation == VERTICAL_LIST) {
+ outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
+ } else {
+ outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/FlowLayout.java b/WordPress/src/main/java/org/wordpress/android/widgets/FlowLayout.java
new file mode 100644
index 000000000..c0b80cf73
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/FlowLayout.java
@@ -0,0 +1,135 @@
+/*
+ * Sample FlowLayout wrote by Romain Guy: http://www.parleys.com/play/514892280364bc17fc56c0e2/chapter38/about
+ * Fixed and tweaked since
+ */
+
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import org.wordpress.android.R;
+
+public class FlowLayout extends ViewGroup {
+ private int mHorizontalSpacing;
+ private int mVerticalSpacing;
+ private Paint mPaint;
+
+ public FlowLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
+ try {
+ mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_horizontalSpacing, 0);
+ mVerticalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_verticalSpacing, 0);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft();
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+
+ boolean growHeight = widthMode != MeasureSpec.UNSPECIFIED;
+
+ int width = 0;
+ int height = getPaddingTop();
+
+ int currentWidth = getPaddingLeft();
+ int currentHeight = 0;
+
+ boolean newLine = false;
+ int spacing = 0;
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ spacing = mHorizontalSpacing;
+ if (lp.horizontalSpacing >= 0) {
+ spacing = lp.horizontalSpacing;
+ }
+
+ if (growHeight && currentWidth + child.getMeasuredWidth() > widthSize) {
+ height += currentHeight + mVerticalSpacing;
+ currentHeight = 0;
+ width = Math.max(width, currentWidth - spacing);
+ currentWidth = getPaddingLeft();
+ newLine = true;
+ } else {
+ newLine = false;
+ }
+
+ lp.x = currentWidth;
+ lp.y = height;
+
+ currentWidth += child.getMeasuredWidth() + spacing;
+ currentHeight = Math.max(currentHeight, child.getMeasuredHeight());
+ }
+
+ if (!newLine) {
+ width = Math.max(width, currentWidth - spacing);
+ }
+ width += getPaddingRight();
+ height += currentHeight + getPaddingBottom();
+
+ setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams;
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p.width, p.height);
+ }
+
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ int x;
+ int y;
+
+ public int horizontalSpacing;
+
+ public LayoutParams(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout_LayoutParams);
+ try {
+ horizontalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_LayoutParams_layout_horizontalSpacing, -1);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/HeaderGridView.java b/WordPress/src/main/java/org/wordpress/android/widgets/HeaderGridView.java
new file mode 100644
index 000000000..9248ee3ce
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/HeaderGridView.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.WrapperListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link GridView} that supports adding header rows in a
+ * very similar way to {@link ListView}.
+ * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}
+ */
+public class HeaderGridView extends GridView {
+ private static final String TAG = "HeaderGridView";
+
+ /**
+ * A class that represents a fixed view in a list, for example a header at the top
+ * or a footer at the bottom.
+ */
+ private static class FixedViewInfo {
+ /** The view to add to the grid */
+ public View view;
+ public ViewGroup viewContainer;
+ /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
+ public Object data;
+ /** <code>true</code> if the fixed view should be selectable in the grid */
+ public boolean isSelectable;
+ }
+
+ private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();
+
+ private void initHeaderGridView() {
+ super.setClipChildren(false);
+ }
+
+ public HeaderGridView(Context context) {
+ super(context);
+ initHeaderGridView();
+ }
+
+ public HeaderGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initHeaderGridView();
+ }
+
+ public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initHeaderGridView();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ ListAdapter adapter = getAdapter();
+ if (adapter != null && adapter instanceof HeaderViewGridAdapter) {
+ ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());
+ }
+ }
+
+ @Override
+ public void setClipChildren(boolean clipChildren) {
+ // Ignore, since the header rows depend on not being clipped
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the grid. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+ * the supplied cursor with one that will also account for header views.
+ *
+ * @param v The view to add.
+ * @param data Data to associate with this view
+ * @param isSelectable whether the item is selectable
+ */
+ public void addHeaderView(View v, Object data, boolean isSelectable) {
+ ListAdapter adapter = getAdapter();
+
+ if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {
+ throw new IllegalStateException(
+ "Cannot add header view to grid -- setAdapter has already been called.");
+ }
+
+ FixedViewInfo info = new FixedViewInfo();
+ FrameLayout fl = new FullWidthFixedViewLayout(getContext());
+ fl.addView(v);
+ info.view = v;
+ info.viewContainer = fl;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mHeaderViewInfos.add(info);
+
+ // in the case of re-adding a header view, or adding one later on,
+ // we need to notify the observer
+ if (adapter != null) {
+ ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the grid. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+ * the supplied cursor with one that will also account for header views.
+ *
+ * @param v The view to add.
+ */
+ public void addHeaderView(View v) {
+ addHeaderView(v, null, true);
+ }
+
+ public int getHeaderViewCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ /**
+ * Removes a previously-added header view.
+ *
+ * @param v The view to remove
+ * @return true if the view was removed, false if the view was not a header
+ * view
+ */
+ public boolean removeHeaderView(View v) {
+ if (mHeaderViewInfos.size() > 0) {
+ boolean result = false;
+ ListAdapter adapter = getAdapter();
+ if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {
+ result = true;
+ }
+ removeFixedViewInfo(v, mHeaderViewInfos);
+ return result;
+ }
+ return false;
+ }
+
+ private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
+ int len = where.size();
+ for (int i = 0; i < len; ++i) {
+ FixedViewInfo info = where.get(i);
+ if (info.view == v) {
+ where.remove(i);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (mHeaderViewInfos.size() > 0) {
+ HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);
+ int numColumns = getNumColumns();
+ if (numColumns > 1) {
+ hadapter.setNumColumns(numColumns);
+ }
+ super.setAdapter(hadapter);
+ } else {
+ super.setAdapter(adapter);
+ }
+ }
+
+ private class FullWidthFixedViewLayout extends FrameLayout {
+ public FullWidthFixedViewLayout(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int targetWidth = HeaderGridView.this.getMeasuredWidth()
+ - HeaderGridView.this.getPaddingLeft()
+ - HeaderGridView.this.getPaddingRight();
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
+ MeasureSpec.getMode(widthMeasureSpec));
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ /**
+ * ListAdapter used when a HeaderGridView has header views. This ListAdapter
+ * wraps another one and also keeps track of the header views and their
+ * associated data objects.
+ *<p>This is intended as a base class; you will probably not need to
+ * use this class directly in your own code.
+ */
+ private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {
+
+ // This is used to notify the container of updates relating to number of columns
+ // or headers changing, which changes the number of placeholders needed
+ private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
+ private final ListAdapter mAdapter;
+ private int mNumColumns = 1;
+
+ // This ArrayList is assumed to NOT be null.
+ ArrayList<FixedViewInfo> mHeaderViewInfos;
+
+ boolean mAreAllFixedViewsSelectable;
+
+ private final boolean mIsFilterable;
+
+ public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {
+ mAdapter = adapter;
+ mIsFilterable = adapter instanceof Filterable;
+
+ if (headerViewInfos == null) {
+ throw new IllegalArgumentException("headerViewInfos cannot be null");
+ }
+ mHeaderViewInfos = headerViewInfos;
+
+ mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+ }
+
+ public int getHeadersCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
+ }
+
+ public void setNumColumns(int numColumns) {
+ if (numColumns < 1) {
+ throw new IllegalArgumentException("Number of columns must be 1 or more");
+ }
+ if (mNumColumns != numColumns) {
+ mNumColumns = numColumns;
+ notifyDataSetChanged();
+ }
+ }
+
+ private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {
+ if (infos != null) {
+ for (FixedViewInfo info : infos) {
+ if (!info.isSelectable) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean removeHeader(View v) {
+ for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+ FixedViewInfo info = mHeaderViewInfos.get(i);
+ if (info.view == v) {
+ mHeaderViewInfos.remove(i);
+
+ mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+
+ mDataSetObservable.notifyChanged();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ if (mAdapter != null) {
+ return getHeadersCount() * mNumColumns + mAdapter.getCount();
+ } else {
+ return getHeadersCount() * mNumColumns;
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ if (mAdapter != null) {
+ return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (position < numHeadersAndPlaceholders) {
+ return (position % mNumColumns == 0)
+ && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.isEnabled(adjPosition);
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (position < numHeadersAndPlaceholders) {
+ if (position % mNumColumns == 0) {
+ return mHeaderViewInfos.get(position / mNumColumns).data;
+ }
+ return null;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItem(adjPosition);
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+ int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemId(adjPosition);
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ if (mAdapter != null) {
+ return mAdapter.hasStableIds();
+ }
+ return false;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;
+ if (position < numHeadersAndPlaceholders) {
+ View headerViewContainer = mHeaderViewInfos
+ .get(position / mNumColumns).viewContainer;
+ if (position % mNumColumns == 0) {
+ return headerViewContainer;
+ } else {
+ if (convertView == null) {
+ convertView = new View(parent.getContext());
+ }
+ // We need to do this because GridView uses the height of the last item
+ // in a row to determine the height for the entire row.
+ convertView.setVisibility(View.INVISIBLE);
+ convertView.setMinimumHeight(headerViewContainer.getHeight());
+ return convertView;
+ }
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getView(adjPosition, convertView, parent);
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
+ // Placeholders get the last view type number
+ return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
+ }
+ if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+ int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemViewType(adjPosition);
+ }
+ }
+
+ return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ if (mAdapter != null) {
+ return mAdapter.getViewTypeCount() + 1;
+ }
+ return 2;
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+ if (mAdapter != null) {
+ mAdapter.registerDataSetObserver(observer);
+ }
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.unregisterObserver(observer);
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(observer);
+ }
+ }
+
+ @Override
+ public Filter getFilter() {
+ if (mIsFilterable) {
+ return ((Filterable) mAdapter).getFilter();
+ }
+ return null;
+ }
+
+ @Override
+ public ListAdapter getWrappedAdapter() {
+ return mAdapter;
+ }
+
+ public void notifyDataSetChanged() {
+ mDataSetObservable.notifyChanged();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/MultiUsernameEditText.java b/WordPress/src/main/java/org/wordpress/android/widgets/MultiUsernameEditText.java
new file mode 100644
index 000000000..c453fc77b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/MultiUsernameEditText.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+
+/**
+ * Used to handle backspace in People Management username field
+ */
+public class MultiUsernameEditText extends WPEditText {
+
+ private OnBackspacePressedListener mOnBackspacePressedListener;
+
+
+ public MultiUsernameEditText(Context context) {
+ super(context);
+ }
+
+ public MultiUsernameEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MultiUsernameEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setOnBackspacePressedListener(OnBackspacePressedListener onBackspacePressedListener) {
+ this.mOnBackspacePressedListener = onBackspacePressedListener;
+ }
+
+ public interface OnBackspacePressedListener {
+ boolean onBackspacePressed();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ //in this case it makes sense to not change EditText to fullscreen mode at landscape
+ outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
+ return new MultiUsernameEditTextInputConnection(this, false);
+ }
+
+
+ private class MultiUsernameEditTextInputConnection extends BaseInputConnection {
+
+ public MultiUsernameEditTextInputConnection(View targetView, boolean fullEditor) {
+ super(targetView, fullEditor);
+ }
+
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
+ if (mOnBackspacePressedListener != null) {
+ //if username was not deleted pass event to parent method and return the result
+ return !mOnBackspacePressedListener.onBackspacePressed() && super.sendKeyEvent(event);
+ }
+ }
+ return super.sendKeyEvent(event);
+ }
+
+ @Override
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ if (beforeLength == 1 && afterLength == 0) {
+ return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
+ && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
+ }
+
+ return super.deleteSurroundingText(beforeLength, afterLength);
+ }
+
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/NoticonTextView.java b/WordPress/src/main/java/org/wordpress/android/widgets/NoticonTextView.java
new file mode 100644
index 000000000..f6255692c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/NoticonTextView.java
@@ -0,0 +1,27 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * TextView that uses noticon icon font
+ */
+public class NoticonTextView extends TextView {
+ private static final String NOTICON_FONT_NAME = "Noticons.ttf";
+
+ public NoticonTextView(Context context) {
+ super(context, null);
+ this.setTypeface(TypefaceCache.getTypefaceForTypefaceName(context, NOTICON_FONT_NAME));
+ }
+
+ public NoticonTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.setTypeface(TypefaceCache.getTypefaceForTypefaceName(context, NOTICON_FONT_NAME));
+ }
+
+ public NoticonTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ this.setTypeface(TypefaceCache.getTypefaceForTypefaceName(context, NOTICON_FONT_NAME));
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/PostListButton.java b/WordPress/src/main/java/org/wordpress/android/widgets/PostListButton.java
new file mode 100644
index 000000000..dfab4c22b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/PostListButton.java
@@ -0,0 +1,134 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+/*
+ * buttons in footer of post cards
+ */
+public class PostListButton extends LinearLayout {
+
+ private ImageView mImageView;
+ private TextView mTextView;
+ private int mButtonType = BUTTON_NONE;
+
+ // from attrs.xml
+ public static final int BUTTON_NONE = 0;
+ public static final int BUTTON_EDIT = 1;
+ public static final int BUTTON_VIEW = 2;
+ public static final int BUTTON_PREVIEW = 3;
+ public static final int BUTTON_STATS = 4;
+ public static final int BUTTON_TRASH = 5;
+ public static final int BUTTON_DELETE = 6;
+ public static final int BUTTON_PUBLISH = 7;
+ public static final int BUTTON_MORE = 8;
+ public static final int BUTTON_BACK = 9;
+
+ public PostListButton(Context context){
+ super(context);
+ initView(context, null);
+ }
+
+ public PostListButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initView(context, attrs);
+ }
+
+ public PostListButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initView(context, attrs);
+ }
+
+ private void initView(Context context, AttributeSet attrs) {
+ inflate(context, R.layout.post_list_button, this);
+
+ mImageView = (ImageView) findViewById(R.id.image);
+ mTextView = (TextView) findViewById(R.id.text);
+
+ int buttonType = 0;
+ if (attrs != null) {
+ TypedArray a = context.getTheme().obtainStyledAttributes(
+ attrs,
+ R.styleable.wpPostListButton,
+ 0, 0);
+ try {
+ buttonType = a.getInteger(R.styleable.wpPostListButton_wpPostButtonType, 0);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ setButtonType(buttonType);
+ }
+
+
+ public int getButtonType() {
+ return mButtonType;
+ }
+
+ public void setButtonType(int buttonType) {
+ if (buttonType == mButtonType) {
+ return;
+ }
+
+ mButtonType = buttonType;
+ mTextView.setText(getButtonTextResId(buttonType));
+ mImageView.setImageResource(getButtonIconResId(buttonType));
+ }
+
+ public static int getButtonTextResId(int buttonType) {
+ switch (buttonType) {
+ case BUTTON_EDIT:
+ return R.string.button_edit;
+ case BUTTON_VIEW:
+ return R.string.button_view;
+ case BUTTON_PREVIEW:
+ return R.string.button_preview;
+ case BUTTON_STATS:
+ return R.string.button_stats;
+ case BUTTON_TRASH:
+ return R.string.button_trash;
+ case BUTTON_DELETE:
+ return R.string.button_delete;
+ case BUTTON_PUBLISH:
+ return R.string.button_publish;
+ case BUTTON_MORE:
+ return R.string.button_more;
+ case BUTTON_BACK:
+ return R.string.button_back;
+ default:
+ return 0;
+ }
+ }
+
+ public static int getButtonIconResId(int buttonType) {
+ switch (buttonType) {
+ case BUTTON_EDIT:
+ return R.drawable.noticon_edit;
+ case BUTTON_VIEW:
+ return R.drawable.noticon_view;
+ case BUTTON_PREVIEW:
+ return R.drawable.noticon_view;
+ case BUTTON_STATS:
+ return R.drawable.noticon_stats;
+ case BUTTON_TRASH:
+ return R.drawable.noticon_trash;
+ case BUTTON_DELETE:
+ return R.drawable.noticon_trash;
+ case BUTTON_PUBLISH:
+ return R.drawable.noticon_publish;
+ case BUTTON_MORE:
+ return R.drawable.noticon_more;
+ case BUTTON_BACK:
+ return R.drawable.noticon_back;
+ default:
+ return 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/RecyclerItemDecoration.java b/WordPress/src/main/java/org/wordpress/android/widgets/RecyclerItemDecoration.java
new file mode 100644
index 000000000..621b591a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/RecyclerItemDecoration.java
@@ -0,0 +1,40 @@
+package org.wordpress.android.widgets;
+
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * simple implementation of RecyclerView dividers
+ */
+public class RecyclerItemDecoration extends RecyclerView.ItemDecoration {
+ private final int mSpacingHorizontal;
+ private final int mSpacingVertical;
+ private final boolean mSkipFirstItem;
+
+ public RecyclerItemDecoration(int spacingHorizontal, int spacingVertical) {
+ super();
+ mSpacingHorizontal = spacingHorizontal;
+ mSpacingVertical = spacingVertical;
+ mSkipFirstItem = false;
+ }
+
+ public RecyclerItemDecoration(int spacingHorizontal, int spacingVertical, boolean skipFirstItem) {
+ super();
+ mSpacingHorizontal = spacingHorizontal;
+ mSpacingVertical = spacingVertical;
+ mSkipFirstItem = skipFirstItem;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ if (mSkipFirstItem && parent.getChildAdapterPosition(view) == 0) {
+ return;
+ }
+ outRect.set(mSpacingHorizontal, // left
+ 0, // top
+ mSpacingHorizontal, // right
+ mSpacingVertical); // bottom
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/SuggestionAutoCompleteText.java b/WordPress/src/main/java/org/wordpress/android/widgets/SuggestionAutoCompleteText.java
new file mode 100644
index 000000000..afc2e0479
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/SuggestionAutoCompleteText.java
@@ -0,0 +1,202 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.MultiAutoCompleteTextView;
+
+import org.wordpress.android.ui.suggestion.util.SuggestionTokenizer;
+import org.wordpress.persistentedittext.PersistentEditTextHelper;
+
+public class SuggestionAutoCompleteText extends MultiAutoCompleteTextView {
+ PersistentEditTextHelper mPersistentEditTextHelper;
+ private OnEditTextBackListener mBackListener;
+
+ public interface OnEditTextBackListener {
+ void onEditTextBack();
+ }
+
+ public SuggestionAutoCompleteText(Context context) {
+ super(context, null);
+ init(context, null);
+ }
+
+ public SuggestionAutoCompleteText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public SuggestionAutoCompleteText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ setTokenizer(new SuggestionTokenizer());
+ setThreshold(1);
+ mPersistentEditTextHelper = new PersistentEditTextHelper(context);
+ // When TYPE_TEXT_FLAG_AUTO_COMPLETE is set, autocorrection is disabled.
+ setRawInputType(getInputType() & ~EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE);
+ }
+
+ public PersistentEditTextHelper getAutoSaveTextHelper() {
+ return mPersistentEditTextHelper;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (getAutoSaveTextHelper().getUniqueId() == null) {
+ return;
+ }
+ getAutoSaveTextHelper().loadString(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (getAutoSaveTextHelper().getUniqueId() == null) {
+ return;
+ }
+ getAutoSaveTextHelper().saveString(this);
+ }
+
+ public void setOnBackListener(OnEditTextBackListener listener) {
+ mBackListener = listener;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ SavedState savedState = new SavedState(super.onSaveInstanceState());
+
+ // store the current Focused state
+ savedState.isFocused = isFocused();
+
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if(!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState savedState = (SavedState)state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+
+ // if we were focused, setup a properly timed future request for focus
+ if (savedState.isFocused) {
+ // this OnLayoutChangeListener will self unregister upon running and it's there so we can properly time the
+ // on-screen IME opening
+ addOnLayoutChangeListener(mOneoffFocusRequest);
+ }
+ }
+
+ private final OnLayoutChangeListener mOneoffFocusRequest = new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop,
+ int oldRight, int oldBottom) {
+ // we're now at a good point in time to launch a focus request
+ post(new Runnable() {
+ @Override
+ public void run() {
+ // self unregister so we won't auto-request focus again
+ removeOnLayoutChangeListener(mOneoffFocusRequest);
+
+ // request focus
+ setFocusableInTouchMode(true);
+ requestFocus();
+ }
+ });
+ }
+ };
+
+ @Override
+ public boolean performClick() {
+ // make sure we are focusable otherwise we will not get focused
+ setFocusableInTouchMode(true);
+ requestFocus();
+
+ return super.performClick();
+ }
+
+ /*
+ * detect when user hits the back button while soft keyboard is showing (hiding the keyboard)
+ */
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ // clear focus but stop being focusable first. This way we won't receive focus if we're the only focusable
+ // widget on the page
+ setFocusableInTouchMode(false);
+ clearFocus();
+
+ if (mBackListener != null) {
+ mBackListener.onEditTextBack();
+ }
+ }
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+ // if no hardware keys are present, associate being focused to having the on-screen keyboard visible
+ if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getContext()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ if (focused) {
+ // show the on-screen keybpoard if we got focused
+ inputMethodManager.showSoftInput(this, 0);
+ } else {
+ // stop being focusable so closing the keyboard won't focus us
+ setFocusableInTouchMode(false);
+ inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ }
+ }
+
+ /**
+ * Local class for holding the EditBox's focused or not state
+ */
+ static class SavedState extends BaseSavedState {
+ boolean isFocused;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ this.isFocused = (in.readInt() == 1);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(this.isFocused ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/TextDrawable.java b/WordPress/src/main/java/org/wordpress/android/widgets/TextDrawable.java
new file mode 100644
index 000000000..b4aa27643
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/TextDrawable.java
@@ -0,0 +1,442 @@
+package org.wordpress.android.widgets;
+
+/**
+ * A Drawable object used to display text content.
+ *
+ * Based on https://github.com/devunwired/textdrawable
+ */
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.TypedValue;
+
+/**
+ * A Drawable object that draws text.
+ * A TextDrawable accepts most of the same parameters that can be applied to
+ * {@link android.widget.TextView} for displaying and formatting text.
+ *
+ * Optionally, a {@link Path} may be supplied on which to draw the text.
+ *
+ * A TextDrawable has an intrinsic size equal to that required to draw all
+ * the text it has been supplied, when possible. In cases where a {@link Path}
+ * has been supplied, the caller must explicitly call
+ * {@link #setBounds(android.graphics.Rect) setBounds()} to provide the Drawable
+ * size based on the Path constraints.
+ */
+public class TextDrawable extends Drawable {
+
+ /* Platform XML constants for typeface */
+ private static final int SANS = 1;
+ private static final int SERIF = 2;
+ private static final int MONOSPACE = 3;
+
+ /* Resources for scaling values to the given device */
+ private Resources mResources;
+ /* Paint to hold most drawing primitives for the text */
+ private TextPaint mTextPaint;
+ /* Layout is used to measure and draw the text */
+ private StaticLayout mTextLayout;
+ /* Alignment of the text inside its bounds */
+ private Layout.Alignment mTextAlignment = Layout.Alignment.ALIGN_NORMAL;
+ /* Optional path on which to draw the text */
+ private Path mTextPath;
+ /* Stateful text color list */
+ private ColorStateList mTextColors;
+ /* Container for the bounds to be reported to widgets */
+ private Rect mTextBounds;
+ /* Text string to draw */
+ private CharSequence mText = "";
+
+ /* Attribute lists to pull default values from the current theme */
+ private static final int[] themeAttributes = {
+ android.R.attr.textAppearance
+ };
+ private static final int[] appearanceAttributes = {
+ android.R.attr.textSize,
+ android.R.attr.typeface,
+ android.R.attr.textStyle,
+ android.R.attr.textColor
+ };
+
+
+ public TextDrawable(Context context) {
+ super();
+ //Used to load and scale resource items
+ mResources = context.getResources();
+ //Definition of this drawables size
+ mTextBounds = new Rect();
+ //Paint to use for the text
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ mTextPaint.density = mResources.getDisplayMetrics().density;
+ mTextPaint.setDither(true);
+
+ int textSize = 15;
+ ColorStateList textColor = null;
+ int styleIndex = -1;
+ int typefaceIndex = -1;
+
+ //Set default parameters from the current theme
+ TypedArray a = context.getTheme().obtainStyledAttributes(themeAttributes);
+ int appearanceId = a.getResourceId(0, -1);
+ a.recycle();
+
+ TypedArray ap = null;
+ if (appearanceId != -1) {
+ ap = context.obtainStyledAttributes(appearanceId, appearanceAttributes);
+ }
+ if (ap != null) {
+ for (int i=0; i < ap.getIndexCount(); i++) {
+ int attr = ap.getIndex(i);
+ switch (attr) {
+ case 0: //Text Size
+ textSize = a.getDimensionPixelSize(attr, textSize);
+ break;
+ case 1: //Typeface
+ typefaceIndex = a.getInt(attr, typefaceIndex);
+ break;
+ case 2: //Text Style
+ styleIndex = a.getInt(attr, styleIndex);
+ break;
+ case 3: //Text Color
+ textColor = a.getColorStateList(attr);
+ break;
+ default:
+ break;
+ }
+ }
+
+ ap.recycle();
+ }
+
+ setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
+ setRawTextSize(textSize);
+
+ Typeface tf = null;
+ switch (typefaceIndex) {
+ case SANS:
+ tf = Typeface.SANS_SERIF;
+ break;
+
+ case SERIF:
+ tf = Typeface.SERIF;
+ break;
+
+ case MONOSPACE:
+ tf = Typeface.MONOSPACE;
+ break;
+ }
+
+ setTypeface(tf, styleIndex);
+ }
+
+
+ public void setText(int text) {
+ this.setText(String.valueOf(text));
+ }
+
+ /**
+ * Set the text that will be displayed
+ * @param text Text to display
+ */
+ public void setText(CharSequence text) {
+ if (text == null) text = "";
+
+ mText = text;
+
+ measureContent();
+ }
+
+ /**
+ * Return the text currently being displayed
+ */
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Return the current text size, in pixels
+ */
+ public float getTextSize() {
+ return mTextPaint.getTextSize();
+ }
+
+ /**
+ * Set the text size. The value will be interpreted in "sp" units
+ * @param size Text size value, in sp
+ */
+ public void setTextSize(float size) {
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
+ }
+
+ /**
+ * Set the text size, using the supplied complex units
+ * @param unit Units for the text size, such as dp or sp
+ * @param size Text size value
+ */
+ public void setTextSize(int unit, float size) {
+ float dimension = TypedValue.applyDimension(unit, size,
+ mResources.getDisplayMetrics());
+ setRawTextSize(dimension);
+ }
+
+ /*
+ * Set the text size, in raw pixels
+ */
+ private void setRawTextSize(float size) {
+ if (size != mTextPaint.getTextSize()) {
+ mTextPaint.setTextSize(size);
+
+ measureContent();
+ }
+ }
+
+ /**
+ * Return the horizontal stretch factor of the text
+ */
+ public float getTextScaleX() {
+ return mTextPaint.getTextScaleX();
+ }
+
+ /**
+ * Set the horizontal stretch factor of the text
+ * @param size Text scale factor
+ */
+ public void setTextScaleX(float size) {
+ if (size != mTextPaint.getTextScaleX()) {
+ mTextPaint.setTextScaleX(size);
+ measureContent();
+ }
+ }
+
+ /**
+ * Return the current text alignment setting
+ */
+ public Layout.Alignment getTextAlign() {
+ return mTextAlignment;
+ }
+
+ /**
+ * Set the text alignment. The alignment itself is based on the text layout direction.
+ * For LTR text NORMAL is left aligned and OPPOSITE is right aligned.
+ * For RTL text, those alignments are reversed.
+ * @param align Text alignment value. Should be set to one of:
+ *
+ * {@link Layout.Alignment#ALIGN_NORMAL},
+ * {@link Layout.Alignment#ALIGN_NORMAL},
+ * {@link Layout.Alignment#ALIGN_OPPOSITE}.
+ */
+ public void setTextAlign(Layout.Alignment align) {
+ if (mTextAlignment != align) {
+ mTextAlignment = align;
+ measureContent();
+ }
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed.
+ * Note that not all Typeface families actually have bold and italic
+ * variants, so you may need to use
+ * {@link #setTypeface(Typeface, int)} to get the appearance
+ * that you actually want.
+ */
+ public void setTypeface(Typeface tf) {
+ if (mTextPaint.getTypeface() != tf) {
+ mTextPaint.setTypeface(tf);
+
+ measureContent();
+ }
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed,
+ * and turns on the fake bold and italic bits in the Paint if the
+ * Typeface that you provided does not have all the bits in the
+ * style that you specified.
+ *
+ */
+ public void setTypeface(Typeface tf, int style) {
+ if (style > 0) {
+ if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ setTypeface(tf);
+ // now compute what (if any) algorithmic styling is needed
+ int typefaceStyle = tf != null ? tf.getStyle() : 0;
+ int need = style & ~typefaceStyle;
+ mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
+ mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
+ } else {
+ mTextPaint.setFakeBoldText(false);
+ mTextPaint.setTextSkewX(0);
+ setTypeface(tf);
+ }
+ }
+
+ /**
+ * Return the current typeface and style that the Paint
+ * using for display.
+ */
+ public Typeface getTypeface() {
+ return mTextPaint.getTypeface();
+ }
+
+ /**
+ * Set a single text color for all states
+ * @param color Color value such as {@link Color#WHITE} or {@link Color#argb(int, int, int, int)}
+ */
+ public void setTextColor(int color) {
+ setTextColor(ColorStateList.valueOf(color));
+ }
+
+ /**
+ * Set the text color as a state list
+ * @param colorStateList ColorStateList of text colors, such as inflated from an R.color resource
+ */
+ public void setTextColor(ColorStateList colorStateList) {
+ mTextColors = colorStateList;
+ updateTextColors(getState());
+ }
+
+ /**
+ * Optional Path object on which to draw the text. If this is set,
+ * TextDrawable cannot properly measure the bounds this drawable will need.
+ * You must call {@link #setBounds(int, int, int, int) setBounds()} before
+ * applying this TextDrawable to any View.
+ *
+ * Calling this method with <code>null</code> will remove any Path currently attached.
+ */
+ public void setTextPath(Path path) {
+ if (mTextPath != path) {
+ mTextPath = path;
+ measureContent();
+ }
+ }
+
+ /**
+ * Internal method to take measurements of the current contents and apply
+ * the correct bounds when possible.
+ */
+ private void measureContent() {
+ //If drawing to a path, we cannot measure intrinsic bounds
+ //We must resly on setBounds being called externally
+ if (mTextPath != null) {
+ //Clear any previous measurement
+ mTextLayout = null;
+ mTextBounds.setEmpty();
+ } else {
+ //Measure text bounds
+ double desired = Math.ceil( Layout.getDesiredWidth(mText, mTextPaint) );
+ mTextLayout = new StaticLayout(mText, mTextPaint, (int)desired,
+ mTextAlignment, 1.0f, 0.0f, false);
+ mTextBounds.set(0, 0, mTextLayout.getWidth(), mTextLayout.getHeight());
+ }
+
+ //We may need to be redrawn
+ invalidateSelf();
+ }
+
+ /**
+ * Internal method to apply the correct text color based on the drawable's state
+ */
+ private boolean updateTextColors(int[] stateSet) {
+ int newColor = mTextColors.getColorForState(stateSet, Color.WHITE);
+ if (mTextPaint.getColor() != newColor) {
+ mTextPaint.setColor(newColor);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ //Update the internal bounds in response to any external requests
+ mTextBounds.set(bounds);
+ }
+
+ @Override
+ public boolean isStateful() {
+ /*
+ * The drawable's ability to represent state is based on
+ * the text color list set
+ */
+ return mTextColors.isStateful();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ //Upon state changes, grab the correct text color
+ return updateTextColors(state);
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ //Return the vertical bounds measured, or -1 if none
+ if (mTextBounds.isEmpty()) {
+ return -1;
+ } else {
+ return (mTextBounds.bottom - mTextBounds.top);
+ }
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ //Return the horizontal bounds measured, or -1 if none
+ if (mTextBounds.isEmpty()) {
+ return -1;
+ } else {
+ return (mTextBounds.right - mTextBounds.left);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Rect bounds = getBounds();
+ final int count = canvas.save();
+ canvas.translate(bounds.left, bounds.top);
+ if (mTextPath == null) {
+ //Allow the layout to draw the text
+ mTextLayout.draw(canvas);
+ } else {
+ //Draw directly on the canvas using the supplied path
+ canvas.drawTextOnPath(mText.toString(), mTextPath, 0, 0, mTextPaint);
+ }
+ canvas.restoreToCount(count);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mTextPaint.getAlpha() != alpha) {
+ mTextPaint.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ public int getOpacity() {
+ return mTextPaint.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ if (mTextPaint.getColorFilter() != cf) {
+ mTextPaint.setColorFilter(cf);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java b/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java
new file mode 100644
index 000000000..ea6dcc142
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java
@@ -0,0 +1,126 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+import java.util.Hashtable;
+
+public class TypefaceCache {
+
+ /**
+ * Cache used for all views that support custom fonts - defaults to the system font, but
+ * Merriweather is also available via the "wpFontFamily" attribute
+ */
+ public static final int FAMILY_DEFAULT = 0;
+ public static final int FAMILY_DEFAULT_LIGHT = 1;
+ public static final int FAMILY_MERRIWEATHER = 2;
+
+ private static final Hashtable<String, Typeface> mTypefaceCache = new Hashtable<>();
+
+ public static Typeface getTypeface(Context context) {
+ return getTypeface(context, FAMILY_DEFAULT, Typeface.NORMAL);
+ }
+ public static Typeface getTypeface(Context context, int family, int fontStyle) {
+ if (context == null) {
+ return null;
+ }
+
+ if (family == FAMILY_MERRIWEATHER) {
+ final String typefaceName;
+ switch (fontStyle) {
+ case Typeface.BOLD:
+ typefaceName = "Merriweather-Bold.ttf";
+ break;
+ case Typeface.ITALIC:
+ typefaceName = "Merriweather-Italic.ttf";
+ break;
+ case Typeface.BOLD_ITALIC:
+ typefaceName = "Merriweather-BoldItalic.ttf";
+ break;
+ default:
+ typefaceName = "Merriweather-Regular.ttf";
+ break;
+ }
+ return getTypefaceForTypefaceName(context, typefaceName);
+ }
+
+ // default system font
+ if (family == FAMILY_DEFAULT_LIGHT) {
+ return Typeface.create("sans-serif-light", fontStyle);
+ } else {
+ return Typeface.defaultFromStyle(fontStyle);
+ }
+ }
+
+ /*
+ * returns the desired typeface from the cache, loading it from app's assets if necessary
+ */
+ protected static Typeface getTypefaceForTypefaceName(Context context, String typefaceName) {
+ if (!mTypefaceCache.containsKey(typefaceName)) {
+ Typeface typeface = Typeface.createFromAsset(context.getApplicationContext().getAssets(), "fonts/"
+ + typefaceName);
+ if (typeface != null) {
+ mTypefaceCache.put(typefaceName, typeface);
+ }
+ }
+
+ return mTypefaceCache.get(typefaceName);
+ }
+
+ /*
+ * sets the typeface for a TextView (or TextView descendant such as EditText or Button) based on
+ * the passed attributes, defaults to normal
+ */
+ protected static void setCustomTypeface(Context context, TextView view, AttributeSet attrs) {
+ if (context == null || view == null) return;
+
+ // skip at design-time
+ if (view.isInEditMode()) return;
+
+ // default if not set in attributes
+ int family = FAMILY_DEFAULT;
+ if (attrs != null) {
+ TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WPTextView, 0, 0);
+ if (a != null) {
+ try {
+ family = a.getInteger(R.styleable.WPTextView_wpFontFamily, FAMILY_DEFAULT);
+ } finally {
+ a.recycle();
+ }
+ }
+ }
+
+ // nothing more to do if this is the default system font
+ if (family == FAMILY_DEFAULT) {
+ return;
+ }
+
+ // determine the font style from the existing typeface
+ final int fontStyle;
+ if (view.getTypeface() != null) {
+ boolean isBold = view.getTypeface().isBold();
+ boolean isItalic = view.getTypeface().isItalic();
+ if (isBold && isItalic) {
+ fontStyle = Typeface.BOLD_ITALIC;
+ } else if (isBold) {
+ fontStyle = Typeface.BOLD;
+ } else if (isItalic) {
+ fontStyle = Typeface.ITALIC;
+ } else {
+ fontStyle = Typeface.NORMAL;
+ }
+ } else {
+ fontStyle = Typeface.NORMAL;
+ }
+
+ Typeface typeface = getTypeface(context, family, fontStyle);
+ if (typeface != null) {
+ view.setTypeface(typeface);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPAlertDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPAlertDialogFragment.java
new file mode 100644
index 000000000..4ac77d68e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPAlertDialogFragment.java
@@ -0,0 +1,140 @@
+package org.wordpress.android.widgets;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.StringUtils;
+
+public class WPAlertDialogFragment extends DialogFragment implements DialogInterface.OnClickListener {
+ private static enum WPAlertDialogType {ALERT, // simple ok dialog with error message
+ CONFIRM, // dialog with yes/no and callback when positive button clicked
+ URL_INFO} // info dialog that shows url when positive button clicked
+
+ private static final String ARG_TITLE = "title";
+ private static final String ARG_MESSAGE = "message";
+ private static final String ARG_TYPE = "type";
+ private static final String ARG_INFO_TITLE = "info-title";
+ private static final String ARG_INFO_URL = "info-url";
+
+ public interface OnDialogConfirmListener {
+ public void onDialogConfirm();
+ }
+
+ public static WPAlertDialogFragment newAlertDialog(String message) {
+ String title = WordPress.getContext().getString(R.string.error_generic);
+ return newAlertDialog(title, message);
+ }
+ public static WPAlertDialogFragment newAlertDialog(String title, String message) {
+ return newInstance(title, message, WPAlertDialogType.ALERT, null, null);
+ }
+
+ public static WPAlertDialogFragment newConfirmDialog(String title,
+ String message) {
+ return newInstance(title, message, WPAlertDialogType.CONFIRM, null, null);
+ }
+
+ public static WPAlertDialogFragment newUrlInfoDialog(String title,
+ String message,
+ String infoTitle,
+ String infoUrl) {
+ return newInstance(title, message, WPAlertDialogType.URL_INFO, infoTitle, infoUrl);
+ }
+
+ private static WPAlertDialogFragment newInstance(String title,
+ String message,
+ WPAlertDialogType alertType,
+ String infoTitle,
+ String infoUrl) {
+ WPAlertDialogFragment dialog = new WPAlertDialogFragment();
+
+ Bundle bundle = new Bundle();
+
+ bundle.putString(ARG_TITLE, StringUtils.notNullStr(title));
+ bundle.putString(ARG_MESSAGE, StringUtils.notNullStr(message));
+ bundle.putSerializable(ARG_TYPE, (alertType != null ? alertType : WPAlertDialogType.ALERT));
+
+ if (alertType == WPAlertDialogType.URL_INFO) {
+ bundle.putString(ARG_INFO_TITLE, StringUtils.notNullStr(infoTitle));
+ bundle.putString(ARG_INFO_URL, StringUtils.notNullStr(infoUrl));
+ }
+
+ dialog.setArguments(bundle);
+
+ return dialog;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setCancelable(true);
+ int style = DialogFragment.STYLE_NORMAL, theme = 0;
+ setStyle(style, theme);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Bundle bundle = getArguments();
+
+ final String title = StringUtils.notNullStr(bundle.getString(ARG_TITLE));
+ final String message = StringUtils.notNullStr(bundle.getString(ARG_MESSAGE));
+
+ final WPAlertDialogType dialogType;
+ if (bundle.containsKey(ARG_TYPE) && bundle.getSerializable(ARG_TYPE) instanceof WPAlertDialogType) {
+ dialogType = (WPAlertDialogType) bundle.getSerializable(ARG_TYPE);
+ } else {
+ dialogType = WPAlertDialogType.ALERT;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+
+ builder.setTitle(title);
+ builder.setMessage(message);
+
+ switch (dialogType) {
+ case ALERT:
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setNeutralButton(R.string.ok, this);
+ break;
+
+ case CONFIRM:
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (getActivity() instanceof OnDialogConfirmListener) {
+ OnDialogConfirmListener act = (OnDialogConfirmListener) getActivity();
+ act.onDialogConfirm();
+ }
+ }
+ });
+ builder.setNegativeButton(R.string.no, this);
+ break;
+
+ case URL_INFO:
+ final String infoTitle = StringUtils.notNullStr(bundle.getString(ARG_INFO_TITLE));
+ final String infoURL = StringUtils.notNullStr(bundle.getString(ARG_INFO_URL));
+ builder.setPositiveButton(infoTitle, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (!TextUtils.isEmpty(infoURL))
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(infoURL)));
+ }
+ });
+ break;
+ }
+
+ return builder.create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPAutoResizeTextView.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPAutoResizeTextView.java
new file mode 100644
index 000000000..b8c8a9d61
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPAutoResizeTextView.java
@@ -0,0 +1,27 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import org.wordpress.android.util.widgets.AutoResizeTextView;
+
+/**
+ * custom AutoTextView used in layouts - enables keeping custom typeface handling in one place (so we
+ * avoid having to set the typeface for every single AutoResizeTextView in every single activity)
+ */
+public class WPAutoResizeTextView extends AutoResizeTextView {
+ public WPAutoResizeTextView(Context context) {
+ super(context);
+ TypefaceCache.setCustomTypeface(context, this, null);
+ }
+
+ public WPAutoResizeTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+
+ public WPAutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPButton.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPButton.java
new file mode 100644
index 000000000..41059f77b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPButton.java
@@ -0,0 +1,22 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatButton;
+import android.util.AttributeSet;
+
+public class WPButton extends AppCompatButton {
+ public WPButton(Context context) {
+ super(context, null);
+ TypefaceCache.setCustomTypeface(context, this, null);
+ }
+
+ public WPButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+
+ public WPButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPCheckBox.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPCheckBox.java
new file mode 100644
index 000000000..93a6b8a6d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPCheckBox.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatCheckBox;
+import android.util.AttributeSet;
+
+/**
+ * A CheckBox that uses the default font from TypefaceCache
+ */
+public class WPCheckBox extends AppCompatCheckBox {
+ public WPCheckBox(Context context) {
+ super(context);
+ TypefaceCache.setCustomTypeface(context, this, null);
+ }
+
+ public WPCheckBox(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+
+ public WPCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPEditText.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPEditText.java
new file mode 100644
index 000000000..fe89045be
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPEditText.java
@@ -0,0 +1,23 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import org.wordpress.persistentedittext.PersistentEditText;
+
+public class WPEditText extends PersistentEditText {
+ public WPEditText(Context context) {
+ super(context, null);
+ TypefaceCache.setCustomTypeface(context, this, null);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPEditTextPreference.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPEditTextPreference.java
new file mode 100644
index 000000000..108764c9f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPEditTextPreference.java
@@ -0,0 +1,28 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.preference.EditTextPreference;
+import android.util.AttributeSet;
+
+public class WPEditTextPreference extends EditTextPreference {
+ public WPEditTextPreference(Context context) {
+ super(context);
+ }
+
+ public WPEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public WPEditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+ if (!positiveResult) {
+ callChangeListener(null);
+ }
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPLinearLayoutSizeBound.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPLinearLayoutSizeBound.java
new file mode 100644
index 000000000..3c1d36822
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPLinearLayoutSizeBound.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.R;
+
+
+public class WPLinearLayoutSizeBound extends LinearLayout {
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+
+ public WPLinearLayoutSizeBound(Context context) {
+ super(context);
+ mMaxWidth = 0;
+ mMaxHeight = 0;
+ }
+
+ public WPLinearLayoutSizeBound(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.WPLinearLayoutSizeBound);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.WPLinearLayoutSizeBound_maxWidth,
+ Integer.MAX_VALUE);
+ mMaxHeight = a.getDimensionPixelSize(R.styleable.WPLinearLayoutSizeBound_maxHeight,
+ Integer.MAX_VALUE);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ if (mMaxWidth > 0 && mMaxWidth < measuredWidth) {
+ int measureMode = MeasureSpec.getMode(widthMeasureSpec);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode);
+ }
+ int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+ if (mMaxHeight > 0 && mMaxHeight < measuredHeight) {
+ int measureMode = MeasureSpec.getMode(heightMeasureSpec);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, measureMode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java
new file mode 100644
index 000000000..1f858664c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java
@@ -0,0 +1,449 @@
+package org.wordpress.android.widgets;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.AsyncTask;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.AppCompatImageView;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderThumbnailTable;
+import org.wordpress.android.ui.reader.utils.ReaderVideoUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.util.HashSet;
+
+/**
+ * most of the code below is from Volley's NetworkImageView, but it's modified to support:
+ * (1) fading in downloaded images
+ * (2) manipulating images before display
+ * (3) automatically retrieving the thumbnail for YouTube & Vimeo videos
+ */
+public class WPNetworkImageView extends AppCompatImageView {
+ public enum ImageType {
+ NONE,
+ PHOTO,
+ PHOTO_ROUNDED,
+ VIDEO,
+ AVATAR,
+ BLAVATAR,
+ GONE_UNTIL_AVAILABLE,
+ }
+
+ public interface ImageLoadListener {
+ void onLoaded();
+ void onError();
+ }
+
+ private ImageType mImageType = ImageType.NONE;
+ private String mUrl;
+ private ImageLoader.ImageContainer mImageContainer;
+
+ private int mDefaultImageResId;
+ private int mErrorImageResId;
+
+ private static final HashSet<String> mUrlSkipList = new HashSet<>();
+
+ public WPNetworkImageView(Context context) {
+ super(context);
+ }
+ public WPNetworkImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ public WPNetworkImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setImageUrl(String url, ImageType imageType) {
+ setImageUrl(url, imageType, null);
+ }
+
+ public void setImageUrl(String url, ImageType imageType, ImageLoadListener imageLoadListener) {
+ mUrl = url;
+ mImageType = imageType;
+
+ // The URL has potentially changed. See if we need to load it.
+ loadImageIfNecessary(false, imageLoadListener);
+ }
+
+ /*
+ * determine whether we can show a thumbnail image for the passed video - currently
+ * we support YouTube, Vimeo & standard images
+ */
+ public static boolean canShowVideoThumbnail(String videoUrl) {
+ return ReaderVideoUtils.isVimeoLink(videoUrl)
+ || ReaderVideoUtils.isYouTubeVideoLink(videoUrl)
+ || MediaUtils.isValidImage(videoUrl);
+ }
+
+ /*
+ * retrieves and displays the thumbnail for the passed video
+ */
+ public void setVideoUrl(final long postId, final String videoUrl) {
+ mImageType = ImageType.VIDEO;
+
+ if (TextUtils.isEmpty(videoUrl)) {
+ showErrorImage();
+ return;
+ }
+
+ // if this is a YouTube video we can determine the thumbnail url from the passed url,
+ // otherwise check if we've already cached the thumbnail url for this video
+ String thumbnailUrl;
+ if (ReaderVideoUtils.isYouTubeVideoLink(videoUrl)) {
+ thumbnailUrl = ReaderVideoUtils.getYouTubeThumbnailUrl(videoUrl);
+ } else {
+ thumbnailUrl = ReaderThumbnailTable.getThumbnailUrl(videoUrl);
+ }
+ if (!TextUtils.isEmpty(thumbnailUrl)) {
+ setImageUrl(thumbnailUrl, ImageType.VIDEO);
+ return;
+ }
+
+ if (MediaUtils.isValidImage(videoUrl)) {
+ setImageUrl(videoUrl, ImageType.VIDEO);
+ } else if (ReaderVideoUtils.isVimeoLink(videoUrl)) {
+ // vimeo videos require network request to get thumbnail
+ showDefaultImage();
+ ReaderVideoUtils.requestVimeoThumbnail(videoUrl, new ReaderVideoUtils.VideoThumbnailListener() {
+ @Override
+ public void onResponse(boolean successful, String thumbnailUrl) {
+ if (successful) {
+ ReaderThumbnailTable.addThumbnail(postId, videoUrl, thumbnailUrl);
+ setImageUrl(thumbnailUrl, ImageType.VIDEO);
+ }
+ }
+ });
+ } else {
+ AppLog.d(AppLog.T.UTILS, "no video thumbnail for " + videoUrl);
+ showErrorImage();
+ }
+ }
+
+ /**
+ * Loads the image for the view if it isn't already loaded.
+ * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
+ */
+ private void loadImageIfNecessary(final boolean isInLayoutPass, final ImageLoadListener imageLoadListener) {
+ // do nothing if image type hasn't been set yet
+ if (mImageType == ImageType.NONE) {
+ return;
+ }
+
+ int width = getWidth();
+ int height = getHeight();
+ ScaleType scaleType = getScaleType();
+
+ boolean wrapWidth = false, wrapHeight = false;
+ if (getLayoutParams() != null) {
+ wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
+ wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
+ }
+
+ // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
+ // view, hold off on loading the image.
+ boolean isFullyWrapContent = wrapWidth && wrapHeight;
+ if (width == 0 && height == 0 && !isFullyWrapContent && mImageType != ImageType.GONE_UNTIL_AVAILABLE) {
+ return;
+ }
+
+ // if the URL to be loaded in this view is empty, cancel any old requests and clear the
+ // currently loaded image.
+ if (TextUtils.isEmpty(mUrl)) {
+ if (mImageContainer != null) {
+ mImageContainer.cancelRequest();
+ mImageContainer = null;
+ }
+ showErrorImage();
+ return;
+ }
+
+ // if there was an old request in this view, check if it needs to be canceled.
+ if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
+ if (mImageContainer.getRequestUrl().equals(mUrl)) {
+ // if the request is from the same URL and it's not GONE_UNTIL_AVAILABLE, return.
+ if (mImageType != ImageType.GONE_UNTIL_AVAILABLE) {
+ // GONE_UNTIL_AVAILABLE image type will make a new request if the previous response wasn't a 404 response,
+ // Volley usually returns it from cache.
+ return;
+ }
+ } else {
+ // if there is a pre-existing request, cancel it if it's fetching a different URL.
+ mImageContainer.cancelRequest();
+ showDefaultImage();
+ }
+ }
+
+ // skip this URL if a previous request for it returned a 404
+ if (mUrlSkipList.contains(mUrl)) {
+ AppLog.d(AppLog.T.UTILS, "skipping image request " + mUrl);
+ showErrorImage();
+ return;
+ }
+
+ // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
+ int maxWidth = wrapWidth ? 0 : width;
+ int maxHeight = wrapHeight ? 0 : height;
+
+ // The pre-existing content of this view didn't match the current URL. Load the new image
+ // from the network.
+ ImageLoader.ImageContainer newContainer = WordPress.imageLoader.get(mUrl,
+ new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ showErrorImage();
+ // keep track of URLs that 404 so we can skip them the next time
+ int statusCode = VolleyUtils.statusCodeFromVolleyError(error);
+ if (statusCode == 404) {
+ mUrlSkipList.add(mUrl);
+ }
+
+ if (imageLoadListener != null) {
+ imageLoadListener.onError();
+ }
+ }
+
+ @Override
+ public void onResponse(final ImageLoader.ImageContainer response, boolean isImmediate) {
+ // If this was an immediate response that was delivered inside of a layout
+ // pass do not set the image immediately as it will trigger a requestLayout
+ // inside of a layout. Instead, defer setting the image by posting back to
+ // the main thread.
+ if (isImmediate && isInLayoutPass) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ handleResponse(response, true, imageLoadListener);
+ }
+ });
+ } else {
+ handleResponse(response, isImmediate, imageLoadListener);
+ }
+ }
+ }, maxWidth, maxHeight, scaleType);
+
+ // update the ImageContainer to be the new bitmap container.
+ mImageContainer = newContainer;
+ }
+
+ private static boolean canFadeInImageType(ImageType imageType) {
+ return imageType == ImageType.PHOTO
+ || imageType == ImageType.VIDEO;
+ }
+
+ private void handleResponse(ImageLoader.ImageContainer response, boolean isCached, ImageLoadListener
+ imageLoadListener) {
+ if (response.getBitmap() != null) {
+ Bitmap bitmap = response.getBitmap();
+
+ if (mImageType == ImageType.GONE_UNTIL_AVAILABLE) {
+ setVisibility(View.VISIBLE);
+ }
+
+ // Apply circular rounding to avatars in a background task
+ if (mImageType == ImageType.AVATAR) {
+ new ShapeBitmapTask(ShapeType.CIRCLE, imageLoadListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap);
+ return;
+ } else if (mImageType == ImageType.PHOTO_ROUNDED) {
+ new ShapeBitmapTask(ShapeType.ROUNDED, imageLoadListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap);
+ return;
+ }
+
+ setImageBitmap(bitmap);
+
+ // fade in photos/videos if not cached (not used for other image types since animation can be expensive)
+ if (!isCached && canFadeInImageType(mImageType)) {
+ fadeIn();
+ }
+ } else {
+ showDefaultImage();
+ }
+ }
+
+ public void invalidateImage() {
+ mUrlSkipList.clear();
+
+ if (mImageContainer != null) {
+ // If the view was bound to an image request, cancel it and clear
+ // out the image from the view.
+ mImageContainer.cancelRequest();
+ setImageBitmap(null);
+ // also clear out the container so we can reload the image if necessary.
+ mImageContainer = null;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!isInEditMode()) {
+ loadImageIfNecessary(true, null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ invalidateImage();
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+
+ private int getColorRes(@ColorRes int resId) {
+ return ContextCompat.getColor(getContext(), resId);
+ }
+
+ public void setDefaultImageResId(@DrawableRes int resourceId) {
+ mDefaultImageResId = resourceId;
+ }
+
+ public void setErrorImageResId(@DrawableRes int resourceId) {
+ mErrorImageResId = resourceId;
+ }
+
+ public void showDefaultImage() {
+ // use default image resource if one was supplied...
+ if (mDefaultImageResId != 0) {
+ setImageResource(mDefaultImageResId);
+ return;
+ }
+
+ // ... otherwise use built-in default
+ switch (mImageType) {
+ case GONE_UNTIL_AVAILABLE:
+ this.setVisibility(View.GONE);
+ break;
+ case NONE:
+ // do nothing
+ break;
+ case AVATAR:
+ // Grey circle for avatars
+ setImageResource(R.drawable.shape_oval_grey_light);
+ break;
+ default :
+ // light grey box for all others
+ setImageDrawable(new ColorDrawable(getColorRes(R.color.grey_light)));
+ break;
+ }
+ }
+
+ private void showErrorImage() {
+ if (mErrorImageResId != 0) {
+ setImageResource(mErrorImageResId);
+ return;
+ }
+
+ switch (mImageType) {
+ case GONE_UNTIL_AVAILABLE:
+ this.setVisibility(View.GONE);
+ break;
+ case NONE:
+ // do nothing
+ break;
+ case AVATAR:
+ // circular "mystery man" for failed avatars
+ showDefaultGravatarImage();
+ break;
+ case BLAVATAR:
+ showDefaultBlavatarImage();
+ break;
+ default :
+ // grey box for all others
+ setImageDrawable(new ColorDrawable(getColorRes(R.color.grey_lighten_30)));
+ break;
+ }
+ }
+
+ public void showDefaultGravatarImage() {
+ if (getContext() == null) return;
+ new ShapeBitmapTask(ShapeType.CIRCLE, null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, BitmapFactory.decodeResource(
+ getContext().getResources(),
+ R.drawable.gravatar_placeholder
+ ));
+ }
+
+ public void showDefaultBlavatarImage() {
+ setImageResource(R.drawable.blavatar_placeholder);
+ }
+
+ // --------------------------------------------------------------------------------------------------
+
+
+ private static final int FADE_TRANSITION = 250;
+
+ private void fadeIn() {
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(this, View.ALPHA, 0.25f, 1f);
+ alpha.setDuration(FADE_TRANSITION);
+ alpha.start();
+ }
+
+ // Circularizes or rounds the corners of a bitmap in a background thread
+ private enum ShapeType { CIRCLE, ROUNDED }
+ private class ShapeBitmapTask extends AsyncTask<Bitmap, Void, Bitmap> {
+ private final ImageLoadListener mImageLoadListener;
+ private final ShapeType mShapeType;
+ private int mRoundedCornerRadiusPx;
+ private static final int ROUNDED_CORNER_RADIUS_DP = 2;
+
+ public ShapeBitmapTask(ShapeType shapeType, ImageLoadListener imageLoadListener) {
+ mImageLoadListener = imageLoadListener;
+ mShapeType = shapeType;
+ if (mShapeType == ShapeType.ROUNDED) {
+ mRoundedCornerRadiusPx = DisplayUtils.dpToPx(getContext(), ROUNDED_CORNER_RADIUS_DP);
+ }
+ }
+
+ @Override
+ protected Bitmap doInBackground(Bitmap... params) {
+ if (params == null || params.length == 0) return null;
+
+ Bitmap bitmap = params[0];
+ switch (mShapeType) {
+ case CIRCLE:
+ return ImageUtils.getCircularBitmap(bitmap);
+ case ROUNDED:
+ return ImageUtils.getRoundedEdgeBitmap(bitmap, mRoundedCornerRadiusPx, Color.TRANSPARENT);
+ default:
+ return bitmap;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null) {
+ setImageBitmap(bitmap);
+ if (mImageLoadListener != null) {
+ mImageLoadListener.onLoaded();
+ fadeIn();
+ }
+ } else {
+ if (mImageLoadListener != null) {
+ mImageLoadListener.onError();
+ }
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPRadioButton.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPRadioButton.java
new file mode 100644
index 000000000..82fbde430
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPRadioButton.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatRadioButton;
+import android.util.AttributeSet;
+
+/**
+ * A RadioButton that uses the default font from TypefaceCache
+ */
+public class WPRadioButton extends AppCompatRadioButton {
+ public WPRadioButton(Context context) {
+ super(context);
+ TypefaceCache.setCustomTypeface(context, this, null);
+ }
+
+ public WPRadioButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+
+ public WPRadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPScrollView.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPScrollView.java
new file mode 100644
index 000000000..0133b6529
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPScrollView.java
@@ -0,0 +1,80 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ScrollView;
+
+/**
+ * ScrollView which reports when user has scrolled up or down, and when scrolling has completed
+ */
+public class WPScrollView extends ScrollView {
+
+ public interface ScrollDirectionListener {
+ void onScrollUp(float distanceY);
+ void onScrollDown(float distanceY);
+ void onScrollCompleted();
+ }
+
+ private ScrollDirectionListener mScrollDirectionListener;
+ private int mInitialScrollCheckY;
+ private static final int SCROLL_CHECK_DELAY = 250;
+
+ public WPScrollView(Context context) {
+ this(context, null);
+ }
+
+ public WPScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WPScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setScrollDirectionListener(ScrollDirectionListener listener) {
+ mScrollDirectionListener = listener;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mScrollDirectionListener != null
+ && event.getActionMasked() == MotionEvent.ACTION_MOVE
+ && event.getHistorySize() > 0) {
+ float initialY = event.getHistoricalY(event.getHistorySize() - 1);
+ float distanceY = initialY - event.getY();
+ if (distanceY < 0) {
+ mScrollDirectionListener.onScrollUp(distanceY);
+ startScrollCheck();
+ } else if (distanceY > 0) {
+ mScrollDirectionListener.onScrollDown(distanceY);
+ startScrollCheck();
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private void startScrollCheck() {
+ mInitialScrollCheckY = getScrollY();
+ post(mScrollTask);
+ }
+
+ private final Runnable mScrollTask = new Runnable() {
+ @Override
+ public void run() {
+ if (mInitialScrollCheckY == getScrollY()) {
+ mScrollDirectionListener.onScrollCompleted();
+ } else {
+ mInitialScrollCheckY = getScrollY();
+ postDelayed(mScrollTask, SCROLL_CHECK_DELAY);
+ }
+ }
+ };
+
+ public boolean canScrollUp() {
+ return canScrollVertically(-1);
+ }
+ public boolean canScrollDown() {
+ return canScrollVertically(1);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java
new file mode 100644
index 000000000..c51ac22fc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.support.v7.widget.SwitchCompat;
+import android.util.AttributeSet;
+
+public class WPSwitch extends SwitchCompat {
+ public WPSwitch(Context context) {
+ super(context);
+ }
+
+ public WPSwitch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+
+ public WPSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPTextView.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPTextView.java
new file mode 100644
index 000000000..7cad53f35
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPTextView.java
@@ -0,0 +1,82 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v7.widget.AppCompatTextView;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import org.wordpress.android.R;
+
+/**
+ * custom TextView used in layouts - enables keeping custom typeface handling in one place (so we
+ * avoid having to set the typeface for every single TextView in every single activity)
+ */
+public class WPTextView extends AppCompatTextView {
+ protected boolean mFixWidowWordEnabled;
+
+ public WPTextView(Context context) {
+ super(context, null);
+ TypefaceCache.setCustomTypeface(context, this, null);
+ }
+
+ public WPTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ readCustomAttrs(context, attrs);
+ }
+
+ public WPTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ TypefaceCache.setCustomTypeface(context, this, attrs);
+ readCustomAttrs(context, attrs);
+ }
+
+ public void setFixWidowWord(boolean enabled) {
+ mFixWidowWordEnabled = enabled;
+ }
+
+ @Override
+ public void setText(CharSequence text, BufferType type) {
+ if (!mFixWidowWordEnabled) {
+ super.setText(text, type);
+ return;
+ }
+ Spannable out;
+ int lastSpace = text.toString().lastIndexOf(' ');
+ if (lastSpace != -1 && lastSpace < text.length() - 1) {
+ // Replace last space character by a non breaking space.
+ CharSequence tmpText = replaceCharacter(text, lastSpace, "\u00A0");
+ out = new SpannableString(tmpText);
+ // Restore spans if text is an instance of Spanned
+ if (text instanceof Spanned) {
+ TextUtils.copySpansFrom((Spanned) text, 0, text.length(), null, out, 0);
+ }
+ } else {
+ out = new SpannableString(text);
+ }
+ super.setText(out, type);
+ }
+
+ private void readCustomAttrs(Context context, AttributeSet attrs) {
+ TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WPTextView, 0, 0);
+ if (array != null) {
+ mFixWidowWordEnabled = array.getBoolean(R.styleable.WPTextView_fixWidowWords, false);
+ if (mFixWidowWordEnabled) {
+ // Force text update
+ setText(getText());
+ }
+ }
+ }
+
+ private CharSequence replaceCharacter(CharSequence source, int charIndex, CharSequence replacement) {
+ if (charIndex != -1 && charIndex < source.length() - 1) {
+ return TextUtils.concat(source.subSequence(0, charIndex), replacement, source.subSequence(charIndex + 1,
+ source.length()));
+ }
+ return source;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager.java
new file mode 100644
index 000000000..35fb330b6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager.java
@@ -0,0 +1,55 @@
+package org.wordpress.android.widgets;
+
+import android.content.Context;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+/**
+ * Custom ViewPager which resolves the "pointer index out of range" bug in the compatibility library
+ * https://code.google.com/p/android/issues/detail?id=16836
+ * https://code.google.com/p/android/issues/detail?id=18990
+ * https://github.com/chrisbanes/PhotoView/issues/31
+ */
+public class WPViewPager extends ViewPager {
+ private boolean mPagingEnabled = true;
+
+ public WPViewPager(Context context) {
+ super(context);
+ }
+
+ public WPViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mPagingEnabled) {
+ try {
+ return super.onInterceptTouchEvent(ev);
+ } catch (IllegalArgumentException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mPagingEnabled) {
+ try {
+ return super.onTouchEvent(ev);
+ } catch (IllegalArgumentException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+ return false;
+ }
+
+ public void setPagingEnabled(boolean pagingEnabled) {
+ mPagingEnabled = pagingEnabled;
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java b/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java
new file mode 100644
index 000000000..a07bc2e6a
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java
@@ -0,0 +1,1189 @@
+package org.xmlrpc.android;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.webkit.URLUtil;
+
+import com.android.volley.DefaultRetryPolicy;
+import com.android.volley.NetworkResponse;
+import com.android.volley.RedirectError;
+import com.android.volley.TimeoutError;
+import com.android.volley.toolbox.RequestFuture;
+import com.android.volley.toolbox.StringRequest;
+import com.google.gson.Gson;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.BlogIdentifier;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.FeatureSet;
+import org.wordpress.android.ui.media.MediaGridFragment.Filter;
+import org.wordpress.android.ui.stats.StatsUtils;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import de.greenrobot.event.EventBus;
+
+public class ApiHelper {
+
+ public static final class Method {
+ public static final String GET_MEDIA_LIBRARY = "wp.getMediaLibrary";
+ public static final String GET_POST_FORMATS = "wp.getPostFormats";
+ public static final String GET_CATEGORIES = "wp.getCategories";
+ public static final String GET_MEDIA_ITEM = "wp.getMediaItem";
+ public static final String GET_COMMENT = "wp.getComment";
+ public static final String GET_COMMENTS = "wp.getComments";
+ public static final String GET_BLOGS = "wp.getUsersBlogs";
+ public static final String GET_OPTIONS = "wp.getOptions";
+ public static final String GET_PROFILE = "wp.getProfile";
+ public static final String GET_PAGES = "wp.getPages";
+ public static final String GET_TERM = "wp.getTerm";
+ public static final String GET_PAGE = "wp.getPage";
+
+ public static final String DELETE_COMMENT = "wp.deleteComment";
+ public static final String DELETE_PAGE = "wp.deletePage";
+ public static final String DELETE_POST = "wp.deletePost";
+
+ public static final String NEW_CATEGORY = "wp.newCategory";
+ public static final String NEW_COMMENT = "wp.newComment";
+
+ public static final String EDIT_POST = "wp.editPost";
+ public static final String EDIT_COMMENT = "wp.editComment";
+
+ public static final String SET_OPTIONS = "wp.setOptions";
+
+ public static final String UPLOAD_FILE = "wp.uploadFile";
+
+ public static final String WPCOM_GET_FEATURES = "wpcom.getFeatures";
+
+ public static final String LIST_METHODS = "system.listMethods";
+ }
+
+ public static final class Param {
+ public static final String SHOW_SUPPORTED_POST_FORMATS = "show-supported";
+ }
+
+ public enum ErrorType {
+ NO_ERROR, UNKNOWN_ERROR, INVALID_CURRENT_BLOG, NETWORK_XMLRPC, INVALID_CONTEXT,
+ INVALID_RESULT, NO_UPLOAD_FILES_CAP, CAST_EXCEPTION, TASK_CANCELLED, UNAUTHORIZED
+ }
+
+ public static final Map<String, String> blogOptionsXMLRPCParameters = new HashMap<String, String>();
+
+ static {
+ blogOptionsXMLRPCParameters.put("software_version", "software_version");
+ blogOptionsXMLRPCParameters.put("post_thumbnail", "post_thumbnail");
+ blogOptionsXMLRPCParameters.put("jetpack_client_id", "jetpack_client_id");
+ blogOptionsXMLRPCParameters.put("blog_public", "blog_public");
+ blogOptionsXMLRPCParameters.put("home_url", "home_url");
+ blogOptionsXMLRPCParameters.put("admin_url", "admin_url");
+ blogOptionsXMLRPCParameters.put("login_url", "login_url");
+ blogOptionsXMLRPCParameters.put("blog_title", "blog_title");
+ blogOptionsXMLRPCParameters.put("time_zone", "time_zone");
+ }
+
+ public static abstract class HelperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
+ protected String mErrorMessage;
+ protected ErrorType mErrorType = ErrorType.NO_ERROR;
+ protected Throwable mThrowable;
+
+ protected void setError(@NonNull ErrorType errorType, String errorMessage) {
+ mErrorMessage = errorMessage;
+ mErrorType = errorType;
+ AppLog.e(T.API, mErrorType.name() + " - " + mErrorMessage);
+ }
+
+ protected void setError(@NonNull ErrorType errorType, String errorMessage, Throwable throwable) {
+ mErrorMessage = errorMessage;
+ mErrorType = errorType;
+ mThrowable = throwable;
+ AppLog.e(T.API, mErrorType.name() + " - " + mErrorMessage, throwable);
+ }
+ }
+
+ public interface GenericErrorCallback {
+ public void onFailure(ErrorType errorType, String errorMessage, Throwable throwable);
+ }
+
+ public interface GenericCallback extends GenericErrorCallback {
+ public void onSuccess();
+ }
+
+ public interface DatabasePersistCallback {
+ void onDataReadyToSave(List list);
+ }
+
+ public static class GetPostFormatsTask extends HelperAsyncTask<Blog, Void, Object> {
+ private Blog mBlog;
+
+ @Override
+ protected Object doInBackground(Blog... blog) {
+ mBlog = blog[0];
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(),
+ mBlog.getHttppassword());
+ Object result = null;
+ Object[] params = { mBlog.getRemoteBlogId(), mBlog.getUsername(),
+ mBlog.getPassword(), Param.SHOW_SUPPORTED_POST_FORMATS };
+ try {
+ result = client.call(Method.GET_POST_FORMATS, params);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ } catch (XMLRPCException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (IOException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (XmlPullParserException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ }
+ return result;
+ }
+
+ protected void onPostExecute(Object result) {
+ if (result != null && result instanceof HashMap) {
+ Map<?, ?> postFormats = (HashMap<?, ?>) result;
+ if (postFormats.size() > 0) {
+ Gson gson = new Gson();
+ String postFormatsJson = gson.toJson(postFormats);
+ if (postFormatsJson != null) {
+ if (mBlog.bsetPostFormats(postFormatsJson)) {
+ WordPress.wpDB.saveBlog(mBlog);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public static synchronized void updateBlogOptions(Blog currentBlog, Map<?, ?> blogOptions) {
+ boolean isModified = false;
+ Gson gson = new Gson();
+ String blogOptionsJson = gson.toJson(blogOptions);
+ if (blogOptionsJson != null) {
+ isModified |= currentBlog.bsetBlogOptions(blogOptionsJson);
+ }
+
+ // Software version
+ if (!currentBlog.isDotcomFlag()) {
+ Map<?, ?> sv = (Map<?, ?>) blogOptions.get("software_version");
+ String wpVersion = MapUtils.getMapStr(sv, "value");
+ if (wpVersion.length() > 0) {
+ isModified |= currentBlog.bsetWpVersion(wpVersion);
+ }
+ }
+
+ // Featured image support
+ Map<?, ?> featuredImageHash = (Map<?, ?>) blogOptions.get("post_thumbnail");
+ if (featuredImageHash != null) {
+ boolean featuredImageCapable = MapUtils.getMapBool(featuredImageHash, "value");
+ isModified |= currentBlog.bsetFeaturedImageCapable(featuredImageCapable);
+ } else {
+ isModified |= currentBlog.bsetFeaturedImageCapable(false);
+ }
+
+ // Blog name
+ Map<?, ?> blogNameHash = (Map<?, ?>) blogOptions.get("blog_title");
+ if (blogNameHash != null) {
+ String blogName = MapUtils.getMapStr(blogNameHash, "value");
+ if (blogName != null && !blogName.equals(currentBlog.getBlogName())) {
+ currentBlog.setBlogName(blogName);
+ isModified = true;
+ }
+ }
+
+ if (isModified) {
+ WordPress.wpDB.saveBlog(currentBlog);
+ }
+ }
+
+ /**
+ * Task to refresh blog level information (WP version number) and stuff
+ * related to the active theme (available post types, recent comments, etc).
+ */
+ public static class RefreshBlogContentTask extends HelperAsyncTask<Boolean, Void, Boolean> {
+ private static HashSet<BlogIdentifier> refreshedBlogs = new HashSet<BlogIdentifier>();
+ private Blog mBlog;
+ private BlogIdentifier mBlogIdentifier;
+ private GenericCallback mCallback;
+
+ public RefreshBlogContentTask(Blog blog, GenericCallback callback) {
+ if (blog == null) {
+ cancel(true);
+ return;
+ }
+
+ mBlogIdentifier = new BlogIdentifier(blog.getUrl(), blog.getRemoteBlogId());
+ if (refreshedBlogs.contains(mBlogIdentifier)) {
+ cancel(true);
+ } else {
+ refreshedBlogs.add(mBlogIdentifier);
+ }
+
+ mBlog = blog;
+ mCallback = callback;
+ }
+
+ private void updateBlogAdmin(Map<String, Object> userInfos) {
+ if (userInfos.containsKey("roles") && ( userInfos.get("roles") instanceof Object[])) {
+ boolean isAdmin = false;
+ Object[] userRoles = (Object[])userInfos.get("roles");
+ for (int i = 0; i < userRoles.length; i++) {
+ if (userRoles[i].toString().equals("administrator")) {
+ isAdmin = true;
+ break;
+ }
+ }
+ if (mBlog.bsetAdmin(isAdmin)) {
+ WordPress.wpDB.saveBlog(mBlog);
+ EventBus.getDefault().post(new CoreEvents.BlogListChanged());
+ }
+ }
+ }
+
+ @Override
+ protected Boolean doInBackground(Boolean... params) {
+ boolean commentsOnly = params[0];
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(),
+ mBlog.getHttppassword());
+
+ boolean alreadyTrackedAsJetpackBlog = mBlog.isJetpackPowered();
+
+ if (!commentsOnly) {
+ // check the WP number if self-hosted
+ Map<String, String> hPost = ApiHelper.blogOptionsXMLRPCParameters;
+
+ Object[] vParams = {mBlog.getRemoteBlogId(),
+ mBlog.getUsername(),
+ mBlog.getPassword(),
+ hPost};
+ Object versionResult = null;
+ try {
+ versionResult = client.call(Method.GET_OPTIONS, vParams);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ return false;
+ } catch (Exception e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ return false;
+ }
+
+ if (versionResult != null) {
+ Map<?, ?> blogOptions = (HashMap<?, ?>) versionResult;
+ ApiHelper.updateBlogOptions(mBlog, blogOptions);
+ }
+
+ if (mBlog.isJetpackPowered() && !alreadyTrackedAsJetpackBlog) {
+ // blog just added to the app, or the value of jetpack_client_id has just changed
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.SIGNED_INTO_JETPACK, mBlog);
+ }
+
+ // get theme post formats
+ new GetPostFormatsTask().execute(mBlog);
+
+ //Update Stats widgets if necessary
+ String currentBlogID = String.valueOf(mBlog.getRemoteBlogId());
+ if (StatsWidgetProvider.isBlogDisplayedInWidget(mBlog.getRemoteBlogId())) {
+ AppLog.d(AppLog.T.STATS, "The blog with remoteID " + currentBlogID + " is NOT displayed in a widget. Blog Refresh Task doesn't call an update of the widget.");
+ String currentDate = StatsUtils.getCurrentDateTZ(mBlog.getLocalTableBlogId());
+ StatsWidgetProvider.enqueueStatsRequestForBlog(WordPress.getContext(), currentBlogID, currentDate);
+ }
+ }
+
+ // Check if user is an admin
+ Object[] userParams = {mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword()};
+ try {
+ Map<String, Object> userInfos = (HashMap<String, Object>) client.call(Method.GET_PROFILE, userParams);
+ updateBlogAdmin(userInfos);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ return false;
+ } catch (XMLRPCException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (IOException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (XmlPullParserException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ }
+
+ // refresh the comments
+ Map<String, Object> hPost = new HashMap<String, Object>();
+ hPost.put("number", 30);
+ Object[] commentParams = {mBlog.getRemoteBlogId(), mBlog.getUsername(),
+ mBlog.getPassword(), hPost};
+ try {
+ ApiHelper.refreshComments(mBlog, commentParams, new DatabasePersistCallback() {
+ @Override
+ public void onDataReadyToSave(List list) {
+ int localBlogId = mBlog.getLocalTableBlogId();
+ CommentTable.deleteCommentsForBlog(localBlogId);
+ CommentTable.saveComments(localBlogId, (CommentList)list);
+ }
+ });
+ } catch (Exception e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (mCallback != null) {
+ if (success) {
+ mCallback.onSuccess();
+ } else {
+ mCallback.onFailure(mErrorType, mErrorMessage, mThrowable);
+ }
+ }
+ refreshedBlogs.remove(mBlogIdentifier);
+ }
+ }
+
+ /**
+ * request deleted comments for passed blog and remove them from local db
+ * @param blog blog to check
+ * @return count of comments that were removed from db
+ */
+ public static int removeDeletedComments(Blog blog) {
+ if (blog == null) {
+ return 0;
+ }
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(
+ blog.getUri(),
+ blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Map<String, Object> hPost = new HashMap<String, Object>();
+ hPost.put("status", "trash");
+
+ Object[] params = { blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ hPost };
+
+ int numDeleted = 0;
+ try {
+ Object[] result = (Object[]) client.call(Method.GET_COMMENTS, params);
+ if (result == null || result.length == 0) {
+ return 0;
+ }
+ Map<?, ?> contentHash;
+ for (Object aComment : result) {
+ contentHash = (Map<?, ?>) aComment;
+ long commentId = Long.parseLong(contentHash.get("comment_id").toString());
+ if (CommentTable.deleteComment(blog.getLocalTableBlogId(), commentId))
+ numDeleted++;
+ }
+ if (numDeleted > 0) {
+ AppLog.d(T.COMMENTS, String.format("removed %d deleted comments", numDeleted));
+ }
+ } catch (XMLRPCException e) {
+ AppLog.e(T.COMMENTS, e);
+ } catch (IOException e) {
+ AppLog.e(T.COMMENTS, e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(T.COMMENTS, e);
+ }
+
+ return numDeleted;
+ }
+
+ public static CommentList refreshComments(Blog blog, Object[] commentParams, DatabasePersistCallback dbCallback)
+ throws XMLRPCException, IOException, XmlPullParserException {
+ if (blog == null) {
+ return null;
+ }
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Object[] result;
+ result = (Object[]) client.call(Method.GET_COMMENTS, commentParams);
+
+ if (result.length == 0) {
+ return null;
+ }
+
+ Map<?, ?> contentHash;
+ long commentID, postID;
+ String authorName, content, status, authorEmail, authorURL, postTitle, pubDate;
+ java.util.Date date;
+ CommentList comments = new CommentList();
+
+ for (int ctr = 0; ctr < result.length; ctr++) {
+ contentHash = (Map<?, ?>) result[ctr];
+ content = contentHash.get("content").toString();
+ status = contentHash.get("status").toString();
+ postID = Long.parseLong(contentHash.get("post_id").toString());
+ commentID = Long.parseLong(contentHash.get("comment_id").toString());
+ authorName = contentHash.get("author").toString();
+ authorURL = contentHash.get("author_url").toString();
+ authorEmail = contentHash.get("author_email").toString();
+ postTitle = contentHash.get("post_title").toString();
+ date = (java.util.Date) contentHash.get("date_created_gmt");
+ pubDate = DateTimeUtils.iso8601FromDate(date);
+
+ Comment comment = new Comment(
+ postID,
+ commentID,
+ authorName,
+ pubDate,
+ content,
+ status,
+ postTitle,
+ authorURL,
+ authorEmail,
+ null);
+
+ comments.add(comment);
+ }
+
+ if (dbCallback != null){
+ dbCallback.onDataReadyToSave(comments);
+ }
+
+ return comments;
+ }
+
+ /**
+ * Delete a single post or page via XML-RPC API parameters follow those of FetchSinglePostTask
+ */
+ public static class DeleteSinglePostTask extends HelperAsyncTask<Object, Boolean, Boolean> {
+
+ @Override
+ protected Boolean doInBackground(Object... arguments) {
+ Blog blog = (Blog) arguments[0];
+ if (blog == null) {
+ return false;
+ }
+
+ String postId = (String) arguments[1];
+ boolean isPage = (Boolean) arguments[2];
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Object[] params = {blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(), postId};
+ try {
+ client.call(isPage ? Method.DELETE_PAGE : Method.DELETE_POST, params);
+ return true;
+ } catch (XMLRPCException | IOException | XmlPullParserException e) {
+ mErrorMessage = e.getMessage();
+ return false;
+ }
+ }
+ }
+
+ public static class SyncMediaLibraryTask extends HelperAsyncTask<java.util.List<?>, Void, Integer> {
+ public interface Callback extends GenericErrorCallback {
+ public void onSuccess(int results);
+ }
+
+ private Callback mCallback;
+ private int mOffset;
+ private Filter mFilter;
+
+ public SyncMediaLibraryTask(int offset, Filter filter, Callback callback) {
+ mOffset = offset;
+ mCallback = callback;
+ mFilter = filter;
+ }
+
+ @Override
+ protected Integer doInBackground(List<?>... params) {
+ List<?> arguments = params[0];
+ WordPress.currentBlog = (Blog) arguments.get(0);
+ Blog blog = WordPress.currentBlog;
+ if (blog == null) {
+ setError(ErrorType.INVALID_CURRENT_BLOG, "ApiHelper - current blog is null");
+ return 0;
+ }
+
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Map<String, Object> filter = new HashMap<String, Object>();
+ filter.put("number", 50);
+ filter.put("offset", mOffset);
+
+ if (mFilter == Filter.IMAGES) {
+ filter.put("mime_type","image/*");
+ } else if(mFilter == Filter.UNATTACHED) {
+ filter.put("parent_id", 0);
+ }
+
+ Object[] apiParams = {blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(),
+ filter};
+
+ Object[] results = null;
+ try {
+ results = (Object[]) client.call(Method.GET_MEDIA_LIBRARY, apiParams);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ return 0;
+ } catch (XMLRPCException e) {
+ prepareErrorMessage(e);
+ return 0;
+ } catch (IOException e) {
+ prepareErrorMessage(e);
+ return 0;
+ } catch (XmlPullParserException e) {
+ prepareErrorMessage(e);
+ return 0;
+ }
+
+ if (blogId == null) {
+ setError(ErrorType.INVALID_CURRENT_BLOG, "Invalid blogId");
+ return 0;
+ }
+
+ if (results == null) {
+ setError(ErrorType.INVALID_RESULT, "Invalid blogId");
+ return 0;
+ }
+
+ Map<?, ?> resultMap;
+ // results returned, so mark everything existing to deleted
+ // since offset is 0, we are doing a full refresh
+ if (mOffset == 0) {
+ WordPress.wpDB.setMediaFilesMarkedForDeleted(blogId);
+ }
+ for (Object result : results) {
+ resultMap = (Map<?, ?>) result;
+ boolean isDotCom = (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isDotcomFlag());
+ MediaFile mediaFile = new MediaFile(blogId, resultMap, isDotCom);
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ }
+ WordPress.wpDB.deleteFilesMarkedForDeleted(blogId);
+ return results.length;
+ }
+
+ private void prepareErrorMessage(Exception e) {
+ // user does not have permission to view media gallery
+ if (e.getMessage() != null && e.getMessage().contains("401")) {
+ setError(ErrorType.NO_UPLOAD_FILES_CAP, e.getMessage(), e);
+ } else {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (mCallback != null) {
+ if (mErrorType == ErrorType.NO_ERROR) {
+ mCallback.onSuccess(result);
+ } else {
+ mCallback.onFailure(mErrorType, mErrorMessage, mThrowable);
+ }
+ }
+ }
+ }
+
+ public static class EditMediaItemTask extends HelperAsyncTask<List<?>, Void, Boolean> {
+ private GenericCallback mCallback;
+ private String mMediaId;
+ private String mTitle;
+ private String mDescription;
+ private String mCaption;
+
+ public EditMediaItemTask(String mediaId, String title, String description, String caption,
+ GenericCallback callback) {
+ mMediaId = mediaId;
+ mCallback = callback;
+ mTitle = title;
+ mCaption = caption;
+ mDescription = description;
+ }
+ @Override
+ protected Boolean doInBackground(List<?>... params) {
+ List<?> arguments = params[0];
+ WordPress.currentBlog = (Blog) arguments.get(0);
+ Blog blog = WordPress.currentBlog;
+
+ if (blog == null) {
+ setError(ErrorType.INVALID_CURRENT_BLOG, "ApiHelper - current blog is null");
+ return null;
+ }
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Map<String, Object> contentStruct = new HashMap<String, Object>();
+ contentStruct.put("post_title", mTitle);
+ contentStruct.put("post_content", mDescription);
+ contentStruct.put("post_excerpt", mCaption);
+
+ Object[] apiParams = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ mMediaId,
+ contentStruct
+ };
+
+ Boolean result = null;
+ try {
+ result = (Boolean) client.call(Method.EDIT_POST, apiParams);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ } catch (XMLRPCException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (IOException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (XmlPullParserException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ }
+
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (mCallback != null) {
+ if (mErrorType == ErrorType.NO_ERROR) {
+ mCallback.onSuccess();
+ } else {
+ mCallback.onFailure(mErrorType, mErrorMessage, mThrowable);
+ }
+ }
+ }
+ }
+
+ public static class GetMediaItemTask extends HelperAsyncTask<List<?>, Void, MediaFile> {
+ public interface Callback extends GenericErrorCallback {
+ public void onSuccess(MediaFile results);
+ }
+ private Callback mCallback;
+ private int mMediaId;
+
+ public GetMediaItemTask(int mediaId, Callback callback) {
+ mMediaId = mediaId;
+ mCallback = callback;
+ }
+
+ @Override
+ protected MediaFile doInBackground(List<?>... params) {
+ List<?> arguments = params[0];
+ WordPress.currentBlog = (Blog) arguments.get(0);
+ Blog blog = WordPress.currentBlog;
+ if (blog == null) {
+ setError(ErrorType.INVALID_CURRENT_BLOG, "ApiHelper - current blog is null");
+ return null;
+ }
+
+ String blogId = String.valueOf(blog.getLocalTableBlogId());
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Object[] apiParams = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ mMediaId
+ };
+ Map<?, ?> results = null;
+ try {
+ results = (Map<?, ?>) client.call(Method.GET_MEDIA_ITEM, apiParams);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ } catch (XMLRPCException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (IOException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (XmlPullParserException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ }
+
+ if (results != null && blogId != null) {
+ boolean isDotCom = (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isDotcomFlag());
+ MediaFile mediaFile = new MediaFile(blogId, results, isDotCom);
+ WordPress.wpDB.saveMediaFile(mediaFile);
+ return mediaFile;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(MediaFile result) {
+ if (mCallback != null) {
+ if (result != null) {
+ mCallback.onSuccess(result);
+ } else {
+ mCallback.onFailure(mErrorType, mErrorMessage, mThrowable);
+ }
+ }
+ }
+ }
+
+ public static class UploadMediaTask extends HelperAsyncTask<List<?>, Void, Map<?, ?>> {
+ public interface Callback extends GenericErrorCallback {
+ void onSuccess(String remoteId, String remoteUrl, String secondaryId);
+ void onProgressUpdate(float progress);
+ }
+ private Callback mCallback;
+ private Context mContext;
+ private MediaFile mMediaFile;
+
+ public UploadMediaTask(Context applicationContext, MediaFile mediaFile,
+ Callback callback) {
+ mContext = applicationContext;
+ mMediaFile = mediaFile;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Map<?, ?> doInBackground(List<?>... params) {
+ List<?> arguments = params[0];
+ WordPress.currentBlog = (Blog) arguments.get(0);
+ Blog blog = WordPress.currentBlog;
+
+ if (blog == null) {
+ setError(ErrorType.INVALID_CURRENT_BLOG, "current blog is null");
+ return null;
+ }
+
+ final XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Map<String, Object> data = new HashMap<String, Object>();
+ data.put("name", mMediaFile.getFileName());
+ data.put("type", mMediaFile.getMimeType());
+ data.put("bits", mMediaFile);
+ data.put("overwrite", true);
+
+ Object[] apiParams = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ data
+ };
+
+ final File tempFile = getTempFile(mContext);
+
+ if (client instanceof XMLRPCClient) {
+ ((XMLRPCClient) client).setOnBytesUploadedListener(new XMLRPCClient.OnBytesUploadedListener() {
+ @Override
+ public void onBytesUploaded(long uploadedBytes) {
+ if (isCancelled()) {
+ // Stop the upload if the task has been cancelled
+ ((XMLRPCClient) client).cancel();
+ }
+
+ if (tempFile == null || tempFile.length() == 0) {
+ return;
+ }
+
+ float fractionUploaded = uploadedBytes / (float) tempFile.length();
+ mCallback.onProgressUpdate(fractionUploaded);
+ }
+ });
+ }
+
+ if (mContext == null) {
+ return null;
+ }
+
+ Map<?, ?> resultMap;
+ try {
+ resultMap = (HashMap<?, ?>) client.call(Method.UPLOAD_FILE, apiParams, tempFile);
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, null, cce);
+ return null;
+ } catch (XMLRPCFault e) {
+ if (e.getFaultCode() == 401) {
+ setError(ErrorType.NETWORK_XMLRPC,
+ mContext.getString(R.string.media_error_no_permission_upload), e);
+ } else {
+ // getFaultString() returns the error message from the server without the "[Code 403]" part.
+ setError(ErrorType.NETWORK_XMLRPC, e.getFaultString(), e);
+ }
+ return null;
+ } catch (XMLRPCException e) {
+ setError(ErrorType.NETWORK_XMLRPC, null, e);
+ return null;
+ } catch (IOException e) {
+ setError(ErrorType.NETWORK_XMLRPC, null, e);
+ return null;
+ } catch (XmlPullParserException e) {
+ setError(ErrorType.NETWORK_XMLRPC, null, e);
+ return null;
+ }
+
+ if (resultMap != null && resultMap.containsKey("id")) {
+ return resultMap;
+ } else {
+ setError(ErrorType.INVALID_RESULT, null);
+ }
+
+ return null;
+ }
+
+ // Create a temp file for media upload
+ private File getTempFile(Context context) {
+ String tempFileName = "wp-" + System.currentTimeMillis();
+ try {
+ context.openFileOutput(tempFileName, Context.MODE_PRIVATE);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ return context.getFileStreamPath(tempFileName);
+ }
+
+ @Override
+ protected void onPostExecute(Map<?, ?> result) {
+ if (mCallback != null) {
+ if (result != null) {
+ String remoteId = (String) result.get("id");
+ String remoteUrl = (String) result.get("url");
+ String videoPressId = (String) result.get("videopress_shortcode");
+ mCallback.onSuccess(remoteId, remoteUrl, videoPressId);
+ } else {
+ mCallback.onFailure(mErrorType, mErrorMessage, mThrowable);
+ }
+ }
+ }
+ }
+
+ public static class DeleteMediaTask extends HelperAsyncTask<List<?>, Void, Void> {
+ private GenericCallback mCallback;
+ private String mMediaId;
+
+ public DeleteMediaTask(String mediaId, GenericCallback callback) {
+ mMediaId = mediaId;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Void doInBackground(List<?>... params) {
+ List<?> arguments = params[0];
+ Blog blog = (Blog) arguments.get(0);
+
+ if (blog == null) {
+ setError(ErrorType.INVALID_CONTEXT, "ApiHelper - invalid blog");
+ return null;
+ }
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Object[] apiParams = new Object[]{blog.getRemoteBlogId(), blog.getUsername(),
+ blog.getPassword(), mMediaId};
+
+ try {
+ if (client != null) {
+ Boolean result = (Boolean) client.call(Method.DELETE_POST, apiParams);
+ if (!result) {
+ setError(ErrorType.INVALID_RESULT, "wp.deletePost returned false");
+ }
+ }
+ } catch (ClassCastException cce) {
+ setError(ErrorType.INVALID_RESULT, cce.getMessage(), cce);
+ } catch (XMLRPCException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (IOException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ } catch (XmlPullParserException e) {
+ setError(ErrorType.NETWORK_XMLRPC, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void v) {
+ if (mCallback != null) {
+ if (mErrorType == ErrorType.NO_ERROR) {
+ mCallback.onSuccess();
+ } else {
+ mCallback.onFailure(mErrorType, mErrorMessage, mThrowable);
+ }
+ }
+ }
+ }
+
+ public static class GetFeatures extends AsyncTask<List<?>, Void, FeatureSet> {
+ public interface Callback {
+ void onResult(FeatureSet featureSet);
+ }
+
+ private Callback mCallback;
+
+ public GetFeatures() {
+ }
+
+ public GetFeatures(Callback callback) {
+ mCallback = callback;
+ }
+
+ public FeatureSet doSynchronously(List<?>... params) {
+ return doInBackground(params);
+ }
+
+ @Override
+ protected FeatureSet doInBackground(List<?>... params) {
+ List<?> arguments = params[0];
+ Blog blog = (Blog) arguments.get(0);
+
+ if (blog == null)
+ return null;
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Object[] apiParams = new Object[] {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ };
+
+ Map<?, ?> resultMap = null;
+ try {
+ resultMap = (HashMap<?, ?>) client.call(Method.WPCOM_GET_FEATURES, apiParams);
+ } catch (ClassCastException cce) {
+ AppLog.e(T.API, "wpcom.getFeatures error", cce);
+ } catch (XMLRPCException e) {
+ AppLog.e(T.API, "wpcom.getFeatures error", e);
+ } catch (IOException e) {
+ AppLog.e(T.API, "wpcom.getFeatures error", e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(T.API, "wpcom.getFeatures error", e);
+ }
+
+ if (resultMap != null) {
+ return new FeatureSet(blog.getRemoteBlogId(), resultMap);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(FeatureSet result) {
+ if (mCallback != null)
+ mCallback.onResult(result);
+ }
+
+ }
+
+ /**
+ * Synchronous method to fetch the String content at the specified HTTP URL.
+ *
+ * @param stringUrl URL to fetch contents for.
+ * @return content of the resource, or null if URL was invalid or resource could not be retrieved.
+ */
+ public static String getResponse(final String stringUrl) throws SSLHandshakeException, TimeoutError, TimeoutException {
+ return getResponse(stringUrl, 0);
+ }
+
+ private static String getRedirectURL(String oldURL, NetworkResponse networkResponse) {
+ if (networkResponse.headers != null && networkResponse.headers.containsKey("Location")) {
+ String newURL = networkResponse.headers.get("Location");
+ // Relative URL
+ if (newURL != null && newURL.startsWith("/")) {
+ Uri oldUri = Uri.parse(oldURL);
+ if (oldUri.getScheme() == null || oldUri.getAuthority() == null) {
+ return null;
+ }
+ return oldUri.getScheme() + "://" + oldUri.getAuthority() + newURL;
+ }
+ // Absolute URL
+ return newURL;
+ }
+ return null;
+ }
+
+ public static String getResponse(final String stringUrl, int numberOfRedirects) throws SSLHandshakeException, TimeoutError, TimeoutException {
+ RequestFuture<String> future = RequestFuture.newFuture();
+ StringRequest request = new StringRequest(stringUrl, future, future);
+ request.setRetryPolicy(new DefaultRetryPolicy(XMLRPCClient.DEFAULT_SOCKET_TIMEOUT_MS, 0, 1));
+ WordPress.requestQueue.add(request);
+ try {
+ return future.get(XMLRPCClient.DEFAULT_SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ AppLog.e(T.API, e);
+ } catch (ExecutionException e) {
+ if (e.getCause() != null && e.getCause() instanceof RedirectError) {
+ // Maximum 5 redirects or die
+ if (numberOfRedirects > 5) {
+ AppLog.e(T.API, "Maximum of 5 redirects reached, aborting.", e);
+ return null;
+ }
+ // Follow redirect
+ RedirectError re = (RedirectError) e.getCause();
+ if (re.networkResponse != null) {
+ String newURL = getRedirectURL(stringUrl, re.networkResponse);
+ if (newURL == null) {
+ AppLog.e(T.API, "Invalid server response", e);
+ return null;
+ }
+ // Abort redirect if old URL was HTTPS and not the new one
+ if (URLUtil.isHttpsUrl(stringUrl) && !URLUtil.isHttpsUrl(newURL)) {
+ AppLog.e(T.API, "Redirect from HTTPS to HTTP not allowed.", e);
+ return null;
+ }
+ // Retry getResponse
+ AppLog.i(T.API, "Follow redirect from " + stringUrl + " to " + newURL);
+ return getResponse(newURL, numberOfRedirects + 1);
+ }
+ } else if (e.getCause() != null && e.getCause() instanceof com.android.volley.TimeoutError) {
+ AppLog.e(T.API, e);
+ throw (com.android.volley.TimeoutError) e.getCause();
+ } else {
+ AppLog.e(T.API, e);
+ }
+ }
+ return null;
+ }
+
+ /*
+ * fetches a single post saves it to the db - note that this should NOT be called from main thread
+ */
+ public static boolean updateSinglePost(int localBlogId, String remotePostId, boolean isPage) {
+ Blog blog = WordPress.getBlog(localBlogId);
+ if (blog == null || TextUtils.isEmpty(remotePostId)) {
+ return false;
+ }
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(
+ blog.getUri(),
+ blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Object[] apiParams;
+ if (isPage) {
+ apiParams = new Object[]{
+ blog.getRemoteBlogId(),
+ remotePostId,
+ blog.getUsername(),
+ blog.getPassword()
+ };
+ } else {
+ apiParams = new Object[]{
+ remotePostId,
+ blog.getUsername(),
+ blog.getPassword()
+ };
+ }
+
+ try {
+ Object result = client.call(isPage ? Method.GET_PAGE : "metaWeblog.getPost", apiParams);
+
+ if (result != null && result instanceof Map) {
+ Map postMap = (HashMap) result;
+ List<Map<?, ?>> postsList = new ArrayList<>();
+ postsList.add(postMap);
+
+ WordPress.wpDB.savePosts(postsList, localBlogId, isPage, true);
+ return true;
+ } else {
+ return false;
+ }
+
+ } catch (XMLRPCException | IOException | XmlPullParserException e) {
+ AppLog.e(AppLog.T.POSTS, e);
+ return false;
+ }
+ }
+
+ public static boolean editComment(Blog blog, Comment comment, CommentStatus newStatus) {
+ if (blog == null) {
+ return false;
+ }
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Map<String, String> postHash = new HashMap<>();
+ postHash.put("status", CommentStatus.toString(newStatus));
+ postHash.put("content", comment.getCommentText());
+ postHash.put("author", comment.getAuthorName());
+ postHash.put("author_url", comment.getAuthorUrl());
+ postHash.put("author_email", comment.getAuthorEmail());
+
+ Object[] params = { blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ Long.toString(comment.commentID),
+ postHash};
+
+ try {
+ Object result = client.call(Method.EDIT_COMMENT, params);
+ return (result != null && Boolean.parseBoolean(result.toString()));
+ } catch (XMLRPCFault xmlrpcFault) {
+ if (xmlrpcFault.getFaultCode() == 500) {
+ // let's check whether the comment is already marked as _newStatus_
+ CommentStatus remoteStatus = getCommentStatus(blog, comment);
+ if (remoteStatus != null && remoteStatus.equals(newStatus)) {
+ // Happy days! Remote is already marked as the desired status
+ return true;
+ }
+ }
+ AppLog.e(T.COMMENTS, "Error while editing comment", xmlrpcFault);
+ } catch (XMLRPCException e) {
+ AppLog.e(T.COMMENTS, "Error while editing comment", e);
+ } catch (IOException e) {
+ AppLog.e(T.COMMENTS, "Error while editing comment", e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(T.COMMENTS, "Error while editing comment", e);
+ }
+
+ return false;
+ }
+
+ /**
+ * Fetches the status of a comment
+ * @param blog the blog the comment is in
+ * @param comment the comment to fetch its status
+ * @return the status of the comment on the server, null if error
+ */
+ public static @Nullable CommentStatus getCommentStatus(Blog blog, Comment comment) {
+ if (blog == null || comment == null) {
+ return null;
+ }
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Object[] params = { blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ Long.toString(comment.commentID)};
+
+ try {
+ Map<?, ?> contentHash = (Map<?, ?>) client.call(Method.GET_COMMENT, params);
+ final Object status = contentHash.get("status");
+ return status == null ? null : CommentStatus.fromString(status.toString());
+ } catch (XMLRPCException e) {
+ AppLog.e(T.COMMENTS, "Error while getting comment", e);
+ } catch (IOException e) {
+ AppLog.e(T.COMMENTS, "Error while getting comment", e);
+ } catch (XmlPullParserException e) {
+ AppLog.e(T.COMMENTS, "Error while getting comment", e);
+ }
+
+ return null;
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/LoggedInputStream.java b/WordPress/src/main/java/org/xmlrpc/android/LoggedInputStream.java
new file mode 100644
index 000000000..ad16c40a0
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/LoggedInputStream.java
@@ -0,0 +1,111 @@
+package org.xmlrpc.android;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/**
+ * A LoggedInputStream adds logging functionality to another input stream.
+ * <p>Note that calls on a LoggedInputStream are passed "as-is" to the underlying stream.</p>
+ *
+ *
+ * <p>We're using a LoggedInputStream in {@code XMLRPClient.java} to log the XML-RPC response document in case of parser errors.<br />
+ *
+ * There are plenty of other ways to log the response, but a {@code XmlPullParser} wants an InputStream as input parameter, and
+ * a LoggedInputStream seems the most reliable solution, with the smallest memory footprint.<br />
+ * Below are other examples of logging we tried:</p>
+ * <ul>
+ * <li>Read the first 1000 characters from the original input stream, then create a new SequenceInputStream with both the characters just read (a new ByteArrayInputStream),
+ * and the original input stream.</li>
+ * <li>Read the whole content in a String and log it, then create an StringInputStream over the string, and pass the new stream to the parser.</li>
+ * </ul>
+ */
+
+public final class LoggedInputStream extends InputStream {
+ private final InputStream inputStream;
+
+ private final static int MAX_LOG_SIZE = 1000;
+ private final byte[] loggedString = new byte[MAX_LOG_SIZE];
+ private int loggedStringSize = 0;
+
+ public LoggedInputStream(InputStream input) {
+ this.inputStream = input;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return inputStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ inputStream.close();
+ }
+
+ @Override
+ public void mark(int readlimit) {
+ inputStream.mark(readlimit);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return inputStream.markSupported();
+ }
+
+ @Override
+ public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
+ int bytesRead = inputStream.read(buffer, byteOffset, byteCount);
+ if (bytesRead != -1) {
+ log(buffer, byteOffset, bytesRead);
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public int read(byte[] buffer) throws IOException {
+ return this.read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int characterRead = inputStream.read();
+ if (characterRead != -1) {
+ log(characterRead);
+ }
+ return characterRead;
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ inputStream.reset();
+ }
+
+ @Override
+ public long skip(long byteCount) throws IOException {
+ return inputStream.skip(byteCount);
+ }
+
+ private void log(byte[] inputArray, int byteOffset, int byteCount) {
+ int availableSpace = MAX_LOG_SIZE - loggedStringSize;
+ if (availableSpace <= 0) {
+ return;
+ }
+ int bytesLength = Math.min(availableSpace, byteCount);
+ int startingPosition = MAX_LOG_SIZE - availableSpace;
+ System.arraycopy(inputArray, byteOffset, loggedString, startingPosition, bytesLength);
+ loggedStringSize += bytesLength;
+ }
+
+ private void log(int inputChar) {
+ byte[] logThis = {(byte) inputChar};
+ log(logThis, 0, 1);
+ }
+
+ public String getResponseDocument() {
+ if (loggedStringSize == 0) {
+ return "";
+ } else {
+ return new String(loggedString, 0, loggedStringSize);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/NullOutputStream.java b/WordPress/src/main/java/org/xmlrpc/android/NullOutputStream.java
new file mode 100644
index 000000000..533311224
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/NullOutputStream.java
@@ -0,0 +1,11 @@
+package org.xmlrpc.android;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**Writes to nowhere*/
+final class NullOutputStream extends OutputStream {
+ @Override
+ public void write(int b) throws IOException {
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/xmlrpc/android/TrustUserSSLCertsSocketFactory.java b/WordPress/src/main/java/org/xmlrpc/android/TrustUserSSLCertsSocketFactory.java
new file mode 100644
index 000000000..007b218cc
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/TrustUserSSLCertsSocketFactory.java
@@ -0,0 +1,162 @@
+package org.xmlrpc.android;
+
+import android.annotation.SuppressLint;
+import android.net.SSLCertificateSocketFactory;
+import android.os.Build;
+
+import org.apache.http.conn.scheme.SocketFactory;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+
+import org.wordpress.android.networking.SelfSignedSSLCertsManager;
+import org.wordpress.android.networking.WPTrustManager;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+
+
+/**
+ * Custom SSLSocketFactory that adds Self-Signed (trusted) certificates,
+ * and SNI support.
+ *
+ * Loosely based on: http://blog.dev001.net/post/67082904181/android-using-sni-and-tlsv1-2-with-apache-httpclient
+ *
+ * Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/1288
+ *
+ */
+public class TrustUserSSLCertsSocketFactory extends SSLSocketFactory {
+ private SSLCertificateSocketFactory mFactory;
+ private static final BrowserCompatHostnameVerifier mHostnameVerifier = new BrowserCompatHostnameVerifier();
+
+ public TrustUserSSLCertsSocketFactory() throws IOException, GeneralSecurityException {
+ super(null);
+ // No handshake timeout used
+ mFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
+ TrustManager[] trustAllowedCerts;
+ try {
+ trustAllowedCerts = new TrustManager[]{
+ new WPTrustManager(SelfSignedSSLCertsManager.getInstance(null).getLocalKeyStore())
+ };
+ mFactory.setTrustManagers(trustAllowedCerts);
+ } catch (GeneralSecurityException e1) {
+ AppLog.e(T.API, "Cannot set TrustAllSSLSocketFactory on our factory. Proceding without it...", e1);
+ }
+ }
+
+ public static SocketFactory getDefault() throws IOException, GeneralSecurityException {
+ return new TrustUserSSLCertsSocketFactory();
+ }
+
+ public Socket createSocket() throws IOException {
+ return mFactory.createSocket();
+ }
+
+ @Override
+ public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
+ if (autoClose) {
+ // we don't need the plainSocket
+ plainSocket.close();
+ }
+
+ SSLSocket ssl = (SSLSocket) mFactory.createSocket(InetAddress.getByName(host), port);
+ return enableSNI(ssl, host);
+ }
+
+ @SuppressLint("NewApi")
+ private Socket enableSNI(SSLSocket ssl, String host) throws SSLPeerUnverifiedException {
+ // enable TLSv1.1/1.2 if available
+ // (see https://github.com/rfc2822/davdroid/issues/229)
+ ssl.setEnabledProtocols(ssl.getSupportedProtocols());
+
+ // set up SNI before the handshake
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ AppLog.i(T.API, "Setting SNI hostname");
+ mFactory.setHostname(ssl, host);
+ } else {
+ AppLog.i(T.API, "No documented SNI support on Android <4.2, trying with reflection");
+ try {
+ java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
+ setHostnameMethod.invoke(ssl, host);
+ } catch (Exception e) {
+ AppLog.e(T.API, "SNI not useable", e);
+ }
+ }
+
+ // verify hostname and certificate
+ SSLSession session = ssl.getSession();
+ if (!mHostnameVerifier.verify(host, session)) {
+ // the verify failed. We need to check if the current certificate is in Trusted Store.
+ try {
+ Certificate[] errorChain = ssl.getSession().getPeerCertificates();
+ X509Certificate[] X509CertificateChain = new X509Certificate[errorChain.length];
+ for (int i = 0; i < errorChain.length; i++) {
+ X509Certificate x509Certificate = (X509Certificate) errorChain[0];
+ X509CertificateChain[i] = x509Certificate;
+ }
+
+ if (X509CertificateChain.length == 0) {
+ throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
+ }
+
+ if (!SelfSignedSSLCertsManager.getInstance(null).isCertificateTrusted(X509CertificateChain[0])) {
+ SelfSignedSSLCertsManager.getInstance(null).setLastFailureChain(X509CertificateChain);
+ throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
+ }
+ } catch (GeneralSecurityException e) {
+ AppLog.e(T.API, "GeneralSecurityException occurred when trying to verify a certificate that has failed" +
+ " the host name verifier check", e);
+ throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
+ } catch (IOException e) {
+ AppLog.e(T.API, "IOException occurred when trying to verify a certificate that has failed" +
+ " the host name verifier check", e);
+ throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
+ } catch (Exception e) {
+ // We don't want crash the app here for an unexpected error
+ AppLog.e(T.API, "An Exception occurred when trying to verify a certificate that has failed" +
+ " the host name verifier check", e);
+ throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
+ }
+ }
+
+ AppLog.i(T.API, "Established " + session.getProtocol()
+ + " connection with " + session.getPeerHost()
+ + " using " + session.getCipherSuite());
+
+ return ssl;
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr1, int j) throws IOException {
+ SSLSocket ssl = (SSLSocket) mFactory.createSocket(inaddr, i, inaddr1, j);
+ return enableSNI(ssl, inaddr.getHostName());
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i) throws IOException {
+ SSLSocket ssl = (SSLSocket) mFactory.createSocket(inaddr, i);
+ return enableSNI(ssl, inaddr.getHostName());
+ }
+
+ public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
+ SSLSocket ssl = (SSLSocket) mFactory.createSocket(s, i, inaddr, j);
+ return enableSNI(ssl, inaddr.getHostName());
+ }
+
+ public Socket createSocket(String s, int i) throws IOException {
+ SSLSocket ssl = (SSLSocket) mFactory.createSocket(s, i);
+ return enableSNI(ssl, s);
+ }
+
+ public String[] getDefaultCipherSuites() { return mFactory.getDefaultCipherSuites(); }
+ public String[] getSupportedCipherSuites() { return mFactory.getSupportedCipherSuites(); }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCCallback.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCCallback.java
new file mode 100644
index 000000000..45471f8dc
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCCallback.java
@@ -0,0 +1,26 @@
+package org.xmlrpc.android;
+
+/**
+ * The XMLRPCCallback interface must be implemented by a listener for an
+ * asynchronous call to a server method.
+ * When the server responds, the corresponding method on the listener is called.
+ *
+ * @author Tim Roes
+ */
+public interface XMLRPCCallback {
+ /**
+ * This callback is called whenever the server successfully responds.
+ *
+ * @param id The id as returned by the XMLRPCClient.asyncCall(..) method for this request.
+ * @param result The Object returned from the server.
+ */
+ public void onSuccess(long id, Object result);
+
+ /**
+ * This callback is called whenever an error occurs during the method call.
+ *
+ * @param id The id as returned by the XMLRPCClient.asyncCall(..) method for this request.
+ * @param error The error occured.
+ */
+ public void onFailure(long id, Exception error);
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java
new file mode 100644
index 000000000..5d6eb8dbe
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java
@@ -0,0 +1,713 @@
+package org.xmlrpc.android;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Xml;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.entity.FileEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.util.EntityUtils;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPUrlUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+import org.xmlrpc.android.ApiHelper.Method;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.net.URI;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * A WordPress XMLRPC Client.
+ * Based on android-xmlrpc: code.google.com/p/android-xmlrpc/
+ * Async support based on aXMLRPC: https://github.com/timroes/aXMLRPC
+ */
+
+public class XMLRPCClient implements XMLRPCClientInterface {
+ public static final int DEFAULT_CONNECTION_TIMEOUT_MS = 30000;
+ public static final int DEFAULT_SOCKET_TIMEOUT_MS = 60000;
+
+ public interface OnBytesUploadedListener {
+ public void onBytesUploaded(long uploadedBytes);
+ }
+
+ private static final String TAG_METHOD_CALL = "methodCall";
+ private static final String TAG_METHOD_NAME = "methodName";
+ private static final String TAG_METHOD_RESPONSE = "methodResponse";
+ private static final String TAG_PARAMS = "params";
+ private static final String TAG_PARAM = "param";
+ private static final String TAG_FAULT = "fault";
+ private static final String TAG_FAULT_CODE = "faultCode";
+ private static final String TAG_FAULT_STRING = "faultString";
+
+ private Map<Long,Caller> backgroundCalls = new HashMap<Long, Caller>();
+
+ private DefaultHttpClient mClient;
+ private OnBytesUploadedListener mOnBytesUploadedListener;
+ private HttpPost mPostMethod;
+ private XmlSerializer mSerializer;
+ private HttpParams mHttpParams;
+ private LoggedInputStream mLoggedInputStream;
+
+ private boolean mIsWpcom;
+
+ /**
+ * XMLRPCClient constructor. Creates new instance based on server URI
+ * @param uri xml-rpc server URI
+ */
+ public XMLRPCClient(URI uri, String httpuser, String httppasswd) {
+ mPostMethod = new HttpPost(uri);
+ mPostMethod.addHeader("Content-Type", "text/xml");
+ mPostMethod.addHeader("charset", "UTF-8");
+ mPostMethod.addHeader("User-Agent", WordPress.getUserAgent());
+ addWPComAuthorizationHeaderIfNeeded();
+
+ mHttpParams = mPostMethod.getParams();
+ HttpProtocolParams.setUseExpectContinue(mHttpParams, false);
+
+ UsernamePasswordCredentials credentials = null;
+ if (!TextUtils.isEmpty(httpuser) && !TextUtils.isEmpty(httppasswd)) {
+ credentials = new UsernamePasswordCredentials(httpuser, httppasswd);
+ }
+
+ mClient = instantiateClientForUri(uri, credentials);
+ mSerializer = Xml.newSerializer();
+ }
+
+ public String getResponse() {
+ if (mLoggedInputStream == null) {
+ return "";
+ }
+ return mLoggedInputStream.getResponseDocument();
+ }
+
+ private class ConnectionClient extends DefaultHttpClient {
+ public ConnectionClient(int port) throws IOException, GeneralSecurityException {
+ super();
+ TrustUserSSLCertsSocketFactory tasslf = new TrustUserSSLCertsSocketFactory();
+ Scheme scheme = new Scheme("https", tasslf, port);
+ getConnectionManager().getSchemeRegistry().register(scheme);
+ }
+ }
+
+ private DefaultHttpClient instantiateClientForUri(URI uri, UsernamePasswordCredentials usernamePasswordCredentials) {
+ DefaultHttpClient client = null;
+ if (WPUrlUtils.isWordPressCom(uri)) {
+ mIsWpcom = true;
+ }
+ if (mIsWpcom) {
+ //wpcom blog or self-hosted blog on plain HTTP
+ client = new DefaultHttpClient();
+ } else {
+ int port = uri.getPort();
+ if (port == -1) {
+ port = 443;
+ }
+
+ try {
+ client = new ConnectionClient(port);
+ } catch (GeneralSecurityException e) {
+ AppLog.e(T.API, "Cannot create the DefaultHttpClient object with our TrustUserSSLCertsSocketFactory", e);
+ client = null;
+ } catch (IOException e) {
+ AppLog.e(T.API, "Cannot create the DefaultHttpClient object with our TrustUserSSLCertsSocketFactory", e);
+ client = null;
+ }
+
+ if (client == null) {
+ client = new DefaultHttpClient();
+ }
+ }
+
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), DEFAULT_CONNECTION_TIMEOUT_MS);
+ HttpConnectionParams.setSoTimeout(client.getParams(), DEFAULT_SOCKET_TIMEOUT_MS);
+
+ // Setup HTTP Basic Auth if necessary
+ if (usernamePasswordCredentials != null) {
+ BasicCredentialsProvider cP = new BasicCredentialsProvider();
+ cP.setCredentials(AuthScope.ANY, usernamePasswordCredentials);
+ client.setCredentialsProvider(cP);
+ }
+
+ return client;
+ }
+
+ public void addQuickPostHeader(String type) {
+ mPostMethod.addHeader("WP-QUICK-POST", type);
+ }
+
+ /**
+ * Convenience constructor. Creates new instance based on server String address
+ * @param url server url
+ */
+ public XMLRPCClient(String url, String httpuser, String httppasswd) {
+ this(URI.create(url), httpuser, httppasswd);
+ }
+
+ /**
+ * Convenience XMLRPCClient constructor. Creates new instance based on server URL
+ * @param url server URL
+ */
+ public XMLRPCClient(URL url, String httpuser, String httppasswd) {
+ this(URI.create(url.toExternalForm()), httpuser, httppasswd);
+ }
+
+ /**
+ * Set WP.com auth header
+ * @param authToken authorization token
+ */
+ public void setAuthorizationHeader(String authToken) {
+ if( authToken != null)
+ mPostMethod.addHeader("Authorization", String.format("Bearer %s", authToken));
+ else
+ mPostMethod.removeHeaders("Authorization");
+ }
+
+ /**
+ * Call method with optional parameters. This is general method.
+ * If you want to call your method with 0-8 parameters, you can use more
+ * convenience call methods
+ *
+ * @param method name of method to call
+ * @param params parameters to pass to method (may be null if method has no parameters)
+ * @return deserialized method return value
+ * @throws XMLRPCException
+ */
+ public Object call(String method, Object[] params) throws XMLRPCException, IOException, XmlPullParserException {
+ return call(method, params, null);
+ }
+
+ /**
+ * Convenience method call with no parameters
+ *
+ * @param method name of method to call
+ * @return deserialized method return value
+ * @throws XMLRPCException
+ */
+ public Object call(String method) throws XMLRPCException, IOException, XmlPullParserException {
+ return call(method, null, null);
+ }
+
+
+ public Object call(String method, Object[] params, File tempFile) throws XMLRPCException, IOException, XmlPullParserException {
+ return new Caller().callXMLRPC(method, params, tempFile);
+ }
+
+ /**
+ * Convenience call for callAsync with two paramaters
+ *
+ * @param listener, methodName, parameters
+ * @return unique id of this async call
+ * @throws XMLRPCException
+ */
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params) {
+ return callAsync(listener, methodName, params, null);
+ }
+
+ /**
+ * Asynchronous XMLRPC call
+ *
+ * @param listener, XMLRPC methodName, XMLRPC parameters, File for large uploads
+ * @return unique id of this async call
+ * @throws XMLRPCException
+ */
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params, File tempFile) {
+ long id = System.currentTimeMillis();
+ new Caller(listener, id, methodName, params, tempFile).start();
+ return id;
+ }
+
+ /**
+ * Cancel the current call
+ */
+ public void cancel() {
+ mPostMethod.abort();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Object parseXMLRPCResponse(InputStream is, HttpEntity entity)
+ throws XMLRPCException, IOException, XmlPullParserException, NumberFormatException {
+ // setup pull parser
+ XmlPullParser pullParser = XmlPullParserFactory.newInstance().newPullParser();
+
+ // Many WordPress configs can output junk before the xml response (php warnings for example), this cleans it.
+ int bomCheck = -1;
+ int stopper = 0;
+ while ((bomCheck = is.read()) != -1 && stopper <= 5000) {
+ stopper++;
+ String snippet = "";
+ // 60 == '<' character
+ if (bomCheck == 60) {
+ for (int i = 0; i < 4; i++) {
+ byte[] chunk = new byte[1];
+ is.read(chunk);
+ snippet += new String(chunk, "UTF-8");
+ }
+ if (snippet.equals("?xml")) {
+ // it's all good, add xml tag back and start parsing
+ String start = "<" + snippet;
+ List<InputStream> streams = Arrays.asList(new ByteArrayInputStream(start.getBytes()), is);
+ is = new SequenceInputStream(Collections.enumeration(streams));
+ break;
+ } else {
+ // keep searching...
+ List<InputStream> streams = Arrays.asList(new ByteArrayInputStream(snippet.getBytes()), is);
+ is = new SequenceInputStream(Collections.enumeration(streams));
+ }
+ }
+ }
+
+ pullParser.setInput(is, "UTF-8");
+
+ // lets start pulling...
+ pullParser.nextTag();
+ pullParser.require(XmlPullParser.START_TAG, null, TAG_METHOD_RESPONSE);
+
+ pullParser.nextTag(); // either TAG_PARAMS (<params>) or TAG_FAULT (<fault>)
+ String tag = pullParser.getName();
+ if (tag.equals(TAG_PARAMS)) {
+ // normal response
+ pullParser.nextTag(); // TAG_PARAM (<param>)
+ pullParser.require(XmlPullParser.START_TAG, null, TAG_PARAM);
+ pullParser.nextTag(); // TAG_VALUE (<value>)
+ // no parser.require() here since its called in XMLRPCSerializer.deserialize() below
+ // deserialize result
+ Object obj = XMLRPCSerializer.deserialize(pullParser);
+ consumeHttpEntity(entity);
+ return obj;
+ } else if (tag.equals(TAG_FAULT)) {
+ // fault response
+ pullParser.nextTag(); // TAG_VALUE (<value>)
+ // no parser.require() here since its called in XMLRPCSerializer.deserialize() below
+ // deserialize fault result
+ Map<String, Object> map = (Map<String, Object>) XMLRPCSerializer.deserialize(pullParser);
+ consumeHttpEntity(entity);
+ //Check that required tags are in the response
+ if (!map.containsKey(TAG_FAULT_STRING) || !map.containsKey(TAG_FAULT_CODE)) {
+ throw new XMLRPCException("Bad XMLRPC Fault response received - <faultCode> and/or <faultString> missing!");
+ }
+ String faultString = String.valueOf(map.get(TAG_FAULT_STRING));
+ int faultCode;
+ try {
+ faultCode = (int) map.get(TAG_FAULT_CODE);
+ } catch (NumberFormatException | ClassCastException e) {
+ throw new XMLRPCException("Bad XMLRPC Fault response received - <faultCode> value is not a valid integer");
+ }
+ throw new XMLRPCFault(faultString, faultCode);
+ } else {
+ consumeHttpEntity(entity);
+ throw new XMLRPCException("Bad tag <" + tag + "> in XMLRPC response - neither <params> nor <fault>");
+ }
+ }
+
+ /**
+ * Deallocate Http Entity and close streams
+ */
+ private static void consumeHttpEntity(HttpEntity entity) {
+ // Ideally we should use EntityUtils.consume(), introduced in apache http utils 4.1 - not available in
+ // Android yet
+ if (entity != null) {
+ try {
+ entity.consumeContent();
+ } catch (IOException e) {
+ // ignore exception (could happen if Content-Length is wrong)
+ }
+ }
+ }
+
+ public void preparePostMethod(String method, Object[] params, File tempFile) throws IOException, XMLRPCException, IllegalArgumentException, IllegalStateException {
+ // prepare POST body
+ if (method.equals(Method.UPLOAD_FILE)) {
+ if (!tempFile.exists() && !tempFile.mkdirs()) {
+ throw new XMLRPCException("Path to file could not be created.");
+ }
+
+ FileWriter fileWriter = new FileWriter(tempFile);
+ mSerializer.setOutput(fileWriter);
+
+ mSerializer.startDocument(null, null);
+ mSerializer.startTag(null, TAG_METHOD_CALL);
+ // set method name
+ mSerializer.startTag(null, TAG_METHOD_NAME).text(method).endTag(null, TAG_METHOD_NAME);
+ if (params != null && params.length != 0) {
+ // set method params
+ mSerializer.startTag(null, TAG_PARAMS);
+ for (int i = 0; i < params.length; i++) {
+ mSerializer.startTag(null, TAG_PARAM).startTag(null, XMLRPCSerializer.TAG_VALUE);
+ XMLRPCSerializer.serialize(mSerializer, params[i]);
+ mSerializer.endTag(null, XMLRPCSerializer.TAG_VALUE).endTag(null, TAG_PARAM);
+ }
+ mSerializer.endTag(null, TAG_PARAMS);
+ }
+ mSerializer.endTag(null, TAG_METHOD_CALL);
+ mSerializer.endDocument();
+
+ fileWriter.flush();
+ fileWriter.close();
+
+ FileEntity fEntity = new FileEntity(tempFile, "text/xml; charset=\"UTF-8\"") {
+ // Hook in a CountingOutputStream to keep track of bytes uploaded
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ super.writeTo(new CountingOutputStream(outstream));
+ }
+ };
+
+ fEntity.setContentType("text/xml");
+ mPostMethod.setEntity(fEntity);
+ } else {
+ StringWriter bodyWriter = new StringWriter();
+ mSerializer.setOutput(bodyWriter);
+
+ mSerializer.startDocument(null, null);
+ mSerializer.startTag(null, TAG_METHOD_CALL);
+ // set method name
+ mSerializer.startTag(null, TAG_METHOD_NAME).text(method).endTag(null, TAG_METHOD_NAME);
+ if (params != null && params.length != 0) {
+ // set method params
+ mSerializer.startTag(null, TAG_PARAMS);
+ for (int i = 0; i < params.length; i++) {
+ mSerializer.startTag(null, TAG_PARAM).startTag(null, XMLRPCSerializer.TAG_VALUE);
+ if (method.equals("metaWeblog.editPost") || method.equals("metaWeblog.newPost")) {
+ XMLRPCSerializer.serialize(mSerializer, params[i]);
+ } else {
+ XMLRPCSerializer.serialize(mSerializer, params[i]);
+ }
+ mSerializer.endTag(null, XMLRPCSerializer.TAG_VALUE).endTag(null, TAG_PARAM);
+ }
+ mSerializer.endTag(null, TAG_PARAMS);
+ }
+ mSerializer.endTag(null, TAG_METHOD_CALL);
+ mSerializer.endDocument();
+
+ HttpEntity entity = new StringEntity(bodyWriter.toString());
+ mPostMethod.setEntity(entity);
+ }
+ }
+
+ /**
+ * The Caller class is used to make asynchronous calls to the server.
+ * For synchronous calls the Thread function of this class isn't used.
+ */
+ private class Caller extends Thread {
+ private XMLRPCCallback listener;
+ private long threadId;
+ private String methodName;
+ private Object[] params;
+ private File tempFile;
+
+ /**
+ * Create a new Caller for asynchronous use.
+ *
+ * @param listener The listener to notice about the response or an error.
+ * @param threadId An id that will be send to the listener.
+ * @param methodName The method name to call.
+ * @param params The parameters of the call or null.
+ */
+ public Caller(XMLRPCCallback listener, long threadId, String methodName, Object[] params, File tempFile) {
+ this.listener = listener;
+ this.threadId = threadId;
+ this.methodName = methodName;
+ this.params = params;
+ this.tempFile = tempFile;
+ }
+
+ /**
+ * Create a new Caller for synchronous use.
+ * If the caller has been created with this constructor you cannot use the
+ * start method to start it as a thread. But you can call the call method
+ * on it for synchronous use.
+ */
+ public Caller() { }
+
+ /**
+ * The run method is invoked when the thread gets started.
+ * This will only work, if the Caller has been created with parameters.
+ * It execute the call method and notify the listener about the result.
+ */
+ @Override
+ public void run() {
+ if(listener == null)
+ return;
+
+ try {
+ backgroundCalls.put(threadId, this);
+ Object o = this.callXMLRPC(methodName, params, tempFile);
+ listener.onSuccess(threadId, o);
+ } catch(CancelException ex) {
+ // Don't notify the listener, if the call has been canceled.
+ } catch (Exception ex) {
+ listener.onFailure(threadId, ex);
+ } finally {
+ backgroundCalls.remove(threadId);
+ }
+
+ }
+
+ /**
+ * Call method with optional parameters
+ *
+ * @param method name of method to call
+ * @param params parameters to pass to method (may be null if method has no parameters)
+ * @return deserialized method return value
+ * @throws XMLRPCException
+ */
+ private Object callXMLRPC(String method, Object[] params, File tempFile)
+ throws XMLRPCException, IOException, XmlPullParserException {
+ mLoggedInputStream = null;
+ try {
+ preparePostMethod(method, params, tempFile);
+
+ // execute HTTP POST request
+ HttpResponse response = mClient.execute(mPostMethod);
+
+ if (response.getStatusLine() == null) // StatusLine is null. We can't read the response code.
+ throw new XMLRPCException( "HTTP Status code is missing!" );
+
+ int statusCode = response.getStatusLine().getStatusCode();
+ HttpEntity entity = response.getEntity();
+
+ if (entity == null) {
+ //This is an error since the parser will fail here.
+ throw new XMLRPCException( "HTTP status code: " + statusCode + " was returned AND no response from the server." );
+ }
+
+ if (statusCode == HttpStatus.SC_OK) {
+ mLoggedInputStream = new LoggedInputStream(entity.getContent());
+ return XMLRPCClient.parseXMLRPCResponse(mLoggedInputStream, entity);
+ }
+
+ String statusLineReasonPhrase = StringUtils.notNullStr(response.getStatusLine().getReasonPhrase());
+ try {
+ String responseString = EntityUtils.toString(entity, "UTF-8");
+ if (TextUtils.isEmpty(responseString)) {
+ AppLog.e(T.API, "No HTTP error document document from the server");
+ } else {
+ AppLog.e(T.API, "HTTP error document received from the server: " + responseString);
+ }
+
+ if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+ //Try to intercept out of memory error here and show a better error message.
+ if (!TextUtils.isEmpty(responseString) && responseString.contains("php fatal error") &&
+ responseString.contains("bytes exhausted")) {
+ String newErrorMsg;
+ if (method.equals(Method.UPLOAD_FILE)) {
+ newErrorMsg =
+ "The server doesn't have enough memory to upload this file. You may need to increase the PHP memory limit on your site.";
+ } else {
+ newErrorMsg =
+ "The server doesn't have enough memory to fulfill the request. You may need to increase the PHP memory limit on your site.";
+ }
+ throw new XMLRPCException( statusLineReasonPhrase + ".\n\n" + newErrorMsg);
+ }
+ }
+
+ } catch (Exception e) {
+ // eat all the exceptions here, we dont want to crash the app when trying to show a
+ // better error message.
+ }
+ throw new XMLRPCException( "HTTP status code: " + statusCode + " was returned. " + statusLineReasonPhrase);
+ } catch (XMLRPCFault e) {
+ if (mLoggedInputStream!=null) {
+ AppLog.w(T.API, "Response document received from the server: " + mLoggedInputStream.getResponseDocument());
+ }
+ // Detect login issues and broadcast a message if the error is known
+ switch (e.getFaultCode()) {
+ case 403:
+ // Ignore 403 error from certain methods known for replying with incorrect error code on
+ // lacking permissions
+ if ("wp.getPostFormats".equals(method) || "wp.getCommentStatusList".equals(method)
+ || "wp.getPostStatusList".equals(method) || "wp.getPageStatusList".equals(method)) {
+ break;
+ }
+ EventBus.getDefault().post(new CoreEvents.InvalidCredentialsDetected());
+ break;
+ case 425:
+ EventBus.getDefault().post(new CoreEvents.TwoFactorAuthenticationDetected());
+ break;
+ //TODO: Check the login limit here
+ default:
+ break;
+ }
+ throw e;
+ } catch (XmlPullParserException e) {
+ AppLog.e(T.API, "Error while parsing the XML-RPC response document received from the server.", e);
+ if (mLoggedInputStream!=null) {
+ AppLog.e(T.API, "Response document received from the server: " + mLoggedInputStream.getResponseDocument());
+ }
+ checkXMLRPCErrorMessage(e);
+ throw e;
+ } catch (NumberFormatException e) {
+ //we can catch NumberFormatException here and re-throw an XMLRPCException.
+ //The response document is not a valid XML-RPC document after all.
+ AppLog.e(T.API, "Error while parsing the XML-RPC response document received from the server.", e);
+ if (mLoggedInputStream!=null) {
+ AppLog.e(T.API, "Response document received from the server: " + mLoggedInputStream.getResponseDocument());
+ }
+ throw new XMLRPCException("The response received contains an invalid number. " + e.getMessage());
+ } catch (XMLRPCException e) {
+ if (mLoggedInputStream!=null) {
+ AppLog.e(T.API, "Response document received from the server: " + mLoggedInputStream.getResponseDocument());
+ }
+ checkXMLRPCErrorMessage(e);
+ throw e;
+ } catch (SSLHandshakeException e) {
+ if (mIsWpcom) {
+ AppLog.e(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected on wordpress.com");
+ } else {
+ AppLog.w(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected.");
+ EventBus.getDefault().post(new CoreEvents.InvalidSslCertificateDetected());
+ }
+ throw e;
+ } catch (SSLPeerUnverifiedException e) {
+ if (mIsWpcom) {
+ AppLog.e(T.NUX, "SSLPeerUnverifiedException failed. Erroneous SSL certificate detected on wordpress.com");
+ } else {
+ AppLog.w(T.NUX, "SSLPeerUnverifiedException failed. Erroneous SSL certificate detected.");
+ EventBus.getDefault().post(new CoreEvents.InvalidSslCertificateDetected());
+ }
+ throw e;
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ deleteTempFile(method, tempFile);
+ try {
+ if (mLoggedInputStream != null) {
+ mLoggedInputStream.close();
+ }
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Detect login issues and broadcast a message if the error is known, App Activities should listen to these
+ * broadcasted events and present user action to take
+ *
+ * @return true if error is known and event broadcasted, false else
+ */
+ private boolean checkXMLRPCErrorMessage(Exception exception) {
+ String errorMessage = exception.getMessage().toLowerCase();
+ if ((errorMessage.contains("code: 503") || errorMessage.contains("code 503")) &&
+ (errorMessage.contains("limit reached") || errorMessage.contains("login limit"))) {
+ EventBus.getDefault().post(new CoreEvents.LoginLimitDetected());
+ return true;
+ }
+ return false;
+ }
+
+ private void deleteTempFile(String method, File tempFile) {
+ if (tempFile != null) {
+ if ((method.equals(Method.UPLOAD_FILE))){ //get rid of the temp file
+ tempFile.delete();
+ }
+ }
+ }
+
+ private void addWPComAuthorizationHeaderIfNeeded() {
+ Context ctx = WordPress.getContext();
+ if (ctx == null) return;
+
+ if (isDotComXMLRPCEndpoint(mPostMethod.getURI())) {
+ String token = AccountHelper.getDefaultAccount().getAccessToken();
+ if (!TextUtils.isEmpty(token)) {
+ setAuthorizationHeader(token);
+ }
+ }
+ }
+
+ // Return true if wpcom XML-RPC Endpoint is called on a secure connection (https).
+ public boolean isDotComXMLRPCEndpoint(URI clientUri) {
+ if (clientUri == null) return false;
+
+ String path = clientUri.getPath();
+ String host = clientUri.getHost();
+ String protocol = clientUri.getScheme();
+ if (path == null || host == null || protocol == null) {
+ return false;
+ }
+
+ return path.equals("/xmlrpc.php") && WPUrlUtils.safeToAddWordPressComAuthToken(clientUri) && protocol.equals("https");
+ }
+
+ private class CancelException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private class CountingOutputStream extends FilterOutputStream {
+
+ private long mTotalBytes;
+
+ CountingOutputStream(final OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ out.write(b);
+ mTotalBytes += b.length;
+
+ if (mOnBytesUploadedListener != null) {
+ mOnBytesUploadedListener.onBytesUploaded(mTotalBytes);
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ mTotalBytes += len;
+
+ if (mOnBytesUploadedListener != null) {
+ mOnBytesUploadedListener.onBytesUploaded(mTotalBytes);
+ }
+ }
+ }
+
+ public void setOnBytesUploadedListener(OnBytesUploadedListener listener) {
+ mOnBytesUploadedListener = listener;
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClientInterface.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClientInterface.java
new file mode 100644
index 000000000..dcdf46d8a
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClientInterface.java
@@ -0,0 +1,17 @@
+package org.xmlrpc.android;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface XMLRPCClientInterface {
+ public void addQuickPostHeader(String type);
+ public void setAuthorizationHeader(String authToken);
+ public Object call(String method, Object[] params) throws XMLRPCException, IOException, XmlPullParserException;
+ public Object call(String method) throws XMLRPCException, IOException, XmlPullParserException;
+ public Object call(String method, Object[] params, File tempFile) throws XMLRPCException, IOException, XmlPullParserException;
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params);
+ public long callAsync(XMLRPCCallback listener, String methodName, Object[] params, File tempFile);
+ public String getResponse();
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCException.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCException.java
new file mode 100644
index 000000000..6d41ec741
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCException.java
@@ -0,0 +1,16 @@
+package org.xmlrpc.android;
+
+public class XMLRPCException extends Exception {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 7499675036625522379L;
+
+ public XMLRPCException(Exception e) {
+ super(e);
+ }
+
+ public XMLRPCException(String string) {
+ super(string);
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactory.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactory.java
new file mode 100644
index 000000000..a3a30b10c
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactory.java
@@ -0,0 +1,14 @@
+package org.xmlrpc.android;
+
+import java.net.URI;
+
+public class XMLRPCFactory {
+ private static XMLRPCFactoryAbstract sFactory;
+
+ public static XMLRPCClientInterface instantiate(URI uri, String httpUser, String httpPassword) {
+ if (sFactory == null) {
+ sFactory = new XMLRPCFactoryDefault();
+ }
+ return sFactory.make(uri, httpUser, httpPassword);
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryAbstract.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryAbstract.java
new file mode 100644
index 000000000..f9640c4a9
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryAbstract.java
@@ -0,0 +1,7 @@
+package org.xmlrpc.android;
+
+import java.net.URI;
+
+public interface XMLRPCFactoryAbstract {
+ public XMLRPCClientInterface make(URI uri, String httpUser, String httpPassword);
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryDefault.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryDefault.java
new file mode 100644
index 000000000..16a3b7ead
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFactoryDefault.java
@@ -0,0 +1,9 @@
+package org.xmlrpc.android;
+
+import java.net.URI;
+
+public class XMLRPCFactoryDefault implements XMLRPCFactoryAbstract {
+ public XMLRPCClientInterface make(URI uri, String httpUser, String httpPassword) {
+ return new XMLRPCClient(uri, httpUser, httpPassword);
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFault.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFault.java
new file mode 100644
index 000000000..ab594e838
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCFault.java
@@ -0,0 +1,28 @@
+package org.xmlrpc.android;
+
+public class XMLRPCFault extends XMLRPCException {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 5676562456612956519L;
+ private String faultString;
+ private int faultCode;
+
+ public XMLRPCFault(String faultString, int faultCode) {
+ super(faultString);
+ this.faultString = faultString;
+ this.faultCode = faultCode;
+ }
+
+ public String getFaultString() {
+ return faultString;
+ }
+
+ public int getFaultCode() {
+ return faultCode;
+ }
+
+ public String getMessage() {
+ return super.getMessage() + " [Code: "+this.faultCode+"]";
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCSerializer.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCSerializer.java
new file mode 100644
index 000000000..5840ea118
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCSerializer.java
@@ -0,0 +1,298 @@
+package org.xmlrpc.android;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Xml;
+
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SimpleTimeZone;
+
+class XMLRPCSerializer {
+ static final String TAG_NAME = "name";
+ static final String TAG_MEMBER = "member";
+ static final String TAG_VALUE = "value";
+ static final String TAG_DATA = "data";
+
+ static final String TYPE_INT = "int";
+ static final String TYPE_I4 = "i4";
+ static final String TYPE_I8 = "i8";
+ static final String TYPE_DOUBLE = "double";
+ static final String TYPE_BOOLEAN = "boolean";
+ static final String TYPE_STRING = "string";
+ static final String TYPE_DATE_TIME_ISO8601 = "dateTime.iso8601";
+ static final String TYPE_BASE64 = "base64";
+ static final String TYPE_ARRAY = "array";
+ static final String TYPE_STRUCT = "struct";
+
+ static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ static Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT"));
+
+ private static final XmlSerializer serializeTester;
+
+ static {
+ serializeTester = Xml.newSerializer();
+ try {
+ serializeTester.setOutput(new NullOutputStream(), "UTF-8");
+ } catch (IllegalArgumentException e) {
+ AppLog.e(AppLog.T.EDITOR, "IllegalArgumentException setting test serializer output stream", e );
+ } catch (IllegalStateException e) {
+ AppLog.e(AppLog.T.EDITOR, "IllegalStateException setting test serializer output stream", e );
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.EDITOR, "IOException setting test serializer output stream", e );
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ static void serialize(XmlSerializer serializer, Object object) throws IOException {
+ // check for scalar types:
+ if (object instanceof Integer || object instanceof Short || object instanceof Byte) {
+ serializer.startTag(null, TYPE_I4).text(object.toString()).endTag(null, TYPE_I4);
+ } else
+ if (object instanceof Long) {
+ // Note Long should be represented by a TYPE_I8 but the WordPress end point doesn't support <i8> tag
+ // Long usually represents IDs, so we convert them to string
+ serializer.startTag(null, TYPE_STRING).text(object.toString()).endTag(null, TYPE_STRING);
+ AppLog.w(T.API, "long type could be misinterpreted when sent to the WordPress XMLRPC end point");
+ } else
+ if (object instanceof Double || object instanceof Float) {
+ serializer.startTag(null, TYPE_DOUBLE).text(object.toString()).endTag(null, TYPE_DOUBLE);
+ } else
+ if (object instanceof Boolean) {
+ Boolean bool = (Boolean) object;
+ String boolStr = bool.booleanValue() ? "1" : "0";
+ serializer.startTag(null, TYPE_BOOLEAN).text(boolStr).endTag(null, TYPE_BOOLEAN);
+ } else
+ if (object instanceof String) {
+ serializer.startTag(null, TYPE_STRING).text(makeValidInputString((String) object)).endTag(null, TYPE_STRING);
+ } else
+ if (object instanceof Date || object instanceof Calendar) {
+ Date date = (Date) object;
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ dateFormat.setCalendar(cal);
+ String sDate = dateFormat.format(date);
+ serializer.startTag(null, TYPE_DATE_TIME_ISO8601).text(sDate).endTag(null, TYPE_DATE_TIME_ISO8601);
+ } else
+ if (object instanceof byte[] ){
+ String value;
+ try {
+ value = Base64.encodeToString((byte[])object, Base64.DEFAULT);
+ serializer.startTag(null, TYPE_BASE64).text(value).endTag(null, TYPE_BASE64);
+ } catch (OutOfMemoryError e) {
+ throw new IOException("Out of memory");
+ }
+ }
+ else if( object instanceof MediaFile ) {
+ //convert media file binary to base64
+ serializer.startTag( null, "base64" );
+ MediaFile mediaFile = (MediaFile) object;
+ InputStream inStream = new DataInputStream(new FileInputStream(mediaFile.getFilePath()));
+ byte[] buffer = new byte[3600];//you must use a 24bit multiple
+ int length = -1;
+ String chunk = null;
+ while ((length = inStream.read(buffer)) > 0) {
+ chunk = Base64.encodeToString(buffer, 0, length, Base64.DEFAULT);
+ serializer.text(chunk);
+ }
+ inStream.close();
+ serializer.endTag(null, "base64");
+ }else
+ if (object instanceof List<?>) {
+ serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA);
+ List<Object> list = (List<Object>) object;
+ Iterator<Object> iter = list.iterator();
+ while (iter.hasNext()) {
+ Object o = iter.next();
+ serializer.startTag(null, TAG_VALUE);
+ serialize(serializer, o);
+ serializer.endTag(null, TAG_VALUE);
+ }
+ serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY);
+ } else
+ if (object instanceof Object[]) {
+ serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA);
+ Object[] objects = (Object[]) object;
+ for (int i=0; i<objects.length; i++) {
+ Object o = objects[i];
+ serializer.startTag(null, TAG_VALUE);
+ serialize(serializer, o);
+ serializer.endTag(null, TAG_VALUE);
+ }
+ serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY);
+ } else
+ if (object instanceof Map) {
+ serializer.startTag(null, TYPE_STRUCT);
+ Map<String, Object> map = (Map<String, Object>) object;
+ Iterator<Entry<String, Object>> iter = map.entrySet().iterator();
+ while (iter.hasNext()) {
+ Entry<String, Object> entry = iter.next();
+ String key = entry.getKey();
+ Object value = entry.getValue();
+
+ serializer.startTag(null, TAG_MEMBER);
+ serializer.startTag(null, TAG_NAME).text(key).endTag(null, TAG_NAME);
+ serializer.startTag(null, TAG_VALUE);
+ serialize(serializer, value);
+ serializer.endTag(null, TAG_VALUE);
+ serializer.endTag(null, TAG_MEMBER);
+ }
+ serializer.endTag(null, TYPE_STRUCT);
+ } else {
+ throw new IOException("Cannot serialize " + object);
+ }
+ }
+
+ private static final String makeValidInputString(final String input) throws IOException {
+ if (TextUtils.isEmpty(input))
+ return "";
+
+ if (serializeTester == null)
+ return input;
+
+ try {
+ // try to encode the string as-is, 99.9% of the time it's OK
+ serializeTester.text(input);
+ return input;
+ } catch (IllegalArgumentException e) {
+ // There are characters outside the XML unicode charset as specified by the XML 1.0 standard
+ // See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
+ AppLog.e(AppLog.T.EDITOR, "There are characters outside the XML unicode charset as specified by the XML 1.0 standard", e );
+ }
+
+ // We need to do the following things:
+ // 1. Replace surrogates with HTML Entity.
+ // 2. Replace emoji with their textual versions (if available on WP)
+ // 3. Try to serialize the resulting string.
+ // 4. If it fails again, strip characters that are not allowed in XML 1.0
+ final String noEmojiString = StringUtils.replaceUnicodeSurrogateBlocksWithHTMLEntities(input);
+ try {
+ serializeTester.text(noEmojiString);
+ return noEmojiString;
+ } catch (IllegalArgumentException e) {
+ AppLog.e(AppLog.T.EDITOR, "noEmojiString still contains characters outside the XML unicode charset as specified by the XML 1.0 standard", e );
+ return StringUtils.stripNonValidXMLCharacters(noEmojiString);
+ }
+ }
+
+ static Object deserialize(XmlPullParser parser) throws XmlPullParserException, IOException, NumberFormatException {
+ parser.require(XmlPullParser.START_TAG, null, TAG_VALUE);
+
+ parser.nextTag();
+ String typeNodeName = parser.getName();
+
+ Object obj;
+ if (typeNodeName.equals(TYPE_INT) || typeNodeName.equals(TYPE_I4)) {
+ String value = parser.nextText();
+ try {
+ obj = Integer.parseInt(value);
+ } catch (NumberFormatException nfe) {
+ AppLog.w(T.API, "Server replied with an invalid 4 bytes int value, trying to parse it as 8 bytes long");
+ obj = Long.parseLong(value);
+ }
+ } else
+ if (typeNodeName.equals(TYPE_I8)) {
+ String value = parser.nextText();
+ obj = Long.parseLong(value);
+ } else
+ if (typeNodeName.equals(TYPE_DOUBLE)) {
+ String value = parser.nextText();
+ obj = Double.parseDouble(value);
+ } else
+ if (typeNodeName.equals(TYPE_BOOLEAN)) {
+ String value = parser.nextText();
+ obj = value.equals("1") ? Boolean.TRUE : Boolean.FALSE;
+ } else
+ if (typeNodeName.equals(TYPE_STRING)) {
+ obj = parser.nextText();
+ } else
+ if (typeNodeName.equals(TYPE_DATE_TIME_ISO8601)) {
+ dateFormat.setCalendar(cal);
+ String value = parser.nextText();
+ try {
+ obj = dateFormat.parseObject(value);
+ } catch (ParseException e) {
+ AppLog.e(T.API, e);
+ obj = value;
+ }
+ } else
+ if (typeNodeName.equals(TYPE_BASE64)) {
+ String value = parser.nextText();
+ BufferedReader reader = new BufferedReader(new StringReader(value));
+ String line;
+ StringBuffer sb = new StringBuffer();
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ obj = Base64.decode(sb.toString(), Base64.DEFAULT);
+ } else
+ if (typeNodeName.equals(TYPE_ARRAY)) {
+ parser.nextTag(); // TAG_DATA (<data>)
+ parser.require(XmlPullParser.START_TAG, null, TAG_DATA);
+
+ parser.nextTag();
+ List<Object> list = new ArrayList<Object>();
+ while (parser.getName().equals(TAG_VALUE)) {
+ list.add(deserialize(parser));
+ parser.nextTag();
+ }
+ parser.require(XmlPullParser.END_TAG, null, TAG_DATA);
+ parser.nextTag(); // TAG_ARRAY (</array>)
+ parser.require(XmlPullParser.END_TAG, null, TYPE_ARRAY);
+ obj = list.toArray();
+ } else
+ if (typeNodeName.equals(TYPE_STRUCT)) {
+ parser.nextTag();
+ Map<String, Object> map = new HashMap<String, Object>();
+ while (parser.getName().equals(TAG_MEMBER)) {
+ String memberName = null;
+ Object memberValue = null;
+ while (true) {
+ parser.nextTag();
+ String name = parser.getName();
+ if (name.equals(TAG_NAME)) {
+ memberName = parser.nextText();
+ } else
+ if (name.equals(TAG_VALUE)) {
+ memberValue = deserialize(parser);
+ } else {
+ break;
+ }
+ }
+ if (memberName != null && memberValue != null) {
+ map.put(memberName, memberValue);
+ }
+ parser.require(XmlPullParser.END_TAG, null, TAG_MEMBER);
+ parser.nextTag();
+ }
+ parser.require(XmlPullParser.END_TAG, null, TYPE_STRUCT);
+ obj = map;
+ } else {
+ throw new IOException("Cannot deserialize " + parser.getName());
+ }
+ parser.nextTag(); // TAG_VALUE (</value>)
+ parser.require(XmlPullParser.END_TAG, null, TAG_VALUE);
+ return obj;
+ }
+}
diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCUtils.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCUtils.java
new file mode 100644
index 000000000..2df0ed270
--- /dev/null
+++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCUtils.java
@@ -0,0 +1,570 @@
+package org.xmlrpc.android;
+
+import android.support.annotation.StringRes;
+import android.text.TextUtils;
+import android.util.Xml;
+import android.webkit.URLUtil;
+
+import com.android.volley.TimeoutError;
+
+import org.apache.http.conn.ConnectTimeoutException;
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPUrlUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.XMLRPCUtils.XMLRPCUtilsException.Kind;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+public class XMLRPCUtils {
+
+ public static class XMLRPCUtilsException extends Exception {
+ public enum Kind {
+ SITE_URL_CANNOT_BE_EMPTY,
+ INVALID_URL,
+ MISSING_XMLRPC_METHOD,
+ ERRONEOUS_SSL_CERTIFICATE,
+ HTTP_AUTH_REQUIRED,
+ SITE_TIME_OUT,
+ NO_SITE_ERROR,
+ XMLRPC_MALFORMED_RESPONSE,
+ XMLRPC_ERROR
+ }
+
+ public final Kind kind;
+ public final
+ @StringRes
+ int errorMsgId;
+ public final String failedUrl;
+ public final String clientResponse;
+
+ public XMLRPCUtilsException(Kind kind, @StringRes int errorMsgId, String failedUrl, String clientResponse) {
+ this.kind = kind;
+ this.errorMsgId = errorMsgId;
+ this.failedUrl = failedUrl;
+ this.clientResponse = clientResponse;
+ }
+ }
+
+ private static @StringRes int handleXmlRpcFault(XMLRPCFault xmlRpcFault) {
+ AppLog.e(AppLog.T.NUX, "XMLRPCFault received from XMLRPC call wp.getUsersBlogs", xmlRpcFault);
+ switch (xmlRpcFault.getFaultCode()) {
+ case 403:
+ return org.wordpress.android.R.string.username_or_password_incorrect;
+ case 404:
+ return org.wordpress.android.R.string.xmlrpc_error;
+ case 425:
+ return org.wordpress.android.R.string.account_two_step_auth_enabled;
+ default:
+ return org.wordpress.android.R.string.no_site_error;
+ }
+ }
+
+ private static boolean isHTTPAuthErrorMessage(Exception e) {
+ return e != null && e.getMessage() != null && e.getMessage().contains("401");
+ }
+
+ private static Object doSystemListMethodsXMLRPC(String url, String httpUsername, String httpPassword) throws
+ XMLRPCException, IOException, XmlPullParserException, XMLRPCUtilsException {
+ if (!UrlUtils.isValidUrlAndHostNotNull(url)) {
+ AppLog.e(AppLog.T.NUX, "invalid URL: " + url);
+ throw new XMLRPCUtilsException(Kind.INVALID_URL, org.wordpress.android.R.string
+ .invalid_site_url_message, url, null);
+ }
+
+ AppLog.i(AppLog.T.NUX, "Trying system.listMethods on the following URL: " + url);
+ URI uri = URI.create(url);
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(uri, httpUsername, httpPassword);
+ return client.call(ApiHelper.Method.LIST_METHODS);
+ }
+
+ private static boolean validateListMethodsResponse(Object[] availableMethods) {
+ if (availableMethods == null) {
+ AppLog.e(AppLog.T.NUX, "The response of system.listMethods was empty!");
+ return false;
+ }
+ // validate xmlrpc methods
+ String[] requiredMethods = {"wp.getUsersBlogs", "wp.getPage", "wp.getCommentStatusList", "wp.newComment",
+ "wp.editComment", "wp.deleteComment", "wp.getComments", "wp.getComment",
+ "wp.getOptions", "wp.uploadFile", "wp.newCategory",
+ "wp.getTags", "wp.getCategories", "wp.editPage", "wp.deletePage",
+ "wp.newPage", "wp.getPages"};
+
+ for (String currentRequiredMethod : requiredMethods) {
+ boolean match = false;
+ for (Object currentAvailableMethod : availableMethods) {
+ if ((currentAvailableMethod).equals(currentRequiredMethod)) {
+ match = true;
+ break;
+ }
+ }
+
+ if (!match) {
+ AppLog.e(AppLog.T.NUX, "The following XML-RPC method: " + currentRequiredMethod + " is missing on the" +
+ " server.");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Append "xmlrpc.php" if missing in the URL
+ private static String appendXMLRPCPath(String url) {
+ // Don't use 'ends' here! Some hosting wants parameters passed to baseURL/xmlrpc-php?my-authcode=XXX
+ if (url.contains("xmlrpc.php")) {
+ return url;
+ } else {
+ return url + "/xmlrpc.php";
+ }
+ }
+
+ /**
+ * Truncate a string beginning at the marker
+ * @param url input string
+ * @param marker the marker to begin the truncation from
+ * @return new string truncated to the begining of the marker or the input string if marker is not found
+ */
+ private static String truncateUrl(String url, String marker) {
+ if (TextUtils.isEmpty(marker) || url.indexOf(marker) < 0) {
+ return url;
+ }
+
+ final String newUrl = url.substring(0, url.indexOf(marker));
+
+ return URLUtil.isValidUrl(newUrl) ? newUrl : url;
+ }
+
+ public static String sanitizeSiteUrl(String siteUrl, boolean addHttps) throws XMLRPCUtilsException {
+ // remove padding whitespace
+ String url = siteUrl.trim();
+
+ if (TextUtils.isEmpty(url)) {
+ throw new XMLRPCUtilsException(XMLRPCUtilsException.Kind.SITE_URL_CANNOT_BE_EMPTY, R.string
+ .invalid_site_url_message, siteUrl, null);
+ }
+
+ // Convert IDN names to punycode if necessary
+ url = UrlUtils.convertUrlToPunycodeIfNeeded(url);
+
+ // Add http to the beginning of the URL if needed
+ url = UrlUtils.addUrlSchemeIfNeeded(url, addHttps);
+
+ // strip url from known usual trailing paths
+ url = XMLRPCUtils.stripKnownPaths(url);
+
+ if (!(URLUtil.isHttpsUrl(url) || URLUtil.isHttpUrl(url))) {
+ throw new XMLRPCUtilsException(Kind.INVALID_URL, R.string.invalid_site_url_message, url, null);
+ }
+
+ return url;
+ }
+
+ private static String stripKnownPaths(String url) {
+ // Remove 'wp-login.php' if available in the URL
+ String sanitizedURL = truncateUrl(url, "wp-login.php");
+
+ // Remove '/wp-admin' if available in the URL
+ sanitizedURL = truncateUrl(sanitizedURL, "/wp-admin");
+
+ // Remove '/wp-content' if available in the URL
+ sanitizedURL = truncateUrl(sanitizedURL, "/wp-content");
+
+ sanitizedURL = truncateUrl(sanitizedURL, "/xmlrpc.php?rsd");
+
+ // remove any trailing slashes
+ while (sanitizedURL.endsWith("/")) {
+ sanitizedURL = sanitizedURL.substring(0, sanitizedURL.length() - 1);
+ }
+
+ return sanitizedURL;
+ }
+
+ private static boolean checkXMLRPCEndpointValidity(String url, String httpUsername, String httpPassword) throws
+ XMLRPCUtilsException {
+ try {
+ Object[] methods = (Object[]) doSystemListMethodsXMLRPC(url, httpUsername, httpPassword);
+ if (methods == null) {
+ AppLog.e(AppLog.T.NUX, "The response of system.listMethods was empty!");
+ return false;
+ }
+ // Exit the loop on the first URL that replies with a XML-RPC doc.
+ AppLog.i(AppLog.T.NUX, "system.listMethods replied with XML-RPC objects on the URL: " + url);
+ AppLog.i(AppLog.T.NUX, "Validating the XML-RPC response...");
+ if (validateListMethodsResponse(methods)) {
+ // Endpoint address found and works fine.
+ AppLog.i(AppLog.T.NUX, "Validation ended with success!!! Endpoint found!!!");
+ return true;
+ } else {
+ // Endpoint found, but it has problem.
+ AppLog.w(AppLog.T.NUX, "Validation ended with errors!!! Endpoint found but doesn't contain all the " +
+ "required methods.");
+ throw new XMLRPCUtilsException(Kind.MISSING_XMLRPC_METHOD, org.wordpress.android
+ .R.string.xmlrpc_missing_method_error, url, null);
+ }
+ } catch (XMLRPCException e) {
+ AppLog.e(AppLog.T.NUX, "system.listMethods failed on: " + url, e);
+ if (isHTTPAuthErrorMessage(e)) {
+ throw new XMLRPCUtilsException(Kind.HTTP_AUTH_REQUIRED, 0, url, null);
+ }
+ } catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
+ if (!WPUrlUtils.isWordPressCom(url)) {
+ throw new XMLRPCUtilsException(Kind.ERRONEOUS_SSL_CERTIFICATE, 0, url, null);
+ }
+ AppLog.e(AppLog.T.NUX, "SSL error. Erroneous SSL certificate detected.", e);
+ } catch (IOException | XmlPullParserException e) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_FAILED_TO_GUESS_XMLRPC);
+ AppLog.e(AppLog.T.NUX, "system.listMethods failed on: " + url, e);
+ if (isHTTPAuthErrorMessage(e)) {
+ throw new XMLRPCUtilsException(Kind.HTTP_AUTH_REQUIRED, 0, url, null);
+ }
+ } catch (IllegalArgumentException e) {
+ // The XML-RPC client returns this error in case of redirect to an invalid URL.
+ AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_FAILED_TO_GUESS_XMLRPC);
+ throw new XMLRPCUtilsException(Kind.INVALID_URL, org.wordpress.android.R.string
+ .invalid_site_url_message, url, null);
+ }
+
+ return false;
+ }
+
+ public static String verifyOrDiscoverXmlRpcUrl(final String siteUrl, final String httpUsername, final String
+ httpPassword) throws XMLRPCUtilsException {
+ String xmlrpcUrl = XMLRPCUtils.verifyXmlrpcUrl(siteUrl, httpUsername, httpPassword);
+
+ if (xmlrpcUrl == null) {
+ AppLog.w(AppLog.T.NUX, "The XML-RPC endpoint was not found by using our 'smart' cleaning approach" +
+ ". Time to start the Endpoint discovery process");
+
+ // Try to discover the XML-RPC Endpoint address
+ xmlrpcUrl = XMLRPCUtils.discoverSelfHostedXmlrpcUrl(siteUrl, httpUsername, httpPassword);
+ }
+
+ // Validate the XML-RPC URL we've found before. This check prevents a crash that can occur
+ // during the setup of self-hosted sites that have malformed xmlrpc URLs in their declaration.
+ if (!URLUtil.isValidUrl(xmlrpcUrl)) {
+ throw new XMLRPCUtilsException(Kind.NO_SITE_ERROR, R.string.invalid_site_url_message, xmlrpcUrl, null);
+ }
+
+ return xmlrpcUrl;
+ }
+
+ private static String verifyXmlrpcUrl(final String siteUrl, final String httpUsername, final String httpPassword)
+ throws XMLRPCUtilsException {
+ // Ordered set of Strings that contains the URLs we want to try. No discovery ;)
+ final Set<String> urlsToTry = new LinkedHashSet<>();
+
+ final String sanitizedSiteUrlHttps = XMLRPCUtils.sanitizeSiteUrl(siteUrl, true);
+ final String sanitizedSiteUrlHttp = XMLRPCUtils.sanitizeSiteUrl(siteUrl, false);
+
+ // start by adding the https URL with 'xmlrpc.php'. This will be the first URL to try.
+ urlsToTry.add(XMLRPCUtils.appendXMLRPCPath(sanitizedSiteUrlHttp));
+ urlsToTry.add(XMLRPCUtils.appendXMLRPCPath(sanitizedSiteUrlHttps));
+
+ // add the sanitized https URL without the '/xmlrpc.php' suffix added to it
+ urlsToTry.add(sanitizedSiteUrlHttp);
+ urlsToTry.add(sanitizedSiteUrlHttps);
+
+ // add the user provided URL as well
+ urlsToTry.add(siteUrl);
+
+ AppLog.i(AppLog.T.NUX, "The app will call system.listMethods on the following URLs: " + urlsToTry);
+ for (String url : urlsToTry) {
+ try {
+ if (XMLRPCUtils.checkXMLRPCEndpointValidity(url, httpUsername, httpPassword)) {
+ // Endpoint found and works fine.
+ return url;
+ }
+ } catch (XMLRPCUtilsException e) {
+ if (e.kind == XMLRPCUtilsException.Kind.ERRONEOUS_SSL_CERTIFICATE ||
+ e.kind == XMLRPCUtilsException.Kind.HTTP_AUTH_REQUIRED ||
+ e.kind == XMLRPCUtilsException.Kind.MISSING_XMLRPC_METHOD) {
+ throw e;
+ }
+ // swallow the error since we are just verifying various URLs
+ continue;
+ } catch (RuntimeException re) {
+ // depending how corrupt the user entered URL is, it can generate several kind of runtime exceptions,
+ // ignore them
+ continue;
+ }
+ }
+
+ // input url was not verified to be working
+ return null;
+ }
+
+ // Attempts to retrieve the xmlrpc url for a self-hosted site.
+ // See diagrams here https://github.com/wordpress-mobile/WordPress-Android/issues/3805 for details about the
+ // whole process.
+ private static String discoverSelfHostedXmlrpcUrl(String siteUrl, String httpUsername, String httpPassword) throws
+ XMLRPCUtilsException {
+ // Ordered set of Strings that contains the URLs we want to try
+ final Set<String> urlsToTry = new LinkedHashSet<>();
+
+ // add the url as provided by the user
+ urlsToTry.add(siteUrl);
+
+ // add a sanitized version of the https url (if the user didn't specify it)
+ urlsToTry.add(sanitizeSiteUrl(siteUrl, true));
+
+ // add a sanitized version of the http url (if the user didn't specify it)
+ urlsToTry.add(sanitizeSiteUrl(siteUrl, false));
+
+ AppLog.i(AppLog.T.NUX, "The app will call the RSD discovery process on the following URLs: " + urlsToTry);
+
+ String xmlrpcUrl = null;
+ for (String currentURL : urlsToTry) {
+ try {
+ // Download the HTML content
+ AppLog.i(AppLog.T.NUX, "Downloading the HTML content at the following URL: " + currentURL);
+ String responseHTML = ApiHelper.getResponse(currentURL);
+ if (TextUtils.isEmpty(responseHTML)) {
+ AppLog.w(AppLog.T.NUX, "Content downloaded but it's empty or null. Skipping this URL");
+ continue;
+ }
+
+ // Try to find the RSD tag with a regex
+ String rsdUrl = getRSDMetaTagHrefRegEx(responseHTML);
+ // If the regex approach fails try to parse the HTML doc and retrieve the RSD tag.
+ if (rsdUrl == null) {
+ rsdUrl = getRSDMetaTagHref(responseHTML);
+ }
+ rsdUrl = UrlUtils.addUrlSchemeIfNeeded(rsdUrl, false);
+
+ // if the RSD URL is empty here, try to see if there is already the pingback or the Apilink in the doc
+ // the user could have inserted a direct link to the xml-rpc endpoint
+ if (rsdUrl == null) {
+ AppLog.i(AppLog.T.NUX, "Can't find the RSD endpoint in the HTML document. Try to check the " +
+ "pingback tag, and the apiLink tag.");
+ xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXMLRPCPingback(responseHTML), false);
+ if (xmlrpcUrl == null) {
+ xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXMLRPCApiLink(responseHTML), false);
+ }
+ } else {
+ AppLog.i(AppLog.T.NUX, "RSD endpoint found at the following address: " + rsdUrl);
+ AppLog.i(AppLog.T.NUX, "Downloading the RSD document...");
+ String rsdEndpointDocument = ApiHelper.getResponse(rsdUrl);
+ if (TextUtils.isEmpty(rsdEndpointDocument)) {
+ AppLog.w(AppLog.T.NUX, "Content downloaded but it's empty or null. Skipping this RSD document" +
+ " URL.");
+ continue;
+ }
+ AppLog.i(AppLog.T.NUX, "Extracting the XML-RPC Endpoint address from the RSD document");
+ xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXMLRPCApiLink(rsdEndpointDocument), false);
+ }
+ if (xmlrpcUrl != null) {
+ AppLog.i(AppLog.T.NUX, "Found the XML-RPC endpoint in the HTML document!!!");
+ break;
+ } else {
+ AppLog.i(AppLog.T.NUX, "XML-RPC endpoint NOT found");
+ }
+ } catch (SSLHandshakeException e) {
+ if (!WPUrlUtils.isWordPressCom(currentURL)) {
+ throw new XMLRPCUtilsException(Kind.ERRONEOUS_SSL_CERTIFICATE, 0, currentURL, null);
+ }
+ AppLog.w(AppLog.T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected.");
+ return null;
+ } catch (TimeoutError | TimeoutException e) {
+ AppLog.w(AppLog.T.NUX, "Timeout error while connecting to the site: " + currentURL);
+ throw new XMLRPCUtilsException(Kind.SITE_TIME_OUT, org.wordpress.android.R
+ .string.site_timeout_error, currentURL, null);
+ }
+ }
+
+ if (URLUtil.isValidUrl(xmlrpcUrl)) {
+ if (checkXMLRPCEndpointValidity(xmlrpcUrl, httpUsername, httpPassword)) {
+ // Endpoint found and works fine.
+ return xmlrpcUrl;
+ }
+ }
+
+ throw new XMLRPCUtilsException(Kind.NO_SITE_ERROR, org.wordpress.android.R.string.no_site_error, null, null);
+ }
+
+ public static List<Map<String, Object>> getUserBlogsList(URI xmlrpcUri, String username, String password, String
+ httpUsername, String httpPassword) throws XMLRPCUtilsException {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(xmlrpcUri, httpUsername, httpPassword);
+ Object[] params = { username, password };
+ try {
+ Object[] userBlogs = (Object[]) client.call(ApiHelper.Method.GET_BLOGS, params);
+ if (userBlogs == null) {
+ // Could happen if the returned server response is truncated
+ throw new XMLRPCUtilsException(Kind.XMLRPC_MALFORMED_RESPONSE, R.string.xmlrpc_malformed_response_error,
+ xmlrpcUri.toString(), client.getResponse());
+ }
+ Arrays.sort(userBlogs, BlogUtils.BlogNameComparator);
+ List<Map<String, Object>> userBlogList = new ArrayList<>();
+ for (Object blog : userBlogs) {
+ try {
+ userBlogList.add((Map<String, Object>) blog);
+ } catch (ClassCastException e) {
+ AppLog.e(AppLog.T.NUX, "invalid data received from XMLRPC call wp.getUsersBlogs");
+ }
+ }
+ return userBlogList;
+ } catch (XmlPullParserException parserException) {
+ AppLog.e(AppLog.T.NUX, "invalid data received from XMLRPC call wp.getUsersBlogs", parserException);
+ throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.xmlrpc_error, xmlrpcUri.toString(), client
+ .getResponse());
+ } catch (XMLRPCFault xmlRpcFault) {
+ AppLog.e(AppLog.T.NUX, "XMLRPCFault received from XMLRPC call wp.getUsersBlogs", xmlRpcFault);
+ throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, handleXmlRpcFault(xmlRpcFault), xmlrpcUri.toString()
+ , client.getResponse());
+ } catch (XMLRPCException xmlRpcException) {
+ AppLog.e(AppLog.T.NUX, "XMLRPCException received from XMLRPC call wp.getUsersBlogs", xmlRpcException);
+ throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.no_site_error, xmlrpcUri.toString(), client
+ .getResponse());
+ } catch (SSLHandshakeException e) {
+ if (!WPUrlUtils.isWordPressCom(xmlrpcUri.toString())) {
+ throw new XMLRPCUtilsException(Kind.ERRONEOUS_SSL_CERTIFICATE, 0, xmlrpcUri.toString(), null);
+ }
+ AppLog.w(AppLog.T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected.");
+ } catch (ConnectTimeoutException e) {
+ AppLog.e(AppLog.T.NUX, "Timeout exception when calling wp.getUsersBlogs", e);
+ throw new XMLRPCUtilsException(Kind.SITE_TIME_OUT, R.string.site_timeout_error,
+ xmlrpcUri.toString(), client.getResponse());
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.NUX, "Exception received from XMLRPC call wp.getUsersBlogs", e);
+ throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.no_site_error, xmlrpcUri.toString(), client
+ .getResponse());
+ }
+
+ throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.no_site_error, xmlrpcUri.toString(), client
+ .getResponse());
+ }
+
+ /**
+ * Regex pattern for matching the RSD link found in most WordPress sites.
+ */
+ private static final Pattern rsdLink = Pattern.compile(
+ "<link\\s*?rel=\"EditURI\"\\s*?type=\"application/rsd\\+xml\"\\s*?title=\"RSD\"\\s*?href=\"(.*?)\"",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ /**
+ * Returns RSD URL based on regex match
+ *
+ * @return String RSD url
+ */
+ private static String getRSDMetaTagHrefRegEx(String html) {
+ if (html != null) {
+ Matcher matcher = rsdLink.matcher(html);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns RSD URL based on html tag search
+ *
+ * @return String RSD url
+ */
+ private static String getRSDMetaTagHref(String data) {
+ // parse the html and get the attribute for xmlrpc endpoint
+ if (data != null) {
+ // Many WordPress configs can output junk before the xml response (php warnings for example), this cleans
+ // it.
+ int indexOfFirstXML = data.indexOf("<?xml");
+ if (indexOfFirstXML > 0) {
+ data = data.substring(indexOfFirstXML);
+ }
+ StringReader stringReader = new StringReader(data);
+ XmlPullParser parser = Xml.newPullParser();
+ try {
+ // auto-detect the encoding from the stream
+ parser.setInput(stringReader);
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ String name;
+ String rel = "";
+ String type = "";
+ String href = "";
+ switch (eventType) {
+ case XmlPullParser.START_TAG:
+ name = parser.getName();
+ if (name.equalsIgnoreCase("link")) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String attrName = parser.getAttributeName(i);
+ String attrValue = parser.getAttributeValue(i);
+ if (attrName.equals("rel")) {
+ rel = attrValue;
+ } else if (attrName.equals("type"))
+ type = attrValue;
+ else if (attrName.equals("href"))
+ href = attrValue;
+ }
+
+ if (rel.equals("EditURI") && type.equals("application/rsd+xml")) {
+ return href;
+ }
+ // currentMessage.setLink(parser.nextText());
+ }
+ break;
+ }
+ eventType = parser.next();
+ }
+ } catch (XmlPullParserException e) {
+ AppLog.e(AppLog.T.API, e);
+ return null;
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.API, e);
+ return null;
+ }
+ }
+ return null; // never found the rsd tag
+ }
+
+ /**
+ * Find the XML-RPC endpoint for the WordPress API.
+ *
+ * @return XML-RPC endpoint for the specified blog, or null if unable to discover endpoint.
+ */
+ private static String getXMLRPCApiLink(String html) {
+ Pattern xmlrpcLink = Pattern.compile("<api\\s*?name=\"WordPress\".*?apiLink=\"(.*?)\"",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ if (html != null) {
+ Matcher matcher = xmlrpcLink.matcher(html);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ }
+ return null; // never found the api link tag
+ }
+
+ /**
+ * Find the XML-RPC endpoint by using the pingback tag
+ *
+ * @return String XML-RPC url
+ */
+ private static String getXMLRPCPingback(String html) {
+ Pattern pingbackLink = Pattern.compile(
+ "<link\\s*?rel=\"pingback\"\\s*?href=\"(.*?)\"",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ if (html != null) {
+ Matcher matcher = pingbackLink.matcher(html);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ }
+ return null;
+ }
+}
diff --git a/WordPress/src/main/res/anim/activity_slide_in_from_left.xml b/WordPress/src/main/res/anim/activity_slide_in_from_left.xml
new file mode 100644
index 000000000..54265e051
--- /dev/null
+++ b/WordPress/src/main/res/anim/activity_slide_in_from_left.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator">
+
+ <translate
+ android:fromXDelta="-100%p"
+ android:toXDelta="0" />
+
+</set> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/activity_slide_in_from_right.xml b/WordPress/src/main/res/anim/activity_slide_in_from_right.xml
new file mode 100644
index 000000000..9300067a7
--- /dev/null
+++ b/WordPress/src/main/res/anim/activity_slide_in_from_right.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator">
+
+ <translate
+ android:fromXDelta="100%p"
+ android:toXDelta="0" />
+
+</set> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/activity_slide_out_to_left.xml b/WordPress/src/main/res/anim/activity_slide_out_to_left.xml
new file mode 100644
index 000000000..f562c7e27
--- /dev/null
+++ b/WordPress/src/main/res/anim/activity_slide_out_to_left.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator">
+
+ <translate
+ android:fromXDelta="0"
+ android:toXDelta="-100%p" />
+
+</set>
diff --git a/WordPress/src/main/res/anim/activity_slide_out_to_right.xml b/WordPress/src/main/res/anim/activity_slide_out_to_right.xml
new file mode 100644
index 000000000..cb6ddae0c
--- /dev/null
+++ b/WordPress/src/main/res/anim/activity_slide_out_to_right.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator">
+
+ <translate
+ android:fromXDelta="0"
+ android:toXDelta="100%p" />
+
+</set>
diff --git a/WordPress/src/main/res/anim/blink.xml b/WordPress/src/main/res/anim/blink.xml
new file mode 100644
index 000000000..2d1035dfe
--- /dev/null
+++ b/WordPress/src/main/res/anim/blink.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true" >
+
+ <alpha
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:repeatMode="reverse"
+ android:duration="600"
+ android:repeatCount="infinite"
+ android:startOffset="200" />
+</set> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/box_with_pages_slide_up_page1.xml b/WordPress/src/main/res/anim/box_with_pages_slide_up_page1.xml
new file mode 100644
index 000000000..a85fc5429
--- /dev/null
+++ b/WordPress/src/main/res/anim/box_with_pages_slide_up_page1.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:startOffset="200"
+ android:duration="600"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"
+ android:interpolator="@android:anim/overshoot_interpolator" />
+
diff --git a/WordPress/src/main/res/anim/box_with_pages_slide_up_page2.xml b/WordPress/src/main/res/anim/box_with_pages_slide_up_page2.xml
new file mode 100644
index 000000000..782df4db5
--- /dev/null
+++ b/WordPress/src/main/res/anim/box_with_pages_slide_up_page2.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:startOffset="0"
+ android:duration="800"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"
+ android:interpolator="@android:anim/overshoot_interpolator" />
+
diff --git a/WordPress/src/main/res/anim/box_with_pages_slide_up_page3.xml b/WordPress/src/main/res/anim/box_with_pages_slide_up_page3.xml
new file mode 100644
index 000000000..bb4f315d8
--- /dev/null
+++ b/WordPress/src/main/res/anim/box_with_pages_slide_up_page3.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:startOffset="300"
+ android:duration="600"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"
+ android:interpolator="@android:anim/overshoot_interpolator" />
+
diff --git a/WordPress/src/main/res/anim/cab_deselect.xml b/WordPress/src/main/res/anim/cab_deselect.xml
new file mode 100644
index 000000000..6f586aa5f
--- /dev/null
+++ b/WordPress/src/main/res/anim/cab_deselect.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Counterpart to cab_select.xml
+-->
+<scale xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fromXScale="1.0"
+ android:toXScale="0.0"
+ android:fromYScale="1.0"
+ android:toYScale="0.0" />
diff --git a/WordPress/src/main/res/anim/cab_select.xml b/WordPress/src/main/res/anim/cab_select.xml
new file mode 100644
index 000000000..a8d0a10ed
--- /dev/null
+++ b/WordPress/src/main/res/anim/cab_select.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ animation used on checkbox when selecting a list item in CAB mode
+ see ui/Comments/CommentAdapter for example
+-->
+<scale xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fromXScale="0.0"
+ android:toXScale="1.0"
+ android:fromYScale="0.0"
+ android:toYScale="1.0" />
diff --git a/WordPress/src/main/res/anim/cycle_5.xml b/WordPress/src/main/res/anim/cycle_5.xml
new file mode 100644
index 000000000..4dfe175d7
--- /dev/null
+++ b/WordPress/src/main/res/anim/cycle_5.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+ <!--
+ Copyright (C) 2007 The Android Open Source Project Licensed under
+ the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a
+ copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ -->
+
+<cycleInterpolator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:cycles="5" />
diff --git a/WordPress/src/main/res/anim/do_nothing.xml b/WordPress/src/main/res/anim/do_nothing.xml
new file mode 100644
index 000000000..4c8f1d992
--- /dev/null
+++ b/WordPress/src/main/res/anim/do_nothing.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="300"
+ android:fromXDelta="0"
+ android:toXDelta="0" />
diff --git a/WordPress/src/main/res/anim/fade_out.xml b/WordPress/src/main/res/anim/fade_out.xml
new file mode 100644
index 000000000..18050e8eb
--- /dev/null
+++ b/WordPress/src/main/res/anim/fade_out.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true" >
+
+ <alpha
+ android:fromAlpha="1"
+ android:toAlpha="0"
+ android:duration="600" />
+</set> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/notifications_button_scale.xml b/WordPress/src/main/res/anim/notifications_button_scale.xml
new file mode 100644
index 000000000..1e33859bf
--- /dev/null
+++ b/WordPress/src/main/res/anim/notifications_button_scale.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- quick scale animation to show for moderation buttons in CommentDetailFragment -->
+<scale xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fillAfter="false"
+ android:fromXScale="1.0"
+ android:fromYScale="1.0"
+ android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="1.5"
+ android:toYScale="1.5"
+ android:repeatMode="reverse"
+ android:repeatCount="1" /> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/pop.xml b/WordPress/src/main/res/anim/pop.xml
new file mode 100644
index 000000000..3da388a95
--- /dev/null
+++ b/WordPress/src/main/res/anim/pop.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<scale xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_longAnimTime"
+ android:fillAfter="true"
+ android:fromXScale="0.0"
+ android:fromYScale="0.0"
+ android:interpolator="@android:anim/overshoot_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="1.0"
+ android:toYScale="1.0" />
diff --git a/WordPress/src/main/res/anim/pressed_card.xml b/WordPress/src/main/res/anim/pressed_card.xml
new file mode 100644
index 000000000..ccab3487d
--- /dev/null
+++ b/WordPress/src/main/res/anim/pressed_card.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_pressed="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="@dimen/card_elevation_pressed"
+ android:valueType="floatType" />
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </item>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/raise.xml b/WordPress/src/main/res/anim/raise.xml
new file mode 100644
index 000000000..0b42ac7a1
--- /dev/null
+++ b/WordPress/src/main/res/anim/raise.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_pressed="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/WordPress/src/main/res/anim/reader_flyin.xml b/WordPress/src/main/res/anim/reader_flyin.xml
new file mode 100644
index 000000000..040a795df
--- /dev/null
+++ b/WordPress/src/main/res/anim/reader_flyin.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"/>
diff --git a/WordPress/src/main/res/anim/reader_flyout.xml b/WordPress/src/main/res/anim/reader_flyout.xml
new file mode 100644
index 000000000..f1928562c
--- /dev/null
+++ b/WordPress/src/main/res/anim/reader_flyout.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromYDelta="0"
+ android:toYDelta="100%p"/>
diff --git a/WordPress/src/main/res/anim/reader_top_bar_in.xml b/WordPress/src/main/res/anim/reader_top_bar_in.xml
new file mode 100644
index 000000000..8d9a73b61
--- /dev/null
+++ b/WordPress/src/main/res/anim/reader_top_bar_in.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator">
+
+ <translate
+ android:fromYDelta="-100%p"
+ android:toYDelta="0"/>
+
+</set> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/reader_top_bar_out.xml b/WordPress/src/main/res/anim/reader_top_bar_out.xml
new file mode 100644
index 000000000..03e36a60a
--- /dev/null
+++ b/WordPress/src/main/res/anim/reader_top_bar_out.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:interpolator="@android:anim/accelerate_interpolator">
+
+ <translate
+ android:fromYDelta="0"
+ android:toYDelta="-100%p"/>
+
+</set> \ No newline at end of file
diff --git a/WordPress/src/main/res/anim/shake.xml b/WordPress/src/main/res/anim/shake.xml
new file mode 100644
index 000000000..628940fca
--- /dev/null
+++ b/WordPress/src/main/res/anim/shake.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+ <!--
+ Copyright (C) 2007 The Android Open Source Project Licensed under
+ the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a
+ copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ -->
+
+<translate
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromXDelta="0"
+ android:toXDelta="10"
+ android:duration="1000"
+ android:interpolator="@anim/cycle_5" />
diff --git a/WordPress/src/main/res/anim/slide_up.xml b/WordPress/src/main/res/anim/slide_up.xml
new file mode 100644
index 000000000..7c6821e29
--- /dev/null
+++ b/WordPress/src/main/res/anim/slide_up.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="300"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"
+ android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
diff --git a/WordPress/src/main/res/color/calypso_segmented_control_text.xml b/WordPress/src/main/res/color/calypso_segmented_control_text.xml
new file mode 100644
index 000000000..89f487e9e
--- /dev/null
+++ b/WordPress/src/main/res/color/calypso_segmented_control_text.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:color="@color/grey_dark"/>
+ <item android:state_checked="true" android:color="@color/grey_dark"/>
+ <item android:color="@color/grey"/>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/calypso_subtitle_text.xml b/WordPress/src/main/res/color/calypso_subtitle_text.xml
new file mode 100644
index 000000000..c59a5eda1
--- /dev/null
+++ b/WordPress/src/main/res/color/calypso_subtitle_text.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:color="@color/grey_lighten_10"
+ android:state_enabled="false" />
+
+ <item android:color="@color/grey_darken_10" />
+
+</selector>
diff --git a/WordPress/src/main/res/color/calypso_title_text.xml b/WordPress/src/main/res/color/calypso_title_text.xml
new file mode 100644
index 000000000..ad850970e
--- /dev/null
+++ b/WordPress/src/main/res/color/calypso_title_text.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:color="@color/grey_lighten_10"
+ android:state_enabled="false" />
+
+ <item android:color="@color/grey_dark" />
+
+</selector>
diff --git a/WordPress/src/main/res/color/dialog_compound_button.xml b/WordPress/src/main/res/color/dialog_compound_button.xml
new file mode 100644
index 000000000..31e228846
--- /dev/null
+++ b/WordPress/src/main/res/color/dialog_compound_button.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:color="@color/blue_medium"
+ android:state_checked="true" />
+
+ <item
+ android:color="@color/grey_lighten_20"
+ android:state_checked="false" />
+
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/media_grid_item_checkstate_text_selector.xml b/WordPress/src/main/res/color/media_grid_item_checkstate_text_selector.xml
new file mode 100644
index 000000000..e0ff141f7
--- /dev/null
+++ b/WordPress/src/main/res/color/media_grid_item_checkstate_text_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:color="@color/media_gallery_option_default" android:state_checked="true"/>
+ <item android:color="@color/media_gallery_option_default" android:state_pressed="true"/>
+ <item android:color="@color/media_gallery_option_selected"/>
+
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/nux_primary_button.xml b/WordPress/src/main/res/color/nux_primary_button.xml
new file mode 100644
index 000000000..258254f0c
--- /dev/null
+++ b/WordPress/src/main/res/color/nux_primary_button.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false" android:color="@color/nux_grey_button" />
+ <item android:color="@color/white"/>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/reader_count_text.xml b/WordPress/src/main/res/color/reader_count_text.xml
new file mode 100644
index 000000000..1ad916ac8
--- /dev/null
+++ b/WordPress/src/main/res/color/reader_count_text.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/grey_disabled" android:state_enabled="false" />
+ <item android:color="@color/blue_light" android:state_pressed="true" />
+ <item android:color="@color/orange_jazzy" android:state_selected="true" />
+ <item android:color="@color/grey_lighten_10" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/reader_follow_button_text.xml b/WordPress/src/main/res/color/reader_follow_button_text.xml
new file mode 100644
index 000000000..7a86e50c3
--- /dev/null
+++ b/WordPress/src/main/res/color/reader_follow_button_text.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/reader_following" android:state_selected="true" />
+ <item android:color="@color/reader_follow" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/reader_like_text.xml b/WordPress/src/main/res/color/reader_like_text.xml
new file mode 100644
index 000000000..2faaa2306
--- /dev/null
+++ b/WordPress/src/main/res/color/reader_like_text.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/grey_disabled" android:state_enabled="false" />
+ <item android:color="@color/orange_jazzy" android:state_pressed="true" />
+ <item android:color="@color/orange_jazzy" android:state_selected="true" />
+ <item android:color="@color/reader_hyperlink" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/color/related_posts_list_header.xml b/WordPress/src/main/res/color/related_posts_list_header.xml
new file mode 100644
index 000000000..6c730e29c
--- /dev/null
+++ b/WordPress/src/main/res/color/related_posts_list_header.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:color="@color/grey_dark"
+ android:state_enabled="true" />
+
+ <item
+ android:color="@color/translucent_grey" />
+
+</selector>
diff --git a/WordPress/src/main/res/color/related_posts_preview_header.xml b/WordPress/src/main/res/color/related_posts_preview_header.xml
new file mode 100644
index 000000000..908d6534c
--- /dev/null
+++ b/WordPress/src/main/res/color/related_posts_preview_header.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:color="@color/grey"
+ android:state_enabled="true" />
+
+ <item
+ android:color="@color/grey_lighten_20" />
+
+</selector>
diff --git a/WordPress/src/main/res/color/tab_text_color.xml b/WordPress/src/main/res/color/tab_text_color.xml
new file mode 100644
index 000000000..e265ce6d1
--- /dev/null
+++ b/WordPress/src/main/res/color/tab_text_color.xml
@@ -0,0 +1,5 @@
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/tab_text_selected" android:state_selected="true" />
+ <item android:color="@color/tab_text_normal" />
+</selector>
diff --git a/WordPress/src/main/res/drawable-hdpi-v4/action_mode_confirm_checkmark.png b/WordPress/src/main/res/drawable-hdpi-v4/action_mode_confirm_checkmark.png
new file mode 100644
index 000000000..64d9d7b73
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi-v4/action_mode_confirm_checkmark.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi-v4/ic_media_play.png b/WordPress/src/main/res/drawable-hdpi-v4/ic_media_play.png
new file mode 100644
index 000000000..2746d17fb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi-v4/ic_media_play.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi-v4/tab_icon_create_gallery.png b/WordPress/src/main/res/drawable-hdpi-v4/tab_icon_create_gallery.png
new file mode 100644
index 000000000..89e2adbe0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi-v4/tab_icon_create_gallery.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/arrow.png b/WordPress/src/main/res/drawable-hdpi/arrow.png
new file mode 100644
index 000000000..406e23d54
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/arrow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/blavatar_placeholder.png b/WordPress/src/main/res/drawable-hdpi/blavatar_placeholder.png
new file mode 100644
index 000000000..f61e63ba0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/blavatar_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/box_with_pages_bottom.png b/WordPress/src/main/res/drawable-hdpi/box_with_pages_bottom.png
new file mode 100644
index 000000000..dfd87c299
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/box_with_pages_bottom.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/box_with_pages_page1.png b/WordPress/src/main/res/drawable-hdpi/box_with_pages_page1.png
new file mode 100644
index 000000000..9b54bda04
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/box_with_pages_page1.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/box_with_pages_page2.png b/WordPress/src/main/res/drawable-hdpi/box_with_pages_page2.png
new file mode 100644
index 000000000..f6446fb21
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/box_with_pages_page2.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/box_with_pages_page3.png b/WordPress/src/main/res/drawable-hdpi/box_with_pages_page3.png
new file mode 100644
index 000000000..e421f2aea
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/box_with_pages_page3.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/box_with_pages_top.png b/WordPress/src/main/res/drawable-hdpi/box_with_pages_top.png
new file mode 100644
index 000000000..7bb2fd0e3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/box_with_pages_top.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/btn_cab_done_default_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/btn_cab_done_default_wordpress.9.png
new file mode 100644
index 000000000..c8d351eda
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/btn_cab_done_default_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/btn_cab_done_focused_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/btn_cab_done_focused_wordpress.9.png
new file mode 100644
index 000000000..e56c779ac
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/btn_cab_done_focused_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/btn_cab_done_pressed_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/btn_cab_done_pressed_wordpress.9.png
new file mode 100644
index 000000000..b3e4c0c56
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/btn_cab_done_pressed_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/button_blue.9.png b/WordPress/src/main/res/drawable-hdpi/button_blue.9.png
new file mode 100644
index 000000000..485f88cd8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/button_blue.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/button_blue_disabled.9.png b/WordPress/src/main/res/drawable-hdpi/button_blue_disabled.9.png
new file mode 100644
index 000000000..71fccc230
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/button_blue_disabled.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/button_blue_focus.9.png b/WordPress/src/main/res/drawable-hdpi/button_blue_focus.9.png
new file mode 100644
index 000000000..a7a817e77
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/button_blue_focus.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/button_darkgray.9.png b/WordPress/src/main/res/drawable-hdpi/button_darkgray.9.png
new file mode 100644
index 000000000..02bd52445
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/button_darkgray.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/button_darkgray_disabled.9.png b/WordPress/src/main/res/drawable-hdpi/button_darkgray_disabled.9.png
new file mode 100644
index 000000000..93fde4da4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/button_darkgray_disabled.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/button_darkgray_focus.9.png b/WordPress/src/main/res/drawable-hdpi/button_darkgray_focus.9.png
new file mode 100644
index 000000000..a9200f484
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/button_darkgray_focus.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_admin_site.png b/WordPress/src/main/res/drawable-hdpi/dashicon_admin_site.png
new file mode 100644
index 000000000..4fed0b7a2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_admin_site.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_admin_users.png b/WordPress/src/main/res/drawable-hdpi/dashicon_admin_users.png
new file mode 100644
index 000000000..604b90e1a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_admin_users.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_edit.png b/WordPress/src/main/res/drawable-hdpi/dashicon_edit.png
new file mode 100644
index 000000000..858a1f81d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_edit.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_email.png b/WordPress/src/main/res/drawable-hdpi/dashicon_email.png
new file mode 100644
index 000000000..8415f1b55
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_email.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_eye_closed.png b/WordPress/src/main/res/drawable-hdpi/dashicon_eye_closed.png
new file mode 100644
index 000000000..b02db8c67
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_eye_closed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_eye_open.png b/WordPress/src/main/res/drawable-hdpi/dashicon_eye_open.png
new file mode 100644
index 000000000..1cdafbeb0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_eye_open.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_info.png b/WordPress/src/main/res/drawable-hdpi/dashicon_info.png
new file mode 100644
index 000000000..4e4a5d97b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_info.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_lock.png b/WordPress/src/main/res/drawable-hdpi/dashicon_lock.png
new file mode 100644
index 000000000..a6b5ac838
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_lock.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dashicon_wordpress_alt.png b/WordPress/src/main/res/drawable-hdpi/dashicon_wordpress_alt.png
new file mode 100644
index 000000000..f9c03d6fa
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dashicon_wordpress_alt.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/dialog_full_holo_light.9.png b/WordPress/src/main/res/drawable-hdpi/dialog_full_holo_light.9.png
new file mode 100644
index 000000000..e029f210b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/dialog_full_holo_light.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/drake_empty_results.png b/WordPress/src/main/res/drawable-hdpi/drake_empty_results.png
new file mode 100644
index 000000000..de38a7065
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/drake_empty_results.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/endlist_logo.png b/WordPress/src/main/res/drawable-hdpi/endlist_logo.png
new file mode 100644
index 000000000..08e2a5b33
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/endlist_logo.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_closed.png b/WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_closed.png
new file mode 100644
index 000000000..c4703afa5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_closed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_open.png b/WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_open.png
new file mode 100644
index 000000000..8a5b9c69b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_arrow_dropdown_open.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_checkbox_empty.png b/WordPress/src/main/res/drawable-hdpi/gallery_checkbox_empty.png
new file mode 100644
index 000000000..984b21b18
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_checkbox_empty.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_circles.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_circles.png
new file mode 100644
index 000000000..4cc4ac4b2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_circles.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_circles_selected.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_circles_selected.png
new file mode 100644
index 000000000..50b4f1e01
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_circles_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow.png
new file mode 100644
index 000000000..43ed49e9f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow_selected.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow_selected.png
new file mode 100644
index 000000000..654c427db
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_slideshow_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_squares.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_squares.png
new file mode 100644
index 000000000..b772930f3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_squares.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_squares_selected.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_squares_selected.png
new file mode 100644
index 000000000..1a10d319d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_squares_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid.png
new file mode 100644
index 000000000..8b196bd02
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid_selected.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid_selected.png
new file mode 100644
index 000000000..422f26c0b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_thumbnailgrid_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled.png
new file mode 100644
index 000000000..97ebc61b1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled_selected.png b/WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled_selected.png
new file mode 100644
index 000000000..f0e68ba11
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_icon_tiled_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gallery_tablet_move_file.png b/WordPress/src/main/res/drawable-hdpi/gallery_tablet_move_file.png
new file mode 100644
index 000000000..c1d030f24
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gallery_tablet_move_file.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gravatar_placeholder.png b/WordPress/src/main/res/drawable-hdpi/gravatar_placeholder.png
new file mode 100644
index 000000000..3397af3ba
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gravatar_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gridicon_create_dark.png b/WordPress/src/main/res/drawable-hdpi/gridicon_create_dark.png
new file mode 100644
index 000000000..5a36a8b45
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gridicon_create_dark.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/gridicon_create_light.png b/WordPress/src/main/res/drawable-hdpi/gridicon_create_light.png
new file mode 100644
index 000000000..e72774170
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/gridicon_create_light.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_approve.png b/WordPress/src/main/res/drawable-hdpi/ic_action_approve.png
new file mode 100644
index 000000000..d4728b555
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_approve.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_approve_active.png b/WordPress/src/main/res/drawable-hdpi/ic_action_approve_active.png
new file mode 100644
index 000000000..933733d95
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_approve_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_camera.png b/WordPress/src/main/res/drawable-hdpi/ic_action_camera.png
new file mode 100644
index 000000000..c1a3549bf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_camera.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_comment.png b/WordPress/src/main/res/drawable-hdpi/ic_action_comment.png
new file mode 100644
index 000000000..b8dea0631
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_like.png b/WordPress/src/main/res/drawable-hdpi/ic_action_like.png
new file mode 100644
index 000000000..35510e1fd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_like_active.png b/WordPress/src/main/res/drawable-hdpi/ic_action_like_active.png
new file mode 100644
index 000000000..9fda1cc76
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_like_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_location_found.png b/WordPress/src/main/res/drawable-hdpi/ic_action_location_found.png
new file mode 100644
index 000000000..681fc14dd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_location_found.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_location_off.png b/WordPress/src/main/res/drawable-hdpi/ic_action_location_off.png
new file mode 100644
index 000000000..1a614257e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_location_off.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_location_searching.png b/WordPress/src/main/res/drawable-hdpi/ic_action_location_searching.png
new file mode 100644
index 000000000..48f4aec5b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_location_searching.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_more.png b/WordPress/src/main/res/drawable-hdpi/ic_action_more.png
new file mode 100644
index 000000000..5287bb92a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_more.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_more_grey.png b/WordPress/src/main/res/drawable-hdpi/ic_action_more_grey.png
new file mode 100644
index 000000000..5c923d1fe
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_more_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_reply.png b/WordPress/src/main/res/drawable-hdpi/ic_action_reply.png
new file mode 100644
index 000000000..d14c71fdb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_reply.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_restore.png b/WordPress/src/main/res/drawable-hdpi/ic_action_restore.png
new file mode 100644
index 000000000..f5d063040
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_restore.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_spam.png b/WordPress/src/main/res/drawable-hdpi/ic_action_spam.png
new file mode 100644
index 000000000..afc42f54b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_spam.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_trash.png b/WordPress/src/main/res/drawable-hdpi/ic_action_trash.png
new file mode 100644
index 000000000..0be1e2e45
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_trash.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_action_video.png b/WordPress/src/main/res/drawable-hdpi/ic_action_video.png
new file mode 100644
index 000000000..0c90ff41a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_action_video.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_add_blue_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_add_blue_24dp.png
new file mode 100644
index 000000000..c67c63101
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_add_blue_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_add_grey600_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_add_grey600_24dp.png
new file mode 100644
index 000000000..492c8f880
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_add_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_add_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_add_white_24dp.png
new file mode 100644
index 000000000..481643ecd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_add_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png
new file mode 100644
index 000000000..5fa494878
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_arrow_up_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_arrow_up_white_24dp.png
new file mode 100644
index 000000000..a9dda3694
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_arrow_up_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_bell_grey.png b/WordPress/src/main/res/drawable-hdpi/ic_bell_grey.png
new file mode 100644
index 000000000..cee7c7399
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_bell_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_check_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_check_white_24dp.png
new file mode 100644
index 000000000..f42a0e2d2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_check_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..325803180
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..0fd15563a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_comment.png b/WordPress/src/main/res/drawable-hdpi/ic_comment.png
new file mode 100644
index 000000000..eca4af4e4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_comment_active.png b/WordPress/src/main/res/drawable-hdpi/ic_comment_active.png
new file mode 100644
index 000000000..b97464901
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_comment_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_comment_disabled.png b/WordPress/src/main/res/drawable-hdpi/ic_comment_disabled.png
new file mode 100644
index 000000000..ca2585ff0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_comment_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.png
new file mode 100644
index 000000000..03b1aac4e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_content_copy_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_create_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_create_white_24dp.png
new file mode 100644
index 000000000..730416c96
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_create_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_delete_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_delete_white_24dp.png
new file mode 100644
index 000000000..a9eac0ca7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_delete_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_email_grey.png b/WordPress/src/main/res/drawable-hdpi/ic_email_grey.png
new file mode 100644
index 000000000..f7b169692
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_email_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_grey_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_grey_24dp.png
new file mode 100644
index 000000000..0323ce67b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_grey_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_white_24dp.png
new file mode 100644
index 000000000..986df32ed
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_genericon_tag_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_genericon_web_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_genericon_web_white_24dp.png
new file mode 100644
index 000000000..fde4bafcd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_genericon_web_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_level_indicator.png b/WordPress/src/main/res/drawable-hdpi/ic_level_indicator.png
new file mode 100644
index 000000000..f83842120
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_level_indicator.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_like.png b/WordPress/src/main/res/drawable-hdpi/ic_like.png
new file mode 100644
index 000000000..836d7e7fe
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_like_active.png b/WordPress/src/main/res/drawable-hdpi/ic_like_active.png
new file mode 100644
index 000000000..57a3c4c3b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_like_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_like_disabled.png b/WordPress/src/main/res/drawable-hdpi/ic_like_disabled.png
new file mode 100644
index 000000000..3edd9fd65
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_like_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_location_on_grey600_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_location_on_grey600_24dp.png
new file mode 100644
index 000000000..cde6394c4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_location_on_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_phone_grey.png b/WordPress/src/main/res/drawable-hdpi/ic_phone_grey.png
new file mode 100644
index 000000000..0d45603a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_phone_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_post_settings.png b/WordPress/src/main/res/drawable-hdpi/ic_post_settings.png
new file mode 100644
index 000000000..4e4959dcd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_post_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png
new file mode 100644
index 000000000..cd16fdd50
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_remove_red_eye_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_remove_red_eye_white_24dp.png
new file mode 100644
index 000000000..ef2023b52
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_remove_red_eye_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_reply_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_reply_white_24dp.png
new file mode 100644
index 000000000..fcf2096dd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_reply_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_report_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_report_white_24dp.png
new file mode 100644
index 000000000..3e1ea6678
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_report_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_save_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_save_white_24dp.png
new file mode 100644
index 000000000..8c9e9cec0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_save_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_search.png b/WordPress/src/main/res/drawable-hdpi/ic_search.png
new file mode 100644
index 000000000..70ade9daf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_search.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_search_white_24dp.png
new file mode 100644
index 000000000..a2fc5b2e7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_search_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png
new file mode 100644
index 000000000..b771392ed
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_send_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_send_white_24dp.png
new file mode 100644
index 000000000..f7753d442
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_send_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_settings_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_settings_white_24dp.png
new file mode 100644
index 000000000..f2918e601
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_settings_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_share_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_share_white_24dp.png
new file mode 100644
index 000000000..93b3c219c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_me_normal.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_me_normal.png
new file mode 100644
index 000000000..7a602dae6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_me_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_me_pressed.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_me_pressed.png
new file mode 100644
index 000000000..2675eaa3f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_me_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_normal.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_normal.png
new file mode 100644
index 000000000..de6be6546
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_pressed.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_pressed.png
new file mode 100644
index 000000000..47f836765
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_notifications_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_reader_normal.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_reader_normal.png
new file mode 100644
index 000000000..c06571123
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_reader_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_reader_pressed.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_reader_pressed.png
new file mode 100644
index 000000000..0c228e568
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_reader_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_sites_normal.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_sites_normal.png
new file mode 100644
index 000000000..0f342c0c4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_sites_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_tab_sites_pressed.png b/WordPress/src/main/res/drawable-hdpi/ic_tab_sites_pressed.png
new file mode 100644
index 000000000..d67054cee
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_tab_sites_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_theme_customize.png b/WordPress/src/main/res/drawable-hdpi/ic_theme_customize.png
new file mode 100644
index 000000000..cae4dca93
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_theme_customize.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_theme_details.png b/WordPress/src/main/res/drawable-hdpi/ic_theme_details.png
new file mode 100644
index 000000000..eeb49a71f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_theme_details.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_theme_loading.png b/WordPress/src/main/res/drawable-hdpi/ic_theme_loading.png
new file mode 100644
index 000000000..92dee05cb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_theme_loading.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_theme_support.png b/WordPress/src/main/res/drawable-hdpi/ic_theme_support.png
new file mode 100644
index 000000000..ad3dc0ae1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_theme_support.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.png
new file mode 100644
index 000000000..c264b3e73
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.png b/WordPress/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.png
new file mode 100644
index 000000000..e8901cd95
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/list_focused_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/list_focused_wordpress.9.png
new file mode 100644
index 000000000..130a206d3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/list_focused_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/me_icon_account_settings.png b/WordPress/src/main/res/drawable-hdpi/me_icon_account_settings.png
new file mode 100644
index 000000000..f5285b4f5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/me_icon_account_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/me_icon_app_settings.png b/WordPress/src/main/res/drawable-hdpi/me_icon_app_settings.png
new file mode 100644
index 000000000..79582c7ca
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/me_icon_app_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/me_icon_login_logout.png b/WordPress/src/main/res/drawable-hdpi/me_icon_login_logout.png
new file mode 100644
index 000000000..a36bba233
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/me_icon_login_logout.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/me_icon_my_profile.png b/WordPress/src/main/res/drawable-hdpi/me_icon_my_profile.png
new file mode 100644
index 000000000..c4435d8de
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/me_icon_my_profile.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/me_icon_notifications.png b/WordPress/src/main/res/drawable-hdpi/me_icon_notifications.png
new file mode 100644
index 000000000..9d618bc52
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/me_icon_notifications.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/me_icon_support.png b/WordPress/src/main/res/drawable-hdpi/me_icon_support.png
new file mode 100644
index 000000000..7561db04d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/me_icon_support.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/media_audio.png b/WordPress/src/main/res/drawable-hdpi/media_audio.png
new file mode 100644
index 000000000..9a344ce47
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/media_audio.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/media_document.png b/WordPress/src/main/res/drawable-hdpi/media_document.png
new file mode 100644
index 000000000..c5039ad7d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/media_document.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/media_image_placeholder.png b/WordPress/src/main/res/drawable-hdpi/media_image_placeholder.png
new file mode 100644
index 000000000..22e50f20d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/media_image_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/media_powerpoint.png b/WordPress/src/main/res/drawable-hdpi/media_powerpoint.png
new file mode 100644
index 000000000..86407e28b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/media_powerpoint.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/media_spreadsheet.png b/WordPress/src/main/res/drawable-hdpi/media_spreadsheet.png
new file mode 100644
index 000000000..898ba89a0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/media_spreadsheet.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/menu_dropdown_panel_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/menu_dropdown_panel_wordpress.9.png
new file mode 100644
index 000000000..92bf2b3c0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/menu_dropdown_panel_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_comments.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_comments.png
new file mode 100644
index 000000000..3ce6092b6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_comments.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_media.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_media.png
new file mode 100644
index 000000000..701dbc4ff
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_media.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_pages.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_pages.png
new file mode 100644
index 000000000..1ea54e813
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_pages.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_posts.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_posts.png
new file mode 100644
index 000000000..7e4f1cb6e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_posts.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_settings.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_settings.png
new file mode 100644
index 000000000..3359c8f26
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_stats.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_stats.png
new file mode 100644
index 000000000..1e7cf658b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_stats.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_themes.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_themes.png
new file mode 100644
index 000000000..fe1057f85
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_themes.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_view_admin.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_view_admin.png
new file mode 100644
index 000000000..5619b4db8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_view_admin.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_icon_view_site.png b/WordPress/src/main/res/drawable-hdpi/my_site_icon_view_site.png
new file mode 100644
index 000000000..fdba0f7ef
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_icon_view_site.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/my_site_no_sites_drake.png b/WordPress/src/main/res/drawable-hdpi/my_site_no_sites_drake.png
new file mode 100644
index 000000000..9b2a0da8d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/my_site_no_sites_drake.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/note_icon_reply.png b/WordPress/src/main/res/drawable-hdpi/note_icon_reply.png
new file mode 100644
index 000000000..67c1cff84
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/note_icon_reply.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_alert_big.png b/WordPress/src/main/res/drawable-hdpi/noticon_alert_big.png
new file mode 100644
index 000000000..770e483f1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_alert_big.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_back.png b/WordPress/src/main/res/drawable-hdpi/noticon_back.png
new file mode 100644
index 000000000..4323fb862
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_back.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_clock.png b/WordPress/src/main/res/drawable-hdpi/noticon_clock.png
new file mode 100644
index 000000000..f09acf857
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_clock.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_edit.png b/WordPress/src/main/res/drawable-hdpi/noticon_edit.png
new file mode 100644
index 000000000..084f7ac40
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_edit.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_more.png b/WordPress/src/main/res/drawable-hdpi/noticon_more.png
new file mode 100644
index 000000000..bac872a34
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_more.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_publish.png b/WordPress/src/main/res/drawable-hdpi/noticon_publish.png
new file mode 100644
index 000000000..bfeffc4b8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_publish.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_restore.png b/WordPress/src/main/res/drawable-hdpi/noticon_restore.png
new file mode 100644
index 000000000..10ed74737
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_restore.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_scheduled.png b/WordPress/src/main/res/drawable-hdpi/noticon_scheduled.png
new file mode 100644
index 000000000..94c08bdc4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_scheduled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_star_active.png b/WordPress/src/main/res/drawable-hdpi/noticon_star_active.png
new file mode 100644
index 000000000..416d7fe80
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_star_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_star_disabled.png b/WordPress/src/main/res/drawable-hdpi/noticon_star_disabled.png
new file mode 100644
index 000000000..b8542fd8a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_star_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_star_unactive.png b/WordPress/src/main/res/drawable-hdpi/noticon_star_unactive.png
new file mode 100644
index 000000000..9497228cb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_star_unactive.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_stats.png b/WordPress/src/main/res/drawable-hdpi/noticon_stats.png
new file mode 100644
index 000000000..ad426ed51
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_stats.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_sticky.png b/WordPress/src/main/res/drawable-hdpi/noticon_sticky.png
new file mode 100644
index 000000000..750ca0abe
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_sticky.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_trash.png b/WordPress/src/main/res/drawable-hdpi/noticon_trash.png
new file mode 100644
index 000000000..68e3c9407
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_trash.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_trashed.png b/WordPress/src/main/res/drawable-hdpi/noticon_trashed.png
new file mode 100644
index 000000000..cc3428242
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_trashed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_view.png b/WordPress/src/main/res/drawable-hdpi/noticon_view.png
new file mode 100644
index 000000000..3b2157941
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_view.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/noticon_warning_big_grey.png b/WordPress/src/main/res/drawable-hdpi/noticon_warning_big_grey.png
new file mode 100644
index 000000000..89482da30
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/noticon_warning_big_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/notification_icon.png b/WordPress/src/main/res/drawable-hdpi/notification_icon.png
new file mode 100644
index 000000000..4e77b5c19
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/notification_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/nux_icon_wp.png b/WordPress/src/main/res/drawable-hdpi/nux_icon_wp.png
new file mode 100644
index 000000000..ffdfbcc7d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/nux_icon_wp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/penandink.png b/WordPress/src/main/res/drawable-hdpi/penandink.png
new file mode 100644
index 000000000..882792098
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/penandink.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/progress_bg_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/progress_bg_wordpress.9.png
new file mode 100644
index 000000000..3d5c707d5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/progress_bg_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/progress_primary_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/progress_primary_wordpress.9.png
new file mode 100644
index 000000000..224681eca
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/progress_primary_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/progress_secondary_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/progress_secondary_wordpress.9.png
new file mode 100644
index 000000000..72663c735
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/progress_secondary_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_comment.png b/WordPress/src/main/res/drawable-hdpi/reader_comment.png
new file mode 100644
index 000000000..b7640d2e0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_comment_active.png b/WordPress/src/main/res/drawable-hdpi/reader_comment_active.png
new file mode 100644
index 000000000..f39ff7254
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_comment_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_dropdown_arrow.png b/WordPress/src/main/res/drawable-hdpi/reader_dropdown_arrow.png
new file mode 100644
index 000000000..87616b264
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_dropdown_arrow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_like.png b/WordPress/src/main/res/drawable-hdpi/reader_like.png
new file mode 100644
index 000000000..08adf30f0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_like_empty.png b/WordPress/src/main/res/drawable-hdpi/reader_like_empty.png
new file mode 100644
index 000000000..97445e4c0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_like_empty.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_like_empty_active.png b/WordPress/src/main/res/drawable-hdpi/reader_like_empty_active.png
new file mode 100644
index 000000000..a074e8db4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_like_empty_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/reader_tear.png b/WordPress/src/main/res/drawable-hdpi/reader_tear.png
new file mode 100644
index 000000000..298f53693
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/reader_tear.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/right_shadow.9.png b/WordPress/src/main/res/drawable-hdpi/right_shadow.9.png
new file mode 100644
index 000000000..46b382826
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/right_shadow.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/spinner_ab_default_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/spinner_ab_default_wordpress.9.png
new file mode 100644
index 000000000..4fd4aeba0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/spinner_ab_default_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/spinner_ab_disabled_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/spinner_ab_disabled_wordpress.9.png
new file mode 100644
index 000000000..d42c97b85
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/spinner_ab_disabled_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/spinner_ab_focused_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/spinner_ab_focused_wordpress.9.png
new file mode 100644
index 000000000..4020890e2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/spinner_ab_focused_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/spinner_ab_pressed_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/spinner_ab_pressed_wordpress.9.png
new file mode 100644
index 000000000..1f3dc9bd8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/spinner_ab_pressed_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_chevron_down.png b/WordPress/src/main/res/drawable-hdpi/stats_chevron_down.png
new file mode 100644
index 000000000..c7160d85d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_chevron_down.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_chevron_right.png b/WordPress/src/main/res/drawable-hdpi/stats_chevron_right.png
new file mode 100644
index 000000000..5e88dc1b8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_chevron_right.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_categories.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_categories.png
new file mode 100644
index 000000000..d60868fab
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_categories.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_comments.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_comments.png
new file mode 100644
index 000000000..cd673f03a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_comments.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_comments_active.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_comments_active.png
new file mode 100644
index 000000000..71aab0736
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_comments_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_default_site_avatar.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_default_site_avatar.png
new file mode 100644
index 000000000..d43662b9e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_default_site_avatar.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_info.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_info.png
new file mode 100644
index 000000000..7a55880e5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_info.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_likes.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_likes.png
new file mode 100644
index 000000000..e347a53e6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_likes.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_tags.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_tags.png
new file mode 100644
index 000000000..48a1a7127
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_tags.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_trophy.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_trophy.png
new file mode 100644
index 000000000..4b46e2477
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_trophy.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_views.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_views.png
new file mode 100644
index 000000000..9bcd89cf1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_views.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_icon_visitors.png b/WordPress/src/main/res/drawable-hdpi/stats_icon_visitors.png
new file mode 100644
index 000000000..21963f284
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_icon_visitors.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/stats_link.png b/WordPress/src/main/res/drawable-hdpi/stats_link.png
new file mode 100644
index 000000000..03305f820
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/stats_link.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/switch_site_button_icon.png b/WordPress/src/main/res/drawable-hdpi/switch_site_button_icon.png
new file mode 100644
index 000000000..7d84326d3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/switch_site_button_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/tab_unselected_pressed_wordpress.9.png b/WordPress/src/main/res/drawable-hdpi/tab_unselected_pressed_wordpress.9.png
new file mode 100644
index 000000000..57be89a5d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/tab_unselected_pressed_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/theme_icon_current.png b/WordPress/src/main/res/drawable-hdpi/theme_icon_current.png
new file mode 100644
index 000000000..b6be9a061
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/theme_icon_current.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/theme_icon_premium.png b/WordPress/src/main/res/drawable-hdpi/theme_icon_premium.png
new file mode 100644
index 000000000..08ec6bed9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/theme_icon_premium.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/theme_icon_tag_current.png b/WordPress/src/main/res/drawable-hdpi/theme_icon_tag_current.png
new file mode 100644
index 000000000..1f5156561
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/theme_icon_tag_current.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/theme_icon_tag_premium.png b/WordPress/src/main/res/drawable-hdpi/theme_icon_tag_premium.png
new file mode 100644
index 000000000..b13fb182a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/theme_icon_tag_premium.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/theme_loading_background.png b/WordPress/src/main/res/drawable-hdpi/theme_loading_background.png
new file mode 100644
index 000000000..c9112b6a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/theme_loading_background.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-hdpi/video_thumbnail.png b/WordPress/src/main/res/drawable-hdpi/video_thumbnail.png
new file mode 100644
index 000000000..01441cc48
--- /dev/null
+++ b/WordPress/src/main/res/drawable-hdpi/video_thumbnail.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-nodpi/stats_widget_preview.png b/WordPress/src/main/res/drawable-nodpi/stats_widget_preview.png
new file mode 100644
index 000000000..e8162435d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-nodpi/stats_widget_preview.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml b/WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml
new file mode 100644
index 000000000..3ba16d9a2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@drawable/ripple_oval" />
+
+</selector>
diff --git a/WordPress/src/main/res/drawable-v21/nux_flat_button_selector.xml b/WordPress/src/main/res/drawable-v21/nux_flat_button_selector.xml
new file mode 100644
index 000000000..ebbed3864
--- /dev/null
+++ b/WordPress/src/main/res/drawable-v21/nux_flat_button_selector.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/color_control_activated">
+ <item>
+ <shape>
+ <solid android:color="@color/blue_wordpress"/>
+ <corners android:radius="2dp"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/WordPress/src/main/res/drawable-v21/nux_primary_button_selector.xml b/WordPress/src/main/res/drawable-v21/nux_primary_button_selector.xml
new file mode 100644
index 000000000..669b1f671
--- /dev/null
+++ b/WordPress/src/main/res/drawable-v21/nux_primary_button_selector.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/blue_dark">
+ <item>
+ <shape>
+ <solid android:color="@color/blue_medium"/>
+ <corners android:radius="2dp"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/WordPress/src/main/res/drawable-v21/ripple_oval.xml b/WordPress/src/main/res/drawable-v21/ripple_oval.xml
new file mode 100644
index 000000000..e1bb360a0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-v21/ripple_oval.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="oval">
+ <solid android:color="@color/blue_medium" />
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable-v21/stats_top_pager_button_selector.xml b/WordPress/src/main/res/drawable-v21/stats_top_pager_button_selector.xml
new file mode 100644
index 000000000..2b115be56
--- /dev/null
+++ b/WordPress/src/main/res/drawable-v21/stats_top_pager_button_selector.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <selector>
+ <item android:drawable="@drawable/stats_pager_button_blue_light" android:state_checked="true" />
+ <item android:drawable="@drawable/stats_pager_button_white" />
+ </selector>
+ </item>
+</ripple>
diff --git a/WordPress/src/main/res/drawable-xhdpi-v4/action_mode_confirm_checkmark.png b/WordPress/src/main/res/drawable-xhdpi-v4/action_mode_confirm_checkmark.png
new file mode 100644
index 000000000..be2ecaed5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi-v4/action_mode_confirm_checkmark.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi-v4/ic_media_play.png b/WordPress/src/main/res/drawable-xhdpi-v4/ic_media_play.png
new file mode 100644
index 000000000..ccfef1805
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi-v4/ic_media_play.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi-v4/tab_icon_create_gallery.png b/WordPress/src/main/res/drawable-xhdpi-v4/tab_icon_create_gallery.png
new file mode 100644
index 000000000..293ba5b27
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi-v4/tab_icon_create_gallery.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/arrow.png b/WordPress/src/main/res/drawable-xhdpi/arrow.png
new file mode 100644
index 000000000..d86d20555
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/arrow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/blavatar_placeholder.png b/WordPress/src/main/res/drawable-xhdpi/blavatar_placeholder.png
new file mode 100644
index 000000000..7f5f48d2b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/blavatar_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/box_with_pages_bottom.png b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_bottom.png
new file mode 100644
index 000000000..ab2c97d2b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_bottom.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page1.png b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page1.png
new file mode 100644
index 000000000..5265f4e80
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page1.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page2.png b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page2.png
new file mode 100644
index 000000000..63158a2b5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page2.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page3.png b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page3.png
new file mode 100644
index 000000000..384aec0b6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_page3.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/box_with_pages_top.png b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_top.png
new file mode 100644
index 000000000..93809947e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/box_with_pages_top.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_default_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_default_wordpress.9.png
new file mode 100644
index 000000000..3521ae38d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_default_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_focused_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_focused_wordpress.9.png
new file mode 100644
index 000000000..039be9b69
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_focused_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_pressed_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_pressed_wordpress.9.png
new file mode 100644
index 000000000..e560fee98
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/btn_cab_done_pressed_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/button_blue.9.png b/WordPress/src/main/res/drawable-xhdpi/button_blue.9.png
new file mode 100644
index 000000000..83d0dc329
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/button_blue.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/button_blue_disabled.9.png b/WordPress/src/main/res/drawable-xhdpi/button_blue_disabled.9.png
new file mode 100644
index 000000000..aef152b50
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/button_blue_disabled.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/button_blue_focus.9.png b/WordPress/src/main/res/drawable-xhdpi/button_blue_focus.9.png
new file mode 100644
index 000000000..d1d041761
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/button_blue_focus.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/button_darkgray.9.png b/WordPress/src/main/res/drawable-xhdpi/button_darkgray.9.png
new file mode 100644
index 000000000..7893254a3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/button_darkgray.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/button_darkgray_disabled.9.png b/WordPress/src/main/res/drawable-xhdpi/button_darkgray_disabled.9.png
new file mode 100644
index 000000000..6c21d5714
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/button_darkgray_disabled.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/button_darkgray_focus.9.png b/WordPress/src/main/res/drawable-xhdpi/button_darkgray_focus.9.png
new file mode 100644
index 000000000..b5fb43402
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/button_darkgray_focus.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_admin_site.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_admin_site.png
new file mode 100644
index 000000000..b8a08fdc8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_admin_site.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_admin_users.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_admin_users.png
new file mode 100644
index 000000000..5bd6f30e0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_admin_users.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_edit.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_edit.png
new file mode 100644
index 000000000..51d99a06e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_edit.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_email.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_email.png
new file mode 100644
index 000000000..1f544e6f0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_email.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_eye_closed.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_eye_closed.png
new file mode 100644
index 000000000..18144f79a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_eye_closed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_eye_open.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_eye_open.png
new file mode 100644
index 000000000..865c2ea13
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_eye_open.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_info.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_info.png
new file mode 100644
index 000000000..64783cb84
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_info.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_lock.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_lock.png
new file mode 100644
index 000000000..5026c8eb0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_lock.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/dashicon_wordpress_alt.png b/WordPress/src/main/res/drawable-xhdpi/dashicon_wordpress_alt.png
new file mode 100644
index 000000000..21bf32f37
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/dashicon_wordpress_alt.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/drake_empty_results.png b/WordPress/src/main/res/drawable-xhdpi/drake_empty_results.png
new file mode 100644
index 000000000..96dd7ee9e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/drake_empty_results.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/endlist_logo.png b/WordPress/src/main/res/drawable-xhdpi/endlist_logo.png
new file mode 100644
index 000000000..31cfb1fa6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/endlist_logo.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_closed.png b/WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_closed.png
new file mode 100644
index 000000000..c24301226
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_closed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_open.png b/WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_open.png
new file mode 100644
index 000000000..13c5d34c6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_arrow_dropdown_open.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_checkbox_empty.png b/WordPress/src/main/res/drawable-xhdpi/gallery_checkbox_empty.png
new file mode 100644
index 000000000..aa9edd6c2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_checkbox_empty.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles.png
new file mode 100644
index 000000000..8d71b61e5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles_selected.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles_selected.png
new file mode 100644
index 000000000..233b7cd9a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_circles_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow.png
new file mode 100644
index 000000000..eed4402e8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow_selected.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow_selected.png
new file mode 100644
index 000000000..6e4e3f406
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_slideshow_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares.png
new file mode 100644
index 000000000..b48bc7d6b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares_selected.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares_selected.png
new file mode 100644
index 000000000..6c799b996
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_squares_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid.png
new file mode 100644
index 000000000..a1400c781
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid_selected.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid_selected.png
new file mode 100644
index 000000000..39b0a1215
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_thumbnailgrid_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled.png
new file mode 100644
index 000000000..e96d38519
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled_selected.png b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled_selected.png
new file mode 100644
index 000000000..ffdb6f433
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_icon_tiled_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gallery_tablet_move_file.png b/WordPress/src/main/res/drawable-xhdpi/gallery_tablet_move_file.png
new file mode 100644
index 000000000..cf0adaae4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gallery_tablet_move_file.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gravatar_placeholder.png b/WordPress/src/main/res/drawable-xhdpi/gravatar_placeholder.png
new file mode 100644
index 000000000..223cd74c8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gravatar_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gridicon_create_dark.png b/WordPress/src/main/res/drawable-xhdpi/gridicon_create_dark.png
new file mode 100644
index 000000000..c0a621916
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gridicon_create_dark.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/gridicon_create_light.png b/WordPress/src/main/res/drawable-xhdpi/gridicon_create_light.png
new file mode 100644
index 000000000..bcc496375
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/gridicon_create_light.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_approve.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_approve.png
new file mode 100644
index 000000000..9f6bbc148
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_approve.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_approve_active.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_approve_active.png
new file mode 100644
index 000000000..f9734d13a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_approve_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_camera.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_camera.png
new file mode 100644
index 000000000..7c5cfc5cf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_camera.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_comment.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_comment.png
new file mode 100644
index 000000000..b8029d86f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_like.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_like.png
new file mode 100644
index 000000000..eb97cfce5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_like_active.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_like_active.png
new file mode 100644
index 000000000..ff452e240
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_like_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_location_found.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_location_found.png
new file mode 100644
index 000000000..4c06441f9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_location_found.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_location_off.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_location_off.png
new file mode 100644
index 000000000..2964edc33
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_location_off.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_location_searching.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_location_searching.png
new file mode 100644
index 000000000..7a3d4984f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_location_searching.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_more.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_more.png
new file mode 100644
index 000000000..f294ab280
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_more.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_more_grey.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_more_grey.png
new file mode 100644
index 000000000..8c4807622
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_more_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_reply.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_reply.png
new file mode 100644
index 000000000..cb3d51ccf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_reply.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_restore.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_restore.png
new file mode 100644
index 000000000..c7e8dd700
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_restore.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_spam.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_spam.png
new file mode 100644
index 000000000..9d64a26b3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_spam.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_trash.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_trash.png
new file mode 100644
index 000000000..31325d129
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_trash.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_action_video.png b/WordPress/src/main/res/drawable-xhdpi/ic_action_video.png
new file mode 100644
index 000000000..0643ea55f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_action_video.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_add_blue_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_add_blue_24dp.png
new file mode 100644
index 000000000..7986bfa34
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_add_blue_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.png
new file mode 100644
index 000000000..0d8af34b9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_add_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
new file mode 100644
index 000000000..67042105d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png
new file mode 100644
index 000000000..addbfc886
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_arrow_up_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_arrow_up_white_24dp.png
new file mode 100644
index 000000000..f00b11bb4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_arrow_up_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_bell_grey.png b/WordPress/src/main/res/drawable-xhdpi/ic_bell_grey.png
new file mode 100644
index 000000000..e0f47d7b1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_bell_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_check_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_check_white_24dp.png
new file mode 100644
index 000000000..e5024472a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_check_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..fb9f88d2a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..76e07f097
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_comment.png b/WordPress/src/main/res/drawable-xhdpi/ic_comment.png
new file mode 100644
index 000000000..0e31ded7e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_comment_active.png b/WordPress/src/main/res/drawable-xhdpi/ic_comment_active.png
new file mode 100644
index 000000000..2167685e4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_comment_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_comment_disabled.png b/WordPress/src/main/res/drawable-xhdpi/ic_comment_disabled.png
new file mode 100644
index 000000000..b61b9c74c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_comment_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.png
new file mode 100644
index 000000000..04a0cc94b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_create_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_create_white_24dp.png
new file mode 100644
index 000000000..7f0ea51bf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_create_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png
new file mode 100644
index 000000000..cdb230c2f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_email_grey.png b/WordPress/src/main/res/drawable-xhdpi/ic_email_grey.png
new file mode 100644
index 000000000..2fb7ca4d2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_email_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_grey_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_grey_24dp.png
new file mode 100644
index 000000000..6c11046e7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_grey_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_white_24dp.png
new file mode 100644
index 000000000..2141d84cc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_genericon_tag_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_genericon_web_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_genericon_web_white_24dp.png
new file mode 100644
index 000000000..c4890bd79
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_genericon_web_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_level_indicator.png b/WordPress/src/main/res/drawable-xhdpi/ic_level_indicator.png
new file mode 100644
index 000000000..08ebe5225
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_level_indicator.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_like.png b/WordPress/src/main/res/drawable-xhdpi/ic_like.png
new file mode 100644
index 000000000..888bd4964
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_like_active.png b/WordPress/src/main/res/drawable-xhdpi/ic_like_active.png
new file mode 100644
index 000000000..b47db0b35
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_like_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_like_disabled.png b/WordPress/src/main/res/drawable-xhdpi/ic_like_disabled.png
new file mode 100644
index 000000000..62b91ad0d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_like_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_location_on_grey600_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_location_on_grey600_24dp.png
new file mode 100644
index 000000000..534b89ead
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_location_on_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_phone_grey.png b/WordPress/src/main/res/drawable-xhdpi/ic_phone_grey.png
new file mode 100644
index 000000000..da85d4a8c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_phone_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_post_settings.png b/WordPress/src/main/res/drawable-xhdpi/ic_post_settings.png
new file mode 100644
index 000000000..3e2020016
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_post_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png
new file mode 100644
index 000000000..5f89fc257
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_24dp.png
new file mode 100644
index 000000000..d3cd8a7c2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_reply_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_reply_white_24dp.png
new file mode 100644
index 000000000..8e661457a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_reply_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_report_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_report_white_24dp.png
new file mode 100644
index 000000000..2592923d0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_report_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_save_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_save_white_24dp.png
new file mode 100644
index 000000000..aa0332092
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_save_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_search.png b/WordPress/src/main/res/drawable-xhdpi/ic_search.png
new file mode 100644
index 000000000..f2fa80b21
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_search.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_search_white_24dp.png
new file mode 100644
index 000000000..043759acd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_search_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png
new file mode 100644
index 000000000..e13804622
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_send_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_send_white_24dp.png
new file mode 100644
index 000000000..6e0931320
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_send_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png
new file mode 100644
index 000000000..2482f308f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_share_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_share_white_24dp.png
new file mode 100644
index 000000000..dd536bca2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_me_normal.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_me_normal.png
new file mode 100644
index 000000000..8c31f4369
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_me_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_me_pressed.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_me_pressed.png
new file mode 100644
index 000000000..cb09353b5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_me_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_normal.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_normal.png
new file mode 100644
index 000000000..5839a0011
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_pressed.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_pressed.png
new file mode 100644
index 000000000..b5df9c9e6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_notifications_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_normal.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_normal.png
new file mode 100644
index 000000000..c55a85697
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_pressed.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_pressed.png
new file mode 100644
index 000000000..054eb9c7b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_reader_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_normal.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_normal.png
new file mode 100644
index 000000000..1f4e43bf3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_pressed.png b/WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_pressed.png
new file mode 100644
index 000000000..d5a0d5db6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_tab_sites_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_theme_customize.png b/WordPress/src/main/res/drawable-xhdpi/ic_theme_customize.png
new file mode 100644
index 000000000..5340027dd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_theme_customize.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_theme_details.png b/WordPress/src/main/res/drawable-xhdpi/ic_theme_details.png
new file mode 100644
index 000000000..00f3a5009
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_theme_details.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_theme_loading.png b/WordPress/src/main/res/drawable-xhdpi/ic_theme_loading.png
new file mode 100644
index 000000000..3214a397d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_theme_loading.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_theme_support.png b/WordPress/src/main/res/drawable-xhdpi/ic_theme_support.png
new file mode 100644
index 000000000..489214e63
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_theme_support.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.png
new file mode 100644
index 000000000..c8aca5f5f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.png
new file mode 100644
index 000000000..7e28ae3cd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png
new file mode 100644
index 000000000..3dbd3937d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/me_icon_account_settings.png b/WordPress/src/main/res/drawable-xhdpi/me_icon_account_settings.png
new file mode 100644
index 000000000..0f1fbd5a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/me_icon_account_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/me_icon_app_settings.png b/WordPress/src/main/res/drawable-xhdpi/me_icon_app_settings.png
new file mode 100644
index 000000000..01436f1eb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/me_icon_app_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/me_icon_login_logout.png b/WordPress/src/main/res/drawable-xhdpi/me_icon_login_logout.png
new file mode 100644
index 000000000..4373360d7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/me_icon_login_logout.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/me_icon_my_profile.png b/WordPress/src/main/res/drawable-xhdpi/me_icon_my_profile.png
new file mode 100644
index 000000000..e568d0df6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/me_icon_my_profile.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/me_icon_notifications.png b/WordPress/src/main/res/drawable-xhdpi/me_icon_notifications.png
new file mode 100644
index 000000000..84e9a924f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/me_icon_notifications.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/me_icon_support.png b/WordPress/src/main/res/drawable-xhdpi/me_icon_support.png
new file mode 100644
index 000000000..3bb07bd24
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/me_icon_support.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/media_audio.png b/WordPress/src/main/res/drawable-xhdpi/media_audio.png
new file mode 100644
index 000000000..d7abe6df5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/media_audio.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/media_document.png b/WordPress/src/main/res/drawable-xhdpi/media_document.png
new file mode 100644
index 000000000..25e8e86cc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/media_document.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/media_image_placeholder.png b/WordPress/src/main/res/drawable-xhdpi/media_image_placeholder.png
new file mode 100644
index 000000000..9510a9e5d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/media_image_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/media_item_placeholder.xml b/WordPress/src/main/res/drawable-xhdpi/media_item_placeholder.xml
new file mode 100644
index 000000000..8461540ef
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/media_item_placeholder.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@color/grey_lighten_10" />
+
+</selector>
diff --git a/WordPress/src/main/res/drawable-xhdpi/media_powerpoint.png b/WordPress/src/main/res/drawable-xhdpi/media_powerpoint.png
new file mode 100644
index 000000000..a70b07951
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/media_powerpoint.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/media_spreadsheet.png b/WordPress/src/main/res/drawable-xhdpi/media_spreadsheet.png
new file mode 100644
index 000000000..621031f3f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/media_spreadsheet.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/menu_dropdown_panel_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/menu_dropdown_panel_wordpress.9.png
new file mode 100644
index 000000000..b0c87cece
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/menu_dropdown_panel_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_comments.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_comments.png
new file mode 100644
index 000000000..0831e42f9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_comments.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_media.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_media.png
new file mode 100644
index 000000000..dc4b01b32
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_media.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_pages.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_pages.png
new file mode 100644
index 000000000..01609d776
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_pages.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_posts.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_posts.png
new file mode 100644
index 000000000..74e5577ca
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_posts.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_settings.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_settings.png
new file mode 100644
index 000000000..a6e4cf723
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_stats.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_stats.png
new file mode 100644
index 000000000..d8ffe31aa
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_stats.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_themes.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_themes.png
new file mode 100644
index 000000000..a40ceb179
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_themes.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_admin.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_admin.png
new file mode 100644
index 000000000..b4ff14e22
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_admin.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_site.png b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_site.png
new file mode 100644
index 000000000..d3db6c75d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_icon_view_site.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/my_site_no_sites_drake.png b/WordPress/src/main/res/drawable-xhdpi/my_site_no_sites_drake.png
new file mode 100644
index 000000000..58701b587
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/my_site_no_sites_drake.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/note_icon_reply.png b/WordPress/src/main/res/drawable-xhdpi/note_icon_reply.png
new file mode 100644
index 000000000..7889a1a0f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/note_icon_reply.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_alert_big.png b/WordPress/src/main/res/drawable-xhdpi/noticon_alert_big.png
new file mode 100644
index 000000000..fca4089aa
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_alert_big.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_back.png b/WordPress/src/main/res/drawable-xhdpi/noticon_back.png
new file mode 100644
index 000000000..a15464ac5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_back.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_clock.png b/WordPress/src/main/res/drawable-xhdpi/noticon_clock.png
new file mode 100644
index 000000000..83fb56533
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_clock.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_edit.png b/WordPress/src/main/res/drawable-xhdpi/noticon_edit.png
new file mode 100644
index 000000000..c292e4ae5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_edit.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_more.png b/WordPress/src/main/res/drawable-xhdpi/noticon_more.png
new file mode 100644
index 000000000..1b36536ae
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_more.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_publish.png b/WordPress/src/main/res/drawable-xhdpi/noticon_publish.png
new file mode 100644
index 000000000..7d985de7a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_publish.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_restore.png b/WordPress/src/main/res/drawable-xhdpi/noticon_restore.png
new file mode 100644
index 000000000..b2369506b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_restore.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_scheduled.png b/WordPress/src/main/res/drawable-xhdpi/noticon_scheduled.png
new file mode 100644
index 000000000..4857b47ab
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_scheduled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_star_active.png b/WordPress/src/main/res/drawable-xhdpi/noticon_star_active.png
new file mode 100644
index 000000000..bda29117c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_star_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_star_disabled.png b/WordPress/src/main/res/drawable-xhdpi/noticon_star_disabled.png
new file mode 100644
index 000000000..b1cafedc6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_star_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_star_unactive.png b/WordPress/src/main/res/drawable-xhdpi/noticon_star_unactive.png
new file mode 100644
index 000000000..6ce558e06
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_star_unactive.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_stats.png b/WordPress/src/main/res/drawable-xhdpi/noticon_stats.png
new file mode 100644
index 000000000..9cef1854e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_stats.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_sticky.png b/WordPress/src/main/res/drawable-xhdpi/noticon_sticky.png
new file mode 100644
index 000000000..3216248e0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_sticky.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_trash.png b/WordPress/src/main/res/drawable-xhdpi/noticon_trash.png
new file mode 100644
index 000000000..5565da0c3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_trash.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_trashed.png b/WordPress/src/main/res/drawable-xhdpi/noticon_trashed.png
new file mode 100644
index 000000000..8411524af
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_trashed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_view.png b/WordPress/src/main/res/drawable-xhdpi/noticon_view.png
new file mode 100644
index 000000000..7293a345a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_view.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/noticon_warning_big_grey.png b/WordPress/src/main/res/drawable-xhdpi/noticon_warning_big_grey.png
new file mode 100644
index 000000000..9503dd930
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/noticon_warning_big_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/notification_icon.png b/WordPress/src/main/res/drawable-xhdpi/notification_icon.png
new file mode 100644
index 000000000..62f5d6a34
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/notification_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/nux_icon_wp.png b/WordPress/src/main/res/drawable-xhdpi/nux_icon_wp.png
new file mode 100644
index 000000000..2c6411a61
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/nux_icon_wp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/penandink.png b/WordPress/src/main/res/drawable-xhdpi/penandink.png
new file mode 100644
index 000000000..b2e5392b2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/penandink.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/progress_bg_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/progress_bg_wordpress.9.png
new file mode 100644
index 000000000..8b4853aa6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/progress_bg_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/progress_primary_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/progress_primary_wordpress.9.png
new file mode 100644
index 000000000..e4eac97d0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/progress_primary_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/progress_secondary_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/progress_secondary_wordpress.9.png
new file mode 100644
index 000000000..82b998233
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/progress_secondary_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_comment.png b/WordPress/src/main/res/drawable-xhdpi/reader_comment.png
new file mode 100644
index 000000000..8a1ae7848
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_comment_active.png b/WordPress/src/main/res/drawable-xhdpi/reader_comment_active.png
new file mode 100644
index 000000000..414bedd43
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_comment_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_dropdown_arrow.png b/WordPress/src/main/res/drawable-xhdpi/reader_dropdown_arrow.png
new file mode 100644
index 000000000..8e522efea
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_dropdown_arrow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_like.png b/WordPress/src/main/res/drawable-xhdpi/reader_like.png
new file mode 100644
index 000000000..cbacca1b5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_like_empty.png b/WordPress/src/main/res/drawable-xhdpi/reader_like_empty.png
new file mode 100644
index 000000000..45d47d455
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_like_empty.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_like_empty_active.png b/WordPress/src/main/res/drawable-xhdpi/reader_like_empty_active.png
new file mode 100644
index 000000000..2cd279404
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_like_empty_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_tear.png b/WordPress/src/main/res/drawable-xhdpi/reader_tear.png
new file mode 100644
index 000000000..1077face5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/reader_tear.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/rppreview1.png b/WordPress/src/main/res/drawable-xhdpi/rppreview1.png
new file mode 100644
index 000000000..e2657c91b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/rppreview1.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/rppreview2.png b/WordPress/src/main/res/drawable-xhdpi/rppreview2.png
new file mode 100644
index 000000000..86bcd8ab5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/rppreview2.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/rppreview3.png b/WordPress/src/main/res/drawable-xhdpi/rppreview3.png
new file mode 100644
index 000000000..ed8e89813
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/rppreview3.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/spinner_ab_default_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_default_wordpress.9.png
new file mode 100644
index 000000000..14b1401de
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_default_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/spinner_ab_disabled_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_disabled_wordpress.9.png
new file mode 100644
index 000000000..c9dfbd605
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_disabled_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/spinner_ab_focused_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_focused_wordpress.9.png
new file mode 100644
index 000000000..91afcfa7e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_focused_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/spinner_ab_pressed_wordpress.9.png b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_pressed_wordpress.9.png
new file mode 100644
index 000000000..130fdce2c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/spinner_ab_pressed_wordpress.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_chevron_down.png b/WordPress/src/main/res/drawable-xhdpi/stats_chevron_down.png
new file mode 100644
index 000000000..712f3f38f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_chevron_down.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_chevron_right.png b/WordPress/src/main/res/drawable-xhdpi/stats_chevron_right.png
new file mode 100644
index 000000000..4e2f7bb7b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_chevron_right.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_categories.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_categories.png
new file mode 100644
index 000000000..42893e5fb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_categories.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_comments.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_comments.png
new file mode 100644
index 000000000..ac0f4b712
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_comments.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_comments_active.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_comments_active.png
new file mode 100644
index 000000000..ca8f07098
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_comments_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_default_site_avatar.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_default_site_avatar.png
new file mode 100644
index 000000000..b1f2beca4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_default_site_avatar.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_info.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_info.png
new file mode 100644
index 000000000..7e31a2cb5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_info.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_likes.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_likes.png
new file mode 100644
index 000000000..d97078fe9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_likes.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_tags.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_tags.png
new file mode 100644
index 000000000..fca742dcd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_tags.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_trophy.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_trophy.png
new file mode 100644
index 000000000..e4226852a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_trophy.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_views.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_views.png
new file mode 100644
index 000000000..b76a45ff4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_views.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_icon_visitors.png b/WordPress/src/main/res/drawable-xhdpi/stats_icon_visitors.png
new file mode 100644
index 000000000..2b1382842
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_icon_visitors.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/stats_link.png b/WordPress/src/main/res/drawable-xhdpi/stats_link.png
new file mode 100644
index 000000000..ee5076525
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/stats_link.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/switch_site_button_icon.png b/WordPress/src/main/res/drawable-xhdpi/switch_site_button_icon.png
new file mode 100644
index 000000000..1cc51ebca
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/switch_site_button_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/theme_icon_current.png b/WordPress/src/main/res/drawable-xhdpi/theme_icon_current.png
new file mode 100644
index 000000000..a69c90a48
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/theme_icon_current.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/theme_icon_premium.png b/WordPress/src/main/res/drawable-xhdpi/theme_icon_premium.png
new file mode 100644
index 000000000..2f4d391de
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/theme_icon_premium.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_current.png b/WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_current.png
new file mode 100644
index 000000000..a1013ef6a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_current.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_premium.png b/WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_premium.png
new file mode 100644
index 000000000..bf927cd70
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xhdpi/theme_icon_tag_premium.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi-v4/action_mode_confirm_checkmark.png b/WordPress/src/main/res/drawable-xxhdpi-v4/action_mode_confirm_checkmark.png
new file mode 100644
index 000000000..39d946fa0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi-v4/action_mode_confirm_checkmark.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi-v4/ic_media_play.png b/WordPress/src/main/res/drawable-xxhdpi-v4/ic_media_play.png
new file mode 100644
index 000000000..41f76bbf9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi-v4/ic_media_play.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi-v4/tab_icon_create_gallery.png b/WordPress/src/main/res/drawable-xxhdpi-v4/tab_icon_create_gallery.png
new file mode 100644
index 000000000..7f95f4c68
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi-v4/tab_icon_create_gallery.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/arrow.png b/WordPress/src/main/res/drawable-xxhdpi/arrow.png
new file mode 100644
index 000000000..6cc3e8100
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/arrow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/blavatar_placeholder.png b/WordPress/src/main/res/drawable-xxhdpi/blavatar_placeholder.png
new file mode 100644
index 000000000..c223eb517
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/blavatar_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_bottom.png b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_bottom.png
new file mode 100644
index 000000000..483b1f941
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_bottom.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page1.png b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page1.png
new file mode 100644
index 000000000..4e9510bc5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page1.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page2.png b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page2.png
new file mode 100644
index 000000000..05e4831e6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page2.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page3.png b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page3.png
new file mode 100644
index 000000000..e9ad4e425
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_page3.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_top.png b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_top.png
new file mode 100644
index 000000000..41906ad6b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/box_with_pages_top.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/button_blue.9.png b/WordPress/src/main/res/drawable-xxhdpi/button_blue.9.png
new file mode 100644
index 000000000..6ed33f72e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/button_blue.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/button_blue_disabled.9.png b/WordPress/src/main/res/drawable-xxhdpi/button_blue_disabled.9.png
new file mode 100644
index 000000000..cf3f827d9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/button_blue_disabled.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/button_blue_focus.9.png b/WordPress/src/main/res/drawable-xxhdpi/button_blue_focus.9.png
new file mode 100644
index 000000000..5743fe47c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/button_blue_focus.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/button_darkgray.9.png b/WordPress/src/main/res/drawable-xxhdpi/button_darkgray.9.png
new file mode 100644
index 000000000..f39baf5ce
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/button_darkgray.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/button_darkgray_disabled.9.png b/WordPress/src/main/res/drawable-xxhdpi/button_darkgray_disabled.9.png
new file mode 100644
index 000000000..9f739b1eb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/button_darkgray_disabled.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/button_darkgray_focus.9.png b/WordPress/src/main/res/drawable-xxhdpi/button_darkgray_focus.9.png
new file mode 100644
index 000000000..1feb30e59
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/button_darkgray_focus.9.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_site.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_site.png
new file mode 100644
index 000000000..9b390a3e6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_site.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_users.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_users.png
new file mode 100644
index 000000000..881f991f0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_admin_users.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_edit.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_edit.png
new file mode 100644
index 000000000..3e085fd82
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_edit.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_email.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_email.png
new file mode 100644
index 000000000..e7295f640
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_email.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_closed.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_closed.png
new file mode 100644
index 000000000..6b9bb59bd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_closed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_open.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_open.png
new file mode 100644
index 000000000..57ad7e5c4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_eye_open.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_info.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_info.png
new file mode 100644
index 000000000..a35e162d6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_info.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_lock.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_lock.png
new file mode 100644
index 000000000..d2bb419fb
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_lock.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/dashicon_wordpress_alt.png b/WordPress/src/main/res/drawable-xxhdpi/dashicon_wordpress_alt.png
new file mode 100644
index 000000000..2a799d4c6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/dashicon_wordpress_alt.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/endlist_logo.png b/WordPress/src/main/res/drawable-xxhdpi/endlist_logo.png
new file mode 100644
index 000000000..583b10da7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/endlist_logo.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_closed.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_closed.png
new file mode 100644
index 000000000..6d103d962
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_closed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_open.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_open.png
new file mode 100644
index 000000000..8b4af6056
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_arrow_dropdown_open.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_checkbox_empty.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_checkbox_empty.png
new file mode 100644
index 000000000..97b529734
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_checkbox_empty.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles.png
new file mode 100644
index 000000000..75e6db836
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles_selected.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles_selected.png
new file mode 100644
index 000000000..087b21bbf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_circles_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow.png
new file mode 100644
index 000000000..f4f4f9776
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow_selected.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow_selected.png
new file mode 100644
index 000000000..68f48d21c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_slideshow_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares.png
new file mode 100644
index 000000000..79da1567e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares_selected.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares_selected.png
new file mode 100644
index 000000000..7cc37b8e6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_squares_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid.png
new file mode 100644
index 000000000..dfe19614f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid_selected.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid_selected.png
new file mode 100644
index 000000000..4b44b5aa8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_thumbnailgrid_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled.png
new file mode 100644
index 000000000..5e3bd7ab0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled_selected.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled_selected.png
new file mode 100644
index 000000000..58b8f5c74
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_icon_tiled_selected.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gallery_tablet_move_file.png b/WordPress/src/main/res/drawable-xxhdpi/gallery_tablet_move_file.png
new file mode 100644
index 000000000..839a0080e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gallery_tablet_move_file.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gravatar_placeholder.png b/WordPress/src/main/res/drawable-xxhdpi/gravatar_placeholder.png
new file mode 100644
index 000000000..8c1a30e7e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gravatar_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gridicon_create_dark.png b/WordPress/src/main/res/drawable-xxhdpi/gridicon_create_dark.png
new file mode 100644
index 000000000..d7b36663c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gridicon_create_dark.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/gridicon_create_light.png b/WordPress/src/main/res/drawable-xxhdpi/gridicon_create_light.png
new file mode 100644
index 000000000..830dc61a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/gridicon_create_light.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_approve.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_approve.png
new file mode 100644
index 000000000..f718d0e20
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_approve.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_approve_active.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_approve_active.png
new file mode 100644
index 000000000..d6bb54ab8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_approve_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_camera.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_camera.png
new file mode 100644
index 000000000..23a9c2efd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_camera.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_comment.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_comment.png
new file mode 100644
index 000000000..a33b4ffe1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_like.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_like.png
new file mode 100644
index 000000000..318a5c324
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_like_active.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_like_active.png
new file mode 100644
index 000000000..01110d83d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_like_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_found.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_found.png
new file mode 100644
index 000000000..43f18ea01
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_found.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_off.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_off.png
new file mode 100644
index 000000000..9d02cbccf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_off.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_searching.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_searching.png
new file mode 100644
index 000000000..3abb7f9c7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_location_searching.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_more.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_more.png
new file mode 100644
index 000000000..a8e568407
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_more.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_more_grey.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_more_grey.png
new file mode 100644
index 000000000..988798a3a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_more_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_reply.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_reply.png
new file mode 100644
index 000000000..e110880dc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_reply.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_restore.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_restore.png
new file mode 100644
index 000000000..0219f2b47
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_restore.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_spam.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_spam.png
new file mode 100644
index 000000000..6d4021d99
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_spam.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_trash.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_trash.png
new file mode 100644
index 000000000..b1dc20c28
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_trash.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_action_video.png b/WordPress/src/main/res/drawable-xxhdpi/ic_action_video.png
new file mode 100644
index 000000000..7f712b74e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_action_video.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_add_blue_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_add_blue_24dp.png
new file mode 100644
index 000000000..482228a49
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_add_blue_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.png
new file mode 100644
index 000000000..70e4e86e7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png
new file mode 100644
index 000000000..72cedcad4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png
new file mode 100644
index 000000000..4057cc545
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_arrow_up_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_arrow_up_white_24dp.png
new file mode 100644
index 000000000..5dcd97a3a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_arrow_up_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_bell_grey.png b/WordPress/src/main/res/drawable-xxhdpi/ic_bell_grey.png
new file mode 100644
index 000000000..c607e59a0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_bell_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png
new file mode 100644
index 000000000..6e03d54cf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_check_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..3179d765c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..0eb9d8b08
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_comment.png b/WordPress/src/main/res/drawable-xxhdpi/ic_comment.png
new file mode 100644
index 000000000..82241c8a0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_comment_active.png b/WordPress/src/main/res/drawable-xxhdpi/ic_comment_active.png
new file mode 100644
index 000000000..56681f6e6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_comment_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_comment_disabled.png b/WordPress/src/main/res/drawable-xxhdpi/ic_comment_disabled.png
new file mode 100644
index 000000000..2521da2c4
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_comment_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.png
new file mode 100644
index 000000000..5fc17a4d1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png
new file mode 100644
index 000000000..34ec7092f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png
new file mode 100644
index 000000000..0e95e9b1d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_email_grey.png b/WordPress/src/main/res/drawable-xxhdpi/ic_email_grey.png
new file mode 100644
index 000000000..4aa5c4183
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_email_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_grey_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_grey_24dp.png
new file mode 100644
index 000000000..e7476e822
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_grey_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_white_24dp.png
new file mode 100644
index 000000000..c860dee46
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_tag_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_web_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_web_white_24dp.png
new file mode 100644
index 000000000..f4359c9f6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_genericon_web_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_level_indicator.png b/WordPress/src/main/res/drawable-xxhdpi/ic_level_indicator.png
new file mode 100644
index 000000000..932da74a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_level_indicator.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_like.png b/WordPress/src/main/res/drawable-xxhdpi/ic_like.png
new file mode 100644
index 000000000..38bff52a3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_like_active.png b/WordPress/src/main/res/drawable-xxhdpi/ic_like_active.png
new file mode 100644
index 000000000..cfc797de2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_like_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_like_disabled.png b/WordPress/src/main/res/drawable-xxhdpi/ic_like_disabled.png
new file mode 100644
index 000000000..844f8956d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_like_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_location_on_grey600_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_location_on_grey600_24dp.png
new file mode 100644
index 000000000..7c1c84dfa
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_location_on_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_phone_grey.png b/WordPress/src/main/res/drawable-xxhdpi/ic_phone_grey.png
new file mode 100644
index 000000000..163917abc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_phone_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_post_settings.png b/WordPress/src/main/res/drawable-xxhdpi/ic_post_settings.png
new file mode 100644
index 000000000..14c9186ef
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_post_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png
new file mode 100644
index 000000000..72128fe69
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_24dp.png
new file mode 100644
index 000000000..6da14d6e1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_reply_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_reply_white_24dp.png
new file mode 100644
index 000000000..0f11be495
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_reply_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_report_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_report_white_24dp.png
new file mode 100644
index 000000000..6abdf80f6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_report_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png
new file mode 100644
index 000000000..6c87e1358
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_search.png b/WordPress/src/main/res/drawable-xxhdpi/ic_search.png
new file mode 100644
index 000000000..bf84b62cd
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_search.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png
new file mode 100644
index 000000000..0bbeab150
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png
new file mode 100644
index 000000000..2c7a80266
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_send_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_send_white_24dp.png
new file mode 100644
index 000000000..cbb64e09f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_send_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png
new file mode 100644
index 000000000..5cd8e5650
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png
new file mode 100644
index 000000000..9963c6a05
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_normal.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_normal.png
new file mode 100644
index 000000000..09662937f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_pressed.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_pressed.png
new file mode 100644
index 000000000..b81dfb998
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_me_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_normal.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_normal.png
new file mode 100644
index 000000000..49d32ed5d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_pressed.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_pressed.png
new file mode 100644
index 000000000..5e56073b3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_notifications_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_normal.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_normal.png
new file mode 100644
index 000000000..b17d76f57
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_pressed.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_pressed.png
new file mode 100644
index 000000000..212127f01
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_reader_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_normal.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_normal.png
new file mode 100644
index 000000000..a79e67054
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_normal.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_pressed.png b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_pressed.png
new file mode 100644
index 000000000..ff2d392a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_tab_sites_pressed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_theme_customize.png b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_customize.png
new file mode 100644
index 000000000..8df629430
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_customize.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_theme_details.png b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_details.png
new file mode 100644
index 000000000..7e1639753
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_details.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_theme_loading.png b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_loading.png
new file mode 100644
index 000000000..abb985039
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_loading.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_theme_support.png b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_support.png
new file mode 100644
index 000000000..0b0c04667
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_theme_support.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.png
new file mode 100644
index 000000000..1e365396f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.png
new file mode 100644
index 000000000..571670e4a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/me_icon_account_settings.png b/WordPress/src/main/res/drawable-xxhdpi/me_icon_account_settings.png
new file mode 100644
index 000000000..78dac6b9c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/me_icon_account_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/me_icon_app_settings.png b/WordPress/src/main/res/drawable-xxhdpi/me_icon_app_settings.png
new file mode 100644
index 000000000..23c9d935f
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/me_icon_app_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/me_icon_login_logout.png b/WordPress/src/main/res/drawable-xxhdpi/me_icon_login_logout.png
new file mode 100644
index 000000000..f174c9119
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/me_icon_login_logout.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/me_icon_my_profile.png b/WordPress/src/main/res/drawable-xxhdpi/me_icon_my_profile.png
new file mode 100644
index 000000000..8e75f224c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/me_icon_my_profile.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/me_icon_notifications.png b/WordPress/src/main/res/drawable-xxhdpi/me_icon_notifications.png
new file mode 100644
index 000000000..8ac8a4ea5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/me_icon_notifications.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/me_icon_support.png b/WordPress/src/main/res/drawable-xxhdpi/me_icon_support.png
new file mode 100644
index 000000000..58482a5d6
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/me_icon_support.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/media_audio.png b/WordPress/src/main/res/drawable-xxhdpi/media_audio.png
new file mode 100644
index 000000000..464d73bf3
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/media_audio.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/media_document.png b/WordPress/src/main/res/drawable-xxhdpi/media_document.png
new file mode 100644
index 000000000..8a00e7dfc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/media_document.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/media_image_placeholder.png b/WordPress/src/main/res/drawable-xxhdpi/media_image_placeholder.png
new file mode 100644
index 000000000..f76dec6ef
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/media_image_placeholder.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/media_item_placeholder.xml b/WordPress/src/main/res/drawable-xxhdpi/media_item_placeholder.xml
new file mode 100644
index 000000000..a2eab5856
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/media_item_placeholder.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@color/grey_darken_10" />
+
+</selector>
diff --git a/WordPress/src/main/res/drawable-xxhdpi/media_powerpoint.png b/WordPress/src/main/res/drawable-xxhdpi/media_powerpoint.png
new file mode 100644
index 000000000..b93be395c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/media_powerpoint.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/media_spreadsheet.png b/WordPress/src/main/res/drawable-xxhdpi/media_spreadsheet.png
new file mode 100644
index 000000000..089d3e11d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/media_spreadsheet.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_comments.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_comments.png
new file mode 100644
index 000000000..d38047e3a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_comments.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_media.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_media.png
new file mode 100644
index 000000000..fbcb51a96
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_media.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_pages.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_pages.png
new file mode 100644
index 000000000..30df2e6bc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_pages.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_posts.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_posts.png
new file mode 100644
index 000000000..c102ab529
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_posts.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_settings.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_settings.png
new file mode 100644
index 000000000..060a7eeb7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_settings.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_stats.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_stats.png
new file mode 100644
index 000000000..45cf6af4b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_stats.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_themes.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_themes.png
new file mode 100644
index 000000000..13c49efb5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_themes.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_admin.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_admin.png
new file mode 100644
index 000000000..7132730af
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_admin.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_site.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_site.png
new file mode 100644
index 000000000..d17642a36
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_icon_view_site.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/my_site_no_sites_drake.png b/WordPress/src/main/res/drawable-xxhdpi/my_site_no_sites_drake.png
new file mode 100644
index 000000000..01c70b03c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/my_site_no_sites_drake.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/note_icon_reply.png b/WordPress/src/main/res/drawable-xxhdpi/note_icon_reply.png
new file mode 100644
index 000000000..2dbb736b5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/note_icon_reply.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_alert_big.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_alert_big.png
new file mode 100644
index 000000000..10d6ff7aa
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_alert_big.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_back.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_back.png
new file mode 100644
index 000000000..8909c9471
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_back.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_clock.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_clock.png
new file mode 100644
index 000000000..ca90db846
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_clock.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_edit.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_edit.png
new file mode 100644
index 000000000..76f637f9a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_edit.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_more.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_more.png
new file mode 100644
index 000000000..e426b6913
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_more.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_publish.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_publish.png
new file mode 100644
index 000000000..2b4fd928c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_publish.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_restore.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_restore.png
new file mode 100644
index 000000000..acb300293
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_restore.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_scheduled.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_scheduled.png
new file mode 100644
index 000000000..c43ec5e03
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_scheduled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_star_active.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_star_active.png
new file mode 100644
index 000000000..1fcf29dba
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_star_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_star_disabled.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_star_disabled.png
new file mode 100644
index 000000000..c027762bf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_star_disabled.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_star_unactive.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_star_unactive.png
new file mode 100644
index 000000000..6231542df
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_star_unactive.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_stats.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_stats.png
new file mode 100644
index 000000000..375c374cc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_stats.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_sticky.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_sticky.png
new file mode 100644
index 000000000..4b08b2e61
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_sticky.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_trash.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_trash.png
new file mode 100644
index 000000000..571487a74
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_trash.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_trashed.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_trashed.png
new file mode 100644
index 000000000..099c5bb28
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_trashed.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_view.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_view.png
new file mode 100644
index 000000000..dc5a62001
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_view.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/noticon_warning_big_grey.png b/WordPress/src/main/res/drawable-xxhdpi/noticon_warning_big_grey.png
new file mode 100644
index 000000000..a4ecc0636
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/noticon_warning_big_grey.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/notification_icon.png b/WordPress/src/main/res/drawable-xxhdpi/notification_icon.png
new file mode 100644
index 000000000..17f3101f8
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/notification_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/nux_icon_wp.png b/WordPress/src/main/res/drawable-xxhdpi/nux_icon_wp.png
new file mode 100644
index 000000000..b50d7e977
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/nux_icon_wp.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/penandink.png b/WordPress/src/main/res/drawable-xxhdpi/penandink.png
new file mode 100644
index 000000000..de2438ebe
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/penandink.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_comment.png b/WordPress/src/main/res/drawable-xxhdpi/reader_comment.png
new file mode 100644
index 000000000..19ae805f9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_comment.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_comment_active.png b/WordPress/src/main/res/drawable-xxhdpi/reader_comment_active.png
new file mode 100644
index 000000000..4ff1a2f0b
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_comment_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_dropdown_arrow.png b/WordPress/src/main/res/drawable-xxhdpi/reader_dropdown_arrow.png
new file mode 100644
index 000000000..97438cefc
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_dropdown_arrow.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_like.png b/WordPress/src/main/res/drawable-xxhdpi/reader_like.png
new file mode 100644
index 000000000..c6e5f2891
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_like.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_like_empty.png b/WordPress/src/main/res/drawable-xxhdpi/reader_like_empty.png
new file mode 100644
index 000000000..eedd2f94d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_like_empty.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_like_empty_active.png b/WordPress/src/main/res/drawable-xxhdpi/reader_like_empty_active.png
new file mode 100644
index 000000000..ceaf4d9a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_like_empty_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_tear.png b/WordPress/src/main/res/drawable-xxhdpi/reader_tear.png
new file mode 100644
index 000000000..4bc2224ac
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/reader_tear.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/rppreview1.png b/WordPress/src/main/res/drawable-xxhdpi/rppreview1.png
new file mode 100644
index 000000000..f4d93fef1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/rppreview1.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/rppreview2.png b/WordPress/src/main/res/drawable-xxhdpi/rppreview2.png
new file mode 100644
index 000000000..8a002bdd2
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/rppreview2.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/rppreview3.png b/WordPress/src/main/res/drawable-xxhdpi/rppreview3.png
new file mode 100644
index 000000000..8b1a388f1
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/rppreview3.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_chevron_down.png b/WordPress/src/main/res/drawable-xxhdpi/stats_chevron_down.png
new file mode 100644
index 000000000..eb8b3ec63
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_chevron_down.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_chevron_right.png b/WordPress/src/main/res/drawable-xxhdpi/stats_chevron_right.png
new file mode 100644
index 000000000..5ac51b3f7
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_chevron_right.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_categories.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_categories.png
new file mode 100644
index 000000000..e33c2179a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_categories.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments.png
new file mode 100644
index 000000000..8dc80f815
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments_active.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments_active.png
new file mode 100644
index 000000000..140fae9a9
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_comments_active.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_default_site_avatar.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_default_site_avatar.png
new file mode 100644
index 000000000..49007650e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_default_site_avatar.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_info.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_info.png
new file mode 100644
index 000000000..8dcc43a7a
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_info.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_likes.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_likes.png
new file mode 100644
index 000000000..34895442d
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_likes.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_tags.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_tags.png
new file mode 100644
index 000000000..cff4af2db
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_tags.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_trophy.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_trophy.png
new file mode 100644
index 000000000..47d5f6f30
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_trophy.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_views.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_views.png
new file mode 100644
index 000000000..35d42219c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_views.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_icon_visitors.png b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_visitors.png
new file mode 100644
index 000000000..4b3c77e49
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_icon_visitors.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/stats_link.png b/WordPress/src/main/res/drawable-xxhdpi/stats_link.png
new file mode 100644
index 000000000..cc2a48b6e
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/stats_link.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/switch_site_button_icon.png b/WordPress/src/main/res/drawable-xxhdpi/switch_site_button_icon.png
new file mode 100644
index 000000000..53e6030b5
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/switch_site_button_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/theme_icon_current.png b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_current.png
new file mode 100644
index 000000000..a7869d397
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_current.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/theme_icon_premium.png b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_premium.png
new file mode 100644
index 000000000..14d9c4e62
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_premium.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_current.png b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_current.png
new file mode 100644
index 000000000..244dc86bf
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_current.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_premium.png b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_premium.png
new file mode 100644
index 000000000..1dde5cf0c
--- /dev/null
+++ b/WordPress/src/main/res/drawable-xxhdpi/theme_icon_tag_premium.png
Binary files differ
diff --git a/WordPress/src/main/res/drawable/badge.xml b/WordPress/src/main/res/drawable/badge.xml
new file mode 100644
index 000000000..d1c565ad4
--- /dev/null
+++ b/WordPress/src/main/res/drawable/badge.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/badge_pressed" android:state_selected="true" />
+ <item android:drawable="@drawable/badge_normal" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/badge_normal.xml b/WordPress/src/main/res/drawable/badge_normal.xml
new file mode 100644
index 000000000..854640936
--- /dev/null
+++ b/WordPress/src/main/res/drawable/badge_normal.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="#8B8A72" />
+ <stroke
+ android:width="3dp"
+ android:color="@color/color_primary" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/badge_pressed.xml b/WordPress/src/main/res/drawable/badge_pressed.xml
new file mode 100644
index 000000000..d588da80e
--- /dev/null
+++ b/WordPress/src/main/res/drawable/badge_pressed.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/color_accent" />
+ <stroke
+ android:width="3dp"
+ android:color="@color/color_primary" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/btn_cab_done_wordpress.xml b/WordPress/src/main/res/drawable/btn_cab_done_wordpress.xml
new file mode 100644
index 000000000..db6bef0a4
--- /dev/null
+++ b/WordPress/src/main/res/drawable/btn_cab_done_wordpress.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_cab_done_pressed_wordpress" />
+ <item android:state_focused="true" android:state_enabled="true"
+ android:drawable="@drawable/btn_cab_done_focused_wordpress" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/btn_cab_done_default_wordpress" />
+</selector>
diff --git a/WordPress/src/main/res/drawable/calypso_bordered_bg.xml b/WordPress/src/main/res/drawable/calypso_bordered_bg.xml
new file mode 100644
index 000000000..f0f2d1a27
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_bordered_bg.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- bordered sides for tablet UI -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_20" />
+ </shape>
+ </item>
+ <item android:left="1dp" android:right="1dp">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_background.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_background.xml
new file mode 100644
index 000000000..7bdbd6398
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_background.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:bottomRightRadius="3dp"
+ android:topRightRadius="3dp"
+ android:bottomLeftRadius="3dp"
+ android:topLeftRadius="3dp" />
+ <stroke
+ android:width="1dp"
+ android:color="@color/grey" />
+ <solid android:color="@color/white" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_button.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_button.xml
new file mode 100644
index 000000000..fb23699dc
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_button.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/calypso_segmented_control_selected" android:state_checked="true" android:state_pressed="false" />
+ <item android:drawable="@drawable/calypso_segmented_control_normal" android:state_checked="false" android:state_pressed="false" />
+ <item android:drawable="@drawable/calypso_segmented_control_selected" android:state_checked="true" android:state_pressed="true" />
+ <item android:drawable="@drawable/calypso_segmented_control_normal" android:state_checked="false" android:state_pressed="true" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_button_end.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_button_end.xml
new file mode 100644
index 000000000..0f9b00f04
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_button_end.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/calypso_segmented_control_selected_end" android:state_checked="true" android:state_pressed="false" />
+ <item android:drawable="@android:color/transparent" android:state_checked="false" android:state_pressed="false" />
+ <item android:drawable="@drawable/calypso_segmented_control_selected_end" android:state_checked="true" android:state_pressed="true" />
+ <item android:drawable="@android:color/transparent" android:state_checked="false" android:state_pressed="true" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_button_start.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_button_start.xml
new file mode 100644
index 000000000..76c14a0d1
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_button_start.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/calypso_segmented_control_selected_start" android:state_checked="true" android:state_pressed="false" />
+ <item android:drawable="@drawable/calypso_segmented_control_normal_start" android:state_checked="false" android:state_pressed="false" />
+ <item android:drawable="@drawable/calypso_segmented_control_selected_start" android:state_checked="true" android:state_pressed="true" />
+ <item android:drawable="@drawable/calypso_segmented_control_normal_start" android:state_checked="false" android:state_pressed="true" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_normal.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_normal.xml
new file mode 100644
index 000000000..bc8ae7f00
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_normal.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:left="-1dp" android:top="-1dp" android:bottom="-1dp">
+ <shape>
+ <solid android:color="@android:color/transparent" />
+ <stroke
+ android:width="1dp"
+ android:color="@color/grey" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_normal_start.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_normal_start.xml
new file mode 100644
index 000000000..468573a5b
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_normal_start.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/white" />
+ <corners
+ android:bottomLeftRadius="3dp"
+ android:topLeftRadius="3dp" />
+ <stroke
+ android:width="1dp"
+ android:color="@color/grey" />
+</shape>
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_selected.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_selected.xml
new file mode 100644
index 000000000..bf306df3a
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_selected.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ <stroke
+ android:width="1dp"
+ android:color="@color/grey_dark" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_selected_end.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_selected_end.xml
new file mode 100644
index 000000000..b79a3b5a1
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_selected_end.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ <corners
+ android:bottomRightRadius="3dp"
+ android:topRightRadius="3dp" />
+ <stroke
+ android:width="1dp"
+ android:color="@color/grey_dark" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/calypso_segmented_control_selected_start.xml b/WordPress/src/main/res/drawable/calypso_segmented_control_selected_start.xml
new file mode 100644
index 000000000..a160b3dba
--- /dev/null
+++ b/WordPress/src/main/res/drawable/calypso_segmented_control_selected_start.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ <corners
+ android:bottomLeftRadius="3dp"
+ android:topLeftRadius="3dp" />
+ <stroke
+ android:width="1dp"
+ android:color="@color/grey_dark" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/comment_reply_background.xml b/WordPress/src/main/res/drawable/comment_reply_background.xml
new file mode 100644
index 000000000..9aa45e2ea
--- /dev/null
+++ b/WordPress/src/main/res/drawable/comment_reply_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </item>
+ <item
+ android:left="@dimen/margin_extra_large"
+ android:right="@dimen/margin_extra_large"
+ android:top="9dp">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ <padding
+ android:bottom="1dp"
+ android:left="20dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/comment_reply_unapproved_background.xml b/WordPress/src/main/res/drawable/comment_reply_unapproved_background.xml
new file mode 100644
index 000000000..6ea63fed8
--- /dev/null
+++ b/WordPress/src/main/res/drawable/comment_reply_unapproved_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </item>
+ <item
+ android:left="@dimen/margin_extra_large">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/notification_status_unapproved" />
+ <padding
+ android:bottom="1dp"
+ android:left="20dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/notification_status_unapproved_background" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/comment_unapproved_background.xml b/WordPress/src/main/res/drawable/comment_unapproved_background.xml
new file mode 100644
index 000000000..237ccc505
--- /dev/null
+++ b/WordPress/src/main/res/drawable/comment_unapproved_background.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/notification_status_unapproved" />
+ <padding
+ android:bottom="1dp"
+ android:left="4dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/notification_status_unapproved_background" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/dialog_info_button_background.xml b/WordPress/src/main/res/drawable/dialog_info_button_background.xml
new file mode 100644
index 000000000..e86873f6b
--- /dev/null
+++ b/WordPress/src/main/res/drawable/dialog_info_button_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@drawable/selectable_background_wordpress" />
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/fab_menu_label_background.xml b/WordPress/src/main/res/drawable/fab_menu_label_background.xml
new file mode 100644
index 000000000..35f7e4dd6
--- /dev/null
+++ b/WordPress/src/main/res/drawable/fab_menu_label_background.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/grey_darken_20" />
+ <corners android:radius="2dp" />
+ <padding
+ android:bottom="@dimen/margin_medium"
+ android:left="@dimen/margin_large"
+ android:right="@dimen/margin_large"
+ android:top="@dimen/margin_medium" />
+
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/gallery_circles_selector.xml b/WordPress/src/main/res/drawable/gallery_circles_selector.xml
new file mode 100644
index 000000000..54faf5d9f
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gallery_circles_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_checked="true" android:drawable="@drawable/gallery_icon_circles_selected"></item>
+ <item android:state_pressed="true" android:drawable="@drawable/gallery_icon_circles_selected"></item>
+ <item android:drawable="@drawable/gallery_icon_circles"></item>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/gallery_slideshow_selector.xml b/WordPress/src/main/res/drawable/gallery_slideshow_selector.xml
new file mode 100644
index 000000000..e1f071e86
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gallery_slideshow_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_checked="true" android:drawable="@drawable/gallery_icon_slideshow_selected"></item>
+ <item android:state_pressed="true" android:drawable="@drawable/gallery_icon_slideshow_selected"></item>
+ <item android:drawable="@drawable/gallery_icon_slideshow"></item>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/gallery_squares_selector.xml b/WordPress/src/main/res/drawable/gallery_squares_selector.xml
new file mode 100644
index 000000000..4edcfb6e3
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gallery_squares_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_checked="true" android:drawable="@drawable/gallery_icon_squares_selected"></item>
+ <item android:state_pressed="true" android:drawable="@drawable/gallery_icon_squares_selected"></item>
+ <item android:drawable="@drawable/gallery_icon_squares"></item>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/gallery_thumbnail_grid_selector.xml b/WordPress/src/main/res/drawable/gallery_thumbnail_grid_selector.xml
new file mode 100644
index 000000000..66f98ad12
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gallery_thumbnail_grid_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_checked="true" android:drawable="@drawable/gallery_icon_thumbnailgrid_selected"></item>
+ <item android:state_pressed="true" android:drawable="@drawable/gallery_icon_thumbnailgrid_selected"></item>
+ <item android:drawable="@drawable/gallery_icon_thumbnailgrid"></item>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/gallery_tiled_selector.xml b/WordPress/src/main/res/drawable/gallery_tiled_selector.xml
new file mode 100644
index 000000000..4e6c3a183
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gallery_tiled_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_checked="true" android:drawable="@drawable/gallery_icon_tiled_selected"></item>
+ <item android:state_pressed="true" android:drawable="@drawable/gallery_icon_tiled_selected"></item>
+ <item android:drawable="@drawable/gallery_icon_tiled"></item>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/gridicons_clipboard.xml b/WordPress/src/main/res/drawable/gridicons_clipboard.xml
new file mode 100644
index 000000000..1fe2aa276
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_clipboard.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF87a6bc"
+ android:pathData="M16,18H8v-2h8V18zM16,12H8v2h8V12zM18,3h-2v2h2v15H6V5h2V3H6C4.895,3 4,3.895 4,5v15c0,1.105 0.895,2 2,2h12c1.105,0 2,-0.895 2,-2V5C20,3.895 19.105,3 18,3zM14,5V4c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2v1C8.895,5 8,5.895 8,7v1h8V7C16,5.895 15.105,5 14,5z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_cog.xml b/WordPress/src/main/res/drawable/gridicons_cog.xml
new file mode 100644
index 000000000..443773e70
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_cog.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20,12c0,-0.568 -0.061,-1.122 -0.174,-1.656l1.834,-1.612l-2,-3.464l-2.322,0.786c-0.819,-0.736 -1.787,-1.308 -2.859,-1.657L14,2h-4L9.521,4.396c-1.072,0.349 -2.04,0.921 -2.859,1.657L4.34,5.268l-2,3.464l1.834,1.612C4.061,10.878 4,11.432 4,12s0.061,1.122 0.174,1.656L2.34,15.268l2,3.464l2.322,-0.786c0.819,0.736 1.787,1.308 2.859,1.657L10,22h4l0.479,-2.396c1.072,-0.349 2.039,-0.921 2.859,-1.657l2.322,0.786l2,-3.464l-1.834,-1.612C19.939,13.122 20,12.568 20,12zM12,16c-2.209,0 -4,-1.791 -4,-4c0,-2.209 1.791,-4 4,-4c2.209,0 4,1.791 4,4C16,14.209 14.209,16 12,16z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_cog_light.xml b/WordPress/src/main/res/drawable/gridicons_cog_light.xml
new file mode 100644
index 000000000..9220cd654
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_cog_light.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M20,12c0,-0.568 -0.061,-1.122 -0.174,-1.656l1.834,-1.612l-2,-3.464l-2.322,0.786c-0.819,-0.736 -1.787,-1.308 -2.859,-1.657L14,2h-4L9.521,4.396c-1.072,0.349 -2.04,0.921 -2.859,1.657L4.34,5.268l-2,3.464l1.834,1.612C4.061,10.878 4,11.432 4,12s0.061,1.122 0.174,1.656L2.34,15.268l2,3.464l2.322,-0.786c0.819,0.736 1.787,1.308 2.859,1.657L10,22h4l0.479,-2.396c1.072,-0.349 2.039,-0.921 2.859,-1.657l2.322,0.786l2,-3.464l-1.834,-1.612C19.939,13.122 20,12.568 20,12zM12,16c-2.209,0 -4,-1.791 -4,-4c0,-2.209 1.791,-4 4,-4c2.209,0 4,1.791 4,4C16,14.209 14.209,16 12,16z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_external.xml b/WordPress/src/main/res/drawable/gridicons_external.xml
new file mode 100644
index 000000000..b8350df6a
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_external.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,13v6c0,1.105 -0.895,2 -2,2H5c-1.105,0 -2,-0.895 -2,-2V7c0,-1.105 0.895,-2 2,-2h6v2H5v12h12v-6H19zM13,3v2h4.586l-7.793,7.793l1.414,1.414L19,6.414V11h2V3H13z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_history.xml b/WordPress/src/main/res/drawable/gridicons_history.xml
new file mode 100644
index 000000000..b4cc4d582
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_history.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M2.12,13.526c0.742,4.781 4.902,8.47 9.881,8.47c5.499,0 9.998,-4.499 9.998,-9.998S17.501,2 12.002,2C8.704,2 5.802,3.601 4,6l0,0V2.001L2.003,2L2,9h7V7L5.801,6.999l0,0C7.2,5.201 9.502,4 12.002,4C16.4,4 20,7.6 20,11.998s-3.6,7.999 -7.999,7.999c-3.878,0 -7.132,-2.794 -7.849,-6.471L2.12,13.526z"/>
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M11.002,6.999l0,5.302l3.201,4.297l1.6,-1.197l-2.801,-3.701l0,-4.701z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_search.xml b/WordPress/src/main/res/drawable/gridicons_search.xml
new file mode 100644
index 000000000..ce42dc946
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_search.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21,19l-5.154,-5.154C16.574,12.742 17,11.421 17,10c0,-3.866 -3.134,-7 -7,-7s-7,3.134 -7,7c0,3.866 3.134,7 7,7c1.421,0 2.742,-0.426 3.846,-1.154L19,21L21,19zM5,10c0,-2.757 2.243,-5 5,-5s5,2.243 5,5s-2.243,5 -5,5S5,12.757 5,10z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_search_light.xml b/WordPress/src/main/res/drawable/gridicons_search_light.xml
new file mode 100644
index 000000000..bdccb4ecf
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_search_light.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M21,19l-5.154,-5.154C16.574,12.742 17,11.421 17,10c0,-3.866 -3.134,-7 -7,-7s-7,3.134 -7,7c0,3.866 3.134,7 7,7c1.421,0 2.742,-0.426 3.846,-1.154L19,21L21,19zM5,10c0,-2.757 2.243,-5 5,-5s5,2.243 5,5s-2.243,5 -5,5S5,12.757 5,10z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/gridicons_trash.xml b/WordPress/src/main/res/drawable/gridicons_trash.xml
new file mode 100644
index 000000000..a1ff3f401
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_trash.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M6.187,8h11.625l-0.695,11.125C17.051,20.179 16.177,21 15.121,21H8.879c-1.056,0 -1.93,-0.821 -1.996,-1.875L6.187,8zM19,5v2H5V5h3V4c0,-1.105 0.895,-2 2,-2h4c1.105,0 2,0.895 2,2v1H19zM10,5h4V4h-4V5z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/ic_select_all_white_24dp.xml b/WordPress/src/main/res/drawable/ic_select_all_white_24dp.xml
new file mode 100644
index 000000000..0fc49c923
--- /dev/null
+++ b/WordPress/src/main/res/drawable/ic_select_all_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/indicator_circle_selected.xml b/WordPress/src/main/res/drawable/indicator_circle_selected.xml
new file mode 100644
index 000000000..f7a3216ac
--- /dev/null
+++ b/WordPress/src/main/res/drawable/indicator_circle_selected.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <stroke
+ android:width="1dp"
+ android:color="@color/blue_dark"/>
+ <solid android:color="@color/white"/>
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/indicator_circle_unselected.xml b/WordPress/src/main/res/drawable/indicator_circle_unselected.xml
new file mode 100644
index 000000000..4df03d829
--- /dev/null
+++ b/WordPress/src/main/res/drawable/indicator_circle_unselected.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/grey_dark"/>
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/invites_border.xml b/WordPress/src/main/res/drawable/invites_border.xml
new file mode 100644
index 000000000..7459f9f1b
--- /dev/null
+++ b/WordPress/src/main/res/drawable/invites_border.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ android:shape="rectangle">
+ <stroke android:width="1dp" android:color="@color/grey_lighten_10"/>
+ <solid android:color="@color/transparent"/>
+</shape>
diff --git a/WordPress/src/main/res/drawable/list_divider.xml b/WordPress/src/main/res/drawable/list_divider.xml
new file mode 100644
index 000000000..b9e69f727
--- /dev/null
+++ b/WordPress/src/main/res/drawable/list_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="@dimen/margin_extra_large"
+ android:insetRight="@dimen/margin_extra_large">
+ <shape android:shape="rectangle">
+
+ <solid android:color="@color/grey_lighten_30" />
+
+ </shape>
+</inset> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/main_tab_me.xml b/WordPress/src/main/res/drawable/main_tab_me.xml
new file mode 100644
index 000000000..b01b1a701
--- /dev/null
+++ b/WordPress/src/main/res/drawable/main_tab_me.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_tab_me_pressed" android:state_selected="true" />
+ <item android:drawable="@drawable/ic_tab_me_normal" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/main_tab_notifications.xml b/WordPress/src/main/res/drawable/main_tab_notifications.xml
new file mode 100644
index 000000000..747d2710e
--- /dev/null
+++ b/WordPress/src/main/res/drawable/main_tab_notifications.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_tab_notifications_pressed" android:state_selected="true" />
+ <item android:drawable="@drawable/ic_tab_notifications_normal" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/main_tab_reader.xml b/WordPress/src/main/res/drawable/main_tab_reader.xml
new file mode 100644
index 000000000..4e531e15b
--- /dev/null
+++ b/WordPress/src/main/res/drawable/main_tab_reader.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_tab_reader_pressed" android:state_selected="true" />
+ <item android:drawable="@drawable/ic_tab_reader_normal" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/main_tab_sites.xml b/WordPress/src/main/res/drawable/main_tab_sites.xml
new file mode 100644
index 000000000..4cee4fb29
--- /dev/null
+++ b/WordPress/src/main/res/drawable/main_tab_sites.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_tab_sites_pressed" android:state_selected="true" />
+ <item android:drawable="@drawable/ic_tab_sites_normal" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_blue_button_selector.xml b/WordPress/src/main/res/drawable/media_blue_button_selector.xml
new file mode 100644
index 000000000..6bd6ae38c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_blue_button_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:drawable="@drawable/button_blue_focus" android:state_pressed="true"/>
+ <item android:drawable="@drawable/button_blue_disabled" android:state_enabled="false"/>
+ <item android:drawable="@drawable/button_blue"/>
+
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_gallery_checkbox_selector.xml b/WordPress/src/main/res/drawable/media_gallery_checkbox_selector.xml
new file mode 100644
index 000000000..cd2568069
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_gallery_checkbox_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_checked="true" android:drawable="@android:drawable/checkbox_on_background" />
+ <item android:drawable="@drawable/gallery_checkbox_empty" />
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/media_gallery_grid_cell.xml b/WordPress/src/main/res/drawable/media_gallery_grid_cell.xml
new file mode 100644
index 000000000..ed05fcabc
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_gallery_grid_cell.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:bottom="1dp"
+ android:left="-2dp"
+ android:right="1dp"
+ android:top="-2dp">
+ <shape android:shape="rectangle" >
+ <stroke
+ android:width="1dp"
+ android:color="#DADADA" />
+
+ <solid android:color="#FFF" />
+
+ <padding
+ android:bottom="2dp"
+ android:left="0dp"
+ android:right="2dp"
+ android:top="0dp" >
+ </padding>
+ </shape>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_gallery_option_default.xml b/WordPress/src/main/res/drawable/media_gallery_option_default.xml
new file mode 100644
index 000000000..209743a1e
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_gallery_option_default.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <solid android:color="@color/media_gallery_option_default" />
+
+ <corners android:radius="2dp" />
+
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_gallery_option_selected.xml b/WordPress/src/main/res/drawable/media_gallery_option_selected.xml
new file mode 100644
index 000000000..9de5f2932
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_gallery_option_selected.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <solid android:color="@color/media_gallery_option_selected" />
+
+ <corners android:radius="2dp" />
+
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_gallery_option_selector.xml b/WordPress/src/main/res/drawable/media_gallery_option_selector.xml
new file mode 100644
index 000000000..2dfafb6a7
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_gallery_option_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item android:state_pressed="true" android:drawable="@drawable/media_gallery_option_selected" ></item>
+ <item android:state_checked="true" android:drawable="@drawable/media_gallery_option_selected" ></item>
+ <item android:drawable="@drawable/media_gallery_option_default" ></item>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/media_grid_item_checkstate_selector.xml b/WordPress/src/main/res/drawable/media_grid_item_checkstate_selector.xml
new file mode 100644
index 000000000..89f2e0a6f
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_grid_item_checkstate_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:drawable="@drawable/tab_unselected_pressed_wordpress" android:state_checked="true"/>
+ <item android:drawable="@android:color/transparent"/>
+
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_item_frame_selector.xml b/WordPress/src/main/res/drawable/media_item_frame_selector.xml
new file mode 100644
index 000000000..dc0e5bfd9
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_item_frame_selector.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@drawable/tab_unselected_pressed_wordpress"
+ android:state_checked="true" />
+
+ <item
+ android:drawable="@android:color/transparent" />
+
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/media_picker_background.xml b/WordPress/src/main/res/drawable/media_picker_background.xml
new file mode 100644
index 000000000..5dc94adb6
--- /dev/null
+++ b/WordPress/src/main/res/drawable/media_picker_background.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@color/grey_lighten_30"/>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/moderate_button_selector.xml b/WordPress/src/main/res/drawable/moderate_button_selector.xml
new file mode 100644
index 000000000..abf03f5b7
--- /dev/null
+++ b/WordPress/src/main/res/drawable/moderate_button_selector.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Drawable for moderation buttons on comment detail
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+ <item android:state_enabled="false">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/blue_dark" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/transparent" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/my_site_add_button.xml b/WordPress/src/main/res/drawable/my_site_add_button.xml
new file mode 100644
index 000000000..71caa62a5
--- /dev/null
+++ b/WordPress/src/main/res/drawable/my_site_add_button.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@color/white" android:state_pressed="true" >
+ <shape>
+ <corners android:radius="@dimen/margin_extra_small" />
+ </shape>
+ </item>
+ <item android:drawable="@color/grey_light" >
+ <shape>
+ <corners android:radius="@dimen/margin_extra_small" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/my_site_add_button_text_color_selector.xml b/WordPress/src/main/res/drawable/my_site_add_button_text_color_selector.xml
new file mode 100644
index 000000000..a0f5af026
--- /dev/null
+++ b/WordPress/src/main/res/drawable/my_site_add_button_text_color_selector.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:color="@color/grey_dark" />
+ <item android:color="@color/grey" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/new_editor_promo_header.xml b/WordPress/src/main/res/drawable/new_editor_promo_header.xml
new file mode 100644
index 000000000..41f01ba12
--- /dev/null
+++ b/WordPress/src/main/res/drawable/new_editor_promo_header.xml
@@ -0,0 +1,33 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="280dp"
+ android:height="120dp"
+ android:viewportWidth="280.0"
+ android:viewportHeight="120.0">
+ <path
+ android:pathData="m0,0c93.33,0 186.67,0 280,0 0,40 0,80 0,120 -93.33,0 -186.67,0 -280,0C0,80 0,40 0,0Z"
+ android:fillColor="#10a9dd"/>
+ <path
+ android:pathData="m139.63,23.57c-14.34,-0.2 -28.16,9.02 -33.59,22.27 -5.68,13.01 -2.73,29.18 7.2,39.33 9.56,10.29 25.31,14.06 38.5,9.25 13.67,-4.65 23.75,-18.07 24.3,-32.52C177.07,45.45 165.34,29.51 149.6,25.01 146.37,24.05 143,23.56 139.63,23.57Z"
+ android:fillColor="#78dcfa"/>
+ <path
+ android:pathData="m122,48.4c0,-5.8 0,-11.6 0,-17.4 5.86,0 11.72,0 17.58,0 2.37,2.35 4.74,4.7 7.11,7.06 -0.11,4.45 -0.21,8.91 -0.32,13.36 0.73,-3.83 1.46,-7.65 2.19,-11.48 6.48,6.57 12.96,13.14 19.45,19.72 0,5.78 0,11.56 0,17.34 -5.81,0 -11.61,0 -17.42,0C141.06,67.47 131.53,57.93 122,48.4Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="m122,31c0,5.8 0,11.6 0,17.4 9.51,9.53 19.01,19.07 28.52,28.6 5.83,0 11.66,0 17.48,0C152.67,61.67 137.33,46.33 122,31Z"
+ android:fillColor="#e4f9ff"/>
+ <path
+ android:pathData="m165.29,72.75c1.01,1.93 -1.71,1.21 -1.37,1.8 2.06,2.12 4.13,4.23 6.19,6.35 2.27,-1.62 0.57,-2.95 -0.86,-4.21 -1.32,-1.31 -2.64,-2.62 -3.96,-3.93z"
+ android:fillColor="#008eb9"/>
+ <path
+ android:pathData="m143.33,80.9c-2.24,-4.85 -4.3,-9.79 -6.43,-14.69 3.19,0.78 4.49,-4.41 1.25,-5.15C130.78,60.93 123.41,61.04 116.05,61c-3.54,0.06 -2.83,5.93 0.62,5.21 -0.02,0.86 -0.95,2.32 -1.33,3.41 -1.74,4.05 -3.54,8.08 -5.24,12.14 0.57,2.08 2.64,4.87 5.27,4.24 8.46,-0.03 16.92,0.05 25.38,-0.04 2.49,-0.2 3.72,-2.94 2.57,-5.06z"
+ android:fillColor="#0092cc"/>
+ <path
+ android:pathData="m127,69c-2.28,0 -4.56,0 -6.84,0 -1.72,4 -3.44,8 -5.16,12 4,0 8,0 12,0 0,-4 0,-8 0,-12z"
+ android:fillColor="#00588f"/>
+ <path
+ android:pathData="m141.68,77.06c-1.57,-3.61 -3.14,-7.22 -4.71,-10.83 3.2,0.78 4.52,-4.42 1.26,-5.17C134.49,60.93 130.74,61.04 127,61c0,2.78 0,5.56 0,8.33 1.46,-0.18 0.27,2.14 0.53,3.09C127.28,76.95 126.88,81.46 127,86c4.77,-0.07 9.55,0.14 14.3,-0.11 2.79,-0.48 3.13,-3.9 1.72,-5.88C142.57,79.02 142.12,78.04 141.68,77.06Z"
+ android:fillColor="#0087be"/>
+ <path
+ android:pathData="m139,81c-1.76,-4 -3.52,-8 -5.27,-12 -2.24,0 -4.48,0 -6.73,0 0,4 0,8 0,12 4,0 8,0 12,0z"
+ android:fillColor="#005082"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/notifications_list_divider.xml b/WordPress/src/main/res/drawable/notifications_list_divider.xml
new file mode 100644
index 000000000..b9e69f727
--- /dev/null
+++ b/WordPress/src/main/res/drawable/notifications_list_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="@dimen/margin_extra_large"
+ android:insetRight="@dimen/margin_extra_large">
+ <shape android:shape="rectangle">
+
+ <solid android:color="@color/grey_lighten_30" />
+
+ </shape>
+</inset> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml b/WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml
new file mode 100644
index 000000000..e87072317
--- /dev/null
+++ b/WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android">
+ <shape android:shape="rectangle">
+
+ <solid android:color="@color/grey_lighten_30" />
+
+ </shape>
+</inset> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/nux_flat_button_grey_text_selector.xml b/WordPress/src/main/res/drawable/nux_flat_button_grey_text_selector.xml
new file mode 100644
index 000000000..ae88af5c0
--- /dev/null
+++ b/WordPress/src/main/res/drawable/nux_flat_button_grey_text_selector.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/grey_lighten_30" android:state_pressed="true" />
+ <item android:color="@color/nux_grey_button" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/nux_flat_button_selector.xml b/WordPress/src/main/res/drawable/nux_flat_button_selector.xml
new file mode 100644
index 000000000..89138d821
--- /dev/null
+++ b/WordPress/src/main/res/drawable/nux_flat_button_selector.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/translucent_grey_lighten_20" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+ <item>
+ <shape>
+ <solid android:color="@color/blue_wordpress" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+</selector>
diff --git a/WordPress/src/main/res/drawable/nux_flat_button_text_selector.xml b/WordPress/src/main/res/drawable/nux_flat_button_text_selector.xml
new file mode 100644
index 000000000..55fe24bed
--- /dev/null
+++ b/WordPress/src/main/res/drawable/nux_flat_button_text_selector.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/grey_light" android:state_pressed="true" />
+ <item android:color="@color/white" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/nux_primary_button_selector.xml b/WordPress/src/main/res/drawable/nux_primary_button_selector.xml
new file mode 100644
index 000000000..1e16c3711
--- /dev/null
+++ b/WordPress/src/main/res/drawable/nux_primary_button_selector.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/blue_dark" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+
+ <item android:state_enabled="true">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/blue_dark" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+
+ <item android:state_enabled="false">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/blue_medium" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+
+ <item>
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/blue_dark" />
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/page_item_background.xml b/WordPress/src/main/res/drawable/page_item_background.xml
new file mode 100644
index 000000000..f368516aa
--- /dev/null
+++ b/WordPress/src/main/res/drawable/page_item_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ background for items in the pages list - provides left and right borders without top or bottom
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_20" />
+ </shape>
+ </item>
+ <item>
+ <inset
+ android:insetLeft="1dp"
+ android:insetRight="1dp">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </inset>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/passcode_logo.xml b/WordPress/src/main/res/drawable/passcode_logo.xml
new file mode 100644
index 000000000..ebc520635
--- /dev/null
+++ b/WordPress/src/main/res/drawable/passcode_logo.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@drawable/nux_icon_wp"/>
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/people_list_divider.xml b/WordPress/src/main/res/drawable/people_list_divider.xml
new file mode 100644
index 000000000..144b9c108
--- /dev/null
+++ b/WordPress/src/main/res/drawable/people_list_divider.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="@dimen/people_list_divider_left_margin">
+ <shape>
+ <solid android:color="@color/grey_lighten_30"/>
+ <size android:height="@dimen/people_list_divider_height"/>
+ </shape>
+</inset> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/plans_business_active.xml b/WordPress/src/main/res/drawable/plans_business_active.xml
new file mode 100644
index 000000000..42353d59f
--- /dev/null
+++ b/WordPress/src/main/res/drawable/plans_business_active.xml
@@ -0,0 +1,56 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="124dp"
+ android:height="125dp"
+ android:viewportWidth="124.0"
+ android:viewportHeight="125.0">
+ <path
+ android:pathData="M62,62.9m-62,0a62,62 0,1 1,124 0a62,62 0,1 1,-124 0"
+ android:strokeColor="#00000000"
+ android:fillColor="#78DCFA"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M57.4,74.7C57.4,72.2 59.4,70.1 62,70.1L62,37.7L52.4,37.7L52.4,49.8C44.4,53.5 38.9,61.5 38.9,70.9C38.9,75.8 40.4,80.3 43,84L43,84C58.4,106.4 58.4,106.4 60,108.8L60.3,108.8L60.3,79C58.6,78.3 57.4,76.6 57.4,74.7L57.4,74.7L57.4,74.7Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M85.2,70.8C85.2,61.4 79.6,53.4 71.7,49.7L71.7,37.7L62,37.7L62,70.2C64.5,70.2 66.6,72.2 66.6,74.8C66.6,76.7 65.4,78.3 63.7,79L63.7,108.8L64,108.8C65.6,106.4 65.6,106.4 81,84L81,84C83.6,80.2 85.2,75.7 85.2,70.8L85.2,70.8L85.2,70.8Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#E9EFF3"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M60.3,108.8L60.3,109.8L63.7,109.8L63.7,108.8L62,108.8L60.3,108.8L60.3,108.8Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#D3DEE6"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M77.7,32.9L46.2,32.9C43,32.9 40.4,35.5 40.4,38.7L40.4,38.9C40.4,42.1 43,44.7 46.2,44.7L77.7,44.7C80.9,44.7 83.5,42.1 83.5,38.9L83.5,38.8C83.6,35.5 81,32.9 77.7,32.9L77.7,32.9L77.7,32.9Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#0087BE"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M62,32.9L46.2,32.9C43,32.9 40.4,35.5 40.4,38.7L40.4,38.9C40.4,42.1 43,44.7 46.2,44.7L62,44.7L62,32.9L62,32.9L62,32.9Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#00AADC"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M62.1,84.9L62.1,84.5L62.1,84.7L62.1,84.9L62.1,84.9Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#D3DEE6"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M62,84.9L62.1,84.7L62,84.5L62,84.9L62,84.9Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#D3DEE6"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M41.43,4.5C42.54,9.98 43.89,15.44 45.5,20.9L50.5,40L62,40L62,1C54.79,1 47.86,2.23 41.43,4.5L41.43,4.5L41.43,4.5Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#00AADC"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M82.6,4.4C81.48,9.95 80.12,15.47 78.5,20.9L73.5,40L62,40L62,0.9C69.22,0.9 76.15,2.13 82.6,4.4L82.6,4.4L82.6,4.4Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#0087BE"
+ android:strokeWidth="1"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/plans_customize.xml b/WordPress/src/main/res/drawable/plans_customize.xml
new file mode 100644
index 000000000..9ca0b96aa
--- /dev/null
+++ b/WordPress/src/main/res/drawable/plans_customize.xml
@@ -0,0 +1,35 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="303dp"
+ android:height="40dp"
+ android:viewportHeight="40.0"
+ android:viewportWidth="303.0">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M263,8C263,4.99 264.56,1.84 267,0C267,1.69 268.38,4 271,4C274.31,4 277,6.69 277,10C277,10.77 276.84,11.5 276.58,12.18C278.06,13.37 279.53,14.56 280.96,15.8L276.8,19.96C275.56,18.53 274.37,17.05 273.19,15.58C272.51,15.84 271.77,16 271,16C266.58,16 263,12.42 263,8L263,8ZM287.3,21.7L289.98,19.02C291.2,19.62 292.55,20 294,20C298.97,20 303,15.97 303,11C303,9.55 302.62,8.2 302.01,6.99L295,14L291,10L298.01,2.99C296.8,2.38 295.45,2 294,2C289.03,2 285,6.03 285,11C285,12.45 285.38,13.8 285.99,15.01L265,36L269,40L282.7,26.3C286.47,30.16 290.44,33.81 294.65,37.2L297.5,39.5L300.5,36.5L298.2,33.65C294.81,29.44 291.16,25.47 287.3,21.7L287.3,21.7Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+ <path
+ android:fillAlpha="0.7"
+ android:fillColor="#FFFFFF"
+ android:pathData="M194,8C194,4.99 195.56,1.84 198,0C198,1.69 199.38,4 202,4C205.31,4 208,6.69 208,10C208,10.77 207.84,11.5 207.58,12.18C209.06,13.37 210.53,14.56 211.96,15.8L207.8,19.96C206.56,18.53 205.37,17.05 204.19,15.58C203.51,15.84 202.77,16 202,16C197.58,16 194,12.42 194,8L194,8ZM218.3,21.7L220.98,19.02C222.2,19.62 223.55,20 225,20C229.97,20 234,15.97 234,11C234,9.55 233.62,8.2 233.01,6.99L226,14L222,10L229.01,2.99C227.8,2.38 226.45,2 225,2C220.03,2 216,6.03 216,11C216,12.45 216.38,13.8 216.99,15.01L196,36L200,40L213.7,26.3C217.47,30.16 221.44,33.81 225.65,37.2L228.5,39.5L231.5,36.5L229.2,33.65C225.81,29.44 222.16,25.47 218.3,21.7L218.3,21.7Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+ <path
+ android:fillAlpha="0.5"
+ android:fillColor="#FFFFFF"
+ android:pathData="M127,8C127,4.99 128.56,1.84 131,0C131,1.69 132.38,4 135,4C138.31,4 141,6.69 141,10C141,10.77 140.84,11.5 140.58,12.18C142.06,13.37 143.53,14.56 144.96,15.8L140.8,19.96C139.56,18.53 138.37,17.05 137.19,15.58C136.51,15.84 135.77,16 135,16C130.58,16 127,12.42 127,8L127,8ZM151.3,21.7L153.98,19.02C155.2,19.62 156.55,20 158,20C162.97,20 167,15.97 167,11C167,9.55 166.62,8.2 166.01,6.99L159,14L155,10L162.01,2.99C160.8,2.38 159.45,2 158,2C153.03,2 149,6.03 149,11C149,12.45 149.38,13.8 149.99,15.01L129,36L133,40L146.7,26.3C150.47,30.16 154.44,33.81 158.65,37.2L161.5,39.5L164.5,36.5L162.2,33.65C158.81,29.44 155.16,25.47 151.3,21.7L151.3,21.7Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+ <path
+ android:fillAlpha="0.3"
+ android:fillColor="#FFFFFF"
+ android:pathData="M63,8C63,4.99 64.56,1.84 67,0C67,1.69 68.38,4 71,4C74.31,4 77,6.69 77,10C77,10.77 76.84,11.5 76.58,12.18C78.06,13.37 79.53,14.56 80.96,15.8L76.8,19.96C75.56,18.53 74.37,17.05 73.19,15.58C72.51,15.84 71.77,16 71,16C66.58,16 63,12.42 63,8L63,8ZM87.3,21.7L89.98,19.02C91.2,19.62 92.55,20 94,20C98.97,20 103,15.97 103,11C103,9.55 102.62,8.2 102.01,6.99L95,14L91,10L98.01,2.99C96.8,2.38 95.45,2 94,2C89.03,2 85,6.03 85,11C85,12.45 85.38,13.8 85.99,15.01L65,36L69,40L82.7,26.3C86.47,30.16 90.44,33.81 94.65,37.2L97.5,39.5L100.5,36.5L98.2,33.65C94.81,29.44 91.16,25.47 87.3,21.7L87.3,21.7Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+ <path
+ android:fillAlpha="0.1"
+ android:fillColor="#FFFFFF"
+ android:pathData="M0,8C0,4.99 1.56,1.84 4,0C4,1.69 5.38,4 8,4C11.31,4 14,6.69 14,10C14,10.77 13.84,11.5 13.58,12.18C15.06,13.37 16.53,14.56 17.96,15.8L13.8,19.96C12.56,18.53 11.37,17.05 10.19,15.58C9.51,15.84 8.77,16 8,16C3.58,16 0,12.42 0,8L0,8ZM24.3,21.7L26.98,19.02C28.2,19.62 29.55,20 31,20C35.97,20 40,15.97 40,11C40,9.55 39.62,8.2 39.01,6.99L32,14L28,10L35.01,2.99C33.8,2.38 32.45,2 31,2C26.03,2 22,6.03 22,11C22,12.45 22.38,13.8 22.99,15.01L2,36L6,40L19.7,26.3C23.47,30.16 27.44,33.81 31.65,37.2L34.5,39.5L37.5,36.5L35.2,33.65C31.81,29.44 28.16,25.47 24.3,21.7L24.3,21.7Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+</vector>
diff --git a/WordPress/src/main/res/drawable/plans_premium_themes.xml b/WordPress/src/main/res/drawable/plans_premium_themes.xml
new file mode 100644
index 000000000..938d45702
--- /dev/null
+++ b/WordPress/src/main/res/drawable/plans_premium_themes.xml
@@ -0,0 +1,35 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="246dp"
+ android:height="46dp"
+ android:viewportWidth="246.0"
+ android:viewportHeight="46.0">
+ <path
+ android:pathData="M104.6,9.2L104.6,9.2C102.06,9.2 100,11.26 100,13.8L100,41.4C100,43.93 102.07,46 104.6,46L132.2,46C134.74,46 136.8,43.94 136.8,41.4L136.8,41.4L104.6,41.4L104.6,9.2L104.6,9.2ZM141.4,0L113.8,0C111.26,0 109.2,2.06 109.2,4.6L109.2,32.2C109.2,34.74 111.26,36.8 113.8,36.8L141.4,36.8C143.94,36.8 146,34.74 146,32.2L146,4.6C146,2.06 143.94,0 141.4,0L141.4,0ZM129.9,32.2L113.8,32.2L113.8,16.1L129.9,16.1L129.9,32.2L129.9,32.2ZM141.4,32.2L134.5,32.2L134.5,16.1L141.4,16.1L141.4,32.2L141.4,32.2ZM141.4,11.5L113.8,11.5L113.8,4.6L141.4,4.6L141.4,11.5L141.4,11.5Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M52.5,16L52.5,16C51.12,16 50,17.12 50,18.5L50,33.5C50,34.88 51.13,36 52.5,36L67.5,36C68.88,36 70,34.88 70,33.5L70,33.5L52.5,33.5L52.5,16L52.5,16ZM72.5,11L57.5,11C56.12,11 55,12.12 55,13.5L55,28.5C55,29.88 56.12,31 57.5,31L72.5,31C73.88,31 75,29.88 75,28.5L75,13.5C75,12.12 73.88,11 72.5,11L72.5,11ZM66.25,28.5L57.5,28.5L57.5,19.75L66.25,19.75L66.25,28.5L66.25,28.5ZM72.5,28.5L68.75,28.5L68.75,19.75L72.5,19.75L72.5,28.5L72.5,28.5ZM72.5,17.25L57.5,17.25L57.5,13.5L72.5,13.5L72.5,17.25L72.5,17.25Z"
+ android:strokeColor="#00000000"
+ android:fillAlpha="0.5"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M173.5,16L173.5,16C172.12,16 171,17.12 171,18.5L171,33.5C171,34.88 172.13,36 173.5,36L188.5,36C189.88,36 191,34.88 191,33.5L191,33.5L173.5,33.5L173.5,16L173.5,16ZM193.5,11L178.5,11C177.12,11 176,12.12 176,13.5L176,28.5C176,29.88 177.12,31 178.5,31L193.5,31C194.88,31 196,29.88 196,28.5L196,13.5C196,12.12 194.88,11 193.5,11L193.5,11ZM187.25,28.5L178.5,28.5L178.5,19.75L187.25,19.75L187.25,28.5L187.25,28.5ZM193.5,28.5L189.75,28.5L189.75,19.75L193.5,19.75L193.5,28.5L193.5,28.5ZM193.5,17.25L178.5,17.25L178.5,13.5L193.5,13.5L193.5,17.25L193.5,17.25Z"
+ android:strokeColor="#00000000"
+ android:fillAlpha="0.5"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M223.5,16L223.5,16C222.12,16 221,17.12 221,18.5L221,33.5C221,34.88 222.13,36 223.5,36L238.5,36C239.88,36 241,34.88 241,33.5L241,33.5L223.5,33.5L223.5,16L223.5,16ZM243.5,11L228.5,11C227.12,11 226,12.12 226,13.5L226,28.5C226,29.88 227.12,31 228.5,31L243.5,31C244.88,31 246,29.88 246,28.5L246,13.5C246,12.12 244.88,11 243.5,11L243.5,11ZM237.25,28.5L228.5,28.5L228.5,19.75L237.25,19.75L237.25,28.5L237.25,28.5ZM243.5,28.5L239.75,28.5L239.75,19.75L243.5,19.75L243.5,28.5L243.5,28.5ZM243.5,17.25L228.5,17.25L228.5,13.5L243.5,13.5L243.5,17.25L243.5,17.25Z"
+ android:strokeColor="#00000000"
+ android:fillAlpha="0.5"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M2.5,16L2.5,16C1.12,16 0,17.12 0,18.5L0,33.5C0,34.88 1.13,36 2.5,36L17.5,36C18.88,36 20,34.88 20,33.5L20,33.5L2.5,33.5L2.5,16L2.5,16ZM22.5,11L7.5,11C6.12,11 5,12.12 5,13.5L5,28.5C5,29.88 6.12,31 7.5,31L22.5,31C23.88,31 25,29.88 25,28.5L25,13.5C25,12.12 23.88,11 22.5,11L22.5,11ZM16.25,28.5L7.5,28.5L7.5,19.75L16.25,19.75L16.25,28.5L16.25,28.5ZM22.5,28.5L18.75,28.5L18.75,19.75L22.5,19.75L22.5,28.5L22.5,28.5ZM22.5,17.25L7.5,17.25L7.5,13.5L22.5,13.5L22.5,17.25L22.5,17.25Z"
+ android:strokeColor="#00000000"
+ android:fillAlpha="0.5"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/plans_video_upload.xml b/WordPress/src/main/res/drawable/plans_video_upload.xml
new file mode 100644
index 000000000..ed43148c0
--- /dev/null
+++ b/WordPress/src/main/res/drawable/plans_video_upload.xml
@@ -0,0 +1,37 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="170dp"
+ android:height="32dp"
+ android:viewportWidth="170.0"
+ android:viewportHeight="32.0">
+ <path
+ android:pathData="M33.92,9.09L33.92,4.66C33.92,2.21 31.94,0.23 29.49,0.23L5.12,0.23C2.68,0.23 0.69,2.21 0.69,4.66L0.69,26.82C0.69,29.27 2.68,31.25 5.12,31.25L29.49,31.25C31.94,31.25 33.92,29.27 33.92,26.82L33.92,22.39L45,31.25L45,0.23L33.92,9.09L33.92,9.09Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M160,14C159.98,14 159.97,14 159.95,14.01C159.44,7.29 153.84,2 147,2C139.82,2 134,7.82 134,15C134,16.05 134.14,17.06 134.37,18.04C134.25,18.03 134.13,18 134,18C129.58,18 126,21.58 126,26C126,28.4 127.08,30.53 128.76,32L165.95,32C168.39,30.18 170,27.29 170,24C170,18.48 165.52,14 160,14L160,14ZM150,22L150,28L146,28L146,22L140,22L148,12L156,22L150,22L150,22Z"
+ android:strokeColor="#00000000"
+ android:fillColor="#FFFFFF"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M113,16L105.97,16"
+ android:strokeLineCap="square"
+ android:strokeColor="#FFFFFF"
+ android:fillColor="#00000000"
+ android:strokeWidth="1"
+ android:strokeAlpha="0.502745697"/>
+ <path
+ android:pathData="M91,16L83.97,16"
+ android:strokeLineCap="square"
+ android:strokeColor="#FFFFFF"
+ android:fillColor="#00000000"
+ android:strokeWidth="1"
+ android:strokeAlpha="0.502745697"/>
+ <path
+ android:pathData="M70,16L62.97,16"
+ android:strokeLineCap="square"
+ android:strokeColor="#FFFFFF"
+ android:fillColor="#00000000"
+ android:strokeWidth="1"
+ android:strokeAlpha="0.502745697"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/preferences_divider.xml b/WordPress/src/main/res/drawable/preferences_divider.xml
new file mode 100644
index 000000000..b912aac17
--- /dev/null
+++ b/WordPress/src/main/res/drawable/preferences_divider.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:left="-10dp"
+ android:right="-10dp">
+
+ <shape
+ android:shape="rectangle">
+
+ <solid
+ android:color="@color/site_settings_pref_divider_color" />
+
+ </shape>
+
+ </item>
+
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/progress_horizontal_wordpress.xml b/WordPress/src/main/res/drawable/progress_horizontal_wordpress.xml
new file mode 100644
index 000000000..d348b7c2a
--- /dev/null
+++ b/WordPress/src/main/res/drawable/progress_horizontal_wordpress.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@android:id/background"
+ android:drawable="@drawable/progress_bg_wordpress" />
+
+ <item android:id="@android:id/secondaryProgress">
+ <scale android:scaleWidth="100%"
+ android:drawable="@drawable/progress_secondary_wordpress" />
+ </item>
+
+ <item android:id="@android:id/progress">
+ <scale android:scaleWidth="100%"
+ android:drawable="@drawable/progress_primary_wordpress" />
+ </item>
+
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/progressbar_horizontal.xml b/WordPress/src/main/res/drawable/progressbar_horizontal.xml
new file mode 100644
index 000000000..83600e535
--- /dev/null
+++ b/WordPress/src/main/res/drawable/progressbar_horizontal.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape>
+ <solid
+ android:color="@color/blue_dark" />
+ </shape>
+ </clip>
+ </item>
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/reader_button_comment.xml b/WordPress/src/main/res/drawable/reader_button_comment.xml
new file mode 100644
index 000000000..13e8a9ea9
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_button_comment.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/reader_comment_active" android:state_pressed="true" />
+ <item android:drawable="@drawable/reader_comment_active" android:state_selected="true" />
+ <item android:drawable="@drawable/reader_comment" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_button_comment_like.xml b/WordPress/src/main/res/drawable/reader_button_comment_like.xml
new file mode 100644
index 000000000..1a7ebb003
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_button_comment_like.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/noticon_star_disabled" android:state_enabled="false" />
+ <item android:drawable="@drawable/noticon_star_active" android:state_pressed="true" />
+ <item android:drawable="@drawable/noticon_star_active" android:state_selected="true" />
+ <item android:drawable="@drawable/noticon_star_unactive" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_button_like.xml b/WordPress/src/main/res/drawable/reader_button_like.xml
new file mode 100644
index 000000000..4ecba8b75
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_button_like.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/reader_like_empty_active" android:state_pressed="true" />
+ <item android:drawable="@drawable/reader_like" android:state_selected="true" />
+ <item android:drawable="@drawable/reader_like_empty" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_follow.xml b/WordPress/src/main/res/drawable/reader_follow.xml
new file mode 100644
index 000000000..27682a93d
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_follow.xml
@@ -0,0 +1,13 @@
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="@dimen/reader_button_icon"
+ android:width="@dimen/reader_button_icon"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0" >
+
+ <path
+ android:fillColor="@color/reader_follow"
+ android:pathData="M23,19v2h-3v3h-2v-3h-3v-2h3v-3h2v3H23zM20,5v9h-4v3h-3v4H4c-1.1,0 -2,-0.9 -2,-2V5H20zM8,16v-1H4v1H8zM11,13H4v1h7V13zM11,11H4v1h7V11zM18,7H4v2h14V7z" >
+ </path>
+
+</vector>
diff --git a/WordPress/src/main/res/drawable/reader_following.xml b/WordPress/src/main/res/drawable/reader_following.xml
new file mode 100644
index 000000000..e01d82a6c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_following.xml
@@ -0,0 +1,13 @@
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="@dimen/reader_button_icon"
+ android:width="@dimen/reader_button_icon"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0" >
+
+ <path
+ android:fillColor="@color/reader_following"
+ android:pathData="M23,16.5L15.5,24L12,20.4l1.4,-1.4l2.1,2.2l6.1,-6.1L23,16.5zM15.5,18.3l4.5,-4.5V5H2v14c0,1.1 0.9,2 2,2h4.5l4.9,-4.8L15.5,18.3zM8,16H4v-1h4V16zM11,14H4v-1h7V14zM11,12H4v-1h7V12zM18,9H4V7h14V9z" >
+ </path>
+
+</vector>
diff --git a/WordPress/src/main/res/drawable/reader_gap_marker_background.xml b/WordPress/src/main/res/drawable/reader_gap_marker_background.xml
new file mode 100644
index 000000000..ed31d33f4
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_gap_marker_background.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/blue_light" />
+ <corners android:radius="4dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_darken_20" />
+ <corners android:radius="4dp" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_new_posts_bar_background.xml b/WordPress/src/main/res/drawable/reader_new_posts_bar_background.xml
new file mode 100644
index 000000000..46edaa9c9
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_new_posts_bar_background.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@color/color_primary_dark" />
+ <corners android:radius="24dp" />
+
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_photo_count_background.xml b/WordPress/src/main/res/drawable/reader_photo_count_background.xml
new file mode 100644
index 000000000..5e4c821fa
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_photo_count_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ background drawable for count in reader photo viewer
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@color/translucent_grey_dark" />
+ <corners android:radius="36dp"/>
+
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_post_background.xml b/WordPress/src/main/res/drawable/reader_post_background.xml
new file mode 100644
index 000000000..d10c0f143
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_post_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ background drawable for reader post list items
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <stroke android:color="@color/grey_light" android:width="1px"/>
+ <solid android:color="@color/white" />
+
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_related_posts_background.xml b/WordPress/src/main/res/drawable/reader_related_posts_background.xml
new file mode 100644
index 000000000..f4a6cbd94
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_related_posts_background.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <stroke
+ android:width="1dp"
+ android:color="@color/reader_divider_grey" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/reader_tear_repeat.xml b/WordPress/src/main/res/drawable/reader_tear_repeat.xml
new file mode 100644
index 000000000..fe2f74e3d
--- /dev/null
+++ b/WordPress/src/main/res/drawable/reader_tear_repeat.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:dither="true"
+ android:src="@drawable/reader_tear"
+ android:tileMode="repeat" /> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/related_posts_divider.xml b/WordPress/src/main/res/drawable/related_posts_divider.xml
new file mode 100644
index 000000000..16e006514
--- /dev/null
+++ b/WordPress/src/main/res/drawable/related_posts_divider.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@color/grey_lighten_30"
+ android:state_enabled="true" />
+
+ <item
+ android:drawable="@color/grey_lighten_30" />
+
+</selector>
diff --git a/WordPress/src/main/res/drawable/scrollbar_transparent_black.xml b/WordPress/src/main/res/drawable/scrollbar_transparent_black.xml
new file mode 100644
index 000000000..6c92afe4c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/scrollbar_transparent_black.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <solid
+ android:color="@color/translucent_grey" />
+
+ <size
+ android:width="3dp" />
+
+ <stroke
+ android:width="0dp" />
+
+</shape>
diff --git a/WordPress/src/main/res/drawable/scrollbar_transparent_white.xml b/WordPress/src/main/res/drawable/scrollbar_transparent_white.xml
new file mode 100644
index 000000000..debf16b15
--- /dev/null
+++ b/WordPress/src/main/res/drawable/scrollbar_transparent_white.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#aaffffff"/>
+ <size android:width="6dp"/>
+ <stroke android:width="3dp"/>
+</shape>
diff --git a/WordPress/src/main/res/drawable/selectable_background_wordpress.xml b/WordPress/src/main/res/drawable/selectable_background_wordpress.xml
new file mode 100644
index 000000000..fbf846f5c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/selectable_background_wordpress.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:exitFadeDuration="@android:integer/config_mediumAnimTime" >
+ <item android:state_pressed="false" android:state_focused="true" android:drawable="@drawable/list_focused_wordpress" />
+ <item android:state_pressed="true" android:drawable="@drawable/pressed_background_wordpress" />
+ <item android:drawable="@android:color/transparent" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/shape_oval_blue.xml b/WordPress/src/main/res/drawable/shape_oval_blue.xml
new file mode 100644
index 000000000..7740e129a
--- /dev/null
+++ b/WordPress/src/main/res/drawable/shape_oval_blue.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
+ <solid android:color="@color/blue_medium" />
+</shape>
diff --git a/WordPress/src/main/res/drawable/shape_oval_blue_white_stroke.xml b/WordPress/src/main/res/drawable/shape_oval_blue_white_stroke.xml
new file mode 100644
index 000000000..5a632b05c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/shape_oval_blue_white_stroke.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
+ <stroke android:color="@color/grey_light" android:width="2dp" />
+ <solid android:color="@color/blue_medium" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/shape_oval_grey.xml b/WordPress/src/main/res/drawable/shape_oval_grey.xml
new file mode 100644
index 000000000..17e9f5150
--- /dev/null
+++ b/WordPress/src/main/res/drawable/shape_oval_grey.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
+ <stroke android:color="@color/white" android:width="2dp" />
+ <solid android:color="@color/grey_lighten_10" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/shape_oval_grey_light.xml b/WordPress/src/main/res/drawable/shape_oval_grey_light.xml
new file mode 100644
index 000000000..b474551c5
--- /dev/null
+++ b/WordPress/src/main/res/drawable/shape_oval_grey_light.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
+ <solid android:color="@color/grey_light" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/shape_oval_orange.xml b/WordPress/src/main/res/drawable/shape_oval_orange.xml
new file mode 100644
index 000000000..cb2ee46cc
--- /dev/null
+++ b/WordPress/src/main/res/drawable/shape_oval_orange.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
+ <solid android:color="@color/notification_status_unapproved" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/shape_oval_translucent.xml b/WordPress/src/main/res/drawable/shape_oval_translucent.xml
new file mode 100644
index 000000000..bbce5f0ba
--- /dev/null
+++ b/WordPress/src/main/res/drawable/shape_oval_translucent.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
+ <solid android:color="@color/translucent" />
+</shape>
diff --git a/WordPress/src/main/res/drawable/spinner_background_ab_wordpress.xml b/WordPress/src/main/res/drawable/spinner_background_ab_wordpress.xml
new file mode 100644
index 000000000..81a36ee4c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/spinner_background_ab_wordpress.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:drawable="@drawable/spinner_ab_disabled_wordpress" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/spinner_ab_pressed_wordpress" />
+ <item android:state_pressed="false" android:state_focused="true"
+ android:drawable="@drawable/spinner_ab_focused_wordpress" />
+ <item android:drawable="@drawable/spinner_ab_default_wordpress" />
+</selector>
diff --git a/WordPress/src/main/res/drawable/stats_barchart_no_activity_background.xml b/WordPress/src/main/res/drawable/stats_barchart_no_activity_background.xml
new file mode 100644
index 000000000..c267c63e5
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_barchart_no_activity_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+<item>
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@color/grey_lighten_30" />
+ <corners android:radius="4dp" />
+
+ </shape>
+</item>
+
+<item android:top="1dp" android:bottom="1dp" android:left="1dp" android:right="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/white" />
+ <corners android:radius="4dp" />
+ </shape>
+</item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_list_item_background.xml b/WordPress/src/main/res/drawable/stats_list_item_background.xml
new file mode 100644
index 000000000..5a8c7fcf3
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_list_item_background.xml
@@ -0,0 +1,17 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+
+ <item android:top="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_list_item_child_background.xml b/WordPress/src/main/res/drawable/stats_list_item_child_background.xml
new file mode 100644
index 000000000..80d2b3df1
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_list_item_child_background.xml
@@ -0,0 +1,17 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+
+ <item android:top="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_list_item_expanded_background.xml b/WordPress/src/main/res/drawable/stats_list_item_expanded_background.xml
new file mode 100644
index 000000000..80d2b3df1
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_list_item_expanded_background.xml
@@ -0,0 +1,17 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+
+ <item android:top="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_pager_button_blue_light.xml b/WordPress/src/main/res/drawable/stats_pager_button_blue_light.xml
new file mode 100644
index 000000000..bf378034d
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_pager_button_blue_light.xml
@@ -0,0 +1,8 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/stats_button_corner_radius" />
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/stats_pager_button_grey.xml b/WordPress/src/main/res/drawable/stats_pager_button_grey.xml
new file mode 100644
index 000000000..9e27ba36c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_pager_button_grey.xml
@@ -0,0 +1,8 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/stats_button_corner_radius" />
+ <solid android:color="@color/light_gray" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/stats_pager_button_white.xml b/WordPress/src/main/res/drawable/stats_pager_button_white.xml
new file mode 100644
index 000000000..638a4ab66
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_pager_button_white.xml
@@ -0,0 +1,15 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/stats_button_corner_radius" />
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+ <item android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/stats_button_corner_radius" />
+ <solid android:color="@color/white"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/stats_pagination_item_background.xml b/WordPress/src/main/res/drawable/stats_pagination_item_background.xml
new file mode 100644
index 000000000..b9c98ee7c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_pagination_item_background.xml
@@ -0,0 +1,18 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+ <item android:top="1dp" android:bottom="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid
+ android:color="@color/white"/>
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_top_pager_button_selector.xml b/WordPress/src/main/res/drawable/stats_top_pager_button_selector.xml
new file mode 100644
index 000000000..84276dd74
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_top_pager_button_selector.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/stats_pager_button_grey" android:state_pressed="true" />
+ <item android:drawable="@drawable/stats_pager_button_blue_light" android:state_checked="true" />
+ <item android:drawable="@drawable/stats_pager_button_white" />
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_view_all_button_background.xml b/WordPress/src/main/res/drawable/stats_view_all_button_background.xml
new file mode 100644
index 000000000..b564667ae
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_view_all_button_background.xml
@@ -0,0 +1,18 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+ <item android:top="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid
+ android:color="@color/white"/>
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_visitors_and_views_button_blue_light.xml b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_blue_light.xml
new file mode 100644
index 000000000..aab007aae
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_blue_light.xml
@@ -0,0 +1,18 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+
+ <item android:right="1dp" android:bottom="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid
+ android:color="@color/grey_light"/>
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_blue_light.xml b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_blue_light.xml
new file mode 100644
index 000000000..98cc068f2
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_blue_light.xml
@@ -0,0 +1,18 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+
+ <item android:bottom="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid
+ android:color="@color/grey_light"/>
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_white.xml b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_white.xml
new file mode 100644
index 000000000..3b4653054
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_latest_white.xml
@@ -0,0 +1,11 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/white" />
+ </shape>
+ </item>
+
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_visitors_and_views_button_white.xml b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_white.xml
new file mode 100644
index 000000000..c18df5545
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_visitors_and_views_button_white.xml
@@ -0,0 +1,17 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+
+ <item android:right="1dp">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid
+ android:color="@color/white"/>
+ </shape>
+ </item>
+</layer-list>
+
+
diff --git a/WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_primary.xml b/WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_primary.xml
new file mode 100644
index 000000000..5aa2dd12b
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_primary.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/stats_bar_graph_main_series"/>
+ <corners android:radius="@dimen/stats_button_corner_radius"/>
+ <size
+ android:width="@dimen/stats_barchart_legend_item"
+ android:height="@dimen/stats_barchart_legend_item"/>
+ </shape>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_secondary.xml b/WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_secondary.xml
new file mode 100644
index 000000000..a613a5e0d
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_visitors_and_views_legend_background_secondary.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/stats_bar_graph_secondary_series"/>
+ <corners android:radius="@dimen/stats_button_corner_radius"/>
+ <size
+ android:width="@dimen/stats_barchart_legend_item"
+ android:height="@dimen/stats_barchart_legend_item"/>
+ </shape>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_white_background.xml b/WordPress/src/main/res/drawable/stats_white_background.xml
new file mode 100644
index 000000000..1dfa42179
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_white_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:bottom="1dp"
+ android:left="-2dp"
+ android:right="1dp"
+ android:top="-2dp">
+ <shape android:shape="rectangle" >
+ <stroke
+ android:width="1dp"
+ android:color="#DADADA" />
+
+ <solid android:color="#FFF" />
+
+ <padding
+ android:bottom="2dp"
+ android:right="2dp">
+ </padding>
+ </shape>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_widget_background.xml b/WordPress/src/main/res/drawable/stats_widget_background.xml
new file mode 100644
index 000000000..9a0d05036
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_widget_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle" >
+ <stroke
+ android:width="1dp"
+ android:color="#DADADA" />
+
+ <solid android:color="#FFF" />
+
+ <corners android:radius="1dp" />
+
+ <padding
+ android:top="1dp"
+ android:bottom="1dp"
+ android:left="1dp"
+ android:right="1dp">
+ </padding>
+ </shape>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_widget_header_background.xml b/WordPress/src/main/res/drawable/stats_widget_header_background.xml
new file mode 100644
index 000000000..150d5d939
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_widget_header_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item>
+ <shape android:shape="rectangle" >
+ <solid android:color="@color/blue_wordpress" />
+ <corners android:topLeftRadius="1dp"
+ android:topRightRadius="1dp"/>
+ </shape>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/stats_widget_promo_header.xml b/WordPress/src/main/res/drawable/stats_widget_promo_header.xml
new file mode 100644
index 000000000..714028a29
--- /dev/null
+++ b/WordPress/src/main/res/drawable/stats_widget_promo_header.xml
@@ -0,0 +1,24 @@
+<vector android:height="120dp" android:viewportHeight="180.0"
+ android:viewportWidth="420.0" android:width="280dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#02aadc" android:pathData="M-0.2,0h420.2v180h-420.2z"/>
+ <path android:fillColor="#fff" android:pathData="M209.9,68c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c-0.1,0.7 -0.6,1.3 -1.3,1.3ZM209.9,50.1c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c-0.1,0.8 -0.6,1.3 -1.3,1.3ZM209.9,32.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-0.9c0,-3.5 2.9,-6.4 6.4,-6.4c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3c-2.1,0 -3.8,1.7 -3.8,3.8l0,0.9c-0.1,0.7 -0.6,1.3 -1.3,1.3l0,0ZM268.5,26.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c-0.1,0.7 -0.6,1.3 -1.3,1.3ZM250.6,26.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3ZM232.8,26.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c-0,0.7 -0.6,1.3 -1.3,1.3ZM317.1,66.7l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM317.1,84.6l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM317.1,48.9l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,-0.1 -1.3,-0.6 -1.3,-1.3ZM317.1,31l0,-0.9c0,-2.1 -1.7,-3.8 -3.8,-3.8c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3c3.5,0 6.4,2.9 6.4,6.4l0,0.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,0ZM258.5,25c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM276.3,25c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3ZM294.1,25c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3Z"/>
+ <path android:fillColor="#fff" android:pathData="M211.1,91.5l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3ZM211.1,109.4l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3ZM211.1,127.2l0,0.9c0,2.1 1.7,3.8 3.8,3.8c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3c-3.5,0 -6.4,-2.9 -6.4,-6.4l0,-0.9c0,-0.7 0.6,-1.3 1.3,-1.3c0.7,0 1.3,0.6 1.3,1.3l-0,-0ZM269.7,133.2c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.8,0 1.3,0.6 1.3,1.3ZM251.9,133.2c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3ZM234.1,133.2c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3l8.9,0c0.7,0 1.3,0.6 1.3,1.3ZM318.4,92.2c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3ZM318.4,108.1c0.7,0 1.3,0.6 1.3,1.3l0,8.9c0,0.7 -0.6,1.3 -1.3,1.3c-0.7,0 -1.3,-0.6 -1.3,-1.3l0,-8.9c0,-0.7 0.6,-1.3 1.3,-1.3ZM318.4,125.9c0.7,0 1.3,0.6 1.3,1.3l0,0.9c0,3.5 -2.9,6.4 -6.4,6.4c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3c2.1,0 3.8,-1.7 3.8,-3.8l0,-0.9c0,-0.7 0.6,-1.3 1.3,-1.3l-0,0ZM259.8,131.9l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3ZM277.6,131.9l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3ZM295.4,131.9l8.9,0c0.7,0 1.3,0.6 1.3,1.3c0,0.7 -0.6,1.3 -1.3,1.3l-8.9,0c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3Z"/>
+ <path android:fillColor="#fff" android:pathData="M268.9,76.4l-5.6,0l0,-5.6c0,-0.8 -0.6,-1.4 -1.4,-1.4c-0.8,0 -1.4,0.6 -1.4,1.4l0,5.6l-5.6,0c-0.8,0 -1.4,0.6 -1.4,1.4c0,0.8 0.6,1.4 1.4,1.4l5.6,0l0,4.9c0,0.8 0.6,1.4 1.4,1.4c0.8,0 1.4,-0.6 1.4,-1.4l0,-4.9l5.6,0c0.8,0 1.4,-0.6 1.4,-1.4c0,-0.8 -0.7,-1.4 -1.4,-1.4Z"/>
+ <path android:fillColor="#eaeff2" android:pathData="M221.2,148.4l-88.8,0c-4.4,0 -8,-3.6 -8,-8l0,-88.8c0,-4.4 3.6,-8 8,-8l88.8,0c4.4,0 8,3.6 8,8l0,88.8c0,4.4 -3.6,8 -8,8Z"/>
+ <path android:fillColor="#92afc2" android:pathData="M140.5,85.1h20.1v54.4h-20.1z"/>
+ <path android:fillColor="#b1c6d4" android:pathData="M140.5,85.1h10.1v54.4h-10.1z"/>
+ <path android:fillColor="#92afc2" android:pathData="M166.7,59.7h20.1v79.7h-20.1z"/>
+ <path android:fillColor="#b1c6d4" android:pathData="M166.7,59.7h10.1v79.7h-10.1z"/>
+ <path android:fillColor="#92afc2" android:pathData="M193,72.7h20.1v66.7h-20.1z"/>
+ <path android:fillColor="#b1c6d4" android:pathData="M193,72.7h10.1v66.7h-10.1z"/>
+ <path android:fillColor="#4a5668" android:pathData="M229.2,74.2l-10.2,13.2c-14.6,19.2 -23.5,29.6 -31.5,33.4c-6.6,3.1 -11.2,4.5 -21.4,-6.7c-1.2,-1.3 -2.3,-2.6 -3.7,-4.2c-0.5,-0.6 -1.1,-1.2 -1.7,-1.9c-5.3,-6.1 -11.1,-8.9 -17.6,-8.5c-7.4,0.5 -14.2,5.2 -18.7,9.8l0,7.5c4,-4.8 11.2,-11.7 19.1,-12.2c4.9,-0.3 9.2,1.9 13.4,6.7c0.6,0.7 1.1,1.3 1.6,1.9c1.4,1.6 2.5,2.9 3.8,4.3c7,7.6 12.4,10.5 17.7,10.5c3.2,0 6.3,-1 9.8,-2.7c9,-4.2 17.9,-14.6 33.4,-34.9l6.1,-7.9l0,-8.3l-0.1,0l-0,0Z"/>
+ <path android:fillColor="#fff" android:pathData="M177.1,124.6m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
+ <path android:fillColor="#fff" android:pathData="M203.3,110.5m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
+ <path android:fillColor="#fff" android:pathData="M150.9,102.8m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
+ <path android:fillAlpha="0.4" android:fillColor="#495668" android:pathData="M160,141.8l-4.9,38.2l-25.9,0l30.8,-38.2Z"/>
+ <path android:fillColor="#e2d1ae" android:pathData="M140.6,180.1l16.7,-28.9c5.2,-9 2.6,-20.3 -5.7,-25.1c-8.3,-4.8 -19.4,-1.4 -24.6,7.6l-26.8,46.4l40.4,0Z"/>
+ <path android:fillColor="#ece3ce" android:pathData="M152.8,148.3c3.5,-6.1 1.7,-13.8 -4.1,-17.1c-5.8,-3.3 -13.4,-1.1 -16.9,5c-0.1,0.1 -0.1,0.2 -0.2,0.3l-0.1,0.1l-5.1,8.8c-0.7,1.2 -0.2,2.8 1.1,3.6l16.2,9.3c1.3,0.8 3,0.4 3.7,-0.8l5.1,-8.8c0,0 0,-0.1 0.1,-0.1c0,-0.1 0.1,-0.2 0.2,-0.3l0,0Z"/>
+ <path android:fillColor="#fff" android:pathData="M143.6,158.3c0.9,-2.3 -1.6,-5.9 -5.8,-8.4c-4.2,-2.4 -8.6,-2.8 -10.1,-0.9l15.9,9.3Z"/>
+ <path android:fillColor="#cfbc96" android:pathData="M139.8,167.6l-18.8,-10.8c-0.6,-0.4 -0.8,-1.1 -0.5,-1.8c0.4,-0.6 1.1,-0.8 1.8,-0.5l18.8,10.8c0.6,0.4 0.8,1.1 0.5,1.8c-0.4,0.7 -1.2,0.9 -1.8,0.5Z"/>
+ <path android:fillColor="#cfbc96" android:pathData="M137,172.5l-18.8,-10.8c-0.6,-0.4 -0.8,-1.1 -0.5,-1.8c0.4,-0.6 1.1,-0.8 1.8,-0.5l18.8,10.8c0.6,0.4 0.8,1.1 0.5,1.8c-0.4,0.6 -1.2,0.9 -1.8,0.5Z"/>
+</vector>
diff --git a/WordPress/src/main/res/drawable/tab_divider_wordpress.xml b/WordPress/src/main/res/drawable/tab_divider_wordpress.xml
new file mode 100644
index 000000000..8ce871628
--- /dev/null
+++ b/WordPress/src/main/res/drawable/tab_divider_wordpress.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/grey_darken_30" />
+ <size android:width="1px" />
+</shape> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/theme_activate_button_selector.xml b/WordPress/src/main/res/drawable/theme_activate_button_selector.xml
new file mode 100644
index 000000000..ea3994082
--- /dev/null
+++ b/WordPress/src/main/res/drawable/theme_activate_button_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:drawable="@drawable/button_darkgray_focus" android:state_pressed="true"/>
+ <item android:drawable="@drawable/button_darkgray_disabled" android:state_enabled="false"/>
+ <item android:drawable="@drawable/button_darkgray"/>
+
+</selector> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/theme_feature_text_bg.xml b/WordPress/src/main/res/drawable/theme_feature_text_bg.xml
new file mode 100644
index 000000000..7345ac7f3
--- /dev/null
+++ b/WordPress/src/main/res/drawable/theme_feature_text_bg.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+ <corners android:radius="4dp" />
+ <solid android:color="@color/grey_lighten_30"/>
+</shape>
diff --git a/WordPress/src/main/res/drawable/theme_loading.xml b/WordPress/src/main/res/drawable/theme_loading.xml
new file mode 100644
index 000000000..64203c2e5
--- /dev/null
+++ b/WordPress/src/main/res/drawable/theme_loading.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:drawable="@drawable/ic_theme_loading"
+ android:gravity="center" />
+
+ <item
+ android:drawable="@drawable/theme_loading_background" />
+
+</layer-list>
diff --git a/WordPress/src/main/res/drawable/view_post_toolbar.xml b/WordPress/src/main/res/drawable/view_post_toolbar.xml
new file mode 100644
index 000000000..a3f48a67c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/view_post_toolbar.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <padding android:top="1dp" />
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+ <item>
+ <shape>
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/drawable/view_post_toolbar_bottom.xml b/WordPress/src/main/res/drawable/view_post_toolbar_bottom.xml
new file mode 100644
index 000000000..fbc7fef8c
--- /dev/null
+++ b/WordPress/src/main/res/drawable/view_post_toolbar_bottom.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <padding android:bottom="1dp" />
+ <solid android:color="@color/grey_light" />
+ </shape>
+ </item>
+
+ <item>
+ <shape>
+ <solid android:color="@color/grey_lighten_30" />
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout-sw600dp-port/theme_details_fragment.xml b/WordPress/src/main/res/layout-sw600dp-port/theme_details_fragment.xml
new file mode 100644
index 000000000..2862c0edd
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw600dp-port/theme_details_fragment.xml
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="10dp" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="20dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/theme_details_name"
+ android:textSize="22sp"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_attributes_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_name"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="30dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_current_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_current"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:paddingRight="16dp"
+ android:text="@string/theme_current_theme"
+ android:textColor="@color/blue_wordpress"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_premium_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_premium"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_premium_theme"
+ android:textColor="@color/theme_details_premium"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <org.wordpress.android.ui.FadeInNetworkImageView
+ android:id="@+id/theme_details_fragment_image"
+ android:layout_width="300dp"
+ android:layout_height="225dp"
+ android:layout_below="@id/theme_details_fragment_attributes_container"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="12dp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_image"
+ android:layout_marginBottom="18dp"
+ android:layout_marginLeft="30dp"
+ android:layout_marginRight="30dp"
+ android:layout_marginTop="18dp"
+ android:orientation="horizontal" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_preview_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="5dp"
+ android:layout_weight="1"
+ android:background="@drawable/media_blue_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/themes_live_preview"
+ android:textColor="@color/theme_details_button"/>
+
+ <FrameLayout
+ android:id="@+id/theme_details_fragment_activate_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_activate_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/theme_activate_button"
+ android:textColor="@color/theme_details_button"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_activating_progress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone" >
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_activating_button"
+ android:textColor="@color/theme_details_button"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ </FrameLayout>
+
+ <Button
+ android:id="@+id/theme_details_fragment_view_site_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/view_site"
+ android:textColor="@color/theme_details_button"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/theme_details_divider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_button_container" />
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_label"
+ style="@style/ThemeDetailsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_divider"
+ android:layout_marginLeft="15dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_details_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_label"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp"
+ android:textStyle="normal"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_features_label"
+ style="@style/ThemeDetailsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_description"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_features_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_features_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_features_label"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+ </LinearLayout>
+ </RelativeLayout>
+
+</ScrollView>
diff --git a/WordPress/src/main/res/layout-sw600dp/media_gallery_activity.xml b/WordPress/src/main/res/layout-sw600dp/media_gallery_activity.xml
new file mode 100644
index 000000000..ab91de950
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw600dp/media_gallery_activity.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:id="@+id/media_gallery_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:orientation="horizontal" >
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.6" >
+
+ <fragment
+ android:id="@+id/mediaGalleryEditFragment"
+ android:name="org.wordpress.android.ui.media.MediaGalleryEditFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/right_shadow"
+ android:orientation="vertical" />
+ </FrameLayout>
+
+ <fragment
+ android:id="@+id/mediaGallerySettingsFragment"
+ android:name="org.wordpress.android.ui.media.MediaGallerySettingsFragment"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.4" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/media_gallery_add_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+ </FrameLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout-sw600dp/media_grid_progress.xml b/WordPress/src/main/res/layout-sw600dp/media_grid_progress.xml
new file mode 100644
index 000000000..8686ba497
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw600dp/media_grid_progress.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_grid_progress_height"
+ android:background="@color/white"
+ android:gravity="center_horizontal" >
+
+ <ProgressBar
+ android:id="@+id/progressbar"
+ style="?android:attr/progressBarStyleInverse"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_centerVertical="true" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/progressbar"
+ android:gravity="center"
+ android:padding="10dp"
+ android:text="@string/loading"
+ android:textColor="#FF464646"
+ android:textSize="16sp" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout-sw600dp/theme_details_fragment.xml b/WordPress/src/main/res/layout-sw600dp/theme_details_fragment.xml
new file mode 100644
index 000000000..9a855bb2c
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw600dp/theme_details_fragment.xml
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:orientation="horizontal"
+ android:padding="8dp" >
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_left_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="25dp"
+ android:layout_marginRight="25dp"
+ android:layout_weight="0.6"
+ android:gravity="center_horizontal"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:layout_marginTop="20dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/theme_details_name"
+ android:textSize="22sp"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_attributes_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_current_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_current"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:paddingRight="16dp"
+ android:text="@string/theme_current_theme"
+ android:textColor="@color/blue_wordpress"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_premium_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_premium"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_premium_theme"
+ android:textColor="@color/theme_details_premium"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <org.wordpress.android.ui.FadeInNetworkImageView
+ android:id="@+id/theme_details_fragment_image"
+ android:layout_width="300dp"
+ android:layout_height="225dp"
+ android:layout_marginBottom="12dp"
+ android:layout_marginTop="12dp" />
+
+ <Button
+ android:id="@+id/theme_details_fragment_preview_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:background="@drawable/media_blue_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/themes_live_preview"
+ android:textColor="@color/theme_details_button"/>
+
+ <FrameLayout
+ android:id="@+id/theme_details_fragment_activate_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_activate_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/theme_activate_button"
+ android:textColor="@color/theme_details_button"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_activating_progress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone" >
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_activating_button"
+ android:textColor="@color/theme_details_button"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ </FrameLayout>
+
+ <Button
+ android:id="@+id/theme_details_fragment_view_site_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/view_site"
+ android:textColor="@color/theme_details_button"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="0.4"
+ android:fillViewport="true" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_label"
+ style="@style/ThemeDetailsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="15dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_details_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_label"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp"
+ android:textStyle="normal"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_features_label"
+ style="@style/ThemeDetailsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_description"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_features_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_features_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_features_label"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+ </LinearLayout>
+ </RelativeLayout>
+ </ScrollView>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout-sw600dp/theme_grid_cardview_header.xml b/WordPress/src/main/res/layout-sw600dp/theme_grid_cardview_header.xml
new file mode 100644
index 000000000..e1191e719
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw600dp/theme_grid_cardview_header.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginBottom="@dimen/cardview_default_radius"
+ android:layout_marginRight="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:orientation="vertical"
+ android:layout_margin="@dimen/theme_browser_cardview_header_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ android:textAllCaps="true"
+ android:text="@string/current_theme" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/header_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textColor="@color/black"
+ android:text="@string/current_theme" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:orientation="horizontal">
+
+ <View
+ android:layout_width="@dimen/theme_browser_separator_thickness"
+ android:layout_height="match_parent"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/customize"
+ android:layout_width="@dimen/theme_browser_header_button_width"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_none"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/customize"
+ app:srcCompat="@drawable/ic_theme_customize" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_dark"
+ android:text="@string/customize" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="@dimen/theme_browser_separator_thickness"
+ android:layout_height="match_parent"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/details"
+ android:layout_width="@dimen/theme_browser_header_button_width"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_none"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/details"
+ app:srcCompat="@drawable/ic_theme_details" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_dark"
+ android:text="@string/details" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="@dimen/theme_browser_separator_thickness"
+ android:layout_height="match_parent"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/support"
+ android:layout_width="@dimen/theme_browser_header_button_width"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_none"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/support"
+ app:srcCompat="@drawable/ic_theme_support" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_dark"
+ android:text="@string/support" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </RelativeLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout-sw720dp/stats_activity.xml b/WordPress/src/main/res/layout-sw720dp/stats_activity.xml
new file mode 100644
index 000000000..54585e28c
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw720dp/stats_activity.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/toolbar"
+ android:id="@+id/toolbar" />
+
+ <android.support.design.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.AppBarLayout
+ android:layout_height="@dimen/toolbar_subtitle_height"
+ android:layout_width="match_parent">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar_filter"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/FilteredRecyclerViewToolbar"
+ app:layout_scrollFlags="scroll|enterAlways">
+
+ <Spinner
+ style="@style/FilteredRecyclerViewSpinner.WordPress"
+ android:id="@+id/filter_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:overlapAnchor="false"/>
+
+ </android.support.v7.widget.Toolbar>
+
+ </android.support.design.widget.AppBarLayout>
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/grey_lighten_30"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <org.wordpress.android.ui.stats.NestedScrollViewExt
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin"
+ android:id="@+id/scroll_view_stats"
+ android:fillViewport="true">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- Insights -->
+ <LinearLayout
+ android:id="@+id/stats_insights_fragments_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_medium"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <!-- LATEST POST SUMMARY SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_latest_post_summary_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <!-- TODAY SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_today_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <!-- ALL TIME SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_all_time_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <!-- POPULAR DAY AND TIME SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_most_popular_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_other_recent_stats_label_insights"
+ android:textAppearance="?android:attr/textAppearance"
+ android:textSize="@dimen/text_sz_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_marginBottom="@dimen/margin_extra_small"
+ android:textColor="@color/stats_blue_labels"
+ android:gravity="start"
+ android:text="@string/stats_other_recent_stats_label" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <!-- Left column -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:layout_marginEnd="@dimen/margin_medium" >
+ <FrameLayout
+ android:id="@+id/stats_comments_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_tags_and_categories_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+ </LinearLayout>
+
+ <!-- Right column -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginStart="@dimen/margin_medium" >
+ <FrameLayout
+ android:id="@+id/stats_followers_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_publicize_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+
+ <!-- Timeline -->
+ <LinearLayout
+ android:id="@+id/stats_timeline_fragments_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_medium"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/stats_visitors_and_views_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <!-- Left column -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:layout_marginEnd="@dimen/margin_medium" >
+ <FrameLayout
+ android:id="@+id/stats_top_posts_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_referrers_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_clicks_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_top_authors_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ </LinearLayout>
+
+ <!-- Right column -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginStart="@dimen/margin_medium" >
+ <FrameLayout
+ android:id="@+id/stats_geoviews_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_search_terms_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_video_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_other_recent_stats_label_timeline"
+ android:textAppearance="?android:attr/textAppearance"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:textColor="@color/stats_blue_labels"
+ android:gravity="start"
+ android:textSize="@dimen/text_sz_large"
+ android:text="@string/stats_other_recent_stats_label" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_other_recent_stats_moved"
+ android:textAppearance="?android:attr/textAppearance"
+ android:background="?android:selectableItemBackground"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_small"
+ android:textColor="@color/stats_link_text_color"
+ android:gravity="start"
+ android:textSize="@dimen/text_sz_small"
+ android:text="@string/stats_other_recent_stats_moved_label" />
+
+ </LinearLayout>
+ </LinearLayout>
+ </org.wordpress.android.ui.stats.NestedScrollViewExt>
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+ </android.support.design.widget.CoordinatorLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout-sw720dp/stats_insights_all_time_item.xml b/WordPress/src/main/res/layout-sw720dp/stats_insights_all_time_item.xml
new file mode 100644
index 000000000..8425ea4a8
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw720dp/stats_insights_all_time_item.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:baselineAligned="false"
+ android:padding="@dimen/margin_extra_large"
+ android:orientation="horizontal">
+
+ <!-- Posts item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/stats_visitors_and_views_tab_icon"
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/my_site_icon_posts" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/posts" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_posts"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ </LinearLayout>
+
+ <include layout="@layout/stats_vertical_line" />
+
+ <!-- Views Item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/stats_icon_views" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_views" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_views"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ </LinearLayout>
+
+ <include layout="@layout/stats_vertical_line" />
+
+ <!-- Visitors Item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/stats_icon_visitors" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_visitors" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_visitors"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ </LinearLayout>
+
+ <include layout="@layout/stats_vertical_line" />
+
+ <!-- Best Ever Item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/stats_icon_trophy" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:textColor="@color/alert_yellow"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_insights_best_ever" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_bestever"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_bestever_date"
+ style="@style/StatsInsightsLabel"
+ android:textColor="@color/grey_darken_10"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ tools:text="August 11, 2012" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout-sw720dp/theme_details_fragment.xml b/WordPress/src/main/res/layout-sw720dp/theme_details_fragment.xml
new file mode 100644
index 000000000..ecff2589a
--- /dev/null
+++ b/WordPress/src/main/res/layout-sw720dp/theme_details_fragment.xml
@@ -0,0 +1,195 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:orientation="horizontal"
+ android:padding="8dp" >
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.5" >
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_attributes_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_name"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="30dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_current_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_current"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:paddingRight="16dp"
+ android:text="@string/theme_current_theme"
+ android:textColor="@color/blue_wordpress"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_premium_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_premium"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_premium_theme"
+ android:textColor="@color/theme_details_premium"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <org.wordpress.android.ui.FadeInNetworkImageView
+ android:id="@+id/theme_details_fragment_image"
+ android:layout_width="300dp"
+ android:layout_height="225dp"
+ android:layout_below="@id/theme_details_fragment_attributes_container"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="12dp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_image"
+ android:layout_marginBottom="16dp"
+ android:layout_marginLeft="25dp"
+ android:layout_marginRight="25dp"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_preview_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="5dp"
+ android:layout_weight="1"
+ android:background="@drawable/media_blue_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/themes_live_preview"
+ android:textColor="@color/theme_details_button"/>
+
+ <FrameLayout
+ android:id="@+id/theme_details_fragment_activate_button_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_activate_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/theme_activate_button"
+ android:textColor="@color/theme_details_button"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_activating_progress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone" >
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_activating_button"
+ android:textColor="@color/theme_details_button"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ </FrameLayout>
+
+ <Button
+ android:id="@+id/theme_details_fragment_view_site_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/view_site"
+ android:textColor="@color/theme_details_button"
+ android:visibility="gone"/>
+ </LinearLayout>
+ </RelativeLayout>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="0.5"
+ android:fillViewport="true" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_label"
+ style="@style/ThemeDetailsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="15dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_details_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_label"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp"
+ android:textStyle="normal"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_features_label"
+ style="@style/ThemeDetailsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_description"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_features_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_features_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_features_label"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+ </LinearLayout>
+ </RelativeLayout>
+ </ScrollView>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/about_activity.xml b/WordPress/src/main/res/layout/about_activity.xml
new file mode 100644
index 000000000..4ddbc2125
--- /dev/null
+++ b/WordPress/src/main/res/layout/about_activity.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/main_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/nux_background"
+ android:fillViewport="true">
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ app:maxWidth="@dimen/nux_width"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:id="@+id/nux_fragment_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ app:srcCompat="@drawable/nux_icon_wp"
+ android:layout_marginTop="@dimen/margin_medium"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXTitle"
+ android:id="@+id/about_first_line"
+ android:text="@string/app_title"
+ android:fontFamily="sans-serif-light"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:layout_marginBottom="@dimen/margin_small"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:id="@+id/about_version"
+ android:text="@string/version"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:id="@+id/about_publisher"
+ android:text="@string/publisher"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:id="@+id/about_copyright"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:id="@+id/about_privacy"
+ android:text="@string/privacy_policy"
+ android:layout_width="match_parent"
+ android:gravity="center"
+ android:layout_marginTop="@dimen/margin_medium"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:id="@+id/about_tos"
+ android:text="@string/tos"
+ android:layout_width="match_parent"
+ android:gravity="center"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:id="@+id/about_url"
+ android:text="@string/automattic_url"
+ android:clickable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginBottom="@dimen/margin_medium"/>
+ </LinearLayout>
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/actionbar_add_media.xml b/WordPress/src/main/res/layout/actionbar_add_media.xml
new file mode 100644
index 000000000..64885ccc6
--- /dev/null
+++ b/WordPress/src/main/res/layout/actionbar_add_media.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/dialog_full_holo_light"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="15dp"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp"
+ android:paddingTop="15dp"
+ android:text="@string/media_add_popup_title"
+ android:textSize="20sp" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="@color/blue_wordpress" />
+
+ <ListView
+ android:id="@+id/actionbar_add_media_listview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@color/blue_wordpress"
+ android:dividerHeight="1dp"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp" >
+ </ListView>
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/actionbar_add_media_cell.xml b/WordPress/src/main/res/layout/actionbar_add_media_cell.xml
new file mode 100644
index 000000000..0aafbdede
--- /dev/null
+++ b/WordPress/src/main/res/layout/actionbar_add_media_cell.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="15dp"
+ android:paddingTop="15dp"
+ android:textSize="16sp" >
+
+</TextView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/add_category.xml b/WordPress/src/main/res/layout/add_category.xml
new file mode 100644
index 000000000..f5c08cc03
--- /dev/null
+++ b/WordPress/src/main/res/layout/add_category.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_large">
+
+ <!-- Category name -->
+ <EditText
+ android:id="@+id/category_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/category_name"
+ android:inputType="text" />
+
+ <!-- Category slug -->
+ <EditText
+ android:id="@+id/category_slug"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/category_name"
+ android:hint="@string/category_slug"
+ android:inputType="text" />
+
+ <!-- Category description -->
+ <EditText
+ android:id="@+id/category_desc"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/category_slug"
+ android:hint="@string/category_desc"
+ android:inputType="text" />
+
+ <!-- Parent category -->
+ <TextView
+ android:id="@+id/parentDescLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/category_desc"
+ android:text="@string/category_parent" />
+
+ <Spinner
+ android:id="@+id/parent_category"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_below="@id/parentDescLabel"
+ android:layout_marginBottom="@dimen/margin_large" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/parent_category"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/cancel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/cancel" />
+
+ <Button
+ android:id="@+id/ok"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/ok" />
+
+ </LinearLayout>
+
+ </RelativeLayout>
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/add_quickpress_shortcut.xml b/WordPress/src/main/res/layout/add_quickpress_shortcut.xml
new file mode 100644
index 000000000..adf481edc
--- /dev/null
+++ b/WordPress/src/main/res/layout/add_quickpress_shortcut.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#FFEEEEEE">
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="@drawable/list_divider"
+ android:dividerHeight="1px"
+ android:cacheColorHint="#00000000" />
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/alert_http_auth.xml b/WordPress/src/main/res/layout/alert_http_auth.xml
new file mode 100644
index 000000000..e76212a4e
--- /dev/null
+++ b/WordPress/src/main/res/layout/alert_http_auth.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:paddingBottom="16dp">
+ <EditText android:id="@+id/http_username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLength="@integer/max_length_username"
+ android:hint="@string/httpuser" />
+ <EditText android:id="@+id/http_password"
+ android:inputType="textPassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLength="@integer/max_length_password"
+ android:hint="@string/httppassword" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/blog_preferences.xml b/WordPress/src/main/res/layout/blog_preferences.xml
new file mode 100644
index 000000000..56aae7516
--- /dev/null
+++ b/WordPress/src/main/res/layout/blog_preferences.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/mainRL"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/grey_light"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/settings"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/settings_padding"
+ android:paddingRight="@dimen/settings_padding">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:id="@+id/sectionContent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/l_section1"
+ style="@style/WordPressSettingsSectionHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/account_details" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/username"
+ android:maxLength="@integer/max_length_username"
+ android:singleLine="true" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password"
+ android:inputType="textPassword"
+ android:maxLength="@integer/max_length_password"
+ android:singleLine="true" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/l_httpuser"
+ style="@style/WordPressSubHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/http_credentials" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/httpuser"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:maxLength="@integer/max_length_username"
+ android:hint="@string/httpuser" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/httppassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textPassword"
+ android:singleLine="true"
+ android:maxLength="@integer/max_length_password"
+ android:hint="@string/httppassword" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/sectionTags"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/l_section2"
+ style="@style/WordPressSettingsSectionHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/media" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/l_maxImageWidth"
+ style="@style/WordPressSubHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/max_thumbnail_px_width" />
+
+ <Spinner
+ android:id="@+id/maxImageWidth"
+ android:layout_marginTop="@dimen/margin_small"
+ android:layout_marginBottom="@dimen/margin_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <org.wordpress.android.widgets.WPCheckBox
+ android:id="@+id/fullSizeImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_dark"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:text="@string/upload_full_size_image" />
+
+ <org.wordpress.android.widgets.WPCheckBox
+ android:id="@+id/scaledImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_dark"
+ android:text="@string/upload_scaled_image" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/l_scaledImage"
+ style="@style/WordPressSubHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/scaled_image" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/scaledImageWidth"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLength="4"
+ android:inputType="number"
+ android:singleLine="true" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPButton
+ android:id="@+id/remove_account"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_account" />
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/categories_row.xml b/WordPress/src/main/res/layout/categories_row.xml
new file mode 100644
index 000000000..489b513c4
--- /dev/null
+++ b/WordPress/src/main/res/layout/categories_row.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.wordpress.android.widgets.CheckedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/category_row_height"
+ android:orientation="horizontal"
+ android:id="@+id/categoryRow"
+ android:clickable="false">
+ <ImageView android:layout_width="@dimen/category_row_height"
+ android:layout_height="@dimen/category_row_height"
+ app:srcCompat="@drawable/ic_level_indicator"
+ android:id="@+id/categoryRowLevelIndicator" android:scaleType="fitEnd"/>
+ <CheckedTextView
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:gravity="center_vertical"
+ android:textColor="#464646"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
+ android:id="@+id/categoryRowText"/>
+</org.wordpress.android.widgets.CheckedLinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/categories_row_parent.xml b/WordPress/src/main/res/layout/categories_row_parent.xml
new file mode 100644
index 000000000..06a0d6a7d
--- /dev/null
+++ b/WordPress/src/main/res/layout/categories_row_parent.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/category_parent_spinner_row_height"
+ android:orientation="horizontal"
+ android:id="@+id/categoryRow"
+ android:clickable="false">
+ <ImageView android:layout_width="@dimen/category_row_height"
+ android:layout_height="@dimen/category_row_height"
+ app:srcCompat="@drawable/ic_level_indicator"
+ android:id="@+id/categoryRowLevelIndicator" android:scaleType="fitEnd"/>
+ <TextView
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:gravity="center_vertical"
+ android:textColor="#464646"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
+ android:id="@+id/categoryRowText"/>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/category_button.xml b/WordPress/src/main/res/layout/category_button.xml
new file mode 100644
index 000000000..de5bec2a6
--- /dev/null
+++ b/WordPress/src/main/res/layout/category_button.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/categoryButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:drawableRight="@drawable/ic_close_grey600_24dp"
+ android:padding="8dp"
+ android:textColor="@color/grey_dark" />
diff --git a/WordPress/src/main/res/layout/category_select_button.xml b/WordPress/src/main/res/layout/category_select_button.xml
new file mode 100644
index 000000000..44856a2d5
--- /dev/null
+++ b/WordPress/src/main/res/layout/category_select_button.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/selectCategories"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:clickable="true"
+ android:drawableEnd="@drawable/ic_add_grey600_24dp"
+ android:drawableRight="@drawable/ic_add_grey600_24dp"
+ android:minHeight="48dp"
+ android:minWidth="48dp"
+ android:paddingRight="12dp"
+ android:contentDescription="@string/add_category"
+ android:tag="select-category" />
diff --git a/WordPress/src/main/res/layout/comment_action_footer.xml b/WordPress/src/main/res/layout/comment_action_footer.xml
new file mode 100644
index 000000000..5294df243
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_action_footer.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/layout_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <!-- like and moderate buttons don't use a compound drawable so the icon can be animated when tapped -->
+ <LinearLayout
+ android:id="@+id/btn_like"
+ android:background="@drawable/moderate_button_selector"
+ android:paddingRight="@dimen/margin_small"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_medium"
+ android:gravity="center"
+ android:clickable="true"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <ImageView
+ android:id="@+id/btn_like_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_small"
+ app:srcCompat="@drawable/ic_action_like"
+ android:contentDescription="@string/reader_label_like" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/btn_like_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-4dp"
+ android:text="@string/reader_label_like"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/btn_moderate"
+ android:background="@drawable/moderate_button_selector"
+ android:paddingRight="@dimen/margin_small"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_medium"
+ android:gravity="center"
+ android:clickable="true"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <ImageView
+ android:id="@+id/btn_moderate_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_small"
+ app:srcCompat="@drawable/ic_action_approve"
+ android:contentDescription="@string/reader_label_like" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/btn_moderate_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-4dp"
+ android:text="@string/mnu_comment_approve"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/image_trash_comment"
+ style="@style/WordPress.ModerateButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/ic_action_trash"
+ android:text="@string/mnu_comment_trash" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_btn_spam"
+ style="@style/WordPress.ModerateButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/ic_action_spam"
+ android:text="@string/mnu_comment_spam" />
+ </LinearLayout>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/comment_activity.xml b/WordPress/src/main/res/layout/comment_activity.xml
new file mode 100644
index 000000000..002fecbe1
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_activity.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include
+ layout="@layout/toolbar"
+ android:id="@+id/toolbar" />
+
+ <FrameLayout
+ android:id="@+id/layout_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.wordpress.android.ui.comments.CommentsActivity"/>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/comment_activity_detail.xml b/WordPress/src/main/res/layout/comment_activity_detail.xml
new file mode 100644
index 000000000..dc4d440e2
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_activity_detail.xml
@@ -0,0 +1,6 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/comment_detail_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".CommentDetailActivity" />
diff --git a/WordPress/src/main/res/layout/comment_detail_fragment.xml b/WordPress/src/main/res/layout/comment_detail_fragment.xml
new file mode 100644
index 000000000..4e1341100
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_detail_fragment.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ comment detail displayed from both the notification list and the comment list
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/grey_light">
+
+ <ScrollView
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/layout_bottom"
+ android:fillViewport="true">
+
+ <LinearLayout
+ android:id="@+id/comment_content_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/comment_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_large"
+ android:background="@color/white">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_avatar"
+ style="@style/ReaderImageView.Avatar"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_small" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/text_date"
+ android:layout_toRightOf="@+id/image_avatar"
+ android:background="?android:selectableItemBackground"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/reader_hyperlink"
+ android:textSize="@dimen/text_sz_extra_large"
+ tools:text="text_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_post_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@+id/text_name"
+ android:layout_below="@+id/text_name"
+ android:layout_toLeftOf="@+id/text_status"
+ android:background="?android:selectableItemBackground"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:paddingBottom="@dimen/margin_extra_small"
+ android:paddingTop="@dimen/margin_extra_small"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="text_post_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_date"
+ style="@style/ReaderTextView.Date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginTop="@dimen/margin_small"
+ android:maxLines="1"
+ tools:text="date" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignTop="@+id/text_post_title"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginTop="@dimen/margin_small"
+ android:textSize="@dimen/text_sz_small"
+ android:visibility="visible"
+ tools:text="STATUS" />
+ </RelativeLayout>
+
+ <!--
+ textIsSelectable is set to false here to avoid
+ https://code.google.com/p/android/issues/detail?id=30961
+ -->
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:textColor="@color/grey_dark"
+ android:textColorLink="@color/reader_hyperlink"
+ android:background="@color/white"
+ android:textIsSelectable="false"
+ android:textSize="@dimen/text_sz_large"
+ app:wpFontFamily="merriweather"
+ tools:text="text_content" />
+
+ <View android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+ </LinearLayout>
+ </ScrollView>
+
+ <LinearLayout
+ android:id="@+id/layout_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/layout_comment_box"
+ layout="@layout/reader_include_comment_box"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/progress_loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/comment_edit_activity.xml b/WordPress/src/main/res/layout/comment_edit_activity.xml
new file mode 100644
index 000000000..83b8f504f
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_edit_activity.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true"
+ android:clipToPadding="false"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_large"
+ android:background="@color/white"
+ android:scrollbarStyle="outsideInset" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/edit_comment_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/author_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:hint="@string/author_name"
+ android:inputType="textPersonName|textNoSuggestions"
+ android:singleLine="true" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/author_email"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:hint="@string/author_email"
+ android:inputType="textEmailAddress"
+ android:singleLine="true" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/author_url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:hint="@string/author_url"
+ android:inputType="textUri"
+ android:singleLine="true" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/edit_comment_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="top"
+ android:hint="@string/hint_comment_content"
+ android:inputType="textCapSentences|textAutoCorrect|textMultiLine"
+ android:minLines="4"
+ android:textColorLink="@color/reader_hyperlink" />
+
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/edit_comment_progress"
+ style="@android:style/Widget.Holo.Light.ProgressBar.Large"
+ android:layout_centerInParent="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:indeterminate="true"
+ android:visibility="gone"/>
+
+ </RelativeLayout>
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/comment_list_fragment.xml b/WordPress/src/main/res/layout/comment_list_fragment.xml
new file mode 100644
index 000000000..6734fcf8f
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_list_fragment.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.ui.FilteredRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/filtered_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
diff --git a/WordPress/src/main/res/layout/comment_listitem.xml b/WordPress/src/main/res/layout/comment_listitem.xml
new file mode 100644
index 000000000..6a73b382d
--- /dev/null
+++ b/WordPress/src/main/res/layout/comment_listitem.xml
@@ -0,0 +1,116 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin"
+ android:paddingTop="@dimen/margin_large">
+
+ <FrameLayout
+ android:id="@+id/frame_avatar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_extra_small">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/avatar"
+ android:layout_width="@dimen/notifications_avatar_sz"
+ android:layout_height="@dimen/notifications_avatar_sz" />
+
+ <ImageView
+ android:id="@+id/image_checkmark"
+ android:layout_width="@dimen/notifications_avatar_sz"
+ android:layout_height="@dimen/notifications_avatar_sz"
+ android:background="@drawable/shape_oval_blue"
+ android:padding="@dimen/margin_medium"
+ app:srcCompat="@drawable/ic_check_white_24dp"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:id="@+id/moderate_progress"
+ android:layout_width="@dimen/notifications_avatar_sz"
+ android:layout_height="@dimen/notifications_avatar_sz"
+ android:background="@drawable/shape_oval_translucent"
+ android:gravity="center"
+ android:padding="@dimen/margin_large"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <ProgressBar
+ style="@android:style/Widget.Holo.ProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:indeterminate="true" />
+ </LinearLayout>
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/layout_date_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_small"
+ android:gravity="right"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_date"
+ style="@style/ReaderTextView.Date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:text="date" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_small"
+ android:textAllCaps="true"
+ android:textSize="@dimen/text_sz_small"
+ android:visibility="gone"
+ tools:text="status"
+ tools:visibility="visible" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/layout_date_status"
+ android:layout_toRightOf="@+id/frame_avatar"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/title"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_extra_small"
+ android:layout_toRightOf="@+id/frame_avatar"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="comment" />
+
+ </RelativeLayout>
+
+</FrameLayout>
diff --git a/WordPress/src/main/res/layout/create_blog_fragment.xml b/WordPress/src/main/res/layout/create_blog_fragment.xml
new file mode 100644
index 000000000..b9933855a
--- /dev/null
+++ b/WordPress/src/main/res/layout/create_blog_fragment.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ app:maxWidth="600dp"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:gravity="center"
+ android:layout_weight="1"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/create_account_label"
+ android:text="@string/create_new_blog_wpcom"
+ android:paddingBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ style="@style/WordPress.NUXTitle"
+ android:fontFamily="sans-serif-light"
+ app:fixWidowWords="true"/>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:id="@+id/relativeLayout">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/site_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@style/WordPress.NUXEditText"
+ android:inputType="textCapSentences"
+ android:hint="@string/title"
+ app:persistenceEnabled="true"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView2"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_edit"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="10dp"
+ android:tint="@color/grey_darken_10"/>
+ </RelativeLayout>
+
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:layout_marginBottom="16dp">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/site_url"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/add_account_blog_url"
+ android:inputType="textUri"
+ android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ android:clickable="true"
+ android:paddingLeft="0dp"
+ android:paddingTop="12dp"
+ android:paddingRight="0dp"
+ android:paddingBottom="12dp"
+ android:layout_toLeftOf="@+id/textView"
+ app:persistenceEnabled="true"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/dot_wordpress_dot_com_url"
+ android:id="@+id/textView"
+ android:layout_alignParentEnd="false"
+ android:layout_marginTop="4dp"
+ android:layout_alignParentStart="false"
+ android:layout_alignParentTop="false"
+ android:paddingRight="8dp"
+ android:enabled="false"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="4dp"
+ android:textColor="@color/grey_darken_10"
+ android:textIsSelectable="true"
+ android:textSize="@dimen/nux_edit_field_font_size"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_admin_site"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="10dp"
+ android:tint="@color/grey_darken_10"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:id="@+id/signup_button"
+ android:layout_width="match_parent"
+ android:text="@string/create_new_blog_wpcom"
+ android:clickable="true"
+ android:gravity="center"
+ android:enabled="false"/>
+
+ <RelativeLayout
+ android:id="@+id/nux_sign_in_progress_bar"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:enabled="false">
+
+ <ProgressBar
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_centerVertical="true"
+ android:layout_centerHorizontal="true" />
+ </RelativeLayout>
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXFlatButton"
+ android:id="@+id/nux_sign_in_progress_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textColor="@color/blue_light"
+ android:gravity="center|top"/>
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="bottom">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXFlatButton"
+ android:id="@+id/cancel_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:text="@string/cancel"
+ android:gravity="center|top"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+ </LinearLayout>
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/date_range_dialog.xml b/WordPress/src/main/res/layout/date_range_dialog.xml
new file mode 100644
index 000000000..cf6108e8b
--- /dev/null
+++ b/WordPress/src/main/res/layout/date_range_dialog.xml
@@ -0,0 +1,53 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:paddingTop="10dp"
+ android:text="@string/date_range_start_date"
+ android:textSize="18sp"
+ android:textStyle="bold"/>
+
+ <DatePicker
+ android:id="@+id/dpStartDate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:calendarViewShown="false"
+ android:datePickerMode="spinner"
+ tools:targetApi="LOLLIPOP"/>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:layout_margin="10dp"
+ android:background="@android:color/darker_gray"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/date_range_end_date"
+ android:textSize="18sp"
+ android:textStyle="bold"/>
+
+ <DatePicker
+ android:id="@+id/dpEndDate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:calendarViewShown="false"
+ android:datePickerMode="spinner"
+ tools:targetApi="LOLLIPOP"/>
+ </LinearLayout>
+
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/delete_site_dialog.xml b/WordPress/src/main/res/layout/delete_site_dialog.xml
new file mode 100644
index 000000000..cd0205a72
--- /dev/null
+++ b/WordPress/src/main/res/layout/delete_site_dialog.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/url_confirmation"
+ android:inputType="textUri"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/start_over_url_margin"
+ android:layout_marginRight="@dimen/start_over_url_margin" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/detail_list_preference.xml b/WordPress/src/main/res/layout/detail_list_preference.xml
new file mode 100644
index 000000000..2a5ff13ce
--- /dev/null
+++ b/WordPress/src/main/res/layout/detail_list_preference.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:background="@color/white"
+ android:paddingLeft="@dimen/dlp_padding_start"
+ android:paddingStart="@dimen/dlp_padding_start"
+ android:paddingRight="@dimen/dlp_padding_end"
+ android:paddingEnd="@dimen/dlp_padding_end"
+ android:paddingTop="@dimen/dlp_padding_top"
+ android:paddingBottom="@dimen/dlp_padding_bottom">
+
+ <RadioButton
+ android:buttonTint="@color/dialog_compound_button"
+ android:id="@+id/radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:targetApi="LOLLIPOP"/>
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginLeft="@dimen/dlp_text_margin_start"
+ android:layout_marginStart="@dimen/dlp_text_margin_start"
+ android:layout_marginRight="@dimen/dlp_text_margin_end"
+ android:layout_marginEnd="@dimen/dlp_text_margin_end"
+ android:layout_centerInParent="true"
+ android:layout_toRightOf="@id/radio">
+
+ <TextView
+ android:id="@+id/main_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_sz_large"
+ android:textColor="@color/grey_dark"
+ android:gravity="left"
+ tools:text="Main Text" />
+
+ <TextView
+ android:id="@+id/detail_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_sz_small"
+ android:textColor="@color/grey_darken_10"
+ android:gravity="left"
+ tools:text="Detail Text" />
+
+ </LinearLayout>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/detail_list_preference_title.xml b/WordPress/src/main/res/layout/detail_list_preference_title.xml
new file mode 100644
index 000000000..48963d4a0
--- /dev/null
+++ b/WordPress/src/main/res/layout/detail_list_preference_title.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="@color/white"
+ android:paddingTop="@dimen/dlp_title_padding_top"
+ android:paddingBottom="@dimen/dlp_title_padding_bottom"
+ android:gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/Calypso.TextAppearance.AlertDialog"
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/domain_removal_preference.xml b/WordPress/src/main/res/layout/domain_removal_preference.xml
new file mode 100644
index 000000000..697d74cd0
--- /dev/null
+++ b/WordPress/src/main/res/layout/domain_removal_preference.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/widget_frame"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/domain"
+ android:paddingLeft="@dimen/start_over_preference_margin_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/primary_domain" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/edit_post_preview_fragment.xml b/WordPress/src/main/res/layout/edit_post_preview_fragment.xml
new file mode 100644
index 000000000..9925916fd
--- /dev/null
+++ b/WordPress/src/main/res/layout/edit_post_preview_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@color/white">
+
+ <FrameLayout
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent">
+
+ <WebView
+ android:id="@+id/post_preview_webview"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+
+ <TextView
+ android:id="@+id/post_preview_textview"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#FFFFFF"
+ android:padding="@dimen/margin_extra_large"
+ android:textSize="@dimen/text_sz_large" />
+ </FrameLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/edit_post_settings_fragment.xml b/WordPress/src/main/res/layout/edit_post_settings_fragment.xml
new file mode 100644
index 000000000..ef7cffbdb
--- /dev/null
+++ b/WordPress/src/main/res/layout/edit_post_settings_fragment.xml
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/settings_fragment_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_medium"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/statusLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/status" />
+
+ <Spinner
+ android:id="@+id/status"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/pubDateLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/publish_date" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/pubDate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:background="@drawable/selectable_background_wordpress"
+ android:text="@string/immediately"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/postFormatLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/post_format" />
+
+ <Spinner
+ android:id="@+id/postFormat"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/post_format" />
+
+ <LinearLayout
+ android:id="@+id/sectionTags"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/categoryLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:text="@string/categories" />
+
+ <org.wordpress.android.widgets.FlowLayout
+ android:id="@+id/sectionCategories"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <org.wordpress.android.widgets.SuggestionAutoCompleteText
+ android:id="@+id/tags"
+ android:layout_width="match_parent"
+ android:layout_marginTop="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginRight="@dimen/margin_small"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_sz_large"
+ android:hint="@string/tags_separate_with_commas"
+ android:inputType="textAutoCorrect" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/postExcerpt"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginRight="@dimen/margin_small"
+ android:textSize="@dimen/text_sz_large"
+ android:gravity="top"
+ android:hint="@string/post_excerpt"
+ android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
+ android:minLines="1" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/post_password"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginRight="@dimen/margin_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_sz_large"
+ android:hint="@string/post_password"
+ android:maxLength="@integer/max_length_password"
+ android:inputType="textPassword" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/featuredImageLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/editor_post_settings_featured_image" />
+
+ <Button
+ android:id="@+id/addFeaturedImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/editor_post_settings_set_featured_image"/>
+
+ <com.android.volley.toolbox.NetworkImageView
+ android:id="@+id/featuredImage"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="fitStart"
+ android:minHeight="@dimen/post_settings_featured_image_height_min"
+ android:maxHeight="@dimen/post_settings_featured_image_height_max"
+ android:visibility="gone"/>
+
+ <ViewStub
+ android:id="@+id/stub_post_location_settings"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout="@layout/post_location_settings" />
+
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/endlist_indicator.xml b/WordPress/src/main/res/layout/endlist_indicator.xml
new file mode 100644
index 000000000..dafc90904
--- /dev/null
+++ b/WordPress/src/main/res/layout/endlist_indicator.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ appears below the last item in the page list
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/endlist_indicator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ tools:background="@color/white">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:background="@color/grey_lighten_20" />
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginRight="@dimen/margin_medium"
+ app:srcCompat="@drawable/endlist_logo" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:background="@color/grey_lighten_20" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/filter_spinner_item.xml b/WordPress/src/main/res/layout/filter_spinner_item.xml
new file mode 100644
index 000000000..82db5a4c6
--- /dev/null
+++ b/WordPress/src/main/res/layout/filter_spinner_item.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/text"
+ style="@style/FilteredRecyclerViewFilterTextView.WordPress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/white"
+ android:paddingRight="0dp"
+ android:paddingLeft="0dp"
+ android:drawableRight="@drawable/arrow"
+ tools:text="spinner item"/>
diff --git a/WordPress/src/main/res/layout/filtered_list_component.xml b/WordPress/src/main/res/layout/filtered_list_component.xml
new file mode 100644
index 000000000..1c5a60bb4
--- /dev/null
+++ b/WordPress/src/main/res/layout/filtered_list_component.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.AppBarLayout
+ android:id="@+id/app_bar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar_with_spinner"
+ style="@style/FilteredRecyclerViewToolbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/toolbar_height"
+ android:focusableInTouchMode="true"
+ app:contentInsetLeft="0dp"
+ app:contentInsetStart="0dp"
+ app:layout_scrollFlags="scroll|enterAlways">
+
+ <Spinner
+ android:id="@+id/filter_spinner"
+ style="@style/FilteredRecyclerViewSpinner.WordPress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:overlapAnchor="false" />
+
+ </android.support.v7.widget.Toolbar>
+
+ </android.support.design.widget.AppBarLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/empty_view"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:gravity="center"
+ android:text="@string/empty_list_default"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/progress_loading"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/margin_extra_large" />
+ </RelativeLayout>
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/WordPress/src/main/res/layout/help_activity_with_helpshift.xml b/WordPress/src/main/res/layout/help_activity_with_helpshift.xml
new file mode 100644
index 000000000..f2875aac8
--- /dev/null
+++ b/WordPress/src/main/res/layout/help_activity_with_helpshift.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/main_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/nux_background"
+ android:fillViewport="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ app:maxWidth="@dimen/nux_width"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXTitle"
+ android:text="@string/help"
+ android:id="@+id/create_account_label"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:text="@string/nux_help_description"
+ android:id="@+id/nux_help_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginBottom="16dp"
+ app:fixWidowWords="true"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:text="@string/contact_us"
+ android:id="@+id/contact_us_button"
+ android:layout_width="match_parent"
+ android:gravity="center"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:text="@string/browse_our_faq_button"
+ android:id="@+id/faq_button"
+ android:layout_width="match_parent"
+ android:gravity="center"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:text="@string/reader_title_applog"
+ android:id="@+id/applog_button"
+ android:layout_width="match_parent"
+ android:gravity="center"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:text="@string/app_name"
+ android:id="@+id/nux_help_version"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"/>
+ </LinearLayout>
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/home_row.xml b/WordPress/src/main/res/layout/home_row.xml
new file mode 100644
index 000000000..0200e3925
--- /dev/null
+++ b/WordPress/src/main/res/layout/home_row.xml
@@ -0,0 +1,67 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <com.android.volley.toolbox.NetworkImageView
+ android:id="@+id/blavatar"
+ android:layout_width="42dip"
+ android:layout_height="42dip"
+ android:layout_marginLeft="8dip"
+ android:layout_marginTop="12dip"
+ android:gravity="center|center"
+ android:scaleType="centerCrop"
+ app:srcCompat="@mipmap/app_icon" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="10dip"
+ android:paddingTop="10dip">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="58dip"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/blogName"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="-2dip"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="left"
+ android:paddingRight="8dip"
+ android:shadowColor="#FFFFFF"
+ android:shadowDx="0"
+ android:shadowDy="1"
+ android:shadowRadius="1"
+ android:singleLine="true"
+ android:text="@string/wordpress_blog"
+ android:textColor="#444444"
+ android:textSize="20dip"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/blogUser"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="left"
+ android:padding="0px"
+ android:singleLine="true"
+ android:text="@string/blogusername"
+ android:textColor="#666666"
+ android:textColorLink="@color/blue_wordpress"
+ android:textSize="12dip" />
+ </LinearLayout>
+ </LinearLayout>
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/invite_username_button.xml b/WordPress/src/main/res/layout/invite_username_button.xml
new file mode 100644
index 000000000..86486158e
--- /dev/null
+++ b/WordPress/src/main/res/layout/invite_username_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="6dp"
+ android:paddingRight="12dp"
+ android:paddingTop="@dimen/margin_large">
+
+ <TextView
+ android:id="@+id/username"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="5dp"
+ android:gravity="center_vertical"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"/>
+
+ <ImageButton
+ android:id="@+id/username_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"
+ android:src="@drawable/ic_close_grey600_24dp"
+ android:background="?attr/selectableItemBackgroundBorderless"/>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/learn_more_pref.xml b/WordPress/src/main/res/layout/learn_more_pref.xml
new file mode 100644
index 000000000..948e68c8c
--- /dev/null
+++ b/WordPress/src/main/res/layout/learn_more_pref.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="16dp"
+ android:paddingStart="12dp"
+ android:paddingRight="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/learn_more_caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/site_settings_learn_more_caption"
+ android:textSize="@dimen/text_sz_small"
+ android:textColor="@color/grey_darken_10" />
+
+ <org.wordpress.android.widgets.WPButton
+ android:id="@+id/learn_more_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-12dp"
+ android:gravity="start|center_vertical"
+ android:background="@color/transparent"
+ android:textStyle="bold"
+ android:textAllCaps="true"
+ android:text="@string/learn_more"
+ android:textSize="@dimen/text_sz_medium"
+ android:textColor="@color/blue_medium"
+ android:fontFamily="sans-serif-light" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/learn_more_pref_screen.xml b/WordPress/src/main/res/layout/learn_more_pref_screen.xml
new file mode 100644
index 000000000..2f28e8a7e
--- /dev/null
+++ b/WordPress/src/main/res/layout/learn_more_pref_screen.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:foregroundGravity="center_horizontal"
+ android:background="@color/transparent">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true" />
+
+</FrameLayout>
diff --git a/WordPress/src/main/res/layout/list_editor.xml b/WordPress/src/main/res/layout/list_editor.xml
new file mode 100644
index 000000000..47708d5d5
--- /dev/null
+++ b/WordPress/src/main/res/layout/list_editor.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/empty_view"
+ android:layout_centerInParent="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_extra_large"
+ android:text="@string/site_settings_list_editor_no_items_text"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="@dimen/text_sz_large"
+ android:visibility="gone" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/list_editor_header_text"
+ android:layout_marginTop="?android:attr/actionBarSize"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_extra_large"
+ android:textSize="@dimen/text_sz_small"
+ android:textColor="@color/grey_darken_10" />
+
+ <View
+ android:background="@color/divider_grey"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_below="@+id/list_editor_header_text"
+ android:layout_height="@dimen/list_divider_height"
+ android:layout_width="match_parent" />
+
+ <org.wordpress.android.ui.prefs.EmptyViewRecyclerView
+ android:id="@android:id/list"
+ android:layout_below="@+id/list_editor_header_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <!-- CoordinatorLayout required: https://code.google.com/p/android/issues/detail?id=175330 -->
+ <android.support.design.widget.CoordinatorLayout
+ android:id="@+id/coordinator"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|bottom"
+ android:layout_marginBottom="@dimen/fab_margin"
+ android:layout_marginRight="@dimen/fab_margin"
+ android:layout_marginEnd="@dimen/fab_margin"
+ app:srcCompat="@drawable/ic_add_white_24dp"
+ android:contentDescription="@string/add"
+ app:borderWidth="0dp"
+ app:rippleColor="@color/fab_pressed" />
+
+ </android.support.design.widget.CoordinatorLayout>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/list_footer_progress.xml b/WordPress/src/main/res/layout/list_footer_progress.xml
new file mode 100644
index 000000000..7167dde08
--- /dev/null
+++ b/WordPress/src/main/res/layout/list_footer_progress.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small">
+
+ <ProgressBar
+ android:id="@+id/progressbar"
+ style="?android:attr/progressBarStyleInverse"
+ android:layout_width="28dp"
+ android:layout_height="28dp"
+ android:layout_centerVertical="true" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/logviewer_activity.xml b/WordPress/src/main/res/layout/logviewer_activity.xml
new file mode 100644
index 000000000..9eca91410
--- /dev/null
+++ b/WordPress/src/main/res/layout/logviewer_activity.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@null"
+ android:dividerHeight="0dp" />
diff --git a/WordPress/src/main/res/layout/logviewer_listitem.xml b/WordPress/src/main/res/layout/logviewer_listitem.xml
new file mode 100644
index 000000000..ccfa0fe16
--- /dev/null
+++ b/WordPress/src/main/res/layout/logviewer_listitem.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_medium"
+ android:paddingTop="@dimen/margin_extra_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_line"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="01" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_log"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:gravity="top"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="application log text" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/magic_link_request_fragment.xml b/WordPress/src/main/res/layout/magic_link_request_fragment.xml
new file mode 100644
index 000000000..af12ac4ae
--- /dev/null
+++ b/WordPress/src/main/res/layout/magic_link_request_fragment.xml
@@ -0,0 +1,112 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/sign_in_scroll_view"
+ android:background="@color/nux_background"
+ android:animateLayoutChanges="true"
+ android:fillViewport="true"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:animateLayoutChanges="true"
+ android:baselineAligned="false"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|bottom"
+ android:baselineAligned="true"
+ android:gravity="right"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/info_button"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:contentDescription="@string/help"
+ app:srcCompat="@drawable/dashicon_info"
+ android:tint="@color/blue_dark" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:animateLayoutChanges="true"
+ android:gravity="center"
+ android:orientation="vertical"
+ app:maxWidth="@dimen/nux_width">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="300dp"
+ android:layout_height="wrap_content"
+ style="@style/WordPress.NUXFlatButton"
+ android:textSize="20sp"
+ android:padding="12dp"
+ android:gravity="center"
+ android:text="@string/get_a_link_sent_to_your_email_to_sign_in_instantly" />
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/magic_button"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="wrap_content"
+ android:padding="12dp"
+ android:clickable="true"
+ android:gravity="center"
+ android:text="@string/send_link" />
+
+ <RelativeLayout
+ android:id="@+id/nux_sign_in_progress_bar"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="match_parent"
+ android:enabled="false"
+ android:visibility="gone">
+
+ <ProgressBar
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true" />
+ </RelativeLayout>
+ </RelativeLayout>
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ android:id="@+id/nux_bottom_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ app:maxWidth="@dimen/nux_width">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/password_layout"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_gravity="center"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:gravity="center"
+ android:text="@string/enter_your_password_instead" />
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/magic_link_sent_fragment.xml b/WordPress/src/main/res/layout/magic_link_sent_fragment.xml
new file mode 100644
index 000000000..bbdd03fab
--- /dev/null
+++ b/WordPress/src/main/res/layout/magic_link_sent_fragment.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/nux_background">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|bottom"
+ android:baselineAligned="true"
+ android:gravity="right"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/info_button"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:contentDescription="@string/help"
+ app:srcCompat="@drawable/dashicon_info"
+ android:tint="@color/blue_dark" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:animateLayoutChanges="true"
+ android:gravity="center"
+ android:orientation="vertical"
+ app:maxWidth="@dimen/nux_width">
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/open_email"
+ style="@style/WordPress.NUXTitle"
+ android:layout_width="match_parent"
+ android:clickable="true"
+ android:gravity="center"
+ android:text="@string/check_your_email"
+ android:layout_height="wrap_content" />
+
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/open_email_button"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="wrap_content"
+ android:padding="12dp"
+ android:clickable="true"
+ android:gravity="center"
+ android:text="@string/launch_your_email_app" />
+
+ <RelativeLayout
+ android:id="@+id/nux_sign_in_progress_bar"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="match_parent"
+ android:enabled="false"
+ android:visibility="gone">
+
+ <ProgressBar
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true" />
+ </RelativeLayout>
+ </RelativeLayout>
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ android:id="@+id/nux_bottom_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ app:maxWidth="@dimen/nux_width">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/password_layout"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_gravity="center"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:gravity="center"
+ android:text="@string/enter_your_password_instead" />
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/main_activity.xml b/WordPress/src/main/res/layout/main_activity.xml
new file mode 100644
index 000000000..347fa5150
--- /dev/null
+++ b/WordPress/src/main/res/layout/main_activity.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root_view_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.ui.main.WPMainTabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/toolbar_height"
+ android:background="@color/tab_background"
+ android:elevation="@dimen/tabs_elevation"
+ app:tabIndicatorColor="@color/tab_indicator"
+ tools:targetApi="LOLLIPOP"/>
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/viewpager_main"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/connection_bar"
+ android:layout_below="@+id/tab_layout" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/connection_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@color/alert_yellow"
+ android:gravity="center"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingTop="@dimen/margin_medium"
+ android:text="@string/connectionbar_no_connection"
+ android:textAllCaps="true"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_small"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/me_fragment.xml b/WordPress/src/main/res/layout/me_fragment.xml
new file mode 100644
index 000000000..fd39663eb
--- /dev/null
+++ b/WordPress/src/main/res/layout/me_fragment.xml
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/me_avatar_margin_top">
+
+ <View
+ android:id="@+id/avatar_tooltip_anchor"
+ android:layout_width="@dimen/avatar_sz_large"
+ android:layout_height="1dp"
+ android:layout_centerHorizontal="true"
+ android:visibility="invisible"/>
+
+ <FrameLayout
+ android:id="@+id/frame_avatar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true">
+
+ <FrameLayout
+ android:id="@+id/avatar_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_medium"
+ android:background="?attr/selectableItemBackgroundBorderless" >
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/me_avatar"
+ android:layout_width="@dimen/avatar_sz_large"
+ android:layout_height="@dimen/avatar_sz_large" />
+ </FrameLayout>
+
+ <ProgressBar
+ android:id="@+id/avatar_progress"
+ style="@android:style/Widget.Holo.ProgressBar"
+ android:layout_width="@dimen/avatar_sz_large"
+ android:layout_height="@dimen/avatar_sz_large"
+ android:background="@drawable/shape_oval_translucent"
+ android:layout_gravity="center"
+ android:padding="@dimen/margin_large"
+ android:indeterminate="true"
+ android:clickable="true"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+ </FrameLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/me_display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/frame_avatar"
+ android:layout_centerHorizontal="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ android:fontFamily="sans-serif-light"
+ tools:text="Full Name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/me_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/me_display_name"
+ android:layout_centerHorizontal="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="username" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/row_my_profile"
+ style="@style/MeListRowLayout"
+ android:layout_marginTop="@dimen/me_list_margin_top">
+
+ <ImageView
+ android:id="@+id/me_my_profile_icon"
+ style="@style/MeListRowIcon"
+ app:srcCompat="@drawable/me_icon_my_profile"
+ android:contentDescription="@string/my_profile"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/me_my_profile_text_view"
+ style="@style/MeListRowTextView"
+ android:text="@string/my_profile" />
+
+ </LinearLayout>
+
+ <View style="@style/MeListSectionDividerView" />
+
+ <LinearLayout
+ android:id="@+id/row_account_settings"
+ style="@style/MeListRowLayout">
+
+ <ImageView
+ android:id="@+id/me_account_settings_icon"
+ style="@style/MeListRowIcon"
+ app:srcCompat="@drawable/me_icon_account_settings"
+ android:contentDescription="@string/account_settings"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/me_account_settings_text_view"
+ style="@style/MeListRowTextView"
+ android:text="@string/account_settings" />
+
+ </LinearLayout>
+
+ <View style="@style/MeListSectionDividerView" />
+
+ <LinearLayout
+ android:id="@+id/row_app_settings"
+ style="@style/MeListRowLayout">
+
+ <ImageView
+ android:id="@+id/me_app_settings_icon"
+ style="@style/MeListRowIcon"
+ app:srcCompat="@drawable/me_icon_app_settings"
+ android:contentDescription="@string/me_btn_app_settings"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/me_app_settings_text_view"
+ style="@style/MeListRowTextView"
+ android:text="@string/me_btn_app_settings" />
+
+ </LinearLayout>
+
+ <View style="@style/MeListSectionDividerView" />
+
+ <LinearLayout
+ android:id="@+id/row_notifications"
+ style="@style/MeListRowLayout">
+
+ <ImageView
+ android:id="@+id/me_notifications_icon"
+ style="@style/MeListRowIcon"
+ app:srcCompat="@drawable/me_icon_notifications"
+ android:contentDescription="@string/notification_settings"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/me_notifications_text_view"
+ style="@style/MeListRowTextView"
+ android:text="@string/notification_settings" />
+
+ </LinearLayout>
+
+ <View android:id="@+id/me_notifications_divider"
+ style="@style/MeListSectionDividerView" />
+
+ <LinearLayout
+ android:id="@+id/row_support"
+ style="@style/MeListRowLayout">
+
+ <ImageView
+ android:id="@+id/me_support_icon"
+ style="@style/MeListRowIcon"
+ app:srcCompat="@drawable/me_icon_support"
+ android:contentDescription="@string/me_btn_support"/>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/me_support_text_view"
+ style="@style/MeListRowTextView"
+ android:text="@string/me_btn_support" />
+
+ </LinearLayout>
+
+ <View style="@style/MeListSectionDividerView" />
+
+ <LinearLayout
+ android:id="@+id/row_logout"
+ style="@style/MeListRowLayout">
+
+ <ImageView
+ android:id="@+id/me_login_logout_icon"
+ style="@style/MeListRowIcon"
+ app:srcCompat="@drawable/me_icon_login_logout"
+ android:contentDescription="@string/me_btn_login_logout"/>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/me_login_logout_text_view"
+ style="@style/MeListRowTextView"
+ tools:text="@string/me_btn_login_logout" />
+
+ </LinearLayout>
+
+ <View style="@style/MeListSectionDividerView" />
+
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/media_browser_activity.xml b/WordPress/src/main/res/layout/media_browser_activity.xml
new file mode 100644
index 000000000..60475f259
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_browser_activity.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/toolbar"
+ android:id="@+id/toolbar" />
+
+ <LinearLayout
+ android:id="@+id/media_browser_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <fragment
+ android:id="@+id/mediaGridFragment"
+ android:name="org.wordpress.android.ui.media.MediaGridFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <fragment
+ android:id="@+id/mediaAddFragment"
+ android:name="org.wordpress.android.ui.media.MediaAddFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/media_edit_fragment.xml b/WordPress/src/main/res/layout/media_edit_fragment.xml
new file mode 100644
index 000000000..f160f31b8
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_edit_fragment.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <LinearLayout
+ android:id="@+id/media_edit_linear_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="@dimen/margin_extra_large"
+ android:visibility="invisible"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_edit_title_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/margin_large"
+ android:text="@string/media_edit_title_text"
+ android:textSize="@dimen/text_sz_large" />
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/media_edit_save_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="20dp"
+ android:background="@drawable/media_blue_button_selector"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp"
+ android:text="@string/save"
+ android:textColor="@color/white"
+ android:visibility="@integer/media_editor_save_button_visibility"
+ tools:visibility="visible"/>
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/media_edit_fragment_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/media_edit_save_button"
+ android:layout_toLeftOf="@id/media_edit_save_button"
+ android:hint="@string/media_edit_title_hint"
+ android:inputType="textCapSentences|textAutoCorrect" />
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_edit_caption_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:text="@string/media_edit_caption_text"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/media_edit_fragment_caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/media_edit_caption_hint"
+ android:inputType="textCapSentences|textAutoCorrect" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_edit_description_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:paddingLeft="@dimen/margin_large"
+ android:text="@string/media_edit_description_text"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/media_edit_fragment_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="top|start"
+ android:hint="@string/media_edit_description_hint"
+ android:inputType="textCapSentences|textAutoCorrect"
+ android:lines="3" />
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large">
+
+ <include
+ android:id="@+id/media_edit_fragment_image_local"
+ layout="@layout/media_grid_image_local" />
+
+ <include
+ android:id="@+id/media_edit_fragment_image_network"
+ layout="@layout/media_grid_image_network" />
+ </FrameLayout>
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_gallery_activity.xml b/WordPress/src/main/res/layout/media_gallery_activity.xml
new file mode 100644
index 000000000..54ce6341e
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_gallery_activity.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.sothree.slidinguppanel.SlidingUpPanelLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_gallery_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <fragment
+ android:id="@+id/mediaGalleryEditFragment"
+ android:name="org.wordpress.android.ui.media.MediaGalleryEditFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <fragment
+ android:id="@+id/mediaGallerySettingsFragment"
+ android:name="org.wordpress.android.ui.media.MediaGallerySettingsFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <FrameLayout
+ android:id="@+id/media_gallery_add_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+ </FrameLayout>
+
+</com.sothree.slidinguppanel.SlidingUpPanelLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_gallery_column_checkbox.xml b/WordPress/src/main/res/layout/media_gallery_column_checkbox.xml
new file mode 100644
index 000000000..8c9c3fa96
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_gallery_column_checkbox.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_gallery_column_checkbox"
+ style="@style/MediaGalleryNumOfColumns"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_gravity="center"
+ android:gravity="center" >
+
+</CheckBox>
diff --git a/WordPress/src/main/res/layout/media_gallery_edit_fragment.xml b/WordPress/src/main/res/layout/media_gallery_edit_fragment.xml
new file mode 100644
index 000000000..e0129b33d
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_gallery_edit_fragment.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <com.mobeta.android.dslv.DragSortListView
+ android:id="@+id/edit_media_gallery_gridview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:numColumns="@integer/media_grid_num_columns"
+ app:click_remove_id="@id/click_remove"
+ app:collapsed_height="1px"
+ app:drag_enabled="true"
+ app:drag_handle_id="@id/drag_handle"
+ app:drag_scroll_start="0.33"
+ app:drag_start_mode="onDown"
+ app:float_alpha="0.6"
+ app:remove_enabled="true"
+ app:remove_mode="clickRemove"
+ app:slide_shuffle_speed="0.3" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_gallery_item.xml b/WordPress/src/main/res/layout/media_gallery_item.xml
new file mode 100644
index 000000000..d87a91499
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_gallery_item.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/media_grid_frame_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <FrameLayout
+ android:id="@+id/drag_handle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" >
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ app:srcCompat="@drawable/gallery_tablet_move_file" />
+ </FrameLayout>
+
+ <com.android.volley.toolbox.NetworkImageView
+ android:id="@+id/media_grid_item_image"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_marginBottom="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_toRightOf="@id/drag_handle"
+ android:inflatedId="@+id/media_grid_item_image"
+ android:scaleType="fitCenter" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_toRightOf="@id/media_grid_item_image"
+ android:orientation="vertical" >
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_small"
+ android:textStyle="bold" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_upload_date"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_small" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_filename"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_small" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_filetype"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_small" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_dimension"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_small" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_gallery_picker_layout.xml b/WordPress/src/main/res/layout/media_gallery_picker_layout.xml
new file mode 100644
index 000000000..02ec4269c
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_gallery_picker_layout.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_gallery_picker_gridview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbarStyle="outsideOverlay"
+ android:stretchMode="columnWidth"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"
+ android:verticalSpacing="@dimen/margin_small"
+ android:horizontalSpacing="@dimen/margin_small"
+ android:numColumns="@integer/media_grid_num_columns"/>
diff --git a/WordPress/src/main/res/layout/media_gallery_settings_fragment.xml b/WordPress/src/main/res/layout/media_gallery_settings_fragment.xml
new file mode 100644
index 000000000..98ac737ac
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_gallery_settings_fragment.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/media_gallery_bg"
+ android:orientation="vertical" >
+
+ <FrameLayout
+ android:id="@+id/media_gallery_settings_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_gallery_settings_title"
+ style="@style/MediaGalleryText"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_gravity="center"
+ android:drawablePadding="8dp"
+ android:gravity="center"
+ android:text="@string/media_gallery_settings_title" />
+ </FrameLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0.5dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="@color/white" />
+
+ <ScrollView
+ android:id="@+id/media_gallery_settings_content_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp" >
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MediaGalleryText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/media_gallery_image_order" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal" >
+
+ <CheckBox
+ android:id="@+id/media_gallery_random_checkbox"
+ style="@style/MediaGalleryText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|center_vertical"
+ android:layout_weight="2"
+ android:button="@drawable/media_gallery_checkbox_selector"
+ android:text="@string/media_gallery_image_order_random" />
+
+ <Button
+ android:id="@+id/media_gallery_settings_reverse_button"
+ style="@style/MediaGalleryText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:layout_weight="1"
+ android:background="@drawable/media_gallery_option_selector"
+ android:padding="8dp"
+ android:text="@string/media_gallery_image_order_reverse"
+ android:textColor="@color/media_grid_item_checkstate_text_selector" />
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0.5dp"
+ android:layout_marginBottom="16dp"
+ android:layout_marginTop="16dp"
+ android:background="@color/white" />
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MediaGalleryText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/media_gallery_type" />
+
+ <LinearLayout
+ android:id="@+id/media_gallery_type_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal"
+ android:weightSum="3" >
+
+ <CheckBox
+ android:id="@+id/media_gallery_type_thumbnail_grid"
+ style="@style/MediaGalleryTypeCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/gallery_thumbnail_grid_selector"
+ android:paddingLeft="0dp"
+ android:text="@string/media_gallery_type_thumbnail_grid" />
+
+ <CheckBox
+ android:id="@+id/media_gallery_type_tiled"
+ style="@style/MediaGalleryTypeCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/gallery_tiled_selector"
+ android:paddingLeft="0dp"
+ android:text="@string/media_gallery_type_tiled" />
+
+ <CheckBox
+ android:id="@+id/media_gallery_type_squares"
+ style="@style/MediaGalleryTypeCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/gallery_squares_selector"
+ android:paddingLeft="0dp"
+ android:text="@string/media_gallery_type_squares" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal" >
+
+ <CheckBox
+ android:id="@+id/media_gallery_type_circles"
+ style="@style/MediaGalleryTypeCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/gallery_circles_selector"
+ android:paddingLeft="0dp"
+ android:text="@string/media_gallery_type_circles" />
+
+ <CheckBox
+ android:id="@+id/media_gallery_type_slideshow"
+ style="@style/MediaGalleryTypeCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:drawableTop="@drawable/gallery_slideshow_selector"
+ android:paddingLeft="0dp"
+ android:text="@string/media_gallery_type_slideshow" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0.5dp"
+ android:layout_marginBottom="16dp"
+ android:layout_marginTop="16dp"
+ android:background="@color/white" />
+
+ <LinearLayout
+ android:id="@+id/media_gallery_settings_num_columns_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical" >
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MediaGalleryText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/media_gallery_num_columns" />
+
+ <org.wordpress.android.ui.ExpandableHeightGridView
+ android:id="@+id/media_gallery_num_columns_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:columnWidth="32dp"
+ android:horizontalSpacing="8dp"
+ android:isScrollContainer="false"
+ android:numColumns="auto_fit"
+ android:stretchMode="columnWidth"
+ android:verticalSpacing="8dp" />
+ </LinearLayout>
+ </LinearLayout>
+ </ScrollView>
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_grid_fragment.xml b/WordPress/src/main/res/layout/media_grid_fragment.xml
new file mode 100644
index 000000000..27eb3e328
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_grid_fragment.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/media_filter_spinner_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/grey_lighten_30"
+ android:clickable="true"
+ android:paddingLeft="8dp"
+ android:paddingRight="16dp">
+
+ <org.wordpress.android.ui.CustomSpinner
+ android:id="@+id/media_filter_spinner"
+ style="@style/DropDownNav.WordPress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:paddingLeft="0dp"/>
+ </FrameLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_filter_result_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:visibility="gone"/>
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <GridView
+ android:id="@+id/media_gridview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:horizontalSpacing="@dimen/margin_small"
+ android:numColumns="@integer/media_grid_num_columns"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"
+ android:scrollbarStyle="outsideOverlay"
+ android:stretchMode="columnWidth"
+ android:verticalSpacing="@dimen/margin_small"
+ tools:listitem="@layout/media_grid_item"/>
+
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+ </LinearLayout>
+
+
+ <LinearLayout
+ android:id="@+id/empty_view"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/empty_view_title"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:text="@string/media_empty_list_custom_date"
+ app:fixWidowWords="true"/>
+
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_grid_image_local.xml b/WordPress/src/main/res/layout/media_grid_image_local.xml
new file mode 100644
index 000000000..93efd88ba
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_grid_image_local.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_grid_item_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter" />
diff --git a/WordPress/src/main/res/layout/media_grid_image_network.xml b/WordPress/src/main/res/layout/media_grid_image_network.xml
new file mode 100644
index 000000000..e3c88a85d
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_grid_image_network.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.ui.FadeInNetworkImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_grid_item_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter" />
diff --git a/WordPress/src/main/res/layout/media_grid_item.xml b/WordPress/src/main/res/layout/media_grid_item.xml
new file mode 100644
index 000000000..22d93f32b
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_grid_item.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.ui.CheckableFrameLayout
+ android:id="@+id/media_grid_frame_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/media_gallery_grid_cell">
+
+ <ViewStub
+ android:id="@+id/media_grid_image_stub"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:inflatedId="@+id/media_grid_item_image" />
+
+ <RelativeLayout
+ android:id="@+id/media_grid_item_upload_state_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone">
+
+ <ProgressBar
+ android:id="@+id/media_grid_item_upload_progress"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_upload_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_below="@id/media_grid_item_upload_progress"
+ android:layout_centerHorizontal="true"
+ android:paddingLeft="@dimen/margin_small"
+ android:textColor="@color/white"
+ android:shadowColor="@color/primary_text_default_material_light"
+ android:shadowRadius="2"
+ android:textSize="@dimen/text_sz_small"/>
+ </RelativeLayout>
+
+ <CheckBox
+ android:id="@+id/media_grid_item_checkstate"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/media_grid_item_checkstate_selector"
+ android:button="@android:color/transparent"
+ android:clickable="false"
+ android:duplicateParentState="true"
+ android:focusable="false"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:padding="@dimen/margin_medium"
+ android:background="@color/media_gallery_grid_label_bg">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:layout_toLeftOf="@+id/media_grid_item_filetype"
+ android:textSize="@dimen/text_sz_small"
+ android:textColor="@color/media_gallery_grid_label"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_grid_item_filetype"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_small"
+ android:layout_alignParentRight="true"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_small"
+ android:textColor="@color/media_gallery_grid_label"/>
+ </RelativeLayout>
+ </org.wordpress.android.ui.CheckableFrameLayout>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/media_grid_progress.xml b/WordPress/src/main/res/layout/media_grid_progress.xml
new file mode 100644
index 000000000..8d947deb2
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_grid_progress.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_grid_progress_height"
+ android:gravity="center_horizontal" >
+
+ <ProgressBar
+ android:id="@+id/progressbar"
+ style="?android:attr/progressBarStyleInverse"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_centerVertical="true" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/progressbar"
+ android:gravity="center"
+ android:padding="10dp"
+ android:text="@string/loading"
+ android:textColor="#FF464646"
+ android:textSize="16sp" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_item_wp_image.xml b/WordPress/src/main/res/layout/media_item_wp_image.xml
new file mode 100644
index 000000000..d7650cc6c
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_item_wp_image.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.wordpress.mediapicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_item_height"
+ android:layout_marginLeft="@dimen/media_item_frame_margin_left"
+ android:layout_marginStart="@dimen/media_item_frame_margin_left"
+ android:layout_marginTop="@dimen/media_item_frame_margin_top"
+ android:layout_marginRight="@dimen/media_item_frame_margin_right"
+ android:layout_marginEnd="@dimen/media_item_frame_margin_right"
+ android:layout_marginBottom="@dimen/media_item_frame_margin_bottom"
+ android:paddingLeft="@dimen/media_item_frame_padding_left"
+ android:paddingTop="@dimen/media_item_frame_padding_top"
+ android:paddingRight="@dimen/media_item_frame_padding_right"
+ android:paddingEnd="@dimen/media_item_frame_padding_right"
+ android:paddingBottom="@dimen/media_item_frame_padding_bottom"
+ android:background="@drawable/media_item_frame_selector">
+
+ <ImageView
+ android:contentDescription="@string/cd_media_item_image"
+ android:id="@+id/wp_image_view_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop" />
+
+ <ImageView
+ android:contentDescription="@string/cd_media_item_overlay"
+ android:id="@+id/wp_image_view_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:paddingEnd="8dp"
+ android:paddingRight="8dp"
+ android:paddingTop="4dp"
+ android:background="@drawable/dashicon_wordpress_alt"
+ android:scaleType="centerInside" />
+
+</org.wordpress.mediapicker.CheckableFrameLayout>
diff --git a/WordPress/src/main/res/layout/media_item_wp_video.xml b/WordPress/src/main/res/layout/media_item_wp_video.xml
new file mode 100644
index 000000000..75f40bd41
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_item_wp_video.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.wordpress.mediapicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_item_height"
+ android:layout_marginLeft="@dimen/media_item_frame_margin_left"
+ android:layout_marginStart="@dimen/media_item_frame_margin_left"
+ android:layout_marginTop="@dimen/media_item_frame_margin_top"
+ android:layout_marginRight="@dimen/media_item_frame_margin_right"
+ android:layout_marginEnd="@dimen/media_item_frame_margin_right"
+ android:layout_marginBottom="@dimen/media_item_frame_margin_bottom"
+ android:paddingLeft="@dimen/media_item_frame_padding_left"
+ android:paddingTop="@dimen/media_item_frame_padding_top"
+ android:paddingRight="@dimen/media_item_frame_padding_right"
+ android:paddingEnd="@dimen/media_item_frame_padding_right"
+ android:paddingBottom="@dimen/media_item_frame_padding_bottom"
+ android:background="@drawable/media_item_frame_selector">
+
+ <ImageView
+ android:contentDescription="@string/cd_media_item_image"
+ android:id="@+id/wp_video_view_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ android:background="@color/grey_darken_10" />
+
+ <ImageView
+ android:contentDescription="@string/cd_media_item_overlay"
+ android:id="@+id/wp_video_view_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:paddingEnd="8dp"
+ android:paddingRight="8dp"
+ android:paddingTop="4dp"
+ android:background="@drawable/dashicon_wordpress_alt"
+ android:scaleType="centerInside" />
+
+ <ImageView
+ android:contentDescription="@string/cd_media_item_overlay"
+ android:id="@+id/wp_video_view_overlay2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:paddingEnd="8dp"
+ android:paddingRight="8dp"
+ android:paddingTop="4dp"
+ android:background="@drawable/ic_media_play"
+ android:scaleType="centerInside" />
+
+</org.wordpress.mediapicker.CheckableFrameLayout>
diff --git a/WordPress/src/main/res/layout/media_listitem_details.xml b/WordPress/src/main/res/layout/media_listitem_details.xml
new file mode 100644
index 000000000..115020585
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_listitem_details.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/layout_image_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/grey_darken_20"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/layout_image_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/media_listitem_details_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ tools:src="@drawable/drake_empty_results" />
+ </FrameLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_listitem_details_caption"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_extra_large"
+ android:textStyle="bold"
+ android:visibility="gone"
+ tools:text="media_listitem_details_caption"
+ tools:visibility="visible" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_listitem_details_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ android:visibility="gone"
+ tools:text="media_listitem_details_description"
+ tools:visibility="visible" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:text="@string/media_details_label_file_name"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_listitem_details_file_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="media_listitem_details_file_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:text="@string/media_details_label_file_type"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_listitem_details_file_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="media_listitem_details_file_type" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_listitem_details_date_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:text="@string/media_details_label_date_uploaded"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_listitem_details_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_extra_large"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="media_listitem_details_date" />
+
+ </LinearLayout>
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/media_picker_activity.xml b/WordPress/src/main/res/layout/media_picker_activity.xml
new file mode 100644
index 000000000..04490e272
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_picker_activity.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/theme_browser_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/tab_background"
+ app:tabIndicatorColor="@color/tab_indicator" />
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/media_picker_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/media_picker_fragment.xml b/WordPress/src/main/res/layout/media_picker_fragment.xml
new file mode 100644
index 000000000..da388ef58
--- /dev/null
+++ b/WordPress/src/main/res/layout/media_picker_fragment.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/media_empty_view"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:textSize="@dimen/empty_view_text_size" />
+
+ <GridView
+ android:id="@+id/media_adapter_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="4dp"
+ android:verticalSpacing="2dp"
+ android:horizontalSpacing="2dp"
+ android:numColumns="2"/>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/my_profile_dialog.xml b/WordPress/src/main/res/layout/my_profile_dialog.xml
new file mode 100644
index 000000000..48ae35ac7
--- /dev/null
+++ b/WordPress/src/main/res/layout/my_profile_dialog.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="@dimen/margin_extra_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_profile_dialog_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginStart="@dimen/margin_small"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_extra_large"
+ android:textStyle="bold"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_profile_dialog_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginStart="@dimen/margin_small"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small"/>
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/my_profile_dialog_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/my_profile_fragment.xml b/WordPress/src/main/res/layout/my_profile_fragment.xml
new file mode 100644
index 000000000..870eef87c
--- /dev/null
+++ b/WordPress/src/main/res/layout/my_profile_fragment.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/first_name_row"
+ style="@style/MyProfileRow"
+ android:layout_marginTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MyProfileLabel"
+ android:text="@string/first_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/first_name"
+ android:text="@string/first_name"
+ style="@style/MyProfileText" />
+
+ </LinearLayout>
+
+ <View style="@style/MyProfileDividerView" />
+
+ <LinearLayout
+ android:id="@+id/last_name_row"
+ style="@style/MyProfileRow">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MyProfileLabel"
+ android:text="@string/last_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/last_name"
+ style="@style/MyProfileText" />
+
+ </LinearLayout>
+
+ <View style="@style/MyProfileDividerView" />
+
+ <LinearLayout
+ android:id="@+id/display_name_row"
+ style="@style/MyProfileRow">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MyProfileLabel"
+ android:text="@string/public_display_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/display_name"
+ style="@style/MyProfileText" />
+
+ </LinearLayout>
+
+ <View style="@style/MyProfileDividerView" />
+
+ <LinearLayout
+ android:id="@+id/about_me_row"
+ style="@style/MyProfileRow">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MyProfileLabel"
+ android:text="@string/about_me" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/about_me"
+ style="@style/MyProfileText" />
+
+ </LinearLayout>
+
+ <View style="@style/MyProfileDividerView" />
+
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/my_site_fragment.xml b/WordPress/src/main/res/layout/my_site_fragment.xml
new file mode 100644
index 000000000..bbb8a9909
--- /dev/null
+++ b/WordPress/src/main/res/layout/my_site_fragment.xml
@@ -0,0 +1,469 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ScrollView
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin"
+ android:orientation="vertical">
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_medium"
+ card_view:cardBackgroundColor="@color/white"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/my_site_blavatar"
+ android:layout_width="@dimen/blavatar_sz"
+ android:layout_height="@dimen/blavatar_sz"
+ android:layout_margin="@dimen/margin_large"
+ android:gravity="center_vertical" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_title_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/my_site_blog_name_margin_top"
+ android:layout_toRightOf="@id/my_site_blavatar"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_subtitle_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/my_site_title_label"
+ android:layout_toRightOf="@id/my_site_blavatar"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/my_site_blavatar"
+ android:background="@color/grey_light">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/switch_site"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawableLeft="@drawable/switch_site_button_icon"
+ android:drawablePadding="@dimen/margin_small"
+ android:gravity="center_vertical"
+ android:minHeight="0dp"
+ android:paddingBottom="@dimen/my_site_switch_site_button_padding_bottom"
+ android:paddingTop="@dimen/my_site_margin_general"
+ android:text="@string/my_site_btn_switch_site"
+ android:textAllCaps="true"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_small" />
+
+ </LinearLayout>
+ </RelativeLayout>
+ </android.support.v7.widget.CardView>
+
+ <!--Stats-->
+ <RelativeLayout
+ android:id="@+id/row_stats"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_stats_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_stats" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_stats_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_stats_icon"
+ android:text="@string/stats" />
+
+ </RelativeLayout>
+
+ <!--Plan-->
+ <RelativeLayout
+ android:id="@+id/row_plan"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_plan_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/gridicons_clipboard" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_plan_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_plan_icon"
+ android:text="@string/plan" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_current_plan_text_view"
+ style="@style/MySiteListRowSecondaryTextView"
+ android:text="@string/plan" />
+
+ </RelativeLayout>
+
+ <!--PUBLISH-->
+ <LinearLayout style="@style/MySiteListHeaderLayout">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MySiteListHeaderTextView"
+ android:text="@string/my_site_header_publish" />
+
+ <View style="@style/MySiteListSectionDividerView" />
+
+ </LinearLayout>
+
+ <!--Blog Posts-->
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <View
+ android:id="@+id/postsGlowBackground"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:alpha="1"
+ android:background="@color/translucent_grey_lighten_20"
+ android:visibility="invisible" />
+
+ <LinearLayout
+ android:id="@+id/row_blog_posts"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_blog_posts_icon"
+ style="@style/MySiteListRowIcon"
+ android:contentDescription="@string/my_site_btn_blog_posts"
+ app:srcCompat="@drawable/my_site_icon_posts" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_blog_posts_text_view"
+ style="@style/MySiteListRowTextView"
+ android:text="@string/my_site_btn_blog_posts" />
+
+ </LinearLayout>
+
+ </FrameLayout>
+
+ <!--Media-->
+ <RelativeLayout
+ android:id="@+id/row_media"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_media_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_media" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_media_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_media_icon"
+ android:text="@string/media" />
+
+ </RelativeLayout>
+
+ <!--Pages-->
+ <RelativeLayout
+ android:id="@+id/row_pages"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_pages_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_pages" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_pages_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_pages_icon"
+ android:text="@string/pages" />
+
+ </RelativeLayout>
+
+ <!--Comments-->
+ <RelativeLayout
+ android:id="@+id/row_comments"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_comments_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_comments" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_comments_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_comments_icon"
+ android:text="@string/my_site_btn_comments" />
+
+ </RelativeLayout>
+
+ <!--Look & Feel-->
+ <LinearLayout
+ android:id="@+id/my_site_look_and_feel_header"
+ style="@style/MySiteListHeaderLayout">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MySiteListHeaderTextView"
+ android:text="@string/my_site_header_look_and_feel" />
+
+ <View style="@style/MySiteListSectionDividerView" />
+
+ </LinearLayout>
+
+ <!--Themes-->
+ <RelativeLayout
+ android:id="@+id/row_themes"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_themes_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_themes" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_themes_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_themes_icon"
+ android:text="@string/themes" />
+
+ </RelativeLayout>
+
+ <!--Configuration-->
+ <LinearLayout
+ android:id="@+id/row_configuration"
+ style="@style/MySiteListHeaderLayout">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MySiteListHeaderTextView"
+ android:text="@string/my_site_header_configuration" />
+
+ <View style="@style/MySiteListSectionDividerView" />
+
+ </LinearLayout>
+
+ <!--People-->
+ <RelativeLayout
+ android:id="@+id/row_people"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_people_icon"
+ style="@style/MySiteListRowIcon"
+ android:contentDescription="@string/people"
+ android:src="@drawable/me_icon_my_profile" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_people_management_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_people_icon"
+ android:text="@string/people" />
+
+ </RelativeLayout>
+
+ <!--Settings-->
+ <RelativeLayout
+ android:id="@+id/row_settings"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_settings_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_settings" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_settings_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_settings_icon"
+ android:text="@string/my_site_btn_site_settings" />
+
+ </RelativeLayout>
+
+ <!--EXTERNAL-->
+ <LinearLayout
+ android:id="@+id/external_section"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout style="@style/MySiteListHeaderLayout">
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/MySiteListHeaderTextView"
+ android:text="@string/my_site_header_external" />
+
+ <View style="@style/MySiteListSectionDividerView" />
+
+ </LinearLayout>
+
+ <!--View Site-->
+ <RelativeLayout
+ android:id="@+id/row_view_site"
+ style="@style/MySiteListRowLayout"
+ android:layout_marginTop="@dimen/my_site_margin_general">
+
+ <ImageView
+ android:id="@+id/my_site_view_site_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_view_site" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_view_site_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_view_site_icon"
+ android:text="@string/my_site_btn_view_site" />
+
+ <ImageView
+ android:id="@+id/my_site_view_site_icon_external"
+ style="@style/MySiteListRowSecondaryIcon"
+ android:layout_toRightOf="@+id/my_site_view_site_text_view"
+ android:tint="@color/grey_darken_10"
+ app:srcCompat="@drawable/gridicons_external" />
+
+ </RelativeLayout>
+
+ <!--View Admin-->
+ <RelativeLayout
+ android:id="@+id/row_admin"
+ style="@style/MySiteListRowLayout">
+
+ <ImageView
+ android:id="@+id/my_site_view_admin_icon"
+ style="@style/MySiteListRowIcon"
+ app:srcCompat="@drawable/my_site_icon_view_admin" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_view_admin_text_view"
+ style="@style/MySiteListRowTextView"
+ android:layout_toRightOf="@id/my_site_view_admin_icon"
+ android:text="@string/my_site_btn_view_admin" />
+
+ <ImageView
+ android:id="@+id/my_site_admin_icon_external"
+ style="@style/MySiteListRowSecondaryIcon"
+ android:layout_toRightOf="@+id/my_site_view_admin_text_view"
+ android:tint="@color/grey_darken_10"
+ app:srcCompat="@drawable/gridicons_external" />
+
+ </RelativeLayout>
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/margin_extra_large" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <LinearLayout
+ android:id="@+id/no_site_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginLeft="@dimen/my_site_no_site_view_margin"
+ android:layout_marginRight="@dimen/my_site_no_site_view_margin"
+ android:layout_marginTop="@dimen/margin_large"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/my_site_no_site_view_drake"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ app:srcCompat="@drawable/my_site_no_sites_drake" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:gravity="center"
+ android:text="@string/my_site_no_sites_view_title"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_double_extra_large"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:gravity="center"
+ android:text="@string/my_site_no_sites_view_subtitle"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium" />
+
+ <android.support.v7.widget.CardView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/empty_list_button_top_margin"
+ card_view:cardBackgroundColor="@color/blue_medium"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/my_site_add_site_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:text="@string/site_picker_add_site"
+ android:textAllCaps="true"
+ android:textColor="@color/white" />
+
+ </android.support.v7.widget.CardView>
+
+ </LinearLayout>
+
+ <!-- this coordinator is only here due to https://code.google.com/p/android/issues/detail?id=175330 -->
+ <android.support.design.widget.CoordinatorLayout
+ android:id="@+id/coordinator"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|bottom"
+ android:layout_marginBottom="@dimen/fab_margin"
+ android:layout_marginRight="@dimen/fab_margin"
+ app:srcCompat="@drawable/gridicon_create_light"
+ android:contentDescription="@string/new_post"
+ app:borderWidth="0dp"
+ app:rippleColor="@color/fab_pressed" />
+ </android.support.design.widget.CoordinatorLayout>
+
+</FrameLayout>
diff --git a/WordPress/src/main/res/layout/new_account_activity.xml b/WordPress/src/main/res/layout/new_account_activity.xml
new file mode 100644
index 000000000..87901ea7e
--- /dev/null
+++ b/WordPress/src/main/res/layout/new_account_activity.xml
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/main_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/nux_background"
+ android:orientation="vertical" >
+
+ <fragment android:name="org.wordpress.android.ui.accounts.NewUserFragment"
+ android:id="@+id/new_user_page_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/new_account_user_fragment_screen.xml b/WordPress/src/main/res/layout/new_account_user_fragment_screen.xml
new file mode 100644
index 000000000..28c382261
--- /dev/null
+++ b/WordPress/src/main/res/layout/new_account_user_fragment_screen.xml
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ app:maxWidth="@dimen/nux_width"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:baselineAligned="true"
+ android:layout_gravity="center_horizontal|bottom">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/info_button"
+ app:srcCompat="@drawable/dashicon_info"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="16dp"
+ android:contentDescription="@string/help"
+ android:tint="@color/blue_dark"/>
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ app:maxWidth="@dimen/nux_width"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:gravity="center"
+ android:layout_weight="1"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/create_account_label"
+ android:text="@string/create_account_wpcom"
+ style="@style/WordPress.NUXTitle"
+ android:fontFamily="sans-serif-light"
+ app:fixWidowWords="true"/>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/white">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/email_address"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@style/WordPress.NUXEditText"
+ android:hint="@string/email_hint"
+ android:inputType="textEmailAddress"
+ app:persistenceEnabled="true"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_email"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="10dp"
+ android:tint="@color/grey_darken_10"/>
+
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:id="@+id/relativeLayout">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/username"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@style/WordPress.NUXEditText"
+ android:inputType="textUri"
+ android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ android:hint="@string/username"
+ android:maxLength="@integer/max_length_username"
+ app:persistenceEnabled="true"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView2"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_admin_users"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="10dp"
+ android:tint="@color/grey_darken_10"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:id="@+id/relativeLayout2">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/password"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textPassword"
+ style="@style/WordPress.NUXEditText"
+ android:hint="@string/password"
+ android:maxLength="@integer/max_length_password"
+ android:layout_marginRight="38dp"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView3"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_lock"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="10dp"
+ android:tint="@color/grey_darken_10"/>
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="wrap_content"
+ android:id="@+id/password_visibility"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_eye_closed"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="16dp"
+ android:layout_alignParentRight="true"
+ android:tint="@color/nux_eye_icon_color_closed"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:layout_marginBottom="16dp">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/site_url"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/add_account_blog_url"
+ android:inputType="textUri"
+ android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ android:clickable="true"
+ android:paddingLeft="0dp"
+ android:paddingTop="12dp"
+ android:paddingRight="0dp"
+ android:paddingBottom="12dp"
+ android:layout_toLeftOf="@+id/textView"
+ app:persistenceEnabled="true"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/dot_wordpress_dot_com_url"
+ android:id="@+id/textView"
+ android:layout_alignParentEnd="false"
+ android:layout_marginTop="4dp"
+ android:layout_alignParentStart="false"
+ android:layout_alignParentTop="false"
+ android:paddingRight="8dp"
+ android:enabled="false"
+ android:focusable="false"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="4dp"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/nux_edit_field_font_size"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView4"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ app:srcCompat="@drawable/dashicon_admin_site"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="10dp"
+ android:tint="@color/grey_darken_10"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXPrimaryButton"
+ android:id="@+id/signup_button"
+ android:layout_width="match_parent"
+ android:text="@string/nux_welcome_create_account"
+ android:clickable="true"
+ android:gravity="center"
+ android:enabled="false"/>
+
+ <RelativeLayout
+ android:id="@+id/nux_sign_in_progress_bar"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:enabled="false">
+
+ <ProgressBar
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_centerVertical="true"
+ android:layout_centerHorizontal="true"/>
+ </RelativeLayout>
+
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/WordPress.NUXFlatButton"
+ android:id="@+id/nux_sign_in_progress_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textColor="@color/blue_light"
+ android:gravity="center|top"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/l_agree_terms_of_service"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/agree_terms_of_service"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ app:fixWidowWords="true"/>
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/new_blog_activity.xml b/WordPress/src/main/res/layout/new_blog_activity.xml
new file mode 100644
index 000000000..8bdf62edb
--- /dev/null
+++ b/WordPress/src/main/res/layout/new_blog_activity.xml
@@ -0,0 +1,14 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/main_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/nux_background"
+ android:orientation="vertical">
+
+ <fragment
+ android:name="org.wordpress.android.ui.accounts.NewBlogFragment"
+ android:id="@+id/new_blog_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/new_edit_post_activity.xml b/WordPress/src/main/res/layout/new_edit_post_activity.xml
new file mode 100644
index 000000000..4a29d21de
--- /dev/null
+++ b/WordPress/src/main/res/layout/new_edit_post_activity.xml
@@ -0,0 +1,6 @@
+<org.wordpress.android.widgets.WPViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditPostActivity" />
diff --git a/WordPress/src/main/res/layout/note_block_basic.xml b/WordPress/src/main/res/layout/note_block_basic.xml
new file mode 100644
index 000000000..e720fa615
--- /dev/null
+++ b/WordPress/src/main/res/layout/note_block_basic.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:paddingBottom="@dimen/margin_medium"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/note_text"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:textSize="@dimen/text_sz_large"
+ android:textColor="@color/grey_dark"
+ android:textColorLink="@color/grey_dark"
+ app:fixWidowWords="true"/>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/note_block_comment_user.xml b/WordPress/src/main/res/layout/note_block_comment_user.xml
new file mode 100644
index 000000000..59b741ee4
--- /dev/null
+++ b/WordPress/src/main/res/layout/note_block_comment_user.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/comment_avatar_margin_top">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/user_avatar"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small" />
+
+ <LinearLayout
+ android:id="@+id/user_name_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/notifications_adjusted_font_margin"
+ android:layout_marginStart="@dimen/notifications_adjusted_font_margin"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="Bob Ross" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_comment_ago"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large"
+ android:visibility="gone"
+ tools:text="5h"
+ tools:visibility="visible" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_comment_bullet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginRight="@dimen/margin_small"
+ android:includeFontPadding="false"
+ android:text="@string/bullet"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_comment_site"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="example.com" />
+ </LinearLayout>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:background="?android:selectableItemBackground"
+ android:paddingBottom="@dimen/margin_medium"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ app:wpFontFamily="merriweather"
+ tools:text="Thanks for stopping by my blog! I hope to see you again. " />
+
+ <View
+ android:id="@+id/divider_view"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+</LinearLayout>
+
+
diff --git a/WordPress/src/main/res/layout/note_block_footer.xml b/WordPress/src/main/res/layout/note_block_footer.xml
new file mode 100644
index 000000000..be9635390
--- /dev/null
+++ b/WordPress/src/main/res/layout/note_block_footer.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/note_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="6dp">
+
+ <org.wordpress.android.widgets.NoticonTextView
+ android:id="@+id/note_footer_noticon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="4dp"
+ android:layout_marginEnd="4dp"
+ android:textColor="@color/grey"
+ android:textSize="18sp" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/note_footer_text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large" />
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/divider_view"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/note_block_header.xml b/WordPress/src/main/res/layout/note_block_header.xml
new file mode 100644
index 000000000..f41bc5b7e
--- /dev/null
+++ b/WordPress/src/main/res/layout/note_block_header.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white">
+
+ <LinearLayout
+ android:id="@+id/header_root_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:clickable="true"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/comment_avatar_margin_top">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/header_avatar"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/notifications_adjusted_font_margin"
+ android:layout_marginStart="@dimen/notifications_adjusted_font_margin"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/header_user"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="Bob Ross" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/header_snippet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/blue_medium"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="www.bobross.com" />
+
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+ <!-- Two footer views, one is used specifically for comments -->
+ <LinearLayout
+ android:id="@+id/header_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/margin_medium" />
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/header_footer_comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:background="@color/grey_lighten_30" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/note_block_user.xml b/WordPress/src/main/res/layout/note_block_user.xml
new file mode 100644
index 000000000..3ba4443ce
--- /dev/null
+++ b/WordPress/src/main/res/layout/note_block_user.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white">
+
+ <LinearLayout
+ android:id="@+id/user_block_root_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:orientation="vertical"
+ android:background="?android:selectableItemBackground"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/user_avatar"
+ android:layout_width="@dimen/notifications_avatar_sz"
+ android:layout_height="@dimen/notifications_avatar_sz"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="@dimen/notifications_adjusted_font_margin"
+ android:layout_marginEnd="@dimen/notifications_adjusted_font_margin" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textSize="@dimen/text_sz_medium"
+ android:textStyle="bold"
+ android:textColor="@color/grey_dark"
+ tools:text="Bob Ross" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_blog_url"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/blue_medium"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="Paint with Me" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/user_blog_tagline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="www.bobross.com" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/notifications_detail_activity.xml b/WordPress/src/main/res/layout/notifications_detail_activity.xml
new file mode 100644
index 000000000..3ffd4f61a
--- /dev/null
+++ b/WordPress/src/main/res/layout/notifications_detail_activity.xml
@@ -0,0 +1,6 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/notifications_detail_container"
+ android:layout_width="match_parent" android:layout_height="match_parent"
+ tools:context="org.wordpress.android.ui.notifications.NotificationsDetailActivity"
+ tools:ignore="MergeRootFrame" />
diff --git a/WordPress/src/main/res/layout/notifications_fragment_detail_list.xml b/WordPress/src/main/res/layout/notifications_fragment_detail_list.xml
new file mode 100644
index 000000000..e4d9013d5
--- /dev/null
+++ b/WordPress/src/main/res/layout/notifications_fragment_detail_list.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- this container is needed in order to center the content of 'badge' notification types. -->
+ <LinearLayout
+ android:id="@+id/notifications_list_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:listSelector="@android:color/transparent" />
+
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/notifications_fragment_notes_list.xml b/WordPress/src/main/res/layout/notifications_fragment_notes_list.xml
new file mode 100644
index 000000000..53a48c6b4
--- /dev/null
+++ b/WordPress/src/main/res/layout/notifications_fragment_notes_list.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view_notes"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/notifications_content_margin"
+ android:paddingRight="@dimen/notifications_content_margin"
+ android:scrollbarStyle="outsideOverlay"
+ android:scrollbars="vertical"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ <android.support.design.widget.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/notifications_content_margin"
+ android:layout_marginRight="@dimen/notifications_content_margin"
+ android:background="@color/white"
+ app:elevation="0dp">
+
+ <LinearLayout
+ android:id="@+id/notifications_filter"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_scrollFlags="scroll|enterAlways">
+
+ <RadioGroup
+ android:id="@+id/notifications_radio_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:checkedButton="@+id/notifications_filter_all"
+ android:orientation="horizontal"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:background="@drawable/calypso_segmented_control_background">
+
+ <org.wordpress.android.widgets.WPRadioButton
+ android:id="@+id/notifications_filter_all"
+ style="@style/Calypso.SegmentedControl"
+ android:background="@drawable/calypso_segmented_control_button_start"
+ android:text="@string/all" />
+
+ <org.wordpress.android.widgets.WPRadioButton
+ android:id="@+id/notifications_filter_unread"
+ style="@style/Calypso.SegmentedControl"
+ android:background="@drawable/calypso_segmented_control_button"
+ android:layout_marginLeft="-1dp"
+ android:layout_marginStart="-1dp"
+ android:text="@string/unread" />
+
+ <org.wordpress.android.widgets.WPRadioButton
+ android:id="@+id/notifications_filter_comments"
+ style="@style/Calypso.SegmentedControl"
+ android:background="@drawable/calypso_segmented_control_button"
+ android:layout_marginLeft="-1dp"
+ android:layout_marginStart="-1dp"
+ android:text="@string/tab_comments" />
+
+ <org.wordpress.android.widgets.WPRadioButton
+ android:id="@+id/notifications_filter_follows"
+ style="@style/Calypso.SegmentedControl"
+ android:background="@drawable/calypso_segmented_control_button"
+ android:layout_marginLeft="-1dp"
+ android:layout_marginStart="-1dp"
+ android:text="@string/follows" />
+
+ <org.wordpress.android.widgets.WPRadioButton
+ android:id="@+id/notifications_filter_likes"
+ style="@style/Calypso.SegmentedControl"
+ android:background="@drawable/calypso_segmented_control_button_end"
+ android:layout_marginLeft="-1dp"
+ android:layout_marginStart="-1dp"
+ android:text="@string/stats_likes" />
+ </RadioGroup>
+
+ <View
+ android:id="@+id/notifications_filter_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider" />
+
+ </LinearLayout>
+ </android.support.design.widget.AppBarLayout>
+
+ <LinearLayout
+ android:id="@+id/empty_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_double_extra_large"
+ android:fontFamily="sans-serif-light"
+ android:id="@+id/text_empty"
+ android:text="@string/notifications_empty_list" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:id="@+id/text_empty_description"
+ android:textSize="@dimen/text_sz_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:textColor="@color/grey" />
+
+ <android.support.v7.widget.CardView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/empty_list_button_top_margin"
+ card_view:cardBackgroundColor="@color/blue_medium"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/button_empty_action"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:textAllCaps="true"
+ android:textColor="@color/white" />
+
+ </android.support.v7.widget.CardView>
+
+ </LinearLayout>
+</android.support.design.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/notifications_list_item.xml b/WordPress/src/main/res/layout/notifications_list_item.xml
new file mode 100644
index 000000000..9f9dc4985
--- /dev/null
+++ b/WordPress/src/main/res/layout/notifications_list_item.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/time_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:background="@color/white"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.NoticonTextView
+ android:id="@+id/header_date_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="-3dp"
+ android:layout_marginStart="-3dp"
+ android:layout_marginBottom="-1dp"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="18sp"
+ android:text="@string/noticon_clock" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/header_date_text"
+ style="@style/Calypso.Text.Header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginStart="@dimen/margin_small"
+ android:textAllCaps="true"
+ tools:text="TODAY" />
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/notifications_list_divider_full_width" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/note_content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_medium">
+
+ <FrameLayout
+ android:id="@+id/avatar_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="1dp">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/note_avatar"
+ android:layout_width="@dimen/notifications_avatar_sz"
+ android:layout_height="@dimen/notifications_avatar_sz"
+ android:layout_marginRight="@dimen/notifications_adjusted_font_margin"
+ android:layout_marginEnd="@dimen/notifications_adjusted_font_margin"
+ android:layout_marginTop="@dimen/margin_small" />
+
+ <LinearLayout
+ android:id="@+id/moderate_progress"
+ android:layout_width="@dimen/notifications_avatar_sz"
+ android:layout_height="@dimen/notifications_avatar_sz"
+ android:layout_marginTop="@dimen/margin_small"
+ android:background="@drawable/shape_oval_translucent"
+ android:gravity="center"
+ android:padding="@dimen/margin_large"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <ProgressBar
+ style="@android:style/Widget.Holo.ProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:indeterminate="true" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.NoticonTextView
+ android:id="@+id/note_icon"
+ android:layout_width="@dimen/note_icon_sz"
+ android:layout_height="@dimen/note_icon_sz"
+ android:layout_marginLeft="31dp"
+ android:layout_marginStart="31dp"
+ android:layout_marginTop="35dp"
+ android:background="@drawable/shape_oval_blue_white_stroke"
+ android:gravity="center"
+ android:textColor="@color/white"
+ android:textSize="14sp"
+ tools:text="@string/noticon_note" />
+ </FrameLayout>
+
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.widgets.NoticonTextView
+ android:id="@+id/note_subject_noticon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_small"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="18sp" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/note_subject"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="Bob Ross commented on your post Happy Trees" />
+
+ </FrameLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/note_detail"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-1dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large"
+ android:visibility="gone"
+ tools:text="What an amazing post!"
+ tools:visibility="visible" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="1dp"
+ android:background="@drawable/notifications_list_divider" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/notifications_settings_activity.xml b/WordPress/src/main/res/layout/notifications_settings_activity.xml
new file mode 100644
index 000000000..02f19e20b
--- /dev/null
+++ b/WordPress/src/main/res/layout/notifications_settings_activity.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/default_background">
+
+ <LinearLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/default_background"
+ android:orientation="vertical" />
+
+ <LinearLayout
+ android:id="@+id/notifications_settings_message_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/default_background"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/notifications_settings_message"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ app:fixWidowWords="true"
+ tools:text="Loading..." />
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/WordPress/src/main/res/layout/notifications_settings_switch.xml b/WordPress/src/main/res/layout/notifications_settings_switch.xml
new file mode 100644
index 000000000..3d6a68e11
--- /dev/null
+++ b/WordPress/src/main/res/layout/notifications_settings_switch.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="@dimen/margin_medium">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/notifications_switch_title"
+ style="@android:style/TextAppearance.Medium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:text="Comments" />
+
+ <TextView
+ android:id="@+id/notifications_switch_summary"
+ style="@android:style/TextAppearance.Small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:text="Some example sub text."
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <Switch
+ android:id="@+id/notifications_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginLeft="24dp"
+ android:layout_marginRight="24dp"
+ android:background="@color/notifications_settings_divider_color" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/number_picker_dialog.xml b/WordPress/src/main/res/layout/number_picker_dialog.xml
new file mode 100644
index 000000000..4cf25f7b3
--- /dev/null
+++ b/WordPress/src/main/res/layout/number_picker_dialog.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPSwitch
+ android:id="@+id/number_picker_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="24dp"
+ android:layout_marginRight="24dp"
+ android:textSize="@dimen/text_sz_large"
+ android:textColor="@color/calypso_title_text" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/number_picker_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="24dp"
+ android:layout_marginRight="24dp"
+ android:textSize="@dimen/text_sz_small"
+ android:textColor="@color/grey_darken_10" />
+
+ <LinearLayout
+ android:id="@+id/number_picker_toggleable"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/number_picker_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="24dp"
+ android:layout_marginTop="18dp"
+ android:layout_marginRight="24dp"
+ android:layout_marginBottom="8dp"
+ android:duplicateParentState="true"
+ android:textSize="@dimen/text_sz_large"
+ android:textColor="@color/calypso_title_text" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_30" />
+
+ <org.wordpress.android.ui.WPNumberPicker
+ android:id="@+id/number_picker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:duplicateParentState="true"
+ android:paddingTop="36dp" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_30" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/page_item.xml b/WordPress/src/main/res/layout/page_item.xml
new file mode 100644
index 000000000..384387ec0
--- /dev/null
+++ b/WordPress/src/main/res/layout/page_item.xml
@@ -0,0 +1,123 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/page_item_background"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/divider_top"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_20" />
+
+ <LinearLayout
+ android:id="@+id/header_date"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/grey_light"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_date"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/noticon_clock"
+ android:drawablePadding="@dimen/margin_small"
+ android:padding="@dimen/margin_medium"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_date" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_20" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_large">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/btn_more"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/text_date"
+ android:ellipsize="end"
+ android:maxLines="3"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ app:wpFontFamily="merriweather"
+ tools:text="text_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small"
+ android:visibility="gone"
+ tools:drawableLeft="@drawable/noticon_scheduled"
+ tools:text="text_status"
+ tools:visibility="visible" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/btn_more"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_medium"
+ android:contentDescription="@string/more"
+ app:srcCompat="@drawable/ic_action_more" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/disabled_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/translucent_white"
+ android:clickable="true"
+ android:focusable="true">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:indeterminate="true"/>
+ </RelativeLayout>
+ </FrameLayout>
+
+
+
+ <View
+ android:id="@+id/divider_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_20" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/people_invite_error_view.xml b/WordPress/src/main/res/layout/people_invite_error_view.xml
new file mode 100644
index 000000000..0997d1391
--- /dev/null
+++ b/WordPress/src/main/res/layout/people_invite_error_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:padding="5dp"
+ android:textColor="@color/alert_red"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="Username 'qwer' is not found"/>
diff --git a/WordPress/src/main/res/layout/people_invite_fragment.xml b/WordPress/src/main/res/layout/people_invite_fragment.xml
new file mode 100644
index 000000000..12ee2ba50
--- /dev/null
+++ b/WordPress/src/main/res/layout/people_invite_fragment.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white"
+ android:clickable="true"
+ android:clipToPadding="false"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:scrollbarStyle="outsideInset">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/usernames_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:text="@string/invite_names_title"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large" />
+
+ <LinearLayout
+ android:id="@+id/usernames_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_extra_large"
+ android:layout_marginTop="5dp"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.FlowLayout
+ android:id="@+id/usernames"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:background="@drawable/invites_border"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="2dp"
+ android:paddingRight="2dp">
+
+ <org.wordpress.android.widgets.MultiUsernameEditText
+ android:id="@+id/invite_usernames"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/transparent"
+ android:imeOptions="actionDone"
+ android:inputType="text"
+ android:minWidth="40dp"
+ android:paddingBottom="0dp"
+ android:paddingLeft="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_medium"
+ android:paddingTop="@dimen/margin_large"
+ android:singleLine="true"
+ android:textColor="@color/grey_dark"
+ android:textColorHint="#AAAAAA"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="sdfwefef" />
+ </org.wordpress.android.widgets.FlowLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="5dp"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="@dimen/text_sz_small"
+ android:textStyle="italic"
+ android:text="@string/invite_message_usernames_limit"/>
+
+ <LinearLayout
+ android:id="@+id/username_errors_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/role_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginBottom="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:text="@string/role"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="@string/role" />
+
+ <ImageView
+ android:id="@+id/imgRoleInfo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"
+ android:src="@drawable/dashicon_info" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/role"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dp"
+ android:layout_marginTop="5dp"
+ android:drawableRight="@drawable/arrow"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="@string/role" />
+
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/message_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:text="@string/invite_message_title"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:background="@drawable/invites_border"
+ android:padding="2dp">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/message"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="0dp"
+ android:gravity="top"
+ android:inputType="textCapSentences|textAutoCorrect|textMultiLine"
+ android:maxLength="@integer/invite_message_char_limit"
+ android:minLines="4" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/message_remaining"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/message"
+ android:gravity="right"
+ android:padding="5dp"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="@dimen/text_sz_small" />
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/message_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="5dp"
+ android:text="@string/invite_message_info"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="@dimen/text_sz_small"
+ android:textStyle="italic" />
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/people_list_fragment.xml b/WordPress/src/main/res/layout/people_list_fragment.xml
new file mode 100644
index 000000000..755996a7d
--- /dev/null
+++ b/WordPress/src/main/res/layout/people_list_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white">
+
+ <org.wordpress.android.ui.FilteredRecyclerView
+ android:id="@+id/filtered_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:scrollbars="vertical"/>
+
+ <ProgressBar
+ android:id="@+id/progress_footer"
+ style="@style/ReaderProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/people_list_row.xml b/WordPress/src/main/res/layout/people_list_row.xml
new file mode 100644
index 000000000..a47f5dc25
--- /dev/null
+++ b/WordPress/src/main/res/layout/people_list_row.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/people_list_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:minHeight="@dimen/people_list_row_height">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/person_avatar"
+ style="@style/PersonAvatar" />
+
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_role"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginEnd="@dimen/margin_extra_large"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginStart="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/people_list_row_role_margin_top"
+ android:ellipsize="end"
+ android:gravity="end"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="role" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_display_name"
+ style="@style/PersonTextView"
+ android:layout_alignTop="@id/person_avatar"
+ android:layout_toEndOf="@id/person_avatar"
+ android:layout_toLeftOf="@id/person_role"
+ android:layout_toRightOf="@id/person_avatar"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="display_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_username"
+ style="@style/PersonTextView"
+ android:layout_alignLeft="@id/person_display_name"
+ android:layout_alignStart="@id/person_display_name"
+ android:layout_below="@id/person_display_name"
+ android:layout_toLeftOf="@id/person_role"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="username" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/follower_subscribed_date"
+ style="@style/PersonTextView"
+ android:layout_alignLeft="@id/person_display_name"
+ android:layout_alignStart="@id/person_display_name"
+ android:layout_below="@id/person_username"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_small"
+ android:layout_toLeftOf="@id/person_role"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="Since {date}" />
+
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/people_management_activity.xml b/WordPress/src/main/res/layout/people_management_activity.xml
new file mode 100644
index 000000000..c03271ff8
--- /dev/null
+++ b/WordPress/src/main/res/layout/people_management_activity.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar"/>
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/person_detail_fragment.xml b/WordPress/src/main/res/layout/person_detail_fragment.xml
new file mode 100644
index 000000000..3c7a2f5ab
--- /dev/null
+++ b/WordPress/src/main/res/layout/person_detail_fragment.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white"
+ android:clickable="true">
+
+ <RelativeLayout
+ android:id="@+id/person_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/color_primary"
+ android:elevation="@dimen/card_elevation"
+ android:paddingBottom="@dimen/margin_medium"
+ tools:targetApi="LOLLIPOP">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/person_avatar"
+ style="@style/PersonAvatar"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_display_name"
+ style="@style/PersonTextView"
+ android:layout_alignTop="@id/person_avatar"
+ android:layout_toEndOf="@id/person_avatar"
+ android:layout_toRightOf="@id/person_avatar"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_extra_large"
+ android:textStyle="bold"
+ tools:text="display_name"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_username"
+ style="@style/PersonTextView"
+ android:layout_alignLeft="@id/person_display_name"
+ android:layout_alignStart="@id/person_display_name"
+ android:layout_below="@id/person_display_name"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="username"/>
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/person_role_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/people_list_row_height"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_below="@id/person_details"
+ android:background="?android:selectableItemBackground"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingEnd="@dimen/margin_extra_large"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingStart="@dimen/margin_extra_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_role_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:text="@string/role"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="@string/role"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/person_role"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="@string/role"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/subscribed_date_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/people_list_row_height"
+ android:layout_alignLeft="@id/person_role_container"
+ android:layout_alignStart="@id/person_role_container"
+ android:layout_below="@id/person_role_container"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingEnd="@dimen/margin_extra_large"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingStart="@dimen/margin_extra_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/subscribed_date_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="@string/title_follower"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/subscribed_date_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="Since {date}"/>
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/people_list_divider_height"
+ android:layout_below="@id/person_role_container"
+ android:background="@color/grey_lighten_20"/>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/plan_feature_item.xml b/WordPress/src/main/res/layout/plan_feature_item.xml
new file mode 100644
index 000000000..3cdc3dfc7
--- /dev/null
+++ b/WordPress/src/main/res/layout/plan_feature_item.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium">
+
+ <com.android.volley.toolbox.NetworkImageView
+ android:id="@+id/image_icon"
+ android:layout_width="28dp"
+ android:layout_height="28dp"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ tools:background="@drawable/noticon_publish"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_feature_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_toRightOf="@+id/image_icon"
+ android:ellipsize="end"
+ android:textColor="@color/grey_darken_20"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ tools:text="text_feature_title"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_feature_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/text_feature_title"
+ android:layout_marginTop="@dimen/margin_small"
+ android:layout_toRightOf="@+id/image_icon"
+ android:ellipsize="end"
+ android:textColor="@color/grey_darken_20"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_feature_description"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/plan_fragment.xml b/WordPress/src/main/res/layout/plan_fragment.xml
new file mode 100644
index 000000000..94034d56e
--- /dev/null
+++ b/WordPress/src/main/res/layout/plan_fragment.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.wordpress.android.widgets.WPScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin">
+
+ <com.android.volley.toolbox.NetworkImageView
+ android:id="@+id/image_plan_icon"
+ android:layout_width="@dimen/plan_icon_size"
+ android:layout_height="@dimen/plan_icon_size"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ tools:background="@drawable/penandink" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_product_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/text_sz_extra_large"
+ tools:text="text_product_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_tagline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="italic"
+ tools:text="text_tagline" />
+
+ <!-- plan features are added to this container view -->
+ <LinearLayout
+ android:id="@+id/plan_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:background="@color/divider_grey" />
+
+ </LinearLayout>
+</org.wordpress.android.widgets.WPScrollView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/plan_post_purchase_activity.xml b/WordPress/src/main/res/layout/plan_post_purchase_activity.xml
new file mode 100644
index 000000000..667524eaf
--- /dev/null
+++ b/WordPress/src/main/res/layout/plan_post_purchase_activity.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/blue_wordpress"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/viewpager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/divider"/>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_above="@+id/layout_nav"
+ android:background="#1078A4"/>
+
+ <RelativeLayout
+ android:id="@+id/layout_nav"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:layout_alignParentBottom="true"
+ android:background="@color/color_primary"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_skip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_medium"
+ android:text="@string/button_skip"
+ android:textAllCaps="true"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_large"/>
+
+ <LinearLayout
+ android:id="@+id/layout_indicator_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/image_indicator_1"
+ android:layout_width="@dimen/plan_indicator_size"
+ android:layout_height="@dimen/plan_indicator_size"
+ android:layout_marginLeft="@dimen/plan_indicator_margin"
+ android:layout_marginRight="@dimen/plan_indicator_margin"
+ android:background="@drawable/indicator_circle_selected"/>
+
+ <ImageView
+ android:id="@+id/image_indicator_2"
+ android:layout_width="@dimen/plan_indicator_size"
+ android:layout_height="@dimen/plan_indicator_size"
+ android:layout_marginLeft="@dimen/plan_indicator_margin"
+ android:layout_marginRight="@dimen/plan_indicator_margin"
+ android:background="@drawable/indicator_circle_unselected"/>
+
+ <ImageView
+ android:id="@+id/image_indicator_3"
+ android:layout_width="@dimen/plan_indicator_size"
+ android:layout_height="@dimen/plan_indicator_size"
+ android:layout_marginLeft="@dimen/plan_indicator_margin"
+ android:layout_marginRight="@dimen/plan_indicator_margin"
+ android:background="@drawable/indicator_circle_unselected"/>
+
+ <ImageView
+ android:id="@+id/image_indicator_4"
+ android:layout_width="@dimen/plan_indicator_size"
+ android:layout_height="@dimen/plan_indicator_size"
+ android:layout_marginLeft="@dimen/plan_indicator_margin"
+ android:layout_marginRight="@dimen/plan_indicator_margin"
+ android:background="@drawable/indicator_circle_unselected"/>
+
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_medium"
+ android:text="@string/button_next"
+ android:textAllCaps="true"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_large"/>
+ </RelativeLayout>
+
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/plan_post_purchase_fragment.xml b/WordPress/src/main/res/layout/plan_post_purchase_fragment.xml
new file mode 100644
index 000000000..4b301a895
--- /dev/null
+++ b/WordPress/src/main/res/layout/plan_post_purchase_fragment.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.percent.PercentRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/percent_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin"
+ tools:background="@color/blue_wordpress">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ app:layout_marginBottomPercent="10%"
+ app:layout_marginTopPercent="25%"
+ tools:src="@drawable/plans_business_active" />
+
+ <LinearLayout
+ android:id="@+id/layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/image"
+ android:layout_centerHorizontal="true"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ app:layout_widthPercent="75%">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_light"
+ android:textSize="@dimen/text_sz_double_extra_large"
+ tools:text="text_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_light"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="text_description_text_description_text_description_text_description_text_description_text_description" />
+
+ <org.wordpress.android.widgets.WPButton
+ android:id="@+id/button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:textColor="@color/blue_wordpress"
+ tools:text="Button" />
+ </LinearLayout>
+
+</android.support.percent.PercentRelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/plan_section_title.xml b/WordPress/src/main/res/layout/plan_section_title.xml
new file mode 100644
index 000000000..a905fc694
--- /dev/null
+++ b/WordPress/src/main/res/layout/plan_section_title.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/divider_grey"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_section_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:ellipsize="end"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ tools:text="text_section_title"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/plans_activity.xml b/WordPress/src/main/res/layout/plans_activity.xml
new file mode 100644
index 000000000..67bcdceb1
--- /dev/null
+++ b/WordPress/src/main/res/layout/plans_activity.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/toolbar"
+ android:background="@color/tab_background"
+ android:visibility="gone"
+ app:tabIndicatorColor="@color/tab_indicator"
+ app:tabMode="scrollable"
+ tools:visibility="visible" />
+
+ </android.support.design.widget.AppBarLayout>
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/viewpager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/frame_manage"
+ android:layout_below="@+id/appbar"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/progress_loading_plans"
+ style="@style/PlansProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <FrameLayout
+ android:id="@+id/frame_manage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@color/color_primary"
+ android:elevation="@dimen/appbar_elevation"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_manage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="web"
+ android:gravity="center"
+ android:linksClickable="false"
+ android:minHeight="@dimen/toolbar_height"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin"
+ android:text="@string/plans_manage"
+ android:textColor="@color/white"
+ android:textColorLink="@color/blue_light"
+ android:textSize="@dimen/text_sz_large" />
+ </FrameLayout>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/popup_menu_item.xml b/WordPress/src/main/res/layout/popup_menu_item.xml
new file mode 100644
index 000000000..518ba57ae
--- /dev/null
+++ b/WordPress/src/main/res/layout/popup_menu_item.xml
@@ -0,0 +1,25 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/menu_item_height"
+ android:gravity="left|center_vertical"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/menu_item_margin"
+ android:paddingRight="@dimen/menu_item_margin">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/margin_large"
+ android:tint="@color/grey_dark"
+ tools:src="@drawable/noticon_trash" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?textAppearanceSmallPopupMenu"
+ tools:text="Menu item" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/post_cardview.xml b/WordPress/src/main/res/layout/post_cardview.xml
new file mode 100644
index 000000000..f9007adff
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_cardview.xml
@@ -0,0 +1,164 @@
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:wp="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:stateListAnimator="@anim/pressed_card"
+ card_view:cardBackgroundColor="@color/white"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation"
+ tools:targetApi="LOLLIPOP">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_featured"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/postlist_featured_image_height"
+ android:layout_below="@+id/layout_post_header"
+ android:scaleType="centerCrop"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ style="@style/ReaderTextView.Post.Title"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ tools:text="text_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_excerpt"
+ style="@style/ReaderTextView.Post.Excerpt"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:maxLines="4"
+ tools:text="text_excerpt" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_date"
+ style="@style/ReaderTextView.Date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ tools:text="text_date" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:gravity="center_vertical"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small"
+ android:visibility="gone"
+ tools:drawableLeft="@drawable/noticon_scheduled"
+ tools:text="text_status"
+ tools:visibility="visible" />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:background="@color/grey_light">
+
+ <LinearLayout
+ android:id="@+id/layout_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_edit"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ wp:wpPostButtonType="edit" />
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_view"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ wp:wpPostButtonType="view" />
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_publish"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ wp:wpPostButtonType="publish" />
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_stats"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:visibility="gone"
+ tools:visibility="visible"
+ wp:wpPostButtonType="stats" />
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_trash"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:visibility="gone"
+ tools:visibility="visible"
+ wp:wpPostButtonType="trash" />
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_more"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ wp:wpPostButtonType="more" />
+
+ <org.wordpress.android.widgets.PostListButton
+ android:id="@+id/btn_back"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ wp:wpPostButtonType="back" />
+
+ </LinearLayout>
+ </FrameLayout>
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/disabled_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/translucent_white"
+ android:clickable="true"
+ android:focusable="true">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:indeterminate="true"/>
+ </RelativeLayout>
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/post_list_activity.xml b/WordPress/src/main/res/layout/post_list_activity.xml
new file mode 100644
index 000000000..4777e7457
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_list_activity.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <fragment
+ android:id="@+id/postList"
+ android:name="org.wordpress.android.ui.posts.PostsListFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/post_list_button.xml b/WordPress/src/main/res/layout/post_list_button.xml
new file mode 100644
index 000000000..e81fda1cf
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_list_button.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_large">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_marginRight="@dimen/margin_extra_small"
+ tools:src="@drawable/noticon_more" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:maxLines="1"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/post_list_fragment.xml b/WordPress/src/main/res/layout/post_list_fragment.xml
new file mode 100644
index 000000000..dab754d94
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_list_fragment.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/empty_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:animateLayoutChanges="true"
+ android:background="@color/background_grey"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <ImageView
+ android:id="@+id/image_empty"
+ android:layout_width="112dp"
+ android:layout_height="86dp"
+ android:layout_marginBottom="@dimen/margin_medium"
+ app:srcCompat="@drawable/penandink" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/title_empty"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:text="@string/empty_list_default"
+ app:fixWidowWords="true" />
+
+ </LinearLayout>
+
+ <android.support.design.widget.CoordinatorLayout
+ android:id="@+id/coordinator"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingTop="@dimen/margin_medium"
+ android:scrollbars="vertical" />
+
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|bottom"
+ android:layout_marginBottom="@dimen/fab_margin"
+ android:layout_marginRight="@dimen/fab_margin"
+ app:srcCompat="@drawable/gridicon_create_light"
+ android:contentDescription="@string/new_post"
+ app:borderWidth="0dp"
+ app:rippleColor="@color/fab_pressed" />
+
+ </android.support.design.widget.CoordinatorLayout>
+
+ <ProgressBar
+ android:id="@+id/progress"
+ style="?android:attr/progressBarStyleInverse"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/post_location_settings.xml b/WordPress/src/main/res/layout/post_location_settings.xml
new file mode 100644
index 000000000..418ce1ded
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_location_settings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/sectionLocation"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:layout_marginTop="8dp">
+
+ <TextView
+ android:id="@+id/locationLabel"
+ style="@style/WordPressSubHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/location"
+ android:textSize="@dimen/text_sz_large"
+ android:layout_marginBottom="@dimen/margin_small" />
+
+ <include layout="@layout/post_location_settings_add" />
+
+ <!-- state: Location search -->
+ <include layout="@layout/post_location_settings_search" />
+
+ <!-- state: Location Set -->
+ <include layout="@layout/post_location_settings_view" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/post_location_settings_add.xml b/WordPress/src/main/res/layout/post_location_settings_add.xml
new file mode 100644
index 000000000..ac9e3aff1
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_location_settings_add.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/sectionLocationAdd"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/locationText"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/addLocation"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_location"
+ android:textSize="@dimen/text_sz_large"
+ android:drawableLeft="@drawable/ic_location_on_grey600_24dp"
+ android:drawablePadding="8dp" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/post_location_settings_search.xml b/WordPress/src/main/res/layout/post_location_settings_search.xml
new file mode 100644
index 000000000..9cd99bdda
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_location_settings_search.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/sectionLocationSearch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:orientation="horizontal">
+
+ <EditText
+ android:id="@+id/searchLocationText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:hint="@string/current_location"
+ android:imeOptions="actionSearch"
+ android:inputType="text"
+ android:textSize="@dimen/text_sz_large" />
+
+ <Button
+ android:id="@+id/searchLocation"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:lines="1"
+ android:text="@string/search_current_location"
+ android:textSize="@dimen/text_sz_large" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/post_location_settings_view.xml b/WordPress/src/main/res/layout/post_location_settings_view.xml
new file mode 100644
index 000000000..b35cdc766
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_location_settings_view.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/sectionLocationView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/locationText"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/locationText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/selectable_background_wordpress"
+ android:drawableLeft="@drawable/ic_action_location_searching"
+ android:gravity="center_vertical"
+ android:paddingBottom="4dp"
+ android:paddingRight="4dp"
+ android:text="@string/loading"
+ android:textSize="@dimen/text_sz_large" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/updateLocation"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_marginLeft="0dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginStart="0dp"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:text="@string/edit_location"
+ android:textSize="@dimen/text_sz_large" />
+
+ <Button
+ android:id="@+id/removeLocation"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="0dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:lines="1"
+ android:text="@string/remove"
+ android:textSize="@dimen/text_sz_large" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/post_preview_activity.xml b/WordPress/src/main/res/layout/post_preview_activity.xml
new file mode 100644
index 000000000..e691a7a8b
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_preview_activity.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white">
+
+ <include layout="@layout/toolbar" />
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/message_container"
+ android:layout_below="@+id/toolbar" />
+
+ <RelativeLayout
+ android:id="@+id/message_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:paddingLeft="@dimen/content_margin"
+ android:paddingRight="@dimen/content_margin"
+ android:visibility="gone"
+ android:background="@color/snackbar_background_color"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/message_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_toLeftOf="@+id/layout_buttons"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="Explanation of what a local draft is or what local changes are" />
+
+ <LinearLayout
+ android:id="@+id/layout_buttons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/btn_publish"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:background="?android:selectableItemBackground"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:text="@string/button_publish"
+ android:textAllCaps="true"
+ android:textColor="@color/color_accent"
+ android:textSize="@dimen/text_sz_small" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/btn_revert"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:background="?android:selectableItemBackground"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:text="@string/button_revert"
+ android:textAllCaps="true"
+ android:textColor="@color/color_accent"
+ android:textSize="@dimen/text_sz_small" />
+ </LinearLayout>
+ </RelativeLayout>
+
+ <ProgressBar
+ android:id="@+id/progress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/post_preview_fragment.xml b/WordPress/src/main/res/layout/post_preview_fragment.xml
new file mode 100644
index 000000000..341443776
--- /dev/null
+++ b/WordPress/src/main/res/layout/post_preview_fragment.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <WebView
+ android:id="@+id/webView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbarStyle="outsideInset" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/preference_category.xml b/WordPress/src/main/res/layout/preference_category.xml
new file mode 100644
index 000000000..75c957422
--- /dev/null
+++ b/WordPress/src/main/res/layout/preference_category.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_extra_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_medium"
+ android:textStyle="bold"
+ android:fontFamily="sans-serif-light" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/preference_coordinator.xml b/WordPress/src/main/res/layout/preference_coordinator.xml
new file mode 100644
index 000000000..b1475513f
--- /dev/null
+++ b/WordPress/src/main/res/layout/preference_coordinator.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/coordinator"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+</android.support.design.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/preference_layout.xml b/WordPress/src/main/res/layout/preference_layout.xml
new file mode 100644
index 000000000..9e3c3d845
--- /dev/null
+++ b/WordPress/src/main/res/layout/preference_layout.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/margin_extra_large">
+
+ <LinearLayout
+ android:id="@+android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ android:orientation="vertical" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@android:id/widget_frame"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/calypso_title_text"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/calypso_subtitle_text"
+ android:textSize="@dimen/text_sz_medium" />
+
+ </LinearLayout>
+
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/progressbar.xml b/WordPress/src/main/res/layout/progressbar.xml
new file mode 100644
index 000000000..ea8390558
--- /dev/null
+++ b/WordPress/src/main/res/layout/progressbar.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+</ProgressBar> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/promo_dialog.xml b/WordPress/src/main/res/layout/promo_dialog.xml
new file mode 100644
index 000000000..66a0e79ad
--- /dev/null
+++ b/WordPress/src/main/res/layout/promo_dialog.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="280dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+
+ <ImageView
+ android:id="@+id/promo_dialog_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ tools:src="@drawable/stats_widget_promo_header" />
+
+ <LinearLayout
+ android:id="@+id/promo_dialog_text"
+ android:layout_width="280dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/promo_dialog_image"
+ android:orientation="vertical"
+ android:padding="24dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/promo_dialog_title"
+ style="@style/StatsModuleTitle"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="@string/stats_widget_promo_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/promo_dialog_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:textColor="@color/grey_darken_20"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="@string/new_editor_promo_desc" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="280dp"
+ android:layout_height="52dp"
+ android:layout_below="@+id/promo_dialog_text"
+ android:padding="8dp"
+ android:gravity="right|end">
+
+ <android.support.v7.widget.AppCompatButton
+ android:id="@+id/promo_dialog_cancel_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="36dp"
+ android:textAllCaps="true"
+ android:textColor="@color/blue_medium"
+ tools:text="@string/stats_widget_promo_ok_btn_label"/>
+ </LinearLayout>
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_activity_comment_list.xml b/WordPress/src/main/res/layout/reader_activity_comment_list.xml
new file mode 100644
index 000000000..7aef53c7d
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_activity_comment_list.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_empty"
+ style="@style/ReaderTextView.EmptyList"
+ android:layout_above="@+id/layout_bottom"
+ android:layout_below="@+id/toolbar"
+ android:text="@string/reader_empty_comments"
+ android:textColor="@color/grey_darken_10"
+ android:visibility="gone"
+ app:fixWidowWords="true"
+ tools:visibility="visible" />
+
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/swipe_to_refresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/layout_bottom"
+ android:layout_below="@+id/toolbar">
+
+ <org.wordpress.android.ui.reader.views.ReaderRecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+
+ <RelativeLayout
+ android:id="@+id/layout_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+
+ <include
+ android:id="@+id/layout_comment_box"
+ layout="@layout/reader_include_comment_box"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_comments_closed"
+ style="@style/ReaderTextView.Label.Large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:gravity="center_horizontal"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_large"
+ android:text="@string/reader_label_comments_closed"
+ android:visibility="gone" />
+ </RelativeLayout>
+
+ <ProgressBar
+ android:id="@+id/progress_loading"
+ style="@style/ReaderProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_activity_photo_viewer.xml b/WordPress/src/main/res/layout/reader_activity_photo_viewer.xml
new file mode 100644
index 000000000..b22683345
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_activity_photo_viewer.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/grey_dark">
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/viewpager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:background="@drawable/reader_photo_count_background"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:textColor="@color/grey_light"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="title" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_activity_post_list.xml b/WordPress/src/main/res/layout/reader_activity_post_list.xml
new file mode 100644
index 000000000..298cb9d32
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_activity_post_list.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include layout="@layout/toolbar" />
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_activity_post_pager.xml b/WordPress/src/main/res/layout/reader_activity_post_pager.xml
new file mode 100644
index 000000000..a5bd8ce6a
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_activity_post_pager.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/viewpager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <ProgressBar
+ android:id="@+id/progress_loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_activity_subs.xml b/WordPress/src/main/res/layout/reader_activity_subs.xml
new file mode 100644
index 000000000..01c6295ac
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_activity_subs.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white">
+
+ <android.support.design.widget.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/toolbar"
+ android:background="@color/tab_background"
+ app:tabIndicatorColor="@color/tab_indicator" />
+ </android.support.design.widget.AppBarLayout>
+
+ <org.wordpress.android.widgets.WPViewPager
+ android:id="@+id/viewpager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/divider_list"
+ android:layout_below="@+id/appbar" />
+
+ <View
+ android:id="@+id/divider_list"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_above="@+id/layout_bottom"
+ android:layout_alignWithParentIfMissing="true"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/layout_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_small">
+
+ <EditText
+ android:id="@+id/edit_add"
+ style="@style/ReaderEditText.Topic"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:hint="@string/reader_hint_add_tag_or_url" />
+
+ <ImageButton
+ android:id="@+id/btn_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_small"
+ android:contentDescription="@string/add"
+ app:srcCompat="@drawable/reader_follow" />
+
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/progress_follow"
+ style="@style/ReaderProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_activity_userlist.xml b/WordPress/src/main/res/layout/reader_activity_userlist.xml
new file mode 100644
index 000000000..7221914d9
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_activity_userlist.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <org.wordpress.android.ui.reader.views.ReaderRecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical"
+ tools:listitem="@layout/reader_listitem_user" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_cardview_post.xml b/WordPress/src/main/res/layout/reader_cardview_post.xml
new file mode 100644
index 000000000..79d1f1db6
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_cardview_post.xml
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:wp="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:stateListAnimator="@anim/pressed_card"
+ card_view:cardBackgroundColor="@color/white"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation"
+ tools:targetApi="LOLLIPOP">
+
+ <LinearLayout
+ android:id="@+id/layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/layout_post_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/reader_card_content_padding"
+ android:paddingRight="@dimen/reader_card_content_padding"
+ android:paddingTop="@dimen/margin_large">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_blavatar"
+ style="@style/ReaderImageView.Avatar"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="@dimen/margin_large"
+ tools:src="@drawable/blavatar_placeholder" />
+
+ <org.wordpress.android.ui.reader.views.ReaderFollowButton
+ android:id="@+id/follow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_large" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_toLeftOf="@+id/follow_button"
+ android:layout_toRightOf="@+id/image_blavatar"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_blog_name"
+ style="@style/ReaderTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_blog_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_domain"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_domain" />
+ </LinearLayout>
+
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_featured"
+ style="@style/ReaderImageView.Featured"
+ android:layout_marginBottom="@dimen/margin_small" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ style="@style/ReaderTextView.Post.Title"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_large"
+ tools:text="text_title" />
+
+ <LinearLayout
+ android:id="@+id/layout_dateline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_extra_small"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_avatar"
+ style="@style/ReaderImageView.Avatar.Tiny"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="@dimen/margin_medium"
+ tools:src="@drawable/gravatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_dateline"
+ style="@style/ReaderTextView.Label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ tools:text="text_dateline" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_excerpt"
+ style="@style/ReaderTextView.Post.Excerpt"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_medium"
+ tools:text="text_excerpt" />
+
+ <org.wordpress.android.ui.reader.views.ReaderThumbnailStrip
+ android:id="@+id/thumbnail_strip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_large"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <!-- attribution section for discover posts -->
+ <LinearLayout
+ android:id="@+id/layout_discover"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_large"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_discover_avatar"
+ style="@style/ReaderImageView.Avatar.Small"
+ android:layout_marginRight="@dimen/margin_large"
+ android:background="?android:selectableItemBackground"
+ tools:src="@drawable/gravatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_discover"
+ style="@style/ReaderTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="3"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_attribution" />
+
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/layout_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_tag"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/count_comments"
+ android:background="?android:selectableItemBackground"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small"
+ android:textColor="@color/reader_hyperlink"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="#text_tag" />
+
+ <org.wordpress.android.ui.reader.views.ReaderIconCountView
+ android:id="@+id/count_comments"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/count_likes"
+ android:padding="@dimen/margin_medium"
+ wp:readerIcon="comment" />
+
+ <org.wordpress.android.ui.reader.views.ReaderIconCountView
+ android:id="@+id/count_likes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/image_more"
+ android:padding="@dimen/margin_medium"
+ wp:readerIcon="like" />
+
+ <ImageView
+ android:id="@+id/image_more"
+ android:layout_width="@dimen/reader_more_icon"
+ android:layout_height="@dimen/reader_more_icon"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/more"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingTop="@dimen/margin_medium"
+ app:srcCompat="@drawable/ic_action_more_grey" />
+
+ </RelativeLayout>
+
+ </LinearLayout>
+</android.support.v7.widget.CardView>
diff --git a/WordPress/src/main/res/layout/reader_cardview_xpost.xml b/WordPress/src/main/res/layout/reader_cardview_xpost.xml
new file mode 100644
index 000000000..7f1e9e54d
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_cardview_xpost.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:stateListAnimator="@anim/pressed_card"
+ card_view:cardBackgroundColor="@color/grey_lighten_30"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="0dp"
+ tools:targetApi="LOLLIPOP">
+
+ <LinearLayout
+ android:id="@+id/layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/reader_card_content_padding"
+ android:paddingRight="@dimen/reader_card_content_padding"
+ android:paddingTop="@dimen/margin_large">
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_blavatar"
+ android:layout_width="@dimen/blavatar_sz"
+ android:layout_height="@dimen/blavatar_sz"
+ android:layout_gravity="top|left"
+ tools:src="@drawable/blavatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_avatar"
+ style="@style/ReaderImageView.Avatar.Small"
+ android:layout_gravity="bottom|right"
+ android:layout_marginLeft="14dp"
+ android:layout_marginTop="14dp"
+ tools:src="@drawable/gravatar_placeholder" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ android:textStyle="bold"
+ tools:text="text_title"
+ app:wpFontFamily="merriweather" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_subtitle"
+ app:fixWidowWords="true" />
+
+ </LinearLayout>
+ </LinearLayout>
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_comments_post_header_view.xml b/WordPress/src/main/res/layout/reader_comments_post_header_view.xml
new file mode 100644
index 000000000..5e0e3627b
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_comments_post_header_view.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ post header on reader comment activity
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/layout_post_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground">
+
+ <!--
+ this inner frame layout is necessary to enforce a background color
+ while still enabling ?android:selectableItemBackground (above)
+ -->
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/grey_lighten_30"
+ android:paddingLeft="@dimen/reader_card_margin"
+ android:paddingRight="@dimen/reader_card_margin"
+ android:paddingTop="@dimen/margin_large">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/reader_card_content_padding"
+ android:paddingRight="@dimen/reader_card_content_padding">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_post_avatar"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="@dimen/margin_medium"
+ tools:src="@drawable/gravatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_blog_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_blog_name" />
+
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_post_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_medium"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ app:wpFontFamily="merriweather"
+ tools:text="text_post_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_post_dateline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_post_date" />
+
+ </LinearLayout>
+ </FrameLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_empty_view.xml b/WordPress/src/main/res/layout/reader_empty_view.xml
new file mode 100644
index 000000000..7b501ab3b
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_empty_view.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/toolbar_height"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <RelativeLayout
+ android:id="@+id/layout_box_images"
+ android:layout_width="wrap_content"
+ android:layout_height="100dp"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <ImageView
+ android:id="@+id/empty_tags_box_bottom"
+ android:layout_width="86dp"
+ android:layout_height="54dp"
+ android:layout_alignParentBottom="true"
+ app:srcCompat="@drawable/box_with_pages_bottom" />
+
+ <ImageView
+ android:id="@+id/empty_tags_box_page3"
+ android:layout_width="54dp"
+ android:layout_height="70dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="2dp"
+ app:srcCompat="@drawable/box_with_pages_page3" />
+
+ <ImageView
+ android:id="@+id/empty_tags_box_page2"
+ android:layout_width="53dp"
+ android:layout_height="70dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginBottom="15dp"
+ android:layout_marginLeft="17dp"
+ app:srcCompat="@drawable/box_with_pages_page2" />
+
+ <ImageView
+ android:id="@+id/empty_tags_box_page1"
+ android:layout_width="56dp"
+ android:layout_height="73.5dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="28dp"
+ app:srcCompat="@drawable/box_with_pages_page1" />
+
+ <ImageView
+ android:id="@+id/empty_tags_box_top"
+ android:layout_width="86dp"
+ android:layout_height="54dp"
+ android:layout_alignParentBottom="true"
+ app:srcCompat="@drawable/box_with_pages_top" />
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/title_empty"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:text="@string/reader_empty_posts_in_tag"
+ app:fixWidowWords="true" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/description_empty"
+ style="@style/WordPress.EmptyList.Description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/empty_list_description_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_description_side_margin"
+ android:layout_marginRight="@dimen/empty_list_description_side_margin"
+ android:text="@string/reader_empty_posts_in_tag"
+ app:fixWidowWords="true" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_follow_button.xml b/WordPress/src/main/res/layout/reader_follow_button.xml
new file mode 100644
index 000000000..d6c90544c
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_follow_button.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/frame_follow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small">
+
+ <ImageView
+ android:id="@+id/image_follow_button"
+ android:layout_width="@dimen/reader_follow_icon"
+ android:layout_height="@dimen/reader_follow_icon"
+ android:layout_gravity="center_vertical"
+ app:srcCompat="@drawable/reader_follow" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_follow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:gravity="center"
+ android:text="@string/reader_btn_follow"
+ android:textColor="@color/reader_follow_button_text"
+ android:textSize="@dimen/text_sz_medium" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_fragment_list.xml b/WordPress/src/main/res/layout/reader_fragment_list.xml
new file mode 100644
index 000000000..0a437b64d
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_fragment_list.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ simple list fragment used to show followed tags, popular tags, followed blogs and recommended blogs
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <org.wordpress.android.ui.reader.views.ReaderRecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical"
+ tools:listitem="@layout/reader_listitem_tag" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_empty"
+ style="@style/ReaderTextView.EmptyList"
+ android:layout_centerInParent="true"
+ android:text="@string/reader_empty_recommended_blogs"
+ android:textColor="@color/grey_darken_10"
+ android:visibility="gone"
+ app:fixWidowWords="true"
+ tools:visibility="visible" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_fragment_photo_viewer.xml b/WordPress/src/main/res/layout/reader_fragment_photo_viewer.xml
new file mode 100644
index 000000000..9a22850cc
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_fragment_photo_viewer.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.wordpress.android.ui.reader.views.ReaderPhotoView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/photo_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/WordPress/src/main/res/layout/reader_fragment_post_cards.xml b/WordPress/src/main/res/layout/reader_fragment_post_cards.xml
new file mode 100644
index 000000000..66a14fe9c
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_fragment_post_cards.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.ui.FilteredRecyclerView
+ android:id="@+id/reader_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <include
+ android:id="@+id/empty_custom_view"
+ layout="@layout/reader_empty_view" />
+
+ <LinearLayout
+ android:id="@+id/layout_new_posts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="24dp"
+ android:background="@drawable/reader_new_posts_bar_background"
+ android:elevation="@dimen/message_bar_elevation"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="@dimen/margin_large"
+ android:visibility="gone"
+ tools:targetApi="LOLLIPOP"
+ tools:visibility="visible">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/reader_label_new_posts"
+ android:textColor="@color/grey_lighten_30"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/reader_label_new_posts_subtitle"
+ android:textColor="@color/grey_lighten_30"
+ android:textSize="@dimen/text_sz_small" />
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/progress_footer"
+ style="@style/ReaderProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_fragment_post_detail.xml b/WordPress/src/main/res/layout/reader_fragment_post_detail.xml
new file mode 100644
index 000000000..90fa6054a
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_fragment_post_detail.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:id="@+id/layout_post_detail_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/white"
+ android:descendantFocusability="blocksDescendants">
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/swipe_to_refresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.widgets.WPScrollView
+ android:id="@+id/scroll_view_reader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:scrollbarStyle="insideOverlay">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_large">
+
+ <include
+ layout="@layout/reader_include_post_detail_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@+id/layout_post_detail_content"
+ android:layout_alignRight="@+id/layout_post_detail_content"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <include
+ layout="@layout/reader_include_post_detail_content"
+ android:layout_below="@+id/layout_post_detail_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="@dimen/reader_detail_margin"
+ android:layout_marginRight="@dimen/reader_detail_margin" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_related_posts_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/layout_post_detail_content"
+ android:layout_alignLeft="@+id/layout_post_detail_content"
+ android:layout_alignRight="@+id/layout_post_detail_content"
+ android:layout_marginBottom="@dimen/reader_related_post_margin"
+ android:layout_marginTop="@dimen/reader_related_post_margin"
+ android:text="@string/reader_label_related_posts"
+ android:textAllCaps="true"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <LinearLayout
+ android:id="@+id/container_related_posts"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/text_related_posts_label"
+ android:layout_alignLeft="@+id/layout_post_detail_content"
+ android:layout_alignRight="@+id/layout_post_detail_content"
+ android:background="@drawable/reader_related_posts_background"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/footer_spacer"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/toolbar_height"
+ android:layout_below="@id/container_related_posts" />
+
+ </RelativeLayout>
+
+ </org.wordpress.android.widgets.WPScrollView>
+
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+
+ <include
+ android:id="@+id/layout_post_detail_footer"
+ layout="@layout/reader_include_post_detail_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true" />
+
+ <ProgressBar
+ android:id="@+id/progress_loading"
+ style="@style/ReaderProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+ </RelativeLayout>
+
+ <!-- error message when requesting post fails -->
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginLeft="@dimen/reader_detail_margin"
+ android:layout_marginRight="@dimen/reader_detail_margin"
+ android:drawablePadding="@dimen/margin_small"
+ android:drawableTop="@drawable/noticon_warning_big_grey"
+ android:gravity="center"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_extra_large"
+ android:visibility="gone"
+ tools:text="Error message"
+ tools:visibility="visible" />
+
+ <!-- container for webView custom view - this is where fullscreen video will appear -->
+ <FrameLayout
+ android:id="@+id/layout_custom_view_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#80000000"
+ android:visibility="gone" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_gap_marker_view.xml b/WordPress/src/main/res/layout/reader_gap_marker_view.xml
new file mode 100644
index 000000000..ea3563411
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_gap_marker_view.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/layout_gap_marker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/image_gap_marker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:background="@drawable/reader_tear_repeat" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_gap_marker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:background="@drawable/reader_gap_marker_background"
+ android:gravity="center"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_medium"
+ android:text="@string/reader_label_gap_marker"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_sz_large" />
+
+ <ProgressBar
+ android:id="@+id/progress_gap_marker"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_icon_count_view.xml b/WordPress/src/main/res/layout/reader_icon_count_view.xml
new file mode 100644
index 000000000..6a661b240
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_icon_count_view.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/image_count"
+ android:layout_width="@dimen/reader_button_icon"
+ android:layout_height="@dimen/reader_button_icon"
+ android:layout_gravity="center_vertical"
+ tools:src="@drawable/reader_button_comment" />
+
+ <TextView
+ android:id="@+id/text_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/margin_extra_small"
+ android:textColor="@color/reader_count_text"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="2" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_include_comment_box.xml b/WordPress/src/main/res/layout/reader_include_comment_box.xml
new file mode 100644
index 000000000..b4e2da28c
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_include_comment_box.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ comment box at bottom of comment detail and reader comment list
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <View
+ android:id="@+id/divider_comment"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_20" />
+
+ <LinearLayout
+ android:id="@+id/reply_box"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/content_margin"
+ app:srcCompat="@drawable/ic_action_reply" />
+
+ <org.wordpress.android.widgets.SuggestionAutoCompleteText
+ android:id="@+id/edit_comment"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:background="@color/transparent"
+ android:hint="@string/reader_hint_comment_on_post"
+ android:imeOptions="actionSend"
+ android:focusableInTouchMode="false"
+ android:inputType="text|textCapSentences|textMultiLine"
+ android:maxLines="4"
+ android:minLines="2"
+ android:paddingBottom="@dimen/margin_extra_small"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_extra_small"
+ android:textColorHint="@color/grey_lighten_10"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/btn_submit_reply"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="@dimen/content_margin"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_medium"
+ android:text="@string/reader_label_submit_comment"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_medium" />
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/progress_submit_comment"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:visibility="gone" />
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_include_post_detail_content.xml b/WordPress/src/main/res/layout/reader_include_post_detail_content.xml
new file mode 100644
index 000000000..369af6023
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_include_post_detail_content.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ included by ReaderPostDetailFragment
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/layout_post_detail_content"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ style="@style/ReaderTextView.Post.Title.Detail"
+ android:layout_alignLeft="@+id/reader_webview"
+ android:layout_alignRight="@+id/reader_webview"
+ tools:text="text_title" />
+
+ <LinearLayout
+ android:id="@+id/layout_dateline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@+id/reader_webview"
+ android:layout_alignRight="@+id/reader_webview"
+ android:layout_below="@id/text_title"
+ android:layout_marginTop="@dimen/margin_extra_small"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_avatar"
+ style="@style/ReaderImageView.Avatar.Tiny"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="@dimen/margin_medium"
+ tools:src="@drawable/gravatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_dateline"
+ style="@style/ReaderTextView.Label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ tools:text="text_dateline" />
+ </LinearLayout>
+
+ <org.wordpress.android.ui.reader.views.ReaderWebView
+ android:id="@+id/reader_webview"
+ android:layout_width="@dimen/reader_webview_width"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/layout_dateline"
+ android:layout_marginTop="@dimen/margin_large"
+ android:scrollbars="none" />
+
+ <View
+ android:id="@+id/layout_liking_users_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/reader_webview"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:background="@color/reader_divider_grey"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_liking_users_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/layout_liking_users_divider"
+ android:layout_gravity="center_vertical"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:text="@string/reader_label_liked_by"
+ android:textAllCaps="true"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <!-- liking avatars are inserted into this view at runtime -->
+ <org.wordpress.android.ui.reader.views.ReaderLikingUsersView
+ android:id="@+id/layout_liking_users_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/avatar_sz_small"
+ android:layout_below="@id/text_liking_users_label"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:background="?android:selectableItemBackground"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml b/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml
new file mode 100644
index 000000000..023caf29c
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ included by ReaderPostDetailFragment
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:wp="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/layout_post_detail_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:paddingBottom="@dimen/margin_medium">
+
+ <View
+ android:id="@+id/divider_footer"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:background="@color/reader_divider_grey" />
+
+ <RelativeLayout
+ android:layout_width="@dimen/reader_webview_width"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="@dimen/reader_detail_margin"
+ android:layout_marginRight="@dimen/reader_detail_margin"
+ android:layout_marginTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_tag"
+ style="@style/ReaderTextView"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/reader_detail_tag_height"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/layout_icons"
+ android:background="?android:selectableItemBackground"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/reader_hyperlink"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="#text_tag" />
+
+ <LinearLayout
+ android:id="@+id/layout_icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.ui.reader.views.ReaderIconCountView
+ android:id="@+id/count_comments"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:padding="@dimen/margin_medium"
+ wp:readerIcon="comment" />
+
+ <org.wordpress.android.ui.reader.views.ReaderIconCountView
+ android:id="@+id/count_likes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingLeft="@dimen/margin_medium"
+ android:paddingRight="0dp"
+ android:paddingTop="@dimen/margin_medium"
+ android:paddingBottom="@dimen/margin_medium"
+ wp:readerIcon="like" />
+
+ </LinearLayout>
+ </RelativeLayout>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/reader_include_post_detail_header.xml b/WordPress/src/main/res/layout/reader_include_post_detail_header.xml
new file mode 100644
index 000000000..8c7c84bdd
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_include_post_detail_header.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ included by ReaderPostDetailFragment
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:wp="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/layout_post_detail_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_blavatar"
+ style="@style/ReaderImageView.Avatar"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="@dimen/margin_large"
+ tools:src="@drawable/blavatar_placeholder" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/follow_button"
+ android:layout_toRightOf="@+id/image_blavatar"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_blog_name"
+ style="@style/ReaderTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_blog_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_domain"
+ style="@style/ReaderTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@+id/text_blog_name"
+ android:layout_alignRight="@+id/text_blog_name"
+ android:layout_below="@+id/text_blog_name"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_domain" />
+ </LinearLayout>
+
+ <org.wordpress.android.ui.reader.views.ReaderFollowButton
+ android:id="@+id/follow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_medium" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_like_avatar.xml b/WordPress/src/main/res/layout/reader_like_avatar.xml
new file mode 100644
index 000000000..2ffe18ea6
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_like_avatar.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ single avatar image inserted into post detail when showing liking users
+-->
+<org.wordpress.android.widgets.WPNetworkImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/image_like_avatar"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small"
+ android:layout_marginRight="@dimen/margin_small" />
diff --git a/WordPress/src/main/res/layout/reader_listitem_blog.xml b/WordPress/src/main/res/layout/reader_listitem_blog.xml
new file mode 100644
index 000000000..3f3779cb2
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_listitem_blog.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!---
+ list item which shows a recommended or followed blog - see ReaderBlogAdapter
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_large">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_blog"
+ android:layout_width="@dimen/avatar_sz_medium"
+ android:layout_height="@dimen/avatar_sz_medium"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_weight="0" />
+
+ <LinearLayout
+ android:id="@+id/layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/reader_hyperlink"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_url" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_description" />
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_listitem_comment.xml b/WordPress/src/main/res/layout/reader_listitem_comment.xml
new file mode 100644
index 000000000..56618f79f
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_listitem_comment.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/reader_card_margin"
+ android:layout_marginRight="@dimen/reader_card_margin"
+ android:background="@color/white"
+ android:paddingLeft="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_medium">
+
+ <!-- spacer_comment_indent width and visibility set at run-time based on comment indent level -->
+ <View
+ android:id="@+id/spacer_comment_indent"
+ android:layout_width="@dimen/reader_comment_indent_per_level"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_toRightOf="@+id/spacer_comment_indent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/layout_author"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_comment_avatar"
+ android:layout_width="@dimen/avatar_sz_extra_small"
+ android:layout_height="@dimen/avatar_sz_extra_small"
+ android:layout_marginRight="@dimen/margin_small"
+ tools:src="@drawable/gravatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_comment_author"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:background="?android:selectableItemBackground"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ android:textStyle="bold"
+ tools:text="text_comment_author" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_comment_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_small"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ android:lineSpacingMultiplier="1.1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ app:wpFontFamily="merriweather"
+ tools:text="text_comment_text" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_comment_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="date" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/count_likes"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/image_comment_reply"
+ android:layout_width="@dimen/reader_button_icon"
+ android:layout_height="@dimen/reader_button_icon"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:padding="@dimen/margin_extra_small"
+ app:srcCompat="@drawable/ic_action_reply" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_comment_reply"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/reader_label_reply"
+ android:textColor="@color/grey_lighten_10"
+ android:textSize="@dimen/text_sz_medium" />
+ </LinearLayout>
+
+ <org.wordpress.android.ui.reader.views.ReaderIconCountView
+ android:id="@+id/count_likes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_large" />
+
+ </RelativeLayout>
+
+ </LinearLayout>
+
+ <!-- progress bar that appears while submitting a comment -->
+ <ProgressBar
+ android:id="@+id/progress_comment"
+ style="?android:attr/progressBarStyleSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_listitem_suggestion.xml b/WordPress/src/main/res/layout/reader_listitem_suggestion.xml
new file mode 100644
index 000000000..caa405f8d
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_listitem_suggestion.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+ <TextView
+ android:id="@+id/text_suggestion"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/image_delete"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:lines="1"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:textColor="@color/grey_dark"
+ tools:text="Suggestion" />
+
+ <ImageView
+ android:id="@+id/image_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_small"
+ android:src="@drawable/ic_close_grey600_24dp" />
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_listitem_tag.xml b/WordPress/src/main/res/layout/reader_listitem_tag.xml
new file mode 100644
index 000000000..b62958a1b
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_listitem_tag.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ list item which shows a followed tag - see ReaderTagAdapter
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingTop="@dimen/margin_medium">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_topic"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="text_topic" />
+
+ <ImageButton
+ android:id="@+id/btn_remove"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/margin_small"
+ android:contentDescription="@string/remove"
+ app:srcCompat="@drawable/ic_close_grey600_24dp" />
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/reader_listitem_user.xml b/WordPress/src/main/res/layout/reader_listitem_user.xml
new file mode 100644
index 000000000..850b3e4dd
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_listitem_user.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:gravity="center_vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:padding="@dimen/margin_large">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_avatar"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small" />
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:layout_weight="1">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_name" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_url"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/text_name"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/reader_hyperlink"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_url" />
+
+ </RelativeLayout>
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_photo_view.xml b/WordPress/src/main/res/layout/reader_photo_view.xml
new file mode 100644
index 000000000..229882da1
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_photo_view.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/image_photo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:text="@string/reader_toast_err_view_image"
+ android:textColor="@color/grey_lighten_30"
+ android:textSize="@dimen/text_sz_large"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/progress_loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+</merge> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_popup_menu_item.xml b/WordPress/src/main/res/layout/reader_popup_menu_item.xml
new file mode 100644
index 000000000..8cb7ee9c2
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_popup_menu_item.xml
@@ -0,0 +1,24 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/menu_item_height"
+ android:gravity="left|center_vertical"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/menu_item_margin"
+ android:paddingRight="@dimen/menu_item_margin">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/margin_large"
+ tools:src="@drawable/reader_following" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?textAppearanceSmallPopupMenu"
+ tools:text="Menu item" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_related_post.xml b/WordPress/src/main/res/layout/reader_related_post.xml
new file mode 100644
index 000000000..6f64e009f
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_related_post.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/reader_related_post_margin">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/image_related_post"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_related_post_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ android:textStyle="bold"
+ app:wpFontFamily="merriweather"
+ tools:text="text_related_post_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_related_post_byline"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="4"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_related_post_byline" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_related_post"
+ android:layout_width="@dimen/reader_related_post_image_size"
+ android:layout_height="@dimen/reader_related_post_image_size"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/reader_related_post_margin"
+ android:visibility="gone"
+ tools:src="@drawable/my_site_no_sites_drake"
+ tools:visibility="visible" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_related_post_divider.xml b/WordPress/src/main/res/layout/reader_related_post_divider.xml
new file mode 100644
index 000000000..488260748
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_related_post_divider.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/reader_divider_grey" /> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_site_header_view.xml b/WordPress/src/main/res/layout/reader_site_header_view.xml
new file mode 100644
index 000000000..8aaa10eeb
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_site_header_view.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/layout_blog_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:elevation="@dimen/card_elevation"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/margin_large"
+ android:paddingLeft="@dimen/reader_card_content_padding"
+ android:paddingRight="@dimen/reader_card_content_padding"
+ android:paddingTop="@dimen/margin_large"
+ tools:targetApi="LOLLIPOP">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_blavatar"
+ style="@style/ReaderImageView.Avatar"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="@dimen/margin_large"
+ tools:src="@drawable/blavatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_blog_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/follow_button"
+ android:layout_toRightOf="@+id/image_blavatar"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="Loading..." />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_domain"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/text_blog_name"
+ android:layout_toLeftOf="@+id/follow_button"
+ android:layout_toRightOf="@+id/image_blavatar"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="text_domain" />
+
+ <org.wordpress.android.ui.reader.views.ReaderFollowButton
+ android:id="@+id/follow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/grey_lighten_30"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/margin_extra_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_blog_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:maxLines="3"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ app:wpFontFamily="merriweather"
+ tools:text="text_description" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:orientation="horizontal">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:background="@color/grey_lighten_20" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_blog_follow_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/text_domain"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="12 followers" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:background="@color/grey_lighten_20" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_tag_header_view.xml b/WordPress/src/main/res/layout/reader_tag_header_view.xml
new file mode 100644
index 000000000..065378350
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_tag_header_view.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/layout_tag_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/reader_card_content_padding"
+ android:layout_marginRight="@dimen/reader_card_content_padding"
+ android:minHeight="@dimen/toolbar_height"
+ android:paddingLeft="@dimen/reader_card_margin"
+ android:paddingRight="@dimen/reader_card_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_tag"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/follow_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="text_tag" />
+
+ <org.wordpress.android.ui.reader.views.ReaderFollowButton
+ android:id="@+id/follow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+
+ </RelativeLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_20" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_thumbnail_strip.xml b/WordPress/src/main/res/layout/reader_thumbnail_strip.xml
new file mode 100644
index 000000000..19caf6597
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_thumbnail_strip.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/thumbnail_strip_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:orientation="horizontal" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/reader_divider_grey" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_thumbnail_strip_image.xml b/WordPress/src/main/res/layout/reader_thumbnail_strip_image.xml
new file mode 100644
index 000000000..b53b9615f
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_thumbnail_strip_image.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPNetworkImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/thumbnail_strip_image"
+ android:layout_width="@dimen/reader_thumbnail_strip_image_size"
+ android:layout_height="@dimen/reader_thumbnail_strip_image_size"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginRight="@dimen/margin_small"
+ android:scaleType="centerCrop" /> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/reader_thumbnail_strip_labels.xml b/WordPress/src/main/res/layout/reader_thumbnail_strip_labels.xml
new file mode 100644
index 000000000..3f643d0af
--- /dev/null
+++ b/WordPress/src/main/res/layout/reader_thumbnail_strip_labels.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/reader_label_view_gallery"
+ android:textColor="@color/blue_medium"
+ android:textSize="@dimen/text_sz_medium" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_gallery_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_small"
+ tools:text="3 images" />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/related_posts_dialog.xml b/WordPress/src/main/res/layout/related_posts_dialog.xml
new file mode 100644
index 000000000..a46a34026
--- /dev/null
+++ b/WordPress/src/main/res/layout/related_posts_dialog.xml
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.wordpress.android.widgets.WPScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/site_settings_divider_height"
+ android:paddingBottom="@dimen/site_settings_divider_height"
+ android:orientation="vertical"
+ android:background="@color/grey_lighten_30">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/related_posts_dialog_padding_left"
+ android:paddingStart="@dimen/related_posts_dialog_padding_left"
+ android:paddingTop="@dimen/related_posts_dialog_padding_top"
+ android:paddingRight="@dimen/related_posts_dialog_padding_right"
+ android:paddingEnd="@dimen/related_posts_dialog_padding_right"
+ android:paddingBottom="@dimen/related_posts_dialog_padding_bottom"
+ android:orientation="vertical"
+ android:background="@color/white">
+
+ <org.wordpress.android.widgets.WPSwitch
+ android:id="@+id/toggle_related_posts_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/site_settings_rp_switch_title"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/toggleRelatedPostsSummary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/site_settings_rp_switch_summary"
+ android:textColor="@color/grey_darken_10"
+ android:textSize="@dimen/text_sz_small" />
+
+ <org.wordpress.android.widgets.WPCheckBox
+ android:id="@+id/show_header_checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:textColor="@color/calypso_title_text"
+ android:textSize="@dimen/text_sz_large"
+ android:text="@string/site_settings_rp_show_header_title" />
+
+ <org.wordpress.android.widgets.WPCheckBox
+ android:id="@+id/show_images_checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:textColor="@color/calypso_title_text"
+ android:textSize="@dimen/text_sz_large"
+ android:text="@string/site_settings_rp_show_images_title" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="4dp"
+ android:background="@drawable/related_posts_divider" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/preview_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp"
+ android:text="@string/preview"
+ android:textStyle="italic|bold"
+ android:textColor="@color/related_posts_preview_header"
+ android:textSize="@dimen/text_sz_medium"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_posts_list_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:text="@string/site_settings_rp_preview_header"
+ android:textStyle="bold"
+ android:textSize="@dimen/text_sz_medium"
+ android:textColor="@color/related_posts_list_header"
+ android:fontFamily="sans-serif-light" />
+
+ <LinearLayout
+ android:id="@+id/related_posts_list"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/related_post_image1"
+ android:contentDescription="@string/cd_related_post_preview_image"
+ android:layout_width="match_parent"
+ android:layout_height="96dp"
+ android:layout_marginTop="4dp"
+ app:srcCompat="@drawable/rppreview1" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_post_title1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textSize="@dimen/text_sz_medium"
+ android:textColor="@color/grey_dark"
+ android:text="@string/site_settings_rp_preview1_title"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_post_site1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:textStyle="italic"
+ android:textSize="@dimen/text_sz_related_post_small"
+ android:textColor="@color/grey_dark"
+ android:text="@string/site_settings_rp_preview1_site" />
+
+ <ImageView
+ android:id="@+id/related_post_image2"
+ android:contentDescription="@string/cd_related_post_preview_image"
+ android:layout_width="match_parent"
+ android:layout_height="96dp"
+ android:layout_marginTop="8dp"
+ app:srcCompat="@drawable/rppreview2" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_post_title2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textSize="@dimen/text_sz_medium"
+ android:textColor="@color/grey_dark"
+ android:text="@string/site_settings_rp_preview2_title"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_post_site2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:textStyle="italic"
+ android:textSize="@dimen/text_sz_related_post_small"
+ android:textColor="@color/grey_dark"
+ android:text="@string/site_settings_rp_preview2_site" />
+
+ <ImageView
+ android:id="@+id/related_post_image3"
+ android:contentDescription="@string/cd_related_post_preview_image"
+ android:layout_width="match_parent"
+ android:layout_height="96dp"
+ android:layout_marginTop="8dp"
+ app:srcCompat="@drawable/rppreview3" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_post_title3"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textSize="@dimen/text_sz_medium"
+ android:textColor="@color/grey_dark"
+ android:text="@string/site_settings_rp_preview3_title"
+ android:fontFamily="sans-serif-light" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/related_post_site3"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:textStyle="italic"
+ android:textSize="@dimen/text_sz_related_post_small"
+ android:textColor="@color/grey_dark"
+ android:text="@string/site_settings_rp_preview3_site"/>
+ </LinearLayout>
+
+ </LinearLayout>
+
+</org.wordpress.android.widgets.WPScrollView>
diff --git a/WordPress/src/main/res/layout/role_list_row.xml b/WordPress/src/main/res/layout/role_list_row.xml
new file mode 100644
index 000000000..07891321d
--- /dev/null
+++ b/WordPress/src/main/res/layout/role_list_row.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/dlp_padding_bottom"
+ android:paddingEnd="@dimen/dlp_padding_end"
+ android:paddingLeft="@dimen/dlp_padding_start"
+ android:paddingRight="@dimen/dlp_padding_end"
+ android:paddingStart="@dimen/dlp_padding_start"
+ android:paddingTop="@dimen/dlp_padding_top">
+
+ <RadioButton
+ android:id="@+id/radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:buttonTint="@color/dialog_compound_button"
+ tools:targetApi="LOLLIPOP"/>
+
+ <TextView
+ android:id="@+id/role_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginEnd="@dimen/dlp_text_margin_end"
+ android:layout_marginLeft="@dimen/dlp_text_margin_start"
+ android:layout_marginRight="@dimen/dlp_text_margin_end"
+ android:layout_marginStart="@dimen/dlp_text_margin_start"
+ android:layout_toEndOf="@id/radio"
+ android:layout_toRightOf="@id/radio"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="Main Text"/>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/select_categories.xml b/WordPress/src/main/res/layout/select_categories.xml
new file mode 100644
index 000000000..3e797b2e1
--- /dev/null
+++ b/WordPress/src/main/res/layout/select_categories.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:cacheColorHint="#00000000"
+ android:divider="@drawable/list_divider"
+ android:dividerHeight="1px"
+ android:textColor="#444444" />
+
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/empty_view"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin"
+ android:layout_marginLeft="@dimen/empty_list_title_side_margin"
+ android:layout_marginRight="@dimen/empty_list_title_side_margin"
+ android:text="@string/empty_list_default"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</FrameLayout>
diff --git a/WordPress/src/main/res/layout/share_intent_receiver_dialog.xml b/WordPress/src/main/res/layout/share_intent_receiver_dialog.xml
new file mode 100644
index 000000000..a86880199
--- /dev/null
+++ b/WordPress/src/main/res/layout/share_intent_receiver_dialog.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@drawable/dialog_full_holo_light"
+ android:minWidth="@dimen/default_dialog_width"
+ android:orientation="vertical">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_large"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/blog_spinner_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/select_a_blog"
+ android:textAllCaps="true"
+ android:textSize="@dimen/text_sz_small"/>
+
+ <Spinner
+ android:id="@+id/blog_spinner"
+ style="@style/DropDownNav.WordPress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/action_spinner_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/share_action_title"
+ android:textAllCaps="true"
+ android:textSize="@dimen/text_sz_small"/>
+
+ <Spinner
+ android:id="@+id/action_spinner"
+ style="@style/DropDownNav.WordPress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+ </ScrollView>
+
+ <Button
+ android:id="@+id/share_intent_receiver_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:onClick="onShareClicked"
+ android:text="@string/share_action"/>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/signin_dialog_fragment.xml b/WordPress/src/main/res/layout/signin_dialog_fragment.xml
new file mode 100644
index 000000000..35c700bf2
--- /dev/null
+++ b/WordPress/src/main/res/layout/signin_dialog_fragment.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/success_view"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:layout_weight="1"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp">
+
+ <ImageView
+ android:id="@+id/nux_dialog_image"
+ android:layout_width="50dp"
+ android:layout_height="52dp"
+ app:srcCompat="@drawable/nux_icon_wp"
+ android:tint="@color/white"
+ android:layout_marginBottom="16dp"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_dialog_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:gravity="center_horizontal"
+ android:textColor="@color/white"
+ android:textSize="@dimen/nux_title_font_size"
+ android:lineSpacingExtra="3sp"
+ android:shadowColor="@color/black"
+ android:shadowDx="0"
+ android:shadowDy="1"
+ android:shadowRadius="2"
+ android:fontFamily="sans-serif-light"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_dialog_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:paddingBottom="8dp"
+ android:paddingTop="8dp"
+ android:textColor="@color/white"
+ android:textSize="16sp"
+ android:shadowColor="@color/black"
+ android:shadowDx="0"
+ android:shadowDy="1"
+ android:shadowRadius="2"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/nux_dialog_footer"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:gravity="center"
+ android:layout_width="fill_parent"
+ android:layout_marginLeft="12dp"
+ android:layout_marginRight="12dp"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_dialog_center_button"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="fill_parent"
+ android:layout_height="38dp"
+ android:textColor="@color/white"
+ android:text="@string/faq_button"
+ android:padding="8dp"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:layout_gravity="center_horizontal"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_dialog_right_button"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="fill_parent"
+ android:layout_height="38dp"
+ android:text="@string/nux_tutorial_get_started_title"
+ android:padding="8dp"
+ android:singleLine="true"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_dialog_left_button"
+ android:layout_width="fill_parent"
+ android:layout_height="38dp"
+ android:textColor="@color/white"
+ android:text="@string/cancel"
+ android:padding="8dp"
+ android:singleLine="true"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/signin_fragment.xml b/WordPress/src/main/res/layout/signin_fragment.xml
new file mode 100644
index 000000000..1ff83c6a8
--- /dev/null
+++ b/WordPress/src/main/res/layout/signin_fragment.xml
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/sign_in_scroll_view"
+ android:background="@color/nux_background"
+ android:animateLayoutChanges="true"
+ android:fillViewport="true"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:animateLayoutChanges="true"
+ android:baselineAligned="false"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|bottom"
+ android:baselineAligned="true"
+ android:gravity="right"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/info_button"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:contentDescription="@string/help"
+ app:srcCompat="@drawable/dashicon_info"
+ android:tint="@color/blue_dark" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:animateLayoutChanges="true"
+ android:gravity="center"
+ android:orientation="vertical"
+ app:maxWidth="@dimen/nux_width">
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp">
+
+ <ImageView
+ android:id="@+id/nux_fragment_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_centerInParent="true"
+ app:srcCompat="@drawable/nux_icon_wp" />
+
+ <ImageView
+ android:id="@+id/info_button_secondary"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="false"
+ android:layout_alignTop="@+id/nux_fragment_icon"
+ android:layout_marginRight="16dp"
+ android:contentDescription="@string/help"
+ app:srcCompat="@drawable/dashicon_info"
+ android:tint="@color/blue_dark" />
+ </RelativeLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_jetpack_auth_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/white"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingBottom="@dimen/margin_extra_large"
+ android:gravity="center_horizontal"
+ android:text="@string/sign_in_jetpack"
+ android:visibility="gone" />
+
+ <RelativeLayout
+ android:id="@+id/nux_username_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:clickable="true">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/nux_username"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/username_email"
+ android:inputType="textEmailAddress"
+ android:imeOptions="actionNext"
+ android:nextFocusDown="@+id/nux_password"
+ android:maxLength="@integer/max_length_username" />
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ android:layout_centerVertical="true"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="10dp"
+ app:srcCompat="@drawable/dashicon_admin_users"
+ android:tint="@color/grey_darken_10" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/nux_password_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ android:clickable="true">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/nux_password"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="38dp"
+ android:hint="@string/password"
+ android:maxLength="@integer/max_length_password"
+ android:inputType="textPassword" />
+
+ <ImageView
+ android:id="@+id/imageView4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ android:layout_centerVertical="true"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="10dp"
+ app:srcCompat="@drawable/dashicon_lock"
+ android:tint="@color/grey_darken_10" />
+
+ <ImageView
+ android:id="@+id/password_visibility"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentStart="false"
+ android:layout_centerVertical="true"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginRight="16dp"
+ app:srcCompat="@drawable/dashicon_eye_closed"
+ android:tint="@color/nux_eye_icon_color_closed" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/two_factor_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:visibility="gone">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/nux_two_step"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="0dp"
+ android:layout_marginStart="0dp"
+ android:gravity="center_horizontal"
+ android:hint="@string/verification_code"
+ android:inputType="number"
+ android:maxLength="8" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/url_button_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:background="@color/white"
+ android:visibility="gone">
+
+ <org.wordpress.android.widgets.WPEditText
+ android:id="@+id/nux_url"
+ style="@style/WordPress.NUXEditText"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/site_address"
+ android:inputType="textUri"
+ app:persistenceEnabled="true" />
+
+ <ImageView
+ android:id="@+id/imageView2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ android:layout_centerVertical="true"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="10dp"
+ app:srcCompat="@drawable/dashicon_admin_site"
+ android:tint="@color/grey_darken_10" />
+ </RelativeLayout>
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="16dp">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_sign_in_button"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="match_parent"
+ android:clickable="true"
+ android:enabled="false"
+ android:gravity="center"
+ android:text="@string/sign_in" />
+
+ <RelativeLayout
+ android:id="@+id/nux_sign_in_progress_bar"
+ style="@style/WordPress.NUXPrimaryButton"
+ android:layout_width="match_parent"
+ android:enabled="false"
+ android:visibility="gone">
+
+ <ProgressBar
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true" />
+ </RelativeLayout>
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/two_step_footer"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/two_step_footer_label"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:gravity="center_horizontal"
+ android:text="@string/two_step_footer_label"
+ android:textColor="@color/white"
+ app:fixWidowWords="true" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/two_step_footer_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:background="@drawable/selectable_background_wordpress"
+ android:text="@string/two_step_footer_button"
+ android:textColor="@color/white" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/forgot_password"
+ style="@style/WordPress.NUXGreyButtonNoBg"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:text="@string/forgot_password" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_sign_in_progress_text"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center|top"
+ android:textColor="@color/blue_light"
+ android:visibility="gone" />
+
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+
+ <org.wordpress.android.widgets.WPLinearLayoutSizeBound
+ android:id="@+id/nux_bottom_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ app:maxWidth="@dimen/nux_width">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_add_selfhosted_button"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_gravity="center"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:gravity="center"
+ android:text="@string/nux_add_selfhosted_blog" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/nux_create_account_button"
+ style="@style/WordPress.NUXFlatButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/nux_main_button_height"
+ android:layout_gravity="center"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:gravity="center"
+ android:text="@string/nux_welcome_create_account" />
+ </org.wordpress.android.widgets.WPLinearLayoutSizeBound>
+ </LinearLayout>
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/simple_spinner_item.xml b/WordPress/src/main/res/layout/simple_spinner_item.xml
new file mode 100644
index 000000000..63ec0d0ce
--- /dev/null
+++ b/WordPress/src/main/res/layout/simple_spinner_item.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ replacement for android.R.layout.simple_spinner_item
+-->
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@android:id/text1"
+ style="?android:attr/spinnerItemStyle"
+ android:textColor="@color/grey_dark"
+ android:singleLine="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:textAlignment="inherit"
+ tools:targetApi="JELLY_BEAN_MR1"/>
diff --git a/WordPress/src/main/res/layout/site_picker_activity.xml b/WordPress/src/main/res/layout/site_picker_activity.xml
new file mode 100644
index 000000000..379ab814d
--- /dev/null
+++ b/WordPress/src/main/res/layout/site_picker_activity.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:fab="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/site_picker_listitem.xml b/WordPress/src/main/res/layout/site_picker_listitem.xml
new file mode 100644
index 000000000..3c6552340
--- /dev/null
+++ b/WordPress/src/main/res/layout/site_picker_listitem.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/site_picker_container_padding_bottom"
+ android:paddingTop="@dimen/site_picker_container_padding_top">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/image_blavatar"
+ android:layout_width="@dimen/blavatar_sz"
+ android:layout_height="@dimen/blavatar_sz"
+ android:layout_marginLeft="@dimen/site_picker_blavatar_margin_left"
+ android:layout_marginRight="@dimen/site_picker_blavatar_margin_right"
+ android:background="@color/white"
+ android:gravity="center_vertical"
+ android:padding="1dp"
+ tools:src="@drawable/blavatar_placeholder" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/image_blavatar"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="text_title" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_domain"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/text_title"
+ android:layout_toRightOf="@id/image_blavatar"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="text_domain" />
+
+ </RelativeLayout>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_20" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/spinner_menu_dropdown_item.xml b/WordPress/src/main/res/layout/spinner_menu_dropdown_item.xml
new file mode 100644
index 000000000..64ab08c4b
--- /dev/null
+++ b/WordPress/src/main/res/layout/spinner_menu_dropdown_item.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/menu_text_dropdown"
+ style="?android:attr/spinnerDropDownItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/toolbar_height"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="menu_text_dropdown" />
diff --git a/WordPress/src/main/res/layout/start_over_preference.xml b/WordPress/src/main/res/layout/start_over_preference.xml
new file mode 100644
index 000000000..110a12510
--- /dev/null
+++ b/WordPress/src/main/res/layout/start_over_preference.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingRight="@dimen/start_over_preference_margin_large"
+ android:paddingLeft="@dimen/start_over_preference_margin_large"
+ android:paddingTop="@dimen/start_over_preference_margin_large"
+ android:paddingBottom="@dimen/start_over_preference_margin_large"
+ android:background="?android:attr/selectableItemBackground"
+ android:baselineAligned="false">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/pref_icon"
+ android:layout_width="@dimen/start_over_icon_size"
+ android:layout_height="@dimen/start_over_icon_size"
+ android:layout_marginRight="@dimen/start_over_icon_margin_right"
+ android:padding="@dimen/start_over_icon_padding"
+ android:layout_gravity="center_vertical"
+ android:tint="@color/grey_darken_30"/>
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:paddingLeft="@dimen/start_over_title_margin"
+ android:layout_gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_marginTop="@dimen/start_over_preference_margin_medium"
+ android:layout_marginBottom="@dimen/start_over_preference_margin_medium"
+ android:layout_marginLeft="@dimen/start_over_summary_margin"
+ android:paddingLeft="@dimen/start_over_preference_margin_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_marginLeft="@dimen/start_over_summary_margin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/start_over_preference_button.xml b/WordPress/src/main/res/layout/start_over_preference_button.xml
new file mode 100644
index 000000000..47ba12a5c
--- /dev/null
+++ b/WordPress/src/main/res/layout/start_over_preference_button.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/widget_frame"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/button_more"
+ android:theme="@style/WhiteButton" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_activity.xml b/WordPress/src/main/res/layout/stats_activity.xml
new file mode 100644
index 000000000..c7d2e93b3
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_activity.xml
@@ -0,0 +1,217 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/toolbar"
+ android:id="@+id/toolbar" />
+
+ <android.support.design.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.AppBarLayout
+ android:layout_height="@dimen/toolbar_subtitle_height"
+ android:layout_width="match_parent">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar_filter"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/FilteredRecyclerViewToolbar"
+ app:contentInsetLeft="@dimen/margin_filter_spinner"
+ app:contentInsetStart="@dimen/margin_filter_spinner"
+ app:layout_scrollFlags="scroll|enterAlways">
+
+ <Spinner
+ style="@style/FilteredRecyclerViewSpinner.WordPress"
+ android:id="@+id/filter_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:overlapAnchor="false"/>
+
+ </android.support.v7.widget.Toolbar>
+
+ </android.support.design.widget.AppBarLayout>
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/grey_lighten_30"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <org.wordpress.android.ui.stats.NestedScrollViewExt
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/scroll_view_stats"
+ android:fillViewport="true">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/stats_insights_fragments_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_medium"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <!-- LATEST POST SUMMARY SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_latest_post_summary_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <!-- TODAY SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_today_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <!-- ALL TIME SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_all_time_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <!-- POPULAR DAY AND TIME SECTION -->
+ <FrameLayout
+ android:id="@+id/stats_insights_most_popular_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_other_recent_stats_label_insights"
+ android:textAppearance="?android:attr/textAppearance"
+ android:textSize="@dimen/text_sz_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_marginBottom="@dimen/margin_extra_small"
+ android:textColor="@color/stats_blue_labels"
+ android:gravity="start"
+ android:text="@string/stats_other_recent_stats_label" />
+
+ <FrameLayout
+ android:id="@+id/stats_comments_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_tags_and_categories_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_followers_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_publicize_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/stats_timeline_fragments_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_medium"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/stats_visitors_and_views_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_top_posts_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_referrers_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_clicks_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_top_authors_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_geoviews_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_search_terms_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+ <FrameLayout
+ android:id="@+id/stats_video_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large" />
+
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_other_recent_stats_label_timeline"
+ android:textAppearance="?android:attr/textAppearance"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:textColor="@color/stats_blue_labels"
+ android:gravity="start"
+ android:textSize="@dimen/text_sz_large"
+ android:text="@string/stats_other_recent_stats_label" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_other_recent_stats_moved"
+ android:textAppearance="?android:attr/textAppearance"
+ android:background="?android:selectableItemBackground"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_small"
+ android:textColor="@color/stats_link_text_color"
+ android:gravity="start"
+ android:textSize="@dimen/text_sz_small"
+ android:text="@string/stats_other_recent_stats_moved_label" />
+
+ </LinearLayout>
+ </LinearLayout>
+ </org.wordpress.android.ui.stats.NestedScrollViewExt>
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+ </android.support.design.widget.CoordinatorLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_activity_single_post_details.xml b/WordPress/src/main/res/layout/stats_activity_single_post_details.xml
new file mode 100644
index 000000000..84a63a0e0
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_activity_single_post_details.xml
@@ -0,0 +1,273 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.ui.stats.ScrollViewExt
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/scroll_view_stats"
+ android:fillViewport="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- The title "Stats for XXX" -->
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_summary_title"
+ android:background="?android:selectableItemBackground"
+ android:textAppearance="?android:attr/textAppearance"
+ android:textSize="@dimen/text_sz_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin"
+ android:textColor="@color/grey_darken_20"
+ android:gravity="center"
+ tools:text="Title"/>
+
+ <!-- Graph -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical"
+ android:padding="@dimen/margin_medium"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin">
+
+ <LinearLayout
+ android:id="@+id/stats_bar_chart_fragment_container"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/stats_barchart_height"
+ android:orientation="vertical" />
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_marginRight="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginBottom="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_views_label"
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_default_number_zero"
+ android:textColor="@color/grey_dark"
+ android:layout_alignParentLeft="true" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_views_totals"
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/notification_status_unapproved_dark"
+ android:text="@string/stats_default_number_zero"
+ android:layout_alignParentRight="true" />
+
+ </RelativeLayout>
+
+ </LinearLayout>
+
+ <!-- Months and Years -->
+ <LinearLayout
+ android:id="@+id/stats_months_years_module"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/margin_small"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_marginLeft="@dimen/margin_large"
+ style="@style/StatsModuleTitle"
+ android:text="@string/stats_months_and_years" />
+
+ <include
+ android:id="@+id/stats_months_years_empty_module_placeholder"
+ layout="@layout/stats_empty_module_placeholder"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <RelativeLayout
+ android:id="@+id/stats_months_years_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:text="@string/stats_period" />
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:text="@string/stats_total" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/stats_months_years_list_linearlayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:visibility="gone">
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <!-- Average per Day -->
+ <LinearLayout
+ android:id="@+id/stats_averages_module"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/margin_small"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_marginLeft="@dimen/margin_large"
+ style="@style/StatsModuleTitle"
+ android:text="@string/stats_average_per_day" />
+
+ <include
+ android:id="@+id/stats_averages_empty_module_placeholder"
+ layout="@layout/stats_empty_module_placeholder"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <RelativeLayout
+ android:id="@+id/stats_averages_list_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:text="@string/stats_period" />
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:text="@string/stats_overall" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/stats_averages_list_linearlayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+
+ <!-- Recent Weeks -->
+ <LinearLayout
+ android:id="@+id/stats_recent_weeks_module"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/margin_small"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_marginLeft="@dimen/margin_large"
+ style="@style/StatsModuleTitle"
+ android:text="@string/stats_recent_weeks" />
+
+ <include
+ android:id="@+id/stats_recent_weeks_empty_module_placeholder"
+ layout="@layout/stats_empty_module_placeholder"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <RelativeLayout
+ android:id="@+id/stats_recent_weeks_list_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:text="@string/stats_period" />
+
+ <org.wordpress.android.widgets.WPTextView
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:text="@string/stats_total" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/stats_recent_weeks_list_linearlayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+
+ </LinearLayout>
+ </org.wordpress.android.ui.stats.ScrollViewExt>
+</org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
diff --git a/WordPress/src/main/res/layout/stats_activity_view_all.xml b/WordPress/src/main/res/layout/stats_activity_view_all.xml
new file mode 100644
index 000000000..1467893dd
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_activity_view_all.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/grey_lighten_30">
+
+ <org.wordpress.android.ui.stats.ScrollViewExt xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/scroll_view_stats"
+ android:fillViewport="true">
+
+ <LinearLayout
+ android:id="@+id/stats_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_medium"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_summary_date"
+ android:textAppearance="?android:attr/textAppearance"
+ android:textSize="@dimen/text_sz_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:textColor="@color/stats_blue_labels"
+ android:gravity="center"
+ tools:text="Date"/>
+
+ <FrameLayout
+ android:id="@+id/stats_single_view_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_marginLeft="@dimen/content_margin"
+ android:layout_marginRight="@dimen/content_margin" />
+
+ </LinearLayout>
+ </org.wordpress.android.ui.stats.ScrollViewExt>
+</org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
diff --git a/WordPress/src/main/res/layout/stats_bar_graph_empty.xml b/WordPress/src/main/res/layout/stats_bar_graph_empty.xml
new file mode 100644
index 000000000..4f495848f
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_bar_graph_empty.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:text="@string/stats_bar_graph_empty"
+ android:id="@+id/stats_bar_graph_empty_label">
+</org.wordpress.android.widgets.WPTextView>
diff --git a/WordPress/src/main/res/layout/stats_empty_module_placeholder.xml b/WordPress/src/main/res/layout/stats_empty_module_placeholder.xml
new file mode 100644
index 000000000..3d4df1052
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_empty_module_placeholder.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="100dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/stats_empty_placeholder_color"
+ android:minWidth="250dp"
+ android:minHeight="30dp"
+ />
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_expandable_list_fragment.xml b/WordPress/src/main/res/layout/stats_expandable_list_fragment.xml
new file mode 100644
index 000000000..b670c7bd9
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_expandable_list_fragment.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_gravity="top"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_module_title"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginBottom="@dimen/margin_small"
+ style="@style/StatsModuleTitle" />
+
+ <include
+ android:id="@+id/stats_empty_module_placeholder"
+ layout="@layout/stats_empty_module_placeholder"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:id="@+id/stats_pager_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:visibility="gone"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_module_totals_label"
+ android:textColor="@color/stats_module_content_list_header"
+ android:textSize="@dimen/text_sz_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingTop="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:visibility="gone" />
+
+ <include
+ android:id="@+id/stats_top_pagination_container"
+ layout="@layout/stats_pagination_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"/>
+
+ <LinearLayout
+ android:id="@+id/stats_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_entry_label"
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ tools:text="TITLE" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_totals_label"
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ tools:text="VIEWS" />
+ </RelativeLayout>
+
+
+ <LinearLayout
+ android:id="@+id/stats_list_linearlayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:visibility="gone"/>
+
+ <include
+ android:id="@+id/stats_bottom_pagination_container"
+ layout="@layout/stats_pagination_item" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_empty_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ android:padding="@dimen/margin_large"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="@drawable/stats_view_all_button_background">
+ <Button
+ android:id="@+id/btnViewAll"
+ style="@style/StatsViewAllButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_view_all"
+ android:visibility="gone" />
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_insights_all_time_item.xml b/WordPress/src/main/res/layout/stats_insights_all_time_item.xml
new file mode 100644
index 000000000..cd59bef7e
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_insights_all_time_item.xml
@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:baselineAligned="false"
+ android:padding="@dimen/margin_extra_large"
+ android:orientation="vertical">
+
+ <!-- First row -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <!-- Posts item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/stats_visitors_and_views_tab_icon"
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/my_site_icon_posts" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/posts" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_posts"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ </LinearLayout>
+
+ <include layout="@layout/stats_vertical_line" />
+
+ <!-- Views Item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/stats_icon_views" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_views" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_views"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+
+ <!-- Second row -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="30dp"
+ android:orientation="horizontal" >
+
+ <!-- Visitors Item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/stats_icon_visitors" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_visitors" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_visitors"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ </LinearLayout>
+
+ <include layout="@layout/stats_vertical_line" />
+
+ <!-- Best Ever Item -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/stats_icon_trophy" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:textColor="@color/alert_yellow"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_insights_best_ever" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_bestever"
+ style="@style/StatsInsightsValues"
+ android:textColor="@color/grey_darken_30"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/stats_default_number_zero" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_all_time_bestever_date"
+ style="@style/StatsInsightsLabel"
+ android:textColor="@color/grey_darken_10"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAllCaps="true"
+ tools:text="August 11, 2012" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_insights_generic_fragment.xml b/WordPress/src/main/res/layout/stats_insights_generic_fragment.xml
new file mode 100644
index 000000000..1f2923f36
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_insights_generic_fragment.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_module_title"
+ android:background="?android:selectableItemBackground"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_small"
+ style="@style/StatsModuleTitle" />
+
+ <include
+ android:id="@+id/stats_empty_module_placeholder"
+ layout="@layout/stats_empty_module_placeholder"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:id="@+id/stats_module_result_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ </LinearLayout>
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_error_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_small"
+ android:padding="@dimen/margin_large"
+ android:visibility="gone" />
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_insights_header_line.xml b/WordPress/src/main/res/layout/stats_insights_header_line.xml
new file mode 100644
index 000000000..8463b5357
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_insights_header_line.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/grey_lighten_30" />
diff --git a/WordPress/src/main/res/layout/stats_insights_latest_post_item.xml b/WordPress/src/main/res/layout/stats_insights_latest_post_item.xml
new file mode 100644
index 000000000..a4fa32748
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_insights_latest_post_item.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/stats_pager_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:orientation="horizontal">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_post_trend_label"
+ android:background="?android:selectableItemBackground"
+ android:textAppearance="?android:attr/textAppearance"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"/>
+
+ </LinearLayout>
+
+ <include layout="@layout/stats_insights_header_line" />
+
+ <LinearLayout
+ android:id="@+id/stats_latest_post_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <!-- VIEWS tab-->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+
+ <!-- LIKES tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+
+ <!-- COMMENTS tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_insights_most_popular_item.xml b/WordPress/src/main/res/layout/stats_insights_most_popular_item.xml
new file mode 100644
index 000000000..19a87cc26
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_insights_most_popular_item.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <include layout="@layout/stats_insights_header_line" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:padding="@dimen/margin_extra_large"
+ android:gravity="center_vertical|center_horizontal"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/margin_medium"
+ android:gravity="center_vertical|center_horizontal" >
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_insights_most_popular_day"
+ android:textAllCaps="true" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:text="@string/stats_insights_most_popular_hour"
+ android:textAllCaps="true" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical|center_horizontal">
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_gravity="center_vertical|center_horizontal"
+ android:id="@+id/stats_most_popular_day"
+ style="@style/StatsInsightsValues"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_default_number_zero"
+ android:textColor="@color/grey_darken_30" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_gravity="center_vertical|center_horizontal"
+ android:id="@+id/stats_most_popular_hour"
+ style="@style/StatsInsightsValues"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_default_number_zero"
+ android:textColor="@color/grey_darken_30" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_medium"
+ android:gravity="center_vertical|center_horizontal" >
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:id="@+id/stats_most_popular_day_percent"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_insights_most_popular_percent_views"
+ android:textColor="@color/grey_darken_10" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ style="@style/StatsInsightsLabel"
+ android:id="@+id/stats_most_popular_hour_percent"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:text="@string/stats_insights_most_popular_percent_views"
+ android:textColor="@color/grey_darken_10" />
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_insights_today_item.xml b/WordPress/src/main/res/layout/stats_insights_today_item.xml
new file mode 100644
index 000000000..a0600778c
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_insights_today_item.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <include layout="@layout/stats_insights_header_line" />
+
+ <LinearLayout
+ android:id="@+id/stats_post_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <!-- VIEWS tab-->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+
+ <!-- VISITORS tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+
+ <!-- LIKES tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+
+ <!-- COMMENTS tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal" />
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_list_cell.xml b/WordPress/src/main/res/layout/stats_list_cell.xml
new file mode 100644
index 000000000..00e8d206e
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_list_cell.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:id="@+id/layout_content"
+ android:background="?android:selectableItemBackground"
+ android:paddingTop="@dimen/margin_medium"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_large"
+ android:paddingLeft="@dimen/margin_large"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/stats_list_cell_link"
+ android:layout_width="14dp"
+ android:layout_height="14dp"
+ android:layout_marginRight="10dp"
+ android:layout_marginEnd="10dp"
+ android:visibility="gone"
+ app:srcCompat="@drawable/stats_link" />
+
+ <ImageView
+ android:id="@+id/stats_list_cell_chevron"
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginEnd="@dimen/margin_large"
+ android:visibility="gone"
+ app:srcCompat="@drawable/stats_chevron_right" />
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/stats_list_cell_image"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginEnd="@dimen/margin_large"
+ android:visibility="gone" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_cell_entry"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/avatar_sz_small"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginEnd="@dimen/margin_large"
+ android:maxLines="3"
+ android:gravity="center_vertical"
+ android:autoLink="none"
+ android:textColor="@color/stats_text_color"
+ android:textColorLink="@color/stats_link_text_color"
+ android:textColorHighlight="@color/transparent"
+ android:textSize="@dimen/text_sz_medium" />
+
+ <ImageView
+ android:id="@+id/image_more"
+ android:visibility="gone"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/margin_small"
+ android:layout_marginStart="@dimen/margin_small"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginEnd="@dimen/margin_large"
+ android:background="?android:selectableItemBackground"
+ android:paddingLeft="@dimen/margin_small"
+ android:paddingRight="@dimen/margin_small"
+ android:contentDescription="@string/more"
+ app:srcCompat="@drawable/ic_action_more" />
+
+ <LinearLayout
+ android:id="@+id/stats_list_cell_total_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="36dp"
+ android:gravity="end">
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_cell_total"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/stats_text_color"
+ android:textSize="@dimen/text_sz_medium" />
+ </LinearLayout>
+ </LinearLayout>
+
+
+ <!-- expanded children will be inserted here -->
+ <LinearLayout
+ android:id="@+id/layout_child_container"
+ android:orientation="vertical"
+ android:visibility="gone"
+ android:background="@color/grey_light"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_list_fragment.xml b/WordPress/src/main/res/layout/stats_list_fragment.xml
new file mode 100644
index 000000000..48ad65214
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_list_fragment.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_gravity="top"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_module_title"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginBottom="@dimen/margin_small"
+ style="@style/StatsModuleTitle" />
+
+ <include
+ android:id="@+id/stats_empty_module_placeholder"
+ layout="@layout/stats_empty_module_placeholder"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:layout_marginTop="@dimen/margin_large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:id="@+id/stats_pager_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/margin_small"
+ android:paddingTop="@dimen/margin_small"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:visibility="gone"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_module_totals_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_small"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingTop="@dimen/margin_large"
+ android:paddingBottom="@dimen/margin_medium"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:visibility="gone" />
+
+ <include
+ android:id="@+id/stats_top_pagination_container"
+ layout="@layout/stats_pagination_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"/>
+
+ <LinearLayout
+ android:id="@+id/stats_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_large"
+ android:layout_marginRight="@dimen/margin_large"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_entry_label"
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ tools:text="TITLE" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_totals_label"
+ style="@style/StatsHeader"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ tools:text="VIEWS" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/stats_list_linearlayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:visibility="gone"/>
+
+ <include
+ android:id="@+id/stats_bottom_pagination_container"
+ layout="@layout/stats_pagination_item" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_list_empty_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ android:padding="@dimen/margin_large"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="@drawable/stats_view_all_button_background">
+ <Button
+ android:id="@+id/btnViewAll"
+ style="@style/StatsViewAllButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/stats_view_all"
+ android:visibility="gone" />
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_pagination_item.xml b/WordPress/src/main/res/layout/stats_pagination_item.xml
new file mode 100644
index 000000000..c991c3221
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_pagination_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@drawable/stats_view_all_button_background"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <Button
+ android:id="@+id/stats_pagination_go_back"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:text="@string/previous_button"
+ android:textColor="@color/stats_link_text_color" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_pagination_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_extra_large" />
+
+ <Button
+ android:id="@+id/stats_pagination_go_forward"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:text="@string/next_button"
+ android:textColor="@color/stats_link_text_color" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_top_module_pager_button.xml b/WordPress/src/main/res/layout/stats_top_module_pager_button.xml
new file mode 100644
index 000000000..a4268440c
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_top_module_pager_button.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="@drawable/stats_top_pager_button_selector"
+ android:padding="@dimen/margin_medium"
+ android:textColor="@color/stats_text_color"
+ android:textSize="@dimen/text_sz_small"
+ android:clickable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" /> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_vertical_line.xml b/WordPress/src/main/res/layout/stats_vertical_line.xml
new file mode 100644
index 000000000..9c4e65a4b
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_vertical_line.xml
@@ -0,0 +1,4 @@
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@color/grey_lighten_30" />
diff --git a/WordPress/src/main/res/layout/stats_visitors_and_views_fragment.xml b/WordPress/src/main/res/layout/stats_visitors_and_views_fragment.xml
new file mode 100644
index 000000000..c563ce6da
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_visitors_and_views_fragment.xml
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_gravity="center"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_white_background"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/stats_pager_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone" >
+
+ <!-- VIEWS tab-->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:layout_weight="1"/>
+
+ <!-- VISITORS tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:layout_weight="1"/>
+
+ <!-- LIKES tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:layout_weight="1"/>
+
+ <!-- COMMENTS tab -->
+ <include
+ layout="@layout/stats_visitors_and_views_tab"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/stats_bar_chart_fragment_container"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/margin_large"
+ android:paddingLeft="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_extra_small"
+ android:paddingBottom="@dimen/margin_large"
+ android:minHeight="@dimen/stats_barchart_height"
+ android:orientation="vertical" />
+ <LinearLayout
+ android:id="@+id/stats_bar_chart_no_activity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:gravity="center_horizontal|center_vertical"
+ android:paddingTop="@dimen/margin_medium"
+ android:paddingBottom="@dimen/margin_medium"
+ android:paddingLeft="@dimen/margin_extra_large"
+ android:paddingStart="@dimen/margin_extra_large"
+ android:paddingRight="@dimen/margin_extra_large"
+ android:paddingEnd="@dimen/margin_extra_large"
+ android:visibility="gone"
+ android:background="@drawable/stats_barchart_no_activity_background"
+ android:orientation="horizontal">
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ app:srcCompat="@drawable/stats_icon_info"
+ android:paddingEnd="@dimen/margin_medium"
+ android:paddingRight="@dimen/margin_medium"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#404040"
+ android:textSize="@dimen/text_sz_medium"
+ android:text="@string/stats_no_activity_this_period"/>
+ </LinearLayout>
+ </RelativeLayout>
+
+ <!-- Bottom Legend -->
+ <LinearLayout
+ android:id="@+id/stats_legend_container"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="@dimen/margin_medium">
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center">
+ <!-- Have to use a CheckedTextView instead of a plain TextView otherwise
+ there was a "jumpy UI" when the CheckBox below 'stats_checkbox_visitors' is not on the screen. This is due to the
+ CheckedTextView/CheckBox touching area that is bigger than the touching area of a TextView .
+ -->
+ <CheckedTextView
+ android:id="@+id/stats_legend_label"
+ android:gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:checkMark="?android:attr/listChoiceIndicatorSingle"
+ android:drawableLeft="@drawable/stats_visitors_and_views_legend_background_primary"
+ android:drawableStart="@drawable/stats_visitors_and_views_legend_background_primary"
+ android:drawablePadding="@dimen/margin_medium"
+ android:textSize="@dimen/text_sz_small"
+ android:text="@string/stats_views"
+ android:textColor="@color/grey_darken_30"/>
+ </LinearLayout>
+ <LinearLayout
+ android:id="@+id/stats_checkbox_visitors_container"
+ android:orientation="horizontal"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:visibility="gone">
+ <CheckBox
+ android:id="@+id/stats_checkbox_visitors"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/stats_visitors_and_views_legend_background_secondary"
+ android:drawableStart="@drawable/stats_visitors_and_views_legend_background_secondary"
+ android:drawablePadding="@dimen/margin_medium"
+ android:textSize="@dimen/text_sz_small"
+ android:text="@string/stats_visitors"
+ android:textColor="@color/grey_darken_30"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_summary_date"
+ android:textAppearance="?android:attr/textAppearance"
+ android:textSize="@dimen/text_sz_large"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/margin_extra_large"
+ android:layout_marginBottom="@dimen/margin_small"
+ android:textColor="@color/grey_darken_20"
+ tools:text="Date"/>
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/stats_visitors_and_views_tab.xml b/WordPress/src/main/res/layout/stats_visitors_and_views_tab.xml
new file mode 100644
index 000000000..dc1bc7685
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_visitors_and_views_tab.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:clickable="true">
+
+ <LinearLayout
+ android:id="@+id/stats_visitors_and_views_tab_inner_container"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:padding="@dimen/margin_medium"
+ android:background="?android:selectableItemBackground"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/stats_visitors_and_views_tab_icon"
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginEnd="3dp"
+ app:srcCompat="@drawable/ic_like" />
+
+ <org.wordpress.android.widgets.WPAutoResizeTextView
+ android:id="@+id/stats_visitors_and_views_tab_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_extra_small"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/stats_visitors_and_views_tab_value"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/text_sz_small"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_extra_small"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_widget_config_activity.xml b/WordPress/src/main/res/layout/stats_widget_config_activity.xml
new file mode 100644
index 000000000..13e57f749
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_widget_config_activity.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_widget_layout.xml b/WordPress/src/main/res/layout/stats_widget_layout.xml
new file mode 100644
index 000000000..a90e68a7e
--- /dev/null
+++ b/WordPress/src/main/res/layout/stats_widget_layout.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/stats_widget_outer_container"
+ android:background="@drawable/stats_widget_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:showIn="@layout/stats_widget_info">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/stats_widget_header_background"
+ android:gravity="start|center_vertical"
+ android:padding="2dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="@dimen/stats_widget_icon_size"
+ android:layout_height="@dimen/stats_widget_icon_size"
+ app:srcCompat="@drawable/ic_tab_sites_pressed" />
+
+ <TextView
+ android:id="@+id/blog_title"
+ style="@style/StatsModuleTitle"
+ android:textSize="@dimen/stats_widget_text_sz"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textColor="@color/white"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:layout_marginLeft="4dp"
+ android:text="@string/stats_widget_name" >
+ </TextView>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:orientation="horizontal"
+ android:background="@color/grey_lighten_30" />
+
+ <LinearLayout
+ android:id="@+id/stats_widget_values_container"
+ android:layout_height="@dimen/stats_widget_main_container_size"
+ android:minHeight="@dimen/stats_widget_main_container_min_size"
+ android:layout_width="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <!-- Views -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:background="@drawable/stats_visitors_and_views_button_white"
+ android:clickable="false">
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="@dimen/stats_widget_image_layout_size"
+ android:layout_height="@dimen/stats_widget_image_layout_size"
+ android:layout_marginRight="@dimen/stats_widget_image_layout_margin"
+ android:layout_marginEnd="@dimen/stats_widget_image_layout_margin"
+ app:srcCompat="@drawable/stats_icon_views" />
+
+ <TextView
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/stats_widget_text_sz_small"
+ android:textAllCaps="true"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/stats_views"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/stats_widget_views"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/stats_widget_text_sz"
+ android:paddingTop="@dimen/margin_extra_small"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/stats_default_number_zero"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <!-- Visitors -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:background="@drawable/stats_visitors_and_views_button_white"
+ android:clickable="false">
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="@dimen/stats_widget_image_layout_size"
+ android:layout_height="@dimen/stats_widget_image_layout_size"
+ android:layout_marginRight="@dimen/stats_widget_image_layout_margin"
+ android:layout_marginEnd="@dimen/stats_widget_image_layout_margin"
+ app:srcCompat="@drawable/stats_icon_visitors" />
+
+ <TextView
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/stats_widget_text_sz_small"
+ android:textAllCaps="true"
+ android:maxLines="1"
+ android:text="@string/stats_visitors"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/stats_widget_visitors"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/stats_widget_text_sz"
+ android:paddingTop="@dimen/margin_extra_small"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/stats_default_number_zero"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <!-- Likes -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:background="@drawable/stats_visitors_and_views_button_white"
+ android:clickable="false">
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="@dimen/stats_widget_image_layout_size"
+ android:layout_height="@dimen/stats_widget_image_layout_size"
+ android:layout_marginRight="@dimen/stats_widget_image_layout_margin"
+ android:layout_marginEnd="@dimen/stats_widget_image_layout_margin"
+ app:srcCompat="@drawable/stats_icon_likes" />
+
+ <TextView
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/stats_widget_text_sz_small"
+ android:textAllCaps="true"
+ android:maxLines="1"
+ android:text="@string/stats_likes"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/stats_widget_likes"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/stats_widget_text_sz"
+ android:paddingTop="@dimen/margin_extra_small"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/stats_default_number_zero"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <!-- Comments -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:background="@drawable/stats_visitors_and_views_button_latest_white"
+ android:clickable="false">
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="@dimen/stats_widget_image_layout_size"
+ android:layout_height="@dimen/stats_widget_image_layout_size"
+ android:layout_marginRight="@dimen/stats_widget_image_layout_margin"
+ android:layout_marginEnd="@dimen/stats_widget_image_layout_margin"
+ app:srcCompat="@drawable/stats_icon_comments" />
+
+ <TextView
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/stats_widget_text_sz_small"
+ android:textAllCaps="true"
+ android:maxLines="1"
+ android:text="@string/stats_comments"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/stats_widget_comments"
+ android:textColor="@color/blue_wordpress"
+ android:textSize="@dimen/stats_widget_text_sz"
+ android:paddingTop="@dimen/margin_extra_small"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/stats_default_number_zero"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+
+ <LinearLayout
+ android:id="@+id/stats_widget_error_container"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/stats_widget_main_container_size"
+ android:minHeight="@dimen/stats_widget_main_container_min_size"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/stats_widget_error_text"
+ android:textSize="@dimen/stats_widget_text_sz"
+ android:textColor="@color/grey_darken_20"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center_horizontal|center_vertical"
+ android:layout_margin="4dip"
+ android:text="@string/stats_widget_loading_data" >
+ </TextView>
+ </LinearLayout>
+
+</LinearLayout>
+
+
diff --git a/WordPress/src/main/res/layout/suggestion_list_row.xml b/WordPress/src/main/res/layout/suggestion_list_row.xml
new file mode 100644
index 000000000..f46d5d1db
--- /dev/null
+++ b/WordPress/src/main/res/layout/suggestion_list_row.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:padding="@dimen/margin_large">
+
+ <org.wordpress.android.widgets.WPNetworkImageView
+ android:id="@+id/suggest_list_row_avatar"
+ android:layout_width="@dimen/avatar_sz_small"
+ android:layout_height="@dimen/avatar_sz_small"
+ android:gravity="center_vertical" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/suggestion_list_row_user_login_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="user_login" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/suggestion_list_row_display_name_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_weight="2"
+ android:ellipsize="end"
+ android:gravity="center_vertical|right"
+ android:maxLines="1"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="display_name" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/tab_icon.xml b/WordPress/src/main/res/layout/tab_icon.xml
new file mode 100644
index 000000000..aaea8f3d9
--- /dev/null
+++ b/WordPress/src/main/res/layout/tab_icon.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Custom view used when showing icons in main activity's tab layout
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/toolbar_height"
+ android:paddingLeft="@dimen/tabstrip_icon_spacing"
+ android:paddingRight="@dimen/tabstrip_icon_spacing"
+ tools:background="@color/color_primary">
+
+ <ImageView
+ android:id="@+id/tab_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ tools:src="@drawable/main_tab_notifications" />
+
+ <View
+ android:id="@+id/tab_badge"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_gravity="center"
+ android:layout_marginLeft="-8dp"
+ android:layout_marginTop="-8dp"
+ android:background="@drawable/badge"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+</FrameLayout>
diff --git a/WordPress/src/main/res/layout/tab_text.xml b/WordPress/src/main/res/layout/tab_text.xml
new file mode 100644
index 000000000..c315e8aed
--- /dev/null
+++ b/WordPress/src/main/res/layout/tab_text.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/text_tab"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/toolbar_height"
+ android:background="?android:selectableItemBackground"
+ android:gravity="center"
+ android:paddingLeft="@dimen/margin_large"
+ android:paddingRight="@dimen/margin_large"
+ android:textAllCaps="true"
+ android:textColor="@color/tab_text_color"
+ android:textSize="@dimen/text_sz_medium"
+ android:textStyle="bold"
+ tools:text="text_tab" /> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/tag_list_row.xml b/WordPress/src/main/res/layout/tag_list_row.xml
new file mode 100644
index 000000000..bdb32192b
--- /dev/null
+++ b/WordPress/src/main/res/layout/tag_list_row.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:padding="@dimen/margin_large">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/tag_list_row_tag_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_medium"
+ tools:text="tag_name" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/theme_browser_activity.xml b/WordPress/src/main/res/layout/theme_browser_activity.xml
new file mode 100644
index 000000000..0a5de6b70
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_browser_activity.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/theme_browser_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <include
+ android:id="@+id/toolbar_search"
+ layout="@layout/toolbar_search" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/theme_browser_fragment.xml b/WordPress/src/main/res/layout/theme_browser_fragment.xml
new file mode 100644
index 000000000..94e53d257
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_browser_fragment.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/theme_no_search_result_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/theme_no_search_result_found"
+ android:visibility="gone"
+ android:layout_margin="@dimen/margin_extra_large"/>
+
+ <org.wordpress.android.util.widgets.CustomSwipeRefreshLayout
+ android:id="@+id/ptr_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.wordpress.android.widgets.HeaderGridView
+ android:id="@+id/theme_listview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:horizontalSpacing="@dimen/margin_none"
+ android:scrollbarStyle="outsideOverlay"
+ android:columnWidth="@dimen/theme_browser_default_column_width"
+ android:numColumns="auto_fit"
+ android:divider="@android:color/transparent"
+ android:stretchMode="columnWidth"
+ android:layout_below="@id/theme_no_search_result_text">
+ </org.wordpress.android.widgets.HeaderGridView>
+ </org.wordpress.android.util.widgets.CustomSwipeRefreshLayout>
+
+ <ProgressBar
+ android:id="@+id/theme_loading_progress_bar"
+ style="?android:attr/progressBarStyleInverse"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="@dimen/margin_large"
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+ <RelativeLayout
+ android:id="@+id/empty_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerHorizontal="true"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <ImageView
+ android:id="@+id/drake_empty_results"
+ android:layout_width="@dimen/drake_themes_width"
+ android:layout_height="wrap_content"
+ app:srcCompat="@drawable/drake_empty_results"
+ android:adjustViewBounds="true"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/theme_no_search_result_found" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text_empty"
+ style="@style/WordPress.EmptyList.Title"
+ android:layout_below="@id/drake_empty_results"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:text="@string/themes_fetching"
+ app:fixWidowWords="true"/>
+ </RelativeLayout>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/theme_details_fragment.xml b/WordPress/src/main/res/layout/theme_details_fragment.xml
new file mode 100644
index 000000000..77962f2e3
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_details_fragment.xml
@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <RelativeLayout
+ android:id="@+id/theme_details_fragment_left_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="10dp" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="20dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/theme_details_name"
+ android:textSize="22sp"
+ tools:text="This is a text"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_attributes_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_name"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/theme_details_fragment_current_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_current"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:paddingRight="16dp"
+ android:text="@string/theme_current_theme"
+ android:textColor="@color/blue_wordpress"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/theme_details_fragment_premium_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/theme_icon_premium"
+ android:drawablePadding="8dp"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_premium_theme"
+ android:textColor="@color/theme_details_premium"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <org.wordpress.android.ui.FadeInNetworkImageView
+ android:id="@+id/theme_details_fragment_image"
+ android:layout_width="300dp"
+ android:layout_height="225dp"
+ android:layout_below="@id/theme_details_fragment_attributes_container"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="12dp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_image"
+ android:layout_marginBottom="18dp"
+ android:layout_marginLeft="30dp"
+ android:layout_marginRight="30dp"
+ android:layout_marginTop="18dp"
+ android:orientation="horizontal" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_preview_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="5dp"
+ android:layout_weight="1"
+ android:background="@drawable/media_blue_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/themes_live_preview"
+ android:textColor="@color/theme_details_button"/>
+
+ <FrameLayout
+ android:id="@+id/theme_details_fragment_activate_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" >
+
+ <Button
+ android:id="@+id/theme_details_fragment_activate_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/theme_activate_button"
+ android:textColor="@color/theme_details_button"/>
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_activating_progress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone" >
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/theme_activating_button"
+ android:textColor="@color/theme_details_button"
+ android:textSize="18sp"/>
+ </LinearLayout>
+ </FrameLayout>
+
+ <Button
+ android:id="@+id/theme_details_fragment_view_site_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:background="@drawable/theme_activate_button_selector"
+ android:fontFamily="sans-serif-light"
+ android:padding="10dp"
+ android:text="@string/view_site"
+ android:textColor="@color/theme_details_button"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/theme_details_divider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_button_container" />
+
+ <TextView
+ style="@style/ThemeDetailsHeader"
+ android:id="@+id/theme_details_fragment_details_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_divider"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_details_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <TextView
+ android:id="@+id/theme_details_fragment_details_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_label"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp"
+ android:textStyle="normal"/>
+
+ <TextView
+ style="@style/ThemeDetailsHeader"
+ android:id="@+id/theme_details_fragment_features_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_details_description"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="18dp"
+ android:text="@string/themes_features_label"
+ android:textColor="@color/grey_dark"
+ android:textSize="16sp" />
+
+ <LinearLayout
+ android:id="@+id/theme_details_fragment_features_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/theme_details_fragment_features_label"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal" >
+ </LinearLayout>
+ </RelativeLayout>
+
+</ScrollView>
diff --git a/WordPress/src/main/res/layout/theme_feature_text.xml b/WordPress/src/main/res/layout/theme_feature_text.xml
new file mode 100644
index 000000000..3d0188b06
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_feature_text.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/theme_feature_text_bg"
+ android:padding="8dp"
+ android:textColor="@color/theme_feature_text" >
+
+</TextView> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/theme_grid_cardview_header.xml b/WordPress/src/main/res/layout/theme_grid_cardview_header.xml
new file mode 100644
index 000000000..6588ed013
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_grid_cardview_header.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginBottom="@dimen/cardview_default_radius"
+ android:layout_marginRight="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_margin="@dimen/theme_browser_cardview_header_margin">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textColor="@color/grey"
+ android:textSize="@dimen/text_sz_small"
+ android:textAllCaps="true"
+ android:text="@string/current_theme" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/header_theme_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textColor="@color/black"
+ android:text="@string/current_theme" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/theme_browser_separator_thickness"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/customize"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/theme_browser_header_button_height"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_none"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/customize"
+ app:srcCompat="@drawable/ic_theme_customize" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_dark"
+ android:text="@string/customize" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="@dimen/theme_browser_separator_thickness"
+ android:layout_height="match_parent"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/details"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/theme_browser_header_button_height"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_none"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/details"
+ app:srcCompat="@drawable/ic_theme_details" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_dark"
+ android:text="@string/details" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="@dimen/theme_browser_separator_thickness"
+ android:layout_height="match_parent"
+ android:background="@color/reader_divider_grey" />
+
+ <LinearLayout
+ android:id="@+id/support"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/theme_browser_header_button_height"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_none"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/support"
+ app:srcCompat="@drawable/ic_theme_support" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/grey_dark"
+ android:text="@string/support" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/theme_grid_cardview_header_search.xml b/WordPress/src/main/res/layout/theme_grid_cardview_header_search.xml
new file mode 100644
index 000000000..04d8f028b
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_grid_cardview_header_search.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginTop="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginBottom="@dimen/cardview_default_radius"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground">
+
+ <Spinner
+ android:id="@+id/theme_filter_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/theme_browser_header_button_height"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:visibility="gone"
+ android:spinnerMode="dropdown" />
+
+ <ImageButton
+ android:id="@+id/theme_search"
+ android:layout_width="@dimen/theme_browser_more_button_height"
+ android:layout_height="@dimen/theme_browser_more_button_height"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"
+ android:layout_centerVertical="true"
+ app:srcCompat="@drawable/ic_search"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/search"
+ android:layout_alignParentLeft="true" />
+
+ </RelativeLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/layout/theme_grid_item.xml b/WordPress/src/main/res/layout/theme_grid_item.xml
new file mode 100644
index 000000000..8c24f3821
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_grid_item.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/theme_grid_card_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.CardView
+ android:id="@+id/theme_grid_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"
+ android:layout_marginRight="@dimen/theme_browser_cardview_margin_large"
+ card_view:cardCornerRadius="@dimen/cardview_default_radius"
+ card_view:cardElevation="@dimen/card_elevation">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/cardview_default_radius">
+
+ <FrameLayout
+ android:id="@+id/theme_grid_item_image_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:foreground="?android:attr/selectableItemBackground">
+
+ <org.wordpress.android.ui.FadeInNetworkImageView
+ android:id="@+id/theme_grid_item_image"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:adjustViewBounds="true"
+ android:scaleType="fitCenter" />
+
+ </FrameLayout>
+
+ <RelativeLayout
+ android:id="@+id/theme_grid_item_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/theme_grid_item_image_layout"
+ android:layout_marginTop="@dimen/cardview_default_radius">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentLeft="true">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/theme_grid_item_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/black"
+ android:textStyle="bold"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_header_margin" />
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/theme_grid_item_price"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textColor="@color/theme_price"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"/>
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/theme_grid_item_active"
+ android:textSize="@dimen/text_sz_extra_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/theme_active"
+ android:textStyle="bold"
+ android:visibility="gone"
+ android:textAllCaps="true"
+ android:text="@string/active"
+ android:layout_marginLeft="@dimen/theme_browser_cardview_margin_large"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true">
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="@dimen/theme_browser_separator_thickness"
+ android:layout_height="match_parent"
+ android:background="@color/reader_divider_grey" />
+
+ <ImageButton
+ android:id="@+id/theme_grid_item_image_button"
+ android:layout_width="@dimen/theme_browser_more_button_width"
+ android:layout_height="@dimen/theme_browser_more_button_height"
+ android:padding="@dimen/theme_browser_more_button_padding"
+ app:srcCompat="@drawable/ic_action_more_grey"
+ android:background="?android:attr/selectableItemBackground"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/button_more" />
+
+ </LinearLayout>
+
+ </RelativeLayout>
+
+ </RelativeLayout>
+
+ </android.support.v7.widget.CardView>
+
+</RelativeLayout>
diff --git a/WordPress/src/main/res/layout/theme_web_activity.xml b/WordPress/src/main/res/layout/theme_web_activity.xml
new file mode 100644
index 000000000..00257a478
--- /dev/null
+++ b/WordPress/src/main/res/layout/theme_web_activity.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/webview_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <WebView
+ android:id="@+id/webView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ style="@android:style/Widget.ProgressBar.Horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/progress_bar_height"
+ android:indeterminate="false"
+ android:layout_alignTop="@+id/webView"
+ android:progressDrawable="@drawable/progressbar_horizontal" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/toolbar.xml b/WordPress/src/main/res/layout/toolbar.xml
new file mode 100644
index 000000000..fec3f0c12
--- /dev/null
+++ b/WordPress/src/main/res/layout/toolbar.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.Toolbar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:elevation="@dimen/appbar_elevation"
+ app:contentInsetLeft="@dimen/toolbar_content_offset"
+ app:contentInsetStart="@dimen/toolbar_content_offset"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
+ app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ tools:targetApi="LOLLIPOP">
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/toolbar_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:visibility="gone"
+ android:textColor="@color/white"
+ android:textStyle="bold"
+ android:textSize="@dimen/text_sz_extra_large"
+ android:fontFamily="sans-serif-light" />
+
+</android.support.v7.widget.Toolbar>
diff --git a/WordPress/src/main/res/layout/toolbar_search.xml b/WordPress/src/main/res/layout/toolbar_search.xml
new file mode 100644
index 000000000..8c70253c2
--- /dev/null
+++ b/WordPress/src/main/res/layout/toolbar_search.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.Toolbar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/toolbar_search"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="@color/white"
+ android:elevation="@dimen/appbar_elevation"
+ app:contentInsetLeft="@dimen/margin_none"
+ app:contentInsetStart="@dimen/margin_none"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
+ app:theme="@style/ThemeOverlay.AppCompat.ActionBar"
+ tools:targetApi="LOLLIPOP"/>
diff --git a/WordPress/src/main/res/layout/toolbar_spinner.xml b/WordPress/src/main/res/layout/toolbar_spinner.xml
new file mode 100644
index 000000000..d8623482e
--- /dev/null
+++ b/WordPress/src/main/res/layout/toolbar_spinner.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ spinner to be inflated within a toolbar
+-->
+<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/action_bar_spinner"
+ style="?android:actionDropDownStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" />
diff --git a/WordPress/src/main/res/layout/toolbar_spinner_dropdown_item.xml b/WordPress/src/main/res/layout/toolbar_spinner_dropdown_item.xml
new file mode 100644
index 000000000..b1dc01cd0
--- /dev/null
+++ b/WordPress/src/main/res/layout/toolbar_spinner_dropdown_item.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_large"
+ android:textColor="@color/grey_dark"
+ android:textSize="@dimen/text_sz_large"
+ tools:text="spinner dropdown item" />
+
diff --git a/WordPress/src/main/res/layout/toolbar_spinner_item.xml b/WordPress/src/main/res/layout/toolbar_spinner_item.xml
new file mode 100644
index 000000000..e4655a9e0
--- /dev/null
+++ b/WordPress/src/main/res/layout/toolbar_spinner_item.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.widgets.WPTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/text"
+ style="?android:spinnerItemStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#ffffff"
+ android:paddingRight="0dp"
+ android:paddingLeft="0dp"
+ tools:text="spinner item"/>
diff --git a/WordPress/src/main/res/layout/webview.xml b/WordPress/src/main/res/layout/webview.xml
new file mode 100644
index 000000000..c0a865dbb
--- /dev/null
+++ b/WordPress/src/main/res/layout/webview.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/webview_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include
+ layout="@layout/toolbar"
+ android:id="@+id/toolbar" />
+
+ <WebView
+ android:id="@+id/webView"
+ android:layout_below="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ style="@android:style/Widget.ProgressBar.Horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/progress_bar_height"
+ android:indeterminate="false"
+ android:layout_alignTop="@+id/webView"
+ android:progressDrawable="@drawable/progressbar_horizontal" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/welcome_activity.xml b/WordPress/src/main/res/layout/welcome_activity.xml
new file mode 100644
index 000000000..2ed25c69e
--- /dev/null
+++ b/WordPress/src/main/res/layout/welcome_activity.xml
@@ -0,0 +1,14 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/main_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/nux_background"
+ android:orientation="vertical"
+ android:baselineAligned="true">
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/WordPress/src/main/res/layout/wp_simple_list_item_1.xml b/WordPress/src/main/res/layout/wp_simple_list_item_1.xml
new file mode 100644
index 000000000..713ab3d58
--- /dev/null
+++ b/WordPress/src/main/res/layout/wp_simple_list_item_1.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="vertical" >
+
+ <org.wordpress.android.widgets.WPTextView
+ android:id="@+id/text"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:gravity="center_vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall" >
+ </org.wordpress.android.widgets.WPTextView>
+
+ <View
+ android:background="@color/divider_grey"
+ android:layout_height="@dimen/list_divider_height"
+ android:layout_width="match_parent" >
+ </View>
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/menu/categories.xml b/WordPress/src/main/res/menu/categories.xml
new file mode 100644
index 000000000..d9a211da4
--- /dev/null
+++ b/WordPress/src/main/res/menu/categories.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_new_category"
+ android:icon="@drawable/ic_add_white_24dp"
+ android:title="@string/add_new_category"
+ app:showAsAction="ifRoom" />
+</menu>
diff --git a/WordPress/src/main/res/menu/comment_detail.xml b/WordPress/src/main/res/menu/comment_detail.xml
new file mode 100644
index 000000000..2745fa0ea
--- /dev/null
+++ b/WordPress/src/main/res/menu/comment_detail.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_edit_comment"
+ android:icon="@drawable/ab_icon_edit"
+ app:showAsAction="never"
+ android:title="@string/edit_comment"/>
+</menu>
diff --git a/WordPress/src/main/res/menu/edit_comment.xml b/WordPress/src/main/res/menu/edit_comment.xml
new file mode 100644
index 000000000..36abb4a7f
--- /dev/null
+++ b/WordPress/src/main/res/menu/edit_comment.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_save_comment"
+ android:icon="@drawable/ic_send_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/upload" />
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/edit_post.xml b/WordPress/src/main/res/menu/edit_post.xml
new file mode 100644
index 000000000..20d86e89d
--- /dev/null
+++ b/WordPress/src/main/res/menu/edit_post.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_post_settings"
+ android:icon="@drawable/ic_settings_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/settings"/>
+ <item
+ android:id="@+id/menu_save_post"
+ app:showAsAction="always"
+ android:title="@string/publish_post"/>
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/edit_post_legacy.xml b/WordPress/src/main/res/menu/edit_post_legacy.xml
new file mode 100644
index 000000000..85c8991a1
--- /dev/null
+++ b/WordPress/src/main/res/menu/edit_post_legacy.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_preview_post"
+ android:icon="@drawable/ic_remove_red_eye_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/preview"/>
+ <item
+ android:id="@+id/menu_save_post"
+ app:showAsAction="always"
+ android:title="@string/publish_post"/>
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/list_editor.xml b/WordPress/src/main/res/menu/list_editor.xml
new file mode 100644
index 000000000..829b5eed7
--- /dev/null
+++ b/WordPress/src/main/res/menu/list_editor.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_select_all"
+ android:title="@string/select_all"
+ android:icon="@drawable/ic_select_all_white_24dp"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/delete"
+ android:icon="@drawable/ic_delete_white_24dp"
+ app:showAsAction="ifRoom" />
+
+</menu>
diff --git a/WordPress/src/main/res/menu/media.xml b/WordPress/src/main/res/menu/media.xml
new file mode 100644
index 000000000..88be791ec
--- /dev/null
+++ b/WordPress/src/main/res/menu/media.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_search"
+ app:actionViewClass="android.support.v7.widget.SearchView"
+ android:icon="@drawable/ic_search_white_24dp"
+ app:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/search" />
+
+ <item
+ android:id="@+id/menu_new_media"
+ android:icon="@drawable/ic_add_white_24dp"
+ app:showAsAction="ifRoom"
+ android:title="@string/new_media" />
+
+</menu>
diff --git a/WordPress/src/main/res/menu/media_details.xml b/WordPress/src/main/res/menu/media_details.xml
new file mode 100644
index 000000000..a9189fc90
--- /dev/null
+++ b/WordPress/src/main/res/menu/media_details.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_edit_media"
+ android:icon="@drawable/ic_create_white_24dp"
+ android:title="@string/edit_media"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/menu_copy_media_url"
+ android:icon="@drawable/ic_content_copy_white_24dp"
+ android:title="@string/media_details_copy_url"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/menu_delete"
+ android:icon="@drawable/ic_delete_white_24dp"
+ android:title="@string/delete"
+ app:showAsAction="ifRoom" />
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/media_edit.xml b/WordPress/src/main/res/menu/media_edit.xml
new file mode 100644
index 000000000..1da847026
--- /dev/null
+++ b/WordPress/src/main/res/menu/media_edit.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_save_media"
+ android:icon="@drawable/ic_save_white_24dp"
+ app:showAsAction="ifRoom"
+ android:title="@string/new_media"/>
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/media_gallery.xml b/WordPress/src/main/res/menu/media_gallery.xml
new file mode 100644
index 000000000..262456dce
--- /dev/null
+++ b/WordPress/src/main/res/menu/media_gallery.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_add_media"
+ android:icon="@drawable/ic_add_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/content_description_add_media"/>
+ <item
+ android:id="@+id/menu_save"
+ android:icon="@drawable/ic_save_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/save"/>
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/media_multiselect.xml b/WordPress/src/main/res/menu/media_multiselect.xml
new file mode 100644
index 000000000..92dd61951
--- /dev/null
+++ b/WordPress/src/main/res/menu/media_multiselect.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/media_multiselect_actionbar_gallery"
+ android:icon="@drawable/tab_icon_create_gallery"
+ app:showAsAction="always"
+ android:title="@string/media_add_new_media_gallery"/>
+ <item
+ android:id="@+id/media_multiselect_actionbar_post"
+ android:icon="@drawable/ic_create_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/new_post"/>
+ <item
+ android:id="@+id/media_multiselect_actionbar_trash"
+ android:icon="@drawable/ic_delete_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/delete"/>
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/media_picker.xml b/WordPress/src/main/res/menu/media_picker.xml
new file mode 100644
index 000000000..9cf0e1ef9
--- /dev/null
+++ b/WordPress/src/main/res/menu/media_picker.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/capture_image"
+ android:title="@string/take_photo"
+ android:icon="@drawable/ic_action_camera"
+ android:enabled="true"
+ android:visible="true"
+ android:orderInCategory="100"
+ app:showAsAction="ifRoom|withText" />
+
+ <item
+ android:id="@+id/capture_video"
+ android:title="@string/take_video"
+ android:icon="@drawable/ic_action_video"
+ android:enabled="true"
+ android:visible="true"
+ android:orderInCategory="101"
+ app:showAsAction="ifRoom|withText" />
+
+</menu>
diff --git a/WordPress/src/main/res/menu/menu_comments_cab.xml b/WordPress/src/main/res/menu/menu_comments_cab.xml
new file mode 100644
index 000000000..e7f0a660a
--- /dev/null
+++ b/WordPress/src/main/res/menu/menu_comments_cab.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Contextual ActionBar menu for comment list
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_approve"
+ android:icon="@drawable/ic_thumb_up_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/mnu_comment_approve" />
+ <item
+ android:id="@+id/menu_unapprove"
+ android:icon="@drawable/ic_thumb_down_white_24dp"
+ app:showAsAction="always"
+ android:title="@string/mnu_comment_unapprove" />
+ <item
+ android:id="@+id/menu_spam"
+ android:icon="@drawable/ic_report_white_24dp"
+ app:showAsAction="ifRoom"
+ android:title="@string/mnu_comment_spam" />
+ <item
+ android:id="@+id/menu_trash"
+ android:icon="@drawable/ic_delete_white_24dp"
+ app:showAsAction="ifRoom"
+ android:title="@string/mnu_comment_trash" />
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/menu_media_picker_action_mode.xml b/WordPress/src/main/res/menu/menu_media_picker_action_mode.xml
new file mode 100644
index 000000000..c17512f90
--- /dev/null
+++ b/WordPress/src/main/res/menu/menu_media_picker_action_mode.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_media_content_selection_gallery"
+ android:title="@string/new_media_gallery"
+ android:icon="@drawable/tab_icon_create_gallery"
+ android:enabled="true"
+ android:visible="true" />
+
+ <item
+ android:id="@+id/menu_media_selection_confirmed"
+ android:title="@string/confirm"
+ android:icon="@drawable/action_mode_confirm_checkmark"
+ android:enabled="true"
+ android:visible="true" />
+
+</menu>
diff --git a/WordPress/src/main/res/menu/notifications_settings.xml b/WordPress/src/main/res/menu/notifications_settings.xml
new file mode 100644
index 000000000..365e32757
--- /dev/null
+++ b/WordPress/src/main/res/menu/notifications_settings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_notifications_settings_search"
+ app:actionViewClass="android.support.v7.widget.SearchView"
+ android:icon="@drawable/ic_search_white_24dp"
+ app:showAsAction="collapseActionView|ifRoom"
+ android:title="@string/search_sites"
+ android:visible="false"/>
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/people_invite.xml b/WordPress/src/main/res/menu/people_invite.xml
new file mode 100644
index 000000000..8f3762717
--- /dev/null
+++ b/WordPress/src/main/res/menu/people_invite.xml
@@ -0,0 +1,8 @@
+<!--<?xml version="1.0" encoding="utf-8"?>-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/send_invitation"
+ android:title="@string/send"
+ app:showAsAction="always" />
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/people_list.xml b/WordPress/src/main/res/menu/people_list.xml
new file mode 100644
index 000000000..0d26a2afb
--- /dev/null
+++ b/WordPress/src/main/res/menu/people_list.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/invite"
+ android:icon="@drawable/ic_add_white_24dp"
+ android:title="@string/button_invite"
+ app:showAsAction="always" />
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/person_detail.xml b/WordPress/src/main/res/menu/person_detail.xml
new file mode 100644
index 000000000..0d5fa9557
--- /dev/null
+++ b/WordPress/src/main/res/menu/person_detail.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/remove_person"
+ android:icon="@drawable/gridicons_trash"
+ android:title="@string/button_delete"
+ app:showAsAction="always" />
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/post_preview.xml b/WordPress/src/main/res/menu/post_preview.xml
new file mode 100644
index 000000000..2fb65b029
--- /dev/null
+++ b/WordPress/src/main/res/menu/post_preview.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_edit"
+ android:title="@string/button_edit"
+ app:showAsAction="always" />
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/reader_detail.xml b/WordPress/src/main/res/menu/reader_detail.xml
new file mode 100644
index 000000000..3de98e08b
--- /dev/null
+++ b/WordPress/src/main/res/menu/reader_detail.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_browse"
+ android:icon="@drawable/ic_genericon_web_white_24dp"
+ android:title="@string/view_in_browser"
+ app:showAsAction="always" />
+
+ <item
+ android:id="@+id/menu_share"
+ android:icon="@drawable/ic_share_white_24dp"
+ android:title="@string/share_action"
+ app:showAsAction="always" />
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/reader_list.xml b/WordPress/src/main/res/menu/reader_list.xml
new file mode 100644
index 000000000..0f810502c
--- /dev/null
+++ b/WordPress/src/main/res/menu/reader_list.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_reader_settings"
+ android:icon="@drawable/gridicons_cog_light"
+ android:title="@string/reader_menu_tags"
+ app:showAsAction="always" />
+
+ <item
+ android:id="@+id/menu_reader_search"
+ android:icon="@drawable/gridicons_search_light"
+ android:title="@string/search"
+ app:actionViewClass="android.support.v7.widget.SearchView"
+ app:showAsAction="always|collapseActionView" />
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/site_picker.xml b/WordPress/src/main/res/menu/site_picker.xml
new file mode 100644
index 000000000..80a05d7d8
--- /dev/null
+++ b/WordPress/src/main/res/menu/site_picker.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/ic_search_white_24dp"
+ android:title="@string/search"
+ app:actionViewClass="android.widget.SearchView"
+ app:showAsAction="collapseActionView|ifRoom" />
+
+ <item
+ android:id="@+id/menu_add"
+ android:icon="@drawable/ic_add_white_24dp"
+ android:title="@string/site_picker_add_site"
+ app:showAsAction="collapseActionView|ifRoom" />
+
+ <item
+ android:id="@+id/menu_edit"
+ android:title="@string/site_picker_edit_visibility"
+ app:showAsAction="never" />
+
+</menu>
diff --git a/WordPress/src/main/res/menu/site_picker_action_mode.xml b/WordPress/src/main/res/menu/site_picker_action_mode.xml
new file mode 100644
index 000000000..cc257d248
--- /dev/null
+++ b/WordPress/src/main/res/menu/site_picker_action_mode.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_show"
+ android:title="@string/show"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/menu_hide"
+ android:title="@string/hide"
+ app:showAsAction="collapseActionView|ifRoom" />
+
+ <item
+ android:id="@+id/menu_select_all"
+ android:title="@string/select_all"
+ app:showAsAction="never" />
+
+ <item
+ android:id="@+id/menu_deselect_all"
+ android:title="@string/deselect_all"
+ app:showAsAction="never" />
+
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/theme.xml b/WordPress/src/main/res/menu/theme.xml
new file mode 100644
index 000000000..f5a6b8b1b
--- /dev/null
+++ b/WordPress/src/main/res/menu/theme.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/ic_search_white_24dp"
+ app:showAsAction="collapseActionView|ifRoom"
+ android:title="@string/search"/>
+
+</menu>
diff --git a/WordPress/src/main/res/menu/theme_more.xml b/WordPress/src/main/res/menu/theme_more.xml
new file mode 100644
index 000000000..20939bad0
--- /dev/null
+++ b/WordPress/src/main/res/menu/theme_more.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_try_and_customize"
+ android:icon="@drawable/ic_search_white_24dp"
+ android:title="@string/theme_try_and_customize"/>
+
+ <item
+ android:id="@+id/menu_activate"
+ android:icon="@drawable/ic_search_white_24dp"
+ android:title="@string/theme_activate"/>
+
+ <item
+ android:id="@+id/menu_view"
+ android:icon="@drawable/ic_search_white_24dp"
+ android:title="@string/theme_view"/>
+
+ <item
+ android:id="@+id/menu_details"
+ android:icon="@drawable/ic_search_white_24dp"
+ android:title="@string/theme_details"/>
+
+ <item
+ android:id="@+id/menu_support"
+ android:icon="@drawable/ic_search_white_24dp"
+ android:title="@string/theme_support"/>
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/menu/theme_search.xml b/WordPress/src/main/res/menu/theme_search.xml
new file mode 100644
index 000000000..9a092667c
--- /dev/null
+++ b/WordPress/src/main/res/menu/theme_search.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_theme_search"
+ app:actionViewClass="android.support.v7.widget.SearchView"
+ android:icon="@drawable/ic_search"
+ app:showAsAction="collapseActionView|ifRoom"
+ android:title="@string/search"/>
+</menu>
diff --git a/WordPress/src/main/res/menu/theme_web.xml b/WordPress/src/main/res/menu/theme_web.xml
new file mode 100644
index 000000000..bf473f2e8
--- /dev/null
+++ b/WordPress/src/main/res/menu/theme_web.xml
@@ -0,0 +1,9 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="org.wordpress.android.ui.themes.ThemeWebActivity">
+ <item android:id="@+id/action_activate"
+ android:title="@string/theme_activate_button"
+ android:orderInCategory="100"
+ app:showAsAction="always" />
+</menu>
diff --git a/WordPress/src/main/res/menu/webview.xml b/WordPress/src/main/res/menu/webview.xml
new file mode 100644
index 000000000..46e310ed7
--- /dev/null
+++ b/WordPress/src/main/res/menu/webview.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_browser"
+ android:icon="@drawable/ic_genericon_web_white_24dp"
+ android:title="@string/view_in_browser"
+ app:showAsAction="ifRoom" />
+ <item
+ android:id="@+id/menu_share"
+ android:icon="@drawable/ic_share_white_24dp"
+ android:title="@string/share_link"
+ app:showAsAction="ifRoom" />
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/ic_refresh_white_24dp"
+ android:title="@string/refresh"
+ app:showAsAction="never" />
+</menu> \ No newline at end of file
diff --git a/WordPress/src/main/res/mipmap-hdpi/app_icon.png b/WordPress/src/main/res/mipmap-hdpi/app_icon.png
new file mode 100644
index 000000000..18b5d0ac2
--- /dev/null
+++ b/WordPress/src/main/res/mipmap-hdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/mipmap-xhdpi/app_icon.png b/WordPress/src/main/res/mipmap-xhdpi/app_icon.png
new file mode 100644
index 000000000..ec028c861
--- /dev/null
+++ b/WordPress/src/main/res/mipmap-xhdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/mipmap-xxhdpi/app_icon.png b/WordPress/src/main/res/mipmap-xxhdpi/app_icon.png
new file mode 100644
index 000000000..cc0da073e
--- /dev/null
+++ b/WordPress/src/main/res/mipmap-xxhdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/mipmap-xxxhdpi/app_icon.png b/WordPress/src/main/res/mipmap-xxxhdpi/app_icon.png
new file mode 100644
index 000000000..0898a945e
--- /dev/null
+++ b/WordPress/src/main/res/mipmap-xxxhdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/main/res/values-ar/strings.xml b/WordPress/src/main/res/values-ar/strings.xml
new file mode 100644
index 000000000..4e41cd1a8
--- /dev/null
+++ b/WordPress/src/main/res/values-ar/strings.xml
@@ -0,0 +1,1142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">المسؤول</string>
+ <string name="role_editor">المحرر</string>
+ <string name="role_author">الكاتب</string>
+ <string name="role_contributor">المساهم</string>
+ <string name="role_follower">المتابع</string>
+ <string name="role_viewer">المشاهد</string>
+ <string name="error_post_my_profile_no_connection">لا يوجد اتصال، تعذر حفظ ملفك الشخصي</string>
+ <string name="alignment_none">لا شيء</string>
+ <string name="alignment_left">يسار</string>
+ <string name="alignment_right">يمين</string>
+ <string name="error_fetch_users_list">تعذر استرداد مستخدمي الموقع</string>
+ <string name="plans_manage">إدارة باقتك على\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">ليس لديك أي مشاهدين بعد.</string>
+ <string name="people_fetching">جارٍ إحضار المستخدمين...</string>
+ <string name="title_follower">المتابع</string>
+ <string name="title_email_follower">المتابع عبر البريد الإلكتروني</string>
+ <string name="people_empty_list_filtered_email_followers">ليس لديك أي متابعين عبر البريد الإلكتروني بعد.</string>
+ <string name="people_empty_list_filtered_followers">ليس لديك أي متابعين بعد.</string>
+ <string name="people_empty_list_filtered_users">ليس لديك أي مستخدمين بعد.</string>
+ <string name="people_dropdown_item_email_followers">المتابعون عبر البريد الإلكتروني</string>
+ <string name="people_dropdown_item_viewers">المشاهدون</string>
+ <string name="people_dropdown_item_followers">المتابعون</string>
+ <string name="people_dropdown_item_team">الفريق</string>
+ <string name="invite_message_usernames_limit">دعوة ما يقارب 10 عناوين بريد إلكتروني و/أو أسماء مستخدمين في وردبرس.كوم. سيتم إرسال تعليمات عن كيفية إنشاء اسم مستخدم لمَن لا يملك اسم مستخدم.</string>
+ <string name="viewer_remove_confirmation_message">إذا قمت بإزالة هذا المشاهد، فلن يتمكن بعد الآن من زيارة هذا الموقع.\n\nألا تزال تريد إزالة هذا المشاهد؟</string>
+ <string name="follower_remove_confirmation_message">في حالة إزالة المتابع، فسيتم وقف إرسال التنبيهات إليه عن هذا الموقع، ما لم يتابع مرة أخرى.\n\nألا تزال تريد إزالة هذا المتابع؟</string>
+ <string name="reader_label_view_gallery">عرض المعرض</string>
+ <string name="error_remove_follower">تعذر إزالة المتابع</string>
+ <string name="error_remove_viewer">تعذر إزالة المشاهد</string>
+ <string name="error_fetch_email_followers_list">تعذر استرداد متابعي الموقع عبر البريد الإلكتروني</string>
+ <string name="error_fetch_followers_list">تعذر استرداد متابعي الموقع</string>
+ <string name="editor_failed_uploads_switch_html">فشل رفع بعض الوسائط. يمكنك الإنتقال الى وضع HTML\nبهذه الحالة. إزالة جميع المرفوعات الفاشلة والمتابعة؟</string>
+ <string name="format_bar_description_html">وضع HTML</string>
+ <string name="visual_editor">المحرر المرئي</string>
+ <string name="image_thumbnail">الصورة المصغرة</string>
+ <string name="format_bar_description_ul">قائمة غير مرتبة</string>
+ <string name="format_bar_description_ol">قائمة مرتبة</string>
+ <string name="format_bar_description_more">إدراج المزيد</string>
+ <string name="format_bar_description_media">إدراج وسائط</string>
+ <string name="format_bar_description_strike">يتوسطه خط</string>
+ <string name="format_bar_description_quote">كتلة الإقتباس</string>
+ <string name="format_bar_description_link">إدراج رابط</string>
+ <string name="format_bar_description_italic">مائل</string>
+ <string name="format_bar_description_underline">أسفله خط</string>
+ <string name="image_settings_save_toast">تم حفظ التغييرات</string>
+ <string name="image_caption">التسمية التوضيحية</string>
+ <string name="image_alt_text">النص البديل</string>
+ <string name="image_link_to">رابط إلى</string>
+ <string name="image_width">العرض</string>
+ <string name="format_bar_description_bold">سميك</string>
+ <string name="image_settings_dismiss_dialog_title">تجاهل التغييرات الغير محفوظة؟</string>
+ <string name="stop_upload_dialog_title">إيقاف الرفع؟</string>
+ <string name="stop_upload_button">إيقاف الرفع</string>
+ <string name="alert_error_adding_media">خطأ ما قد حدث أثناء إدراج الوسائط</string>
+ <string name="alert_action_while_uploading">أنت تقوم برفع وسائط حاليًا. يرجى الانتظار حتى يكتمل هذا.</string>
+ <string name="alert_insert_image_html_mode">لا يمكن إدراج الوسائط مباشرة في وضع HTML. يرجى التبديل إلى الوضع المرئي.</string>
+ <string name="uploading_gallery_placeholder">جاري رفع المعرض...</string>
+ <string name="invite_error_some_failed">تم إرسال الدعوة ولكن بعض الأخطاء قد وقعت!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">تم إرسال الدعوة بنجاح</string>
+ <string name="tap_to_try_again">اضغط للمحاولة مرة أخرى!</string>
+ <string name="invite_error_sending">حدث خطأ ما أثناء محاولة إرسال الدعوة!</string>
+ <string name="invite_error_invalid_usernames_multiple">لا يمكن الإرسال: هناك اسم مستخدم أو بريد إلكتروني غير صالح</string>
+ <string name="invite_error_invalid_usernames_one">لا يمكن الإرسال: اسم المستخدم أو البريد الإلكتروني غير صالح</string>
+ <string name="invite_error_no_usernames">الرجاء إضافة اسم مستخدم واحد على الأقل</string>
+ <string name="invite_message_info">(اختياري) يمكنك إدخال رسالة مخصصة ﻻ تزيد عن 500 حرف والتي سوف يتم تضمينها في دعوتك للمستخدمين.</string>
+ <string name="invite_message_remaining_other">الأحرف المتبقية %d</string>
+ <string name="invite_message_remaining_one">1 حرف متبقي</string>
+ <string name="invite_message_remaining_zero">0 حرف متبقي</string>
+ <string name="invite_invalid_email">عنوان البريد الإلكتروني \'%s\' غير صالح</string>
+ <string name="invite_message_title">رسالة مخصصة</string>
+ <string name="invite_already_a_member">هناك عضو بالفعل باسم المستخدم \'%s\'</string>
+ <string name="invite_username_not_found">لم يتم العثور على مستخدم باسم \'%s\'</string>
+ <string name="invite">دعوة</string>
+ <string name="invite_names_title">اسم المستخدم أو البريد الإلكتروني</string>
+ <string name="signup_succeed_signin_failed">لقد تم إنشاء الحساب الخاص بك ولكن خطأ ما قد حدث أثناء قيامنا بتسجيل دخولك \n.\n حاول تسجيل الدخول باستخدام اسم المستخدم وكلمة المرور التي تم إنشائهم حديثًا.</string>
+ <string name="send_link">إرسال رابط</string>
+ <string name="my_site_header_external">خارجي</string>
+ <string name="invite_people">دعوة أشخاص</string>
+ <string name="label_clear_search_history">مسح محفوظات البحث</string>
+ <string name="dlg_confirm_clear_search_history">مسح محفوظات البحث؟</string>
+ <string name="reader_empty_posts_in_search_description">لم يتم العثور على مقالات عن %s للغة الخاصة بك</string>
+ <string name="reader_label_post_search_running">البحث...</string>
+ <string name="reader_label_related_posts">القراءة ذات الصلة</string>
+ <string name="reader_empty_posts_in_search_title">لم يتم العثور على مقالات</string>
+ <string name="reader_label_post_search_explainer">البحث في جميع مدونات WordPress.com العامة</string>
+ <string name="reader_hint_post_search">البحث في WordPress.com</string>
+ <string name="reader_title_related_post_detail">مقالة ذات صلة</string>
+ <string name="reader_title_search_results">البحث عن %s</string>
+ <string name="preview_screen_links_disabled">تم تعطيل الروابط في شاشة المعاينة</string>
+ <string name="draft_explainer">هذه المقالة مسود والتي لم تنشر بعد.</string>
+ <string name="send">إرسال</string>
+ <string name="user_remove_confirmation_message">إذا قمت بإزالة %1$s، فلن يعود هذا المستخدم قادرًا على الوصول إلى هذا الموقع، ومع ذلك سيبقى أي محتوى تم إنشاؤه بواسطة %1$s على الموقع.\n\nهل لا تزال ترغب في إزالة هذا المستخدم؟</string>
+ <string name="person_removed">تمت الإزالة بنجاح @%1$s</string>
+ <string name="person_remove_confirmation_title">إزالة %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">لم تنشر المواقع الموجودة في هذه القائمة أي شيء مؤخرًا</string>
+ <string name="people">أشخاص</string>
+ <string name="edit_user">تحرير إعدادات العضو</string>
+ <string name="role">الدور</string>
+ <string name="error_remove_user">تعذرت إزالة المستخدم</string>
+ <string name="error_fetch_viewers_list">تعذر استرداد مستخدمي الموقع</string>
+ <string name="error_update_role">تعذر تحديث دور المستخدم</string>
+ <string name="gravatar_camera_and_media_permission_required">يلزم الحصول على الإذن لتتمكن من تحديد صورة أو التقاطها</string>
+ <string name="error_updating_gravatar">حدث خطأ أثناء تحديث Gravatar الخاصة بك</string>
+ <string name="error_locating_image">حدث خطأ أثناء تحديد موقع الصورة التي تم اقتصاصها</string>
+ <string name="error_refreshing_gravatar">حدث خطأ أثناء إعادة تحميل جرافتار</string>
+ <string name="gravatar_tip">جديد! انقر على جرافاتار تغييره!</string>
+ <string name="error_cropping_image">حدث خطأ أثناء اقتصاص الصورة</string>
+ <string name="launch_your_email_app">بدء تشغيل تطبيق البريد الإلكتروني</string>
+ <string name="checking_email">التحقق من البريد الإلكتروني</string>
+ <string name="not_on_wordpress_com">أليست مشتركًا في وردبرس.كوم؟</string>
+ <string name="magic_link_unavailable_error_message">غير متاح حاليًا. يُرجى إدخال كلمة مرورك</string>
+ <string name="check_your_email">تحقق من بريدك الإلكتروني</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">احصل على الرابط المرسل إلى بريدك الإلكتروني لتتمكن من تسجيل الدخول في الحال</string>
+ <string name="logging_in">تسجيل الدخول</string>
+ <string name="enter_your_password_instead">أدخل كلمة مرورك بدلاً من ذلك</string>
+ <string name="web_address_dialog_hint">تظهر للعامة أثناء تعليقك.</string>
+ <string name="jetpack_not_connected_message">إضافة Jetpack مثبتة، ولكنها ليست متصلة بوردبرس.كوم. هل تريد توصيل Jetpack؟</string>
+ <string name="username_email">البريد الإلكتروني أو اسم المستخدم</string>
+ <string name="jetpack_not_connected">إضافة Jetpack غير متصلة</string>
+ <string name="new_editor_reflection_error">المحرر المرئي غير متوافق مع جهازك. كان\n معطلاً تلقائيًا.</string>
+ <string name="stats_insights_latest_post_no_title">(بلا عنوان)</string>
+ <string name="capture_or_pick_photo">التقاط صورة أو تحديدها</string>
+ <string name="plans_post_purchase_text_themes">يتوفر لديك الآن الوصول غير المحدود إلى القوالب المميزة. قم بمعاينة أي قالب في الموقع للبدء.</string>
+ <string name="plans_post_purchase_button_themes">تصفح القوالب</string>
+ <string name="plans_post_purchase_title_themes">البحث عن قالب مميز ومثالي</string>
+ <string name="plans_post_purchase_button_video">بدء مقال جديد</string>
+ <string name="plans_post_purchase_text_video">يمكنك رفع مقاطع الفيديو واستضافتها في موقعك باستخدام فيديوبرس وتخزين الوسائط الموسع لديك.</string>
+ <string name="plans_post_purchase_title_video">إحياء المقالات بملفات الفيديو</string>
+ <string name="plans_post_purchase_button_customize">تخصيص موقعي</string>
+ <string name="plans_post_purchase_text_customize">يمكنك الآن الوصول إلى خطوط مخصصة وألوان مخصصة وقدرات تحرير CSS مخصصة.</string>
+ <string name="plans_post_purchase_text_intro">يقوم موقعك ببعض الحركات البهلوانية من أجل الإثارة! الآن استكشف الميزات الجديدة في موقعك واختر المكان الذي تود البدء به.</string>
+ <string name="plans_post_purchase_title_customize">تخصيص الخطوط والألوان</string>
+ <string name="plans_post_purchase_title_intro">رهن إشارتك! أحسنت!</string>
+ <string name="export_your_content_message">سيتم إرسال مقالاتك وصفحاتك وإعداداتك إليك عبر البريد الإلكتروني على %s.</string>
+ <string name="plan">خطة</string>
+ <string name="plans">الخطط</string>
+ <string name="plans_loading_error">تعذر تحميل الخطط</string>
+ <string name="export_your_content">تصدير المحتوى الخاص بك</string>
+ <string name="exporting_content_progress">جارٍ استكشاف المحتوى…</string>
+ <string name="export_email_sent">تم إرسال البريد الإلكتروني للتصدير!</string>
+ <string name="premium_upgrades_message">لديك ترقيات متميزة نشطة على موقعك. يرجى إلغاء ترقياتك قبل حذف موقعك.</string>
+ <string name="show_purchases">إظهار عمليات الشراء</string>
+ <string name="checking_purchases">التحقق من عمليات الشراء</string>
+ <string name="premium_upgrades_title">ترقيات متميزة</string>
+ <string name="purchases_request_error">حدث خطأ. تعذر طلب عمليات الشراء.</string>
+ <string name="delete_site_progress">جارٍ حذف الموقع…</string>
+ <string name="delete_site_summary">يتعذر التراجع عن هذا الإجراء. سيؤدي حذف موقعك إلى إزالة جميع المحتويات والمساهمين والنطاقات من الموقع.</string>
+ <string name="delete_site_hint">حذف الموقع</string>
+ <string name="export_site_hint">تصدير موقعك إلى ملف XML</string>
+ <string name="are_you_sure">هل أنت متأكد؟</string>
+ <string name="export_site_summary">إذا كنت متأكدًا، فاستغرق وقتًا، من فضلك، وقم بتصدير المحتوى الآن. سيتعذر استرداده في المستقبل.</string>
+ <string name="keep_your_content">الاحتفاظ بالمحتويات</string>
+ <string name="domain_removal_hint">النطاقات التي لن تعمل بمجرد إزالة موقعك</string>
+ <string name="domain_removal_summary">انتبه! سيؤدي حذف موقعك أيضًا إلى إزالة النطاق (النطاقات) المدرج أدناه.</string>
+ <string name="primary_domain">النطاق الرئيسي</string>
+ <string name="domain_removal">إزالة النطاق</string>
+ <string name="error_deleting_site_summary">حدث خطأ أثناء حذف موقعك. يرجى الاتصال بالدعم للحصول على مزيد من المساعدة</string>
+ <string name="error_deleting_site">حدث خطأ أثناء حذف الموقع</string>
+ <string name="confirm_delete_site_prompt">يرجى كتابة %1$s في الحقل أدناه للتأكيد. لن يبقى لموقعك بعد ذلك أي أثر.</string>
+ <string name="site_settings_export_content_title">تصدير المحتوى</string>
+ <string name="contact_support">اتصل بالدعم</string>
+ <string name="confirm_delete_site">تأكيد حذف الموقع</string>
+ <string name="start_over_text">إذا كنت تريد موقعًا ولا تريد أيًّا من المقالات أو الصفحات التي لديك الآن، فيمكن لفريق الدعم لدينا حذف المقالات والصفحات والوسائط والتعليقات الخاصة بك.\n\nسيبقي ذلك على موقعك وعنوان URL في حالة نشطة، ولكنه يعطيك بداية جديدة عند إنشاء المحتوى الخاص بك. اتصل بنا فقط لحذف المحتوى الحالي الخاص بك.</string>
+ <string name="site_settings_start_over_hint">ابدأ موقعك من جديد</string>
+ <string name="let_us_help">دعنا نساعدك</string>
+ <string name="me_btn_app_settings">إعدادات التطبيق</string>
+ <string name="start_over">البدء من جديد</string>
+ <string name="editor_remove_failed_uploads">إزالة عمليات الرفع الفاشلة</string>
+ <string name="editor_toast_failed_uploads">فشل بعض عمليات رفع الوسائط. لا يمكنك الحفظ أو النشر\n مقالتك بهذه الحالة. هل تريد إزالة كافة الوسائط الفاشلة؟</string>
+ <string name="comments_empty_list_filtered_trashed">لا توجد تعليقات بسلة المهملات</string>
+ <string name="site_settings_advanced_header">متقدم</string>
+ <string name="comments_empty_list_filtered_pending">لا توجد تعليقات في قائمة الانتظار</string>
+ <string name="comments_empty_list_filtered_approved">لا توجد تعليقات تمَّت الموافقة عليها</string>
+ <string name="button_done">تم</string>
+ <string name="button_skip">تخطي</string>
+ <string name="site_timeout_error">تعذر الاتصال بموقع وردبرس لوجود خطأ في المهلة.</string>
+ <string name="xmlrpc_malformed_response_error">تعذر الاتصال. أنتجت عملية تثبيت وردبرس مستند XML-RPC غير صالح.</string>
+ <string name="xmlrpc_missing_method_error">تعذر الاتصال. طرق XML-RPC غير موجودة على الخادم.</string>
+ <string name="post_format_status">الحالة</string>
+ <string name="post_format_video">الفيديو</string>
+ <string name="theme_free">مجانًا</string>
+ <string name="theme_all">الكل</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">محادثة</string>
+ <string name="post_format_gallery">معرض</string>
+ <string name="post_format_image">صورة</string>
+ <string name="post_format_link">الرابط</string>
+ <string name="post_format_quote">اقتباس</string>
+ <string name="post_format_standard">قياسي</string>
+ <string name="notif_events">معلومات حول دورات وردبرس.كوم ومناسباته (على الإنترنت وشخصيًا).</string>
+ <string name="post_format_aside">ملاحظة</string>
+ <string name="post_format_audio">الصوت</string>
+ <string name="notif_surveys">فرص المشاركة في أبحاث وردبرس.كوم واستطلاعاته.</string>
+ <string name="notif_tips">نصائح للحصول على أكبر استفادة من وردبرس.كوم.</string>
+ <string name="notif_community">المجتمع</string>
+ <string name="replies_to_my_comments">الردود على تعليقاتي</string>
+ <string name="notif_suggestions">اقتراحات</string>
+ <string name="notif_research">الأبحاث</string>
+ <string name="site_achievements">إنجازات الموقع</string>
+ <string name="username_mentions">الإشارات إلى اسم المستخدم</string>
+ <string name="likes_on_my_posts">الإعجابات بمقالاتي</string>
+ <string name="site_follows">متابعات للموقع</string>
+ <string name="likes_on_my_comments">الإعجابات بتعليقاتي</string>
+ <string name="comments_on_my_site">التعليقات على موقعي</string>
+ <string name="site_settings_list_editor_summary_other">%d من العناصر</string>
+ <string name="site_settings_list_editor_summary_one">عنصر واحد</string>
+ <string name="approve_auto_if_previously_approved">تعليقات المستخدمين المعروفين</string>
+ <string name="approve_auto">كل المستخدمين</string>
+ <string name="approve_manual">لا توجد تعليقات</string>
+ <string name="site_settings_paging_summary_other">%d من التعليقات في الصفحة</string>
+ <string name="site_settings_paging_summary_one">تعليق واحد في الصفحة</string>
+ <string name="site_settings_multiple_links_summary_other">تلزم الموافقة على أكثر من %d من الروابط</string>
+ <string name="site_settings_multiple_links_summary_one">تلزم الموافقة على أكثر من رابط واحد</string>
+ <string name="site_settings_multiple_links_summary_zero">تلزم الموافقة على أكثر من 0 من الروابط</string>
+ <string name="detail_approve_auto">موافقة تلقائية على تعليقات الجميع.</string>
+ <string name="detail_approve_auto_if_previously_approved">موافقة تلقائية إذا كان لدى المستخدم تعليق تمت الموافقة عليه مسبقًا</string>
+ <string name="detail_approve_manual">تلزم موافقة يدوية على تعليقات الجميع.</string>
+ <string name="filter_trashed_posts">تم وضعها في سلة المهملات</string>
+ <string name="days_quantity_one">يوم واحد</string>
+ <string name="days_quantity_other">%d من الأيام</string>
+ <string name="filter_published_posts">تم النشر</string>
+ <string name="filter_draft_posts">المسودات</string>
+ <string name="filter_scheduled_posts">تمت الجدولة</string>
+ <string name="pending_email_change_snackbar">انقر فوق رابط التحقق في البريد الإلكتروني المرسل إلى %1$s للتأكيد على عنوانك الجديد</string>
+ <string name="primary_site">موقع أساسي</string>
+ <string name="web_address">عنوان الويب</string>
+ <string name="editor_toast_uploading_please_wait">أنت تقوم حاليًا برفع وسائط. يُرجى الانتظار حتى تكتمل هذه العملية.</string>
+ <string name="error_refresh_comments_showing_older">يتعذر تحديث التعليقات في الوقت الحالي - إظهار تعليقات أقدم</string>
+ <string name="editor_post_settings_set_featured_image">تعيين الصورة المميزة</string>
+ <string name="editor_post_settings_featured_image">الصورة المميزة</string>
+ <string name="new_editor_promo_desc">يتضمن الآن تطبيق وردبرس.كوم الخاص بنظام Android رؤية جديدة رائعة\n المحرر. جرِّبه من خلال إنشاء مقالة جديدة.</string>
+ <string name="new_editor_promo_title">محرر جديد تمامًا</string>
+ <string name="new_editor_promo_button_label">رائع، شكرًا!</string>
+ <string name="visual_editor_enabled">تم تمكين محرر مرئي</string>
+ <string name="editor_content_placeholder">شارك قصتك هنا…</string>
+ <string name="editor_page_title_placeholder">عنوان الصفحة</string>
+ <string name="editor_post_title_placeholder">عنوان المقالة</string>
+ <string name="email_address">عنوان البريد الإلكتروني</string>
+ <string name="preference_show_visual_editor">إظهار محرر مرئي</string>
+ <string name="dlg_sure_to_delete_comments">هل ترغب في إزالة هذه التعليقات نهائيًا؟</string>
+ <string name="preference_editor">المحرر</string>
+ <string name="dlg_sure_to_delete_comment">هل ترغب في إزالة هذا التعليق نهائيًا؟</string>
+ <string name="mnu_comment_delete_permanently">حذف</string>
+ <string name="comment_deleted_permanently">تم حذف تعليق</string>
+ <string name="mnu_comment_untrash">استعادة</string>
+ <string name="comments_empty_list_filtered_spam">لا توجد تعليقات مزعجة</string>
+ <string name="could_not_load_page">يتعذر تحميل الصفحة</string>
+ <string name="comment_status_all">الكل</string>
+ <string name="interface_language">لغة الواجهة</string>
+ <string name="off">إيقاف تشغيل</string>
+ <string name="about_the_app">حول التطبيق</string>
+ <string name="error_post_account_settings">تعذر حفظ إعدادات الحساب</string>
+ <string name="error_post_my_profile">تعذر حفظ الملف الشخصي</string>
+ <string name="error_fetch_account_settings">Deine Kontoeinstellungen konnten nicht abgerufen werden.</string>
+ <string name="error_fetch_my_profile">Dein Profil konnte nicht abgerufen werden.</string>
+ <string name="stats_widget_promo_ok_btn_label">حسنًا، فهمت</string>
+ <string name="stats_widget_promo_desc">Füge das Widget zu deinem Startbildschirm hinzu, um auf deine Statistiken mit einem Klick zugreifen zu können.</string>
+ <string name="stats_widget_promo_title">Statistik-Widget für den Startbildschirm</string>
+ <string name="site_settings_unknown_language_code_error">Sprachcode nicht erkannt</string>
+ <string name="site_settings_threading_dialog_description">Zulassen, dass Kommentare in Threads verschachtelt werden.</string>
+ <string name="site_settings_threading_dialog_header">Verschachteln bis zu</string>
+ <string name="remove">إزالة</string>
+ <string name="search">بحث</string>
+ <string name="add_category">إضافة فئة</string>
+ <string name="disabled">معطّلة</string>
+ <string name="site_settings_image_original_size">الحجم الأصلي</string>
+ <string name="privacy_private">سيكون موقعك مرئيًّا لك وللمستخدمين الذين توافق عليهم فقط</string>
+ <string name="privacy_public_not_indexed">سيكون موقعك مرئيًّا للجميع مع مطالبة محركات البحث بعدم فهرسته في الوقت نفسه</string>
+ <string name="privacy_public">سيكون موقعك مرئيًّا للجميع ويمكن فهرسته بواسطة محركات البحث</string>
+ <string name="about_me_hint">كلمات قليلة عنك...</string>
+ <string name="public_display_name_hint">سيكون اسم العرض افتراضيًا لاسم المستخدم الخاص بك إذا لم يتم تعيينه</string>
+ <string name="about_me">نبذة عني</string>
+ <string name="public_display_name">اسم العرض العام</string>
+ <string name="my_profile">ملفي الشخصي</string>
+ <string name="first_name">الاسم الأول</string>
+ <string name="last_name">اسم العائلة</string>
+ <string name="site_privacy_public_desc">السماح لمحركات البحث بفهرسة هذا الموقع</string>
+ <string name="site_privacy_hidden_desc">منع محركات البحث من فهرسة هذا الموقع</string>
+ <string name="site_privacy_private_desc">أود أن يكون موقعي خاصًا، مرئيًا فقط للمستخدمين الذين أختارهم</string>
+ <string name="cd_related_post_preview_image">صورة معاينة المقالة ذات الصلة</string>
+ <string name="error_post_remote_site_settings">تعذر حفظ معلومات الموقع</string>
+ <string name="error_fetch_remote_site_settings">تعذر استرداد معلومات الموقع</string>
+ <string name="error_media_upload_connection">حدث خطأ في الاتصال أثناء رفع ملفات الوسائط</string>
+ <string name="site_settings_disconnected_toast">غير متصل، التحرير مُعطّل.</string>
+ <string name="site_settings_unsupported_version_error">إصدار وردبرس غير مدعوم</string>
+ <string name="site_settings_multiple_links_dialog_description">يلزم موافقة على التعليقات التي تتضمن أكثر من هذا العدد من الروابط.</string>
+ <string name="site_settings_close_after_dialog_switch_text">إغلاق تلقائي</string>
+ <string name="site_settings_close_after_dialog_description">إغلاق تلقائي للتعليقات على المقالات.</string>
+ <string name="site_settings_paging_dialog_description">تحديد مناقشات التعليقات في صفحات متعددة.</string>
+ <string name="site_settings_paging_dialog_header">التعليقات لكل صفحة</string>
+ <string name="site_settings_close_after_dialog_title">إغلاق التعليق</string>
+ <string name="site_settings_blacklist_description">عندما يحتوي تعليق على أي من هذه الكلمات في محتواه أو اسمه أو عنوان URL أو عنوان البريد الإلكتروني أو بروتوكول IP الخاص به، سيتم وضع علامة عليه باعتباره تعليقًا مزعجًا. يمكنك إدخال كلمات غير مكتملة، إذ ستطابق كلمة "برس" كلمة "وردبرس".</string>
+ <string name="site_settings_hold_for_moderation_description">عندما يحتوي تعليق على أي من هذه الكلمات في محتواه أو اسمه أو عنوان URL أو عنوان البريد الإلكتروني أو بروتوكول IP الخاص به، سيتم حجزه في قائمة انتظار الإدارة. يمكنك إدخال كلمات غير مكتملة، إذ ستطابق كلمة "برس" كلمة "وردبرس".</string>
+ <string name="site_settings_list_editor_input_hint">أدخل كلمة أو عبارة</string>
+ <string name="site_settings_list_editor_no_items_text">لا توجد عناصر</string>
+ <string name="site_settings_learn_more_caption">يمكنك تجاوز هذه الإعدادات للمقالات الفردية.</string>
+ <string name="site_settings_rp_preview3_site">في "الترقية"</string>
+ <string name="site_settings_rp_preview3_title">نطاق الترقية: فيديوبرس لحفلات الزفاف</string>
+ <string name="site_settings_rp_preview2_site">في "التطبيقات"</string>
+ <string name="site_settings_rp_preview2_title">تجري حاليًا صيانة تطبيق وردبرس للأندرويد</string>
+ <string name="site_settings_rp_preview1_site">في "الجوال"</string>
+ <string name="site_settings_rp_preview1_title">تحديث iPhone/iPad كبير متوفر الآن</string>
+ <string name="site_settings_rp_show_images_title">عرض الصور</string>
+ <string name="site_settings_rp_show_header_title">عرض الترويسة</string>
+ <string name="site_settings_rp_switch_summary">تعرض المقالات ذات الصلة المحتوى ذا الصلة من موقعك أسفل مقالاتك.</string>
+ <string name="site_settings_rp_switch_title">عرض المقالات ذات الصلة</string>
+ <string name="site_settings_delete_site_hint">يُزيل بيانات موقعك من التطبيق</string>
+ <string name="site_settings_blacklist_hint">يتم وضع علامة على التعليقات التي تتطابق مع عامل التصفية باعتبارها بريدًا مزعجًا</string>
+ <string name="site_settings_moderation_hold_hint">يتم وضع التعليقات التي تتطابق مع عامل التصفية في قائمة انتظار الإدارة</string>
+ <string name="site_settings_multiple_links_hint">يتجاهل حد الرابط من المستخدمين المعروفين</string>
+ <string name="site_settings_whitelist_hint">يجب أن يملك صاحب التعليق تعليقات سابقة</string>
+ <string name="site_settings_user_account_required_hint">يجب على الزوار التسجيل ليتمكنوا من التعليق</string>
+ <string name="site_settings_identity_required_hint">يجب على كاتب التعليق كتابة الاسم والبريد الإلكتروني</string>
+ <string name="site_settings_manual_approval_hint">يجب الموافقة على التعليق يدويًا</string>
+ <string name="site_settings_paging_hint">عرض التعليقات في أجزاء من الحجم المحدد</string>
+ <string name="site_settings_threading_hint">السماح للتعليقات المتداخلة بالوصول إلى عمق معين</string>
+ <string name="site_settings_sort_by_hint">يحدد الترتيب الذي تُعرض به التعليقات</string>
+ <string name="site_settings_close_after_hint">إيقاف التعليقات بعد وقت محدد</string>
+ <string name="site_settings_receive_pingbacks_hint">السماح بتنبيهات الروابط من المدونات الأخرى</string>
+ <string name="site_settings_send_pingbacks_hint">محاولة التنبيه بوجود أي مدونات مرتبطة بالمقالة</string>
+ <string name="site_settings_allow_comments_hint">السماح للقرّاء بنشر التعليقات</string>
+ <string name="site_settings_discussion_hint">عرض إعدادات المناقشة على موقعك وتغييرها</string>
+ <string name="site_settings_more_hint">عرض جميع إعدادات المناقشة المتوفرة</string>
+ <string name="site_settings_related_posts_hint">إظهار المقالات ذات الصلة أو إخفاؤها في القارئ</string>
+ <string name="site_settings_upload_and_link_image_hint">التمكين لرفع صورة بالحجم الكامل دائمًا</string>
+ <string name="site_settings_image_width_hint">تغيير حجم الصور في المقالات إلى هذا العرض</string>
+ <string name="site_settings_format_hint">يُعيّن تنسيق مقالة جديدة</string>
+ <string name="site_settings_category_hint">تعيين فئة مقالة جديدة</string>
+ <string name="site_settings_location_hint">إضافة بيانات الموقع إلى مقالاتك تلقائيًا</string>
+ <string name="site_settings_password_hint">تغيير كلمة مرورك</string>
+ <string name="site_settings_username_hint">حساب المستخدم الحالي</string>
+ <string name="site_settings_language_hint">اللغة التي تُكتب بها هذه المدونة في الأصل.</string>
+ <string name="site_settings_privacy_hint">التحكم في من يمكنه رؤية موقعك</string>
+ <string name="site_settings_address_hint">تغيير عنوانك غير مدعوم حاليًا</string>
+ <string name="site_settings_tagline_hint">وصف موجز أو عبارة جذابة يراد بها وصف مدونتك</string>
+ <string name="site_settings_title_hint">بكلمات قليلة، اكتب نبذة موجزة عن هذا الموقع</string>
+ <string name="site_settings_whitelist_known_summary">تعليقات من مستخدمين معروفين</string>
+ <string name="site_settings_whitelist_all_summary">تعليقات من جميع المستخدمين</string>
+ <string name="site_settings_threading_summary">مستويات %d</string>
+ <string name="site_settings_privacy_private_summary">خاص</string>
+ <string name="site_settings_privacy_hidden_summary">مخفي</string>
+ <string name="site_settings_delete_site_title">حذف الموقع</string>
+ <string name="site_settings_privacy_public_summary">عام</string>
+ <string name="site_settings_blacklist_title">قائمة الحظر</string>
+ <string name="site_settings_moderation_hold_title">احتجاز من أجل الإدارة</string>
+ <string name="site_settings_multiple_links_title">الروابط في التعليقات</string>
+ <string name="site_settings_whitelist_title">الموافقة تلقائيًا</string>
+ <string name="site_settings_threading_title">مؤشر الترابط</string>
+ <string name="site_settings_paging_title">ترقيم الصفحات</string>
+ <string name="site_settings_sort_by_title">فرز حسب</string>
+ <string name="site_settings_account_required_title">يجب على المستخدمين تسجيل الدخول</string>
+ <string name="site_settings_identity_required_title">لا بد من تضمين الاسم والبريد الإلكتروني</string>
+ <string name="site_settings_receive_pingbacks_title">تلقي تنبيهات</string>
+ <string name="site_settings_send_pingbacks_title">إرسال تنبيهات</string>
+ <string name="site_settings_allow_comments_title">السماح بالتعليقات</string>
+ <string name="site_settings_default_format_title">التنسيق الافتراضي</string>
+ <string name="site_settings_default_category_title">الفئة الافتراضية</string>
+ <string name="site_settings_location_title">تمكين الموقع</string>
+ <string name="site_settings_address_title">العنوان</string>
+ <string name="site_settings_title_title">عنوان الموقع</string>
+ <string name="site_settings_tagline_title">الشعار</string>
+ <string name="site_settings_this_device_header">هذا الجهاز</string>
+ <string name="site_settings_discussion_new_posts_header">الإعدادات الافتراضية للمقالات الجديدة</string>
+ <string name="site_settings_account_header">الحساب</string>
+ <string name="site_settings_writing_header">كتابة</string>
+ <string name="newest_first">الأحدث أولاً</string>
+ <string name="site_settings_general_header">عام</string>
+ <string name="discussion">مناقشة</string>
+ <string name="privacy">الخصوصية</string>
+ <string name="related_posts">المقالات ذات الصلة</string>
+ <string name="comments">التعليقات</string>
+ <string name="close_after">إغلاق بعد</string>
+ <string name="oldest_first">الأقدم أولاً</string>
+ <string name="media_error_no_permission_upload">ليست لديك الصلاحية اللازمة لرفع ملفات الوسائط إلى هذا الموقع</string>
+ <string name="never">أبدًا</string>
+ <string name="unknown">غير معروف</string>
+ <string name="reader_err_get_post_not_found">لم تعد هذه المقالة موجودة</string>
+ <string name="reader_err_get_post_not_authorized">لست مخولاً لعرض هذه المقالة</string>
+ <string name="reader_err_get_post_generic">يتعذر استرجاع هذه المقالة</string>
+ <string name="blog_name_no_spaced_allowed">لا يمكن أن يحتوي عنوان الموقع على مسافات</string>
+ <string name="invalid_username_no_spaces">لا يمكن أن يحتوي اسم المستخدم على مسافات</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">لم تنشر المواقع التي تتابعها أي شيء مؤخرًا</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">لا توجد مقالات حديثة</string>
+ <string name="media_details_copy_url_toast">تم نسخ عنوان URL إلى الحافظة</string>
+ <string name="edit_media">تحرير الوسائط</string>
+ <string name="media_details_copy_url">نسخ عنوان URL</string>
+ <string name="media_details_label_date_uploaded">تم التحميل</string>
+ <string name="media_details_label_date_added">تمت الإضافة</string>
+ <string name="selected_theme">تحديد قالب</string>
+ <string name="could_not_load_theme">يتعذر تحميل القالب</string>
+ <string name="theme_activation_error">حدث خطأ. يتعذر تنشيط القالب</string>
+ <string name="theme_by_author_prompt_append"> حسب %1$s</string>
+ <string name="theme_prompt">شكرًا لك على اختيار %1$s</string>
+ <string name="theme_try_and_customize">محاولة وتخصيص</string>
+ <string name="theme_view">عرض</string>
+ <string name="theme_details">تفاصيل</string>
+ <string name="theme_support">الدعم</string>
+ <string name="theme_done">تم</string>
+ <string name="theme_manage_site">إدارة الموقع</string>
+ <string name="title_activity_theme_support">القوالب</string>
+ <string name="theme_activate">تفعيل</string>
+ <string name="date_range_start_date">تاريخ البدء</string>
+ <string name="date_range_end_date">تاريخ الانتهاء</string>
+ <string name="current_theme">القالب الحالي</string>
+ <string name="customize">تخصيص</string>
+ <string name="details">تفاصيل</string>
+ <string name="support">الدعم</string>
+ <string name="active">مفعل</string>
+ <string name="stats_referrers_spam_generic_error">حدث خطأ أثناء التشغيل. لم تتغير حالة البريد المزعج</string>
+ <string name="stats_referrers_marking_not_spam">تحديد كغير مزعج</string>
+ <string name="stats_referrers_unspam">ليس بريدًا مزعجًا</string>
+ <string name="stats_referrers_marking_spam">تحديد كمزعج</string>
+ <string name="theme_auth_error_authenticate">فشل في إحضار القوالب: فشلت مصادقة المستخدم</string>
+ <string name="post_published">تم نشر المقالة</string>
+ <string name="page_published">تم نشر الصفحة</string>
+ <string name="post_updated">تم تحديث المقالة</string>
+ <string name="page_updated">تم تحديث الصفحة</string>
+ <string name="stats_referrers_spam">البريد المزعج</string>
+ <string name="theme_no_search_result_found">عذرًا، لم يتم العثور على أي قوالب.</string>
+ <string name="media_file_name">اسم الملف: %s</string>
+ <string name="media_uploaded_on">تم الرفع في: %s</string>
+ <string name="media_dimensions">أبعاد: %s</string>
+ <string name="upload_queued">تم الوضع في قائمة الانتظار</string>
+ <string name="media_file_type">نوع الملف: %s</string>
+ <string name="reader_label_gap_marker">تحميل المزيد من المقالات</string>
+ <string name="notifications_no_search_results">لا توجد مواقع مطابقة "%s"</string>
+ <string name="search_sites">بحث في المواقع</string>
+ <string name="notifications_empty_view_reader">عرض القارئ</string>
+ <string name="unread">غير مقروء</string>
+ <string name="notifications_empty_action_followers_likes">ملاحظة: التعليق على المقالات التي قرأتها.</string>
+ <string name="notifications_empty_action_comments">الانضمام إلى محادثة: التعليق على المقالات من المدونات التي تتابعها.</string>
+ <string name="notifications_empty_action_unread">إعادة بدء المحادثة: اكتب مقالة جديدة.</string>
+ <string name="notifications_empty_action_all">كن نشطًا! علّق على مقالات من المدونات التي تتابعها.</string>
+ <string name="notifications_empty_likes">لا توجد إعجابات جديدة لإظهارها…حتى الآن.</string>
+ <string name="notifications_empty_followers">ليس هناك متابعون جدد للإبلاغ عنهم…حتى الآن.</string>
+ <string name="notifications_empty_comments">ليست هناك تعليقات جديدة…حتى الآن.</string>
+ <string name="notifications_empty_unread">تمت قراءة الكل!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">يرجى الوصول إلى الإحصاءات في التطبيق، وجرب إضافة المربع الجانبي لاحقًا</string>
+ <string name="stats_widget_error_readd_widget">يرجى إزالة المربع الجانبي وإضافته مرة أخرى</string>
+ <string name="stats_widget_error_no_visible_blog">يتعذر الوصول إلى الإحصاءات بدون مدونة مرئية</string>
+ <string name="stats_widget_error_no_permissions">يتعذر على حسابك في وردبرس.كوم الوصول إلى الإحصاءات في هذه المدونة</string>
+ <string name="stats_widget_error_no_account">يرجى تسجيل الدخول إلى وردبرس</string>
+ <string name="stats_widget_error_generic">يتعذر تحميل الإحصاءات</string>
+ <string name="stats_widget_loading_data">جارٍ تحميل البيانات…</string>
+ <string name="stats_widget_name_for_blog">إحصاءات اليوم لـ %1$s</string>
+ <string name="stats_widget_name">إحصاءات وردبرس اليوم</string>
+ <string name="add_location_permission_required">يلزم وجود صلاحية لإضافة موقع</string>
+ <string name="add_media_permission_required">يلزم وجود أذونات لإضافة الوسائط</string>
+ <string name="access_media_permission_required">يلزم وجود أذونات للوصول إلى الوسائط</string>
+ <string name="stats_enable_rest_api_in_jetpack">لعرض الإحصائيات الخاصة بك، قم بتمكين وحدة JSON API في Jetpack.</string>
+ <string name="error_open_list_from_notification">تم نشر هذا المنشور أو هذه الصفحة في موقع آخر</string>
+ <string name="reader_short_comment_count_multi">%s تعليقات</string>
+ <string name="reader_short_comment_count_one">تعليق واحد</string>
+ <string name="reader_label_submit_comment">إرسال</string>
+ <string name="reader_hint_comment_on_post">رد على المقالة...</string>
+ <string name="reader_discover_visit_blog">زيارة %s</string>
+ <string name="reader_discover_attribution_blog">تم النشر في الأصل على %s</string>
+ <string name="reader_discover_attribution_author">تم النشر في الأصل بواسطة %s</string>
+ <string name="reader_discover_attribution_author_and_blog">تم النشر في الأصل بواسطة %1$s في %2$s</string>
+ <string name="reader_short_like_count_multi">%s إعجابات</string>
+ <string name="reader_short_like_count_one">إعجاب واحد</string>
+ <string name="reader_label_follow_count">%,d متابعين</string>
+ <string name="reader_short_like_count_none">أعجبني</string>
+ <string name="reader_menu_tags">تحرير الوسوم والمدوّنات</string>
+ <string name="reader_title_post_detail">مقالة القارئ</string>
+ <string name="local_draft_explainer">هذه المقالة مسودة محلية لم تُنشَر بعد</string>
+ <string name="local_changes_explainer">تشتمل هذه المقالة على تغييرات محلية لم تُنشَر بعد</string>
+ <string name="notifications_push_summary">إعدادات للتنبيهات التي تظهر على جهازك.</string>
+ <string name="notifications_email_summary">إعدادات للتنبيهات التي يتم إرسالها إلى البريد الإلكتروني المرتبط بحسابك.</string>
+ <string name="notifications_tab_summary">إعدادات للتنبيهات التي تظهر في علامة تبويب "التنبيهات".</string>
+ <string name="notifications_disabled">تم تعطيل تنبيهات التطبيق. انقر هنا لتمكينها في "الإعدادات".</string>
+ <string name="notification_types">أنواع التنبيهات</string>
+ <string name="error_loading_notifications">تعذر تحميل إعدادات التنبيهات</string>
+ <string name="replies_to_your_comments">الردود على تعليقاتك</string>
+ <string name="comment_likes">الإعجابات بتعليقك</string>
+ <string name="app_notifications">تنبيهات التطبيق</string>
+ <string name="notifications_tab">علامة تبويب التنبيهات</string>
+ <string name="email">البريد الإلكتروني</string>
+ <string name="notifications_comments_other_blogs">التعليقات على المواقع الأخرى</string>
+ <string name="notifications_wpcom_updates">تحديثات وردبرس.كوم</string>
+ <string name="notifications_other">أخرى</string>
+ <string name="notifications_account_emails">بريد إلكتروني من WordPress.com</string>
+ <string name="notifications_account_emails_summary">نحن نرسل دومًا رسائل بريد إلكتروني مهمة بخصوص حسابك، ومع ذلك يمكنك الحصول على بعض الإضافات المفيدة أيضًا.</string>
+ <string name="notifications_sights_and_sounds">المناظر والأصوات</string>
+ <string name="your_sites">مواقعك</string>
+ <string name="stats_insights_latest_post_trend">لم يتم نشر %2$s منذ %1$s. فيما يلي بيان لمستوى أداء المقالة حتى الآن…</string>
+ <string name="stats_insights_latest_post_summary">ملخص آخر مقالة</string>
+ <string name="button_revert">إلغاء</string>
+ <string name="days_ago">منذ %d أيام ماضية</string>
+ <string name="yesterday">أمس</string>
+ <string name="connectionbar_no_connection">لا يوجد اتصال</string>
+ <string name="page_trashed">تم إرسال الصفحة إلى سلة المهملات</string>
+ <string name="post_deleted">تم حذف المقالة</string>
+ <string name="post_trashed">تم إرسال المقالة إلى سلة المهملات</string>
+ <string name="stats_no_activity_this_period">لا يوجد أي نشاط في هذه الفترة</string>
+ <string name="trashed">أُضيف إلى سلة المهملات</string>
+ <string name="button_back">رجوع</string>
+ <string name="page_deleted">تم حذف الصفحة</string>
+ <string name="button_stats">إحصاءات</string>
+ <string name="button_trash">سلة المهملات</string>
+ <string name="button_preview">معاينة</string>
+ <string name="button_view">عرض</string>
+ <string name="button_edit">تحرير</string>
+ <string name="button_publish">نشر</string>
+ <string name="my_site_no_sites_view_subtitle">هل ترغب في إضافة واحد؟</string>
+ <string name="my_site_no_sites_view_title">ليس لديك أي مواقع وردبرس حتى الآن.</string>
+ <string name="my_site_no_sites_view_drake">رسم توضيحي</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">أنت غير معتمد للوصول إلى هذه المدونة</string>
+ <string name="reader_toast_err_follow_blog_not_found">تعذر العثور على هذه المدونة</string>
+ <string name="undo">تراجع</string>
+ <string name="tabbar_accessibility_label_my_site">موقعي</string>
+ <string name="tabbar_accessibility_label_me">أنا</string>
+ <string name="passcodelock_prompt_message">أدخل مفتاح الـ PIN</string>
+ <string name="editor_toast_changes_saved">تم حفظ التغييرات</string>
+ <string name="push_auth_expired">انتهت فترة صلاحية الطلب. قم بتسجيل الدخول إلى وردبرس.كوم للمحاولة مرة أخرى.</string>
+ <string name="stats_insights_best_ever">المشاهدات الأفضل على الإطلاق</string>
+ <string name="ignore">تجاهل</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% من المشاهدات</string>
+ <string name="stats_insights_most_popular_hour">الساعة الأكثر شيوعًا</string>
+ <string name="stats_insights_most_popular_day">اليوم الأكثر شيوعًا</string>
+ <string name="stats_insights_popular">اليوم والساعة الأكثر شيوعًا</string>
+ <string name="stats_insights_today">إحصاءات اليوم</string>
+ <string name="stats_insights_all_time">مقالات ومشاهدات وزوار كل الأوقات</string>
+ <string name="stats_insights">الرؤى</string>
+ <string name="stats_sign_in_jetpack_different_com_account">لعرض إحصاءاتك، قم بتسجيل الدخول إلى حساب وردبرس.كوم الذي تستخدمه للاتصال بـ Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">هل تبحث عن إحصاءاتك الحديثة الأخرى؟ لقد نقلناها إلى صفحة الرؤى.</string>
+ <string name="me_disconnect_from_wordpress_com">قطع الاتصال بموقع وردبرس.كوم</string>
+ <string name="me_connect_to_wordpress_com">الاتصال بموقع وردبرس.كوم</string>
+ <string name="me_btn_login_logout">تسجيل الدخول/تسجيل الخروج</string>
+ <string name="account_settings">إعدادات الحساب</string>
+ <string name="me_btn_support">المساعدة والدعم</string>
+ <string name="site_picker_cant_hide_current_site">"%s" لم يكن مخفيًا لأنه الموقع الحالي</string>
+ <string name="site_picker_create_dotcom">إنشاء موقع وردبرس.كوم</string>
+ <string name="site_picker_add_site">إضافة موقع</string>
+ <string name="site_picker_add_self_hosted">إضافة موقع مستضاف ذاتيًا</string>
+ <string name="site_picker_edit_visibility">إظهار/إخفاء المواقع</string>
+ <string name="my_site_btn_view_admin">عرض المسؤول</string>
+ <string name="my_site_btn_view_site">عرض الموقع</string>
+ <string name="site_picker_title">إختر الموقع</string>
+ <string name="my_site_btn_switch_site">تبديل الموقع</string>
+ <string name="my_site_btn_blog_posts">مقالات المدونة</string>
+ <string name="my_site_btn_site_settings">الإعدادات</string>
+ <string name="my_site_header_look_and_feel">الشكل والطابع</string>
+ <string name="my_site_header_publish">نشر</string>
+ <string name="my_site_header_configuration">التكوين</string>
+ <string name="reader_label_new_posts_subtitle">انقر لعرضها</string>
+ <string name="notifications_account_required">تسجيل الدخول إلى موقع وردبرس.كوم للحصول على التنبيهات</string>
+ <string name="stats_unknown_author">كاتب غير معروف</string>
+ <string name="image_added">تمت إضافة صورة</string>
+ <string name="signout">قطع الاتصال</string>
+ <string name="deselect_all">إلغاء تحديد الكل</string>
+ <string name="show">إظهار</string>
+ <string name="hide">إخفاء</string>
+ <string name="select_all">اختيار الكل</string>
+ <string name="sign_out_wpcom_confirm">سيؤدي قطع اتصال حسابك إلى إزالة كل بيانات وردبرس.كوم الخاصة بـ %s@ من هذا الجهاز، بما في ذلك المسودات المحلية والتغييرات المحلية.</string>
+ <string name="select_from_new_picker">تحديد متعدد بأداة الانتقاء الجديدة</string>
+ <string name="stats_generic_error">تعذر تحميل الإحصاءات المطلوبة</string>
+ <string name="no_device_videos">لا توجد مقاطع فيديو</string>
+ <string name="no_blog_images">لا توجد صور</string>
+ <string name="no_blog_videos">لا توجد مقاطع فيديو</string>
+ <string name="no_device_images">لا توجد صور</string>
+ <string name="error_loading_blog_images">يتعذر إحضار الصور</string>
+ <string name="error_loading_blog_videos">يتعذر إحضار مقاطع الفيديو</string>
+ <string name="error_loading_images">حدث خطأ أثناء تحميل الصور</string>
+ <string name="error_loading_videos">حدث خطأ أثناء تحميل مقاطع الفيديو</string>
+ <string name="loading_blog_images">جارٍ إحضار الصور</string>
+ <string name="loading_blog_videos">جارٍ إحضار مقاطع الفيديو</string>
+ <string name="no_media_sources">تعذر إحضار الوسائط</string>
+ <string name="loading_videos">جارٍ تحميل مقاطع الفيديو</string>
+ <string name="loading_images">جارٍ تحميل الصور</string>
+ <string name="no_media">لا توجد وسائط</string>
+ <string name="device">جهاز</string>
+ <string name="language">اللغة</string>
+ <string name="add_to_post">إضافة مقالة</string>
+ <string name="media_picker_title">تحديد الوسائط</string>
+ <string name="take_photo">خذ صورة</string>
+ <string name="take_video">التقاط فيديو</string>
+ <string name="tab_title_device_images">صور الأجهزة</string>
+ <string name="tab_title_device_videos">مقاطع فيديو للأجهزة</string>
+ <string name="tab_title_site_images">صور المواقع</string>
+ <string name="tab_title_site_videos">مقاطع فيديو للمواقع</string>
+ <string name="media_details_label_file_name">اسم الملف</string>
+ <string name="media_details_label_file_type">نوع الملف</string>
+ <string name="error_publish_no_network">لا يمكن النشر في حالة عدم وجود اتصال. تم الحفظ كمسودة.</string>
+ <string name="editor_toast_invalid_path">مسار الملف غير صالح</string>
+ <string name="verification_code">رمز التحقق</string>
+ <string name="invalid_verification_code">رمز التحقق غير صالح</string>
+ <string name="verify">تحقق</string>
+ <string name="two_step_footer_label">أدخل الرمز من تطبيق الموثق.</string>
+ <string name="two_step_footer_button">إرسال الكود عن طريق رسالة نصية</string>
+ <string name="two_step_sms_sent">تحقق من رسائلك النصية للاطلاع على رمز التحقق.</string>
+ <string name="sign_in_jetpack">قم بتسجيل الدخول إلى حساب وردبرس.كوم للاتصال بـ Jetpack.</string>
+ <string name="auth_required">قم بتسجيل الدخول مرة أخرى للمتابعة.</string>
+ <string name="reader_empty_posts_request_failed">يتعذر استرداد المقالات</string>
+ <string name="publisher">الناشر:</string>
+ <string name="error_notification_open">تعذر فتح التنبيه</string>
+ <string name="stats_followers_total_email_paged">إظهار %1$d - %2$d من %3$s من متابعي البريد الإلكتروني</string>
+ <string name="stats_search_terms_unknown_search_terms">مصطلحات بحث غير معروفة</string>
+ <string name="stats_followers_total_wpcom_paged">إظهار %1$d - %2$d من %3$s من متابعي وردبرس.كوم</string>
+ <string name="stats_empty_search_terms_desc">اعرف المزيد حول حركة البحث الخاصة بك عن طريق الاطلاع على المصطلحات التي بحث عنها زوارك للعثور على موقعك.</string>
+ <string name="stats_empty_search_terms">لم تُسجل أي مصطلحات بحث</string>
+ <string name="stats_entry_search_terms">مصطلح البحث</string>
+ <string name="stats_view_authors">المدونون</string>
+ <string name="stats_view_search_terms">مصطلحات البحث</string>
+ <string name="comments_fetching">جارٍ إحضار التعليقات…</string>
+ <string name="pages_fetching">جارٍ إحضار الصفحات…</string>
+ <string name="toast_err_post_uploading">يتعذر فتح مقالة أثناء رفعها</string>
+ <string name="posts_fetching">جار إحضار المقالات…</string>
+ <string name="media_fetching">جار إحضار الوسائط…</string>
+ <string name="post_uploading">جارٍ الرفع</string>
+ <string name="stats_total">الإجمالي</string>
+ <string name="stats_overall">الكلي</string>
+ <string name="stats_period">الفترة</string>
+ <string name="logs_copied_to_clipboard">تم نسخ سجلات التطبيق إلى الحافظة</string>
+ <string name="reader_label_new_posts">مقالات جديدة</string>
+ <string name="reader_empty_posts_in_blog">هذه المدونة فارغة</string>
+ <string name="stats_average_per_day">المتوسط لكل يوم</string>
+ <string name="stats_recent_weeks">الأسابيع الأخيرة</string>
+ <string name="error_copy_to_clipboard">حدث خطأ أثناء نسخ النص إلى الحافظة</string>
+ <string name="reader_page_recommended_blogs">مواقع قد تحوز إعجابك</string>
+ <string name="stats_months_and_years">شهور وسنوات</string>
+ <string name="themes_fetching">جارٍ إحضار القوالب…</string>
+ <string name="stats_for">إحصاءات %s</string>
+ <string name="stats_other_recent_stats_label">الإحصاءات الحديثة الأخرى</string>
+ <string name="stats_view_all">عرض الكل</string>
+ <string name="stats_view">عرض</string>
+ <string name="stats_followers_months">%1$d من الشهور</string>
+ <string name="stats_followers_a_year">كل عام</string>
+ <string name="stats_followers_years">%1$d من السنوات</string>
+ <string name="stats_followers_a_month">كل شهر</string>
+ <string name="stats_followers_minutes">%1$d من الدقائق</string>
+ <string name="stats_followers_an_hour_ago">منذ ساعة مضت</string>
+ <string name="stats_followers_hours">%1$d من الساعات</string>
+ <string name="stats_followers_a_day">كل يوم</string>
+ <string name="stats_followers_days">%1$d من الأيام</string>
+ <string name="stats_followers_a_minute_ago">منذ دقيقة مضت</string>
+ <string name="stats_followers_seconds_ago">منذ ثوانٍ مضت</string>
+ <string name="stats_followers_total_email">إجمالي متابعي البريد الإلكتروني: %1$s</string>
+ <string name="stats_followers_wpcom_selector">ووردبريس.كوم</string>
+ <string name="stats_followers_email_selector">البريد الإلكتروني</string>
+ <string name="stats_followers_total_wpcom">إجمالي متابعي ووردبريس.كوم: %1$s</string>
+ <string name="stats_comments_total_comments_followers">إجمالي المقالات مع متابعي التعليقات: %1$s</string>
+ <string name="stats_comments_by_authors">حسب المؤلفين</string>
+ <string name="stats_comments_by_posts_and_pages">حسب المقالات والصفحات</string>
+ <string name="stats_empty_followers_desc">تتبع العدد الإجمالي للمتابعين والمدة التي قضاها كل منهم في متابعة موقعك.</string>
+ <string name="stats_empty_followers">لا يوجد متابعون</string>
+ <string name="stats_empty_publicize_desc">تتبع متابعيك من مختلف خدمات شبكات التواصل الاجتماعي باستخدام ميزة الإشهار.</string>
+ <string name="stats_empty_publicize">لم يتم تسجيل متابعي الإشهار</string>
+ <string name="stats_empty_video">لم يتم تشغيل أي مقاطع فيديو</string>
+ <string name="stats_empty_video_desc">إذا قمت برفع مقاطع فيديو باستخدام VideoPress، فتعرف على عدد المرات التي تمت مشاهدتها.</string>
+ <string name="stats_empty_comments_desc">إذا أتحت التعليقات على موقعك، فتتبع أبرز المعلقين واكتشف أي المحتويات يشعل الحيوية في المحادثات بناءً على أحدث 1000 تعليق.</string>
+ <string name="stats_empty_tags_and_categories_desc">ألقِ نظرة عامة على الموضوعات الأكثر انتشارًا على موقعك، كما هو موَّضح في مقالاتك الأعلى من الأسبوع الماضي.</string>
+ <string name="stats_empty_top_authors_desc">تتبع الآراء المطروحة بمقالات المشاركين ومن خلال التقريب استكشف أكثر المحتويات شعبية لكل مؤلف.</string>
+ <string name="stats_empty_tags_and_categories">لا توجد مقالات أو صفحات موسومة معروضة</string>
+ <string name="stats_empty_clicks_desc">عندما يتضمن المحتوى الخاص بك روابط لمواقع أخرى، سترى أيها يقوم زوَّارك بالنقر عليها أكثر.</string>
+ <string name="stats_empty_referrers_desc">تعلم المزيد حول رؤية موقعك بالبحث في مواقع الويب ومحركات البحث التي ترسل إليك أكبر عدد من حركات المرور</string>
+ <string name="stats_empty_clicks_title">لم يتم تسجيل نقرات</string>
+ <string name="stats_empty_referrers_title">لم يتم تسجيل توجيهات</string>
+ <string name="stats_empty_top_posts_title">لا توجد مقالات أو صفحات معروضة</string>
+ <string name="stats_empty_top_posts_desc">اكتشف المحتوى الخاص بك الأكثر مشاهدة وتحقق من مستوى أداء المقالات والصفحات الفردية بمرور الوقت.</string>
+ <string name="stats_totals_followers">منذ</string>
+ <string name="stats_empty_geoviews">لم يتم تسجيل أي دول</string>
+ <string name="stats_empty_geoviews_desc">استعرض القائمة لترى أي الدول والمناطق التي يتولد منها أكبر عدد من الزوَّار لموقعك.</string>
+ <string name="stats_entry_video_plays">فيديو</string>
+ <string name="stats_entry_top_commenter">المؤلف</string>
+ <string name="stats_entry_publicize">الخدمة</string>
+ <string name="stats_entry_followers">متابع</string>
+ <string name="stats_totals_publicize">متابعون</string>
+ <string name="stats_entry_clicks_link">رابط</string>
+ <string name="stats_view_top_posts_and_pages">المقالات والصفحات</string>
+ <string name="stats_view_videos">مقاطع الفيديو</string>
+ <string name="stats_view_publicize">إشهار</string>
+ <string name="stats_view_followers">متابعون</string>
+ <string name="stats_view_countries">البلاد</string>
+ <string name="stats_likes">إعجابات</string>
+ <string name="stats_pagination_label">الصفحة %1$s من %2$s</string>
+ <string name="stats_timeframe_years">سنوات</string>
+ <string name="stats_views">مشاهدات</string>
+ <string name="stats_visitors">الزوار</string>
+ <string name="ssl_certificate_details">تفاصيل</string>
+ <string name="delete_sure_post">حذف هذه المقالة</string>
+ <string name="delete_sure">حذف هذه المسودة</string>
+ <string name="delete_sure_page">حذف هذه الصفحة</string>
+ <string name="confirm_delete_multi_media">هل تريد حذف العناصر المحددة؟</string>
+ <string name="confirm_delete_media">هل تريد حذف العنصر المحدد؟</string>
+ <string name="cab_selected">تم تحديد %d</string>
+ <string name="media_gallery_date_range">عرض الوسائط من %1$s إلى %2$s</string>
+ <string name="sure_to_remove_account">هل ترغب في إزالة هذا الموقع؟</string>
+ <string name="reader_empty_followed_blogs_title">لم تتابع أي مواقع حتى الآن</string>
+ <string name="reader_empty_posts_liked">لم تُعجب بأي مقالات</string>
+ <string name="faq_button">الأسئلة المتكررّة</string>
+ <string name="browse_our_faq_button">استعراض الأسئلة المتداولة</string>
+ <string name="nux_help_description">تفضل بزيارة مركز المساعدة للاطلاع على إجابات الأسئلة الشائعة أو تفضل بزيارة المنتديات لطرح أسئلة جديدة</string>
+ <string name="agree_terms_of_service">توافق - من خلال إنشاء حساب جديد - على تسهيل %1$sشروط الخدمة%2$s</string>
+ <string name="create_new_blog_wpcom">إنشاء مدونة WordPress.com</string>
+ <string name="new_blog_wpcom_created">تم إنشاء مدونة WordPress.com!</string>
+ <string name="reader_empty_comments">لا توجد تعليقات حتى الآن</string>
+ <string name="reader_empty_posts_in_tag">لا توجد مقالات تحمل هذا الوسم</string>
+ <string name="reader_label_comment_count_multi">%,d من التعليقات</string>
+ <string name="reader_label_view_original">عرض المقالة الأصلية</string>
+ <string name="reader_label_like">أعجبني</string>
+ <string name="reader_label_comment_count_single">تعليق واحد</string>
+ <string name="reader_label_comments_closed">التعليقات مغلقة</string>
+ <string name="reader_label_comments_on">تعليقات على</string>
+ <string name="reader_title_photo_viewer">%1$d من %2$d</string>
+ <string name="error_publish_empty_post">يتعذر نشر مقالة فارغة</string>
+ <string name="error_refresh_unauthorized_posts">ليس لديك الإذن اللازم لعرض المقالات أو تحريرها</string>
+ <string name="error_refresh_unauthorized_pages">ليس لديك الإذن اللازم لعرض الصفحات أو تحريرها</string>
+ <string name="error_refresh_unauthorized_comments">ليس لديك الإذن اللازم لعرض التعليقات أو تحريرها</string>
+ <string name="older_month">أقدم من شهر</string>
+ <string name="more">المزيد</string>
+ <string name="older_two_days">أقدم من يومين</string>
+ <string name="older_last_week">أقدم من أسبوع</string>
+ <string name="stats_no_blog">يتعذر تحميل الإحصاءات للمدونة المطلوبة</string>
+ <string name="select_a_blog">تحديد موقع WordPress</string>
+ <string name="sending_content">تحميل محتوى %s</string>
+ <string name="uploading_total">تحميل %1$d من %2$d</string>
+ <string name="mnu_comment_liked">أعجبني</string>
+ <string name="comment">تعليق</string>
+ <string name="comment_trashed">تم وضع التعليق بسلة المهملات</string>
+ <string name="posts_empty_list">لا توجد مقالات حتى الآن. لماذا لا تبادر بإنشاء واحدة؟</string>
+ <string name="comment_reply_to_user">رد على %s</string>
+ <string name="pages_empty_list">لا توجد صفحات حتى الآن. لماذا لا تبادر بإنشاء واحدة؟</string>
+ <string name="media_empty_list_custom_date">لا توجد وسائط في هذا الفاصل الزمني</string>
+ <string name="posting_post">نشر "%s"</string>
+ <string name="signing_out">جارٍ تسجيل الخروج…</string>
+ <string name="reader_toast_err_generic">غير قادر على تنفيذ هذا الإجراء</string>
+ <string name="reader_toast_err_block_blog">غير قادر على حظر المدونة</string>
+ <string name="reader_toast_blog_blocked">لن يتم عرض مقالات هذه المدونة بعد الآن</string>
+ <string name="reader_menu_block_blog">حظر هذه المدونة</string>
+ <string name="contact_us">اتصل بنا</string>
+ <string name="hs__conversation_detail_error">أوصف المشكلة التي تراها</string>
+ <string name="hs__new_conversation_header">محادثة الدعم الفني</string>
+ <string name="hs__conversation_header">محادثة الدعم الفني</string>
+ <string name="hs__username_blank_error">أدخل اسمًا صالحًا</string>
+ <string name="hs__invalid_email_error">أدخل عنوان بريد إلكتروني صالحًا</string>
+ <string name="add_location">أضف مكان</string>
+ <string name="current_location">المكان الحالي</string>
+ <string name="search_location">بحث</string>
+ <string name="edit_location">تحرير</string>
+ <string name="search_current_location">حدد</string>
+ <string name="preference_send_usage_stats">إرسال الإحصائيات</string>
+ <string name="preference_send_usage_stats_summary">إرسال إحصائيات الاستخدام تلقائيًا لمساعدتنا في تطوير WordPress للإندرويد</string>
+ <string name="update_verb">تحديث</string>
+ <string name="schedule_verb">جدولة</string>
+ <string name="reader_title_blog_preview">مدوّنة القارئ</string>
+ <string name="reader_title_tag_preview">وسم القارئ</string>
+ <string name="reader_title_subs">الوسوم والمدونات</string>
+ <string name="reader_page_followed_tags">وسوم تمت متابعتها</string>
+ <string name="reader_page_followed_blogs">المواقع المتابَعة</string>
+ <string name="reader_hint_add_tag_or_url">أدخل عنوان URL أو اضغط للمتابعة</string>
+ <string name="reader_label_followed_blog">مدونة تمت متابعتها</string>
+ <string name="reader_label_tag_preview">مقالات موسومة %s</string>
+ <string name="reader_toast_err_get_blog_info">غير قادر على إظهار هذه المدونة</string>
+ <string name="reader_toast_err_already_follow_blog">أنت متابع لهذه المدونة بالفعل</string>
+ <string name="reader_toast_err_follow_blog">غير قادر على متابعة هذه المدونة </string>
+ <string name="reader_toast_err_unfollow_blog">غير قادر على عدم متابعة هذه المدونة </string>
+ <string name="reader_empty_recommended_blogs">لا يوجد مدونات موصى بها</string>
+ <string name="saving">حفظ...</string>
+ <string name="media_empty_list">لا وسائط</string>
+ <string name="ptr_tip_message">نصيحة: اسحب للأسفل من أجل التحديث</string>
+ <string name="help">المساعدة</string>
+ <string name="forgot_password">نسيت كلمة المرور؟</string>
+ <string name="forums">المنتديات</string>
+ <string name="help_center">مركز المساعدة</string>
+ <string name="ssl_certificate_error">شهادة SSL غير صالحة</string>
+ <string name="ssl_certificate_ask_trust">إذا كنت عادة تتصل بهذا الموقع بدون أي مشاكل، فربما يعني هذا الخطأ أن شخصًا ما يحاول سرقة هذا الموقع, وينبغي ألا تقوم بالاستمرار. هل ترغب في الوثوق بالشهادة على أي حال؟</string>
+ <string name="out_of_memory">الجهاز ممتليء الذاكرة</string>
+ <string name="no_network_message">لاتوجد شبكة متاحة</string>
+ <string name="could_not_remove_account">تعذر إزالة الموقع</string>
+ <string name="gallery_error">لا يمكن استعادة عنصر الوسائط المتعددة</string>
+ <string name="blog_not_found">حدث خطأ أثناء دخول هذه المدونة </string>
+ <string name="wait_until_upload_completes">انتظر حتى يتم الرفع</string>
+ <string name="theme_fetch_failed">فشل في إحضار القوالب</string>
+ <string name="theme_set_failed">فشل في تعيين القالب</string>
+ <string name="theme_auth_error_message">تاكد من أن لديك صلاحيات لتعيين القوالب</string>
+ <string name="comments_empty_list">لا تعليقات</string>
+ <string name="mnu_comment_unspam">ليس بريدًا مزعجًا</string>
+ <string name="no_site_error">غير قادر على الاتصال بموقع الووردبريس</string>
+ <string name="adding_cat_failed">إضافة التصنيف فشلت</string>
+ <string name="adding_cat_success">تم إضافة التصنيف بنجاح</string>
+ <string name="cat_name_required">حقل اسم التصنيف مطلوب</string>
+ <string name="category_automatically_renamed">اسم التصنيف %1$s ليس صالحًا . تم اعادة تسميته لـ %2$s.</string>
+ <string name="no_account">لم يتم العثور على حساب ووردبريس، أضف حسابًا وحاول من جديد</string>
+ <string name="sdcard_message">بطاقة ذاكرة SD مطلوبة لرفع ملفات الوسائط</string>
+ <string name="stats_empty_comments">لا تعليقات حتى الآن</string>
+ <string name="stats_bar_graph_empty">الإحصائيات غير متوفرة</string>
+ <string name="invalid_url_message">تحقق من أن عنوان URL الذي تم إدخاله صحيح</string>
+ <string name="reply_failed">فشل الرد</string>
+ <string name="notifications_empty_list">لا تنبيهات</string>
+ <string name="error_delete_post">حدث خطأ أثناء حذف الـ %s</string>
+ <string name="error_refresh_posts">المقالات لايمكن تحديثها في الوقت الحاضر</string>
+ <string name="error_refresh_pages">الصفحات لايمكن تحديثها في الوقت الحاضر</string>
+ <string name="error_refresh_notifications">التنبيهات لايمكن تحديثها في الوقت الحاضر</string>
+ <string name="error_refresh_comments">لا يمكن تحديث التعليقات في الوقت الحاضر</string>
+ <string name="error_refresh_stats">الإحصائيات لا يمكن تحديثها في هذا الوقت</string>
+ <string name="error_generic">حدث خطأ</string>
+ <string name="error_moderate_comment">حدث خطأ أثناء الإدارة</string>
+ <string name="error_edit_comment">حدث خطأ أثناء تعديل التعليق </string>
+ <string name="error_upload">حدث خطأ أثناء رفع ال %s</string>
+ <string name="error_load_comment">لايمكن تحميل التعليق</string>
+ <string name="error_downloading_image">خطأ في تحميل الصورة</string>
+ <string name="passcode_wrong_passcode">مفتاح الـ PIN خاطئ</string>
+ <string name="invalid_email_message">عنوان بريدك الإلكتروني غير صالح</string>
+ <string name="invalid_password_message">كلمة المرور يجب أن تحتوي على 4 أحرف على الأقل</string>
+ <string name="invalid_username_too_short">اسم المستخدم يجب أن يكون أطول من 4 أحرف</string>
+ <string name="invalid_username_too_long">اسم المستخدم يجب أن يكون أقل من 61 حرفًا</string>
+ <string name="username_only_lowercase_letters_and_numbers">اسم المستخدم يمكن أن يحتوي على أحرف صغيرة فقط ( a - z ) وأرقام</string>
+ <string name="username_required">أدخل اسم المستخدم</string>
+ <string name="username_not_allowed">اسم المستخدم غير مسموح به</string>
+ <string name="username_must_be_at_least_four_characters">يجب ألا يقل اسم المستخدم عن 4 أحرف</string>
+ <string name="username_contains_invalid_characters">لا يمكن أن يحتوي اسم المستخدم على الحرف "_"</string>
+ <string name="username_must_include_letters">يجب أن يحتوي اسم المستخدم على الأقل على 1 حرف (a إلى z)</string>
+ <string name="email_invalid">أدخل عنوان بريد إلكتروني صالح</string>
+ <string name="email_not_allowed">عنوان البريد الإلكتروني هذا غير مسموح به</string>
+ <string name="username_exists">اسم المستخدم هذا موجود بالفعل</string>
+ <string name="email_exists">عنوان البريد الإلكتروني قيد الاستخدام بالفعل</string>
+ <string name="username_reserved_but_may_be_available">اسم المستخدم محجوز حاليًا ولكن قد يكون متاحًا في بضعة أيام</string>
+ <string name="blog_name_required">أدخل عنوان موقعك</string>
+ <string name="blog_name_not_allowed">عنوان الموقع هذا غير مسموح به</string>
+ <string name="blog_name_must_be_at_least_four_characters">يجب أن يكون عنوان الموقع 4 أحرف على الأقل</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">يجب أن لا يكون عنوان الموقع أكثر من 64 حرفًا</string>
+ <string name="blog_name_contains_invalid_characters">عنوان الموقع ربما لا يحتوي على حرف "_"</string>
+ <string name="blog_name_cant_be_used">ربما لا تستخدم عنوان هذا الموقع</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">عنوان الموقع يمكنه أن يحتوي فقط على حروف صغيرة (a-z) وأرقام</string>
+ <string name="blog_name_exists">هذا الموقع موجود مسبقًا</string>
+ <string name="blog_name_reserved">هذا الموقع محجوز</string>
+ <string name="blog_name_reserved_but_may_be_available">هذا الموقع محجوز حاليًا وممكن أن يكون متوفرًا خلال بضعة أيام</string>
+ <string name="username_or_password_incorrect">اسم المستخدم أو كلمة المرور غير صحيحة</string>
+ <string name="nux_cannot_log_in">نحن لا نستطيع تسجيل دخولك</string>
+ <string name="xmlrpc_error">تعذر الاتصال. أدخل المسار الكامل لملف xmlrpc.php الموجود على موقعك ثم أعد المحاولة</string>
+ <string name="select_categories">تحديد التصنيفات</string>
+ <string name="account_details">تفاصيل الحساب</string>
+ <string name="edit_post">تحرير المقال</string>
+ <string name="add_comment">أضف تعليق</string>
+ <string name="connection_error">خطأ في الاتصال</string>
+ <string name="cancel_edit">الغاء التحرير</string>
+ <string name="scaled_image_error">أدخل قيمة عرض تحجيم صالحة</string>
+ <string name="post_not_found">حدث خطأ أثناء تحميل المقال . قم بتحديث مقالاتك ثم أعد المحاولة.</string>
+ <string name="learn_more">تعلم المزيد</string>
+ <string name="media_gallery_settings_title">اعدادات المعرض</string>
+ <string name="media_gallery_image_order">طلب صورة</string>
+ <string name="media_gallery_num_columns">عدد الاعمدة</string>
+ <string name="media_gallery_type_thumbnail_grid">الشبكة المصغرة</string>
+ <string name="media_gallery_edit">تعديل المعرض</string>
+ <string name="media_error_no_permission">ليس لديك تصريح لعرض مكتبة الوسائط</string>
+ <string name="cannot_delete_multi_media_items">بعض الوسائط لايمكن حذفها في الوقت الحاضر. حاول مرة أخرى فيما بعد.</string>
+ <string name="themes_live_preview">المعاينة المباشرة</string>
+ <string name="theme_current_theme">القالب الحالي</string>
+ <string name="theme_premium_theme">قالب مميز</string>
+ <string name="link_enter_url_text">نص الرابط (اختياري)</string>
+ <string name="create_a_link">إنشاء رابط</string>
+ <string name="page_settings">إعدادات الصفحة</string>
+ <string name="local_draft">مسودة محلية</string>
+ <string name="upload_failed">فشل الرفع</string>
+ <string name="horizontal_alignment">المحاذاة الأفقية</string>
+ <string name="file_not_found">لايمكن إيجاد ملف الوسائط للرفع. هل تم حذفه أو نقله؟</string>
+ <string name="post_settings">إعدادات المقالة</string>
+ <string name="delete_post">حذف المقال</string>
+ <string name="delete_page">حذف الصفحة</string>
+ <string name="comment_status_approved">تمت الموافقة</string>
+ <string name="comment_status_unapproved">معلق</string>
+ <string name="comment_status_spam">مزعج</string>
+ <string name="comment_status_trash">تم حذفه</string>
+ <string name="edit_comment">تعديل التعليق</string>
+ <string name="mnu_comment_approve">موافق</string>
+ <string name="mnu_comment_unapprove">غير موافق</string>
+ <string name="mnu_comment_spam">مزعج</string>
+ <string name="mnu_comment_trash">حذف</string>
+ <string name="dlg_approving_comments">تتم الآن الموافقة</string>
+ <string name="dlg_unapproving_comments">غير مصدق</string>
+ <string name="dlg_spamming_comments">تحديد كمزعج</string>
+ <string name="dlg_trashing_comments">إرسال إلى سلة المهملات</string>
+ <string name="dlg_confirm_trash_comments">إرسال إلى سلة المهملات؟</string>
+ <string name="trash_yes">سلة المهملات</string>
+ <string name="trash_no">عدم الوضع بسلة المهملات</string>
+ <string name="trash">سلة المهملات</string>
+ <string name="author_name">اسم المؤلف</string>
+ <string name="author_email">البريد الإلكتروني للمؤلف</string>
+ <string name="author_url">رابط المؤلف</string>
+ <string name="hint_comment_content">تعليق</string>
+ <string name="saving_changes">يتم حفظ التغييرات</string>
+ <string name="sure_to_cancel_edit_comment">إلغاء تعديل هذا التعليق؟</string>
+ <string name="content_required">التعليق مطلوب</string>
+ <string name="toast_comment_unedited">لم يتم تغيير التعليق</string>
+ <string name="remove_account">إزالة الموقع</string>
+ <string name="blog_removed_successfully">تمت إزالة الموقع بنجاح</string>
+ <string name="delete_draft">حذف المسودة</string>
+ <string name="preview_page">معاينة الصفحة</string>
+ <string name="preview_post">معاينة المقالة</string>
+ <string name="comment_added">تم إضافة التعليق بنجاح</string>
+ <string name="post_not_published">حالة المقالة لم تنشر </string>
+ <string name="page_not_published">حالة الصفحة لم تنشر</string>
+ <string name="view_in_browser">عرض في المستعرض</string>
+ <string name="add_new_category">إضافة تصنيف جديد</string>
+ <string name="category_name">اسم التصنيف</string>
+ <string name="category_slug">لقب التصنيف ( اختياري )</string>
+ <string name="category_desc">وصف التصنيف ( اختياري )</string>
+ <string name="category_parent">التصنيف الأصل ( اختياري )</string>
+ <string name="share_action_post">مقال جديد</string>
+ <string name="share_action_media">مكتبة الوسائط</string>
+ <string name="file_error_create">تعذر إنشاء ملف مؤقت لرفع الوسائط. تأكد من وجود مساحة حرة كافية على الجهاز.</string>
+ <string name="location_not_found">الموقع غير معروف</string>
+ <string name="open_source_licenses">رخص البرمجيات الحرة</string>
+ <string name="invalid_site_url_message">تحقق من أن عنوان URL الذي تم إدخاله في الموقع صحيح</string>
+ <string name="pending_review">بانتظار المراجعة</string>
+ <string name="http_credentials">تفويضات HTTP (إختياري)</string>
+ <string name="http_authorization_required">مطلوب ترخيص</string>
+ <string name="post_format">تنسيق المقالة</string>
+ <string name="notifications_empty_all">ليست هناك تنبيهات…حتى الآن.</string>
+ <string name="new_post">مقال جديد</string>
+ <string name="new_media">وسائط جديدة</string>
+ <string name="view_site">عرض الموقع</string>
+ <string name="privacy_policy">سياسة الخصوصية</string>
+ <string name="local_changes">التغيرات المحلية</string>
+ <string name="image_settings">إعدادات الصورة</string>
+ <string name="add_account_blog_url">عنوان المدونة</string>
+ <string name="wordpress_blog">مدونة ووردبريس</string>
+ <string name="error_blog_hidden">هذه المدونة مخفية و لايمكن تحميلها . قم بتفعيلها مرة اخرى من الإعدادات وأعد المحاولة.</string>
+ <string name="fatal_db_error">حدث خطأ اثناء إنشاء قاعدة بيانات التطبيق. حاول إعادة تنصيب التطبيق.</string>
+ <string name="jetpack_message_not_admin">اضافة Jetpack مطلوبة للإحصائيات. اتصل بمسؤول الموقع.</string>
+ <string name="reader_title_applog">سجل التطبيق</string>
+ <string name="reader_share_link">مشاركة رابط</string>
+ <string name="reader_toast_err_add_tag">غير قادر على إضافة هذا الوسم</string>
+ <string name="reader_toast_err_remove_tag">غير قادر على حذف هذا الوسم</string>
+ <string name="required_field">حقل مطلوب</string>
+ <string name="email_hint">البريد الإلكتروني</string>
+ <string name="site_address">عنوانك المستضاف ذاتيًا ( URL )</string>
+ <string name="email_cant_be_used_to_signup">لا يمكنك استخدام عنوان البريد الإلكتروني هذا للتسجيل. نحن لدينا مشاكل معهم تتمثل في حجبهم بعض عناوين البريد الإلكتروني لدينا. استخدم مزود بريد إلكتروني آخر.</string>
+ <string name="email_reserved">لقد تم بالفعل استخدام عنوان البريد الإلكتروني هذا. تحقق من صندوق البريد الوارد لبريد التنشيط. إذا لم تقم بالتنشيط يمكنك المحاولة مرة أخرى في غضون أيام قليلة.</string>
+ <string name="blog_name_must_include_letters">عنوان الموقع يجب أن يحتوي على الأقل على حرف واحد من ( a - z ) </string>
+ <string name="blog_name_invalid">عنوان موقع غير صالح </string>
+ <string name="blog_title_invalid">عنوان الموقع غير صالح</string>
+ <string name="deleting_page">يتم الان حذف الصفحة</string>
+ <string name="deleting_post">يتم الان حذف المقالة</string>
+ <string name="share_url_post">مشاركة المقالة</string>
+ <string name="share_url_page">مشاركة الصفحة</string>
+ <string name="share_link">مشاركة الرابط</string>
+ <string name="creating_your_account">يتم الآن إنشاء حسابك</string>
+ <string name="creating_your_site">يتم الآن إنشاء موقعك</string>
+ <string name="reader_empty_posts_in_tag_updating">يتم الآن جلب المقالات ...</string>
+ <string name="error_refresh_media">حدث شيء خطأ عندما كان يتم تحديث مكتبة الوسائط. أعد المحاولة في وقت لاحق.</string>
+ <string name="reader_likes_you_and_multi">أنت و %,d آخرون أحببتم هذا</string>
+ <string name="reader_likes_multi">%,d أحبوا هذا</string>
+ <string name="reader_toast_err_get_comment">غير قادر على استعادة هذا التعليق</string>
+ <string name="reader_label_reply">رد</string>
+ <string name="video">فيديو</string>
+ <string name="download">تحميل الوسائط</string>
+ <string name="comment_spammed">تم وضع علامة إزعاج على التعليق</string>
+ <string name="cant_share_no_visible_blog">لا تستطيع مشاركتها إلى ووردبريس بدون أن تجعل المدونة مرئية</string>
+ <string name="select_time">تحديد الوقت</string>
+ <string name="reader_likes_you_and_one">أنت وشخص آخر معجبان بهذا</string>
+ <string name="reader_empty_followed_blogs_description">ومع ذلك لا داعي للقلق، فما عليك سوى النقر على الأيقونة في أعلى اليمين لبدء الاستكشاف!</string>
+ <string name="select_date">تحديد التاريخ</string>
+ <string name="pick_photo">تحديد صورة</string>
+ <string name="account_two_step_auth_enabled">هذا الحساب تبقى عليه اثنتان من خطوات المصادقة. قم بزيارة إعدادات الأمان على ووردبريس.كوم وإنشاء كلمة مرور خاصة بالتطبيقات.</string>
+ <string name="pick_video">تحديد فيديو</string>
+ <string name="reader_toast_err_get_post">غير قادر على استرجاع هذه المقالة</string>
+ <string name="validating_user_data">التحقق من صحة بيانات المستخدم</string>
+ <string name="validating_site_data">التحقق من صحة بيانات الموقع</string>
+ <string name="password_invalid">تحتاج لكلمة مرور أكثر أمنًا. تأكدمن استخدام 7 رموز أو أكثر، اخلط الأحرف الكبيرة أو الأحرف الصغيرة أو الأرقام أو الرموز الخاصة</string>
+ <string name="nux_tap_continue">متابعة</string>
+ <string name="nux_welcome_create_account">انشئ حساب</string>
+ <string name="signing_in">جاري تسجيل الدخول...</string>
+ <string name="nux_add_selfhosted_blog">أضف موقعك المستضاف ذاتيًا</string>
+ <string name="nux_oops_not_selfhosted_blog">تسجيل الدخول إلى ووردبريس.كوم</string>
+ <string name="media_add_popup_title">اضف إلى مكتبة الوسائط</string>
+ <string name="media_add_new_media_gallery">انشاء معرض</string>
+ <string name="empty_list_default">هذه القائمة فارغة.</string>
+ <string name="select_from_media_library">اختر من مكتبة الوسائط</string>
+ <string name="jetpack_message">مطلوب اضافة الـ Jetpack للإحصائيات , هل تريد تثبيت الـ Jetpack ؟</string>
+ <string name="jetpack_not_found">اضافة Jetpack غير موجودة</string>
+ <string name="reader_untitled_post">( بدون اسم )</string>
+ <string name="reader_share_subject">تمت المشاركة من %s</string>
+ <string name="reader_btn_share">مشاركة</string>
+ <string name="reader_btn_follow">تابع</string>
+ <string name="reader_btn_unfollow">متابع</string>
+ <string name="reader_hint_comment_on_comment">رد على التعليق...</string>
+ <string name="reader_label_added_tag">أضيف %s</string>
+ <string name="reader_label_removed_tag">تم حذف %s</string>
+ <string name="reader_likes_one">شخص واحد اعجب بهذا</string>
+ <string name="reader_likes_only_you">انت معجب بهذا</string>
+ <string name="reader_toast_err_comment_failed">لايمكن نشر تعليقك</string>
+ <string name="reader_toast_err_tag_exists">أنت متابع لهذا الوسم بالفعل</string>
+ <string name="reader_toast_err_tag_invalid">هذا الوسم غير صالح</string>
+ <string name="reader_toast_err_share_intent">لايمكن المشاركة</string>
+ <string name="reader_toast_err_view_image">لايمكن عرض الصورة</string>
+ <string name="reader_toast_err_url_intent">لايمكن فتح %s</string>
+ <string name="reader_empty_followed_tags">لا تتابع أي وسوم</string>
+ <string name="create_account_wpcom">إنشاء حساب على ووردبريس.كوم</string>
+ <string name="button_next">التالي</string>
+ <string name="connecting_wpcom">الاتصال بـ WordPress.com</string>
+ <string name="username_invalid">اسم المستخدم غير صحيح</string>
+ <string name="limit_reached">وصلت للحد المسموح , يمكنك المحاولة مرة أخرى خلال 1 دقيقة. المحاولة مرة أخرى قبل ذلك ستؤدي لزيادة الوقت الواجب انتظاره قبل أن يتم رفع الحظر. إذا كنت تعتقد أن هذا هو الخطأ، اتصل بالدعم.</string>
+ <string name="nux_tutorial_get_started_title">ابدأ!</string>
+ <string name="themes">القوالب</string>
+ <string name="all">الكل</string>
+ <string name="images">الصور</string>
+ <string name="unattached">غير مرفق</string>
+ <string name="custom_date">تاريخ مخصص</string>
+ <string name="media_add_popup_capture_photo">التقط صورة</string>
+ <string name="media_add_popup_capture_video">ألتقط فيديو</string>
+ <string name="media_gallery_image_order_random">عشوائي</string>
+ <string name="media_gallery_image_order_reverse">عكس</string>
+ <string name="media_gallery_type">النوع</string>
+ <string name="media_gallery_type_squares">مربعات</string>
+ <string name="media_gallery_type_tiled">متجانب</string>
+ <string name="media_gallery_type_circles">دوائر</string>
+ <string name="media_gallery_type_slideshow">عارض الشرائح</string>
+ <string name="media_edit_title_text">العنوان</string>
+ <string name="media_edit_caption_text">كلمات توضيحية</string>
+ <string name="media_edit_description_text">الوصف</string>
+ <string name="media_edit_title_hint">أدخل العنوان هنا</string>
+ <string name="media_edit_caption_hint">أدخل الكلمات التوضيحية هنا</string>
+ <string name="media_edit_description_hint">أدخل الوصف هنا</string>
+ <string name="media_edit_success">تم التحديث</string>
+ <string name="media_edit_failure">فشل التحديث</string>
+ <string name="themes_details_label">التفاصيل</string>
+ <string name="themes_features_label">المميزات</string>
+ <string name="theme_activate_button">تفعيل</string>
+ <string name="theme_activating_button">جاري التفعيل</string>
+ <string name="theme_set_success">نجح في جعل الثيم افتراضي</string>
+ <string name="theme_auth_error_title">فشل في جلب القالب</string>
+ <string name="post_excerpt">المقتطف</string>
+ <string name="share_action_title">أضف إلى...</string>
+ <string name="share_action">المشاركة</string>
+ <string name="stats">الإحصائيات</string>
+ <string name="stats_view_visitors_and_views">الزوار و الزيارات</string>
+ <string name="stats_view_clicks">النقرات</string>
+ <string name="stats_view_tags_and_categories">الوسوم و التصنيفات</string>
+ <string name="stats_view_referrers">إحالة الدعوات</string>
+ <string name="stats_timeframe_today">اليوم</string>
+ <string name="stats_timeframe_yesterday">الأمس</string>
+ <string name="stats_timeframe_days">الأيام</string>
+ <string name="stats_timeframe_weeks">الأسابيع</string>
+ <string name="stats_timeframe_months">الشهور</string>
+ <string name="stats_entry_country">الدولة</string>
+ <string name="stats_entry_posts_and_pages">العنوان</string>
+ <string name="stats_entry_tags_and_categories">الموضوع</string>
+ <string name="stats_entry_authors">الكاتب</string>
+ <string name="stats_entry_referrers">الإحالات</string>
+ <string name="stats_totals_views">الزيارات</string>
+ <string name="stats_totals_clicks">النقرات</string>
+ <string name="stats_totals_plays">يشغل</string>
+ <string name="passcode_manage">ادارة قفل الـ PIN</string>
+ <string name="passcode_enter_passcode">أدخل مفتاح الـ PIN</string>
+ <string name="passcode_enter_old_passcode">أدخل مفتاح الـ PIN القديم</string>
+ <string name="passcode_re_enter_passcode">اعادة إدخال مفتاح الـ PIN</string>
+ <string name="passcode_change_passcode">تغيير مفتاح الـ PIN</string>
+ <string name="passcode_set">تعيين مفتاح الـ PIN</string>
+ <string name="passcode_preference_title">قفل الـ PIN</string>
+ <string name="passcode_turn_off">ايقاف قفل رمز PIN</string>
+ <string name="passcode_turn_on">تشغيل قفل رمز PIN</string>
+ <string name="upload">رفع</string>
+ <string name="discard">تجاهل</string>
+ <string name="sign_in">تسجيل الدخول</string>
+ <string name="notifications">الإشعارات</string>
+ <string name="note_reply_successful">تم نشر الرد</string>
+ <string name="follows">متابعات</string>
+ <string name="new_notifications">%d تنبيهات جديدة</string>
+ <string name="more_notifications">و %d اكثر</string>
+ <string name="loading">جاري التحميل...</string>
+ <string name="httpuser">اسم مستخدم HTTP</string>
+ <string name="httppassword">كلمة مرور HTTP</string>
+ <string name="error_media_upload">حدث خطأ أثناء رفع ملفات الوسائط</string>
+ <string name="post_content">المحتوى (انقر لإضافة نص ووسائط)</string>
+ <string name="publish_date">نشر</string>
+ <string name="content_description_add_media">أضف وسائط</string>
+ <string name="incorrect_credentials">اسم المستخدم و/أو كلمة المرور غير صحيحين.</string>
+ <string name="password">كلمة المرور</string>
+ <string name="username">إسم المستخدم</string>
+ <string name="reader">القارئ</string>
+ <string name="featured">استخدام كصورة بارزة</string>
+ <string name="featured_in_post">إدراج الصورة في محتوى المقالة</string>
+ <string name="no_network_title">لا توجد شبكة متاحة</string>
+ <string name="pages">صفحات</string>
+ <string name="caption">الكلمات التوضيحية (اختياري)</string>
+ <string name="width">عرض</string>
+ <string name="posts">المقالات</string>
+ <string name="anonymous">مجهول</string>
+ <string name="page">صفحة</string>
+ <string name="post">مقالة</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">موافق</string>
+ <string name="upload_scaled_image">الرفع والربط بالصورة المحجمة</string>
+ <string name="scaled_image">تحجيم عرض الصورة</string>
+ <string name="scheduled">مجدول</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">جاري الرفع...</string>
+ <string name="version">النسخة</string>
+ <string name="tos">بنود الخدمة</string>
+ <string name="app_title">ووردبريس للآندرويد</string>
+ <string name="max_thumbnail_px_width">عرض الصورة الافتراضي</string>
+ <string name="image_alignment">المحاذاة</string>
+ <string name="refresh">تجديد</string>
+ <string name="untitled">بدون عنوان</string>
+ <string name="edit">تحرير</string>
+ <string name="post_id">المقالة</string>
+ <string name="page_id">الصفحة</string>
+ <string name="post_password">كلمة المرور (اختياري)</string>
+ <string name="immediately">حالاً</string>
+ <string name="quickpress_add_alert_title">تعيين الاسم المختصر</string>
+ <string name="today">اليوم</string>
+ <string name="settings">الإعدادات</string>
+ <string name="share_url">رابط المشاركة</string>
+ <string name="quickpress_window_title">اختر مدونة لاختصار QuickPress</string>
+ <string name="quickpress_add_error">الاسم المختصر لايمكن أن يكون فارغًا</string>
+ <string name="publish_post">نشر</string>
+ <string name="draft">مسودة </string>
+ <string name="post_private">خاص</string>
+ <string name="upload_full_size_image">الرفع والربط بالصورة الكاملة</string>
+ <string name="title">العنوان</string>
+ <string name="tags_separate_with_commas">الوسوم (إفصل بينها بقاصلة)</string>
+ <string name="categories">التصنيفات</string>
+ <string name="dlg_deleting_comments">جارٍ حذف التعليقات</string>
+ <string name="notification_blink">وميض ضوء التنبيهات</string>
+ <string name="notification_sound">صوت التنبيهات</string>
+ <string name="notification_vibrate">الهزاز</string>
+ <string name="status">الحالة</string>
+ <string name="location">الموقع</string>
+ <string name="sdcard_title">بطاقة الذاكرة مطلوبة</string>
+ <string name="select_video">اختر فيديو من المعرض</string>
+ <string name="media">الوسائط</string>
+ <string name="delete">حذف</string>
+ <string name="none">بدون</string>
+ <string name="blogs">المدونات</string>
+ <string name="select_photo">اختر صورة من المعرض</string>
+ <string name="error">خطأ </string>
+ <string name="cancel">إلغاء</string>
+ <string name="save">حفظ </string>
+ <string name="add">أضف</string>
+ <string name="category_refresh_error">خطأ في تحديث القسم</string>
+ <string name="preview">معاينة</string>
+ <string name="on">تشغيل</string>
+ <string name="reply">الرد</string>
+ <string name="notification_settings">إعدادات التنبيهات</string>
+ <string name="yes">نعم</string>
+ <string name="no">لا</string>
+</resources>
diff --git a/WordPress/src/main/res/values-az/strings.xml b/WordPress/src/main/res/values-az/strings.xml
new file mode 100644
index 000000000..7893297df
--- /dev/null
+++ b/WordPress/src/main/res/values-az/strings.xml
@@ -0,0 +1,653 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="theme_support">Dəstək</string>
+ <string name="theme_view">Bax</string>
+ <string name="title_activity_theme_support">Mövzular</string>
+ <string name="active">Aktiv</string>
+ <string name="support">Dəstək</string>
+ <string name="stats_referrers_unspam">Spam deyil</string>
+ <string name="page_updated">Səhifə yeniləndi</string>
+ <string name="post_updated">Yazı yeniləndi</string>
+ <string name="page_published">Səhifə dərc edildi</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="post_published">Yazı dərc edildi</string>
+ <string name="reader_short_comment_count_multi">%s Şərh</string>
+ <string name="reader_short_comment_count_one">1 Şərh</string>
+ <string name="reader_short_like_count_multi">%s Bəyənmə</string>
+ <string name="email">E-poçt</string>
+ <string name="notifications_other">Digər</string>
+ <string name="days_ago">%d gün öncə</string>
+ <string name="connectionbar_no_connection">Qoşulma yoxdur</string>
+ <string name="page_trashed">Səhifəni zibil qutusuna göndər</string>
+ <string name="post_deleted">Yazı silindi</string>
+ <string name="post_trashed">Yazını zibil qutusuna göndər</string>
+ <string name="button_back">Geri</string>
+ <string name="page_deleted">Səhifə silindi</string>
+ <string name="button_stats">Statistikalar</string>
+ <string name="button_trash">Zibil qutusu</string>
+ <string name="button_preview">Önizləmə</string>
+ <string name="button_view">Bax</string>
+ <string name="button_edit">Redaktə et</string>
+ <string name="button_publish">Dərc et</string>
+ <string name="trashed">Zibil qutusuna göndərildi</string>
+ <string name="stats_no_activity_this_period">Bu dövrə aid aktivlik yoxdur</string>
+ <string name="my_site_no_sites_view_subtitle">Bir ədədini əlavə etmək istərdinizmi?</string>
+ <string name="my_site_no_sites_view_title">Hələlik heç bir WordPress saytınız yoxdur.</string>
+ <string name="my_site_no_sites_view_drake">İllüstrasiya</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Bu bloqa giriş izniniz yoxdur</string>
+ <string name="reader_toast_err_follow_blog_not_found">Bloq tapılmadı</string>
+ <string name="undo">Geri al</string>
+ <string name="tabbar_accessibility_label_my_site">Saytım</string>
+ <string name="tabbar_accessibility_label_me">Mən</string>
+ <string name="passcodelock_prompt_message">PIN kodunuzu daxil edin</string>
+ <string name="editor_toast_changes_saved">Dəyişikliklər qeyd edildi</string>
+ <string name="push_auth_expired">İstək müddəti doldu. WordPress.com saytında giriş edərək təkrar yoxlayın.</string>
+ <string name="stats_insights_best_ever">Bü Günə Qədərki Ən Yaxşı Baxış</string>
+ <string name="ignore">Yox say</string>
+ <string name="stats_insights_most_popular_percent_views">Baxışlarda %1$d%% </string>
+ <string name="stats_insights_most_popular_hour">Ən populyar saat</string>
+ <string name="stats_insights_most_popular_day">Ən populyar gün</string>
+ <string name="stats_insights_popular">Ən populyar gün və saat</string>
+ <string name="stats_insights_today">Bugünkü Statistikalar</string>
+ <string name="stats_insights_all_time">Bütün zamanlardakı yazılar, baxışlar və ziyarətçilər</string>
+ <string name="stats_insights">Tək Baxışda</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Statistikalarınıza baxmaq üçün, Jetpack-a qoşulmaq üçün istifadə etdiyiniz WordPress.com hesabına daxil olun.</string>
+ <string name="stats_other_recent_stats_moved_label">Digər Son Statistikaları axtarırsınızmı? Bu məlumatları Tək Baxışda səhifəsinə daşıdıq.</string>
+ <string name="me_disconnect_from_wordpress_com">WordPress.com Bağlantısını Kəs</string>
+ <string name="me_connect_to_wordpress_com">WordPress.com-a qoşul</string>
+ <string name="me_btn_login_logout">Giriş/Çıxış</string>
+ <string name="me_btn_support">Yardım və Dəstək</string>
+ <string name="site_picker_cant_hide_current_site">"%s" hazırkı sayt olduğundan gizlədilə bilmədi</string>
+ <string name="site_picker_create_dotcom">WordPress.com saytı yarat</string>
+ <string name="site_picker_add_site">Sayt əlavə et</string>
+ <string name="site_picker_add_self_hosted">İstifadəçinin öz hostundakı saytı əlavə et</string>
+ <string name="site_picker_edit_visibility">Saytları göstər/gizlət</string>
+ <string name="my_site_btn_view_admin">Nəzarətçiyə Bax</string>
+ <string name="my_site_btn_view_site">Sayta Bax</string>
+ <string name="site_picker_title">Sayt seç</string>
+ <string name="my_site_btn_switch_site">Saytı Dəyişdir</string>
+ <string name="my_site_btn_blog_posts">Bloq Yazıları</string>
+ <string name="my_site_btn_site_settings">Parametrlər</string>
+ <string name="my_site_header_look_and_feel">Əsas Görünüş</string>
+ <string name="my_site_header_publish">Dərc et</string>
+ <string name="my_site_header_configuration">Konfiqurasiya</string>
+ <string name="reader_label_new_posts_subtitle">Göstərmək üçün toxunun</string>
+ <string name="notifications_account_required">Bildirişlər üçün WordPress.com-a giriş et</string>
+ <string name="stats_unknown_author">Naməlum Müəllif</string>
+ <string name="image_added">Təsvir əlavə edildi</string>
+ <string name="signout">Bağlantını Kəs</string>
+ <string name="deselect_all">Heç birini seçmə</string>
+ <string name="show">Göstər</string>
+ <string name="hide">Gizlət</string>
+ <string name="select_all">Tamamını seç</string>
+ <string name="sign_out_wpcom_confirm">Hesabınızın bağlantısını kəsdiyinizdə yerli qaralama və dəyişikliklər daxil olmaq üzrə @%s istifadəçisinə aid bütün WordPress.com məlumatları bu cihazdan silinir.</string>
+ <string name="select_from_new_picker">Yeni seçici ilə birdən çox maddə seç</string>
+ <string name="stats_generic_error">Gərəkli statistikalar yüklənmir</string>
+ <string name="no_device_videos">Video yoxdur</string>
+ <string name="no_blog_images">Təsvir yoxdur</string>
+ <string name="no_blog_videos">Video yoxdur</string>
+ <string name="no_device_images">Təsvir yoxdur</string>
+ <string name="error_loading_blog_images">Təsvirlər alınmır</string>
+ <string name="error_loading_blog_videos">Videolar alınmır</string>
+ <string name="error_loading_images">Təsvirlər yüklənərkən bir xəta oldu</string>
+ <string name="error_loading_videos">Videolar yüklənərkən bir xəta oldu</string>
+ <string name="loading_blog_images">Təsvirlər alınır</string>
+ <string name="loading_blog_videos">Videolar alınır</string>
+ <string name="no_media_sources">Media faylı alınmadı</string>
+ <string name="loading_videos">Videolar yüklənir</string>
+ <string name="loading_images">Təsvirlər yüklənir</string>
+ <string name="no_media">Media yoxdur</string>
+ <string name="device">Cihaz</string>
+ <string name="language">Dil</string>
+ <string name="add_to_post">Yazıya əlavə et</string>
+ <string name="media_picker_title">Media seç</string>
+ <string name="take_photo">Foto çək</string>
+ <string name="take_video">Video çək</string>
+ <string name="tab_title_device_images">Cihaz Təsvirləri</string>
+ <string name="tab_title_device_videos">Cihaz Videoları</string>
+ <string name="tab_title_site_images">Sayt Təsvirləri</string>
+ <string name="tab_title_site_videos">Sayt Videoları</string>
+ <string name="error_publish_no_network">Qoşulma olmadığında dərc edilə bilmir. Qaralama olaraq qeyd edildi.</string>
+ <string name="editor_toast_invalid_path">Keçərsiz fayl yolu.</string>
+ <string name="verification_code">Təsdiqləmə kodu</string>
+ <string name="invalid_verification_code">Keçərsiz təsdiqləmə kodu</string>
+ <string name="verify">Təsdiqlə</string>
+ <string name="two_step_footer_label">Təsdiqləmə proqramınızdakı kodu daxil edin.</string>
+ <string name="two_step_footer_button">Kodu mətn mesajı olaraq göndər</string>
+ <string name="two_step_sms_sent">Mətn mesajlarınızı təsdiqləmə kodu üçün yoxlayın.</string>
+ <string name="sign_in_jetpack">Jetpack qoşulması üçün WordPress.com hesabınıza giriş edin.</string>
+ <string name="auth_required">Davam etmək üçün giriş edin.</string>
+ <string name="reader_empty_posts_request_failed">Yazılar geri alına bilmir</string>
+ <string name="publisher">Naşir:</string>
+ <string name="error_notification_open">Bildiriş açılmadı</string>
+ <string name="stats_followers_total_email_paged">%1$d - %2$d / %3$s E-Poçt İzləyicisi göstərilir</string>
+ <string name="stats_search_terms_unknown_search_terms">Bilinməyən Axtarış Kriteriyaları</string>
+ <string name="stats_followers_total_wpcom_paged">%1$d - %2$d / %3$s WordPress.com İzləyicisi gösterilir</string>
+ <string name="stats_empty_search_terms_desc">Ziyarətçilərin saytınızı tapması üçün axtardığı kriteriyaları analiz edərək axtarış trafikiniz haqqında daha çox məlumat sahibi olun.</string>
+ <string name="stats_empty_search_terms">Qeyd edilmiş heç bir axtarış kriteriyası yoxdur</string>
+ <string name="stats_entry_search_terms">Axtarış Kriteriyası</string>
+ <string name="stats_view_authors">Müəlliflər</string>
+ <string name="stats_view_search_terms">Axtarış Kriteriyaları</string>
+ <string name="comments_fetching">Şərhlər alınır...</string>
+ <string name="pages_fetching">Səhifələr alınır...</string>
+ <string name="toast_err_post_uploading">Yazı yüklənərkən açılmır</string>
+ <string name="posts_fetching">Yazılar alınır...</string>
+ <string name="media_fetching">Media faylı alınır...</string>
+ <string name="post_uploading">Yüklənir</string>
+ <string name="stats_total">Cəmi</string>
+ <string name="stats_overall">Əsas</string>
+ <string name="stats_period">Dövr</string>
+ <string name="logs_copied_to_clipboard">Proqram jurnalı lövhəyə köçürüldü</string>
+ <string name="reader_label_new_posts">Yeni yazılar</string>
+ <string name="reader_empty_posts_in_blog">Bu bloq boşdur</string>
+ <string name="stats_average_per_day">Gün ərzində orta hesabla</string>
+ <string name="stats_recent_weeks">Son Həftələr</string>
+ <string name="error_copy_to_clipboard">Mətn lövhəyə köçürülərkən xəta baş verdi</string>
+ <string name="stats_months_and_years">Aylar və İllər</string>
+ <string name="themes_fetching">Mövzular alınır...</string>
+ <string name="stats_for">%s üçün statistikalar</string>
+ <string name="stats_other_recent_stats_label">Digər Son Statistikalar</string>
+ <string name="stats_view_all">Tamamına bax</string>
+ <string name="stats_view">Baxış</string>
+ <string name="stats_followers_months">%1$d ay</string>
+ <string name="stats_followers_a_year">Bir il</string>
+ <string name="stats_followers_years">%1$d il</string>
+ <string name="stats_followers_a_month">Bir ay</string>
+ <string name="stats_followers_minutes">%1$d dəqiqə</string>
+ <string name="stats_followers_an_hour_ago">bir saat öncə</string>
+ <string name="stats_followers_hours">%1$d saat</string>
+ <string name="stats_followers_a_day">Bir gün</string>
+ <string name="stats_followers_days">%1$d gün</string>
+ <string name="stats_followers_a_minute_ago">dəqiqə öncə</string>
+ <string name="stats_followers_seconds_ago">saniyə öncə</string>
+ <string name="stats_followers_total_email">Cəmi E-poçt İzləyiciləri: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">E-poçt</string>
+ <string name="stats_followers_total_wpcom">Cəmi WordPress.com İzləyicisi: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Şərh izləyicili cəmi yazı miqdarı: %1$s</string>
+ <string name="stats_comments_by_authors">Müəlliflərə Görə</string>
+ <string name="stats_comments_by_posts_and_pages">Yazılara və Səhifələrə Görə</string>
+ <string name="stats_empty_followers_desc">Əsas izləyici miqdarınızı və hər birininsaytınızı hansı vaxtdan bəri izlədiyini izləyin.</string>
+ <string name="stats_empty_followers">İzləyici yoxdur</string>
+ <string name="stats_empty_publicize_desc">Bəyan xüsusiyyətindən istifadə edərək müxtəlif sosial şəbəkə xidmətlərindən izləyicilərinizi izləyin.</string>
+ <string name="stats_empty_publicize">Qeyd edilmiş heç bir bəyan izləyicisi yoxdur</string>
+ <string name="stats_empty_video">İfa edilmiş video yoxdur</string>
+ <string name="stats_empty_video_desc">VideoPress-dən istifadə edərək video yükləmisinizsə, videolarınızın neçə dəfə izləndiyini öyrənin.</string>
+ <string name="stats_empty_comments_desc">Saytınızda şərhlərə izin verirsinizsə, ən son 1000 şərh əsas alındığında yazılarınıza ən çox şərh edənləri izləyin və hansı içəriklərin ən canlı müzakirələrə yol açdığını kəşf edin.</string>
+ <string name="stats_empty_tags_and_categories_desc">Keçən həftə ən çox oxunan yazılarınızda görüldüyü üzrə saytınızdakı ən populyar bəhslərdən xəbərdar olun.</string>
+ <string name="stats_empty_top_authors_desc">Əmək sərf edənlərin yazılarının nə qədər baxıldığını izləyin və hər yazının ən popolyar içəriyini kəşf etmək üçün ətraflı olaraq analiz edin.</string>
+ <string name="stats_empty_tags_and_categories">Etiketlənmiş heç bir yazı ya da səhifəyə baxılmamışdır</string>
+ <string name="stats_empty_clicks_desc">İçəriyiniz digər saytlara bağlantılar verdiyində ziyarətçilərinizin ən çox hansıları tıkladığını görəcəksiniz.</string>
+ <string name="stats_empty_referrers_desc">Sizə ən çox trafik yönləndirən veb saytlarını və axtarış sistemlərini analiz edərək saytınızın görünüşü haqqında daha çox məlumat əldə edin</string>
+ <string name="stats_empty_clicks_title">Qeyd edilmiş heç bir tıklama yoxdur</string>
+ <string name="stats_empty_referrers_title">Qeyd edilmiş heç bir yönləndirmə yoxdur</string>
+ <string name="stats_empty_top_posts_title">Baxılmış heç bir səhifə və ya yazı yoxdur</string>
+ <string name="stats_empty_top_posts_desc">Ən çox baxılan içəriyinizin hansı olduğunu kəşf edin və tək-tək yazılarla səhifələrin zaman ərzindəki müvəffəqiyyətini analiz edin.</string>
+ <string name="stats_totals_followers">Bu tarixdən başlayaraq:</string>
+ <string name="stats_empty_geoviews">Qeyd edilmiş heç bir ölkə yoxdur</string>
+ <string name="stats_empty_geoviews_desc">Saytınıza ən çox hansı ölkə və bölgələrdən trafik olduğunu görmək üçün siyahını analiz edin.</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Müəllif</string>
+ <string name="stats_entry_publicize">Xidmət</string>
+ <string name="stats_entry_followers">İzləyici</string>
+ <string name="stats_totals_publicize">İzləyicilər</string>
+ <string name="stats_entry_clicks_link">Bağlantı</string>
+ <string name="stats_view_top_posts_and_pages">Yazı və Səhifələr</string>
+ <string name="stats_view_videos">Videolar</string>
+ <string name="stats_view_publicize">Bəyan</string>
+ <string name="stats_view_followers">İzləyici</string>
+ <string name="stats_view_countries">Ölkələr</string>
+ <string name="stats_likes">Bəyənmə</string>
+ <string name="stats_pagination_label">Səhifə %1$s / %2$s</string>
+ <string name="stats_timeframe_years">İl</string>
+ <string name="stats_views">Baxış</string>
+ <string name="stats_visitors">Ziyarətçi</string>
+ <string name="ssl_certificate_details">Detallar</string>
+ <string name="delete_sure_post">Bu yazını sil</string>
+ <string name="delete_sure">Bu qaralamanı sil</string>
+ <string name="delete_sure_page">Bu səhifəni sil</string>
+ <string name="confirm_delete_multi_media">Seçilmiş maddələr silinsinmi?</string>
+ <string name="confirm_delete_media">Seçilmiş maddə silinsinmi?</string>
+ <string name="cab_selected">%d seçildi</string>
+ <string name="media_gallery_date_range">%1$s və %2$s arası media faylları göstərilir</string>
+ <string name="sure_to_remove_account">Bu sayt silinsinmi?</string>
+ <string name="faq_button">ÇVS</string>
+ <string name="create_new_blog_wpcom">WordPress.com bloqu yarat</string>
+ <string name="new_blog_wpcom_created">WordPress.com blou yaradıldı!</string>
+ <string name="reader_empty_comments">Şərh yoxdur</string>
+ <string name="reader_label_comment_count_multi">%,d şərh</string>
+ <string name="reader_label_view_original">Orijinal məqaləyə bax</string>
+ <string name="reader_label_like">Bəyən</string>
+ <string name="reader_label_comment_count_single">Bir şərh</string>
+ <string name="reader_label_comments_closed">Şərhlər bağlıdır</string>
+ <string name="reader_label_comments_on">Şərh</string>
+ <string name="reader_title_photo_viewer">%1$d / %2$d</string>
+ <string name="error_refresh_unauthorized_posts">Yazıya baxmağa və ya onu redaktə etməyə izniniz yoxdur</string>
+ <string name="select_a_blog">WordPress saytı seç</string>
+ <string name="mnu_comment_liked">Bəyənildi</string>
+ <string name="comment">Şərh</string>
+ <string name="comment_trashed">Şərh zibil qutusuna göndərildi</string>
+ <string name="signing_out">Çıxış…</string>
+ <string name="reader_empty_posts_liked">Hələki heç bir yazı bəyənməmisiniz</string>
+ <string name="browse_our_faq_button">SVS səhifəmizə göz atın</string>
+ <string name="nux_help_description">Sıx verilən suallara cavab almaq üçün yardım mərkəzini, yeni suallar soruşmaq üçün forumları ziyarət edin</string>
+ <string name="agree_terms_of_service">Bir hesab açaraq %1$sXidmət Şərtləri%2$s-ni qəbul etmiş olursunuz.</string>
+ <string name="reader_empty_posts_in_tag">Bu etiketə sahib olan yazı yoxdur</string>
+ <string name="error_publish_empty_post">Boş bir yazı dərc edilə bilməz</string>
+ <string name="error_refresh_unauthorized_pages">Səhifələrə baxma və ya redaktə etmə izniniz yoxdur</string>
+ <string name="error_refresh_unauthorized_comments">Şərhlərə baxma və ya redaktə etmə izniniz yoxdur</string>
+ <string name="older_month">Bir aydan əski</string>
+ <string name="more">Daha çox</string>
+ <string name="older_two_days">2 gündən əski</string>
+ <string name="older_last_week">1 həftədən əski</string>
+ <string name="stats_no_blog">İstənən bloq üçün statistiklar yüklənmir</string>
+ <string name="sending_content">%s içəriyi yüklənir</string>
+ <string name="uploading_total">%1$d / %2$d yüklənir</string>
+ <string name="posts_empty_list">Hələlik yazı yoxdur. Bir yazı yaratmağa nə deyərsiniz?</string>
+ <string name="comment_reply_to_user">Bunu cavabla: %s</string>
+ <string name="pages_empty_list">Hələlik səhifə yoxdur. Bir səhifə yaratmağa nə deyərsiniz?</string>
+ <string name="media_empty_list_custom_date">Bu zaman aralığında media yoxdur</string>
+ <string name="posting_post">Yazılan: "%s"</string>
+ <string name="reader_empty_followed_blogs_title">Hələki heç bir sayt izləmirsiniz</string>
+ <string name="reader_toast_err_generic">Bu hərəkət həyata keçirilə bilmir</string>
+ <string name="reader_toast_err_block_blog">Bu bloq əngəllənə bilmir</string>
+ <string name="reader_toast_blog_blocked">Bu bloqdan yazılar artıq göstərilməyəcək</string>
+ <string name="reader_menu_block_blog">Bu bloqu blok edin</string>
+ <string name="contact_us">Əlaqə yaradın</string>
+ <string name="hs__conversation_detail_error">Qarşılaşdığınız problem haqqında məlumat verin</string>
+ <string name="hs__new_conversation_header">Dəstək söhbəti</string>
+ <string name="hs__conversation_header">Dəstək söhbəti</string>
+ <string name="hs__username_blank_error">Keçərli bir ad daxil edin</string>
+ <string name="hs__invalid_email_error">Keçərli bir e-poçt ünvanı daxil edin</string>
+ <string name="add_location">Mövqe əlavə edin</string>
+ <string name="current_location">Hazırkı mövqe</string>
+ <string name="search_location">Axtarış</string>
+ <string name="edit_location">Redaktə et</string>
+ <string name="search_current_location">Yerləşdir</string>
+ <string name="preference_send_usage_stats">Statistikaları göndər</string>
+ <string name="preference_send_usage_stats_summary">Android üçün WordPress-i inkişaf etdirməyimizə yardımçı olmaq üçün bizə avtomatik olaraq istifadə statistikalarını göndərin. </string>
+ <string name="update_verb">Yenilə</string>
+ <string name="schedule_verb">Planla</string>
+ <string name="reader_title_subs">Etiketlər &amp; Bloqlar</string>
+ <string name="reader_page_followed_tags">İzlənilən etiketlər</string>
+ <string name="reader_label_followed_blog">Bloq izlənilir</string>
+ <string name="reader_label_tag_preview">Yazılar %s etiketləndi</string>
+ <string name="reader_toast_err_get_blog_info">Bloqu göstərmək mümkün olmadı</string>
+ <string name="reader_toast_err_already_follow_blog">Bu bloqu artıq izləyirsiniz</string>
+ <string name="reader_toast_err_follow_blog">Bu bloqu izləmək mümkün deyil</string>
+ <string name="reader_hint_add_tag_or_url">İzləmək üçün etiket və ya URL daxil edin</string>
+ <string name="reader_toast_err_unfollow_blog">Bu bloqun izlənməsi ləğv edilə bilmir</string>
+ <string name="reader_empty_recommended_blogs">Məsləhət bilinən bloq yoxdur</string>
+ <string name="reader_page_followed_blogs">İzlənilən saytlar</string>
+ <string name="saving">Qeyd edilir…</string>
+ <string name="ptr_tip_message">Məsləhət: Yeniləmək üçün aşağı dartın</string>
+ <string name="media_empty_list">Mediafayl yoxdur</string>
+ <string name="forums">Forumlar</string>
+ <string name="help_center">Yardım mərkəzi</string>
+ <string name="ssl_certificate_error">Yanlış SSL sertifikatı</string>
+ <string name="help">Yardım</string>
+ <string name="forgot_password">Parolunuzu itirdinizmi?</string>
+ <string name="ssl_certificate_ask_trust">Əgər bu sayta əsasən problemsiz şəkildə qoşula bilirsinizsə, bu xəta başqalarının saytı təqlid etdiyi mənasına gəlir. yenə də sertifikata etibar etmək istəyirsinizmi?</string>
+ <string name="comments_empty_list">Şərh yoxdur</string>
+ <string name="mnu_comment_unspam">Spam deyil</string>
+ <string name="out_of_memory">Cihazın yaddaşı doldu</string>
+ <string name="no_network_message">Uyğun vəziyyətdə şəbəkə yoxdur</string>
+ <string name="gallery_error">Media maddəsi bərpa edilə bilmədi</string>
+ <string name="blog_not_found">Bu bloqla əlaqə qurularkən bir xəta ilə qarşılaşıldı</string>
+ <string name="wait_until_upload_completes">Yükləmə bitənə qədər gözləyin</string>
+ <string name="theme_fetch_failed">Mövzuları qəbul etmək mümkün olmadı</string>
+ <string name="theme_set_failed">Mövzunu qurmaq mümkün olmadı</string>
+ <string name="theme_auth_error_message">Mövzu qurma səlahiyyətinə sahib olduğunuzdan əmin olun</string>
+ <string name="no_site_error">WordPress saytına qoşulma baş tutmadı</string>
+ <string name="adding_cat_failed">Kateqoriya əlavə etmə baş tutmadı</string>
+ <string name="adding_cat_success">Kateqoriya müvəffəqiyyətlə əlavə edildi</string>
+ <string name="cat_name_required">Kateqoriya adı vacibdir</string>
+ <string name="category_automatically_renamed">Kateqoriya adı %1$s yararsızdır. Ad %2$s olaraq dəyişdirildi</string>
+ <string name="no_account">WordPress hesabı tapılmadı, bir hesab əlavə edib təkrar yoxlayın</string>
+ <string name="sdcard_message">Media yükləmək üçün taxılmış SD kart gərəklidir</string>
+ <string name="stats_empty_comments">Hələlik şərh yoxdur</string>
+ <string name="stats_bar_graph_empty">Baxılacaq statistika yoxdur</string>
+ <string name="reply_failed">Caavab yazma baş tutmadı</string>
+ <string name="notifications_empty_list">Bildirş yoxdur</string>
+ <string name="error_delete_post">%s silinərkən xəta ilə qarşılaşıldı</string>
+ <string name="error_refresh_posts">Yazılar hazırda təzələnə bilmir</string>
+ <string name="error_refresh_pages">Səhifələr hazırda təzələnə bilmir</string>
+ <string name="error_refresh_notifications">Bildirişlər hazırda təzələnə bilmir</string>
+ <string name="error_refresh_comments">Şərhlər hazırda təzələnə bilmir</string>
+ <string name="error_refresh_stats">Statistikalar hazırda təzələnə bilmir</string>
+ <string name="error_generic">Bir xəta baş verdi</string>
+ <string name="error_moderate_comment">Moderasiya sırasında xəta baş verdi</string>
+ <string name="error_upload">%s yüklənərkən xəta baş verdi </string>
+ <string name="error_load_comment">Şərh yüklənmədi</string>
+ <string name="error_downloading_image">Təsvir endirilərkən xəta baş verdi</string>
+ <string name="passcode_wrong_passcode">Yanlış PIN</string>
+ <string name="error_edit_comment">Şərh redaktəsi sırasında xəta baş verdi</string>
+ <string name="invalid_email_message">Keçərsiz e-poçt ünvanı</string>
+ <string name="invalid_password_message">Parollar ən az 4 simvol olmalıdır</string>
+ <string name="invalid_username_too_short">İstifadəçi adı 4 simvoldan artıq olmalıdır</string>
+ <string name="invalid_username_too_long">İstifadəçi adı 61 simvoldan qısa olmalıdır</string>
+ <string name="username_only_lowercase_letters_and_numbers">İstifadəçi adı sadəcə kiçik hərflərdən (a-z) və rəqəmlərdən ibarət olmalıdır</string>
+ <string name="username_required">İstifadəçi adınızı daxil edin</string>
+ <string name="username_not_allowed">İstifadəçi adına izin verilmir</string>
+ <string name="username_must_be_at_least_four_characters">İstifadəçi adı ən az 4 simvol olmalıdır</string>
+ <string name="username_contains_invalid_characters">İstifadəçi adında "_" simvolu ola bilməz</string>
+ <string name="username_must_include_letters">İstifadəçi adında ən az 1 hərf(a-z) olmalıdır</string>
+ <string name="email_invalid">Etibarlı e-poçt ünvanı daxil edin</string>
+ <string name="email_not_allowed">Bu e-poçt ünvanına izin verilmir</string>
+ <string name="username_exists">Bu istifadəçi adı artıq istifadə edilir</string>
+ <string name="email_exists">Bu e-poçt ünvanı artıq istifadə edilir</string>
+ <string name="username_reserved_but_may_be_available">Bu istifadəçi adı ehtiyatdadır, ancaq bir neçə gün ərzində müsaid ola bilər</string>
+ <string name="blog_name_required">Sayt ünvanını daxil edin</string>
+ <string name="blog_name_not_allowed">Bu sayt ünvanına izin verilmir</string>
+ <string name="blog_name_must_be_at_least_four_characters">Sayt ünvanı ən az 4 simvol olmalıdır</string>
+ <string name="blog_name_contains_invalid_characters">Sayt ünvanında “_” simvolundan istifadə edilə bilməz</string>
+ <string name="blog_name_cant_be_used">Bu sayt ünvanından istifadə edə bilməzsiniz </string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Sayt ünvanı 64 simvoldan qısa olmalıdır</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Sayt ünvanı kiçik hərflərdən (a-z) və rəqəmlərdən ibarət olmalıdır</string>
+ <string name="blog_name_exists">Bu sayt artıq istifadə edilir</string>
+ <string name="blog_name_reserved">Bu sayt ehtiyatdadır</string>
+ <string name="blog_name_reserved_but_may_be_available">Bu sayt adı ehtiyatdadır, ancaq bir neçə gün ərzində müsaid ola bilər</string>
+ <string name="username_or_password_incorrect">Yanlış istifadəçi adı və ya parol daxil etdiniz</string>
+ <string name="nux_cannot_log_in">Daxil ola bilmirsiniz</string>
+ <string name="could_not_remove_account">Sayt silinmədi</string>
+ <string name="select_categories">Kateqoriyaları seç</string>
+ <string name="account_details">Hesab məlumatları</string>
+ <string name="edit_post">Yazını redaktə et</string>
+ <string name="add_comment">Şərh əlavə et</string>
+ <string name="connection_error">Qoşulma xətası</string>
+ <string name="cancel_edit">Redaktəni ləğv et</string>
+ <string name="post_format">Yazı formatı</string>
+ <string name="new_post">Yeni yazı</string>
+ <string name="new_media">Yeni media</string>
+ <string name="blog_name_invalid">Keçərsiz sayt ünvanı</string>
+ <string name="blog_title_invalid">Keçərsiz sayt başlığı</string>
+ <string name="email_cant_be_used_to_signup">Bu e-poçt ünvanından qeydiyyat üçün istifadə edə bilməzsiniz. Bəzi e-poçtlarımızın qadağan olması ilə əlaqədar problemlə qarşılaşırıq. Başqa bir e-poçt provayderindən istifadə edin.</string>
+ <string name="email_reserved">Bu e-poçt ünvanından artıq istifadə edilmişdir. Daxil olan poçt qutunuzu aktivasiya e-poçtu üçün yoxlayın. Əgər aktivasiya etməsəniz, bir neçə gün ərzində təkrar yoxlaya bilərsiniz.</string>
+ <string name="blog_name_must_include_letters">Sayt ünvanı ən az bir ədəd hərfə sahib (a-z) olmalıdır</string>
+ <string name="jetpack_message_not_admin">Statistikalar üçün Jetpack plaqininə ehtiyac var. Sayt nəzarətçisi ilə əlaqə qurun.</string>
+ <string name="reader_title_applog">Proqram jurnalı</string>
+ <string name="reader_share_link">Bağlantını paylaş</string>
+ <string name="reader_toast_err_add_tag">Bu etiket əlavə edilə bilmir</string>
+ <string name="reader_toast_err_remove_tag">Bu etiket silinə bilmir</string>
+ <string name="required_field">Gərəkli sahə</string>
+ <string name="email_hint">E-poçt ünvanı</string>
+ <string name="site_address">Öz serverinizdə mövcud olan bloq ünvanınız(URL)</string>
+ <string name="view_site">Sayta bax</string>
+ <string name="privacy_policy">Gizlilik siyasəti</string>
+ <string name="local_changes">Yerli dəyişikliklər</string>
+ <string name="image_settings">Təsvir parametrləri</string>
+ <string name="add_account_blog_url">Bloq ünvanı</string>
+ <string name="wordpress_blog">WordPress bloqu</string>
+ <string name="error_blog_hidden">Bu bloq gizlidir və yüklənmədi. Parametrlərdən yenidən aktivləşdirin və təkrar yoxlayın.</string>
+ <string name="fatal_db_error">Proqram üçün verilənlər bazası yaradılarkən bir xəta baş verdi. Proqramı təkrar qurmağa cəhd edin.</string>
+ <string name="pending_review">Ön baxış üçün gözləyir</string>
+ <string name="http_credentials">HTTP məlumatları (seçimə bağlı)</string>
+ <string name="http_authorization_required">Qeydiyyat gərəklidir</string>
+ <string name="view_in_browser">Brauzerdə bax</string>
+ <string name="add_new_category">Yeni kateqoriya əlavə et</string>
+ <string name="category_name">Kateqoriya adı</string>
+ <string name="category_slug">Kateqoriya yarlığı (seçimə bağlı)</string>
+ <string name="category_desc">Kateqoriya açıqlaması (seçimə bağlı)</string>
+ <string name="category_parent">Valideyin kateqoriya (seçimə bağlı):</string>
+ <string name="share_action_post">Yeni yazı</string>
+ <string name="share_action_media">Media kitabxanası</string>
+ <string name="file_error_create">Müvəqqəti media faylı yaradıla bilmir. Cihazınızda yetərli böş sahə olduğundan əmin olun.</string>
+ <string name="location_not_found">Bilinməyən mövqe</string>
+ <string name="open_source_licenses">Açıq mənbə lisenziyaları</string>
+ <string name="xmlrpc_error">Əlaqə qurula bilmədi. Saytınızdakı xmlrpc.php-nin tam yolunu daxil edib təkrar yoxlayın</string>
+ <string name="hint_comment_content">Şərh</string>
+ <string name="saving_changes">Dəyişikliklər qeyd edilir</string>
+ <string name="sure_to_cancel_edit_comment">Şərhin redaktəsi ləğv edilsinmi?</string>
+ <string name="content_required">Şərh vacibdir</string>
+ <string name="toast_comment_unedited">Şərh dəyişdirilmədi</string>
+ <string name="delete_draft">Qaralamanı sil</string>
+ <string name="preview_page">Səhifəyə ön baxış et</string>
+ <string name="preview_post">Yazıya ön baxış et</string>
+ <string name="comment_added">Şərh müvəffəqiyyətlə əlavə edildi</string>
+ <string name="post_not_published">Yazı statusu dərc edilmədi</string>
+ <string name="page_not_published">Səhifə statusu dərc edilmədi</string>
+ <string name="mnu_comment_approve">Təsdiqlə</string>
+ <string name="mnu_comment_unapprove">Rədd et</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Zibil qutusu</string>
+ <string name="dlg_approving_comments">Təsdiqlənir</string>
+ <string name="dlg_unapproving_comments">Rədd edilir</string>
+ <string name="dlg_spamming_comments">Spam olaraq işarələnir</string>
+ <string name="dlg_trashing_comments">Zibil qutusuna göndərilir</string>
+ <string name="dlg_confirm_trash_comments">Zibil qutusuna göndərilsinmi?</string>
+ <string name="trash_yes">Zibil</string>
+ <string name="trash_no">Zibil deyil</string>
+ <string name="trash">Zibil</string>
+ <string name="author_name">Müəllifin adı</string>
+ <string name="author_email">Müəllifin e-poçtu</string>
+ <string name="author_url">Müəllifin URL</string>
+ <string name="page_settings">Səhifə parametrləri</string>
+ <string name="local_draft">Yerli qaralama</string>
+ <string name="upload_failed">Yükləmə baş tutmadı</string>
+ <string name="horizontal_alignment">Üfüqi qruplaşma</string>
+ <string name="file_not_found">Yüklənəcək media faylı tapılmadı. Silinmiş vəya daşınmış ola bilərmi?</string>
+ <string name="post_settings">Yazı parametrləri</string>
+ <string name="delete_post">Yazını sil</string>
+ <string name="delete_page">Səhifəni sil</string>
+ <string name="comment_status_approved">Təsdiqlənən</string>
+ <string name="comment_status_unapproved">Gözləyir</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Zibil</string>
+ <string name="edit_comment">Şərhi redaktə et</string>
+ <string name="scaled_image_error">Keçərli miqyasda genişlik dəyəri daxil edin</string>
+ <string name="post_not_found">Yazı yüklənərkən xəta baş verdi. Yazını təzələyin və təkrar cəhd edin.</string>
+ <string name="learn_more">Daha ətraflı məlumat</string>
+ <string name="media_gallery_settings_title">Qalereya parametrləri</string>
+ <string name="media_gallery_image_order">Təsvir sırası</string>
+ <string name="media_gallery_num_columns">Sütun miqdarı</string>
+ <string name="media_gallery_type_thumbnail_grid">Miniatür təsvir</string>
+ <string name="media_gallery_edit">Qalereyanı redaktə et</string>
+ <string name="media_error_no_permission">Media kitabxanasına baxma izniniz yoxdur</string>
+ <string name="cannot_delete_multi_media_items">Bəzi media faylları silinmədi. Daha sonra təkrar yoxlayın.</string>
+ <string name="themes_live_preview">Canlı ön izləmə</string>
+ <string name="theme_current_theme">Hazırkı mövzu</string>
+ <string name="theme_premium_theme">Premium mövzu</string>
+ <string name="link_enter_url_text">Bağlantı mətni (seçimə bağlı)</string>
+ <string name="create_a_link">Bağlantı yarat</string>
+ <string name="remove_account">Saytı sil</string>
+ <string name="blog_removed_successfully">Sayt uğurla silindi</string>
+ <string name="notifications_empty_all">Bildiriş yoxdur...</string>
+ <string name="deleting_page">Səhifə silinir</string>
+ <string name="deleting_post">Yazı silinir</string>
+ <string name="share_url_post">Yazını paylaş</string>
+ <string name="share_url_page">Səhifəni paylaş</string>
+ <string name="share_link">Bağlantını paylaş</string>
+ <string name="creating_your_account">Yeni hesabınız yaradılır</string>
+ <string name="creating_your_site">Yeni saytınız yaradılır</string>
+ <string name="error_refresh_media">Media kitabxanasının yenilənməsi zamanı bəzi şeylər tərs getdi. Daha sonra təkrar yoxlayın.</string>
+ <string name="reader_empty_posts_in_tag_updating">Yazılar gətirilir...</string>
+ <string name="reader_label_reply">Cavab</string>
+ <string name="comment_spammed">Şərh spam olaraq işarələnmişdir</string>
+ <string name="video">Video</string>
+ <string name="reader_likes_multi">%,d şəxs bunu bəyənir</string>
+ <string name="reader_likes_you_and_multi">Siz və bir digər şəxs %,d bunu bəyənir</string>
+ <string name="reader_toast_err_get_comment">Bu şərhi qəbul etmək mümkün deyil</string>
+ <string name="download">Media endirilir</string>
+ <string name="cant_share_no_visible_blog">Görünən bir bloqunuz olmadan WordPress ilə paylaşa bilməzsiniz</string>
+ <string name="select_time">Saat seç</string>
+ <string name="select_date">Tarix seç</string>
+ <string name="pick_photo">Foto seç</string>
+ <string name="pick_video">Video seç</string>
+ <string name="reader_toast_err_get_post">Bu yazını almaq mümkün deyil</string>
+ <string name="validating_user_data">İstifadəçi məlumatları təsdiqlənir</string>
+ <string name="validating_site_data">Sayt məlumatları təsdiqlənir</string>
+ <string name="reader_likes_you_and_one">Siz və bir diğəri bunu bəyənir</string>
+ <string name="account_two_step_auth_enabled">Bu hesab iki məhələli təsdiqləməyə sahibdir. Посетите настройки безопасности на WordPress.com-da təhlükəsizlik parametrlərini ziyarət edin və konkret proqram üçün parol hazırlayın.</string>
+ <string name="nux_tap_continue">Davam et</string>
+ <string name="nux_welcome_create_account">Hesab yarat</string>
+ <string name="password_invalid">Sizə daha etibarlı parol lazımdır. 7 simvoldan artıq böyük və kiçik hərflərin, rəqəm və ya özəl simvolların qarışığından istifadə edin.</string>
+ <string name="nux_add_selfhosted_blog">Öz-xostinq saytını əlavə et</string>
+ <string name="nux_oops_not_selfhosted_blog">WordPress.com ilə daxil ol</string>
+ <string name="signing_in">Giriş…</string>
+ <string name="media_add_popup_title">Media kitabxanasına əlavə et</string>
+ <string name="media_add_new_media_gallery">Qalereya yarat</string>
+ <string name="empty_list_default">Bu siyahı boşdur</string>
+ <string name="reader_btn_share">Paylaş</string>
+ <string name="jetpack_not_found">Jetpack plaqini tapılmadı</string>
+ <string name="select_from_media_library">Media kitabxanasından seç</string>
+ <string name="username_invalid">Keçərsiz istifadəçi adı</string>
+ <string name="nux_tutorial_get_started_title">Başlayın!</string>
+ <string name="jetpack_message">Statistikalar üçün Jetpack plaqininə ehtiyac var. Jetpack plaqinini qurmaq istəyirsinizmi?</string>
+ <string name="reader_untitled_post">(adsız)</string>
+ <string name="reader_share_subject">Paylaşılan %s</string>
+ <string name="reader_label_added_tag"> %s əlavə edildi</string>
+ <string name="reader_label_removed_tag">%s silindi</string>
+ <string name="connecting_wpcom">WordPress.com-a qoşulma</string>
+ <string name="limit_reached">Limit həddini aşdınız. 1 dəqiqə ərzində təkrar yoxlaya bilərsiniz. Bir daha cəhd etsəniz qadağanın ləğv edilməsi üçün bir az gözləməli olacaqsınız. Bunun bir xəta olduğunu düşünürsünüzsə dəstək xidmətinə müraciət edin.</string>
+ <string name="reader_empty_followed_tags">Siz hər hansı bir etiketi izləyə bilməyəcəksiniz</string>
+ <string name="create_account_wpcom">WordPress.com-da hesab yaradın</string>
+ <string name="reader_btn_follow">İzlə</string>
+ <string name="reader_btn_unfollow">İzlənən</string>
+ <string name="reader_likes_one">Bir nəfər bunu bəyənir</string>
+ <string name="reader_likes_only_you">Bunu bəyənirsiniz</string>
+ <string name="reader_toast_err_comment_failed">Şərhi dərc etmək baş tutmadı</string>
+ <string name="reader_toast_err_tag_exists">Siz artıq bu etiketi izləyirsiniz</string>
+ <string name="reader_toast_err_tag_invalid">Bu keçərli bir etiket deyil</string>
+ <string name="reader_toast_err_share_intent">Paylaşma baş tutmadı</string>
+ <string name="reader_toast_err_view_image">Təsviri görüntüləmək baş tutmadı</string>
+ <string name="reader_toast_err_url_intent">%s açmaq baş tutmadı</string>
+ <string name="reader_hint_comment_on_comment">Şərhə cavab yaz...</string>
+ <string name="themes">Mövzular</string>
+ <string name="images">Təsvirlər</string>
+ <string name="media_gallery_type_slideshow">Slaydşou</string>
+ <string name="media_edit_description_text">Açıqlama</string>
+ <string name="share_action">Göndər</string>
+ <string name="stats_view_tags_and_categories">Etiketlər və Kateqoriyalar</string>
+ <string name="stats_entry_authors">Müəllif</string>
+ <string name="stats_entry_posts_and_pages">Başlıq</string>
+ <string name="all">Tamamı</string>
+ <string name="custom_date">Özəl tarix</string>
+ <string name="media_gallery_image_order_random">Qarışıq</string>
+ <string name="media_gallery_image_order_reverse">Təkrar</string>
+ <string name="media_add_popup_capture_photo">Foto tut</string>
+ <string name="media_add_popup_capture_video">Video tut</string>
+ <string name="share_action_title">Əlavə et</string>
+ <string name="stats_entry_country">Ölkə</string>
+ <string name="stats_entry_tags_and_categories">Bəhs</string>
+ <string name="stats_entry_referrers">Yönləndirmə</string>
+ <string name="media_gallery_type">Növ</string>
+ <string name="media_edit_caption_text">Başlıq</string>
+ <string name="media_edit_success">Yenilənib</string>
+ <string name="media_edit_title_hint">Buraya bir şüar daxil edin</string>
+ <string name="media_edit_caption_hint">Buraya bir başlıq daxil edin</string>
+ <string name="media_edit_description_hint">Buraya bir açıqlama daxil edin</string>
+ <string name="post_excerpt">Çıxarış</string>
+ <string name="stats">Statistikalar</string>
+ <string name="stats_view_visitors_and_views">Qonaqlar və Baxışlar</string>
+ <string name="stats_view_clicks">Tıklamalar</string>
+ <string name="unattached">Bərkidilməmiş</string>
+ <string name="passcode_change_passcode">PIN kodun idarə edilməsi</string>
+ <string name="passcode_set">PIN kod quruludur</string>
+ <string name="passcode_preference_title">PIN bağlıdır</string>
+ <string name="passcode_turn_off">PIN bloku ləğv et</string>
+ <string name="passcode_turn_on">PIN bloku qoş</string>
+ <string name="media_gallery_type_squares">Kvadratlar</string>
+ <string name="media_gallery_type_tiled">Döşənmiş</string>
+ <string name="media_gallery_type_circles">Çəmbərlər</string>
+ <string name="media_edit_failure">Yenilənmə sirasında xəta</string>
+ <string name="themes_details_label">Ətraflı detallar</string>
+ <string name="themes_features_label">Özəlliklər</string>
+ <string name="theme_activate_button">Aktivasiya</string>
+ <string name="theme_activating_button">Aktivləşdirmək</string>
+ <string name="theme_set_success">Mövzu müvəffəqiyyətlə quruldu!</string>
+ <string name="theme_auth_error_title">Mövzu qəbulu sırasında xəta</string>
+ <string name="stats_view_referrers">Bağlantı verənlər</string>
+ <string name="stats_timeframe_today">Bu gün</string>
+ <string name="stats_timeframe_yesterday">Dünən</string>
+ <string name="stats_timeframe_days">Günlər</string>
+ <string name="stats_timeframe_weeks">Həftələr</string>
+ <string name="stats_timeframe_months">Aylar</string>
+ <string name="stats_totals_views">Baxışlar</string>
+ <string name="stats_totals_clicks">Tıklamalar</string>
+ <string name="stats_totals_plays">İfalar</string>
+ <string name="media_edit_title_text">Başlıq</string>
+ <string name="passcode_re_enter_passcode">PIN kodunu təkrar daxil et</string>
+ <string name="passcode_enter_passcode">PIN kodunu daxil et</string>
+ <string name="passcode_enter_old_passcode">Əvvəlki PIN kodunu daxil et</string>
+ <string name="passcode_manage">PIN bloklamasının idarəetməsi</string>
+ <string name="upload">Yüklə</string>
+ <string name="discard">Ləğv et</string>
+ <string name="sign_in">Giriş</string>
+ <string name="notifications">Bildirişlər</string>
+ <string name="note_reply_successful">Cavab dərc edildi</string>
+ <string name="more_notifications">və %d daha çox.</string>
+ <string name="new_notifications">%d yeni bildirişlər</string>
+ <string name="follows">İzləmələr</string>
+ <string name="loading">Yüklənir...</string>
+ <string name="httpuser">HTTP istifadəçi adı</string>
+ <string name="httppassword">HTTP parolu</string>
+ <string name="error_media_upload">Media faylının yüklənməsi zamanı xəta baş verdi.</string>
+ <string name="content_description_add_media">Media əlavə et</string>
+ <string name="post_content">Mühtəviyyat (mətn və media əlavə etmək üçün toxunun)</string>
+ <string name="publish_date">Dərc edilmiş</string>
+ <string name="incorrect_credentials">Yanlış istifadəçi adı və ya parol</string>
+ <string name="password">Parol</string>
+ <string name="username">İstifadəçi adı</string>
+ <string name="reader">Oxuyucu</string>
+ <string name="anonymous">Anonim</string>
+ <string name="posts">Yazılar</string>
+ <string name="page">Səhifə</string>
+ <string name="post">Yazı</string>
+ <string name="no_network_title">Şəbəkə əlçatan deyil</string>
+ <string name="pages">Səhifələr</string>
+ <string name="caption">Başlıq(istəyə bağlı)</string>
+ <string name="width">genişlik</string>
+ <string name="featured">Fərdi təsvir olaraq istifadə edin</string>
+ <string name="featured_in_post">Təsviri yazı mətninə əlavə et</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">bloqistifadəçiadı</string>
+ <string name="upload_scaled_image">Ölçüləndirilmiş təsviri yüklə və bağlantı ver</string>
+ <string name="scaled_image">Mütənasib təsvirin genişliyi</string>
+ <string name="scheduled">Ertələndi</string>
+ <string name="link_enter_url">URL</string>
+ <string name="version">Versiya</string>
+ <string name="app_title">Android üçün WordPress</string>
+ <string name="tos">Xidmət Şərtləri</string>
+ <string name="image_alignment">Mövqeləndirmə</string>
+ <string name="refresh">Təzələ</string>
+ <string name="untitled">Başlıqsız</string>
+ <string name="edit">Redaktə</string>
+ <string name="post_id">Yazı</string>
+ <string name="page_id">Səhifə</string>
+ <string name="post_password">Parol(istəyə bağlı)</string>
+ <string name="immediately">Həmən</string>
+ <string name="quickpress_add_alert_title">Qısa yol ismi təyin et</string>
+ <string name="settings">Parametrlər</string>
+ <string name="share_url">URL paylaş</string>
+ <string name="quickpress_window_title">QuickPress qısa yolu üçün bloq seçin</string>
+ <string name="quickpress_add_error">Qısa yol ismi boş ola bilməz</string>
+ <string name="draft">Qaralama</string>
+ <string name="publish_post">Dərc et</string>
+ <string name="post_private">Özəl</string>
+ <string name="upload_full_size_image">Yüklə və tam təsvirə bağlantı ver</string>
+ <string name="categories">Kateqoriyalar</string>
+ <string name="tags_separate_with_commas">Etiketlər(etiketləri vergüllə ayırın)</string>
+ <string name="title">Başlıq</string>
+ <string name="notification_vibrate">Vibrasiya</string>
+ <string name="notification_blink">Məlumatlandırma işığını parlat</string>
+ <string name="status">Status</string>
+ <string name="select_video">Qalereyadan bir video seçin</string>
+ <string name="sdcard_title">SD Kart Gərəkdir</string>
+ <string name="location">Mövqe</string>
+ <string name="media">Media</string>
+ <string name="delete">Sil</string>
+ <string name="none">Heç biri</string>
+ <string name="blogs">Bloqlar</string>
+ <string name="select_photo">Qalereyadan bir foto seçin</string>
+ <string name="no">Xeyr</string>
+ <string name="yes">Bəli</string>
+ <string name="reply">Cavabla</string>
+ <string name="error">Xəta</string>
+ <string name="cancel">İmtina</string>
+ <string name="save">Qeyd et</string>
+ <string name="add">Əlavə et</string>
+ <string name="preview">Önizləmə</string>
+ <string name="category_refresh_error">Kateqoriya təzələnməsi əsnasında xəta baş verdi.</string>
+ <string name="on">tərəfndən</string>
+ <string name="notification_settings">Bildiriş Parametrləri</string>
+</resources>
diff --git a/WordPress/src/main/res/values-bg/strings.xml b/WordPress/src/main/res/values-bg/strings.xml
new file mode 100644
index 000000000..042689ce9
--- /dev/null
+++ b/WordPress/src/main/res/values-bg/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Администратор</string>
+ <string name="role_editor">Редактор</string>
+ <string name="role_author">Автор</string>
+ <string name="role_contributor">Сътрудник</string>
+ <string name="role_follower">Последовател</string>
+ <string name="role_viewer">Потребител</string>
+ <string name="error_post_my_profile_no_connection">Няма връзка, профилът не беше запазен</string>
+ <string name="alignment_none">Без</string>
+ <string name="alignment_left">Ляво</string>
+ <string name="alignment_right">Дясно</string>
+ <string name="site_settings_list_editor_action_mode_title">Избрано %1$d</string>
+ <string name="error_fetch_users_list">Неуспешно извикване на потребителите</string>
+ <string name="plans_manage">Управление на плана ви\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Все още нямате потребители.</string>
+ <string name="people_fetching">Търсене на потребителите...</string>
+ <string name="title_follower">Последовател</string>
+ <string name="title_email_follower">Абонат</string>
+ <string name="people_empty_list_filtered_email_followers">Все още нямате последователи по имейл.</string>
+ <string name="people_empty_list_filtered_followers">Все още нямате последователи.</string>
+ <string name="people_empty_list_filtered_users">Все още нямате потребители.</string>
+ <string name="people_dropdown_item_email_followers">Абонати</string>
+ <string name="people_dropdown_item_viewers">Потребители</string>
+ <string name="people_dropdown_item_followers">Последователи</string>
+ <string name="people_dropdown_item_team">Екип</string>
+ <string name="invite_message_usernames_limit">Можете да поканите до 10 души като въведете имейл адреси или техните потребителски имена в WordPress.com. Ако те нямат профили там, ще им бъдат изпратени инструкции как да си създадат.</string>
+ <string name="viewer_remove_confirmation_message">Потребителите няма да могат да посещават сайта ако ги премахнете от тук.\n\nЖелаете ли да продължите с премахването?</string>
+ <string name="follower_remove_confirmation_message">Последователите ще спрат да получават известия за сайта ако ги премахнете, освен ако не го добавят отново.\n\nЖелаете ли да продължите с премахването?</string>
+ <string name="follower_subscribed_since">От %1$s</string>
+ <string name="reader_label_view_gallery">Преглед</string>
+ <string name="error_remove_follower">Неуспешно премахване на последователя</string>
+ <string name="error_remove_viewer">Неуспешно премахване на потребителя</string>
+ <string name="error_fetch_email_followers_list">Неуспешно показване на последователите по имейл.</string>
+ <string name="error_fetch_followers_list">Неуспешно показване на последователите на сайта</string>
+ <string name="editor_failed_uploads_switch_html">Някои файлове не се качиха. Преминете в режим HTML\n на сайта. Премахване на всички неуспешно качени файлове и напред?</string>
+ <string name="format_bar_description_html">Режим HTML </string>
+ <string name="visual_editor">Визуален редактор</string>
+ <string name="image_thumbnail">Малка картинка</string>
+ <string name="format_bar_description_ul">Неподреден списък</string>
+ <string name="format_bar_description_ol">Подреден списък</string>
+ <string name="format_bar_description_more">Прекъсване на текста</string>
+ <string name="format_bar_description_media">Вмъкване на файл</string>
+ <string name="format_bar_description_strike">Зачертаване</string>
+ <string name="format_bar_description_quote">Цитат</string>
+ <string name="format_bar_description_link">Вмъкване на връзка</string>
+ <string name="format_bar_description_italic">Курсив</string>
+ <string name="format_bar_description_underline">Подчертаване</string>
+ <string name="image_settings_save_toast">Промените са запазени</string>
+ <string name="image_caption">Описание</string>
+ <string name="image_alt_text">Алт текст</string>
+ <string name="image_link_to">Връзка към</string>
+ <string name="image_width">Ширина</string>
+ <string name="format_bar_description_bold">Черно</string>
+ <string name="image_settings_dismiss_dialog_title">Отказ от промените?</string>
+ <string name="stop_upload_dialog_title">Спиране на качването?</string>
+ <string name="stop_upload_button">Спиране на качването</string>
+ <string name="alert_error_adding_media">Грешка при добавянето на файла</string>
+ <string name="alert_action_while_uploading">В момента качвате файл. Изчакайте докато процесът приключи.</string>
+ <string name="alert_insert_image_html_mode">Файлове не могат да се добавят в режим HTML. Превключете обратно към визуален режим.</string>
+ <string name="uploading_gallery_placeholder">Качване на галерията...</string>
+ <string name="invite_error_some_failed">Поканата беше изпратена, но възникнаха грешки! </string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Успешно изпращане на поканата</string>
+ <string name="tap_to_try_again">Тап за повторен опит</string>
+ <string name="invite_error_sending">Докато се изпращаше поканата възникна грешка!</string>
+ <string name="invite_error_invalid_usernames_multiple">Има невалидни имена или имейл адреси.</string>
+ <string name="invite_error_invalid_usernames_one">Неуспешно изпращане: Невалиден имейл адрес или име</string>
+ <string name="invite_error_no_usernames">Нужно е поне едно потребителско име</string>
+ <string name="invite_message_info">(По желание) Тук може да добавите съобщение до 500 символа, което ще бъде включено в поканата до потербителите.</string>
+ <string name="invite_message_remaining_other">Оставащи символи: %d</string>
+ <string name="invite_message_remaining_one">Оставащи символи: 1</string>
+ <string name="invite_message_remaining_zero">0 оставащи символи</string>
+ <string name="invite_invalid_email">Имейл адресът \'%s\' е невалиден</string>
+ <string name="invite_message_title">Съобщение</string>
+ <string name="invite_already_a_member">Вече съществува потребител с име \'%s\'</string>
+ <string name="invite_username_not_found">Няма потребител с име \'%s\'</string>
+ <string name="invite">Покана</string>
+ <string name="invite_names_title">Имена или имейл адреси</string>
+ <string name="signup_succeed_signin_failed">Профилът ви беше създаден, но възникна грешка при добавянето му.\n Опитайте да влезете с новите си име и парола.</string>
+ <string name="send_link">Изпращане на връзка</string>
+ <string name="my_site_header_external">Външна</string>
+ <string name="invite_people">Покана</string>
+ <string name="label_clear_search_history">Изчистване на историята на търсенето</string>
+ <string name="dlg_confirm_clear_search_history">Изчистване на историята на търсенето?</string>
+ <string name="reader_empty_posts_in_search_description">Няма намерени публикации за %s на вашия език</string>
+ <string name="reader_label_post_search_running">Търсене...</string>
+ <string name="reader_label_related_posts">Свързани статии</string>
+ <string name="reader_empty_posts_in_search_title">Няма намерени публикации</string>
+ <string name="reader_label_post_search_explainer">Търсене в публичните блогове на WordPress.com</string>
+ <string name="reader_hint_post_search">Търсене в WordPress.com</string>
+ <string name="reader_title_related_post_detail">Свързана публикация</string>
+ <string name="reader_title_search_results">Търсене за %s</string>
+ <string name="preview_screen_links_disabled">Връзките на екрана за преглед са деактивирани</string>
+ <string name="draft_explainer">Тази публикация е непубликувана чернова</string>
+ <string name="send">Изпращане</string>
+ <string name="user_remove_confirmation_message">Ако премахнете %1$s, потребителят вече няма да има достъп до сайта, но съдържанието, създадено от %1$s ще остане.\n\nЖелаете ли да продължите с премахването?</string>
+ <string name="person_removed">%1$s бе успешно премахнат</string>
+ <string name="person_remove_confirmation_title">Премахване на %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Няма нови публикации в сайтовете от този списък</string>
+ <string name="people">Хора</string>
+ <string name="edit_user">Редактиране на потребител</string>
+ <string name="role">Роля</string>
+ <string name="error_remove_user">Потребителят не може да бъде премахнат</string>
+ <string name="error_fetch_viewers_list">Грешка при зареждане на потребителите на сайта</string>
+ <string name="error_update_role">Грешка при обновяване на потребителската роля</string>
+ <string name="gravatar_camera_and_media_permission_required">Изисква се позволение за избор и добавяне на описание на снимка</string>
+ <string name="error_updating_gravatar">Грешка при обновяването на вашия аватар в Gravatar</string>
+ <string name="error_locating_image">Неуспешно откриване на изрязаното изображение</string>
+ <string name="error_refreshing_gravatar">Грешка при презареждането на вашия аватар в Gravatar</string>
+ <string name="gravatar_tip">Ново! Натиснете на Граватара си за промяна!</string>
+ <string name="error_cropping_image">Грешка при изрязване на изображението</string>
+ <string name="launch_your_email_app">Пускане на имейл приложението</string>
+ <string name="checking_email">Проверяване на имейл</string>
+ <string name="not_on_wordpress_com">Не сте на WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">В момента не е налично. Въведете вашата парола</string>
+ <string name="check_your_email">Проверете вашия имейл</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Изпратете си връзка по имейл за да влезете веднага</string>
+ <string name="logging_in">Влизане</string>
+ <string name="enter_your_password_instead">Въведете паролата си</string>
+ <string name="web_address_dialog_hint">Ще се показва когато коментирате.</string>
+ <string name="jetpack_not_connected_message">Разширенитео Jetpack е инсталирано, но не е свързано с WordPress.com. Желаете ли да свържете Jetpack?</string>
+ <string name="username_email">Имейл или потребителско име</string>
+ <string name="jetpack_not_connected">Разширението Jetpack не е свързано</string>
+ <string name="new_editor_reflection_error">Визуалният редактор не е съвместим с устройството ви. \n Беше изключен автоматично.</string>
+ <string name="stats_insights_latest_post_no_title">(без заглавие)</string>
+ <string name="capture_or_pick_photo">Заснемане или избор на снимка</string>
+ <string name="plans_post_purchase_text_themes">Вече имате неограничен достъп до премиум теми. Прегледайте която и да е тема на сайта си за да започнете.</string>
+ <string name="plans_post_purchase_button_themes">Разглеждане на теми</string>
+ <string name="plans_post_purchase_title_themes">Отркрийте перфектната премиум тема</string>
+ <string name="plans_post_purchase_button_video">Започване на нова публикация</string>
+ <string name="plans_post_purchase_text_video">С VideoPress и разширеното ви пространство за файлове можете да качвате и съхранявате видео.</string>
+ <string name="plans_post_purchase_title_video">Съживете публикациите си с видео</string>
+ <string name="plans_post_purchase_button_customize">Персонализиране на моя сайт</string>
+ <string name="plans_post_purchase_text_customize">Разполагате със собствени шрифтове и имате възможност да редактирате и добавяте собствен CSS.</string>
+ <string name="plans_post_purchase_text_intro">Сайтът ви подскача от щастие! Разходете се из новите добавки и изберете къде да започнете.</string>
+ <string name="plans_post_purchase_title_customize">Персонализиране на шрифтове и цветове</string>
+ <string name="plans_post_purchase_title_intro">Вие сте, давайте!</string>
+ <string name="export_your_content_message">Публикациите, страниците и настройките ви ще ви бъдат изпратени по имейл на %s.</string>
+ <string name="plan">План</string>
+ <string name="plans">Планове</string>
+ <string name="plans_loading_error">Неуспешно зареждане на плановете</string>
+ <string name="export_your_content">Експортиране на вашето съдържание</string>
+ <string name="exporting_content_progress">Експортиране...</string>
+ <string name="export_email_sent">Писмото за прехвърлянето е изпратено!</string>
+ <string name="premium_upgrades_message">Имате активни премиум ъпгрейди на сайта си. Отменете ги преди да изтриете сайта си.</string>
+ <string name="show_purchases">Покупки</string>
+ <string name="checking_purchases">Проверка на покупките</string>
+ <string name="premium_upgrades_title">Премиум ъпгрейди</string>
+ <string name="purchases_request_error">Нещо се счупи. Покупките не бяха заявени.</string>
+ <string name="delete_site_progress">Сайтът се изтрива...</string>
+ <string name="delete_site_summary">Това действие е необратимо. При изтриване на вашия сайт, всички домейни, сътрудници и съдържание ще бъдат премахнати.</string>
+ <string name="delete_site_hint">Изтриване на сайт</string>
+ <string name="export_site_hint">Експортиране на вашия сайт в XML файл</string>
+ <string name="are_you_sure">Сигурни ли сте?</string>
+ <string name="export_site_summary">Ако сте сигурни, отделете малко време и направете експорт на съдържанието си, за да го запазите. Няма да можете да го възстановите после.</string>
+ <string name="keep_your_content">Запазете съдържанието си</string>
+ <string name="domain_removal_hint">Домейните, които няма да работят ако премахнете сайта си</string>
+ <string name="domain_removal_summary">Внимание! Изтриването на вашия сайт ще премахне и долупосочените домейни.</string>
+ <string name="primary_domain">Основен домейн</string>
+ <string name="domain_removal">Премахване на домейн</string>
+ <string name="error_deleting_site_summary">Възникна грешка при изтриване на вашия сайт. Свържете се с отдела по поддръжката, за да получите помощ.</string>
+ <string name="error_deleting_site">Грешка при изтриване на сайта</string>
+ <string name="confirm_delete_site_prompt">Въведете %1$s в полето отдолу за финално потвърждение. След това сайтът ви ще бъде изтрит завинаги.</string>
+ <string name="site_settings_export_content_title">Експортиране на съдържание</string>
+ <string name="contact_support">Свързване с отдела по поддръжка</string>
+ <string name="confirm_delete_site">Потвърдете, че искате да изтриете сайта</string>
+ <string name="start_over_text">Ако искате сайт, но не искате съдържанието, което имате сега, екипът ни може да изтриете цялото ви съдържание.\n\nТова ще запази сайтът и адресът му активни, но ще ви даде възможност да започнете на чисто. Свържете се с нас, за да изчистим съдържанието.</string>
+ <string name="site_settings_start_over_hint">Започване отначало</string>
+ <string name="let_us_help">Нека помогнем</string>
+ <string name="me_btn_app_settings">Настройки на приложението</string>
+ <string name="start_over">Започване отначало</string>
+ <string name="editor_remove_failed_uploads">Премахване на неуспешно качените файлове</string>
+ <string name="editor_toast_failed_uploads">Някои файлове не се качиха. Не можете да\n запазите или публикувате в това състояние. Желаете ли да премахнете всички неуспешно качени файлове?</string>
+ <string name="comments_empty_list_filtered_trashed">Няма коментари в кошчето</string>
+ <string name="site_settings_advanced_header">Разширени</string>
+ <string name="comments_empty_list_filtered_pending">Няма коментари, очакващи одобрение</string>
+ <string name="comments_empty_list_filtered_approved">Няма одобрени коментари</string>
+ <string name="button_done">Готово</string>
+ <string name="button_skip">Пропускане</string>
+ <string name="site_timeout_error">Неуспешно свързване с WordPress поради изтекла връзка.</string>
+ <string name="xmlrpc_malformed_response_error">Грешка при свързването. Инсталацията на WordPress отговори с невалиден XML-RPC документ.</string>
+ <string name="xmlrpc_missing_method_error">Грешка при свързването. Задължителни XML-RPC методи липсват на този сървър.</string>
+ <string name="post_format_status">Състояние</string>
+ <string name="post_format_video">Видео клип</string>
+ <string name="alignment_center">В средата</string>
+ <string name="theme_free">Безплатна</string>
+ <string name="theme_all">Всички</string>
+ <string name="theme_premium">Премиум</string>
+ <string name="post_format_chat">Чат</string>
+ <string name="post_format_gallery">Галерия</string>
+ <string name="post_format_image">Изображение</string>
+ <string name="post_format_link">Връзка</string>
+ <string name="post_format_quote">Цитат</string>
+ <string name="post_format_standard">Стандартен</string>
+ <string name="notif_events">Информация за курсове и събития на WordPress.com (онлайн и на живо).</string>
+ <string name="post_format_aside">Бележка</string>
+ <string name="post_format_audio">Аудио</string>
+ <string name="notif_surveys">Възможност за участие в проучвания на WordPress.com.</string>
+ <string name="notif_tips">Съвети за използване на WordPress.com.</string>
+ <string name="notif_community">Общност</string>
+ <string name="replies_to_my_comments">Отговори на моите коментари</string>
+ <string name="notif_suggestions">Предложения</string>
+ <string name="notif_research">Проучване</string>
+ <string name="site_achievements">Постижения</string>
+ <string name="username_mentions">Споменавания</string>
+ <string name="likes_on_my_posts">Харесвания на моите публикации</string>
+ <string name="site_follows">Сайтът следва</string>
+ <string name="likes_on_my_comments">Харесвания на моите коментари</string>
+ <string name="comments_on_my_site">Коментари на моя сайт</string>
+ <string name="site_settings_list_editor_summary_other">%d елемента</string>
+ <string name="site_settings_list_editor_summary_one">1 елемент</string>
+ <string name="approve_auto_if_previously_approved">Коментари от познати потребители</string>
+ <string name="approve_auto">Всички потребители</string>
+ <string name="approve_manual">Няма коментари</string>
+ <string name="site_settings_paging_summary_other">%d коментара на страница</string>
+ <string name="site_settings_paging_summary_one">1 коментар на страница</string>
+ <string name="site_settings_multiple_links_summary_other">Задължително одобряване на поне %d връзки</string>
+ <string name="site_settings_multiple_links_summary_one">Задължително одобряване на поне 1 връзка</string>
+ <string name="site_settings_multiple_links_summary_zero">Задължително одобряване на повече от 0 връзки</string>
+ <string name="detail_approve_auto">Автоматично одобряване на всички коментари.</string>
+ <string name="detail_approve_auto_if_previously_approved">Автоматично одобряване ако потребителят има предишно одобрен коментар</string>
+ <string name="detail_approve_manual">Ръчно одобряване на всички коментари.</string>
+ <string name="filter_trashed_posts">В кошчето</string>
+ <string name="days_quantity_one">1 ден</string>
+ <string name="days_quantity_other">%d дни</string>
+ <string name="filter_published_posts">Публикувани</string>
+ <string name="filter_draft_posts">Чернови</string>
+ <string name="filter_scheduled_posts">Планирани</string>
+ <string name="pending_email_change_snackbar">Потвърдете имейл адреса си през връзката, която сте получили на %1$s</string>
+ <string name="primary_site">Основен сайт</string>
+ <string name="web_address">Адрес</string>
+ <string name="editor_toast_uploading_please_wait">В момента качвате файл. Изчакайте докато процесът приключи.</string>
+ <string name="error_refresh_comments_showing_older">Коментарите не могат да бъдат обновени в момента - показват се по-стари коментари</string>
+ <string name="editor_post_settings_set_featured_image">Задаване на основно изображение</string>
+ <string name="editor_post_settings_featured_image">Основно изображение</string>
+ <string name="new_editor_promo_desc">WordPress приложението за Андроид включва прекрасен нов визуален\nредактор. Пробвайте го като създадете нова публикация.</string>
+ <string name="new_editor_promo_title">Чисто нов редактор</string>
+ <string name="new_editor_promo_button_label">Чудесно, благодаря!</string>
+ <string name="visual_editor_enabled">Визуалният редактор е активиран</string>
+ <string name="editor_content_placeholder">Споделете вашата история тук...</string>
+ <string name="editor_page_title_placeholder">Заглавие на страницата</string>
+ <string name="editor_post_title_placeholder">Заглавие на публикация</string>
+ <string name="email_address">Имейл адрес</string>
+ <string name="preference_show_visual_editor">Показване на визуалния редактор</string>
+ <string name="dlg_sure_to_delete_comments">Изтриване на коментарите завинаги?</string>
+ <string name="preference_editor">Редактор</string>
+ <string name="dlg_sure_to_delete_comment">Изтриване на коментара завинаги?</string>
+ <string name="mnu_comment_delete_permanently">Изтриване</string>
+ <string name="comment_deleted_permanently">Коментарът е изтрит</string>
+ <string name="mnu_comment_untrash">Възстановяване</string>
+ <string name="comments_empty_list_filtered_spam">Няма спам коментари</string>
+ <string name="could_not_load_page">Неуспешно зареждане на страницата</string>
+ <string name="comment_status_all">Всички</string>
+ <string name="interface_language">Език на интерфейса</string>
+ <string name="off">Изключено</string>
+ <string name="about_the_app">За приложението</string>
+ <string name="error_post_account_settings">Грешка при запазване на вашите профилни настройки</string>
+ <string name="error_post_my_profile">Грешка при запазване на вашия профил</string>
+ <string name="error_fetch_account_settings">Грешка при зареждане на вашите профилни настройки</string>
+ <string name="error_fetch_my_profile">Грешка при зареждане на вашия профил</string>
+ <string name="stats_widget_promo_ok_btn_label">Разбрах</string>
+ <string name="stats_widget_promo_desc">Добавете връзка към началния си екран за по-лесен достъп до статистиката.</string>
+ <string name="stats_widget_promo_title">Статистика на начален екран</string>
+ <string name="site_settings_unknown_language_code_error">Неразпознат код на езика</string>
+ <string name="site_settings_threading_dialog_description">Позволяване на подреждането на коментарите дървовидно.</string>
+ <string name="site_settings_threading_dialog_header">Подкоментари до</string>
+ <string name="remove">Премахване</string>
+ <string name="search">Търсене</string>
+ <string name="add_category">Добавяне на категория</string>
+ <string name="disabled">Деактивирано</string>
+ <string name="site_settings_image_original_size">Оригинален размер</string>
+ <string name="privacy_private">Вашият сайт е видим за вас и одобрените от вас потребители</string>
+ <string name="privacy_public_not_indexed">Сайтът ви се вижда от всички, но не позволява на търсачките да го индексират</string>
+ <string name="privacy_public">Вашият сайт е видим от всички и може да бъде индексиран от търсачките</string>
+ <string name="about_me_hint">Няколко думи за вас...</string>
+ <string name="public_display_name_hint">Потребителското ви име ще стане официално име ако такова не е специално зададено</string>
+ <string name="about_me">За мен</string>
+ <string name="public_display_name">Публично име</string>
+ <string name="my_profile">Моят профил</string>
+ <string name="first_name">Собствено име</string>
+ <string name="last_name">Фамилия</string>
+ <string name="site_privacy_public_desc">Позволяване на търсещите машини да индексират сайта</string>
+ <string name="site_privacy_hidden_desc">Не позволявайте на търсещите машини да индексират този сайт</string>
+ <string name="site_privacy_private_desc">Искам сайтът ми да се вижда само от потребители, които аз избера</string>
+ <string name="cd_related_post_preview_image">Изображение на свързаната публикация</string>
+ <string name="error_post_remote_site_settings">Грешка при запазване на информацията за сайта</string>
+ <string name="error_fetch_remote_site_settings">Грешка при зареждане на информацията за сайта</string>
+ <string name="error_media_upload_connection">Връзката изчезна докато файловете се качваха</string>
+ <string name="site_settings_disconnected_toast">Няма връзка, редактирането е изключено.</string>
+ <string name="site_settings_unsupported_version_error">Тази версия на WordPress не се поддържа</string>
+ <string name="site_settings_multiple_links_dialog_description">Коментарите с повече от този брой връзки да изискват специално одобрение.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Автоматично затваряне</string>
+ <string name="site_settings_close_after_dialog_description">Автоматично затваряне на коментарите на публикациите.</string>
+ <string name="site_settings_paging_dialog_description">Разделяне на коментарите на няколко страници.</string>
+ <string name="site_settings_paging_dialog_header">Коментари на страница</string>
+ <string name="site_settings_close_after_dialog_title">Затваряне на коментарите</string>
+ <string name="site_settings_blacklist_description">Когато коментар съдържа която и да е от тези думи в съдържанието, името, адрес или IP адреса си, той ще бъде отбелязан като спам. Частични думи също работят, може да въведете „press“, което ще съвпадне с „WordPress“.</string>
+ <string name="site_settings_hold_for_moderation_description">Когато коментар съдържа която и да е от тези думи в съдържанието, името, адрес или IP адреса си, той ще бъде изпращан за модерация. Частични думи също работят, може да въведете „press“, което ще съвпадне с „WordPress“.</string>
+ <string name="site_settings_list_editor_input_hint">Въведете дума или фраза</string>
+ <string name="site_settings_list_editor_no_items_text">Няма елементи</string>
+ <string name="site_settings_learn_more_caption">Можете да презаписвате тези настройки за отделните публикации.</string>
+ <string name="site_settings_rp_preview3_site">в "Обновяване"</string>
+ <string name="site_settings_rp_preview3_title">Фокус на промяната: Видео Прес за сватби</string>
+ <string name="site_settings_rp_preview2_site">в "Приложения"</string>
+ <string name="site_settings_rp_preview2_title">Подобрения в приложението на WordPress за Андроид</string>
+ <string name="site_settings_rp_preview1_site">в "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Налични са големи подобрения за iPhone/iPad</string>
+ <string name="site_settings_rp_show_images_title">Показване на изображения</string>
+ <string name="site_settings_rp_show_header_title">Показване на заглавката</string>
+ <string name="site_settings_rp_switch_summary">Показва подобно съдържание от вашия сайт под вашите публикации.</string>
+ <string name="site_settings_rp_switch_title">Показване на подобни публикации</string>
+ <string name="site_settings_delete_site_hint">Премахва данните за вашите сайтове от приложението</string>
+ <string name="site_settings_blacklist_hint">Коментарите, които съвпаднат с термин за филтриране, ще бъдат отбелязани като спам.</string>
+ <string name="site_settings_moderation_hold_hint">Коментарите, които отговарят на този филтър, ще бъдат добавени в опашката за модериране.</string>
+ <string name="site_settings_multiple_links_hint">Игнорира лимита за връзки от познати потребители</string>
+ <string name="site_settings_whitelist_hint">Авторът на коментара трябва да има поне един одобрен коментар</string>
+ <string name="site_settings_user_account_required_hint">Потребителите трябва да са регистрирани и влезли за да коментират</string>
+ <string name="site_settings_identity_required_hint">Авторът на коментара трябва да попълни своите име и имейл</string>
+ <string name="site_settings_manual_approval_hint">Коментарите трябва да бъдат ръчно одобрявани</string>
+ <string name="site_settings_paging_hint">Показване на коментарите в части с определен размер</string>
+ <string name="site_settings_threading_hint">Позволяване на ограничен брой поднива на коментарите</string>
+ <string name="site_settings_sort_by_hint">Определя реда на извеждане на коментарите</string>
+ <string name="site_settings_close_after_hint">Изключване на коментарите след посоченото време</string>
+ <string name="site_settings_receive_pingbacks_hint">Позволяване на известия за връзки от други блогове</string>
+ <string name="site_settings_send_pingbacks_hint">Опит за известие на блоговете, към които има връзки в този материал.</string>
+ <string name="site_settings_allow_comments_hint">Позволяване коментарите да бъдат четени</string>
+ <string name="site_settings_discussion_hint">Разглеждане и промяна на настройките за коментари на вашите сайтове</string>
+ <string name="site_settings_more_hint">Разглеждане на всички настройки за коментарите</string>
+ <string name="site_settings_related_posts_hint">Показване или скриване на свързани публикации в четеца</string>
+ <string name="site_settings_upload_and_link_image_hint">Включете, за да качвате файловете в пълен размер винаги.</string>
+ <string name="site_settings_image_width_hint">Преоразмерява изображенията в публикациите до тази ширина</string>
+ <string name="site_settings_format_hint">Задава нов формат на публикацията</string>
+ <string name="site_settings_category_hint">Задава нова категория на публикацията</string>
+ <string name="site_settings_location_hint">Автоматично добавяне на данни за местоположение към вашите публикации</string>
+ <string name="site_settings_password_hint">Промяна на вашата парола</string>
+ <string name="site_settings_username_hint">Текущ потребителски профил</string>
+ <string name="site_settings_language_hint">Основен език на блога</string>
+ <string name="site_settings_privacy_hint">Контролира кой може да вижда вашия сайт</string>
+ <string name="site_settings_address_hint">В момента не можете да променяте вашия адрес</string>
+ <string name="site_settings_tagline_hint">Кратък текст или привлекателна фраза, описваща вашия блог</string>
+ <string name="site_settings_title_hint">Описание на сайта</string>
+ <string name="site_settings_whitelist_known_summary">Коментари от познати потребители</string>
+ <string name="site_settings_whitelist_all_summary">Коментари от всички потребители</string>
+ <string name="site_settings_threading_summary">%d нива</string>
+ <string name="site_settings_privacy_private_summary">Частен</string>
+ <string name="site_settings_privacy_hidden_summary">Скрит</string>
+ <string name="site_settings_delete_site_title">Изтриване на сайт</string>
+ <string name="site_settings_privacy_public_summary">Публичен</string>
+ <string name="site_settings_blacklist_title">Черен списък</string>
+ <string name="site_settings_moderation_hold_title">За модерация</string>
+ <string name="site_settings_multiple_links_title">Връзки в коментарите</string>
+ <string name="site_settings_whitelist_title">Автоматично одобряване</string>
+ <string name="site_settings_threading_title">Опашка</string>
+ <string name="site_settings_paging_title">Страниране</string>
+ <string name="site_settings_sort_by_title">Сортиране по</string>
+ <string name="site_settings_account_required_title">Потребителите трябва да са влезли</string>
+ <string name="site_settings_identity_required_title">Нужни са име и имейл адрес</string>
+ <string name="site_settings_receive_pingbacks_title">Получаване на pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Изпращане на pingbacks</string>
+ <string name="site_settings_allow_comments_title">Включване на коментарите</string>
+ <string name="site_settings_default_format_title">Формат по подразбиране</string>
+ <string name="site_settings_default_category_title">Категория по подразбиране</string>
+ <string name="site_settings_location_title">Включване на местоположение</string>
+ <string name="site_settings_address_title">Адрес</string>
+ <string name="site_settings_title_title">Заглавие на сайта</string>
+ <string name="site_settings_tagline_title">Кратко описание</string>
+ <string name="site_settings_this_device_header">Това устройство</string>
+ <string name="site_settings_discussion_new_posts_header">По подразбиране за нови публикации</string>
+ <string name="site_settings_account_header">Профил</string>
+ <string name="site_settings_writing_header">Писане</string>
+ <string name="newest_first">Първо най-новите</string>
+ <string name="site_settings_general_header">Общи</string>
+ <string name="discussion">Дискусия</string>
+ <string name="privacy">Поверителност</string>
+ <string name="related_posts">Подобни публикации</string>
+ <string name="comments">Коментари</string>
+ <string name="close_after">Затваряне след</string>
+ <string name="oldest_first">Първо най-старите</string>
+ <string name="media_error_no_permission_upload">Нямате права да качвате файлове в този сайт</string>
+ <string name="never">Никога</string>
+ <string name="unknown">Неизвестно</string>
+ <string name="reader_err_get_post_not_found">Тази публикация вече не съществува</string>
+ <string name="reader_err_get_post_not_authorized">Нямате права да четете тази публикация</string>
+ <string name="reader_err_get_post_generic">Неуспешно отваряне на публикацията</string>
+ <string name="blog_name_no_spaced_allowed">Адресът на сайта не може да съдържа интервали</string>
+ <string name="invalid_username_no_spaces">Потребителското име не може да съдържа интервали</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Сайтовете, които следвате, нямат нови публикации</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Няма скорошни публикации</string>
+ <string name="media_details_copy_url_toast">Адресът е копиран </string>
+ <string name="edit_media">Редактиране на файл</string>
+ <string name="media_details_copy_url">Копиране на URL адрес</string>
+ <string name="media_details_label_date_uploaded">Дата на качване</string>
+ <string name="media_details_label_date_added">Добавено</string>
+ <string name="selected_theme">Избрана тема</string>
+ <string name="could_not_load_theme">Неуспешно зареждане на темата</string>
+ <string name="theme_activation_error">Нещо се обърка при активирането на темата.</string>
+ <string name="theme_by_author_prompt_append"> от %1$s</string>
+ <string name="theme_prompt">Благодарим ви че избрахте %1$s</string>
+ <string name="theme_try_and_customize">Насройки</string>
+ <string name="theme_view">Преглед</string>
+ <string name="theme_details">Детайли</string>
+ <string name="theme_support">Поддръжка</string>
+ <string name="theme_done">ГОТОВО</string>
+ <string name="theme_manage_site">УПРАВЛЕНИЕ НА САЙТ</string>
+ <string name="title_activity_theme_support">Теми</string>
+ <string name="theme_activate">Активиране</string>
+ <string name="date_range_start_date">Начална дата</string>
+ <string name="date_range_end_date">Крайна дата</string>
+ <string name="current_theme">Текуща тема</string>
+ <string name="customize">Персонализиране</string>
+ <string name="details">Детайли</string>
+ <string name="support">Поддръжка</string>
+ <string name="active">Активен</string>
+ <string name="stats_referrers_spam_generic_error">Нещо се обърка. Спам статусът е без промяна.</string>
+ <string name="stats_referrers_marking_not_spam">Маркиране като не-спам</string>
+ <string name="stats_referrers_unspam">Не е спам</string>
+ <string name="stats_referrers_marking_spam">Маркиране като спам</string>
+ <string name="theme_auth_error_authenticate">Неуспешно намиране на тема: проблем с разпознаването на потребителя</string>
+ <string name="post_published">Публикацията е публикувана</string>
+ <string name="page_published">Страницата е публикувана</string>
+ <string name="post_updated">Публикацията е обновена</string>
+ <string name="page_updated">Страницата бе обновена</string>
+ <string name="stats_referrers_spam">Спам</string>
+ <string name="theme_no_search_result_found">Няма открити теми.</string>
+ <string name="media_file_name">Име на файл: %s</string>
+ <string name="media_uploaded_on">Дата на качване: %s</string>
+ <string name="media_dimensions">Размери: %s</string>
+ <string name="upload_queued">В опашката</string>
+ <string name="media_file_type">Тип на файл: %s</string>
+ <string name="reader_label_gap_marker">Зареждане на още публикации</string>
+ <string name="notifications_no_search_results">Нито един сайт няма \'%s\'</string>
+ <string name="search_sites">Търсене в сайтовете</string>
+ <string name="notifications_empty_view_reader">Преглед на Reader</string>
+ <string name="unread">Непрочетени</string>
+ <string name="notifications_empty_action_followers_likes">Нека ви забележат: коментирайте прочетени от вас публикации.</string>
+ <string name="notifications_empty_action_comments">Присъединяване към дискусия: обсъждайте публикации в следваните от вас блогове.</string>
+ <string name="notifications_empty_action_unread">Раздвижете комуникацията чрез нова публикация.</string>
+ <string name="notifications_empty_action_all">Бъдете активни! Коментирайте публикации на следените от вас блогове.</string>
+ <string name="notifications_empty_likes">Все още няма нови харесвания.</string>
+ <string name="notifications_empty_followers">Все още нямате нови последователи.</string>
+ <string name="notifications_empty_comments">Все още нямате нови коментари.</string>
+ <string name="notifications_empty_unread">Всичко е наваксано!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Пробвайте да достъпите статистическите данните в приложението, и по-късно опитайте да добавите джаджата.</string>
+ <string name="stats_widget_error_readd_widget">Джаджата трябва да бъде премахната и добавена отново.</string>
+ <string name="stats_widget_error_no_visible_blog">Данните не са достъпни без видим блог.</string>
+ <string name="stats_widget_error_no_permissions">Профилът ви в WordPress.com не дава достъп до данните в този блог </string>
+ <string name="stats_widget_error_no_account">Влезте във WordPress</string>
+ <string name="stats_widget_error_generic">Данните не могат да бъдат заредени</string>
+ <string name="stats_widget_loading_data">Зареждане на данните...</string>
+ <string name="stats_widget_name_for_blog">Днешните данни за %1$s</string>
+ <string name="stats_widget_name">Днешните данни за WordPress</string>
+ <string name="add_location_permission_required">Изисква се разрешение, за да добавите местоположение</string>
+ <string name="add_media_permission_required">Изискват се разрешения, за да добавите файл</string>
+ <string name="access_media_permission_required">Изискват се разрешения, за да имате достъп до файловете</string>
+ <string name="stats_enable_rest_api_in_jetpack">За да видите статистиката, то активирайте модула JSON API в Jetpack.</string>
+ <string name="error_open_list_from_notification">Публикацията или страницата бяха публикувани на друг сайт</string>
+ <string name="reader_short_comment_count_multi">%s коментара</string>
+ <string name="reader_short_comment_count_one">1 коментар</string>
+ <string name="reader_label_submit_comment">ИЗПРАЩАНЕ</string>
+ <string name="reader_hint_comment_on_post">Отговор на публикацията...</string>
+ <string name="reader_discover_visit_blog">Посещаване на %s</string>
+ <string name="reader_discover_attribution_blog">Оригинална публикация на %s</string>
+ <string name="reader_discover_attribution_author">Оригинално публикувано от %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Оригинално публикувано от %1$s на %2$s</string>
+ <string name="reader_short_like_count_multi">%s харесвания</string>
+ <string name="reader_short_like_count_one">1 харесване</string>
+ <string name="reader_label_follow_count">%,d последователи</string>
+ <string name="reader_short_like_count_none">Харесване</string>
+ <string name="reader_menu_tags">Редактиране на етикети и блогове</string>
+ <string name="reader_title_post_detail">Публикации в Reader</string>
+ <string name="local_draft_explainer">Публикацията е локална чернова, която не е публикувана</string>
+ <string name="local_changes_explainer">Публикацията има локални промени, които не са публикувани</string>
+ <string name="notifications_push_summary">Настройки на известявания, които се появяват на вашето устройство</string>
+ <string name="notifications_email_summary">Настройки за известяванията, които се изпращат на пощата асоциирана с вашия профил.</string>
+ <string name="notifications_tab_summary">Настройки за известяванията, които се появяват в подпрозореца "Известявания"</string>
+ <string name="notifications_disabled">Известяванията са спрени. Натиснете тук, за да ги активирате.</string>
+ <string name="notification_types">Видове известия</string>
+ <string name="error_loading_notifications">Настройките за известяванията не могат да бъдат заредени</string>
+ <string name="replies_to_your_comments">Отговори на вашите коментари</string>
+ <string name="comment_likes">Харесвания на коментари</string>
+ <string name="app_notifications">Известия на приложението</string>
+ <string name="notifications_tab">Подпрозорец за известия</string>
+ <string name="email">Ел. поща</string>
+ <string name="notifications_comments_other_blogs">Коментари на други сайтове</string>
+ <string name="notifications_wpcom_updates">Актуализации за WordPress.com</string>
+ <string name="notifications_other">Други</string>
+ <string name="notifications_account_emails">Ел. поща от WordPress.com</string>
+ <string name="notifications_account_emails_summary">Винаги изпращаме писма, съдържащи важна информация относно вашия профил. Но също така известяваме и за полезни екстри.</string>
+ <string name="notifications_sights_and_sounds">Гледки и звуци</string>
+ <string name="your_sites">Вашите сайтове</string>
+ <string name="stats_insights_latest_post_trend">Мина %1$s от публикуването на %2$s. Ето как се представи публикацията досега...</string>
+ <string name="stats_insights_latest_post_summary">Обзор на последната публикация</string>
+ <string name="button_revert">Връщане</string>
+ <string name="days_ago">преди %d дни</string>
+ <string name="yesterday">Вчера</string>
+ <string name="connectionbar_no_connection">Няма връзка</string>
+ <string name="page_trashed">Страницата е изпратена към кошчето</string>
+ <string name="post_deleted">Публикацията е изтрита</string>
+ <string name="post_trashed">Публиказията е изпратена към кошчето</string>
+ <string name="stats_no_activity_this_period">За този период няма дейности</string>
+ <string name="trashed">Изхвърлени</string>
+ <string name="button_back">Назад</string>
+ <string name="page_deleted">Страницата е изтрита</string>
+ <string name="button_stats">Статистика</string>
+ <string name="button_trash">Кошче</string>
+ <string name="button_preview">Преглед</string>
+ <string name="button_view">Изглед</string>
+ <string name="button_edit">Редактиране</string>
+ <string name="button_publish">Публикуване</string>
+ <string name="my_site_no_sites_view_subtitle">Добавяне на един?</string>
+ <string name="my_site_no_sites_view_title">Все още нямате сайтове на WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Илюстрация</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Нямате право да достъпвате този блог</string>
+ <string name="reader_toast_err_follow_blog_not_found">Блогът не може да бъде намерен</string>
+ <string name="undo">Отмяна</string>
+ <string name="tabbar_accessibility_label_my_site">Моят сайт</string>
+ <string name="tabbar_accessibility_label_me">Аз</string>
+ <string name="passcodelock_prompt_message">Въведете вашия ПИН</string>
+ <string name="editor_toast_changes_saved">Промените са запазени</string>
+ <string name="push_auth_expired">Заявка е изтекла. Влезте в WordPress.com и опитайте отново.</string>
+ <string name="stats_insights_best_ever">Най-добри преглеждания</string>
+ <string name="ignore">Игнориране</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% преглеждания</string>
+ <string name="stats_insights_most_popular_hour">Най-популярен час</string>
+ <string name="stats_insights_most_popular_day">Най-популярен ден</string>
+ <string name="stats_insights_popular">Най-популярен ден и час</string>
+ <string name="stats_insights_today">Статистики от днес</string>
+ <string name="stats_insights_all_time">Всички публикации, преглеждания и посетители</string>
+ <string name="stats_insights">Данни</string>
+ <string name="stats_sign_in_jetpack_different_com_account">За да прегледате статистиката, влезте в профила си в WordPress.com, който използвате за връзка с Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Търсите другите скорошни статистики? Преместихме ги в страницата за проникновения.</string>
+ <string name="me_disconnect_from_wordpress_com">Прекъсване на връзката с WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Свързване с WordPress.com</string>
+ <string name="me_btn_login_logout">Вход/Изход</string>
+ <string name="account_settings">Настройки на профила</string>
+ <string name="me_btn_support">Помощ и поддръжка</string>
+ <string name="site_picker_cant_hide_current_site">"%s" не беше скрит защото е текущият уебсайт</string>
+ <string name="site_picker_create_dotcom">Създаване на WordPress.com уебсайт</string>
+ <string name="site_picker_add_site">Добавяне на сайт</string>
+ <string name="site_picker_add_self_hosted">Добавяне на самостоятелно хостван уебсайт</string>
+ <string name="site_picker_edit_visibility">Показване/скриване на сайтовете</string>
+ <string name="my_site_btn_view_admin">Преглед на администрацията</string>
+ <string name="my_site_btn_view_site">Преглед на сайта</string>
+ <string name="site_picker_title">Избор на сайт</string>
+ <string name="my_site_btn_switch_site">Превключване на сайта</string>
+ <string name="my_site_btn_blog_posts">Публикации</string>
+ <string name="my_site_btn_site_settings">Настройки</string>
+ <string name="my_site_header_look_and_feel">Изглед</string>
+ <string name="my_site_header_publish">Публикуване</string>
+ <string name="my_site_header_configuration">Настройки</string>
+ <string name="reader_label_new_posts_subtitle">Натиснете за да ги прегледате</string>
+ <string name="notifications_account_required">За да прегледате вашите известявания влезте в WordPress.com</string>
+ <string name="stats_unknown_author">Непознат автор</string>
+ <string name="image_added">Снимката е добавена</string>
+ <string name="signout">Изход</string>
+ <string name="deselect_all">От-маркиране на всичко</string>
+ <string name="show">Показване</string>
+ <string name="hide">Скриване</string>
+ <string name="select_all">Избор на всичко</string>
+ <string name="sign_out_wpcom_confirm">Излизането от профила ще премахне вските данни на @%s в WordPress.com от това устройство. Включително локалните чернови и локалните промени.</string>
+ <string name="select_from_new_picker">Множествен избор с новия инструмен за избор</string>
+ <string name="stats_generic_error">Желаната статистина не може да бъде заредена</string>
+ <string name="no_device_videos">Няма налични видеа</string>
+ <string name="no_blog_images">Няма налични изображения</string>
+ <string name="no_blog_videos">Няма налични видеа</string>
+ <string name="no_device_images">Няма налични изображения</string>
+ <string name="error_loading_blog_images">Извличането на изображенията е неуспешно</string>
+ <string name="error_loading_blog_videos">Извличането на видеата е неуспешно</string>
+ <string name="error_loading_images">Грешка при зареждане на изображенията</string>
+ <string name="error_loading_videos">Грешка при зареждане на видеата</string>
+ <string name="loading_blog_images">Извличане на изображения</string>
+ <string name="loading_blog_videos">Извличане на видеа</string>
+ <string name="no_media_sources">Извличането на медиата е неуспешно</string>
+ <string name="loading_videos">Видеата се зареждат</string>
+ <string name="loading_images">Зареждане на изображенията</string>
+ <string name="no_media">Няма файлове</string>
+ <string name="device">Устройство</string>
+ <string name="language">Език</string>
+ <string name="add_to_post">Добавяне към публикацията</string>
+ <string name="media_picker_title">Избор на файлове</string>
+ <string name="take_photo">Снимане</string>
+ <string name="take_video">Запис на видео</string>
+ <string name="tab_title_device_images">Снимки от устройството</string>
+ <string name="tab_title_device_videos">Видеа от устройството</string>
+ <string name="tab_title_site_images">Снимки на уебсайта</string>
+ <string name="tab_title_site_videos">Видеа в уебсайта</string>
+ <string name="media_details_label_file_name">Име на файл</string>
+ <string name="media_details_label_file_type">Тип на файла</string>
+ <string name="error_publish_no_network">Не може да бъде публикувано когато няма връзка с интернет. Запазено в чернови.</string>
+ <string name="editor_toast_invalid_path">Невалиден път до файла</string>
+ <string name="verification_code">Верификационен код</string>
+ <string name="invalid_verification_code">Невалиден верификационен код</string>
+ <string name="verify">Верифициране</string>
+ <string name="two_step_footer_label">Въведете кода, предоставен от приложението за оторизиране</string>
+ <string name="two_step_footer_button">Изпращане на кода чрез текстово съобщение</string>
+ <string name="two_step_sms_sent">Проверете текстовите ви съобщения за верификационния код</string>
+ <string name="sign_in_jetpack">За да се свържете с Jetpack влезте с WordPress.com акаунт.</string>
+ <string name="auth_required">За да продължите влезте отново.</string>
+ <string name="reader_empty_posts_request_failed">Възникна грешка при извличане на публикациите</string>
+ <string name="publisher">Издател:</string>
+ <string name="error_notification_open">Уведомлението не може да бъде отворено</string>
+ <string name="stats_followers_total_email_paged">Показване на %1$d - %2$d от %3$s email последователи</string>
+ <string name="stats_search_terms_unknown_search_terms">Непознати термини за търсене</string>
+ <string name="stats_followers_total_wpcom_paged">Показване на %1$d - %2$d от %3$s WordPress.com последователи</string>
+ <string name="stats_empty_search_terms_desc">Преглед на ключовите думи, които потребителите използват в търсачките, за да намират вашия сайт</string>
+ <string name="stats_empty_search_terms">Няма записани ключови думи за търсене</string>
+ <string name="stats_entry_search_terms">Ключова дума</string>
+ <string name="stats_view_authors">Автори</string>
+ <string name="stats_view_search_terms">Ключови думи</string>
+ <string name="comments_fetching">Извличане на коментари...</string>
+ <string name="pages_fetching">Извличане на страници...</string>
+ <string name="toast_err_post_uploading">Публикацията не може да бъде отворена докато се качва</string>
+ <string name="posts_fetching">Извличане на публикации...</string>
+ <string name="media_fetching">Извличане на файлове...</string>
+ <string name="post_uploading">Качване</string>
+ <string name="stats_total">Общо</string>
+ <string name="stats_overall">Общо</string>
+ <string name="stats_period">Период</string>
+ <string name="logs_copied_to_clipboard">Логовете на приложението бяха копирани в буфера за обмен на данни</string>
+ <string name="reader_label_new_posts">Нови публикации</string>
+ <string name="reader_empty_posts_in_blog">Този блог е празен</string>
+ <string name="stats_average_per_day">Средно на ден</string>
+ <string name="stats_recent_weeks">Последните седмици</string>
+ <string name="error_copy_to_clipboard">Възникна грешка при копиране на текста в clipboard-а</string>
+ <string name="reader_page_recommended_blogs">Блогове които бихте харесали</string>
+ <string name="stats_months_and_years">Месеци и години</string>
+ <string name="themes_fetching">Извличане на теми...</string>
+ <string name="stats_for">Статистика за %s</string>
+ <string name="stats_other_recent_stats_label">Други скорошни статистики</string>
+ <string name="stats_view_all">Преглед на всички</string>
+ <string name="stats_view">Преглеждания</string>
+ <string name="stats_followers_months">%1$d месеца</string>
+ <string name="stats_followers_a_year">Година</string>
+ <string name="stats_followers_years">%1$d години</string>
+ <string name="stats_followers_a_month">Месец</string>
+ <string name="stats_followers_minutes">%1$d минути</string>
+ <string name="stats_followers_an_hour_ago">преди час</string>
+ <string name="stats_followers_hours">%1$d часа</string>
+ <string name="stats_followers_a_day">Ден</string>
+ <string name="stats_followers_days">%1$d дни</string>
+ <string name="stats_followers_a_minute_ago">преди минута</string>
+ <string name="stats_followers_seconds_ago">секунди</string>
+ <string name="stats_followers_total_email">Всички последователи от пощата: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Електронна поща</string>
+ <string name="stats_followers_total_wpcom">Общ брой на последователите от WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Всички публикации със последователи в коментарите: %1$s</string>
+ <string name="stats_comments_by_authors">От автори</string>
+ <string name="stats_comments_by_posts_and_pages">От публикации и страници</string>
+ <string name="stats_empty_followers_desc">Следете бройката на вашите последователи и колко дълго всеки един от тях е следва вашия сайт.</string>
+ <string name="stats_empty_followers">Няма последователи</string>
+ <string name="stats_empty_publicize_desc">Следете вашите последователи от различни социални мрежи чрез publicize.</string>
+ <string name="stats_empty_publicize">Няма регистрирани последователи чрез publicize</string>
+ <string name="stats_empty_video">Няма преглеждани видеа</string>
+ <string name="stats_empty_video_desc">Ако сте качили видеа през VideoPress можете да разберете колко пъти са били гледани.</string>
+ <string name="stats_empty_comments_desc">В случай, че разрешавате коментари на вашия сайт, от тук може да проследите топ коментаторите, а също така да намерите съдържанието, което провокира най-живите разговор. Статистиката е базирана на последните 1000 коментара.</string>
+ <string name="stats_empty_tags_and_categories_desc">Обзор на най-популярните теми на вашия сайт, в резултат на вашите топ публикации от миналата седмица.</string>
+ <string name="stats_empty_top_authors_desc">От тук може да проследите преглежданията на публикациите на всеки автор, а също така можете да откриете най-популярното съдържание от всеки автор.</string>
+ <string name="stats_empty_tags_and_categories">Няма гледани тагнати публикации или страници</string>
+ <string name="stats_empty_clicks_desc">В случай, че вашите публикации съдържат връзки към други сайтове, ще виждате върху кои връзки потребителите натискат най-често.</string>
+ <string name="stats_empty_referrers_desc">За да научите повече за видимостта на вашия сайт, то следете уебсайтовете и търсачките, които ви изпращат най-много посещения</string>
+ <string name="stats_empty_clicks_title">Няма записани натискания</string>
+ <string name="stats_empty_referrers_title">Няма отчетени препращащи сайтове</string>
+ <string name="stats_empty_top_posts_title">Няма гледани публикации или страници</string>
+ <string name="stats_empty_top_posts_desc">Разберете кое е най-гледаното ви съдържание и вижте как индивидуалните публикации и страници се представят с течение на времето.</string>
+ <string name="stats_totals_followers">От</string>
+ <string name="stats_empty_geoviews">Няма записани държави</string>
+ <string name="stats_empty_geoviews_desc">Разгледайте списъка за да разберете кои страни и региони генерират най-много трафик към Вашия уебсайт.</string>
+ <string name="stats_entry_video_plays">Видео</string>
+ <string name="stats_entry_top_commenter">Автор</string>
+ <string name="stats_entry_publicize">Услуга</string>
+ <string name="stats_entry_followers">Последовател</string>
+ <string name="stats_totals_publicize">Последователи</string>
+ <string name="stats_entry_clicks_link">Връзка</string>
+ <string name="stats_view_top_posts_and_pages">Публикации и страници</string>
+ <string name="stats_view_videos">Видеа</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Последователи</string>
+ <string name="stats_view_countries">Държави</string>
+ <string name="stats_likes">Харесвания</string>
+ <string name="stats_pagination_label">Страница %1$s от %2$s</string>
+ <string name="stats_timeframe_years">Години</string>
+ <string name="stats_views">Преглеждания</string>
+ <string name="stats_visitors">Посетители</string>
+ <string name="ssl_certificate_details">Информация</string>
+ <string name="delete_sure_post">Изтриване на публикацията</string>
+ <string name="delete_sure">Изтриване на черновата</string>
+ <string name="delete_sure_page">Изтриване на страницата</string>
+ <string name="confirm_delete_multi_media">Изтриване на избраните елементи?</string>
+ <string name="confirm_delete_media">Изтриване на отбелязания елемент?</string>
+ <string name="cab_selected">%d избрани</string>
+ <string name="media_gallery_date_range">Показване на файл от %1$s до %2$s</string>
+ <string name="sure_to_remove_account">Изтриване на този уебсайт?</string>
+ <string name="reader_empty_followed_blogs_title">Все още не следвате блогове</string>
+ <string name="reader_empty_posts_liked">Не сте харесали нито една публикация</string>
+ <string name="faq_button">ЧЗВ</string>
+ <string name="browse_our_faq_button">Преглед на секцията за ЧЗВ</string>
+ <string name="nux_help_description">Посете помощния център за да намерите отговор на често задавани въпроси или посете форума за да зададете нови</string>
+ <string name="agree_terms_of_service">Създавайки профил, вие се съгласявате със нашите очарователни %1$sПравила за ползване%2$s</string>
+ <string name="create_new_blog_wpcom">Създаване блог в WordPress.com</string>
+ <string name="new_blog_wpcom_created">Блог-а в WordPress.com е създаден!</string>
+ <string name="reader_empty_comments">Все още няма коментари</string>
+ <string name="reader_empty_posts_in_tag">Няма публикации с този таг</string>
+ <string name="reader_label_comment_count_multi">%,d коментари</string>
+ <string name="reader_label_view_original">Преглед оригиналната статия</string>
+ <string name="reader_label_like">Харесване</string>
+ <string name="reader_label_liked_by">Харесано от</string>
+ <string name="reader_label_comment_count_single">Един коментар</string>
+ <string name="reader_label_comments_closed">Коментарите са изключени</string>
+ <string name="reader_label_comments_on">Коментари за</string>
+ <string name="reader_title_photo_viewer">%1$d от %2$d</string>
+ <string name="error_publish_empty_post">Публикуването на празна публикация не е разрешено</string>
+ <string name="error_refresh_unauthorized_posts">Нямате нужните права за да преглеждате или променяте публикации</string>
+ <string name="error_refresh_unauthorized_pages">Нямате нужните права за да преглеждате или редактирате страници</string>
+ <string name="error_refresh_unauthorized_comments">Нямате нужните права за преглеждане или редактиране на коментари</string>
+ <string name="older_month">По-стари от месец</string>
+ <string name="more">Още</string>
+ <string name="older_two_days">По-стари от 2 дена</string>
+ <string name="older_last_week">По-стари от седмица</string>
+ <string name="stats_no_blog">Възникна грешка при зареждане на статистиката за този блог</string>
+ <string name="select_a_blog">Изберете WordPress уебсайт</string>
+ <string name="sending_content">Качване на съдържанието %s</string>
+ <string name="uploading_total">Качване на %1$d от %2$d</string>
+ <string name="mnu_comment_liked">Харесано</string>
+ <string name="comment">Коментар</string>
+ <string name="comment_trashed">Коментарът беше изтрит</string>
+ <string name="posts_empty_list">Все още няма публикации. Защо не създадете една?</string>
+ <string name="comment_reply_to_user">Отговоряне на %s</string>
+ <string name="pages_empty_list">Все ощя няма страници. Защо не създадете една?</string>
+ <string name="media_empty_list_custom_date">За този времеви интервал няма файлове</string>
+ <string name="posting_post">Публикуване на "%s"</string>
+ <string name="signing_out">Излизане...</string>
+ <string name="reader_toast_err_generic">Действието беше неуспешно</string>
+ <string name="reader_toast_err_block_blog">Блокирането на този блог е неуспешно.</string>
+ <string name="reader_toast_blog_blocked">Публикациите от този блог вече няма да бъдат покавзани</string>
+ <string name="reader_menu_block_blog">Блокиране на този блог</string>
+ <string name="contact_us">Връзка с нас</string>
+ <string name="hs__conversation_detail_error">Опишете проблема</string>
+ <string name="hs__new_conversation_header">Чат за поддръжка</string>
+ <string name="hs__conversation_header">Чат за поддръжка</string>
+ <string name="hs__username_blank_error">Въведете валидно име</string>
+ <string name="hs__invalid_email_error">Въведете валидна електронна поща</string>
+ <string name="add_location">Добавете местоположение</string>
+ <string name="current_location">Текущо местоположение</string>
+ <string name="search_location">Търсене</string>
+ <string name="edit_location">Редактиране</string>
+ <string name="search_current_location">Местоположение</string>
+ <string name="preference_send_usage_stats">Изпращане на статистика</string>
+ <string name="preference_send_usage_stats_summary">Автоматично изпращане на статистика за ползването, за да помогнете на развитието на WordPress за Android</string>
+ <string name="update_verb">Актуализиране</string>
+ <string name="schedule_verb">Планиране</string>
+ <string name="reader_title_blog_preview">Блог от Reader</string>
+ <string name="reader_title_tag_preview">Етикет от Reader</string>
+ <string name="reader_title_subs">Етикети и блогове</string>
+ <string name="reader_page_followed_tags">Следени етикети</string>
+ <string name="reader_page_followed_blogs">Следени сайтове</string>
+ <string name="reader_hint_add_tag_or_url">Въведете етикет или адрес за следване</string>
+ <string name="reader_label_followed_blog">Блогът беше последван</string>
+ <string name="reader_label_tag_preview">Публикации с етикет %s</string>
+ <string name="reader_toast_err_get_blog_info">Блогът не може да бъде видян</string>
+ <string name="reader_toast_err_already_follow_blog">Вече следвате този блог</string>
+ <string name="reader_toast_err_follow_blog">Последването на този блог е неуспешно</string>
+ <string name="reader_toast_err_unfollow_blog">Спирането на следенето на този блог е неуспешно</string>
+ <string name="reader_empty_recommended_blogs">Няма препоръчани блогове</string>
+ <string name="saving">Записване...</string>
+ <string name="media_empty_list">Няма медиа</string>
+ <string name="ptr_tip_message">Съвет: прекарайте пръст надолу за да обновите списъка</string>
+ <string name="help">Помощ</string>
+ <string name="forgot_password">Изгубена парола?</string>
+ <string name="forums">Форуми</string>
+ <string name="help_center">Помощен център</string>
+ <string name="ssl_certificate_error">Невалиден SSL сертификат</string>
+ <string name="ssl_certificate_ask_trust">Ако обикновено се свързвате с този сайт без проблеми, тази грешка може да означава, че някой се опитва да имитира този сайт и не трябва да продължавате. Искате ли да се доверите на този сертификат въпреки това?</string>
+ <string name="out_of_memory">Няма повече налична памет в устройството</string>
+ <string name="no_network_message">Няма налична мрежа</string>
+ <string name="could_not_remove_account">Сайтът не може да бъде премахнат</string>
+ <string name="gallery_error">Възникна грешка при извличането на файла</string>
+ <string name="blog_not_found">Възникна грешка при достъпване на този блог</string>
+ <string name="wait_until_upload_completes">Изчакай трансфера да завърши</string>
+ <string name="theme_fetch_failed">Възникна грешка при извличането на снимките</string>
+ <string name="theme_set_failed">Възникна грешка при смяна на темата</string>
+ <string name="theme_auth_error_message">Уверете се че имате нужните права за да сменяте теми</string>
+ <string name="comments_empty_list">Няма коментари</string>
+ <string name="mnu_comment_unspam">Не е спам</string>
+ <string name="no_site_error">Не може да се свърже със WordPress уебсайта</string>
+ <string name="adding_cat_failed">Добавянето на категория е неуспешно</string>
+ <string name="adding_cat_success">Категорията е добавена успешно</string>
+ <string name="cat_name_required">Полето "име на категория" е задължително</string>
+ <string name="category_automatically_renamed">Категорията %1$s не е валидна. Преименувана е на %2$s,</string>
+ <string name="no_account">Няма наличен профил за WordPress, добавете такъв и опитайте отново</string>
+ <string name="sdcard_message">Трябва да имате поставена SD карта, за да качите файлове</string>
+ <string name="stats_empty_comments">Няма коментари</string>
+ <string name="stats_bar_graph_empty">Няма налична статистика</string>
+ <string name="invalid_url_message">Проверете дали зададеният адрес е валиден</string>
+ <string name="reply_failed">Неуспешен отговор</string>
+ <string name="notifications_empty_list">Нямате нови известявания</string>
+ <string name="error_delete_post">Грешка при изтриване на %s</string>
+ <string name="error_refresh_posts">Публикациите не могат да бъдат обновени в момента</string>
+ <string name="error_refresh_pages">Страниците не могат да бъдат обновени в момента</string>
+ <string name="error_refresh_notifications">Известията не могат да бъдат обновени в моемнта</string>
+ <string name="error_refresh_comments">Коментарите не могат да бъдат обновени в момента</string>
+ <string name="error_refresh_stats">Статистиката не може да бъде обновена в момента</string>
+ <string name="error_generic">Възникна грешка</string>
+ <string name="error_moderate_comment">Възникна грешка по време на модерирането</string>
+ <string name="error_edit_comment">Възникна грешка при редактирането на коментара</string>
+ <string name="error_upload">Възникна грешка при качването на %s</string>
+ <string name="error_load_comment">Коментарът не можеше да бъде зареден</string>
+ <string name="error_downloading_image">Възникна грешка при извличане на това изображение</string>
+ <string name="passcode_wrong_passcode">Грешен ПИН</string>
+ <string name="invalid_email_message">Адресът на електронната поща не е валиден</string>
+ <string name="invalid_password_message">Паролата трябва да съдържа най-малко 4 знака</string>
+ <string name="invalid_username_too_short">Потребителското име трябва да е по-дълго от 4 символа</string>
+ <string name="invalid_username_too_long">Потребителското име трябва да е по-кратко от 61 символа</string>
+ <string name="username_only_lowercase_letters_and_numbers">Потребителското име може да съдържа само малки латински букви (a-z) и числа</string>
+ <string name="username_required">Въведете потребителско име</string>
+ <string name="username_not_allowed">Това потребителско име не е разрешено</string>
+ <string name="username_must_be_at_least_four_characters">Потребителското име трябва да е поне 4 символа дълго</string>
+ <string name="username_contains_invalid_characters">Потребителското име не трябва да съдържа символа "_"</string>
+ <string name="username_must_include_letters">Потребителското име трябва да съдържа поне 1 латинска буква (a-z)</string>
+ <string name="email_invalid">Добавете валидна електронна поща</string>
+ <string name="email_not_allowed">Тази електронна поща не е позволена</string>
+ <string name="username_exists">Това потребителско име вече съществува</string>
+ <string name="email_exists">Тази електронна поща вече се ползва</string>
+ <string name="username_reserved_but_may_be_available">Това потребителско име е резервирано в момента, но може да бъде налично в близките дни</string>
+ <string name="blog_name_required">Въведете адрес на сайта</string>
+ <string name="blog_name_not_allowed">Този адрес не е позволен</string>
+ <string name="blog_name_must_be_at_least_four_characters">Дължината на адреса на уебсайта трябва да бъде поне 4 символа дълга</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Адресът на сайта трябва да е по-малък от 64 символа</string>
+ <string name="blog_name_contains_invalid_characters">Адреса на уебсайта не може да съдържа символа "_"</string>
+ <string name="blog_name_cant_be_used">Не можете да използвате тозу уебсайт адрес</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Адреса на уебсайта може да съдържа само малки латински букви (a-z) и числа</string>
+ <string name="blog_name_exists">Този уебсайт вече съществува</string>
+ <string name="blog_name_reserved">Този уебсайт е резервиран</string>
+ <string name="blog_name_reserved_but_may_be_available">Този уебсайт е резервиран в момента, но въпреки това може да бъде наличен в близките дни</string>
+ <string name="username_or_password_incorrect">Името или паролата, които зададохте, са грешни</string>
+ <string name="nux_cannot_log_in">Свързването е невъзможно с момента</string>
+ <string name="xmlrpc_error">Връзката не беше осъществена. Въведете пълният път до xmlrpc.php на вашия уебсайт и опитайте отново.</string>
+ <string name="select_categories">Избери категории</string>
+ <string name="account_details">Информация за потребителя</string>
+ <string name="edit_post">Редактиране на публикацията</string>
+ <string name="add_comment">Добавяне на коментар</string>
+ <string name="connection_error">Грешка при свързване</string>
+ <string name="cancel_edit">Отказване</string>
+ <string name="scaled_image_error">Въведете валидна стойност за мащаба на ширината</string>
+ <string name="post_not_found">Възникна грешка при зареждане на тази плубликация. Опреснете Вашите публикации и опитайте отново.</string>
+ <string name="learn_more">Още</string>
+ <string name="media_gallery_settings_title">Настройки на галерията</string>
+ <string name="media_gallery_image_order">Подредба на изображенията</string>
+ <string name="media_gallery_num_columns">Брой колони</string>
+ <string name="media_gallery_type_thumbnail_grid">Решетка с малки изображения</string>
+ <string name="media_gallery_edit">Редактиране на галерията</string>
+ <string name="media_error_no_permission">Нямате нужните права за да прегледате файловете</string>
+ <string name="cannot_delete_multi_media_items">Някои от файловете не могат да бъдат изтрити в момента. Опитайте по-късно.</string>
+ <string name="themes_live_preview">Преглед на живо</string>
+ <string name="theme_current_theme">Текуща тема</string>
+ <string name="theme_premium_theme">Премиум тема</string>
+ <string name="link_enter_url_text">Текст на връзката (по избор)</string>
+ <string name="create_a_link">Създаване на връзка</string>
+ <string name="page_settings">Настройки на страницата</string>
+ <string name="local_draft">Локална чернова</string>
+ <string name="upload_failed">Качването е неуспешно</string>
+ <string name="horizontal_alignment">Хоризонтална подредба</string>
+ <string name="file_not_found">Файла за качване не беше намерен? Бил ли е изтрит или преместен?</string>
+ <string name="post_settings">Настройки на публикацията</string>
+ <string name="delete_post">Изтриване на публикация</string>
+ <string name="delete_page">Изтриване на страница</string>
+ <string name="comment_status_approved">Одобрено</string>
+ <string name="comment_status_unapproved">Предстоящ</string>
+ <string name="comment_status_spam">Спам</string>
+ <string name="comment_status_trash">Изхвърлено</string>
+ <string name="edit_comment">Редактиране на коментар</string>
+ <string name="mnu_comment_approve">Одобряване</string>
+ <string name="mnu_comment_unapprove">Неодобряване</string>
+ <string name="mnu_comment_spam">Спам</string>
+ <string name="mnu_comment_trash">Боклук</string>
+ <string name="dlg_approving_comments">Одобряване</string>
+ <string name="dlg_unapproving_comments">Неодобряване</string>
+ <string name="dlg_spamming_comments">Отбелязване като спам</string>
+ <string name="dlg_trashing_comments">Изпрати в боклук</string>
+ <string name="dlg_confirm_trash_comments">Изпращане в кошчето</string>
+ <string name="trash_yes">Изтриване</string>
+ <string name="trash_no">Да не се изтрива</string>
+ <string name="trash">Кошче</string>
+ <string name="author_name">Име на автора</string>
+ <string name="author_email">Ел. поща на автора</string>
+ <string name="author_url">URL на автора</string>
+ <string name="hint_comment_content">Коментар</string>
+ <string name="saving_changes">Промените се запазват</string>
+ <string name="sure_to_cancel_edit_comment">Преустановяване редактирането на този коментар?</string>
+ <string name="content_required">Коментарът е задължителен</string>
+ <string name="toast_comment_unedited">Коментарът не беше променен</string>
+ <string name="remove_account">Премахване на сайта</string>
+ <string name="blog_removed_successfully">Уебсайта беше премахнат успешно</string>
+ <string name="delete_draft">Изтриване на черновата</string>
+ <string name="preview_page">Преглед на страницата</string>
+ <string name="preview_post">Прегледайте публикацията</string>
+ <string name="comment_added">Коментарът е добавен успешно</string>
+ <string name="post_not_published">Статусът на публикацията не е публикуван</string>
+ <string name="page_not_published">Статусът на страницата не е публикуван</string>
+ <string name="view_in_browser">Преглед в браузър</string>
+ <string name="add_new_category">Добавяне на нова категория</string>
+ <string name="category_name">Име на категорията</string>
+ <string name="category_slug">URL на категорията (по-избор)</string>
+ <string name="category_desc">Описание на категорията (по избор)</string>
+ <string name="category_parent">Родител на категорията (по избор)</string>
+ <string name="share_action_post">Нова публикация</string>
+ <string name="share_action_media">Библиотека с файлове</string>
+ <string name="file_error_create">Грешка при създаване на временния файл. Уверете се че има достатъчно свободно място на вашето устройство.</string>
+ <string name="location_not_found">Непознато населено място</string>
+ <string name="open_source_licenses">Лиценз с отворен код</string>
+ <string name="invalid_site_url_message">Уверете се че въведения URL адрес на сайт е валиден</string>
+ <string name="pending_review">Предстои удобрение</string>
+ <string name="http_credentials">HTTP удостоверение за самоличност (по-избор)</string>
+ <string name="http_authorization_required">Нужна е оторизация</string>
+ <string name="post_format">Формат на публикацията</string>
+ <string name="notifications_empty_all">Все още няма известявания.</string>
+ <string name="new_post">Нова публикация</string>
+ <string name="new_media">Нов файл</string>
+ <string name="view_site">Преглед на сайта</string>
+ <string name="privacy_policy">Политика за поверителност</string>
+ <string name="local_changes">Локални промени</string>
+ <string name="image_settings">Настройки на изображението</string>
+ <string name="add_account_blog_url">Адрес на блога</string>
+ <string name="wordpress_blog">Блог от вида WordPress</string>
+ <string name="error_blog_hidden">Този блог е скрит и не може да бъде зареден. Пуснете го отново от настройките и опитайте отново.</string>
+ <string name="fatal_db_error">Възникна грешка при създаване на базата от данни на приложението. Моля преинсталирайте приложението.</string>
+ <string name="jetpack_message_not_admin">Плъгина Jetpack е нужен за събиране/изобразяване на статистики. Моля свържете се с администратора на уебсайта.</string>
+ <string name="reader_title_applog">Дневник на приложението</string>
+ <string name="reader_share_link">Споделяне на връзка</string>
+ <string name="reader_toast_err_add_tag">Етикетът не може да бъде добавен</string>
+ <string name="reader_toast_err_remove_tag">Етикетът не може да бъде махнат</string>
+ <string name="required_field">Задължително поле</string>
+ <string name="email_hint">Електронна поща</string>
+ <string name="site_address">URL на вашия самостоятелно хостван уебсайт.</string>
+ <string name="email_cant_be_used_to_signup">Не може да използвате тази електронна поща за да влезете. Имаме проблеми с доставчика на тази пощенска услуга, който блокира част от нашите писма. Използвайте друг доставчик на електронна поща.</string>
+ <string name="email_reserved">Тази електронна поща вече се използва. Проверете вашата пощенска кутия за писмо с данни за активиране. Ако не активирате сега можете да опитате отново след няколко дни.</string>
+ <string name="blog_name_must_include_letters">Адресът на сайта трябва да има най-малко 1 буква (а-я)</string>
+ <string name="blog_name_invalid">Невалиден адрес на уебсайта</string>
+ <string name="blog_title_invalid">Невалидно име на уебсайта</string>
+ <string name="deleting_page">Изтриване на страница</string>
+ <string name="deleting_post">Изтриване на статия</string>
+ <string name="share_url_post">Споделяне на статия</string>
+ <string name="share_url_page">Споделяне на страница</string>
+ <string name="share_link">Споделяне на връзка</string>
+ <string name="creating_your_account">Създаване на вашия профил</string>
+ <string name="creating_your_site">Създаване на вашия сайт</string>
+ <string name="reader_empty_posts_in_tag_updating">Извличане на публикации…</string>
+ <string name="error_refresh_media">Нещо се обърка при зареждането на мултимедийната библиотека. Моля, опитайте по-късно.</string>
+ <string name="reader_likes_you_and_multi">Вие и %,d още харесахте това</string>
+ <string name="reader_likes_multi">%,d човека харесаха това</string>
+ <string name="reader_toast_err_get_comment">Възникна грешка при извличането на този коментар</string>
+ <string name="reader_label_reply">Отговор</string>
+ <string name="video">Видео</string>
+ <string name="download">Сваляне на медиа</string>
+ <string name="comment_spammed">Коментарът е отбелязан като спам</string>
+ <string name="cant_share_no_visible_blog">Не можете да споделяте към WordPress без видим блог</string>
+ <string name="select_time">Избор на време</string>
+ <string name="reader_likes_you_and_one">Вие и още някой харесвате това.</string>
+ <string name="reader_empty_followed_blogs_description">Без притеснения! Просто натиснете върху иконата горе вдясно, за да започнете да разглеждате.</string>
+ <string name="select_date">Избор на дата</string>
+ <string name="pick_photo">Избор на снимка</string>
+ <string name="account_two_step_auth_enabled">За този профил е активирана двустепенна защита. За да генерирате необходимата парола посетете WordPress.com .</string>
+ <string name="pick_video">Избор на видео</string>
+ <string name="reader_toast_err_get_post">Извличането на тази публикация е неуспешно</string>
+ <string name="validating_user_data">Валидиране на данните на потребителя</string>
+ <string name="validating_site_data">Валидиране на даннитена сайта</string>
+ <string name="password_invalid">Имате нужда от по-сигурна парола. Моля уверете се че използвате поне 7 символа, смесени с главни и малки букви, числа или специални символи.</string>
+ <string name="nux_tap_continue">Напред</string>
+ <string name="nux_welcome_create_account">Създаване на профил</string>
+ <string name="signing_in">Влизане...</string>
+ <string name="nux_add_selfhosted_blog">Добавяне на самостоятелно хостван уебсайт</string>
+ <string name="nux_oops_not_selfhosted_blog">Влизане в WordPress.com</string>
+ <string name="media_add_popup_title">Добавяне в библиотеката с файлове</string>
+ <string name="media_add_new_media_gallery">Създаване на галерия</string>
+ <string name="empty_list_default">Списъкът е празен</string>
+ <string name="select_from_media_library">Избор от медийната библиотека</string>
+ <string name="jetpack_message">Jetpack е нужен за извличане на статистиката. Искате ли да инсталирате Jetpack?</string>
+ <string name="jetpack_not_found">Jetpack не беше намерен</string>
+ <string name="reader_untitled_post">(Неозаглавен)</string>
+ <string name="reader_share_subject">Споделено от %s</string>
+ <string name="reader_btn_share">Споделяне</string>
+ <string name="reader_btn_follow">Последване</string>
+ <string name="reader_btn_unfollow">Следва</string>
+ <string name="reader_hint_comment_on_comment">Отговор на коментар...</string>
+ <string name="reader_label_added_tag">%s е добавен</string>
+ <string name="reader_label_removed_tag">%s е премахнат</string>
+ <string name="reader_likes_one">Едно лице хареса това</string>
+ <string name="reader_likes_only_you">Ти хареса това</string>
+ <string name="reader_toast_err_comment_failed">Коментарът не беше публикуван</string>
+ <string name="reader_toast_err_tag_exists">Вече следвате този етикет</string>
+ <string name="reader_toast_err_tag_invalid">Това не е валиден етикет</string>
+ <string name="reader_toast_err_share_intent">Споделянето е неуспешно</string>
+ <string name="reader_toast_err_view_image">Преглеждането на изображението е неуспешно</string>
+ <string name="reader_toast_err_url_intent">%s не може да се отвори</string>
+ <string name="reader_empty_followed_tags">Вие не следвате етикети</string>
+ <string name="create_account_wpcom">Създайте профил в WordPress.com</string>
+ <string name="button_next">Нататък</string>
+ <string name="connecting_wpcom">Свързване с WordPress.com</string>
+ <string name="username_invalid">Невалидно потребителско име</string>
+ <string name="limit_reached">Достигнахте ограничение. Пробвайте отново след 1 минута. Ако пробвате по-скоро от една минута ще увеличите продължителността на ограничението. В случай, че това е грешка, трябва да се свържете с поддръжката.</string>
+ <string name="nux_tutorial_get_started_title">Начало!</string>
+ <string name="themes">Теми</string>
+ <string name="all">Всички</string>
+ <string name="images">Изображение</string>
+ <string name="unattached">Неприкачен</string>
+ <string name="custom_date">Дата по избор</string>
+ <string name="media_add_popup_capture_photo">Снимане</string>
+ <string name="media_add_popup_capture_video">Запис на видео</string>
+ <string name="media_gallery_image_order_random">Произволен</string>
+ <string name="media_gallery_image_order_reverse">Обратен ред</string>
+ <string name="media_gallery_type">Вид</string>
+ <string name="media_gallery_type_squares">Квадрати</string>
+ <string name="media_gallery_type_tiled">Плочки</string>
+ <string name="media_gallery_type_circles">Кръгове</string>
+ <string name="media_gallery_type_slideshow">Слайдшоу</string>
+ <string name="media_edit_title_text">Заглавие</string>
+ <string name="media_edit_caption_text">Подзаглавие</string>
+ <string name="media_edit_description_text">Описание</string>
+ <string name="media_edit_title_hint">Въведете заглавие</string>
+ <string name="media_edit_caption_hint">Въведете описание</string>
+ <string name="media_edit_description_hint">Добави описание</string>
+ <string name="media_edit_success">Обновено</string>
+ <string name="media_edit_failure">Актуализирането е неуспешно</string>
+ <string name="themes_details_label">Подробности</string>
+ <string name="themes_features_label">Възможности</string>
+ <string name="theme_activate_button">Активация</string>
+ <string name="theme_activating_button">Активиране</string>
+ <string name="theme_set_success">Промяната на темата беше успешна!</string>
+ <string name="theme_auth_error_title">Извличането на теми е неуспешно</string>
+ <string name="post_excerpt">Откъс</string>
+ <string name="share_action_title">Добавяне към ...</string>
+ <string name="share_action">Споделяне</string>
+ <string name="stats">Статистика</string>
+ <string name="stats_view_visitors_and_views">Посетители и разглеждания</string>
+ <string name="stats_view_clicks">Кликове</string>
+ <string name="stats_view_tags_and_categories">Етикети и категории</string>
+ <string name="stats_view_referrers">Референти</string>
+ <string name="stats_timeframe_today">Днес</string>
+ <string name="stats_timeframe_yesterday">Вчера</string>
+ <string name="stats_timeframe_days">Дни</string>
+ <string name="stats_timeframe_weeks">Седмици</string>
+ <string name="stats_timeframe_months">Месеци</string>
+ <string name="stats_entry_country">Държава</string>
+ <string name="stats_entry_posts_and_pages">Заглавие</string>
+ <string name="stats_entry_tags_and_categories">Тема</string>
+ <string name="stats_entry_authors">Автор</string>
+ <string name="stats_entry_referrers">Референт</string>
+ <string name="stats_totals_views">Преглеждания</string>
+ <string name="stats_totals_clicks">Кликове</string>
+ <string name="stats_totals_plays">Гледания</string>
+ <string name="passcode_manage">Управление на заключването чрез ПИН</string>
+ <string name="passcode_enter_passcode">Въведете ПИН</string>
+ <string name="passcode_enter_old_passcode">Въведете стария ПИН</string>
+ <string name="passcode_re_enter_passcode">Повторно въвеждане на ПИН</string>
+ <string name="passcode_change_passcode">Промяна на ПИН</string>
+ <string name="passcode_set">Запазване на ПИН</string>
+ <string name="passcode_preference_title">ПИН ключ</string>
+ <string name="passcode_turn_off">Изключи ПИН ключ</string>
+ <string name="passcode_turn_on">Вклюши ПИН ключ</string>
+ <string name="upload">Качване</string>
+ <string name="discard">Изхвърли</string>
+ <string name="sign_in">Влизане</string>
+ <string name="notifications">Известия</string>
+ <string name="note_reply_successful">Отговорът е публикуван</string>
+ <string name="follows">Следва</string>
+ <string name="new_notifications">%d нови известия</string>
+ <string name="more_notifications">и още %d.</string>
+ <string name="loading">Зареждане...</string>
+ <string name="httpuser">Потребител за HTTP</string>
+ <string name="httppassword">HTTP Парола</string>
+ <string name="error_media_upload">Грешка при качването на медийния файл</string>
+ <string name="post_content">Съдържание (докосни, за да добавиш текст и снимки)</string>
+ <string name="publish_date">Публикуване</string>
+ <string name="content_description_add_media">Добавяне на медия</string>
+ <string name="incorrect_credentials">Грешно име и/или парола</string>
+ <string name="password">Парола</string>
+ <string name="username">Име</string>
+ <string name="reader">Читател</string>
+ <string name="featured">Задаване като основно изображение на публикацията</string>
+ <string name="featured_in_post">Използване на снимката в съдържанието на публикацията</string>
+ <string name="no_network_title">Няма активна мрежа</string>
+ <string name="pages">Страници</string>
+ <string name="caption">Надпис (незадължителен)</string>
+ <string name="width">Ширина</string>
+ <string name="posts">Публикации</string>
+ <string name="anonymous">Анонимен</string>
+ <string name="page">Страница</string>
+ <string name="post">Публикация</string>
+ <string name="blogusername">потребителско име</string>
+ <string name="ok">ОК</string>
+ <string name="upload_scaled_image">Качване и свързване с оразмерено изображение</string>
+ <string name="scaled_image">Оразмеряване ширината на изображението</string>
+ <string name="scheduled">Планирана</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Качва се...</string>
+ <string name="version">Версия</string>
+ <string name="tos">Условия за ползване</string>
+ <string name="app_title">WordPress за Android</string>
+ <string name="max_thumbnail_px_width">Широчина на изображение по подразбиране</string>
+ <string name="image_alignment">Подравняване</string>
+ <string name="refresh">Презареждане</string>
+ <string name="untitled">Неозаглавен</string>
+ <string name="edit">Редактирай</string>
+ <string name="post_id">Публикация</string>
+ <string name="page_id">Страница</string>
+ <string name="post_password">Парола (по избор)</string>
+ <string name="immediately">Веднага</string>
+ <string name="quickpress_add_alert_title">Задаване на име на пряк път</string>
+ <string name="today">Днес</string>
+ <string name="settings">Настройки</string>
+ <string name="share_url">Споделяне на URL</string>
+ <string name="quickpress_window_title">Избор на блог за QuickPress препратката</string>
+ <string name="quickpress_add_error">Името на препратката не може да бъде празно</string>
+ <string name="publish_post">Публикуване</string>
+ <string name="draft">Чернова</string>
+ <string name="post_private">Частен</string>
+ <string name="upload_full_size_image">Качване и свързване към пълното изображение</string>
+ <string name="title">Заглавие</string>
+ <string name="tags_separate_with_commas">Етикети (разделени със запетаи)</string>
+ <string name="categories">Категории</string>
+ <string name="dlg_deleting_comments">Изтриване на коментарите</string>
+ <string name="notification_blink">Светлинно известяване</string>
+ <string name="notification_sound">Звуково известяване</string>
+ <string name="notification_vibrate">Вибриране</string>
+ <string name="status">Състояние</string>
+ <string name="location">Местоположение</string>
+ <string name="sdcard_title">Необходима е SD карта</string>
+ <string name="select_video">Изберете видео от галерията</string>
+ <string name="media">Медия</string>
+ <string name="delete">Изтриване</string>
+ <string name="none">Няма</string>
+ <string name="blogs">Блогове</string>
+ <string name="select_photo">Избор на изображение от галерията</string>
+ <string name="error">Грешка</string>
+ <string name="cancel">Отказ</string>
+ <string name="save">Запазване</string>
+ <string name="add">Добавяне</string>
+ <string name="category_refresh_error">Грешка при обновлението на категориите</string>
+ <string name="preview">Предвариетелен преглед</string>
+ <string name="on">на</string>
+ <string name="reply">Отговор</string>
+ <string name="notification_settings">Настройки на известяванията</string>
+ <string name="yes">Да</string>
+ <string name="no">Не</string>
+</resources>
diff --git a/WordPress/src/main/res/values-cs/strings.xml b/WordPress/src/main/res/values-cs/strings.xml
new file mode 100644
index 000000000..bc0f734be
--- /dev/null
+++ b/WordPress/src/main/res/values-cs/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrátor</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Redaktor</string>
+ <string name="role_contributor">Spolupracovník</string>
+ <string name="role_viewer">Návštěvník</string>
+ <string name="role_follower">Následovník</string>
+ <string name="error_post_my_profile_no_connection">Není spojení,nebylo možno uložit váš profil</string>
+ <string name="alignment_none">Žádné</string>
+ <string name="alignment_left">Doleva</string>
+ <string name="alignment_right">Doprava</string>
+ <string name="site_settings_list_editor_action_mode_title">Vyberte %1$d</string>
+ <string name="error_fetch_users_list">Nelze načíst uživatele webu</string>
+ <string name="plans_manage">Spravuj svůj plán na\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Nemáte ještě nějaké otázky.</string>
+ <string name="people_fetching">Načítání uživatelů...</string>
+ <string name="title_follower">Fanoušek</string>
+ <string name="title_email_follower">Email fanouška</string>
+ <string name="people_empty_list_filtered_email_followers">Nemáte ještě žádné emaily od svých fanoušků.</string>
+ <string name="people_empty_list_filtered_followers">Nemáte ještě žádné fanoušky.</string>
+ <string name="people_empty_list_filtered_users">Nemáte ještě žádné uživatele.</string>
+ <string name="people_dropdown_item_viewers">Zobrazení</string>
+ <string name="people_dropdown_item_email_followers">Emaily fanoušků</string>
+ <string name="people_dropdown_item_followers">Fanoušci</string>
+ <string name="people_dropdown_item_team">Tým</string>
+ <string name="invite_message_usernames_limit">Pozvěte uživatele pomocí až 10 emailů / nebo WordPress.com uživatelských jmen. Ty, kteří si budou chtít uživatelské jméno vytvořit, dostanou pokynu pro vytvoření.</string>
+ <string name="viewer_remove_confirmation_message">Chcete odstranit tento prohlížeč, pak už nikdo nebude moct navštívit tuto webovou stránku.\n\nChcete určitě odstranit tento prohlížeč?</string>
+ <string name="follower_remove_confirmation_message">Pokud odstraníte tohoto fanouška, nebude už dostávat žádné upozornění z tohoto webu..\n\nChcete určitě odstranit tohoto fanouška?</string>
+ <string name="follower_subscribed_since">Od %1$s</string>
+ <string name="reader_label_view_gallery">Zobrazit galerii</string>
+ <string name="error_remove_follower">Nepodařilo se odstranit fanouška</string>
+ <string name="error_remove_viewer">Nepodařilo se odstranit zobrazení</string>
+ <string name="error_fetch_email_followers_list">Nepodařilo se načít emaily fanoušků</string>
+ <string name="error_fetch_followers_list">Nepodařilo se načít webové stránky fanoušků</string>
+ <string name="editor_failed_uploads_switch_html">Některá média se nenahrála. Nelze přepnout do režimu HTML\n v tomhle stavu. Chcete odstranit všechny neúspěšně nahrané soubory a pak pokračovat?</string>
+ <string name="format_bar_description_html">HTML</string>
+ <string name="visual_editor">Vizuální editor</string>
+ <string name="image_thumbnail">Náhled</string>
+ <string name="format_bar_description_ul">Neuspořádaný seznam</string>
+ <string name="format_bar_description_ol">Uspořádaný seznam</string>
+ <string name="format_bar_description_more">Vložit více</string>
+ <string name="format_bar_description_media">Vložit média</string>
+ <string name="format_bar_description_link">Vložit URL</string>
+ <string name="format_bar_description_quote">Citace</string>
+ <string name="format_bar_description_strike">Přeškrtnutí</string>
+ <string name="format_bar_description_italic">Kurzíva</string>
+ <string name="format_bar_description_underline">Tlustě</string>
+ <string name="format_bar_description_bold">Tučně</string>
+ <string name="image_settings_save_toast">Změny byly uloženy</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_caption">Titulek</string>
+ <string name="image_link_to">Odkaz na</string>
+ <string name="image_width">Šířka</string>
+ <string name="image_settings_dismiss_dialog_title">Zahodit neuložené změny?</string>
+ <string name="stop_upload_dialog_title">Zastavit nahrávání?</string>
+ <string name="stop_upload_button">Nahrávání zastaveno</string>
+ <string name="alert_error_adding_media">Během vkládání média se vyskytla neočekávaná chyba</string>
+ <string name="alert_action_while_uploading">Momentálně se nahrávají média. Počkejte, dokud se to nedokončí.</string>
+ <string name="alert_insert_image_html_mode">Média se nedaří vložit přímo v režimu HTML. Přejděte do editoru.</string>
+ <string name="uploading_gallery_placeholder">Nahrávám galerii...</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_some_failed">Pozvánka odeslána, ale vyskytla se chyba(y)!</string>
+ <string name="invite_sent">Pozvánka úspěšně odeslána.</string>
+ <string name="tap_to_try_again">Klepnutím na tlačítko to můžete zkusit znovu!</string>
+ <string name="invite_error_sending">Během pokusu o odeslání pozvánky se vyskytla chyba!</string>
+ <string name="invite_error_invalid_usernames_multiple">Nelze odeslat: Uživatelská jména nebo emaily jsou neplatné</string>
+ <string name="invite_error_invalid_usernames_one">Nelze odeslat: Uživatelské jméno nebo email je neplatné</string>
+ <string name="invite_error_no_usernames">Přidejte alespoň jedno uživatelské jméno</string>
+ <string name="invite_message_info">(nepovinné) Můžete zadat vlastní zprávu (max 500 znaků), která bude součástí pozvánky uživatele/uživatelů.</string>
+ <string name="invite_message_remaining_other">%d zbývajících znaků</string>
+ <string name="invite_message_remaining_one">1 zbývající znak</string>
+ <string name="invite_message_remaining_zero">Žádný zbývající znak</string>
+ <string name="invite_message_title">Vlastní zpráva</string>
+ <string name="invite_invalid_email">Emailová adresa \'%s\' není platná</string>
+ <string name="invite_already_a_member">Vámi zvolené uživatelské jméno \'%s\' jíž někdo používá.</string>
+ <string name="invite_username_not_found">Žádný uživatel nemá vámi zvolené uživatelské jméno \'%s\'</string>
+ <string name="invite">Pozvat</string>
+ <string name="invite_names_title">Uživatelská jména nebo emaily</string>
+ <string name="send_link">Poslat URL</string>
+ <string name="my_site_header_external">Externí</string>
+ <string name="invite_people">Přidat uživatele</string>
+ <string name="signup_succeed_signin_failed">Váš účet byl vytvořen, ale došlo k chybě, když jsme vás zaregistrovali.\n Zkuste se přihlásit pomocí svého nově vytvořeného uživatelského jména a hesla.</string>
+ <string name="label_clear_search_history">Smazat historii prohlížení</string>
+ <string name="dlg_confirm_clear_search_history">Smazat historii prohlížení?</string>
+ <string name="reader_empty_posts_in_search_description">Žádný příspěvek nebyl nalezen %s pro váš jazyk.</string>
+ <string name="reader_empty_posts_in_search_title">Nenalezeny žádné příspěvky</string>
+ <string name="reader_label_post_search_running">Hledání...</string>
+ <string name="reader_label_related_posts">Související zobrazení</string>
+ <string name="reader_label_post_search_explainer">Hledat všechny veřejné WordPress.com blogy</string>
+ <string name="reader_hint_post_search">Prohledat WordPress.com</string>
+ <string name="reader_title_related_post_detail">Související příspěvky </string>
+ <string name="reader_title_search_results">Vyhledávání v %s</string>
+ <string name="preview_screen_links_disabled">Odkazy jsou zakázány v náhledu na obrazovce</string>
+ <string name="draft_explainer">Tento příspěvek je koncept, který nebyl publikován</string>
+ <string name="send">Poslat</string>
+ <string name="person_removed">%1$s úspěšně odstraněno</string>
+ <string name="user_remove_confirmation_message">Máte odstranit %1$s, tento uživatel již nebude moci přihlašovat k tomuto webu, ale všechen obsah, který vytvořil %1$s zůstanou na webu.\n\nChcete i přesto odstranit tohoto uživatele?</string>
+ <string name="person_remove_confirmation_title">Ostranit %1$s</string>
+ <string name="people">Lidé</string>
+ <string name="edit_user">Upravit uživatele</string>
+ <string name="role">Role</string>
+ <string name="reader_empty_posts_in_custom_list">Web v tomto seznamu nezveřejnil v poslední době nic</string>
+ <string name="error_remove_user">Nepodařilo se odstranit vybraného uživatele</string>
+ <string name="error_fetch_viewers_list">Nemohu získat zobrazení webu</string>
+ <string name="error_update_role">Nemohu aktualizovat role uživatele</string>
+ <string name="gravatar_camera_and_media_permission_required">Oprávnění která jsou nezbytná pro výběr nebo pořízení fotografie</string>
+ <string name="error_updating_gravatar">Chyba při nahrávání vašeho Gravataru</string>
+ <string name="error_locating_image">Chyba při umístění oříznutého obrázku</string>
+ <string name="error_refreshing_gravatar">Chyba při znovu nahrání vašeho Gravataru</string>
+ <string name="gravatar_tip">Nový! Zobrazte svůj Gravatar pro změnění.</string>
+ <string name="error_cropping_image">Chyba při oříznutí obrázku</string>
+ <string name="not_on_wordpress_com">Není na WordPress.com?</string>
+ <string name="launch_your_email_app">Zapněte vaší emailovou aplikaci</string>
+ <string name="checking_email">Ověřuji email</string>
+ <string name="check_your_email">Zkontroluj svůj email</string>
+ <string name="magic_link_unavailable_error_message">Momentálně nedostupné. Prosím zadejte své heslo</string>
+ <string name="logging_in">Přihlašování</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Odkaz byl zaslán na váš email, přihlašte se co nejdříve</string>
+ <string name="enter_your_password_instead">Zadejte vaše heslo místo tohoto</string>
+ <string name="web_address_dialog_hint">Veřejně se zobrazí při komentování.</string>
+ <string name="jetpack_not_connected_message">Jetpack plugin je nainstalován, ale není propojen s WordPress.com. Chcete jej propojit?</string>
+ <string name="username_email">Email nebo uživatelské jméno</string>
+ <string name="jetpack_not_connected">Jetpack není připojen</string>
+ <string name="new_editor_reflection_error">Vizuální editor není kompatibilní s vaším zařízením.\n Proto byl automaticky zakázán.</string>
+ <string name="stats_insights_latest_post_no_title">(žádný nadpis)</string>
+ <string name="capture_or_pick_photo">Vyfoťte nebo vyberte fotku</string>
+ <string name="plans_post_purchase_button_themes">Procházet šablony</string>
+ <string name="plans_post_purchase_text_themes">Nyní máte neomezený přístup k Premium šablonám. Máte možnost náhledu jakékoliv šablony na vašem webu, abyste mohli začít.</string>
+ <string name="plans_post_purchase_title_themes">Najděte výbornou premium šablonu</string>
+ <string name="plans_post_purchase_button_video">Začít nový příspěvek</string>
+ <string name="plans_post_purchase_text_video">Můžete nahrávat a ukládat videa na svých stránkách s VideoPress a zvětšit tak množství médií.</string>
+ <string name="plans_post_purchase_title_video">Přiveďte příspěvky k životu pomocí videa</string>
+ <string name="plans_post_purchase_button_customize">Přizpůsobte si svůj web</string>
+ <string name="plans_post_purchase_text_customize">Nyní máte přístup k vlastním fontům, vlastním barvám a máte možnost upravovat si CSS.</string>
+ <string name="plans_post_purchase_title_customize">Upravit písmo a barvy</string>
+ <string name="plans_post_purchase_text_intro">Váš web dělá pokroky! Nyní máte možnost prozkoumat nové funkce a vybrat si, které chcete používat.</string>
+ <string name="plans_post_purchase_title_intro">Je to všechno tvoje! Jen tak dál!</string>
+ <string name="plan">Plán</string>
+ <string name="plans">Plány</string>
+ <string name="plans_loading_error">Plány se nepodařilo načíst</string>
+ <string name="export_your_content_message">Vaše příspěvky, stránky a nastavení budou zaslány na %s.</string>
+ <string name="export_your_content">Exportovat svůj obsah</string>
+ <string name="exporting_content_progress">Exportování obsahu...</string>
+ <string name="export_email_sent">Export odeslán do emailu!</string>
+ <string name="checking_purchases">Ověřuji platby</string>
+ <string name="show_purchases">Zobrazit nákupy</string>
+ <string name="premium_upgrades_message">Máte na svém webu aktivní upgrade na prémium. Zrušte tento upgrade před smazáním vašeho webu.</string>
+ <string name="premium_upgrades_title">Premium upgrade </string>
+ <string name="purchases_request_error">Něco se pokazilo. Nákup nešel provést. </string>
+ <string name="delete_site_progress">Mazání stránky...</string>
+ <string name="delete_site_hint">Smazat web</string>
+ <string name="delete_site_summary">Tuto akci nelze vrátit zpět! Smazáním vaší stránky se odstraní veškerý obsah, uživatelé a domény z webu.</string>
+ <string name="export_site_hint">Export vaší stránky do XML souboru</string>
+ <string name="are_you_sure">Opravdu?</string>
+ <string name="export_site_summary">Jste si jisti, že jste si nezapomněli v čas exportovat váš obsah. To už v budoucnu nepůjde získat.</string>
+ <string name="keep_your_content">Udržujte svůj obsah</string>
+ <string name="domain_removal_hint">Doména přestane fungovat, jakmile odstraníte svoje stránky</string>
+ <string name="domain_removal_summary">Buďte opatrní! Odstraněním vašeho webu také odeberete následující doménu/y.</string>
+ <string name="primary_domain">Primární doména</string>
+ <string name="domain_removal">Odebrání domény</string>
+ <string name="error_deleting_site_summary">Došlo k chybě při mazání vašeho webu. Obraťte se na podporu pro další informace</string>
+ <string name="error_deleting_site">Chyba při mazaní webové stránky</string>
+ <string name="site_settings_export_content_title">Export obsahu</string>
+ <string name="confirm_delete_site_prompt">Prosím, zadejte %1$s v poli níže pro potvrzení. Váš web pak bude navždy pryč!</string>
+ <string name="contact_support">Kontaktovat podporu</string>
+ <string name="confirm_delete_site">Potvrďte smazání stránky</string>
+ <string name="start_over_text">Chcete webové stránky, ale nechcete některé z těchto příspěvků a stránek, které tam teď jsou. Náš tým podpory může mazat příspěvky, stránky, média a i komentáře.\n\nAž bude váš web a URL adresa aktivní, budete mít nové možnosti ve vytváření obsahu.</string>
+ <string name="let_us_help">Pomůžete</string>
+ <string name="site_settings_start_over_hint">Začněte svůj web spravovat </string>
+ <string name="me_btn_app_settings">Nastavení aplikace</string>
+ <string name="start_over">Začít znovu</string>
+ <string name="editor_remove_failed_uploads">Odstranit</string>
+ <string name="editor_toast_failed_uploads">Nahrávání některých souborů selhalo. V tomto stavu nemůžete\n publikovat příspěvek. Chcete odstranit neúspěšně přenesené soubory?</string>
+ <string name="site_settings_advanced_header">Pokročilé</string>
+ <string name="comments_empty_list_filtered_trashed">Žádné komentáře v koši</string>
+ <string name="comments_empty_list_filtered_pending">Žádné komentáře nečekají na schválení</string>
+ <string name="comments_empty_list_filtered_approved">Žádné schválené komentáře</string>
+ <string name="button_done">Hotovo</string>
+ <string name="button_skip">Přeskočit</string>
+ <string name="site_timeout_error">Nelze se připojit k serveru WordPress, kvůli chybě v časového limitu.</string>
+ <string name="xmlrpc_malformed_response_error">Nelze se připojit. Instalace WordPress reagoval na neplatný XML-RPC.</string>
+ <string name="xmlrpc_missing_method_error">Nelze se připojit. Požadované metody XML-RPC chybí na serveru.</string>
+ <string name="post_format_status">Aktualita</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Na střed</string>
+ <string name="theme_free">Zdarma</string>
+ <string name="theme_all">Vše</string>
+ <string name="theme_premium">Placeno</string>
+ <string name="post_format_gallery">Galerie</string>
+ <string name="post_format_image">Obrázek</string>
+ <string name="post_format_link">Odkaz</string>
+ <string name="post_format_quote">Citace</string>
+ <string name="post_format_standard">Základní</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_aside">Poznámka</string>
+ <string name="post_format_audio">Zvuk</string>
+ <string name="notif_events">Informace o WordPress.com kurzech a akcích (on-line a osobně).</string>
+ <string name="notif_surveys">Příležitosti k účasti na WordPress.com výzkumech a průzkumech.</string>
+ <string name="notif_tips">Tipy pro získání maxima z WordPress.com.</string>
+ <string name="notif_community">Komunita</string>
+ <string name="replies_to_my_comments">Odpovědi na mé komentáře</string>
+ <string name="notif_suggestions">Doporučení</string>
+ <string name="notif_research">Výzkum</string>
+ <string name="site_achievements">Úspěchy webu</string>
+ <string name="username_mentions">Uživatelské jméno obsahuje</string>
+ <string name="likes_on_my_posts">To se mi líbí u mých příspěvků</string>
+ <string name="site_follows">Weboví fanoušci</string>
+ <string name="likes_on_my_comments">To se mi líbí u mých komentářů</string>
+ <string name="comments_on_my_site">Komentáře na mém webu</string>
+ <string name="site_settings_list_editor_summary_other">%d položek</string>
+ <string name="site_settings_list_editor_summary_one">1 položka</string>
+ <string name="approve_auto">Všichni uživatelé</string>
+ <string name="approve_auto_if_previously_approved">Komentáře známých uživatelů</string>
+ <string name="approve_manual">Žádné komentáře</string>
+ <string name="site_settings_paging_summary_other">%d komentářů na stránce</string>
+ <string name="site_settings_paging_summary_one">1 komentář na stránce</string>
+ <string name="site_settings_multiple_links_summary_other">Vyžadovat schválení pro více než %d odkazů</string>
+ <string name="site_settings_multiple_links_summary_one">Vyžadovat schválení pro více než 1 odkaz</string>
+ <string name="site_settings_multiple_links_summary_zero">Vyžadovat schválení pro více než 0 odkazů</string>
+ <string name="detail_approve_auto">Automaticky schválit veškeré komentáře.</string>
+ <string name="detail_approve_auto_if_previously_approved">Automaticky schválit pokud již má uživatel dříve schválených komentář</string>
+ <string name="detail_approve_manual">Vyžadovat manuální schválení veškerých komentářů.</string>
+ <string name="days_quantity_other">%d dnů</string>
+ <string name="days_quantity_one">1 den</string>
+ <string name="filter_trashed_posts">V koši</string>
+ <string name="filter_published_posts">Publikováno</string>
+ <string name="filter_draft_posts">Koncepty</string>
+ <string name="filter_scheduled_posts">Naplánováno</string>
+ <string name="web_address">Webová adresa</string>
+ <string name="primary_site">Hlavní web</string>
+ <string name="pending_email_change_snackbar">Kliknutím na ověřovací odkaz v emailu zaslaném na %1$s musíte potvrdit tuto adresu</string>
+ <string name="editor_toast_uploading_please_wait">Právě teď se nahrávají média. Počkejte, dokud se to nedokončí.</string>
+ <string name="error_refresh_comments_showing_older">Komentáře nemohou být v tuto chvíli aktualizonány - zobrazující se starší komentáře</string>
+ <string name="editor_post_settings_set_featured_image">Nastavit vybrané obrázky</string>
+ <string name="editor_post_settings_featured_image">Vybraný obrázek</string>
+ <string name="new_editor_promo_desc">WordPress aplikace pro Android nyní zahrnuje krásný nový vizuální editor.\n Vyzkoušejte si ho tím, že vytvoří nový příspěvek.</string>
+ <string name="new_editor_promo_title">Zburusu nový editor</string>
+ <string name="new_editor_promo_button_label">Skvělé, díky!</string>
+ <string name="visual_editor_enabled">Vizuální editor povolen</string>
+ <string name="editor_content_placeholder">Sdílejte svůj příběh zde...</string>
+ <string name="editor_page_title_placeholder">Název stránky</string>
+ <string name="editor_post_title_placeholder">Název příspěvku</string>
+ <string name="email_address">Emailová adresa</string>
+ <string name="preference_show_visual_editor">Zobrazit vizuální editor</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comments">Smazat trvale tyto komentáře?</string>
+ <string name="dlg_sure_to_delete_comment">Smazat trvale tento komentář?</string>
+ <string name="mnu_comment_delete_permanently">Smazat</string>
+ <string name="mnu_comment_untrash">Obnovit</string>
+ <string name="comment_deleted_permanently">Komentář smazán</string>
+ <string name="comments_empty_list_filtered_spam">Žádný spam komentář</string>
+ <string name="comment_status_all">Vše</string>
+ <string name="could_not_load_page">Stránku se nepodařilo načíst</string>
+ <string name="off">Vypnout</string>
+ <string name="interface_language">Jazyk prostředí</string>
+ <string name="about_the_app">O aplikaci</string>
+ <string name="error_post_account_settings">Nelze uložit nastavení účtu</string>
+ <string name="error_post_my_profile">Nelze uložit váš profil</string>
+ <string name="error_fetch_account_settings">Nelze načíst nastavení účtu</string>
+ <string name="error_fetch_my_profile">Nelze načíst váš profil</string>
+ <string name="stats_widget_promo_ok_btn_label">Rozumím</string>
+ <string name="stats_widget_promo_desc">Přidejte si widget na úvodní obrazovku pro jednoduchý přístup ke statistikám.</string>
+ <string name="stats_widget_promo_title">Widget se statistikou na obrazovku</string>
+ <string name="site_settings_unknown_language_code_error">Kód jazyka nebyl rozpoznán</string>
+ <string name="site_settings_threading_dialog_description">Povolit vnoření komentářů do vláken.</string>
+ <string name="site_settings_threading_dialog_header">Vlákno do</string>
+ <string name="add_category">Vytvořit novou rubriku</string>
+ <string name="search">Hledat</string>
+ <string name="remove">Odstranit</string>
+ <string name="disabled">Deaktivováno</string>
+ <string name="site_settings_image_original_size">Originální velikost</string>
+ <string name="privacy_private">Váš web je viditelný pouze pro vás a pro vámi schválené uživatele</string>
+ <string name="privacy_public_not_indexed">Váš web je viditelný pro všechny, ale žádá vyhledávače aby jej neindexovali</string>
+ <string name="privacy_public">Váš web je viditelný pro všechny a může být indexován vyhledávači</string>
+ <string name="about_me_hint">Osobní informace</string>
+ <string name="public_display_name_hint">Zobrazované jméno bude vaše uživatelské jméno, pokud nenastavíte jiné</string>
+ <string name="about_me">O mně</string>
+ <string name="public_display_name">Veřejně zobrazovat jako</string>
+ <string name="my_profile">Můj profil</string>
+ <string name="first_name">Jméno</string>
+ <string name="last_name">Příjmení</string>
+ <string name="site_privacy_public_desc">Povolit indexování tohoto webu</string>
+ <string name="site_privacy_hidden_desc">Zakázat indexování tohoto webu</string>
+ <string name="site_privacy_private_desc">Přeji si aby byl můj web soukromý, viditelný pouze uživatelům, které si sám zvolím.</string>
+ <string name="cd_related_post_preview_image">Náhledový obrázek souvisejících obrázků</string>
+ <string name="error_post_remote_site_settings">Nepodařilo se uložit informace o stránce</string>
+ <string name="error_fetch_remote_site_settings">Nepodařilo se získat informace o stránce</string>
+ <string name="error_media_upload_connection">Během nahrávání mediálního souboru, nastala chyba připojení</string>
+ <string name="site_settings_disconnected_toast">Odpojeno, úpravy vypnuty.</string>
+ <string name="site_settings_unsupported_version_error">Nepodporovaná verze WordPress</string>
+ <string name="site_settings_multiple_links_dialog_description">Vyžadovat schválení pro komentáře, které obsahují více než tento počet odkazů.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Uzavřít automaticky</string>
+ <string name="site_settings_close_after_dialog_description">Automaticky uzavře komentáře k článkům.</string>
+ <string name="site_settings_paging_dialog_description">Rozdělit vlákna komentářů do více stránek.</string>
+ <string name="site_settings_paging_dialog_header">Komentářů na stránku</string>
+ <string name="site_settings_close_after_dialog_title">Uzavřít komentování</string>
+ <string name="site_settings_blacklist_description">Pokud komentář obsahuje jakékoliv z těchto slov ve svém obsahu, jméně, URL, e-mailu, nebo IP, bude označen jako spam. Můžete také zadat části slov, takže podmínce "press" bude odpovídat "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">Pokud komentář obsahuje jakékoliv z těchto slov ve svém obsahu, jméně, URL, e-mailu, nebo IP, tento komentář bude pozdržen a moderován. Můžete také zadat části slov, takže podmínce "press" bude odpovídat "WordPress".</string>
+ <string name="site_settings_list_editor_input_hint">Zadejte slovo nebo frázi</string>
+ <string name="site_settings_list_editor_no_items_text">Žádné položky</string>
+ <string name="site_settings_learn_more_caption">Tato nastavení můžete změnit pro jednotlivé příspěvky.</string>
+ <string name="site_settings_rp_preview3_site">v "aktualizaci"</string>
+ <string name="site_settings_rp_preview3_title">Zaměření upgradu: VideoPress pro svatby</string>
+ <string name="site_settings_rp_preview2_site">v "Aplikaci"</string>
+ <string name="site_settings_rp_preview2_title">Aplikace WordPress pro Android má vylepšený vzhled</string>
+ <string name="site_settings_rp_preview1_site">v "Mobilu"</string>
+ <string name="site_settings_rp_preview1_title">Velký iPhone/iPad dostává nyní aktualizaci</string>
+ <string name="site_settings_rp_show_images_title">Zobrazit obrázky</string>
+ <string name="site_settings_rp_show_header_title">Zobrazit hlavičku</string>
+ <string name="site_settings_rp_switch_summary">Podobné příspěvky zobrazí relevantní obsah vašeho webu, přímo pod příspěvky.</string>
+ <string name="site_settings_rp_switch_title">Zobrazit podobné příspěvky</string>
+ <string name="site_settings_delete_site_hint">Odstraní vaše data o webu z aplikace</string>
+ <string name="site_settings_blacklist_hint">Komentáře odpovídající filtru jsou označeny jako spam</string>
+ <string name="site_settings_moderation_hold_hint">Komentáře odpovídající filtru jsou přesunuty do fronty pro moderaci</string>
+ <string name="site_settings_multiple_links_hint">Ignorovat limit množství odkazů pro známé uživatele</string>
+ <string name="site_settings_whitelist_hint">Autor komentáře musí mít dříve schválený komentář</string>
+ <string name="site_settings_user_account_required_hint">Pro přidávání komentářů, musí být uživatelé registrovaní a přihlášeni</string>
+ <string name="site_settings_identity_required_hint">Autor komentáře musí vyplnit své jméno a e-mail</string>
+ <string name="site_settings_manual_approval_hint">Komentáře musí být ručně schváleny</string>
+ <string name="site_settings_paging_hint">Zobrazovat komentáře v bloku o specifické velikosti</string>
+ <string name="site_settings_threading_hint">Povolit neslušné komentáře do určité hloubky</string>
+ <string name="site_settings_sort_by_hint">Určuje v jakém pořadí jsou komentáře zobrazovány</string>
+ <string name="site_settings_close_after_hint">Po určeném čase zakázat komentáře</string>
+ <string name="site_settings_receive_pingbacks_hint">Povolit odkazy z ostatních webů</string>
+ <string name="site_settings_send_pingbacks_hint">Pokusit se informovat veškeré weby propojené s daným článkem</string>
+ <string name="site_settings_allow_comments_hint">Umožnit čtenářům přidávat komentáře</string>
+ <string name="site_settings_discussion_hint">Prohlédněte a změňte si nastavení vašich webových diskusí</string>
+ <string name="site_settings_more_hint">Zobrazit veškerá dostupná nastavení diskusí</string>
+ <string name="site_settings_related_posts_hint">Zobrazte nebo skryjte podobné příspěvky ve čtečce</string>
+ <string name="site_settings_upload_and_link_image_hint">Povolte pokud chcete vždy nahrávat plnou velikost obrázků</string>
+ <string name="site_settings_image_width_hint">Změní velikost obrázků v příspěvcích na tuto šířku</string>
+ <string name="site_settings_format_hint">Nastaví nový formát příspěvku</string>
+ <string name="site_settings_category_hint">Nastaví novou rubriku příspěvku</string>
+ <string name="site_settings_location_hint">Automaticky vkládat data polohy do vašich příspěvků</string>
+ <string name="site_settings_password_hint">Změňte si své heslo</string>
+ <string name="site_settings_username_hint">Aktuální uživatelský účet</string>
+ <string name="site_settings_language_hint">Jazyk, kterým je tento web psán je</string>
+ <string name="site_settings_privacy_hint">Řídí kdo může zobrazit váš web</string>
+ <string name="site_settings_address_hint">Změna vaší adresy momentálně není k dispozici</string>
+ <string name="site_settings_tagline_hint">Několika slovy popište, čím se budete na webu zabývat.</string>
+ <string name="site_settings_title_hint">Několika slovy popište, čím se budete na webu zabývat</string>
+ <string name="site_settings_whitelist_known_summary">Komentáře pro známé uživatele</string>
+ <string name="site_settings_whitelist_all_summary">Komentáře pro všechny uživatele</string>
+ <string name="site_settings_threading_summary">%d úrovní</string>
+ <string name="site_settings_privacy_private_summary">Soukromé</string>
+ <string name="site_settings_privacy_hidden_summary">Skrytý</string>
+ <string name="site_settings_delete_site_title">Smazat stránku</string>
+ <string name="site_settings_privacy_public_summary">Publikovat</string>
+ <string name="site_settings_blacklist_title">Černá listina</string>
+ <string name="site_settings_moderation_hold_title">Podržet pro moderaci</string>
+ <string name="site_settings_multiple_links_title">Odkazy v komentářích</string>
+ <string name="site_settings_whitelist_title">Schválit automaticky</string>
+ <string name="site_settings_threading_title">Posloupnosti</string>
+ <string name="site_settings_paging_title">Stránkování</string>
+ <string name="site_settings_sort_by_title">Řadit dle</string>
+ <string name="site_settings_account_required_title">Uživatel musí být přihlášen</string>
+ <string name="site_settings_identity_required_title">Musí obsahovat jméno a email</string>
+ <string name="site_settings_receive_pingbacks_title">Přijímat pingbacky</string>
+ <string name="site_settings_send_pingbacks_title">Odesílat pingbacky</string>
+ <string name="site_settings_allow_comments_title">Povolit komentáře</string>
+ <string name="site_settings_default_format_title">Výchozí formát</string>
+ <string name="site_settings_default_category_title">Výchozí rubrika</string>
+ <string name="site_settings_location_title">Povolit polohu</string>
+ <string name="site_settings_address_title">Adresa</string>
+ <string name="site_settings_tagline_title">Popis webu</string>
+ <string name="site_settings_title_title">Název webu</string>
+ <string name="site_settings_this_device_header">Toto zařízení</string>
+ <string name="site_settings_discussion_new_posts_header">Výchozí pro nové příspěvky</string>
+ <string name="site_settings_writing_header">Psaní</string>
+ <string name="site_settings_account_header">Účet</string>
+ <string name="site_settings_general_header">Obecné</string>
+ <string name="newest_first">Nejnovější první</string>
+ <string name="discussion">Diskuse</string>
+ <string name="privacy">Soukromí</string>
+ <string name="comments">Komentáře</string>
+ <string name="related_posts">Související příspěvky</string>
+ <string name="oldest_first">Starší první</string>
+ <string name="close_after">Uzavřít po</string>
+ <string name="media_error_no_permission_upload">Nemáte oprávnění pro nahrávání mediálních souborů na tento web</string>
+ <string name="never">Nikdy</string>
+ <string name="unknown">Neznámý</string>
+ <string name="reader_err_get_post_not_found">Tento příspěvek již neexistuje</string>
+ <string name="reader_err_get_post_not_authorized">Nemáte oprávnění prozobrazení tohoto příspěvku</string>
+ <string name="reader_err_get_post_generic">Nelze načíst tento příspěvek</string>
+ <string name="blog_name_no_spaced_allowed">Adresa webu nesmí obsahovat mezery</string>
+ <string name="invalid_username_no_spaces">Uživatelské jméno nemůže obsahovat mezery</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Stránky, které sledujete v poslední době neměly žádné příspěvky</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Žádné nedávné příspěvky</string>
+ <string name="media_details_copy_url_toast">URL adresa byla zkopírována do schránky</string>
+ <string name="edit_media">Upravit média</string>
+ <string name="media_details_copy_url">Kopírovat URL adresu</string>
+ <string name="media_details_label_date_uploaded">Nahráno</string>
+ <string name="media_details_label_date_added">Přidáno</string>
+ <string name="selected_theme">Vybraná šablona</string>
+ <string name="could_not_load_theme">Šablonu nejde načíst</string>
+ <string name="theme_activation_error">Nastala neočekávaná chyba. Šablonu se nepodařilo aktivovat</string>
+ <string name="theme_by_author_prompt_append">od %1$s</string>
+ <string name="theme_prompt">Děkujeme, že používáte %1$s</string>
+ <string name="theme_try_and_customize">Vyzkoušet a upravit</string>
+ <string name="theme_done">HOTOVO</string>
+ <string name="theme_support">Podpora</string>
+ <string name="theme_view">Zobrazit</string>
+ <string name="theme_details">Podrobnosti</string>
+ <string name="theme_manage_site">SPRAVOVAT WEB</string>
+ <string name="theme_activate">Aktivovat</string>
+ <string name="title_activity_theme_support">Šablony</string>
+ <string name="customize">Přizpůsobit</string>
+ <string name="active">Aktivní</string>
+ <string name="support">Podpora</string>
+ <string name="details">Podrobnosti</string>
+ <string name="current_theme">Aktivní šablona</string>
+ <string name="date_range_start_date">Počáteční datum</string>
+ <string name="date_range_end_date">Koncové datum</string>
+ <string name="stats_referrers_spam_generic_error">Během operace nastala chyba. Komentář označen jako spam.</string>
+ <string name="stats_referrers_marking_not_spam">Označuji jako běžný komentář</string>
+ <string name="stats_referrers_unspam">Není spam</string>
+ <string name="stats_referrers_marking_spam">Označuji jako spam</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="post_published">Příspěvek byl publikován</string>
+ <string name="page_published">Stránka byla publikována</string>
+ <string name="post_updated">Příspěvek aktualizován</string>
+ <string name="page_updated">Stránka aktualizována</string>
+ <string name="theme_auth_error_authenticate">Nepodařilo se načíst šablony: Nepodařilo se ověřit uživatele</string>
+ <string name="theme_no_search_result_found">Promiňte, šablona nebyla nalezena.</string>
+ <string name="media_file_name">Název souboru: %s</string>
+ <string name="media_uploaded_on">Nahráno: %s</string>
+ <string name="media_dimensions">Rozměry: %s</string>
+ <string name="upload_queued">Ve frontě</string>
+ <string name="media_file_type">Typ souboru: %s</string>
+ <string name="reader_label_gap_marker">Načíst více příspěvků</string>
+ <string name="notifications_no_search_results">Žádné webové stránky se neshodují s \'%s\'</string>
+ <string name="search_sites">Hledat webové stránky</string>
+ <string name="notifications_empty_view_reader">Zobrazit čtečku</string>
+ <string name="unread">Nepřečtené</string>
+ <string name="notifications_empty_action_followers_likes">Všimněte si: komentujte příspěvky které jste četli.</string>
+ <string name="notifications_empty_action_comments">Připojte se ke konverzaci: komentujte příspěvky z webů, které sledujete.</string>
+ <string name="notifications_empty_action_unread">Začněte konverzaci: napište nový příspěvek.</string>
+ <string name="notifications_empty_action_all">Buďte aktivní! Komentujte příspěvky z webů, které sledujete.</string>
+ <string name="notifications_empty_likes">Žádné nové liky...zatím.</string>
+ <string name="notifications_empty_followers">Žádný nový sledující...zatím.</string>
+ <string name="notifications_empty_comments">Žádné nové komentáře...zatím.</string>
+ <string name="notifications_empty_unread">Všechny jsou přečtené.</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Prosím získejte Statistiky v aplikaci, a zkuste později přidat widget</string>
+ <string name="stats_widget_error_readd_widget">Prosím odstraňte widget a později jej znovu přidejte</string>
+ <string name="stats_widget_error_no_visible_blog">Statistiky nemohou být přístupné bez viditelného webu</string>
+ <string name="stats_widget_error_no_permissions">Váš účet WordPress.com nemá přístup k tomuto webu</string>
+ <string name="stats_widget_error_no_account">Přihlaste se prosím do WordPress účtu</string>
+ <string name="stats_widget_error_generic">Nelze načíst statistiku</string>
+ <string name="stats_widget_loading_data">Načítám data...</string>
+ <string name="stats_widget_name_for_blog">Dnešní statistiky pro %1$s</string>
+ <string name="stats_widget_name">Dnešní statistiky Wordpress</string>
+ <string name="add_location_permission_required">Je vyžadován souhlas k přidání polohy</string>
+ <string name="add_media_permission_required">Je vyžadován souhlas k přidání souborů</string>
+ <string name="access_media_permission_required">Je vyžadován souhlas pro přístup k souborům</string>
+ <string name="stats_enable_rest_api_in_jetpack">Povolte modul JSON API v Jetpacku, k náhledu vašich statistik.</string>
+ <string name="error_open_list_from_notification">Tento příspěvek nebo stránka, byl zveřejněn na jiném webu</string>
+ <string name="reader_short_comment_count_multi">%s komentářů</string>
+ <string name="reader_short_comment_count_one">1 komentář</string>
+ <string name="reader_label_submit_comment">Publikovat</string>
+ <string name="reader_hint_comment_on_post">Odpovědět na příspěvek...</string>
+ <string name="reader_discover_visit_blog">Navštivte %s</string>
+ <string name="reader_discover_attribution_blog">Původně publikováno %s</string>
+ <string name="reader_discover_attribution_author">Původně publikoval %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Původně publikoval %1$s na webu %2$s</string>
+ <string name="reader_short_like_count_multi">%s To se mi líbí</string>
+ <string name="reader_short_like_count_one">1 To se mi líbí</string>
+ <string name="reader_short_like_count_none">To se mi líbí</string>
+ <string name="reader_label_follow_count">sledující %,d</string>
+ <string name="reader_menu_tags">Upravit štítky a weby</string>
+ <string name="reader_title_post_detail">Čtenáři příspěvku</string>
+ <string name="local_draft_explainer">Tento příspěvek je místní koncept, který zatím nebyl publikován</string>
+ <string name="local_changes_explainer">Tento příspěvek obsahuje změny, které ještě nebyly publikovány</string>
+ <string name="notifications_push_summary">Nastavení upozornění, které se objeví na vašem přístroji.</string>
+ <string name="notifications_email_summary">Nastavení upozornění, které jsou odesílány na email vázaný na váš účet.</string>
+ <string name="notifications_tab_summary">Nastavení upozornění, které se zobrazí v záložce oznámení.</string>
+ <string name="notifications_disabled">Oznámení z aplikace byly zakázány. Klepnutě sem a budete je moct nastavit.</string>
+ <string name="notification_types">Typ upozornění</string>
+ <string name="error_loading_notifications">Nelze načíst nastavení oznámení</string>
+ <string name="replies_to_your_comments">Odpovědi na vaše komentáře</string>
+ <string name="comment_likes">Oblíbené komentáře</string>
+ <string name="app_notifications">Upozornění aplikace</string>
+ <string name="notifications_tab">Karta oznámení</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Komentáře na ostatních webech</string>
+ <string name="notifications_wpcom_updates">Aktualizace WordPress.com</string>
+ <string name="notifications_other">Ostatní</string>
+ <string name="notifications_account_emails">Email od WordPress.com</string>
+ <string name="notifications_account_emails_summary">Budeme vždy odesílat důležité emaily týkající se vašeho účtu, ale můžete také získat některé zajímavé bonusy.</string>
+ <string name="your_sites">Vaše stránky</string>
+ <string name="notifications_sights_and_sounds">Notifikační upozornění</string>
+ <string name="stats_insights_latest_post_trend">Bylo to %1$s od %2$s publikován. Zde je návod, jak byl příspěvek doposud upravován...</string>
+ <string name="stats_insights_latest_post_summary">Přehled posledních příspěvků</string>
+ <string name="button_revert">Vrátit</string>
+ <string name="yesterday">Včera</string>
+ <string name="days_ago">před %d dny</string>
+ <string name="connectionbar_no_connection">Bez připojení</string>
+ <string name="button_back">Zpět</string>
+ <string name="button_publish">Publikovat</string>
+ <string name="button_edit">Upravit</string>
+ <string name="button_view">Zobrazit</string>
+ <string name="page_trashed">Stránka byla přesunuta do koše</string>
+ <string name="post_deleted">Příspěvek byl odstraněn</string>
+ <string name="post_trashed">Příspěvek byl přesunut do koše</string>
+ <string name="page_deleted">Stránka byla odstraněna</string>
+ <string name="button_stats">Statistiky</string>
+ <string name="button_trash">Koš</string>
+ <string name="button_preview">Náhled</string>
+ <string name="trashed">Přesunuto do koše</string>
+ <string name="stats_no_activity_this_period">Žádná aktivita v tomto období</string>
+ <string name="my_site_no_sites_view_subtitle">Chtěli byste přidat jednu?</string>
+ <string name="my_site_no_sites_view_title">Zatím nemáte žádný web WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustrace</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Nemáte oprávnění pro přístup k tomuto webu</string>
+ <string name="reader_toast_err_follow_blog_not_found">Web nebyl nalezen</string>
+ <string name="undo">Zpět</string>
+ <string name="tabbar_accessibility_label_me">Já</string>
+ <string name="tabbar_accessibility_label_my_site">Můj web</string>
+ <string name="editor_toast_changes_saved">Změny byly uloženy</string>
+ <string name="passcodelock_prompt_message">Zadejte PIN</string>
+ <string name="push_auth_expired">Žádost vypršela. Přihlaste se znovu přes WordPress.com </string>
+ <string name="ignore">Ignorovat</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% shlédnutí</string>
+ <string name="stats_insights_best_ever">Nejlepší shlédnutí</string>
+ <string name="stats_insights_most_popular_hour">Nejoblíbenější hodina</string>
+ <string name="stats_insights_most_popular_day">Nejoblíbenější den</string>
+ <string name="stats_insights_popular">Nejoblíbenější den a hodina</string>
+ <string name="stats_insights_today">Dnešní statistika</string>
+ <string name="stats_insights_all_time">Všechny příspěvky, zobrazení a návštěvníci</string>
+ <string name="stats_insights">Přehledy</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Chcete-li zobrazit statistiky, přihlaste se do účtu WordPress.com, který jste použili pro připojení Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Hledáte vaše nedávné statistiky? Přesunuli jsme je na stránku statistiky.</string>
+ <string name="me_disconnect_from_wordpress_com">Odpojit od WordPress.com</string>
+ <string name="me_btn_login_logout">Přihlášení/Odhlášení</string>
+ <string name="me_connect_to_wordpress_com">Připojit k WordPress.com</string>
+ <string name="account_settings">Uživatelské nastavení</string>
+ <string name="me_btn_support">Pomoc a podpora</string>
+ <string name="site_picker_cant_hide_current_site">"%s" nebyl skrytý, protože je to aktuální stránka</string>
+ <string name="site_picker_create_dotcom">Vytvořit web na WordPress.com</string>
+ <string name="site_picker_add_self_hosted">Přidat vlastní WordPress web</string>
+ <string name="site_picker_add_site">Přidat web</string>
+ <string name="site_picker_edit_visibility">Zobrazitt/skrýt stránky</string>
+ <string name="site_picker_title">Vybrat web</string>
+ <string name="my_site_btn_switch_site">Přepnout web</string>
+ <string name="my_site_btn_view_admin">Zobrazit administraci</string>
+ <string name="my_site_btn_view_site">Zobrazit web</string>
+ <string name="my_site_header_publish">Publikovat</string>
+ <string name="my_site_btn_site_settings">Nastavení</string>
+ <string name="my_site_btn_blog_posts">Příspěvky</string>
+ <string name="my_site_header_look_and_feel">Vzhled</string>
+ <string name="my_site_header_configuration">Nastavení</string>
+ <string name="reader_label_new_posts_subtitle">Klepněte pro jejich zobrazení</string>
+ <string name="notifications_account_required">Přihlaste se přes WordPress.com kvůli notifikacím</string>
+ <string name="stats_unknown_author">Neznámý autor</string>
+ <string name="image_added">Obrázek byl přidán</string>
+ <string name="signout">Odpojit</string>
+ <string name="hide">Skrýt</string>
+ <string name="select_all">Vybrat vše</string>
+ <string name="deselect_all">Zrušit výběr</string>
+ <string name="sign_out_wpcom_confirm">Po odpojení se vám z účtu vymažou všechny @%s WordPress.com údaje z tohoto zařízení, včetně lokálních konceptů a změn.</string>
+ <string name="show">Zobrazit</string>
+ <string name="select_from_new_picker">Nový vícenásobný výběr</string>
+ <string name="no_device_images">Žádné obrázky</string>
+ <string name="no_blog_videos">Žádná videa</string>
+ <string name="no_device_videos">Žádná videa</string>
+ <string name="no_blog_images">Žádné obrázky</string>
+ <string name="error_loading_images">Chyba při zobrazování obrázků</string>
+ <string name="error_loading_videos">Chyba při zobrazování videí</string>
+ <string name="loading_blog_images">Načítání obrázků</string>
+ <string name="loading_blog_videos">Načítání videí</string>
+ <string name="no_media_sources">Nelze načíst média</string>
+ <string name="error_loading_blog_videos">Nelze načíst videa</string>
+ <string name="stats_generic_error">Požadované statistiky se nepodařilo načíst</string>
+ <string name="error_loading_blog_images">Nelze načíst obrázky</string>
+ <string name="loading_images">Načítání obrázků</string>
+ <string name="loading_videos">Načítání videí</string>
+ <string name="no_media">Žádná média</string>
+ <string name="verify">Ověřit</string>
+ <string name="tab_title_site_images">Obrázky na webu</string>
+ <string name="tab_title_site_videos">Videa na webu</string>
+ <string name="tab_title_device_videos">Videa v zařízení</string>
+ <string name="take_video">Vybrat video</string>
+ <string name="tab_title_device_images">Obrázky v zařízení</string>
+ <string name="take_photo">Vybrat obrázek</string>
+ <string name="media_picker_title">Vybrat médium</string>
+ <string name="language">Jazyk</string>
+ <string name="device">Zařízení</string>
+ <string name="add_to_post">Přidat do příspěvku</string>
+ <string name="error_publish_no_network">Nelze publikovat bez připojení. Uloženo jako koncept.</string>
+ <string name="editor_toast_invalid_path">Neplatná cesta k souboru</string>
+ <string name="verification_code">Ověřovací kód</string>
+ <string name="invalid_verification_code">Neplatný ověřovací kód</string>
+ <string name="two_step_footer_button">Odeslat kód pomocí SMS</string>
+ <string name="two_step_sms_sent">Zkontrolujte si své SMSky pro ověřovací kód.</string>
+ <string name="two_step_footer_label">Zadejte kód z ověřovací aplikace</string>
+ <string name="media_details_label_file_type">Typ souboru</string>
+ <string name="media_details_label_file_name">Název souboru</string>
+ <string name="sign_in_jetpack">Přihlaste se k účtu WordPress.com pro připojení k Jetpacku.</string>
+ <string name="auth_required">Pro pokračování, se přihlaste ještě jednou.</string>
+ <string name="comments_fetching">Načítám komentáře...</string>
+ <string name="pages_fetching">Načítám stránky...</string>
+ <string name="toast_err_post_uploading">Při nahrávání nelze příspěvek otevřít</string>
+ <string name="posts_fetching">Načítám příspěvky...</string>
+ <string name="media_fetching">Načítám mediální soubory...</string>
+ <string name="reader_empty_posts_request_failed">Nedaří se načíst příspěvky</string>
+ <string name="stats_entry_search_terms">Hledaný výraz</string>
+ <string name="stats_view_search_terms">Hledané výrazy</string>
+ <string name="stats_view_authors">Autoři</string>
+ <string name="publisher">Vydavatel:</string>
+ <string name="stats_search_terms_unknown_search_terms">Neznámé hledané výrazy</string>
+ <string name="error_notification_open">Oznámení se nepodařilo otevřít</string>
+ <string name="stats_empty_search_terms">Nebyly zaznamenány žádné hledané výrazy</string>
+ <string name="stats_followers_total_email_paged">Zobrazuje se %1$d - %2$d z %3$s odberatelů</string>
+ <string name="stats_followers_total_wpcom_paged">WordPress.com odběratelé %1$d - %2$d z %3$s</string>
+ <string name="stats_empty_search_terms_desc">Zjistěte, jaká klíčová slova zvyšují návštěvnost vašeho webu.</string>
+ <string name="reader_label_new_posts">Nové příspěvky</string>
+ <string name="post_uploading">Nahrávám</string>
+ <string name="reader_empty_posts_in_blog">Tento web je prázdný</string>
+ <string name="reader_page_recommended_blogs">Stránky které by se vám mohly líbit</string>
+ <string name="stats_months_and_years">Měsíce a Roky</string>
+ <string name="stats_period">Období</string>
+ <string name="logs_copied_to_clipboard">Aplikační protokoly byly zkopírovány do schránky</string>
+ <string name="stats_total">Dohromady</string>
+ <string name="stats_overall">Celkem</string>
+ <string name="stats_average_per_day">Průměr za den</string>
+ <string name="stats_recent_weeks">Poslední týdny</string>
+ <string name="error_copy_to_clipboard">Při kopírování textu do schránky nastala chyba</string>
+ <string name="stats_visitors">Návštěvníci</string>
+ <string name="stats_views">Zobrazení</string>
+ <string name="stats_timeframe_years">Roky</string>
+ <string name="stats_pagination_label">Stránka %1$s do %2$s</string>
+ <string name="stats_likes">To se mi líbí</string>
+ <string name="stats_view_countries">Země</string>
+ <string name="stats_view_videos">Videa</string>
+ <string name="stats_view_top_posts_and_pages">Příspěvky a stránky</string>
+ <string name="stats_entry_clicks_link">Odkaz</string>
+ <string name="stats_totals_publicize">Odběratelé</string>
+ <string name="stats_entry_followers">Odběratel</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_empty_followers">Žádní odběratelé</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_a_day">Za den</string>
+ <string name="stats_followers_days">%1$d dny</string>
+ <string name="stats_followers_hours">%1$d hodiny</string>
+ <string name="stats_followers_minutes">%1$d minuty</string>
+ <string name="stats_followers_a_month">Za měsíce</string>
+ <string name="stats_followers_years">%1$d roky</string>
+ <string name="stats_followers_a_year">Za rok</string>
+ <string name="stats_view">Zobrazit</string>
+ <string name="stats_followers_months">%1$d měsíc</string>
+ <string name="stats_view_all">Zobrazit vše</string>
+ <string name="stats_view_followers">Sledující</string>
+ <string name="stats_view_publicize">Publikovaná</string>
+ <string name="stats_followers_an_hour_ago">za poslední hodinu</string>
+ <string name="themes_fetching">Načítám šablony...</string>
+ <string name="stats_for">Statistiky pro %s</string>
+ <string name="stats_other_recent_stats_label">Další nedávné statistiky</string>
+ <string name="stats_followers_a_minute_ago">před minutou</string>
+ <string name="stats_followers_seconds_ago">před pár sekundami</string>
+ <string name="stats_comments_by_authors">Podle autorů</string>
+ <string name="stats_comments_by_posts_and_pages">Podle příspěvků a stránek</string>
+ <string name="stats_followers_total_wpcom">Počet odběratelů WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Celkový počet článků s odběrateli: %1$s</string>
+ <string name="stats_empty_video">Žádná přehraná videa</string>
+ <string name="stats_entry_publicize">Služba</string>
+ <string name="stats_empty_publicize_desc">Sledujte celkový počet odběratelů přicházejících ze sociálních sítí prostřednictvím propagace.</string>
+ <string name="stats_empty_video_desc">Jestli jste nahráli videa pomocí VideoPressu, zjistíte kolikrát byla videa zobrazena.</string>
+ <string name="stats_empty_comments_desc">Jestli na své stránce povolíte komentáře, můžete sledovat nejaktivnější komentátory a na základě posledních 1000 komentářů zjistit, jaký obsah podněcuje živou diskuzi.</string>
+ <string name="stats_totals_followers">Od</string>
+ <string name="stats_empty_publicize">Nezaznamenaná žádná data pro propagaci </string>
+ <string name="stats_empty_clicks_title">Nezaznaménána žádná kliknutí</string>
+ <string name="stats_empty_referrers_title">Nezaznamenané žádné reference</string>
+ <string name="stats_empty_top_posts_title">Žádné příspěvky nebo stránky k prohlížení</string>
+ <string name="stats_empty_geoviews">Nezaznaménána žádná země</string>
+ <string name="stats_empty_top_authors_desc">Sledujte zobrazení jednotlivých příspěvků autorů a zjistěte, který obsah je od jednotlivých autorů populární.</string>
+ <string name="stats_empty_tags_and_categories">Žádné označené příspěvky nebo zobrazené stránky</string>
+ <string name="stats_empty_clicks_desc">Pokud váš obsah obsahuje odkazy na jiné stránky, podívejte se, na které návštěvníci nejvíce klikají.</string>
+ <string name="stats_empty_referrers_desc">Zjistěte více o viditelnosti stránky prostřednictvím webových stránek a vyhledávačů, které zvyšují její návštěvnost</string>
+ <string name="stats_empty_top_posts_desc">Zjistěte, co je váš nejúspěšnější obsah a zkontrolujte, jak se daří jednotlivým příspěvkům a stránkám v průběhu času.</string>
+ <string name="stats_empty_followers_desc">Sledujte celkový počet odběratelů a dobu odběru vašeho webu.</string>
+ <string name="stats_empty_tags_and_categories_desc">Získejte přehled nejpopulárnějších témat na webu podle příspěvků z uplynulého týdne.</string>
+ <string name="stats_empty_geoviews_desc">Podívejte se na seznam zemí, které produkují vašemu webu nejvyšší návštěvnost.</string>
+ <string name="stats_followers_total_email">Počet odběratelů emailů: %1$s</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="ssl_certificate_details">Detaily</string>
+ <string name="sure_to_remove_account">Smazat web?</string>
+ <string name="media_gallery_date_range">Zobraz média od %1$s do %2$s</string>
+ <string name="cab_selected">%d vybráno</string>
+ <string name="delete_sure_page">Smazat stránku</string>
+ <string name="delete_sure">Smazat koncept</string>
+ <string name="delete_sure_post">Smazat příspěvek</string>
+ <string name="confirm_delete_multi_media">Odstranit zvolené položky?</string>
+ <string name="confirm_delete_media">Odstranit zvolenou položku?</string>
+ <string name="reader_title_photo_viewer">%1$d z %2$d</string>
+ <string name="reader_label_comments_closed">Komentáře jsou uzavřené</string>
+ <string name="reader_label_comment_count_single">Jeden komentář</string>
+ <string name="reader_label_liked_by">Tohle se líbí</string>
+ <string name="reader_label_like">To se mi líbí</string>
+ <string name="signing_out">Odhlásit se </string>
+ <string name="reader_empty_comments">Žádné komentáře</string>
+ <string name="reader_label_comment_count_multi">%,d komentářů</string>
+ <string name="reader_label_view_original">Zobrazit původní článek</string>
+ <string name="reader_label_comments_on">Komentáře k</string>
+ <string name="error_publish_empty_post">Nelze publikovat prázdný příspěvek</string>
+ <string name="error_refresh_unauthorized_posts">Nemáte oprávnění pro zobrazení, nebo pro úpravy příspěvků</string>
+ <string name="error_refresh_unauthorized_pages">Nemáte oprávnění pro zobrazení, nebo pro úpravy stránek</string>
+ <string name="error_refresh_unauthorized_comments">Nemáte oprávnění pro zobrazení, nebo pro úpravy komentářů</string>
+ <string name="reader_empty_posts_liked">Žádné příspěvky jste neoznačili jako To se mi líbí</string>
+ <string name="older_month">Starší než měsíc</string>
+ <string name="older_two_days">Starší než 2 dny</string>
+ <string name="older_last_week">Starší než týden</string>
+ <string name="uploading_total">Nahrávám %1$d z %2$d</string>
+ <string name="comment">Komentář</string>
+ <string name="comment_trashed">Komentář byl přesunut do koše</string>
+ <string name="comment_reply_to_user">Odpovědět uživateli %s</string>
+ <string name="posting_post">Publikuji "%s"</string>
+ <string name="faq_button">Časté dotazy</string>
+ <string name="browse_our_faq_button">Prohledat časté dotazy</string>
+ <string name="nux_help_description">Pro odpovědi na časté dotazy navštivte naše centrum nápovědy nebo fórum</string>
+ <string name="reader_empty_posts_in_tag">Neexistuje žádný příspěvek s tímto štítkem</string>
+ <string name="stats_no_blog">Statistiky pro daný web nebylo možné načíst</string>
+ <string name="sending_content">Nahrávání obsahu %s</string>
+ <string name="posts_empty_list">Žádné příspěvky. Přejete si nějaký vytvořit?</string>
+ <string name="pages_empty_list">Žádné stránky. Přejete si nějakou vytvořit?</string>
+ <string name="media_empty_list_custom_date">Žádné soubory v tomto časovém intervalu</string>
+ <string name="agree_terms_of_service">Založením účtu souhlasíte s našimi %1$sPodmínkami používání služby%2$s</string>
+ <string name="mnu_comment_liked">Líbilo se mi</string>
+ <string name="create_new_blog_wpcom">Vytvořit web na WordPress.com</string>
+ <string name="more">Více</string>
+ <string name="new_blog_wpcom_created">WordPress.com web byl vytvořen!</string>
+ <string name="select_a_blog">Vybrat web WordPress</string>
+ <string name="reader_empty_followed_blogs_title">Zatím nesledujete žádné weby</string>
+ <string name="reader_toast_err_generic">Nepodařilo se provést tuto akci</string>
+ <string name="reader_toast_blog_blocked">Příspěvky z tohoto webu se již nebudou zobrazovat</string>
+ <string name="reader_toast_err_block_blog">Nepodařilo se zablokovat tento web</string>
+ <string name="reader_menu_block_blog">Zablokovat tento web</string>
+ <string name="contact_us">Kontaktujte nás</string>
+ <string name="hs__conversation_detail_error">Popište problém, který vidíte</string>
+ <string name="hs__username_blank_error">Zadejte platné jméno</string>
+ <string name="hs__new_conversation_header">Chat podpory</string>
+ <string name="hs__conversation_header">Chat podpory</string>
+ <string name="hs__invalid_email_error">Zadejte platnou emailovou adresu</string>
+ <string name="current_location">Současná poloha</string>
+ <string name="add_location">Přidat polohu</string>
+ <string name="edit_location">Upravit</string>
+ <string name="search_location">Hledat</string>
+ <string name="search_current_location">Lokalizovat</string>
+ <string name="preference_send_usage_stats_summary">Automaticky odesílat statistiky používání pro zlepšení WordPressu pro Android</string>
+ <string name="preference_send_usage_stats">Odesílat statistiky</string>
+ <string name="update_verb">Aktualizovat</string>
+ <string name="schedule_verb">Rozvrh</string>
+ <string name="reader_toast_err_get_blog_info">Nepodařilo se zobrazit tento web</string>
+ <string name="reader_toast_err_already_follow_blog">Tento web už sledujete</string>
+ <string name="reader_toast_err_follow_blog">Přidání webu mezi sledované se nezdařilo</string>
+ <string name="reader_toast_err_unfollow_blog">Zrušení sledování tohoto webu se nezdařilo</string>
+ <string name="reader_empty_recommended_blogs">Nejsou k dispozici žádné doporučené weby</string>
+ <string name="reader_page_followed_tags">Štítky které sledujete</string>
+ <string name="reader_page_followed_blogs">Stránky které sledujete</string>
+ <string name="reader_hint_add_tag_or_url">Zadejte URL nebo štítek, který chcete sledovat</string>
+ <string name="reader_label_followed_blog">Web sledován</string>
+ <string name="reader_title_subs">Štítky a weby</string>
+ <string name="reader_title_blog_preview">Web čtenáře</string>
+ <string name="reader_title_tag_preview">Štítek čtenáře</string>
+ <string name="reader_label_tag_preview">Příspěvek ozančený: %s</string>
+ <string name="saving">Ukládání...</string>
+ <string name="media_empty_list">Žádná média</string>
+ <string name="ptr_tip_message">Tip: Aktualizujte tažením dolů</string>
+ <string name="help">Nápověda</string>
+ <string name="forgot_password">Zapomněl jsi heslo?</string>
+ <string name="forums">Diskusní fórum</string>
+ <string name="help_center">Centrum nápovědy</string>
+ <string name="ssl_certificate_error">Neplatný SSL certifikát</string>
+ <string name="ssl_certificate_ask_trust">Pokud se k tomuto webu obvykle připojujete bez problémů, pak tato chyba znamená, že se někdo snaží podvrhnout vaši identitu a neměli byste dále pokračovat. Chcete i přesto důvěřovat tomuto certifikátu?</string>
+ <string name="out_of_memory">Nedostatek paměti v zařízení</string>
+ <string name="no_network_message">Není dostupná žádná síť</string>
+ <string name="gallery_error">Multimediální položka nemohla být stažena</string>
+ <string name="wait_until_upload_completes">Čekej dokud neskončí nahrávání</string>
+ <string name="theme_fetch_failed">Chyba při získávání motivů</string>
+ <string name="theme_set_failed">Chyba při nastavení motivu</string>
+ <string name="theme_auth_error_message">Ujisti se, že máš práva k nastavení motivu</string>
+ <string name="comments_empty_list">Žádný komentář</string>
+ <string name="mnu_comment_unspam">Není spam</string>
+ <string name="no_account">Nebyl nalezen žádný WordPress účet. Přidej účet a zkus znovu</string>
+ <string name="sdcard_message">Pro nahrání media musí být připojená SD karta</string>
+ <string name="stats_empty_comments">Žádné komentáře</string>
+ <string name="stats_bar_graph_empty">Statistika není dostupná</string>
+ <string name="reply_failed">Odpověď se nezdařila</string>
+ <string name="notifications_empty_list">Žádná oznámení</string>
+ <string name="error_delete_post">Nastala chyba při mazání %s</string>
+ <string name="error_refresh_posts">Články momentálně nemohly být obnoveny</string>
+ <string name="error_refresh_pages">Stránky momentálně nemohly být obnoveny</string>
+ <string name="error_refresh_notifications">Oznámení momentálně nemohla být obnovena</string>
+ <string name="error_refresh_comments">Komentáře momentálně nemohly být obnoveny</string>
+ <string name="error_refresh_stats">Statistika momentálně nemohla být obnovena</string>
+ <string name="error_generic">Nastala chyba</string>
+ <string name="error_edit_comment">Nastala chyba při opravě komentáře</string>
+ <string name="error_upload">Nastala chyba při nahrávání %s</string>
+ <string name="error_load_comment">Nemohl nahrát komentář</string>
+ <string name="error_downloading_image">Chyba při stahování obrázku</string>
+ <string name="passcode_wrong_passcode">Špatný PIN</string>
+ <string name="invalid_email_message">Tvoje emailová adresa není platná</string>
+ <string name="invalid_password_message">Heslo musí obsahovat alespoň 4 znaky</string>
+ <string name="invalid_username_too_short">Uživatelské jméno musí být delší než 4 znaky</string>
+ <string name="invalid_username_too_long">Uživatelské jméno musí být kratší než 61 znaků</string>
+ <string name="username_only_lowercase_letters_and_numbers">Uživatelské jméno může obsahovat pouze malá písmena (a-z) a číslice</string>
+ <string name="username_required">Vlož uživatelské jméno</string>
+ <string name="username_not_allowed">Uživatelské jméno není povoleno</string>
+ <string name="username_must_be_at_least_four_characters">Uživatelské jméno musí být nejméně 4 znaky</string>
+ <string name="username_contains_invalid_characters">Uživatelské jméno nesmí obsahovat znak "_"</string>
+ <string name="username_must_include_letters">Uživatelské Jméno musí mít alespoň 1 písmeno (a-z)</string>
+ <string name="email_invalid">Vlož platnou emailovou adresu</string>
+ <string name="email_not_allowed">Tato emailová adresa není dovolena</string>
+ <string name="username_exists">Toto uživatelské jméno již existuje</string>
+ <string name="email_exists">Tato emailová adresa je již používána</string>
+ <string name="username_reserved_but_may_be_available">Toto uživatelské jméno je momentálně rezervované, ale může být dostupné během pár dnů</string>
+ <string name="nux_cannot_log_in">Nemohli jsme tě přihlásit</string>
+ <string name="could_not_remove_account">Nelze odstranit web</string>
+ <string name="error_moderate_comment">Nastala chyba při moderování</string>
+ <string name="username_or_password_incorrect">Vložené uživatelské jméno nebo heslo je neplatné</string>
+ <string name="blog_name_cant_be_used">Nemůžeš použít tuto adresu webu</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Adresa webu musí být kratší než 64 znaků</string>
+ <string name="blog_name_reserved">Tento web je rezervován</string>
+ <string name="blog_name_reserved_but_may_be_available">Tento web je momentálně rezervován ale může být dostupný během pár dnů</string>
+ <string name="blog_name_exists">Tento web již existuje</string>
+ <string name="blog_name_not_allowed">Tato adresa webu není povolena</string>
+ <string name="blog_name_must_be_at_least_four_characters">Adresa webu musí obsahovat alespoň 4 znaky</string>
+ <string name="blog_name_contains_invalid_characters">Adresa webu nesmí obsahovat znak "_"</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Adresa webu může obsahovat pouze malá písmena (a-z) a číslice</string>
+ <string name="blog_name_required">Vlož adresu webu</string>
+ <string name="invalid_url_message">Zkontrolujte platnost zadané adresy webu</string>
+ <string name="blog_not_found">Nastala chyba při přístupu na tento web</string>
+ <string name="no_site_error">Nemohu se připojit k serveru WordPress</string>
+ <string name="adding_cat_failed">Chyba při přidávání rubriky</string>
+ <string name="adding_cat_success">Rubrubrika úspěšně přidána</string>
+ <string name="cat_name_required">Název rubriky je vyžadován</string>
+ <string name="category_automatically_renamed">Název rubriky %1$s není platný. Byl změněn na %2$s.</string>
+ <string name="reader_share_link">Odkaz pro sdílení</string>
+ <string name="view_site">Zobrazit stránku</string>
+ <string name="new_media">Nahrát soubor</string>
+ <string name="new_post">Vytvořit příspěvěk</string>
+ <string name="preview_post">Zobrazit příspěvek</string>
+ <string name="preview_page">Zobrazit stránku</string>
+ <string name="delete_draft">Smazat koš</string>
+ <string name="author_email">Email autora</string>
+ <string name="author_name">Jméno autora</string>
+ <string name="trash">Koš</string>
+ <string name="delete_post">Smazat příspěvěk</string>
+ <string name="delete_page">Smazat stránku</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Koš</string>
+ <string name="edit_comment">Upravit komentář</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Koš</string>
+ <string name="page_settings">Nastavení stránky</string>
+ <string name="upload_failed">Nahrávání se nezdařilo</string>
+ <string name="link_enter_url_text">Text odkazu</string>
+ <string name="create_a_link">Vytvořit odkaz</string>
+ <string name="media_gallery_edit">Upravit galerii</string>
+ <string name="cancel_edit">Zrušit úpravy</string>
+ <string name="media_gallery_settings_title">Nastavení galerie</string>
+ <string name="connection_error">Ztráta spojení</string>
+ <string name="add_comment">Přidat komentář</string>
+ <string name="post_format">Formát příspěvku</string>
+ <string name="remove_account">Odstranit web</string>
+ <string name="account_details">Detaily účtu</string>
+ <string name="edit_post">Uprav článek</string>
+ <string name="comment_status_unapproved">Čekající na schválení</string>
+ <string name="scaled_image_error">Zadejte platný rozsah hodnot</string>
+ <string name="themes_live_preview">Živý náhled</string>
+ <string name="theme_current_theme">Aktuální šablona</string>
+ <string name="theme_premium_theme">Prémiová šablona</string>
+ <string name="local_draft">Místní návrh</string>
+ <string name="saving_changes">Ukládám změny</string>
+ <string name="author_url">URL autora</string>
+ <string name="hint_comment_content">Komentář</string>
+ <string name="content_required">Je vyžadován komentář</string>
+ <string name="toast_comment_unedited">Komentář nebyl změněn</string>
+ <string name="comment_added">Komentář byl úspěšně přidán</string>
+ <string name="view_in_browser">Zobrazit v prohlížeči</string>
+ <string name="add_new_category">Přidat novou rubriku</string>
+ <string name="category_name">Název rubriky</string>
+ <string name="post_not_found">Nastala chyba při načítání příspěvku. Aktualizujte své příspěvky a zkuste to znovu.</string>
+ <string name="learn_more">Více informací</string>
+ <string name="media_gallery_image_order">Řazení obrázků</string>
+ <string name="media_gallery_num_columns">Počet sloupců</string>
+ <string name="media_gallery_type_thumbnail_grid">Mřížka náhledů</string>
+ <string name="media_error_no_permission">Nemáte oprávnění pro zobrazení mediální knihovny</string>
+ <string name="cannot_delete_multi_media_items">Některá média momentálně nelze odstranit. Zkuste to později.</string>
+ <string name="horizontal_alignment">Vodorovné zarovnání</string>
+ <string name="file_not_found">Nepodařilo se najít mediální soubor pro nahrávání. Nebyl odstraněný nebo přesunutý?</string>
+ <string name="comment_status_approved">Schváleno</string>
+ <string name="mnu_comment_approve">Schválit</string>
+ <string name="mnu_comment_unapprove">Zamítnout</string>
+ <string name="dlg_approving_comments">Schvaluji</string>
+ <string name="dlg_unapproving_comments">Zamítám</string>
+ <string name="dlg_spamming_comments">Označuji jako spam</string>
+ <string name="dlg_trashing_comments">Přesunuji do koše</string>
+ <string name="dlg_confirm_trash_comments">Přesunout do koše?</string>
+ <string name="sure_to_cancel_edit_comment">Ukončit úpravy tohoto komentáře?</string>
+ <string name="share_action_post">Nový příspěvek</string>
+ <string name="image_settings">Vlastnosti obrázku</string>
+ <string name="fatal_db_error">Vyskytla se chyba při vytváření databáze aplikace. Zkuste přeinstalovat tuto aplikaci.</string>
+ <string name="jetpack_message_not_admin">Plugin Jetpack je pro statistiky vyžadován. Kontaktujte administrátora webu.</string>
+ <string name="reader_toast_err_add_tag">Nelze přidat tento štítek</string>
+ <string name="required_field">Povinné pole</string>
+ <string name="reader_toast_err_remove_tag">Nelze odstranit tento štítek</string>
+ <string name="open_source_licenses">Licence Open source</string>
+ <string name="local_changes">Místní změny</string>
+ <string name="privacy_policy">Zásady ochrany osobních údajů</string>
+ <string name="reader_title_applog">Protokol aplikace</string>
+ <string name="post_settings">Nastavení příspěvku</string>
+ <string name="location_not_found">Neznámé umístění</string>
+ <string name="http_authorization_required">Je vyžadována autorizace</string>
+ <string name="http_credentials">Přihlašovací údaje k HTTP (nepovinné)</string>
+ <string name="blog_title_invalid">Neplatný popis webu</string>
+ <string name="blog_name_must_include_letters">Webová adresa musí mít alespoň 1 písmeno (a-z)</string>
+ <string name="blog_name_invalid">Neplatná adresa webu</string>
+ <string name="share_action_media">Knihovna médií</string>
+ <string name="pending_review">Čekající na schválení</string>
+ <string name="notifications_empty_all">Doposud nejsou k dispozici žádná oznámení.</string>
+ <string name="error_blog_hidden">Tento web je skrytý a proto nemohl být načten. Zpřístupněte jej v nastavení a zkuste to znovu.</string>
+ <string name="file_error_create">Nepodařilo se vytvořit soubor temp pro nahrávání. Ujistěte se, že máte na svém zařízení dostatek volného místa.</string>
+ <string name="trash_yes">Odstranit</string>
+ <string name="trash_no">Ponechat</string>
+ <string name="post_not_published">Příspěvek není zveřejněn</string>
+ <string name="page_not_published">Stránka není zveřejněna</string>
+ <string name="site_address">Vaše vlastní adresa (URL)</string>
+ <string name="wordpress_blog">WordPress web</string>
+ <string name="xmlrpc_error">Nelze se připojit. Vlož celou cestu k xmlrpc.php na svém webu a zkus znovu.</string>
+ <string name="add_account_blog_url">Zadej URL webu</string>
+ <string name="blog_removed_successfully">Web byl úspěšně odstraněn</string>
+ <string name="select_categories">Vyberte rubriku</string>
+ <string name="category_slug">Název rubriky v URL (nepovinné)</string>
+ <string name="category_desc">Popis rubriky (nepovinné)</string>
+ <string name="category_parent">Nadřazená rubrika (nepovinné):</string>
+ <string name="invalid_site_url_message">Zkontrolujte platnost URL adresy stránky</string>
+ <string name="email_hint">Email</string>
+ <string name="email_cant_be_used_to_signup">Tuto emailovou adresu pro registraci nemůžete použít. Tento poskytovatel blokuje některé z našich emailů. Použijte jiného emailového poskytovatele.</string>
+ <string name="email_reserved">Tato emailová adresa již byla použita. Zkontrolujte prosím, zda nemáte v doručené poště aktivační email. Pokud aktivaci neprovedete hned, můžete zkusit znovu za pár dní.</string>
+ <string name="share_url_page">Sdílení stránky</string>
+ <string name="deleting_page">Mazání stránky</string>
+ <string name="deleting_post">Smazat příspěvek</string>
+ <string name="share_url_post">Sdílet příspěvek</string>
+ <string name="share_link">Sdílet odkaz</string>
+ <string name="creating_your_account">Vytvářím váš účet</string>
+ <string name="creating_your_site">Vytvářím vaši stránku</string>
+ <string name="reader_empty_posts_in_tag_updating">Načítám příspěvky...</string>
+ <string name="error_refresh_media">Nastal chyba při aktualizaci knihovny médií. Zkuste to prosím později.</string>
+ <string name="video">Video</string>
+ <string name="download">Stahování média</string>
+ <string name="reader_label_reply">Znovu</string>
+ <string name="comment_spammed">Komentář byl označen jako spam</string>
+ <string name="reader_likes_you_and_multi">To se líbí vám a dalším %,d</string>
+ <string name="reader_likes_multi">To se mi líbí (%,d)</string>
+ <string name="reader_toast_err_get_comment">Nepodařilo se získat tento komentář</string>
+ <string name="cant_share_no_visible_blog">Nemůžete sdílet s WordPressem bez viditelného webu</string>
+ <string name="select_date">Vyberte datum</string>
+ <string name="pick_photo">Vyberte fotku</string>
+ <string name="pick_video">Vyberte video</string>
+ <string name="validating_user_data">Ověřuji data uživatele</string>
+ <string name="validating_site_data">Ověřuji data stránky</string>
+ <string name="reader_toast_err_get_post">Tento příspěvek se nedá načíst</string>
+ <string name="select_time">Zadejte čas</string>
+ <string name="reader_likes_you_and_one">To se líbí vám a jedné další osobě</string>
+ <string name="reader_empty_followed_blogs_description">Ale nebojte se, klepněte jen na ikonu v pravém horním rohu, a začněte poznávat!</string>
+ <string name="account_two_step_auth_enabled">Tento účet má zapnutou dvoufázovou autorizaci. Navštivte nastavení zabezpečení na WordPress.com a nechte si vygenerovat heslo pro konkrétní aplikaci.</string>
+ <string name="nux_tap_continue">Pokračovat</string>
+ <string name="signing_in">Přihlašuji...</string>
+ <string name="password_invalid">Potřebujete bezpečnější heslo. Ujistěte se, že používáte více než 7 znaků, kombinujete velká a malá písmena, čísla, nebo speciální znaky.</string>
+ <string name="nux_welcome_create_account">Vytvořit účet</string>
+ <string name="nux_oops_not_selfhosted_blog">Přihlásit se k WordPress.com</string>
+ <string name="nux_add_selfhosted_blog">Přidejte vlastní web</string>
+ <string name="nux_tutorial_get_started_title">Můžeme začít!</string>
+ <string name="reader_btn_share">Sdílet</string>
+ <string name="username_invalid">Špatné uživatelské jméno</string>
+ <string name="media_add_popup_title">Přidat do knihovny medií</string>
+ <string name="media_add_new_media_gallery">Vytvořit galerii</string>
+ <string name="empty_list_default">Tento seznam je prázdný</string>
+ <string name="select_from_media_library">Vyberte z knihovny medií</string>
+ <string name="jetpack_not_found">Plugin Jetpack nebyl nalezen</string>
+ <string name="reader_untitled_post">(bez názvu)</string>
+ <string name="reader_share_subject">Sdíleno z %s</string>
+ <string name="reader_btn_follow">Sledovat</string>
+ <string name="jetpack_message">Plugin Jetpack je vyžadován pro zobrazení statistik. Přejete si nainstalovat tento plugin?</string>
+ <string name="reader_btn_unfollow">Sleduji</string>
+ <string name="reader_hint_comment_on_comment">Odpovědět na komentář...</string>
+ <string name="reader_likes_one">To se líbí jedné osobě</string>
+ <string name="reader_toast_err_share_intent">Nelze sdílet</string>
+ <string name="reader_toast_err_view_image">Nelze zobrazit obrázek</string>
+ <string name="reader_toast_err_url_intent">Nelze otevřít %s</string>
+ <string name="reader_empty_followed_tags">Nesledujete žádné štítky</string>
+ <string name="create_account_wpcom">Vytvořte si účet na WordPress.com</string>
+ <string name="connecting_wpcom">Připojuji se k WordPress.com</string>
+ <string name="reader_toast_err_comment_failed">Nepodařilo se publikovat váš komentář</string>
+ <string name="reader_toast_err_tag_exists">Tento štítek už sledujete</string>
+ <string name="reader_toast_err_tag_invalid">Toto není platný štítek</string>
+ <string name="reader_label_removed_tag">%s byl odebrán</string>
+ <string name="reader_label_added_tag">%s byl přidán</string>
+ <string name="limit_reached">Dosažen limit. Můžete to zkusit za 1 minutu. Další pokusy před vypršením limitu, tento limit pouze prodlouží. Pokud se domníváte, že je to chyba, kontaktujte prosím podporu.</string>
+ <string name="reader_likes_only_you">To se vám líbí</string>
+ <string name="button_next">Další</string>
+ <string name="stats_entry_posts_and_pages">Nadpis</string>
+ <string name="stats_entry_tags_and_categories">Téma</string>
+ <string name="stats_totals_views">Zobrazení</string>
+ <string name="stats_timeframe_days">Dny</string>
+ <string name="stats_timeframe_months">Měsíce</string>
+ <string name="media_gallery_image_order_reverse">Obrátit</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="stats_timeframe_today">Dnes</string>
+ <string name="stats_timeframe_yesterday">Včera</string>
+ <string name="images">Obrázky</string>
+ <string name="media_gallery_image_order_random">Náhodný</string>
+ <string name="all">Vše</string>
+ <string name="passcode_set">Nastavení PINU</string>
+ <string name="passcode_re_enter_passcode">Potvrdit PIN</string>
+ <string name="passcode_change_passcode">Změnit PIN</string>
+ <string name="passcode_enter_passcode">Zadat PIN</string>
+ <string name="passcode_enter_old_passcode">Zadat starý PIN</string>
+ <string name="passcode_manage">Spravovat PIN</string>
+ <string name="stats_totals_plays">Přehrávání</string>
+ <string name="stats_view_tags_and_categories">Štítky a rubriky</string>
+ <string name="theme_auth_error_title">Nepodařilo se načíst vzhled</string>
+ <string name="share_action_title">Přidat do ...</string>
+ <string name="theme_activating_button">Aktivace</string>
+ <string name="theme_set_success">Úspěšně nastavený vzhled!</string>
+ <string name="themes_features_label">Funkce</string>
+ <string name="theme_activate_button">Aktivovat</string>
+ <string name="themes_details_label">Detaily</string>
+ <string name="media_edit_success">Aktualizováno</string>
+ <string name="media_edit_failure">Aktualizace se nezdařila</string>
+ <string name="media_edit_description_hint">Zadejte popis zde</string>
+ <string name="media_edit_title_hint">Zadejte název zde</string>
+ <string name="media_edit_caption_text">Titulek</string>
+ <string name="media_edit_description_text">Popis</string>
+ <string name="media_gallery_type">Typ</string>
+ <string name="media_edit_title_text">Nadpis</string>
+ <string name="share_action">Sdílet</string>
+ <string name="stats">Statistiky</string>
+ <string name="media_edit_caption_hint">Zadejte titulek</string>
+ <string name="themes">Šablony</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="media_add_popup_capture_photo">Zachytit fotografii</string>
+ <string name="media_gallery_type_squares">Čtverce</string>
+ <string name="media_gallery_type_circles">Kruhy</string>
+ <string name="stats_view_clicks">Kliknutí</string>
+ <string name="stats_timeframe_weeks">Týdny</string>
+ <string name="unattached">Nepřiřazeno</string>
+ <string name="custom_date">Vlastní datum</string>
+ <string name="post_excerpt">Stručný obsah</string>
+ <string name="passcode_preference_title">Zámek PIN</string>
+ <string name="passcode_turn_off">Vypnout zámek PIN</string>
+ <string name="passcode_turn_on">Zapnout zámek PIN</string>
+ <string name="media_add_popup_capture_video">Natočit video</string>
+ <string name="media_gallery_type_tiled">Dlaždice</string>
+ <string name="stats_view_visitors_and_views">Návštěvnící a zobrazení</string>
+ <string name="stats_entry_country">Země</string>
+ <string name="stats_totals_clicks">Prokliky</string>
+ <string name="stats_view_referrers">Odkazující</string>
+ <string name="stats_entry_referrers">Odkazující</string>
+ <string name="upload">Nahrát</string>
+ <string name="discard">Zahodit</string>
+ <string name="notifications">Notifikace</string>
+ <string name="note_reply_successful">Odpověď publikována</string>
+ <string name="follows">Sledovat</string>
+ <string name="new_notifications">%d nových notifikací</string>
+ <string name="more_notifications">a %d dalších.</string>
+ <string name="sign_in">Přihlásit se</string>
+ <string name="loading">Nahrávám...</string>
+ <string name="httpuser">HTTP uživ.jméno</string>
+ <string name="httppassword">HTTP heslo</string>
+ <string name="error_media_upload">Došlo k chybě během nahrávání médií</string>
+ <string name="publish_date">Publikovat</string>
+ <string name="content_description_add_media">Přidat média</string>
+ <string name="post_content">Obsah článku (klepněte pro přidání textu a mediálního souboru)</string>
+ <string name="incorrect_credentials">Nesprávné uživatelské jméno nebo heslo.</string>
+ <string name="password">Heslo</string>
+ <string name="username">Uživatelské jméno</string>
+ <string name="reader">Čtenář</string>
+ <string name="featured_in_post">Vložit obrázek do obsahu příspěvku</string>
+ <string name="no_network_title">Síť není dostupná</string>
+ <string name="pages">Stránky</string>
+ <string name="posts">Příspěvky</string>
+ <string name="anonymous">Anonymní</string>
+ <string name="width">Šířka</string>
+ <string name="page">Stránka</string>
+ <string name="post">Příspěvek</string>
+ <string name="caption">Popis (nepovinné)</string>
+ <string name="featured">Použít jako náhledový obrázek</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">Jmémo blogera</string>
+ <string name="scaled_image">Šířka zmenšeného obrázku</string>
+ <string name="upload_scaled_image">Nahrát a odkazovat na zmenšený obrázek</string>
+ <string name="scheduled">Načasováno</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Nahrávám...</string>
+ <string name="app_title">WordPress pro Android</string>
+ <string name="version">Verze</string>
+ <string name="tos">Podmínky služby</string>
+ <string name="max_thumbnail_px_width">Výchozí šířka obrázku</string>
+ <string name="image_alignment">Zarovnání</string>
+ <string name="refresh">Obnovit</string>
+ <string name="untitled">Bez názvu</string>
+ <string name="edit">Upravit</string>
+ <string name="post_id">Příspěvek</string>
+ <string name="page_id">Stránka</string>
+ <string name="immediately">Okamžitě</string>
+ <string name="post_password">Heslo (nepovinné)</string>
+ <string name="quickpress_add_alert_title">Nastavit název zkráceného odkazu</string>
+ <string name="settings">Nastavení</string>
+ <string name="today">Dnes</string>
+ <string name="share_url">Sdílej adresu</string>
+ <string name="quickpress_add_error">Název zkráceného odkazu nemůže být prázdný</string>
+ <string name="quickpress_window_title">Vyberte web pro zkrácený odkaz QuickPress</string>
+ <string name="draft">Koncept</string>
+ <string name="post_private">Soukromé</string>
+ <string name="publish_post">Publikováno</string>
+ <string name="upload_full_size_image">Nahrát a odkazovat na plný obrázek</string>
+ <string name="title">Název</string>
+ <string name="categories">Rubriky</string>
+ <string name="tags_separate_with_commas">Štítky (oddělte štítky čárkami)</string>
+ <string name="dlg_deleting_comments">Odstraňování komentářů</string>
+ <string name="notification_sound">Přehrát zvuk oznámení</string>
+ <string name="notification_blink">Blikat při upozornění</string>
+ <string name="notification_vibrate">Vibrovat</string>
+ <string name="status">Stav</string>
+ <string name="sdcard_title">Vyžadována SD karta</string>
+ <string name="location">Umístění</string>
+ <string name="select_video">Vybrat video z galerie</string>
+ <string name="media">Média</string>
+ <string name="delete">Vymazat</string>
+ <string name="none">Žádný</string>
+ <string name="blogs">Weby</string>
+ <string name="select_photo">Vyberte fotografii z galerie</string>
+ <string name="reply">Odpovědět</string>
+ <string name="preview">Náhled</string>
+ <string name="on">na</string>
+ <string name="cancel">Zrušit</string>
+ <string name="save">Uložit</string>
+ <string name="add">Přidat</string>
+ <string name="notification_settings">Nastavení upozornění</string>
+ <string name="yes">Ano</string>
+ <string name="no">Ne</string>
+ <string name="error">Chyba</string>
+ <string name="category_refresh_error">Chyba obnovení rubrik</string>
+</resources>
diff --git a/WordPress/src/main/res/values-cy/strings.xml b/WordPress/src/main/res/values-cy/strings.xml
new file mode 100644
index 000000000..ef89695a7
--- /dev/null
+++ b/WordPress/src/main/res/values-cy/strings.xml
@@ -0,0 +1,1102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="editor_failed_uploads_switch_html">Mae rhai lwythi cyfryngau wedi methu. Nid oes modd newid i\'r modd HTML\n yn y cyflwr hwn. Tynnu pob cyfrwng sydd wedi methu a pharhau?</string>
+ <string name="format_bar_description_html">Modd HTML</string>
+ <string name="visual_editor">Golygydd gweledol</string>
+ <string name="image_thumbnail">Llun bach delwedd</string>
+ <string name="format_bar_description_ul">Rhestr heb ei drefnu</string>
+ <string name="format_bar_description_ol">Rhestr wedi\'i drefnu</string>
+ <string name="format_bar_description_more">Mewnosod rhagor</string>
+ <string name="format_bar_description_media">Mewnosod cyfrwng</string>
+ <string name="format_bar_description_strike">Llinell Drwodd</string>
+ <string name="format_bar_description_quote">Dyfyniad bloc</string>
+ <string name="format_bar_description_link">Mewnosod dolen</string>
+ <string name="format_bar_description_italic">Italig</string>
+ <string name="format_bar_description_underline">Tanlinellu</string>
+ <string name="image_settings_save_toast">Mae\'r newidiadau wedi eu cadw</string>
+ <string name="image_caption">Egluryn</string>
+ <string name="image_alt_text">Testun Amgen</string>
+ <string name="image_link_to">Cyswllt i</string>
+ <string name="image_width">Lled</string>
+ <string name="format_bar_description_bold">Trwm</string>
+ <string name="image_settings_dismiss_dialog_title">Dileu\'r newidiadau heb eu cadw?</string>
+ <string name="stop_upload_dialog_title">Atal y llwytho?</string>
+ <string name="stop_upload_button">Atal y Llwytho</string>
+ <string name="alert_error_adding_media">Digwyddodd gwall wrth fewnosod y cyfrwng</string>
+ <string name="alert_action_while_uploading">Rydych wrthi\'n llwytho cyfrwng. Arhoswch nes i hyn orffen.</string>
+ <string name="alert_insert_image_html_mode">Nid oes modd mewnosod y cyfrwng yn uniongyrchol yn y modd HTML.Ewch i\'r modd gweledol</string>
+ <string name="uploading_gallery_placeholder">Llwytho\'r oriel...</string>
+ <string name="invite_sent">wedi anfon gwahoddiad yn llwyddiannus</string>
+ <string name="tap_to_try_again">Tapiwch i geisio eto!</string>
+ <string name="invite_message_info">(Dewisol) Gallwch gynnwys neges bersonol o hyd at 500 nod a fydd yn cael ei gynnwys yn y gwahoddiad i\'r defnyddiwr.</string>
+ <string name="invite_message_remaining_other">%d nod yn weddill</string>
+ <string name="invite_message_remaining_one">1 nod yn weddill</string>
+ <string name="invite_message_remaining_zero">0 nod yn weddill</string>
+ <string name="invite_invalid_email">Nid yw\'r cyfeiriad \'%s\' yn ddilys</string>
+ <string name="invite_message_title">Neges Bersonol</string>
+ <string name="invite_already_a_member">Mae yna eisoes aelod sy\'n defnyddio\'r enw defnyddiwr \'%s\'</string>
+ <string name="invite_username_not_found">Nid oes defnyddiwr yn defnyddio\'r enw \'%s\'</string>
+ <string name="invite">Gwahodd</string>
+ <string name="invite_names_title">Enwau defnyddwyr neu E-byst</string>
+ <string name="signup_succeed_signin_failed">Mae eich cyfrif wedi ei greu ond bu gwall wrth i ni eich mewngofnodi\n Ceisiwch fewngofnodi gyda\'ch enw defnyddiwr a chyfrinair newydd.</string>
+ <string name="send_link">Anfon dolen</string>
+ <string name="my_site_header_external">Allanol</string>
+ <string name="invite_people">Gwahodd Pobl</string>
+ <string name="label_clear_search_history">Clirio hanes chwilio</string>
+ <string name="dlg_confirm_clear_search_history">Clirio hanes chwilio?</string>
+ <string name="reader_empty_posts_in_search_description">Dim cofnodion gan %s yn eich iaith</string>
+ <string name="reader_label_post_search_running">Chwilio…</string>
+ <string name="reader_label_related_posts">Darllen Perthnasol</string>
+ <string name="reader_empty_posts_in_search_title">Heb ganfod unrhyw gofnod</string>
+ <string name="reader_label_post_search_explainer">Chwilio pob blog WordPress.com gyhoeddus</string>
+ <string name="reader_hint_post_search">Chwilio WordPress.com</string>
+ <string name="reader_title_related_post_detail">Cofnodion Perthnasol</string>
+ <string name="reader_title_search_results">Chwilio am %s</string>
+ <string name="preview_screen_links_disabled">Mae dolenni wedi eu hanalluogi ar y sgrin rhagolwg</string>
+ <string name="draft_explainer">Mae\'r cofnod yn ddrafft sydd heb ei gyhoeddi</string>
+ <string name="send">Anfon</string>
+ <string name="person_remove_confirmation_title">Tynnu %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Nid yw\'r gwefannau ar y rhestr hon wedi cofnodi dim yn ddiweddar</string>
+ <string name="people">Pobl</string>
+ <string name="edit_user">Golygu Defnyddiwr</string>
+ <string name="role">Rôl</string>
+ <string name="error_remove_user">Methu tynnu defnyddiwr</string>
+ <string name="error_update_role">Methu diweddaru rôl defnyddiwr</string>
+ <string name="gravatar_camera_and_media_permission_required">Y caniatâd sydd eu hange i ddewis neu gipio llun</string>
+ <string name="error_updating_gravatar">Gwall wrth ddiweddaru eich Gravatar</string>
+ <string name="error_locating_image">Gwall wrth ganfod y ddelwedd wedi ei thocio</string>
+ <string name="error_refreshing_gravatar">Gwall wrth ail-lwytho eich Gravatar</string>
+ <string name="gravatar_tip">Newydd! Tapiwch eich Gravatar i\'w newid!</string>
+ <string name="error_cropping_image">Bu gwall wrth docio eich delwedd.</string>
+ <string name="launch_your_email_app">Cychwyn eich rhaglen e-bost</string>
+ <string name="checking_email">Gwirio e-bost</string>
+ <string name="not_on_wordpress_com">Nid ar WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Ddim ar gael ar hyn o bryd. Rhowch eich cyfrinair</string>
+ <string name="check_your_email">Gwiriwch eich e-bost</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Derbyn dolen wedi ei anfon at eich e-bost er mwyn mewngofnodi\'n syth</string>
+ <string name="logging_in">Mewngofnodi</string>
+ <string name="enter_your_password_instead">Rhowch eich cyfrinair yn hytrach</string>
+ <string name="web_address_dialog_hint">Dangos yn gyhoeddus pan fyddwch yn gwneud sylw</string>
+ <string name="jetpack_not_connected_message">Mae\'r ategyn Jetpack wedi\'i osod, ond heb ei gysylltu â WordPress.com. Ydych chi am gysylltu Jetpack?</string>
+ <string name="username_email">E-bost neu enw defnyddiwr</string>
+ <string name="jetpack_not_connected">Nid yw\'r ategyn jetpack wedi ei gysylltu</string>
+ <string name="new_editor_reflection_error">Nid yw\'r golygydd gweledol yn gydnaws â\'ch dyfais. Cafodd\n ei analluogi\'n awtomatig.</string>
+ <string name="stats_insights_latest_post_no_title">(dim teit)</string>
+ <string name="capture_or_pick_photo">Cipio neu ddewis llun</string>
+ <string name="plans_post_purchase_text_themes">Mae gennych nawr fynediad diderfyn i themâu Premiwm. Er mwyn cychwyn arni, gallwch gael rhagolwg o unrhyw thema ar eich gwefan.</string>
+ <string name="plans_post_purchase_button_themes">Pori\'r Themâu</string>
+ <string name="plans_post_purchase_title_themes">Canfod y thema Premiwm perffaith</string>
+ <string name="plans_post_purchase_button_video">Cychwyn cofnod newydd</string>
+ <string name="plans_post_purchase_text_video">Gallwch lwytho a lletya fideos ar eich gwefan gyda VideoPress a\'ch storio cyfryngau ehangach.</string>
+ <string name="plans_post_purchase_title_video">Bywiogwch eich cofnodion gyda fideo</string>
+ <string name="plans_post_purchase_button_customize">Cyfaddasu eich Gwefan</string>
+ <string name="plans_post_purchase_text_customize">Bellach mae gennych fynediad i ffontiau, lliwiau a galluoedd golygu CSS.</string>
+ <string name="plans_post_purchase_text_intro">Mae eich gwefan yn llawn cyffro! Ewch i archwilio nodweddion newydd eich gwefan a dewis be i ddechrau.</string>
+ <string name="plans_post_purchase_title_customize">Cyfaddasu Ffontiau a Lliwiau</string>
+ <string name="plans_post_purchase_title_intro">Chi bia hwn nawr, i ffwrdd â chi!</string>
+ <string name="export_your_content_message">Bydd cofnodion, tudalennau a gosodiadau yn cael eu e-bost atoch i %s.</string>
+ <string name="plan">Cynllun</string>
+ <string name="plans">Cynlluniau</string>
+ <string name="plans_loading_error">Methu llwytho cynlluniau</string>
+ <string name="export_your_content">Allforio eich cynnwys</string>
+ <string name="exporting_content_progress">Allforio cynnwys…</string>
+ <string name="export_email_sent">Anfonwyd yr e-bost allforio!</string>
+ <string name="premium_upgrades_message">Mae gennych uwchraddiadau premiwm gweithredol ar eich gwefan. Diddymwch eich uwchraddiadau cyn dileu eich gwefan.</string>
+ <string name="show_purchases">Dangos y nwyddau â brynwyd</string>
+ <string name="checking_purchases">Gwirio\'r nwyddau â brynwyd</string>
+ <string name="premium_upgrades_title">Uwchraddiadau Premiwm</string>
+ <string name="purchases_request_error">Aeth rhywbeth o\'i le. Methu prynu nwyddau.</string>
+ <string name="delete_site_progress">Wrthi\'n dileu gwefan…</string>
+ <string name="delete_site_summary">Nid oes modd dadwneud y weithred hon. Bydd dileu\'r eich gwefan yn cael gwared ar yr holl gynnwys, cyfranwyr, a pharthau\'r wefan.</string>
+ <string name="delete_site_hint">Dileu\'r wefan</string>
+ <string name="export_site_hint">Allforio eich gwefan i ffeil XML</string>
+ <string name="are_you_sure">Ydych chi\'n Siŵr?</string>
+ <string name="export_site_summary">Os ydych yn siŵr, cymrwch eich amser ac allforio eich cynnwys nawr. Ni fydd modd ei adfer yn y dyfodol.</string>
+ <string name="keep_your_content">Cadw Eich Cynnwys</string>
+ <string name="domain_removal_hint">Ni fydd y parth yn gweithio unwaith y dilëwch eich gwefan</string>
+ <string name="domain_removal_summary">Byddwch yn ofalus! Bydd dileu eich gwefan hefyd yn dileu\'r eich parth(au), isod.</string>
+ <string name="primary_domain">Prif Barth</string>
+ <string name="domain_removal">Tynnu\'r Parth</string>
+ <string name="error_deleting_site_summary">Bu gwall wrth ddileu\'r wefan. Cysylltwch a\'n gwasanaeth cymorth am gymorth pellach</string>
+ <string name="error_deleting_site">Gwall wrth ddileu\'r wefan</string>
+ <string name="confirm_delete_site_prompt">Teipiwch %1$s yn y maes isod i gadarnhau. Wedyn, bydd eich gwefan wedi mynd am byth.</string>
+ <string name="site_settings_export_content_title">Allforio cynnwys</string>
+ <string name="contact_support">Cysylltu â\'n cefnogaeth</string>
+ <string name="confirm_delete_site">Cadarnhau Dileu Gwefan</string>
+ <string name="start_over_text">Os ydych eisiau gwefan ond dim o\'r cofnodion a thudalennau sydd gennych ar hyn o bryd gall ein tîm cymorth ddileu eich cofnodion, tudalennau, cyfryngau, a sylwadau ar eich cyfer.\n\nBydd hyn yn cadw eich gwefan ac URL byw, ond yn rhoi dechrau newydd i\'ch creu cynnwys. Cysylltwch â ni i ni gael clirio eich cynnwys cyfredol.</string>
+ <string name="site_settings_start_over_hint">Cychwyn eich gwefan eto</string>
+ <string name="let_us_help">Gadewch i ni Helpu</string>
+ <string name="me_btn_app_settings">Gosodiadau Apiau</string>
+ <string name="start_over">Cychwyn Eto</string>
+ <string name="editor_remove_failed_uploads">Tynnu llwythi sydd wedi methu</string>
+ <string name="editor_toast_failed_uploads">Mae rhai lwythi cyfryngau wedi methu. Nid oes modd cadw na chyhoeddi\n eich cofnod yn y cyflwr hwn. Hoffech chi dynnu pob cyfrwng sydd wedi methu?</string>
+ <string name="comments_empty_list_filtered_trashed">Dim sylwadau yn y Sbwriel</string>
+ <string name="site_settings_advanced_header">Uwch</string>
+ <string name="comments_empty_list_filtered_pending">Dim sylwadau o Dan Ystyriaeth</string>
+ <string name="comments_empty_list_filtered_approved">Dim sylwadau Cymeradwy</string>
+ <string name="button_done">Gorffen</string>
+ <string name="button_skip">Hepgor</string>
+ <string name="site_timeout_error">Methwyd cysylltu â gwefan WordPress oherwydd gwall Terfyn Amser.</string>
+ <string name="xmlrpc_malformed_response_error">Doedd dim modd cysylltu. Ymatebodd y gosodiad WordPress gyda dogfen XML-RPC annilys.</string>
+ <string name="xmlrpc_missing_method_error">Doedd dim modd cysylltu. Mae\'r dulliau XML-RPC angenrheidiol ar goll o\'r gweinydd.</string>
+ <string name="post_format_status">Statws</string>
+ <string name="post_format_video">Fideo</string>
+ <string name="theme_free">Rhad ac am Ddim</string>
+ <string name="theme_all">Y Cyfan</string>
+ <string name="theme_premium">Premiwm</string>
+ <string name="post_format_chat">Sgwrs</string>
+ <string name="post_format_gallery">Oriel</string>
+ <string name="post_format_image">Delwedd</string>
+ <string name="post_format_link">Dolen</string>
+ <string name="post_format_quote">Dyfyniad</string>
+ <string name="post_format_standard">Safonol</string>
+ <string name="notif_events">Gwybodaeth am gyrsiau a digwyddiadau WordPress.com (ar-lein a byw).</string>
+ <string name="post_format_aside">Neilleb</string>
+ <string name="post_format_audio">Sain</string>
+ <string name="notif_surveys">Cyfleoedd i gymryd rhan mewn ymchwil a holiaduron WordPress.com.</string>
+ <string name="notif_tips">Awgrymiadau ar sut i gael y gorau o WordPress.com.</string>
+ <string name="notif_community">Cymuned</string>
+ <string name="replies_to_my_comments">Atebion i fy sylwadau</string>
+ <string name="notif_suggestions">Awgrymiadau</string>
+ <string name="notif_research">Ymchwil</string>
+ <string name="site_achievements">Llwyddiannau\'r wefan</string>
+ <string name="username_mentions">Cyfeiriadau gan yr enw defnyddiwr</string>
+ <string name="likes_on_my_posts">Hoffi fy nghofnodion</string>
+ <string name="site_follows">Mae\'r wefan yn dilyn</string>
+ <string name="likes_on_my_comments">Hoffi fy sylwadau</string>
+ <string name="comments_on_my_site">Sylwadau ar fy ngwefan</string>
+ <string name="site_settings_list_editor_summary_other">%d eitem</string>
+ <string name="site_settings_list_editor_summary_one">1 eitem</string>
+ <string name="approve_auto_if_previously_approved">Sylwadau defnyddwyr hysbys</string>
+ <string name="approve_auto">Pob defnyddiwr</string>
+ <string name="approve_manual">Dim sylwadau</string>
+ <string name="site_settings_paging_summary_other">%d sylw i\'r dudalen</string>
+ <string name="site_settings_paging_summary_one">1 sylw i\'r dudalen</string>
+ <string name="site_settings_multiple_links_summary_other">Angen cymeradwyaeth ar gyfer mwy na %d dolen</string>
+ <string name="site_settings_multiple_links_summary_one">Angen cymeradwyaeth ar gyfer mwy nag 1 dolen</string>
+ <string name="site_settings_multiple_links_summary_zero">Angen cymeradwyaeth ar gyfer mwy na 0 dolen</string>
+ <string name="detail_approve_auto">Cymeradwyo sylwadau pawb yn awtomatig.</string>
+ <string name="detail_approve_auto_if_previously_approved">Cymeradwyo\'n awtomatig os yw\'r defnyddiwr eisoes wedi cymeradwyo sylw</string>
+ <string name="detail_approve_manual">Angen cymeradwyaeth â llaw ar gyfer sylwadau pawb.</string>
+ <string name="filter_trashed_posts">Yn y Sbwriel</string>
+ <string name="days_quantity_one">1 diwrnod</string>
+ <string name="days_quantity_other">%d diwrnod</string>
+ <string name="filter_published_posts">Cyhoeddwyd</string>
+ <string name="filter_draft_posts">Drafftiau</string>
+ <string name="filter_scheduled_posts">Amserlenwyd</string>
+ <string name="pending_email_change_snackbar">Cliciwch ar y ddolen dilysu yn yr e-bost a anfonwyd at i%1$s i gadarnhau eich cyfeiriad newydd</string>
+ <string name="primary_site">Prif wefan</string>
+ <string name="web_address">Cyfeiriad Gwe</string>
+ <string name="editor_toast_uploading_please_wait">Rydych wrthi\'n llwytho cyfryngau. Arhoswch nes i hyn orffen.</string>
+ <string name="error_refresh_comments_showing_older">Nid oes modd adnewyddu sylwadau ar hyn o bryd - yn dangos sylwadau hŷn</string>
+ <string name="editor_post_settings_set_featured_image">Gosod Delwedd Nodwedd</string>
+ <string name="editor_post_settings_featured_image">Delwedd Nodwedd</string>
+ <string name="new_editor_promo_desc">Mae\'r ap WordPress Android bellach yn cynnwys golygydd gweledol\n newydd hardd. Rhowch gynnig arno drwy greu cofnod newydd.</string>
+ <string name="new_editor_promo_title">Golygydd newydd sbon</string>
+ <string name="new_editor_promo_button_label">Gwych, diolch!</string>
+ <string name="visual_editor_enabled">Galluogwyd y Golygydd Gweledol</string>
+ <string name="editor_content_placeholder">Rhanwch eich stori yma…</string>
+ <string name="editor_page_title_placeholder">Teitl Tudalen</string>
+ <string name="editor_post_title_placeholder">Teitl Cofnod</string>
+ <string name="email_address">Cyfeiriad e-bost</string>
+ <string name="preference_show_visual_editor">Dangos y golygydd gweledol</string>
+ <string name="dlg_sure_to_delete_comments">Dileu\'r sylwadau hyn yn barhaol?</string>
+ <string name="preference_editor">Golygydd</string>
+ <string name="dlg_sure_to_delete_comment">Dileu\'r sylwadau hyn yn barhaol?</string>
+ <string name="mnu_comment_delete_permanently">Dileu</string>
+ <string name="comment_deleted_permanently">Wedi dileu\'r sylw</string>
+ <string name="mnu_comment_untrash">Adfer</string>
+ <string name="comments_empty_list_filtered_spam">Dim sylwadau Sbam</string>
+ <string name="could_not_load_page">Methu llwytho\'r dudalen</string>
+ <string name="comment_status_all">Y Cyfan</string>
+ <string name="interface_language">Iaith y Rhyngwyneb</string>
+ <string name="off">Diffodd</string>
+ <string name="about_the_app">Am yr ap</string>
+ <string name="error_post_account_settings">Nid oedd modd cadw manylion eich cyfrif</string>
+ <string name="error_post_my_profile">Nid oedd modd cadw eich proffil</string>
+ <string name="error_fetch_account_settings">Methu adfer gosodiadau eich cyfrif</string>
+ <string name="error_fetch_my_profile">Methu adfer eich proffil</string>
+ <string name="stats_widget_promo_ok_btn_label">Iawn, rwy\'n ei ddeall</string>
+ <string name="stats_widget_promo_desc">Ychwanegwch y teclyn i\'ch sgrin cartref er mwyn cael mynediad at eich Ystadegau mewn un clic.</string>
+ <string name="stats_widget_promo_title">Teclyn Ystadegau\'r Sgrin Cartref</string>
+ <string name="site_settings_unknown_language_code_error">Heb adnabd y cod iaith</string>
+ <string name="site_settings_threading_dialog_description">Caniatáu sylwadau i\'w nythu mewn edafedd.</string>
+ <string name="site_settings_threading_dialog_header">Edafedd hyd at</string>
+ <string name="remove">Tynnu</string>
+ <string name="search">Chwilio</string>
+ <string name="add_category">Ychwanegu categori</string>
+ <string name="disabled">Analluogwyd</string>
+ <string name="site_settings_image_original_size">Maint Gwreiddiol</string>
+ <string name="privacy_private">Mae eich gwefan yn weladwy i chi yn unig a defnyddwyr rydych wedi eu cymeradwyo</string>
+ <string name="privacy_public_not_indexed">Mae eich gwefan yn weladwy i bawb, ond mae\'n gofyn i beiriannau chwilio i beidio mynegeio eich gwefan</string>
+ <string name="privacy_public">Mae eich gwefan yn weladwy i bawb, ac o bosib wedi ei fynegeio gan beiriannau chwilio</string>
+ <string name="about_me_hint">Ychydig o eiriau amdanoch chi…</string>
+ <string name="public_display_name_hint">Bydd yr enw dangos yn defnyddio eich enw defnyddiwr os yw wedi ei osod</string>
+ <string name="about_me">Amdanaf fi</string>
+ <string name="public_display_name">Enw dangos cyhoeddus</string>
+ <string name="my_profile">Fy Mhroffil</string>
+ <string name="first_name">Enw cyntaf</string>
+ <string name="last_name">Enw olaf</string>
+ <string name="site_privacy_public_desc">Caniatáu i beiriannau chwilio fynegeio\'r wefan</string>
+ <string name="site_privacy_hidden_desc">Annog peiriannau chwilio i beidio mynegeio\'r wefan</string>
+ <string name="site_privacy_private_desc">Hoffwn i fy ngwefan fod yn breifat, yn weladwy yn unig i ddefnyddwyr rwy\'n eu dewis</string>
+ <string name="cd_related_post_preview_image">Delwedd sy\'n perthyn ar ôl y rhagolwg</string>
+ <string name="error_post_remote_site_settings">Methu cadw manylion y wefan</string>
+ <string name="error_fetch_remote_site_settings">Methu estyn manylion y wefan</string>
+ <string name="error_media_upload_connection">Digwyddodd gwall cysylltu wrth lwytho\'r cyfrwng</string>
+ <string name="site_settings_disconnected_toast">Datgysylltwyd, analluogwyd y golygu.</string>
+ <string name="site_settings_unsupported_version_error">Fersiwn o WordPress sydd heb ei gynnal</string>
+ <string name="site_settings_multiple_links_dialog_description">Bydd angen cymeradwyaeth ar gyfer sylwadau sy\'n cynnwys mwy na\'r nifer yma o ddolenni.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Cau\'n awtomatig</string>
+ <string name="site_settings_close_after_dialog_description">Cau\'r sylwadau ar erthyglau yn awtomatig.</string>
+ <string name="site_settings_paging_dialog_description">Torri\'r edefyn sylw i dudalennau lluosog.</string>
+ <string name="site_settings_paging_dialog_header">Sylwadau fesul tudalen</string>
+ <string name="site_settings_close_after_dialog_title">Cau sylwadau</string>
+ <string name="site_settings_blacklist_description">Pan fydd sylw yn cynnwys unrhyw un o\'r geiriau hyn yn ei gynnwys, enw, URL, e-bost, neu IP, bydd yn cael ei farcio fel sbam. Gallwch gynnig geiriau rhannol, felly bydd "press" yn canfod "WordPress"</string>
+ <string name="site_settings_hold_for_moderation_description">Pan fydd sylw yn cynnwys unrhyw un o\'r geiriau hyn yn ei gynnwys, enw, URL, e-bost neu IP, bydd yn cael ei gadw yn y rhes cymedroli. Gallwch gynnig geiriau rhannol, felly bydd "press" yn canfod "WordPress"</string>
+ <string name="site_settings_list_editor_input_hint">Teipiwch air neu ymadrodd</string>
+ <string name="site_settings_list_editor_no_items_text">Dim eitemau</string>
+ <string name="site_settings_learn_more_caption">Gallwch diystyru\'r gosodiadau hyn ar gyfer cofnodion unigol.</string>
+ <string name="site_settings_rp_preview3_site">yn "Diweddariad"</string>
+ <string name="site_settings_rp_preview3_title">Canolbwynt Diweddaru: VideoPress ar gyfer Priodasau</string>
+ <string name="site_settings_rp_preview2_site">yn "Apiau"</string>
+ <string name="site_settings_rp_preview2_title">Mae Ap WordPress Android yn Derbyn Adnewyddiad Sylweddol</string>
+ <string name="site_settings_rp_preview1_site">yn "Symudol"</string>
+ <string name="site_settings_rp_preview1_title">Diweddariad iPhone/iPad Mawr Nawr ar Gael</string>
+ <string name="site_settings_rp_show_images_title">Dangos Delweddau</string>
+ <string name="site_settings_rp_show_header_title">Dangos Pennyn</string>
+ <string name="site_settings_rp_switch_summary">Mae Cofnodion Perthynol yn dangos cynnwys perthnasol o\'ch gwefan islaw eich cofnodion.</string>
+ <string name="site_settings_rp_switch_title">Dangos Cofnodion sy\'n Perthyn</string>
+ <string name="site_settings_delete_site_hint">Dileu data eich gwefan o\'r ap</string>
+ <string name="site_settings_blacklist_hint">Mae sylwadau sy\'n cyfateb i hidl yn cael eu marcio fel sbam</string>
+ <string name="site_settings_moderation_hold_hint">Mae sylwadau sy\'n cyfateb i hidl yn cael gosod yn y rhes cymedroli</string>
+ <string name="site_settings_multiple_links_hint">Anwybyddu terfyn dolen gan ddefnyddwyr hysbys</string>
+ <string name="site_settings_whitelist_hint">Rhaid i awdur y sylw fod wedi cyfrannu sylw cymeradwy yn y gorffennol</string>
+ <string name="site_settings_user_account_required_hint">Rhaid i ddefnyddwyr fod wedi\'u cofrestru ac wedi mewngofnodi cyn cynnig sylw</string>
+ <string name="site_settings_identity_required_hint">Rhaid i awdur sylw rhoi enw ac e-bost</string>
+ <string name="site_settings_manual_approval_hint">Rhaid i sylwadau gael eu cymeradwyo â llaw</string>
+ <string name="site_settings_paging_hint">Dangos sylwadau mewn darnau o faint penodol</string>
+ <string name="site_settings_threading_hint">Caniatáu sylwadau nythog i ddyfnder penodol</string>
+ <string name="site_settings_sort_by_hint">Pennu\'r drefn mae\'r sylwadau yn cael eu dangos</string>
+ <string name="site_settings_close_after_hint">Peidio â chaniatáu sylwadau ar ôl amser penodol</string>
+ <string name="site_settings_receive_pingbacks_hint">Caniatáu hysbysiadau dolenni o flogiau eraill</string>
+ <string name="site_settings_send_pingbacks_hint">Ceisio hysbysu unrhyw flogiau sy\'n cael eu cysylltu atyn nhw yn yr erthygl</string>
+ <string name="site_settings_allow_comments_hint">Caniatáu darllenwyr i gofnodi sylwadau</string>
+ <string name="site_settings_discussion_hint">Gweld a newid gosodiadau trafodaethau ar eich gwefannau</string>
+ <string name="site_settings_more_hint">Gweld pob gosodiad trafodaeth sydd ar gael</string>
+ <string name="site_settings_related_posts_hint">Dangos neu guddio cofnodion cysylltiedig yn y darllenydd</string>
+ <string name="site_settings_upload_and_link_image_hint">Galluogi llwytho delwedd maint llawn bob tro</string>
+ <string name="site_settings_image_width_hint">Newid maint delweddau mewn cofnodion i\'r lled hwn</string>
+ <string name="site_settings_format_hint">Gosod fformat cofnod newydd</string>
+ <string name="site_settings_category_hint">Gosod categori cofnod newydd</string>
+ <string name="site_settings_location_hint">Ychwanegu data lleoliad yn awtomatig at eich cofnodion</string>
+ <string name="site_settings_password_hint">Newid eich cyfrinair</string>
+ <string name="site_settings_username_hint">Cyfrif y defnyddiwr cyfredol</string>
+ <string name="site_settings_language_hint">Yr iaith mae\'r blog yn cael ei ysgrifennu ynddi yn bennaf</string>
+ <string name="site_settings_privacy_hint">Rheoli pwy sy\'n gallu gweld eich gwefan</string>
+ <string name="site_settings_address_hint">Nid yw newid eich cyfeiriad yn cael ei gynnal ar hyn o bryd</string>
+ <string name="site_settings_tagline_hint">Disgrifiad byr neu ymadrodd bachog i ddisgrifio eich blog</string>
+ <string name="site_settings_title_hint">Mewn ychydig eiriau, yn esboniwch beth yw natur y wefan hon</string>
+ <string name="site_settings_whitelist_known_summary">Sylwadau gan ddefnyddwyr hysbys</string>
+ <string name="site_settings_whitelist_all_summary">Sylwadau gan pob defnyddiwr</string>
+ <string name="site_settings_threading_summary">%d lefel</string>
+ <string name="site_settings_privacy_private_summary">Preifat</string>
+ <string name="site_settings_privacy_hidden_summary">Cudd</string>
+ <string name="site_settings_delete_site_title">Dileu Gwefan</string>
+ <string name="site_settings_privacy_public_summary">Cyhoeddus</string>
+ <string name="site_settings_blacklist_title">Rhestr ddu</string>
+ <string name="site_settings_moderation_hold_title">Dal ar gyfer Cymedroli</string>
+ <string name="site_settings_multiple_links_title">Dolenni mewn sylwadau</string>
+ <string name="site_settings_whitelist_title">Gymeradwyo\'n awtomatig</string>
+ <string name="site_settings_threading_title">Edafu</string>
+ <string name="site_settings_paging_title">Tudalennu</string>
+ <string name="site_settings_sort_by_title">Trefnu yn ôl</string>
+ <string name="site_settings_account_required_title">Rhaid llofnodi defnyddwyr</string>
+ <string name="site_settings_identity_required_title">Rhaid cynnwys enw ac e-bost</string>
+ <string name="site_settings_receive_pingbacks_title">Derbyn Hysbysiad Cyfeirio</string>
+ <string name="site_settings_send_pingbacks_title">Anfon Hysbysiadau Cyfeirio</string>
+ <string name="site_settings_allow_comments_title">Caniatáu Sylwadau</string>
+ <string name="site_settings_default_format_title">Fformat Rhagosodedig</string>
+ <string name="site_settings_default_category_title">Categori Rhagosodedig</string>
+ <string name="site_settings_location_title">Galluogi Lleoliad</string>
+ <string name="site_settings_address_title">Cyfeiriad</string>
+ <string name="site_settings_title_title">Teitl y Wefan</string>
+ <string name="site_settings_tagline_title">Llinell tag</string>
+ <string name="site_settings_this_device_header">Y ddyfais hon</string>
+ <string name="site_settings_discussion_new_posts_header">Rhagosodiadau ar gyfer cofnodion ewydd</string>
+ <string name="site_settings_account_header">Cyfrif</string>
+ <string name="site_settings_writing_header">Ysgrifennu</string>
+ <string name="newest_first">Diweddaraf yn gyntaf</string>
+ <string name="site_settings_general_header">Cyfredinol</string>
+ <string name="discussion">Trafodaeth</string>
+ <string name="privacy">Preifatrwydd</string>
+ <string name="related_posts">Cofnodion sy\'n Perthyn</string>
+ <string name="comments">Sylwadau</string>
+ <string name="close_after">Cau ar ôl</string>
+ <string name="oldest_first">Hynaf yn gyntaf</string>
+ <string name="media_error_no_permission_upload">Nid oes gennych ganiatâd i lwytho cyfrwng i\'r wefan</string>
+ <string name="never">Byth</string>
+ <string name="unknown">Anhysbys</string>
+ <string name="reader_err_get_post_not_found">Nid yw\'r cofnod yma\'n bodoli bellach</string>
+ <string name="reader_err_get_post_not_authorized">Nid ydych wedi eich awdurdodi i weld y cofnod hwn</string>
+ <string name="reader_err_get_post_generic">Methu estyn y cofnod hwn</string>
+ <string name="blog_name_no_spaced_allowed">Nid oes modd i gyfeiriad gwefan gynnwys bylchau</string>
+ <string name="invalid_username_no_spaces">Nid oes modd i enw defnyddiwr gynnwys bylchau</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Nid yw\'r gwefannau rydych yn eu dilyn gofnodi dim yn ddiweddar</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Dim cofnodion diweddar</string>
+ <string name="media_details_copy_url_toast">URL wedi ei gopïo i\'r clipfwrdd</string>
+ <string name="edit_media">Golygu cyfrwng</string>
+ <string name="media_details_copy_url">Copïo URL</string>
+ <string name="media_details_label_date_uploaded">Wedi ei lwytho</string>
+ <string name="media_details_label_date_added">Ychwanegwyd</string>
+ <string name="selected_theme">Thema Dewiswyd</string>
+ <string name="could_not_load_theme">Methu llwytho\'r thema</string>
+ <string name="theme_activation_error">Aeth rhywbeth o\'i le. Methu cychwyn y thema</string>
+ <string name="theme_by_author_prompt_append"> gan %1$s</string>
+ <string name="theme_prompt">Diolch am ddewis %1$s</string>
+ <string name="theme_try_and_customize">Defnyddio a Chyfaddasu</string>
+ <string name="theme_view">Golwg</string>
+ <string name="theme_details">Manylion</string>
+ <string name="theme_support">Cefnogaeth</string>
+ <string name="theme_done">GORFFEN</string>
+ <string name="theme_manage_site">RHEOLI GWEFAN</string>
+ <string name="title_activity_theme_support">Themâu</string>
+ <string name="theme_activate">Gweithredu</string>
+ <string name="date_range_start_date">Dyddiad Cychwyn</string>
+ <string name="date_range_end_date">Dyddiad Gorffen</string>
+ <string name="current_theme">Thema Gyfredol</string>
+ <string name="customize">Cyfaddasu</string>
+ <string name="details">Manylion</string>
+ <string name="support">Cefnogaeth</string>
+ <string name="active">Gweithredol</string>
+ <string name="stats_referrers_spam_generic_error">Aeth rhywbeth o\'i le wrth wneud hynny. Nid yw\'r cyflwr sbam wedi newid.</string>
+ <string name="stats_referrers_marking_not_spam">Nodi fel nad sbam</string>
+ <string name="stats_referrers_unspam">Nid sbam</string>
+ <string name="stats_referrers_marking_spam">Marciwyd fel sbam</string>
+ <string name="theme_auth_error_authenticate">Methwyd estyn themâu: methwyd dilysu defnyddiwr</string>
+ <string name="post_published">Cyhoeddwyd y cofnod</string>
+ <string name="page_published">Cyhoeddwyd y dudalen</string>
+ <string name="post_updated">Diweddarwyd y cofnod</string>
+ <string name="page_updated">Diweddarwyd y dudalen</string>
+ <string name="stats_referrers_spam">Sbam</string>
+ <string name="theme_no_search_result_found">Ymddiheuriadau, heb ganfod thema.</string>
+ <string name="media_file_name">Enw ffeil: %s</string>
+ <string name="media_uploaded_on">Llwythwyd i fyny ar: %s</string>
+ <string name="media_dimensions">Dimensiynau: %s</string>
+ <string name="upload_queued">Mewn ciw</string>
+ <string name="media_file_type">Math o ffeil: %s</string>
+ <string name="reader_label_gap_marker">Llwytho rhagor o gofnodion</string>
+ <string name="notifications_no_search_results">Dim gwefannau\'n cydweddu â \'%s\'</string>
+ <string name="search_sites">Chwilio gwefannau</string>
+ <string name="notifications_empty_view_reader">Gweld y Darllennydd</string>
+ <string name="unread">Deb ei ddarllen</string>
+ <string name="notifications_empty_action_followers_likes">Denu sylw: rhowch sylw ar gofnod rydych wedi ei ddarllen.</string>
+ <string name="notifications_empty_action_comments">Ymunwch â sgwrs: rhowch sylw ar flogiau rydych yn eu dilyn.</string>
+ <string name="notifications_empty_action_unread">Ail-gynnwch sgwrs: ysgrifennwch gofnod newydd.</string>
+ <string name="notifications_empty_action_all">Byddwch egniol! Rhowch sylwadau ar flogiau rydych yn eu dilyn.</string>
+ <string name="notifications_empty_likes">Dim hoffi newydd i\'w dangos...eto.</string>
+ <string name="notifications_empty_followers">Dim dilynwyr newydd...eto.</string>
+ <string name="notifications_empty_comments">Dim sylwadau newydd...eto.</string>
+ <string name="notifications_empty_unread">Wedi dal i fyny!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Ewch i\'r Ystadegau yn yr ap a cheisio ychwanegu\'r teclyn yn hwyrach</string>
+ <string name="stats_widget_error_readd_widget">Tynnwch y teclyn a\'i ychwanegu eto</string>
+ <string name="stats_widget_error_no_visible_blog">Nid oes modd mynd i\'r Ystadegau heb flog gweladwy</string>
+ <string name="stats_widget_error_no_permissions">Nid yw eich cyfrif WordPress.com yn cael mynediad i\'r Ystadegau ar y blog</string>
+ <string name="stats_widget_error_no_account">Mewngofnodwch i WordPress</string>
+ <string name="stats_widget_error_generic">Methu llwytho\'r Ystadegau</string>
+ <string name="stats_widget_loading_data">Llwytho data…</string>
+ <string name="stats_widget_name_for_blog">Ystadegau %1$s Heddiw</string>
+ <string name="stats_widget_name">Ystadegau Heddiw WordPress</string>
+ <string name="add_location_permission_required">Caniatâd angenrheidiol er mwyn ychwanegu lleoliad</string>
+ <string name="add_media_permission_required">Caniatâd angenrheidiol er mwyn ychwanegu cyfrwng</string>
+ <string name="access_media_permission_required">Caniatâd angenrheidiol er mwyn cael mynediad at gyfrwng</string>
+ <string name="stats_enable_rest_api_in_jetpack">I weld eich ystadegau, galluogwch y modiwl JSON API yn Jetpack.</string>
+ <string name="error_open_list_from_notification">Cafodd y cofnod neu dudalen hwn ei gyhoeddi ar wefan arall</string>
+ <string name="reader_short_comment_count_multi">%s Sylw</string>
+ <string name="reader_short_comment_count_one">1 Sylw</string>
+ <string name="reader_label_submit_comment">ANFON</string>
+ <string name="reader_hint_comment_on_post">Ateb cofnod…</string>
+ <string name="reader_discover_visit_blog">Mynd i %s</string>
+ <string name="reader_discover_attribution_blog">Cofnodwyd yn wreiddiol ar %s</string>
+ <string name="reader_discover_attribution_author">Cofnodwyd yn wreiddiol gan %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Cofnodwyd yn wreiddiol gan %1$s ar %2$s</string>
+ <string name="reader_short_like_count_multi">%s Hoffi</string>
+ <string name="reader_short_like_count_one">1 Hoffi</string>
+ <string name="reader_label_follow_count">%,d dilynnwr</string>
+ <string name="reader_short_like_count_none">Hoffi</string>
+ <string name="reader_menu_tags">Golygu tagiau a blogiau</string>
+ <string name="reader_title_post_detail">Cofnod Darllennydd</string>
+ <string name="local_draft_explainer">Mae\'r cofnod yn ddrafft lleol sydd heb ei gyhoeddi</string>
+ <string name="local_changes_explainer">Mae gan y cofnod newidiadau lleol sydd heb eu cyhoeddi</string>
+ <string name="notifications_push_summary">Gosodiadau hysbysiadau sy\'n ymddangos ar eich dyfais.</string>
+ <string name="notifications_email_summary">Gosodiadau hysbysiadau sy\'n cael eu hanfon at e-bost sy\'n gysylltiedig â\'ch cyfrif.</string>
+ <string name="notifications_tab_summary">Gosodiadau hysbysiadau sy\'n ymddangos ar y tab Hysbysiadau.</string>
+ <string name="notifications_disabled">Mae hysbysiadau apiau wedi eu hanalluogi. Tapiwch yma i\'w galluogi yn y Gosodiadau.</string>
+ <string name="notification_types">Mathau o Hysbysiadau</string>
+ <string name="error_loading_notifications">Methu llwytho gosodiadau hysbysiadau</string>
+ <string name="replies_to_your_comments">Atebion i\'ch sylwadau</string>
+ <string name="comment_likes">Hoffi sylwadau</string>
+ <string name="app_notifications">Hysbysiadau apiau</string>
+ <string name="notifications_tab">Tab hysbysiadau</string>
+ <string name="email">E-bost</string>
+ <string name="notifications_comments_other_blogs">Sylwadau ar wefannau eraill</string>
+ <string name="notifications_wpcom_updates">Diweddariadau WordPress.com</string>
+ <string name="notifications_other">Arall</string>
+ <string name="notifications_account_emails">E-bost gan WordPress.com</string>
+ <string name="notifications_account_emails_summary">Byddwn yn anfon e-byst pwysig am eich cyfrif a hefyd cewch bethau ychwanegol a defnyddiol.</string>
+ <string name="notifications_sights_and_sounds">Golwg a Sain</string>
+ <string name="your_sites">Eich Gwefannau</string>
+ <string name="stats_insights_latest_post_trend">Mae wedi bod yn %1$s ers cyhoeddi %2$s. Dyma sut mae\'r cofnod wedi performio hyd yma…</string>
+ <string name="stats_insights_latest_post_summary">Crynodeb o\'r Cofnod Diweddaraf</string>
+ <string name="button_revert">Dychwelyd</string>
+ <string name="days_ago">%d diwrnod yn ôl</string>
+ <string name="yesterday">Ddoe</string>
+ <string name="connectionbar_no_connection">Dim cysylltiad</string>
+ <string name="page_trashed">Anfonwyd y dudalen i\'r Sbwriel</string>
+ <string name="post_deleted">Dilëwyd y dudalen</string>
+ <string name="post_trashed">Anfonwyd y cofnod i\'r Sbwriel</string>
+ <string name="stats_no_activity_this_period">Dim gweithgaredd yn ystod y cyfnod hwn</string>
+ <string name="trashed">I\'r Sbwriel</string>
+ <string name="button_back">Nôl</string>
+ <string name="page_deleted">Dilëwyd y dudalen</string>
+ <string name="button_stats">Ystadegau</string>
+ <string name="button_trash">Sbwriel</string>
+ <string name="button_preview">Rhagolwg</string>
+ <string name="button_view">Gweld</string>
+ <string name="button_edit">Golygu</string>
+ <string name="button_publish">Cyhoeddi</string>
+ <string name="my_site_no_sites_view_subtitle">Hoffech chi ychwanegu un?</string>
+ <string name="my_site_no_sites_view_title">Nid oes gennych wefannau WordPress eto.</string>
+ <string name="my_site_no_sites_view_drake">Darlun</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Nid oes gennych hawl i gael mynediad at y blog hwn</string>
+ <string name="reader_toast_err_follow_blog_not_found">Nid oedd modd canfod y blog hwn</string>
+ <string name="undo">Dadwneud</string>
+ <string name="tabbar_accessibility_label_my_site">Fy Ngwefan</string>
+ <string name="tabbar_accessibility_label_me">Fi</string>
+ <string name="passcodelock_prompt_message">Rhowch eich PIN</string>
+ <string name="editor_toast_changes_saved">Mae\'r newidiadau wedi eu cadw</string>
+ <string name="push_auth_expired">Daeth y cais i ben. Mewngofnodwch i WordPress.com a cheisio eto.</string>
+ <string name="stats_insights_best_ever">Golygon Gorau Erioed</string>
+ <string name="ignore">Anwybyddu</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% golwg</string>
+ <string name="stats_insights_most_popular_hour">Awr mwyaf poblogaidd</string>
+ <string name="stats_insights_most_popular_day">Diwrnod mwyaf poblogaidd</string>
+ <string name="stats_insights_popular">Diwrnod ac awr mwyaf poblogaidd</string>
+ <string name="stats_insights_today">Ystadegau Heddiw</string>
+ <string name="stats_insights_all_time">Cofnodion, golygon ac ymwelwyr erioed</string>
+ <string name="stats_insights">Mewnwelediadau</string>
+ <string name="stats_sign_in_jetpack_different_com_account">I weld eich ystadegau, mewngofnodwch i\'r cyfrif WordPress.com rydych wedi ei ddefnyddio i gysylltu Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Chwilio am eich Ystadegau Diweddar Eraill? Mae nhw wedi eu symud i\'r dudalen Mewnwelediad.</string>
+ <string name="me_disconnect_from_wordpress_com">Datgysylltwch o WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Cysylltwch i WordPress.com</string>
+ <string name="me_btn_login_logout">Mewngofnodi/Allgofnodi</string>
+ <string name="account_settings">Gosodiadau\'r Cyfrif</string>
+ <string name="me_btn_support">Cymorth a Chefnogaeth</string>
+ <string name="site_picker_cant_hide_current_site">Nid oedd "%s" wedi ei guddio gan taw hi yw\'r wefan gyfredol</string>
+ <string name="site_picker_create_dotcom">Creu gwefan WordPress.com</string>
+ <string name="site_picker_add_site">Ychwanegu gwefan</string>
+ <string name="site_picker_add_self_hosted">Ychwanegu gwefan hunanwesteio</string>
+ <string name="site_picker_edit_visibility">Dangos/cuddio gwefannau</string>
+ <string name="my_site_btn_view_admin">Gweld Gweinyddu</string>
+ <string name="my_site_btn_view_site">Gweld Gwefan</string>
+ <string name="site_picker_title">Dewis gwefan</string>
+ <string name="my_site_btn_switch_site">Newid Gwefan</string>
+ <string name="my_site_btn_blog_posts">Cofnodion Blog</string>
+ <string name="my_site_btn_site_settings">Gosodiadau</string>
+ <string name="my_site_header_look_and_feel">Golwg a Theimlad</string>
+ <string name="my_site_header_publish">Cyhoeddi</string>
+ <string name="my_site_header_configuration">Ffurfweddiad</string>
+ <string name="reader_label_new_posts_subtitle">Tapio i\'w dangos</string>
+ <string name="notifications_account_required">Mewngofnodi i WordPress.com am hysbysiadau</string>
+ <string name="stats_unknown_author">Awdur Anhysbys</string>
+ <string name="image_added">Ychwanegwyd delwedd</string>
+ <string name="signout">Datgysylltu</string>
+ <string name="deselect_all">Dad-ddewis y cyfan</string>
+ <string name="show">Dangos</string>
+ <string name="hide">Cuddio</string>
+ <string name="select_all">Dewis y cyfan</string>
+ <string name="sign_out_wpcom_confirm">Bydd datgysylltu eich cyfrif yn tynnu holl fanylion WordPress.com @%s o\'r ddyfais hon, gan gynnwys drafftiau a newidiadau lleol.</string>
+ <string name="select_from_new_picker">Dewis lluosog gyda dewisydd newydd</string>
+ <string name="stats_generic_error">Methu llwytho\'r ystadegau angenrheidiol</string>
+ <string name="no_device_videos">Dim fideos</string>
+ <string name="no_blog_images">Dim delweddau</string>
+ <string name="no_blog_videos">Dim fideos</string>
+ <string name="no_device_images">Dim delweddau</string>
+ <string name="error_loading_blog_images">Methu estyn delweddau</string>
+ <string name="error_loading_blog_videos">Methu estyn fideos</string>
+ <string name="error_loading_images">Gwall wrth lwytho delweddau</string>
+ <string name="error_loading_videos">Gwall wrth lwytho fideos</string>
+ <string name="loading_blog_images">Estyn delweddau</string>
+ <string name="loading_blog_videos">Estyn fideos</string>
+ <string name="no_media_sources">Methu estyn y cyfrwng</string>
+ <string name="loading_videos">Llwytho fideos</string>
+ <string name="loading_images">Llwytho delweddau</string>
+ <string name="no_media">Dim cyfrwng</string>
+ <string name="device">Dyfais</string>
+ <string name="language">Iaith</string>
+ <string name="add_to_post">Ychwanegu at Gofnod</string>
+ <string name="media_picker_title">Dewis cyfrwng</string>
+ <string name="take_photo">Cymryd llun</string>
+ <string name="take_video">Cymryd fideo</string>
+ <string name="tab_title_device_images">Delweddau Dyfais</string>
+ <string name="tab_title_device_videos">Fideos Dyfais</string>
+ <string name="tab_title_site_images">Delweddau Gwefan</string>
+ <string name="tab_title_site_videos">Fideos Gwefan</string>
+ <string name="media_details_label_file_name">Enw ffeil</string>
+ <string name="media_details_label_file_type">Math o ffeil</string>
+ <string name="error_publish_no_network">Methu cyhoeddi heb gysylltiad. Cadw fel drafft.</string>
+ <string name="editor_toast_invalid_path">Llwybr ffeil annilys</string>
+ <string name="verification_code">Cod dilysu</string>
+ <string name="invalid_verification_code">Cod dilysu annilys</string>
+ <string name="verify">Dilysu</string>
+ <string name="two_step_footer_label">Rhowch y cod o\'ch ap dilysu.</string>
+ <string name="two_step_footer_button">Anfonwch god drwy neges testun</string>
+ <string name="two_step_sms_sent">Gwiriwch eich neges testun am god dilysu.</string>
+ <string name="sign_in_jetpack">Mewngofnodwch i\'ch cyfrif WordPress.com i gysylltu â Jetpack.</string>
+ <string name="auth_required">Mewngofnodwch eto i barhau.</string>
+ <string name="reader_empty_posts_request_failed">Methu adfer cofnodion</string>
+ <string name="publisher">Cyhoeddwr:</string>
+ <string name="error_notification_open">Methu agor hysbysiadau</string>
+ <string name="stats_followers_total_email_paged">Dangos %1$d - %2$d o %3$s Dilynwr E-bost</string>
+ <string name="stats_search_terms_unknown_search_terms">Termau Chwilio Anhysbys</string>
+ <string name="stats_followers_total_wpcom_paged">Dangos %1$d - %2$d o %3$s Dilynwr WordPress.com</string>
+ <string name="stats_empty_search_terms_desc">Dysgwch ragor am eich traffig chwilio drwy edrych ar eiriau defnyddiodd eich ymwelwyr i ddod o hyd i\'ch gwefan.</string>
+ <string name="stats_empty_search_terms">Heb gofnodi geiriau chwilio</string>
+ <string name="stats_entry_search_terms">Gair Chwilio</string>
+ <string name="stats_view_authors">Awduron</string>
+ <string name="stats_view_search_terms">Geiriau Chwilio</string>
+ <string name="comments_fetching">Estyn sylwadau…</string>
+ <string name="pages_fetching">Estyn tudalennau…</string>
+ <string name="toast_err_post_uploading">Methu agor cofnod tra ei fod yn llwytho</string>
+ <string name="posts_fetching">Estyn cofnodion…</string>
+ <string name="media_fetching">Estyn cyfrwng…</string>
+ <string name="post_uploading">Llwytho</string>
+ <string name="stats_total">Cyfanswm</string>
+ <string name="stats_overall">O\'r Cyfan</string>
+ <string name="stats_period">Cyfnod</string>
+ <string name="logs_copied_to_clipboard">Cofnodion y rhaglen wedi\'u copïo i\'r clipfwrdd</string>
+ <string name="reader_label_new_posts">Cofnodion newydd</string>
+ <string name="reader_empty_posts_in_blog">Mae\'r blog yn wag</string>
+ <string name="stats_average_per_day">Cyfartaledd Diwrnod</string>
+ <string name="stats_recent_weeks">Wythnosau Diweddar</string>
+ <string name="error_copy_to_clipboard">Digwyddodd gwall wrth gopïo testun i\'r clipfwrdd</string>
+ <string name="reader_page_recommended_blogs">Gwefannau fyddech chi\'n eu hoffi</string>
+ <string name="stats_months_and_years">Misoedd a Blynyddoedd</string>
+ <string name="themes_fetching">Estyn themâu…</string>
+ <string name="stats_for">Ystadegau %s</string>
+ <string name="stats_other_recent_stats_label">Ystadegau Diweddar Eraill</string>
+ <string name="stats_view_all">Gweld y cyfan</string>
+ <string name="stats_view">Gweld</string>
+ <string name="stats_followers_months">%1$d mis</string>
+ <string name="stats_followers_a_year">Blwyddyn</string>
+ <string name="stats_followers_years">%1$d blwyddyn</string>
+ <string name="stats_followers_a_month">Mis</string>
+ <string name="stats_followers_minutes">%1$d munud</string>
+ <string name="stats_followers_an_hour_ago">awr yn ôl</string>
+ <string name="stats_followers_hours">%1$d awr</string>
+ <string name="stats_followers_a_day">Diwrnod</string>
+ <string name="stats_followers_days">%1$d diwrnod</string>
+ <string name="stats_followers_a_minute_ago">munud yn ôl</string>
+ <string name="stats_followers_seconds_ago">eiliad yn ôl</string>
+ <string name="stats_followers_total_email">Cyfanswm Dilynwyr E-bost: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">E-bost</string>
+ <string name="stats_followers_total_wpcom">Cyfanswm Dilynwyr WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Cyfanswm cofnodion gyda sylwadau dilynwyr: %1$s</string>
+ <string name="stats_comments_by_authors">Yn ôl Awdur</string>
+ <string name="stats_comments_by_posts_and_pages">Yn ôl Cofnod a Thudalen</string>
+ <string name="stats_empty_followers_desc">Cadwch gyfrif o\'ch niferoedd o ddilynwyr a pha mor hir mae bob un wedi bod yn dilyn eich gwefan.</string>
+ <string name="stats_empty_followers">Dim dilynwyr</string>
+ <string name="stats_empty_publicize_desc">Cadwch gyfrif o\'ch niferoedd o ddilynwyr o\'r rhwydweithiau cymdeithasol amrywilo gan ddefnyddio publicize.</string>
+ <string name="stats_empty_publicize">Dim dilynwyr publicize wedi ei cofnodi</string>
+ <string name="stats_empty_video">Dim fideos wedi eu chwarae</string>
+ <string name="stats_empty_video_desc">Os ydych wedi llwytho fideos drwy VideoPress, gweld sawl gwaith mae nhw wedi cael eu gwylio.</string>
+ <string name="stats_empty_comments_desc">Os ydych yn caniatáu sylwadau ar eich gwefan, dilynwch eich prif sylwebwyr a deall pa gynnwys sy\'n achosi\'r sgyrsiau mwyaf brwd, ar sail y 1000 sylw diweddaraf.</string>
+ <string name="stats_empty_tags_and_categories_desc">Derbyn trosolwg o\'r pynciau mwyaf poblogaidd ar eich gwefan, yn ôl y prif gofnodion yn ystod yr wythnos diwethaf.</string>
+ <string name="stats_empty_top_authors_desc">Dilyn golygon ar gofnodion eich cyfrannwyr ac edrych yn fanwl ar gynnwys mwyaf poblogaidd pob awdur.</string>
+ <string name="stats_empty_tags_and_categories">Dim cofnodion wedi\'u tagio na thudalennau wedi eu darllen</string>
+ <string name="stats_empty_clicks_desc">Pan mae eich cynnwys yn cynnwys dolenni i wefannau eraill, byddwch yn gweld pa rai mae eich ymwelwyr yn clicio arnynt amlaf.</string>
+ <string name="stats_empty_referrers_desc">Dysgu rhagor am welededd eich gwefan drwy edrych ar y gwefannau a pheiriannau chwilio sy\'n anfon y mwyaf o draffig atoch</string>
+ <string name="stats_empty_clicks_title">Dim cliciau wedi eu cofnodi</string>
+ <string name="stats_empty_referrers_title">Dim cyfeirwyr wedi eu cofnodi</string>
+ <string name="stats_empty_top_posts_title">Dim cofnodion na thudalennau wedi eu darllen</string>
+ <string name="stats_empty_top_posts_desc">Canfod pa gynnwys sydd wedi ei ddarllen fwyaf a gweld sut mae cofnodion unigol a thudalennau\'n perfformio dros amser.</string>
+ <string name="stats_totals_followers">Ers</string>
+ <string name="stats_empty_geoviews">Heb gofnodi gwledydd</string>
+ <string name="stats_empty_geoviews_desc">Gweld o\'r rhestr i weld pa wledydd ac ardaloedd sy\'n cynhyrchu\'r mwyaf o draffig i\'ch gwefan.</string>
+ <string name="stats_entry_video_plays">Fideo</string>
+ <string name="stats_entry_top_commenter">Awdur</string>
+ <string name="stats_entry_publicize">Gwasanaeth</string>
+ <string name="stats_entry_followers">Dilynwr</string>
+ <string name="stats_totals_publicize">Dilynwyr</string>
+ <string name="stats_entry_clicks_link">Dolen</string>
+ <string name="stats_view_top_posts_and_pages">Cofnod a Thudalen</string>
+ <string name="stats_view_videos">Fideos</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Dilynwyr</string>
+ <string name="stats_view_countries">Gwledydd</string>
+ <string name="stats_likes">Hoffi</string>
+ <string name="stats_pagination_label">Tudalen %1$s o %2$s</string>
+ <string name="stats_timeframe_years">Blwyddyn</string>
+ <string name="stats_views">Golwg</string>
+ <string name="stats_visitors">Ymwelwyr</string>
+ <string name="ssl_certificate_details">Manylion</string>
+ <string name="delete_sure_post">Dileu\'r cofnod</string>
+ <string name="delete_sure">Dileu\'r drafft</string>
+ <string name="delete_sure_page">Dileu\'r dudalen</string>
+ <string name="confirm_delete_multi_media">Dileu\'r eitemau hyn?</string>
+ <string name="confirm_delete_media">Dileu\'r eitem hon?</string>
+ <string name="cab_selected">%d wedi eu dewis</string>
+ <string name="media_gallery_date_range">Dangos cyfrwng o %1$s hyd %2$s</string>
+ <string name="sure_to_remove_account">Tynnu\'r blog?</string>
+ <string name="reader_empty_followed_blogs_title">Nid ydych yn dilyn unrhyw wefan hyd yn hyn</string>
+ <string name="reader_empty_posts_liked">Nid ydych wedi hoffi unrhyw gofnod</string>
+ <string name="faq_button">Cwestiynnau Cyffredin</string>
+ <string name="browse_our_faq_button">Porwch ein Cwestiynnau Cyffredin</string>
+ <string name="nux_help_description">Ewch i\'n canolfan gymorth i gael atebion i\'r cwestiynnau mwyaf cyffredin neu i\'n fforymau i ofyn rhai newydd</string>
+ <string name="agree_terms_of_service">Drwy greu cyfrif rydych y cytuno â\'r %1$sAmodau Gwasanaeth%2$s difyr</string>
+ <string name="create_new_blog_wpcom">Creu blog WordPress.com</string>
+ <string name="new_blog_wpcom_created">Mae\'r blog WordPress.com wedi ei greu!</string>
+ <string name="reader_empty_comments">Dim sylwadau eto</string>
+ <string name="reader_empty_posts_in_tag">Dim cofnod gyda\'r tag hwn</string>
+ <string name="reader_label_comment_count_multi">%,d sylw</string>
+ <string name="reader_label_view_original">Darllen yr erthygl wreiddiol</string>
+ <string name="reader_label_like">Hoffi</string>
+ <string name="reader_label_comment_count_single">Un sylw</string>
+ <string name="reader_label_comments_closed">Mae\'r sylwadau wedi cau</string>
+ <string name="reader_label_comments_on">Sylwadau ar</string>
+ <string name="reader_title_photo_viewer">%1$d o %2$d</string>
+ <string name="error_publish_empty_post">Methu cyhoeddi cofnod gwag</string>
+ <string name="error_refresh_unauthorized_posts">Nid oes gennych ganiatâd i weld na golygu cofnodion</string>
+ <string name="error_refresh_unauthorized_pages">Nid oes gennych ganiatâd i weld na golygu tudalennau</string>
+ <string name="error_refresh_unauthorized_comments">Nid oes gennych ganiatâd i weld na golygu sylwadau</string>
+ <string name="older_month">Mwy na mis</string>
+ <string name="more">Rhagor</string>
+ <string name="older_two_days">Mwy na 2 ddiwrnod</string>
+ <string name="older_last_week">Mwy nag wythnos</string>
+ <string name="stats_no_blog">Nid oedd modd llwytho ystadegau\'r blog dan sylw</string>
+ <string name="select_a_blog">Dewis gwefan WordPress</string>
+ <string name="sending_content">Llwytho cynnwys %s</string>
+ <string name="uploading_total">Llwytho %1$d o %2$d</string>
+ <string name="mnu_comment_liked">Hoffi</string>
+ <string name="comment">Sylw</string>
+ <string name="comment_trashed">Gosod y sylw yn y sbwriel</string>
+ <string name="posts_empty_list">Dim cofnod eto. Beth am greu un?</string>
+ <string name="comment_reply_to_user">Ateb %s</string>
+ <string name="pages_empty_list">Dim tudalen eto. Beth am greu un?</string>
+ <string name="media_empty_list_custom_date">Dim cyfrwng yn y cyfnod hwn</string>
+ <string name="posting_post">Cofnodi "%s"</string>
+ <string name="signing_out">Allgofnodi…</string>
+ <string name="reader_toast_err_generic">Methu cyflawni\'r weithred hon</string>
+ <string name="reader_toast_err_block_blog">Methu rhwystro\'r blog hwn</string>
+ <string name="reader_toast_blog_blocked">Ni fydd cofnodion o\'r blog yn cael eu dangos bellach</string>
+ <string name="reader_menu_block_blog">Rhwstro\'r blog</string>
+ <string name="contact_us">Cysylltu â ni</string>
+ <string name="hs__conversation_detail_error">Disgrifiwch eich anhawster</string>
+ <string name="hs__new_conversation_header">Cefnogaeth drwy sgwrs</string>
+ <string name="hs__conversation_header">Cefnogaeth drwy sgwrs</string>
+ <string name="hs__username_blank_error">Rhowch enw dilys</string>
+ <string name="hs__invalid_email_error">Rhowch gyfeiriad e-bost dilys</string>
+ <string name="add_location">Ychwanegwch leoliad</string>
+ <string name="current_location">Lleoliad cyfredol</string>
+ <string name="search_location">Chwilio</string>
+ <string name="edit_location">Golygu</string>
+ <string name="search_current_location">Lleoli</string>
+ <string name="preference_send_usage_stats">Anfon ystadegau</string>
+ <string name="preference_send_usage_stats_summary">Anfonwch ystadegau defnydd i\'n cynorthwyo i wella WordPress Android</string>
+ <string name="update_verb">Diweddaru</string>
+ <string name="schedule_verb">Amserlen</string>
+ <string name="reader_title_blog_preview">Blog Darllenydd</string>
+ <string name="reader_title_tag_preview">Tag Darllenydd</string>
+ <string name="reader_title_subs">Tagiau a Blogiau</string>
+ <string name="reader_page_followed_tags">Tagiau sy\'n cael eu dilyn</string>
+ <string name="reader_page_followed_blogs">Gwefannau sy\'n cael eu dilyn</string>
+ <string name="reader_hint_add_tag_or_url">Rhowch URL neu dag i\'w ddilyn</string>
+ <string name="reader_label_followed_blog">Blogiau rwy\'n eu diyn</string>
+ <string name="reader_label_tag_preview">Cofnodion wedi\'u tagio %s</string>
+ <string name="reader_toast_err_get_blog_info">Methu dangos y blog</string>
+ <string name="reader_toast_err_already_follow_blog">Rydych eisoes yn dilyn blog hwn</string>
+ <string name="reader_toast_err_follow_blog">Methu dilyn y blog hwn</string>
+ <string name="reader_toast_err_unfollow_blog">Methu peidio â dilyn y blog hwn</string>
+ <string name="reader_empty_recommended_blogs">Dim blogiau cymeradwy</string>
+ <string name="saving">Cadw…</string>
+ <string name="media_empty_list">Dim cyfrwng</string>
+ <string name="ptr_tip_message">Cyngor: Tynnwch i lawr i\'w adnewyddu</string>
+ <string name="help">Cymorth</string>
+ <string name="forgot_password">Wedi colli eich cyfrinair?</string>
+ <string name="forums">Fforymau</string>
+ <string name="help_center">Canolfan gymorth</string>
+ <string name="ssl_certificate_error">Tystysgrif SSL annilys</string>
+ <string name="ssl_certificate_ask_trust">Os ydych fel arfer yn cysylltu â\'r wefan hon heb broblemau, gall y gwall hwn olygu bod rhywun yn ceisio dynwared y wefan a dylech chi beidio parhau. Hoffech chi ymddiried yn y dystysgrif beth bynnag?</string>
+ <string name="out_of_memory">Dim cof ar ôl</string>
+ <string name="no_network_message">Nid oes rhwydwaith ar gael</string>
+ <string name="could_not_remove_account">Methu tynnu\'r blog</string>
+ <string name="gallery_error">Methu estyn y cyfrwng</string>
+ <string name="blog_not_found">Digwyddodd gwall wrth geisio cael mynediad at y blog</string>
+ <string name="wait_until_upload_completes">Arhoswch nes bod y llwytho i fyny wedi cwblhau</string>
+ <string name="theme_fetch_failed">Methu estyn themâu</string>
+ <string name="theme_set_failed">Methu gosod thema</string>
+ <string name="theme_auth_error_message">Sicrhewch bod gennych breintiau gosod themâu</string>
+ <string name="comments_empty_list">Dim sylwadau</string>
+ <string name="mnu_comment_unspam">Nid sbam</string>
+ <string name="no_site_error">Methwyd cysylltu â gwefan WordPress</string>
+ <string name="adding_cat_failed">Methodd ychwanegu categori</string>
+ <string name="adding_cat_success">Ychwanegwyd categori yn llwyddiannus</string>
+ <string name="cat_name_required">Mae angen maes enw categori</string>
+ <string name="category_automatically_renamed">Nid yw enw categori %1$s yn ddilys. Mae wedi ei ail-enwi i%2$s.</string>
+ <string name="no_account">Heb ganfod cyfrif WordPress, ychwanegwch cyfrif a cheisiwch eto</string>
+ <string name="sdcard_message">Mae angen cerdyn SD gosodedig i lwytho cyfryngau</string>
+ <string name="stats_empty_comments">Dim sylwadau eto</string>
+ <string name="stats_bar_graph_empty">Dim ystadegau gael</string>
+ <string name="invalid_url_message">Gwiriwch fod URL y blog yn ddilys</string>
+ <string name="reply_failed">Methodd yr ateb</string>
+ <string name="notifications_empty_list">Dim hysbysiadau</string>
+ <string name="error_delete_post">Digwyddodd gwall wrth ddileu y%s</string>
+ <string name="error_refresh_posts">Nid oedd modd adnewyddu\'r cofnodion ar hyn o bryd</string>
+ <string name="error_refresh_pages">Nid oedd modd adnewyddu\'r tudalennau ar hyn o bryd</string>
+ <string name="error_refresh_notifications">Nid oedd modd adnewyddu\'r hysbysiadau ar hyn o bryd</string>
+ <string name="error_refresh_comments">Nid oedd modd adnewyddu\'r cofnodion ar hyn o bryd</string>
+ <string name="error_refresh_stats">Nid oedd modd adnewyddu\'r ystadegau ar hyn o bryd</string>
+ <string name="error_generic">Digwyddodd gwall</string>
+ <string name="error_moderate_comment">Digwyddodd gwall wrth gymedroli</string>
+ <string name="error_edit_comment">Digwyddodd gwall wrth olygu\'r sylw</string>
+ <string name="error_upload">Digwyddodd gwall wrth lwytho\'r %s</string>
+ <string name="error_load_comment">Methu llwytho\'r sylw</string>
+ <string name="error_downloading_image">Gwall wrth lwytho delwedd i lawr</string>
+ <string name="passcode_wrong_passcode">PIN anghywir</string>
+ <string name="invalid_email_message">Nid yw eich cyfeiriad e-bost yn ddilys</string>
+ <string name="invalid_password_message">Rhaid i enw defnyddiwr gynnwys o leiaf yn 4 nod</string>
+ <string name="invalid_username_too_short">Rhaid i enw defnyddiwr fod o leiaf yn 4 nod o hyd</string>
+ <string name="invalid_username_too_long">Rhaid i enw defnyddiwr fod yn llai na 61 nod</string>
+ <string name="username_only_lowercase_letters_and_numbers">Gall enwau defnyddiwr gynnwys dim ond llythrennau bach (a-z) a rhifau</string>
+ <string name="username_required">Rhowch enw defnyddiwr</string>
+ <string name="username_not_allowed">Nid yw\'r enw defnyddiwr yna yn cael ei ganiatáu</string>
+ <string name="username_must_be_at_least_four_characters">Rhaid i enw defnyddiwr fod o leiaf 4 nod</string>
+ <string name="username_contains_invalid_characters">Nid yw enw defnyddiwr yn gallu cynnwys y nod “_”</string>
+ <string name="username_must_include_letters">Rhiad i enw defnyddiwr gynnwys o leiaf 1 llythyren (a-z)</string>
+ <string name="email_invalid">Rhowch gyfeiriad e-bost dilys</string>
+ <string name="email_not_allowed">Nid yw\'r cyfeiriad e-bost yna yn cael ei ganiatáu</string>
+ <string name="username_exists">Mae\'r enw defnyddiwr yna\'n bodoli eisoes</string>
+ <string name="email_exists">Mae\'r cyfeiriad e-bost yna eisoes yn cael ei ddefnyddio</string>
+ <string name="username_reserved_but_may_be_available">Mae\'r enw defnyddiwr yna wedi\'i neilltuo ar hyn o bryd ond efallai y bydd ar gael ymhen ychydig o ddyddiau</string>
+ <string name="blog_name_required">Rhowch gyfeiriad gwefan</string>
+ <string name="blog_name_not_allowed">Nid yw\'r cyfeiriad gwefan yna\'n cael ei ganiatáu</string>
+ <string name="blog_name_must_be_at_least_four_characters">Rhaid i gyfeiriad gwefan fod o leiaf 4 nod</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Rhaid i gyfeiriad y wefan fod yn llai na 64 nod</string>
+ <string name="blog_name_contains_invalid_characters">Nid yw cyfeiriad gwefan yn gallu cynnwys y nod “_”</string>
+ <string name="blog_name_cant_be_used">Nid oes modd i chi ddefnyddio\'r cyfeiriad gwefan hwn</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Gall cyfeiriad gwefan gynnwys dim ond llythrennau bach (a-z) a rhifau</string>
+ <string name="blog_name_exists">Mae\'r cyfeiriad gwefan hwnnw\'n bodoli eisoes</string>
+ <string name="blog_name_reserved">Mae\'r wefan wedi ei neilltuo</string>
+ <string name="blog_name_reserved_but_may_be_available">Mae\'r wefan yna wedi\'i neilltuo ar hyn o bryd ond efallai y bydd ar gael ymhen ychydig o ddyddiau</string>
+ <string name="username_or_password_incorrect">Mae\'r enw defnyddiwr neu gyfrinair yn anghywir</string>
+ <string name="nux_cannot_log_in">Nid oes modd i ni eich mewngofnodi</string>
+ <string name="xmlrpc_error">Methwyd cysylltu. Rhowch y llwybr llawn i xmlrpc.php ar eich gwefan a cheisiwch eto.</string>
+ <string name="select_categories">Dewis categorïau</string>
+ <string name="account_details">Manylion cyfrif</string>
+ <string name="edit_post">Golygu cofnod</string>
+ <string name="add_comment">Ychwanegu sylw</string>
+ <string name="connection_error">Gwall cysylltu</string>
+ <string name="cancel_edit">Diddymu golygu</string>
+ <string name="scaled_image_error">Rhowch werth graddfa lled ddilys</string>
+ <string name="post_not_found">Digwyddodd gwall wrth lwytho\'r cofnod. Adnewyddwch eich cofnodion a cheisiwch eto.</string>
+ <string name="learn_more">Dysgu rhagor</string>
+ <string name="media_gallery_settings_title">Gosodiadau\'r oriel</string>
+ <string name="media_gallery_image_order">Trefn delweddau</string>
+ <string name="media_gallery_num_columns">Nifer y colofnau</string>
+ <string name="media_gallery_type_thumbnail_grid">Grid lluniau bach</string>
+ <string name="media_gallery_edit">Golygu\'r oriel</string>
+ <string name="media_error_no_permission">Nid oes gennych ganiatâd i weld y llyfrgell cyfryngau</string>
+ <string name="cannot_delete_multi_media_items">Nid oes modd dileu rhai cyfryngau ar hyn o bryd. Ceisiwch eto yn nes ymlaen.</string>
+ <string name="themes_live_preview">Rhagolwg byw</string>
+ <string name="theme_current_theme">Y thema bresennol</string>
+ <string name="theme_premium_theme">Themâu premiwm</string>
+ <string name="link_enter_url_text">Testun dolen (dewisol)</string>
+ <string name="create_a_link">Creu dolen</string>
+ <string name="page_settings">Gosodiadau tudalen</string>
+ <string name="local_draft">Drafft lleol</string>
+ <string name="upload_failed">Methodd y llwytho</string>
+ <string name="horizontal_alignment">Alinio llorweddol</string>
+ <string name="file_not_found">Methu canfod y ffeil cyfryngau i\'w llwytho i fyny. A yw wedi ei dileu neu ei symud?</string>
+ <string name="post_settings">Gosodiadau cofnodion</string>
+ <string name="delete_post">Dileu\'r cofnod</string>
+ <string name="delete_page">Dileu\'r dudalen</string>
+ <string name="comment_status_approved">Cymeradwy</string>
+ <string name="comment_status_unapproved">Dan ystyriaeth</string>
+ <string name="comment_status_spam">Sbam</string>
+ <string name="comment_status_trash">Gosodwyd yn y sbwriel</string>
+ <string name="edit_comment">Golygu sylw</string>
+ <string name="mnu_comment_approve">Cymeradwyo</string>
+ <string name="mnu_comment_unapprove">Anghymeradwyo</string>
+ <string name="mnu_comment_spam">Sbam</string>
+ <string name="mnu_comment_trash">Sbwriel</string>
+ <string name="dlg_approving_comments">Cymeradwyo</string>
+ <string name="dlg_unapproving_comments">Anghymeradwyo</string>
+ <string name="dlg_spamming_comments">Marcio fel sbam</string>
+ <string name="dlg_trashing_comments">Anfon i\'r sbwriel</string>
+ <string name="dlg_confirm_trash_comments">Anfon i\'r sbwriel?</string>
+ <string name="trash_yes">Sbwriel</string>
+ <string name="trash_no">Peidiwch â\'u hanfon i\'r sbwriel</string>
+ <string name="trash">Sbwriel</string>
+ <string name="author_name">Enw awdur</string>
+ <string name="author_email">E-bost awdur</string>
+ <string name="author_url">URL Awdur</string>
+ <string name="hint_comment_content">Sylw</string>
+ <string name="saving_changes">Cadw\'r newidiadau</string>
+ <string name="sure_to_cancel_edit_comment">Diddymu golygu\'r sylw hwn?</string>
+ <string name="content_required">Mae angen sylw</string>
+ <string name="toast_comment_unedited">Nid yw\'r sylw wedi newid</string>
+ <string name="remove_account">Tynnu\'r blog</string>
+ <string name="blog_removed_successfully">Tynwyd y blog yn llwyddiannus</string>
+ <string name="delete_draft">Dileu\'r drafft</string>
+ <string name="preview_page">Rhagolwg tudalen</string>
+ <string name="preview_post">Rhagolwg post</string>
+ <string name="comment_added">Sylw wedi ei ychwanegu\'n llwyddiannus</string>
+ <string name="post_not_published">Nid yw statws cofnod wedi ei gyhoeddi</string>
+ <string name="page_not_published">Nid yw statws tudalen wedi ei gyhoeddi</string>
+ <string name="view_in_browser">Gweld mewn porwr</string>
+ <string name="add_new_category">Ychwanegu categori newydd</string>
+ <string name="category_name">Enw categori</string>
+ <string name="category_slug">Categori bonyn (dewisol)</string>
+ <string name="category_desc">Disgrifiad categori (dewisol)</string>
+ <string name="category_parent">Categori rhiant (dewisol):</string>
+ <string name="share_action_post">Cofnod newydd</string>
+ <string name="share_action_media">Llyfrgell cyfrwng</string>
+ <string name="file_error_create">Methwyd creu ffeil dros dro ar gyfer llwytho cyfryngau. Gwnewch yn siwr bod digon o le rhydd ar eich dyfais.</string>
+ <string name="location_not_found">Lleoliad anhysbys</string>
+ <string name="open_source_licenses">Trwyddedau cod agored</string>
+ <string name="invalid_site_url_message">Gwiriwch fod URL y blog yn ddilys</string>
+ <string name="pending_review">Disgwyl adolygiad</string>
+ <string name="http_credentials">Cymwysterau HTTP (dewisol)</string>
+ <string name="http_authorization_required">Mae angen awdurdodi</string>
+ <string name="post_format">Fformat cofnod</string>
+ <string name="notifications_empty_all">Dim hysbysiadau...eto.</string>
+ <string name="new_post">Cofnod newydd</string>
+ <string name="new_media">Cyfryngau newydd</string>
+ <string name="view_site">Gweld y wefan</string>
+ <string name="privacy_policy">Polisi preifatrwydd</string>
+ <string name="local_changes">Newidiadau lleol</string>
+ <string name="image_settings">Gosodiadau delweddau</string>
+ <string name="add_account_blog_url">Cyfeiriad y blog</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="error_blog_hidden">Mae\'r blog hwn yn gudd ac nid oes modd ei lwytho. Galluogwch y blog eto o fewn y gosodiadau a cheisiwch eto.</string>
+ <string name="fatal_db_error">Digwyddodd gwall wrth greu cronfa ddata\'r ap. Ceisiwch ailosod yr ap.</string>
+ <string name="jetpack_message_not_admin">Mae angen yr ategyn Jetpack ar gyfer adrodd ar yr ystadegau. Cysylltwch â gweinyddwr y wefan.</string>
+ <string name="reader_title_applog">Cofnod yr ap</string>
+ <string name="reader_share_link">Rhannu dolen</string>
+ <string name="reader_toast_err_add_tag">Methu ychwanegu\'r tag hwn</string>
+ <string name="reader_toast_err_remove_tag">Methu tynnu\'r tag hwn</string>
+ <string name="required_field">Maes angenrheidiol</string>
+ <string name="email_hint">Cyfeiriad e-bost</string>
+ <string name="site_address">Eich cyfeiriad hunan-letya (URL)</string>
+ <string name="email_cant_be_used_to_signup">Nid oes modd i chi ddefnyddio\'r cyfeiriad e-bost yna i ymuno. Mae gennym anhawster ohonynt yn rhwystro rhai o\'n e-byst. Defnyddiwch darparwr e-bost arall.</string>
+ <string name="email_reserved">Mae\'r cyfeiriad e-bost hwnnw eisoes wedi cael ei ddefnyddio. Gwiriwch eich blwch derbyn ar gyfer e-bost cychwyn. Os nad ydych yn cychwyn yn fuan gallwch roi cynnig arall ymhen ychydig ddyddiau.</string>
+ <string name="blog_name_must_include_letters">Rhaid i gyfeiriad gwefan gynnwys o leiaf 1 llythyren (a-z)</string>
+ <string name="blog_name_invalid">Cyfeiriad gwefan annilys</string>
+ <string name="blog_title_invalid">Teitl gwefan annilys</string>
+ <string name="deleting_page">Wrthi\'n dileu tudalen</string>
+ <string name="deleting_post">Wrthi\'n dileu cofnod</string>
+ <string name="share_url_post">Rhannu cofnod</string>
+ <string name="share_url_page">Rhannu tudalen</string>
+ <string name="share_link">Rhannu dolen</string>
+ <string name="creating_your_account">Creu eich cyfrif</string>
+ <string name="creating_your_site">Creu eich gwefan</string>
+ <string name="reader_empty_posts_in_tag_updating">Estyn cofnodion…</string>
+ <string name="error_refresh_media">Aeth rhywbeth o\'i le wrth adnewyddu\'r llyfrgell cyfryngau. Rhowch gynnig arall arni\'n nes ymlaen.</string>
+ <string name="reader_likes_you_and_multi">Rydych chi a %,d o bobl eraill yn hoffi hwn</string>
+ <string name="reader_likes_multi">%,d o pobl yn hoffi hwn</string>
+ <string name="reader_toast_err_get_comment">Methu adfer y sylw hwn</string>
+ <string name="reader_label_reply">Ateb</string>
+ <string name="video">Fideo</string>
+ <string name="download">Wrthi\'n llwytho i lawr cyfryngau</string>
+ <string name="comment_spammed">Mae\'r sylw hwn wedi ei farcio fel sbam</string>
+ <string name="cant_share_no_visible_blog">Nid oes modd rhannu i WordPress heb flog gweladwy</string>
+ <string name="select_time">Dewis amser</string>
+ <string name="reader_likes_you_and_one">Rydych chi ac eraill yn hoffi hwn</string>
+ <string name="reader_empty_followed_blogs_description">Ond peidiwch â phoeni, tapiwch yr eicon tag ar y brig de i ddechrau darganfod!</string>
+ <string name="select_date">Dewis dyddiad</string>
+ <string name="pick_photo">Dewis ffoto</string>
+ <string name="account_two_step_auth_enabled">Mae dilysu dau gam wedi ei alluogi ar gyfer y cyfrif hwn. Ewch i\'ch eich gosodiadau diogelwch ar WordPress.com a chrëwch gyfrinair rhaglen benodol.</string>
+ <string name="pick_video">Dewiswch fideo</string>
+ <string name="reader_toast_err_get_post">Does dim modd adfer y cofnod hwn</string>
+ <string name="validating_user_data">Dilysu data defnyddiwr</string>
+ <string name="validating_site_data">Dilysu data gwefan</string>
+ <string name="password_invalid">Mae angen cyfrinair mwy diogel arnoch chi. Gwnewch yn siŵr eich bod yn defnyddio 7 nod neu fwy, cymysgedd o briflythyren a llythrennau bach, rhifau neu nodau arbennig.</string>
+ <string name="nux_tap_continue">Parhau</string>
+ <string name="nux_welcome_create_account">Creu cyfrif</string>
+ <string name="signing_in">Mewngofnodi…</string>
+ <string name="nux_add_selfhosted_blog">Ychwanegu gwefan hunan-letya</string>
+ <string name="nux_oops_not_selfhosted_blog">Mewngofnodi i WordPress.com</string>
+ <string name="media_add_popup_title">Ychwanegu at y llyfrgell cyfryngau</string>
+ <string name="media_add_new_media_gallery">Creu oriel</string>
+ <string name="empty_list_default">Mae\'r rhestr hon yn wag</string>
+ <string name="select_from_media_library">Dewiswch o\'r llyfrgell cyfryngau</string>
+ <string name="jetpack_message">Mae angen yr ategyn Jetpack ar gyfer adrodd ar yr ystadegau. Hoffech chi osod Jetpack?</string>
+ <string name="jetpack_not_found">Heb ganfod ategyn jetpack</string>
+ <string name="reader_untitled_post">(Dideitl)</string>
+ <string name="reader_share_subject">Rhannu o %s</string>
+ <string name="reader_btn_share">Rhannu</string>
+ <string name="reader_btn_follow">Dilyn</string>
+ <string name="reader_btn_unfollow">Yn dilyn</string>
+ <string name="reader_hint_comment_on_comment">Ateb sylw…</string>
+ <string name="reader_label_added_tag">Ychwanegu %s</string>
+ <string name="reader_label_removed_tag">Tynnu %s</string>
+ <string name="reader_likes_one">Mae un person yn hoffi hwn</string>
+ <string name="reader_likes_only_you">Rydych chi\'n hoffi hwn</string>
+ <string name="reader_toast_err_comment_failed">Methu cofnodi\'ch sylw</string>
+ <string name="reader_toast_err_tag_exists">Rydych eisoes yn dilyn y tag hwn</string>
+ <string name="reader_toast_err_tag_invalid">Nid yw\'n dag dilys</string>
+ <string name="reader_toast_err_share_intent">Methu â rhannu</string>
+ <string name="reader_toast_err_view_image">Methu â gweld delwedd</string>
+ <string name="reader_toast_err_url_intent">Methu agor %s</string>
+ <string name="reader_empty_followed_tags">Nid ydych yn dlyn unrhyw dagiau</string>
+ <string name="create_account_wpcom">Creu cyfrif ar WordPress.com</string>
+ <string name="button_next">Nesaf</string>
+ <string name="connecting_wpcom">Cysylltu â WordPress.com</string>
+ <string name="username_invalid">Enw defnyddiwr annilys</string>
+ <string name="limit_reached">Wedi cyrraedd y terfyn. Gallwch geisio eto mewn 1 munud. Bydd ceisio eto cyn yn cynyddu amser fydd gennych cyn codi\'r gwaharddiad. Os ydych chi\'n credu bod hwn yn gamgymeriad, cysylltwch â Cymorth.</string>
+ <string name="nux_tutorial_get_started_title">Cychwyn arni!</string>
+ <string name="themes">Themâu</string>
+ <string name="all">Y Cyfan</string>
+ <string name="images">Delweddau</string>
+ <string name="unattached">Heb ei atodi</string>
+ <string name="custom_date">Dyddiad Cyfaddas</string>
+ <string name="media_add_popup_capture_photo">Cipio ffoto</string>
+ <string name="media_add_popup_capture_video">Cipio fideo</string>
+ <string name="media_gallery_image_order_random">Hap</string>
+ <string name="media_gallery_image_order_reverse">Gwrthdroi</string>
+ <string name="media_gallery_type">Math</string>
+ <string name="media_gallery_type_squares">Sgwariau</string>
+ <string name="media_gallery_type_tiled">Teils</string>
+ <string name="media_gallery_type_circles">Cylchoedd</string>
+ <string name="media_gallery_type_slideshow">Sioe sleidiau</string>
+ <string name="media_edit_title_text">Teitl</string>
+ <string name="media_edit_caption_text">Egluryn</string>
+ <string name="media_edit_description_text">Disgrifiad</string>
+ <string name="media_edit_title_hint">Rhowch deitl yma</string>
+ <string name="media_edit_caption_hint">Rhowch egluryn yma</string>
+ <string name="media_edit_description_hint">Rhowch ddisgrifiad yma</string>
+ <string name="media_edit_success">Diweddarwyd</string>
+ <string name="media_edit_failure">Methwyd diweddaru</string>
+ <string name="themes_details_label">Manylion</string>
+ <string name="themes_features_label">Nodweddion</string>
+ <string name="theme_activate_button">Cychwyn</string>
+ <string name="theme_activating_button">Wrthi\'n cychwyn</string>
+ <string name="theme_set_success">Gosodwyd y thema\'n llwyddiannus!</string>
+ <string name="theme_auth_error_title">Methwyd estyn themâu</string>
+ <string name="post_excerpt">Dyfyniad</string>
+ <string name="share_action_title">Ychwanegu at …</string>
+ <string name="share_action">Rhannu</string>
+ <string name="stats">Ystadegau</string>
+ <string name="stats_view_visitors_and_views">Ymwelwyr a Golygon</string>
+ <string name="stats_view_clicks">Cliciau</string>
+ <string name="stats_view_tags_and_categories">Tagiau a Chategorïau</string>
+ <string name="stats_view_referrers">Cyfeirwyr</string>
+ <string name="stats_timeframe_today">Heddiw</string>
+ <string name="stats_timeframe_yesterday">Ddoe</string>
+ <string name="stats_timeframe_days">Diwrnod</string>
+ <string name="stats_timeframe_weeks">Wythnos</string>
+ <string name="stats_timeframe_months">Mis</string>
+ <string name="stats_entry_country">Gwlad</string>
+ <string name="stats_entry_posts_and_pages">Teitl</string>
+ <string name="stats_entry_tags_and_categories">Pwnc</string>
+ <string name="stats_entry_authors">Awdur</string>
+ <string name="stats_entry_referrers">Cyfeiriwr</string>
+ <string name="stats_totals_views">Golwg</string>
+ <string name="stats_totals_clicks">Cliciau</string>
+ <string name="stats_totals_plays">Chwarae</string>
+ <string name="passcode_manage">Rheoli clo PIN</string>
+ <string name="passcode_enter_passcode">Rhowch eich PIN</string>
+ <string name="passcode_enter_old_passcode">Rhowch eich hen PIN</string>
+ <string name="passcode_re_enter_passcode">Ail gyflwynwch eich PIN</string>
+ <string name="passcode_change_passcode">Newid PIN</string>
+ <string name="passcode_set">PIN wedi ei osod</string>
+ <string name="passcode_preference_title">Clo PIN</string>
+ <string name="passcode_turn_off">Diffodd clo PIN</string>
+ <string name="passcode_turn_on">Troi clo PIN ymlaen</string>
+ <string name="upload">Llwytho i fyny</string>
+ <string name="discard">Gwaredu</string>
+ <string name="sign_in">Mewngofnodi</string>
+ <string name="notifications">Hysbysiadau</string>
+ <string name="note_reply_successful">Ateb wedi ei gyhoeddi</string>
+ <string name="follows">Yn Dilyn</string>
+ <string name="new_notifications">%d hysbysiadau newydd</string>
+ <string name="more_notifications">a %d yn rhagor.</string>
+ <string name="loading">Llwytho…</string>
+ <string name="httpuser">Enw defnyddiwr HTTP</string>
+ <string name="httppassword">Cyfrinair HTTP</string>
+ <string name="error_media_upload">Digwyddodd gwall wrth lwytho\'r cyfrwng</string>
+ <string name="post_content">Cynnwys (tapiwch i ychwanegu testun a chyfrwng)</string>
+ <string name="publish_date">Cyhoeddi</string>
+ <string name="content_description_add_media">Ychwanegu cyfrwng</string>
+ <string name="incorrect_credentials">Enw cyfrif neu gyfrinair anghywir.</string>
+ <string name="password">Cyfrinair</string>
+ <string name="username">Enw defnyddiwr</string>
+ <string name="reader">Darllennydd</string>
+ <string name="featured">Defnyddio fel delwedd nodwedd</string>
+ <string name="featured_in_post">Cynnwys delwedd yng nghynnwys y cofnod</string>
+ <string name="no_network_title">Dim rhwydwaith ar gael</string>
+ <string name="pages">Tudalennau</string>
+ <string name="caption">Egluryn (dewisol)</string>
+ <string name="width">Lled</string>
+ <string name="posts">Cofnodion</string>
+ <string name="anonymous">Di-enw</string>
+ <string name="page">Tudalen</string>
+ <string name="post">Cofnod</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">Iawn</string>
+ <string name="upload_scaled_image">Llwytho a chysylltu â delwedd graddedig</string>
+ <string name="scaled_image">Lled y ddelwedd graddedig</string>
+ <string name="scheduled">Amserlennwyd</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Llwytho...</string>
+ <string name="version">Fersiwn</string>
+ <string name="tos">Amodau Gwasanaeth</string>
+ <string name="app_title">WordPress ar gyfer Android</string>
+ <string name="max_thumbnail_px_width">Lled Rhagosodedig y Ddelwedd</string>
+ <string name="image_alignment">Alinio</string>
+ <string name="refresh">Adnewyddu</string>
+ <string name="untitled">Dideitl</string>
+ <string name="edit">Golygu</string>
+ <string name="post_id">Cofnod</string>
+ <string name="page_id">Tudalen</string>
+ <string name="post_password">Cyfrinair (dewisol)</string>
+ <string name="immediately">Ar unwaith</string>
+ <string name="quickpress_add_alert_title">Gosod enw\'r llwybr byr</string>
+ <string name="today">Heddiw</string>
+ <string name="settings">Gosodiadau</string>
+ <string name="share_url">Rhannu URL</string>
+ <string name="quickpress_window_title">Dewiswch flog ar gyfer llwybr byr QuickPress</string>
+ <string name="quickpress_add_error">Nid yw enw\'r llwybr byr yn gallu bod yn wag</string>
+ <string name="publish_post">Cyhoeddi</string>
+ <string name="draft">Drafft</string>
+ <string name="post_private">Preifat</string>
+ <string name="upload_full_size_image">Llwytho a\'u cysylltu â delwedd llawn</string>
+ <string name="title">Teitl</string>
+ <string name="tags_separate_with_commas">Tagiau (gwahanu tagiau gydag atalnodau)</string>
+ <string name="categories">Categorïau</string>
+ <string name="dlg_deleting_comments">Dileu sylwadau</string>
+ <string name="notification_blink">Fflachio\'r golau hysbysu</string>
+ <string name="notification_sound">Sain hysbysu</string>
+ <string name="notification_vibrate">Dirgrynu</string>
+ <string name="status">Statws</string>
+ <string name="location">Lleoliad</string>
+ <string name="sdcard_title">Mae angen cerdyn SD</string>
+ <string name="select_video">Dewiswch fideo o\'r oriel</string>
+ <string name="media">Cyfrwng</string>
+ <string name="delete">Dileu</string>
+ <string name="none">Dim</string>
+ <string name="blogs">Blogiau</string>
+ <string name="select_photo">Dewiswch lun o\'r oriel</string>
+ <string name="error">Gwall</string>
+ <string name="cancel">Diddymu</string>
+ <string name="save">Cadw</string>
+ <string name="add">Ychwanegu</string>
+ <string name="category_refresh_error">Gwall adnewyddu categori</string>
+ <string name="preview">Rhagolwg</string>
+ <string name="on">ar</string>
+ <string name="reply">Ateb</string>
+ <string name="notification_settings">Gosodiadau Hysbysiadau</string>
+ <string name="yes">Iawn</string>
+ <string name="no">Na</string>
+</resources>
diff --git a/WordPress/src/main/res/values-da/strings.xml b/WordPress/src/main/res/values-da/strings.xml
new file mode 100644
index 000000000..049a06343
--- /dev/null
+++ b/WordPress/src/main/res/values-da/strings.xml
@@ -0,0 +1,629 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="plans_post_purchase_button_themes">Gennemse temaer</string>
+ <string name="plans_post_purchase_title_themes">Find et perfekt, premium tema</string>
+ <string name="plans_post_purchase_button_video">Start på nyt indlæg</string>
+ <string name="plans_post_purchase_title_customize">Tilpas skrifttyper og farver</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Planer</string>
+ <string name="plans_loading_error">Kunne ikke hente planer</string>
+ <string name="export_your_content">Eksporter dit indhold</string>
+ <string name="exporting_content_progress">Eksporterer indhold...</string>
+ <string name="show_purchases">Vis køb</string>
+ <string name="delete_site_progress">Sletter websted...</string>
+ <string name="are_you_sure">Er du sikker?</string>
+ <string name="export_site_hint">Eksporter dit websted til en XML-fil</string>
+ <string name="keep_your_content">Behold dit indhold</string>
+ <string name="primary_domain">Primært domæne</string>
+ <string name="site_settings_export_content_title">Eksporter indhold</string>
+ <string name="contact_support">Kontakt support</string>
+ <string name="site_settings_advanced_header">Avanceret</string>
+ <string name="button_skip">Spring over</string>
+ <string name="theme_free">Gratis</string>
+ <string name="post_format_video">Video</string>
+ <string name="post_format_status">Status</string>
+ <string name="theme_all">Alle</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Galleri</string>
+ <string name="post_format_image">Billede</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Citat</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="post_format_audio">Lyd</string>
+ <string name="reader_discover_visit_blog">Besøg %s</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_label_follow_count">%,d følgere</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="tabbar_accessibility_label_my_site">Mit websted</string>
+ <string name="tabbar_accessibility_label_me">Mig</string>
+ <string name="passcodelock_prompt_message">Indtast din PIN</string>
+ <string name="editor_toast_changes_saved">Ændringer gemt</string>
+ <string name="push_auth_expired">Forspørgslen er udløbet. Log ind på WordPress.com for at prøve igen.</string>
+ <string name="ignore">Ignorer</string>
+ <string name="stats_insights_best_ever">Bedste visninger nogensinde</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% af visninger</string>
+ <string name="stats_insights_most_popular_hour">Mest populære time</string>
+ <string name="stats_insights_most_popular_day">Mest populære dag</string>
+ <string name="stats_insights_popular">Mest populære dag og time</string>
+ <string name="stats_insights_today">Dagens statistikker</string>
+ <string name="stats_insights_all_time">Totale indlæg, visninger og besøgende</string>
+ <string name="stats_insights">Indsigt</string>
+ <string name="me_disconnect_from_wordpress_com">Afbryd forbindelse til WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Forbind til WordPress.com</string>
+ <string name="me_btn_login_logout">Log ind/ud</string>
+ <string name="me_btn_support">Hjælp &amp; Support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" blev ikke skjult, fordi det er det aktuelle websted</string>
+ <string name="site_picker_create_dotcom">Opret websted på WordPress.com</string>
+ <string name="site_picker_add_site">Tilføj websted</string>
+ <string name="site_picker_add_self_hosted">Tilføj websted fra egen udbyder</string>
+ <string name="site_picker_edit_visibility">Vis/skjul websteder</string>
+ <string name="my_site_btn_view_site">Vis websted</string>
+ <string name="site_picker_title">Vælg websted</string>
+ <string name="my_site_btn_switch_site">Skift websted</string>
+ <string name="my_site_btn_view_admin">Vis administration</string>
+ <string name="my_site_btn_site_settings">Indstillinger</string>
+ <string name="my_site_header_publish">Udgiv</string>
+ <string name="my_site_header_look_and_feel">Udseende</string>
+ <string name="my_site_btn_blog_posts">Blogindlæg</string>
+ <string name="my_site_header_configuration">Konfiguration</string>
+ <string name="notifications_account_required">Log ind på WordPress.com for notifikationer</string>
+ <string name="stats_unknown_author">Ukendt forfatter</string>
+ <string name="image_added">Billede tilføjet</string>
+ <string name="signout">Afbryd</string>
+ <string name="deselect_all">Fravælg alle</string>
+ <string name="show">Vis</string>
+ <string name="hide">Skjul</string>
+ <string name="select_all">Vælg alle</string>
+ <string name="no_media_sources">Medier kunne ikke hentes</string>
+ <string name="loading_blog_videos">Henter videoer</string>
+ <string name="error_loading_videos">Videoer kunne ikke loades</string>
+ <string name="loading_blog_images">Henter billeder</string>
+ <string name="error_loading_images">Billeder kunne ikke loades</string>
+ <string name="error_loading_blog_videos">Videoer kunne ikke hentes</string>
+ <string name="error_loading_blog_images">Billeder kunne ikke hentes</string>
+ <string name="no_device_images">Ingen billeder</string>
+ <string name="no_blog_videos">Ingen videoer</string>
+ <string name="no_device_videos">Ingen videoer</string>
+ <string name="no_blog_images">Ingen billeder</string>
+ <string name="no_media">Ingen medier</string>
+ <string name="loading_images">Loader billeder</string>
+ <string name="loading_videos">Loader videoer</string>
+ <string name="two_step_sms_sent">Gennemse dine SMS\'er for godkendelseskoden.</string>
+ <string name="two_step_footer_button">Send kode i en SMS</string>
+ <string name="editor_toast_invalid_path">Ugyldig filsti</string>
+ <string name="invalid_verification_code">Ugyldig godkendelseskode</string>
+ <string name="verification_code">Godkendelseskode</string>
+ <string name="verify">Bekræft</string>
+ <string name="auth_required">Log ind igen for at fortsætte.</string>
+ <string name="tab_title_device_images">Enhedsbilleder</string>
+ <string name="tab_title_device_videos">Enhedsvideoer</string>
+ <string name="tab_title_site_images">Webstedsbilleder</string>
+ <string name="tab_title_site_videos">Webstedsvideoer</string>
+ <string name="media_details_label_file_type">Filtype</string>
+ <string name="media_picker_title">Vælg filer</string>
+ <string name="device">Enhed</string>
+ <string name="add_to_post">Tilføj til indlæg</string>
+ <string name="take_video">Optag en video</string>
+ <string name="take_photo">Tag et billede</string>
+ <string name="language">Sprog</string>
+ <string name="sign_in_jetpack">Log ind på din WordPress.com-konto og forbind til Jetpack.</string>
+ <string name="error_publish_no_network">Kan ikke udgive uden forbindelse. Gemt som kladde.</string>
+ <string name="media_details_label_file_name">Filnavn</string>
+ <string name="stats_followers_total_email_paged">Viser %1$d - %2$d af %3$s følgere på e-mail</string>
+ <string name="stats_followers_total_wpcom_paged">Viser %1$d - %2$d af %3$s følgere på WordPress.com</string>
+ <string name="stats_empty_search_terms">Ingen søgeforespørgsler registreret</string>
+ <string name="stats_search_terms_unknown_search_terms">Ukendte søgeord</string>
+ <string name="media_fetching">Henter medier...</string>
+ <string name="posts_fetching">Henter indlæg...</string>
+ <string name="error_notification_open">Kunne ikke åbne notifikation</string>
+ <string name="pages_fetching">Henter sider...</string>
+ <string name="comments_fetching">Henter kommentarer...</string>
+ <string name="stats_view_search_terms">Søgeord</string>
+ <string name="stats_view_authors">Forfattere</string>
+ <string name="stats_entry_search_terms">Søgeord</string>
+ <string name="reader_empty_posts_request_failed">Kunne ikke hente indlæg</string>
+ <string name="publisher">Udgiver:</string>
+ <string name="toast_err_post_uploading">Kan ikke åbne indlæg, mens det overføres</string>
+ <string name="stats_overall">I alt</string>
+ <string name="error_copy_to_clipboard">Der skete en fejl under kopieringen af teksten til udklipsholderen</string>
+ <string name="logs_copied_to_clipboard">Log-filerne er kopieret til udklipsholderen</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_months_and_years">Måneder og år</string>
+ <string name="stats_recent_weeks">Seneste uger</string>
+ <string name="reader_label_new_posts">Nye indlæg</string>
+ <string name="reader_empty_posts_in_blog">Denne blog er tom</string>
+ <string name="stats_average_per_day">Dagligt gennemsnit</string>
+ <string name="stats_period">Periode</string>
+ <string name="post_uploading">Overfører</string>
+ <string name="reader_page_recommended_blogs">Websteder du måske vil synes om</string>
+ <string name="stats_followers_total_email">Total antal følgere på email: %1$s</string>
+ <string name="stats_followers_total_wpcom">Totale antal følgere på WordPress.com: %1$s</string>
+ <string name="stats_empty_publicize">Ingen publicize følgere registreret</string>
+ <string name="stats_view_publicize">publicize</string>
+ <string name="stats_empty_geoviews_desc">Gå på opdagelse i listen og se hvilke lande og regioner der skaber mest trafik til dit websted.</string>
+ <string name="stats_empty_video_desc">Hvis du har uploadet videoer via VideoPress, så find ud af hvor mange gange de er blevet set.</string>
+ <string name="stats_empty_tags_and_categories_desc">Få overblik over de mest populære emner på dit websted, i form af topindlæg fra den seneste uge.</string>
+ <string name="stats_empty_comments_desc">Hvis du tillader kommentarer på dit websted, så hold øje med dine topkommentatorer og opdag hvilket indhold, der skaber de mest livlige samtaler, baseret på de seneste 1.000 kommentarer.</string>
+ <string name="stats_empty_clicks_desc">Når dit inhold har links til andre websteder, vil du se hvilke dine besøgende klikker mest på.</string>
+ <string name="stats_empty_referrers_desc">Lær mere om dit websteds synlighed ved at se på hvilke websteder og søgemaskiner der sender dig mest trafik</string>
+ <string name="stats_empty_referrers_title">Ingen henvisninger er registreret</string>
+ <string name="stats_empty_top_posts_title">Ingen indlæg elller sider er blevet vist</string>
+ <string name="stats_empty_tags_and_categories">Ingen markerede indlæg eller sider er blevet set</string>
+ <string name="stats_comments_by_posts_and_pages">Efter indlæg og sider</string>
+ <string name="stats_other_recent_stats_label">Andre nylige statistikker</string>
+ <string name="stats_for">Statistikker for %s</string>
+ <string name="stats_view_top_posts_and_pages">Indlæg &amp; sider</string>
+ <string name="stats_empty_top_posts_desc">Opdag hvad dit mest sete indhold er og tjek hvordan individuelle indlæg og sider med tiden klarer sig.</string>
+ <string name="stats_visitors">Besøgende</string>
+ <string name="stats_views">Visninger</string>
+ <string name="stats_timeframe_years">År</string>
+ <string name="stats_pagination_label">Side %1$s af %2$s</string>
+ <string name="stats_totals_followers">Siden</string>
+ <string name="stats_empty_clicks_title">Ingen klik registreret</string>
+ <string name="stats_empty_video">Ingen videoer afspillet</string>
+ <string name="stats_view_countries">Lande</string>
+ <string name="stats_likes">Likes</string>
+ <string name="stats_view_followers">Følgere</string>
+ <string name="stats_view_videos">Videoer</string>
+ <string name="stats_entry_followers">Følger</string>
+ <string name="stats_totals_publicize">Følgere</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Forfatter</string>
+ <string name="stats_empty_followers_desc">Hold styr på antallet af følgere og hvor længe hver enkelt har fulgt dit site.</string>
+ <string name="stats_empty_publicize_desc">Hold styr på dine følgere fra diverse sociale netværk service ved at bruge publicize.</string>
+ <string name="stats_empty_geoviews">Ingen lande registreret</string>
+ <string name="stats_comments_by_authors">Af forfattere</string>
+ <string name="stats_empty_followers">Ingen følgere</string>
+ <string name="stats_followers_email_selector">E-mail</string>
+ <string name="themes_fetching">Henter temaer...</string>
+ <string name="stats_followers_months">%1$d måneder</string>
+ <string name="stats_view_all">Se alle</string>
+ <string name="stats_view">Se</string>
+ <string name="stats_followers_a_year">Et år</string>
+ <string name="stats_followers_a_month">En måned</string>
+ <string name="stats_followers_years">%1$d år</string>
+ <string name="stats_followers_an_hour_ago">en time siden</string>
+ <string name="stats_followers_minutes">%1$d minutter</string>
+ <string name="stats_followers_hours">%1$d timer</string>
+ <string name="stats_followers_a_day">En dag</string>
+ <string name="stats_followers_days">%1$d dage</string>
+ <string name="stats_followers_a_minute_ago">et minut siden</string>
+ <string name="stats_followers_seconds_ago">sekunder siden</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_comments_total_comments_followers">Total antal indlæg med kommentarfølgere: %1$s</string>
+ <string name="ssl_certificate_details">Detaljer</string>
+ <string name="sure_to_remove_account">Fjern dette websted?</string>
+ <string name="delete_sure_post">Slet dette indlæg</string>
+ <string name="delete_sure">Slet denne kladde</string>
+ <string name="delete_sure_page">Slet denne side</string>
+ <string name="confirm_delete_multi_media">Slet markerede elementer</string>
+ <string name="confirm_delete_media">Slet markerede element?</string>
+ <string name="cab_selected">%d markeret</string>
+ <string name="media_gallery_date_range">Viser medier fra %1$s til %2$s</string>
+ <string name="mnu_comment_liked">Synes godt om</string>
+ <string name="stats_no_blog">Statistiken kunne ikke hentes for den valgte blog</string>
+ <string name="pages_empty_list">Endnu ingen sider! Hvorfor ikke lave en?</string>
+ <string name="posts_empty_list">Ingen indlæg endnu! Hvorfor ikke skabe et?</string>
+ <string name="media_empty_list_custom_date">Ingen medier indenfor dette tidsinterval</string>
+ <string name="posting_post">Sender "%s"</string>
+ <string name="select_a_blog">Vælg et WordPress-websted</string>
+ <string name="sending_content">Overfører %s indhold</string>
+ <string name="uploading_total">Overfører %1$d af %2$d</string>
+ <string name="comment_reply_to_user">Besvar %s</string>
+ <string name="older_month">Ældre end en måned</string>
+ <string name="signing_out">Logger ud...</string>
+ <string name="more">Mere</string>
+ <string name="older_two_days">Ældre end 2 dage</string>
+ <string name="comment_trashed">Kommentar slettet</string>
+ <string name="comment">Kommentar</string>
+ <string name="older_last_week">Ældre end en uge</string>
+ <string name="reader_empty_posts_liked">Du har ikke synes om nogen indlæg</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Gennemse vores FAQ</string>
+ <string name="nux_help_description">Besøg hjælpecenteret for at få svar på almindelige spørgsmål eller besøg forumene for at stille nogle nye</string>
+ <string name="agree_terms_of_service">Ved at oprette en konto accepterer du de fascinerende %1$sServicevilkår%2$s</string>
+ <string name="create_new_blog_wpcom">Opret en WordPress.com-blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com-blog oprettet!</string>
+ <string name="reader_empty_comments">Endnu ingen kommentarer</string>
+ <string name="reader_empty_posts_in_tag">Ingen indlæg med dette tag</string>
+ <string name="reader_label_comment_count_multi">%,d kommentarer</string>
+ <string name="reader_label_view_original">Vis oprindelig artikel</string>
+ <string name="reader_label_like">Synes om</string>
+ <string name="reader_label_comment_count_single">En kommentar</string>
+ <string name="reader_label_comments_closed">Der er lukket for kommentarer</string>
+ <string name="reader_label_comments_on">Kommentarer slået til</string>
+ <string name="reader_title_photo_viewer">%1$d af %2$d</string>
+ <string name="error_publish_empty_post">Kan ikke udgive omt indlæg</string>
+ <string name="error_refresh_unauthorized_posts">Du har ikke tilladelse til at vise eller redigere indlæg</string>
+ <string name="error_refresh_unauthorized_pages">Du har ikke tilladelse til at vise eller redigere sider</string>
+ <string name="error_refresh_unauthorized_comments">Du har ikke tilladelse til at vise eller redigere kommentarer</string>
+ <string name="reader_empty_followed_blogs_title">Du følger ikke nogen websteder endnu</string>
+ <string name="reader_toast_err_generic">Denne handling kan ikke udføres</string>
+ <string name="reader_toast_blog_blocked">Indlæg fra denne blog vil ikke længere blive vist</string>
+ <string name="reader_toast_err_block_blog">Det er ikke muligt at blokere denne blog</string>
+ <string name="reader_menu_block_blog">Bloker denne blog</string>
+ <string name="hs__new_conversation_header">Support chat</string>
+ <string name="hs__conversation_header">Support chat</string>
+ <string name="hs__conversation_detail_error">Beskriv det problem du oplever</string>
+ <string name="hs__invalid_email_error">Indtast en gyldig e-mailadresse</string>
+ <string name="hs__username_blank_error">Indtast et gyldigt navn</string>
+ <string name="contact_us">Kontakt os</string>
+ <string name="current_location">Nuværende placering</string>
+ <string name="add_location">Tilføj placering</string>
+ <string name="search_current_location">Lokaliser</string>
+ <string name="search_location">Søg</string>
+ <string name="edit_location">Rediger</string>
+ <string name="preference_send_usage_stats_summary">Send automatisk brugsstatistik for at hjælpe os med at forbedre WordPress til Android</string>
+ <string name="preference_send_usage_stats">Indsend statistikker</string>
+ <string name="schedule_verb">Planlæg</string>
+ <string name="update_verb">Opdater</string>
+ <string name="reader_label_followed_blog">Blog fulgt</string>
+ <string name="reader_toast_err_get_blog_info">Det er ikke muligt at vise denne blog</string>
+ <string name="reader_toast_err_follow_blog">Det er ikke muligt at følge denne blog</string>
+ <string name="reader_toast_err_unfollow_blog">Det er ikke muligt at stoppe med at følge denne blog</string>
+ <string name="reader_title_subs">Tags &amp; blogs</string>
+ <string name="reader_page_followed_tags">Fulgte tags</string>
+ <string name="reader_label_tag_preview">Indlæg tagget %s</string>
+ <string name="reader_empty_recommended_blogs">Ingen anbefalede blogs</string>
+ <string name="reader_toast_err_already_follow_blog">Du følger allerede denne blog</string>
+ <string name="reader_page_followed_blogs">Fulgte websteder</string>
+ <string name="reader_hint_add_tag_or_url">Indtast en URL eller et tag for at følge</string>
+ <string name="ptr_tip_message">Tip: Træk ned for at genindlæse siden</string>
+ <string name="media_empty_list">Ingen medier</string>
+ <string name="saving">Gemmer...</string>
+ <string name="ssl_certificate_ask_trust">Hvis du plejer at tilgå dit websted på denne måde uden problemer, kan denne fejl betyde at nogen forsøger udgive sig for at være dit websted, og du burde ikke fortsætte. Vælger du at stole på certifikatet alligevel?</string>
+ <string name="help_center">Hjælpecenter</string>
+ <string name="ssl_certificate_error">Dette er ikke et gyldigt SLL certifikat</string>
+ <string name="forgot_password">Mistet din adgangskode?</string>
+ <string name="forums">Fora</string>
+ <string name="help">Hjælp</string>
+ <string name="blog_name_reserved_but_may_be_available">Det websted er pt. reserveret, men det kan være tilgængeligt om et par dage</string>
+ <string name="username_or_password_incorrect">Brugernavnet eller adgangskoden, du indtastede, er forkert</string>
+ <string name="error_moderate_comment">Kunne ikke moderere</string>
+ <string name="error_edit_comment">Kunne ikke redigere kommentaren</string>
+ <string name="error_upload">Kunne ikke overføre %s</string>
+ <string name="error_load_comment">Kunne ikke loade kommentaren</string>
+ <string name="error_downloading_image">Kunne ikke downloade billede</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Webstedsadresse må kun indeholde små bogstaver (a-z) og tal</string>
+ <string name="blog_name_cant_be_used">Du kan ikke benytte den webstedsadresse</string>
+ <string name="blog_name_contains_invalid_characters">Webstedsadresse må ikke indeholde tegnet “_”</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Webstedsadressen skal være kortere end 64 tegn</string>
+ <string name="blog_name_must_be_at_least_four_characters">Webstedsadresse skal være på mindst 4 tegn</string>
+ <string name="nux_cannot_log_in">Vi kan ikke logge dig ind</string>
+ <string name="blog_name_reserved">Det websted er reserveret</string>
+ <string name="out_of_memory">Enheden er løbet tør for hukommelse</string>
+ <string name="no_network_message">Der er intet netværk tilgængeligt</string>
+ <string name="sdcard_message">Et SD-kort skal være stukket ind og tilgængeligt for at kunne uploade medier</string>
+ <string name="no_account">Ingen WordPress konto fundet. Tilføj en konto og prøv igen</string>
+ <string name="theme_set_failed">Det lykkedes ikke at indstille temaet på plads</string>
+ <string name="theme_fetch_failed">Det lykkedes ikke at hente temaer</string>
+ <string name="theme_auth_error_message">Vær sikker på du har de rette tilladelse til at indstille temaer</string>
+ <string name="no_site_error">Kunne ikke få forbindelse til WordPress webstedet</string>
+ <string name="invalid_username_too_short">Brugernavnet skal være længere end fire tegn</string>
+ <string name="invalid_username_too_long">Brugernavnet må maksimum være på 61 tegn</string>
+ <string name="username_only_lowercase_letters_and_numbers">Brugernavnet må kun indeholde små bogstaver og tal (f.eks. a-z og 0-9)</string>
+ <string name="username_must_be_at_least_four_characters">Brugernavnet skal være mindst fire tegn</string>
+ <string name="username_contains_invalid_characters">Brugernavnet må ikke indholde tegnet "_"</string>
+ <string name="username_must_include_letters">Brugernavnet skal have mindst et bogstav (fra a-z)</string>
+ <string name="email_not_allowed">Denne e-mailadresse er ikke tilladt</string>
+ <string name="username_reserved_but_may_be_available">Brugernavnet er reserveret, men det kan blive ledigt om et par dage</string>
+ <string name="passcode_wrong_passcode">Forkert PIN</string>
+ <string name="blog_name_not_allowed">Dette websted er ikke tilladt</string>
+ <string name="email_invalid">Indtast en gyldig e-mailadresse</string>
+ <string name="invalid_email_message">Din e-mailadresse er ikke gyldig</string>
+ <string name="invalid_password_message">Adgangskoden skal være på mindst fire tegn</string>
+ <string name="stats_bar_graph_empty">Ingen statistik er tilgængelig</string>
+ <string name="stats_empty_comments">Ingen kommentarer endnu</string>
+ <string name="reply_failed">Svar fejlede</string>
+ <string name="error_delete_post">Der skete en fejl mens %s blev slettet</string>
+ <string name="error_refresh_posts">Indlæg kunne ikke genindlæses </string>
+ <string name="error_refresh_pages">Sider kunne ikke genindlæses</string>
+ <string name="error_refresh_notifications">Beskeder kunne ikke genindlæses</string>
+ <string name="error_refresh_comments">Kommentarer kunne ikke genindlæses</string>
+ <string name="blog_not_found">Der skete en fejl ved tilgangen til denne blog</string>
+ <string name="wait_until_upload_completes">Vent venligst til overførslen er færdig</string>
+ <string name="blog_name_exists">Webstedet eksisterer allerede</string>
+ <string name="error_generic">Der opstod en fejl</string>
+ <string name="adding_cat_success">Kategori tilføjet</string>
+ <string name="adding_cat_failed">Kategorien kunne ikke tilføjes</string>
+ <string name="email_exists">E-mailadressen er allerede i brug</string>
+ <string name="blog_name_required">Indtast en webstedsadresse</string>
+ <string name="username_required">Indtast et brugernavn</string>
+ <string name="username_exists">Brugernavnet existerer allerede</string>
+ <string name="username_not_allowed">Brugernavnet er ikke tilladt</string>
+ <string name="mnu_comment_unspam">Ikke spam</string>
+ <string name="comments_empty_list">Ingen kommentarer</string>
+ <string name="notifications_empty_list">Ingen notifikationer</string>
+ <string name="gallery_error">Mediet kunne ikke hentes</string>
+ <string name="error_refresh_stats">Statistikker kan ikke genindlæses lige nu</string>
+ <string name="could_not_remove_account">Kunne ikke fjerne websted</string>
+ <string name="invalid_url_message">Undersøg om den indtastede URL er gyldig</string>
+ <string name="http_credentials">HTTP-oplysninger (valgfri)</string>
+ <string name="category_slug">Kategori-korttitel (valgfri)</string>
+ <string name="post_not_published">Indlægsstatus er ikke udgivet</string>
+ <string name="page_not_published">Sidestatus er ikke udgivet</string>
+ <string name="category_desc">Kategoribeskrivelse (valgfri)</string>
+ <string name="category_parent">Kategoriforælder (valgfri)</string>
+ <string name="pending_review">Afventer gennemlæsning</string>
+ <string name="http_authorization_required">Godkendelse påkrævet</string>
+ <string name="sure_to_cancel_edit_comment">Annuller redigering af denne kommentar?</string>
+ <string name="blog_name_must_include_letters">Webstedsadresse skal mindst indeholde 1 bogstav (a-z)</string>
+ <string name="local_changes">Lokale ændringer</string>
+ <string name="horizontal_alignment">Vandret justering</string>
+ <string name="link_enter_url_text">Linktekst (valgfri)</string>
+ <string name="media_gallery_image_order">Billederækkefølge</string>
+ <string name="blog_name_invalid">Ugyldig webstedsadresse</string>
+ <string name="blog_title_invalid">Ugyldig webstedstitel</string>
+ <string name="reader_toast_err_remove_tag">Kunne ikke fjerne dette tag</string>
+ <string name="reader_toast_err_add_tag">Kunne ikke tilføje dette tag</string>
+ <string name="reader_title_applog">Applikationslog</string>
+ <string name="cancel_edit">Fortryd redigering</string>
+ <string name="theme_premium_theme">Premium tema</string>
+ <string name="open_source_licenses">Open source-licenser</string>
+ <string name="location_not_found">Ukendt placering</string>
+ <string name="share_action_media">Mediebibliotek</string>
+ <string name="share_action_post">Nyt indlæg</string>
+ <string name="local_draft">Lokal kladde</string>
+ <string name="media_gallery_edit">Rediger galleri</string>
+ <string name="toast_comment_unedited">Kommentar er ikke ændret</string>
+ <string name="required_field">Påkrævet felt</string>
+ <string name="content_required">Kommentar er påkrævet</string>
+ <string name="email_hint">E-mailadresse</string>
+ <string name="reader_share_link">Del link</string>
+ <string name="image_settings">Billedeindstillinger</string>
+ <string name="add_account_blog_url">Blog-adresse</string>
+ <string name="privacy_policy">Privatlivspolitik</string>
+ <string name="wordpress_blog">WordPress-blog</string>
+ <string name="new_media">Nyt medie</string>
+ <string name="view_site">Vis websted</string>
+ <string name="new_post">Nyt indlæg</string>
+ <string name="post_format">Indlægsformat</string>
+ <string name="media_gallery_num_columns">Antal kolonner</string>
+ <string name="category_name">Kategorinavn</string>
+ <string name="add_new_category">Tilføj ny kategori</string>
+ <string name="view_in_browser">Vis i browser</string>
+ <string name="dlg_trashing_comments">Smider i papirkurven</string>
+ <string name="dlg_spamming_comments">Markerer som spam</string>
+ <string name="dlg_unapproving_comments">Fjerner godkendelse</string>
+ <string name="dlg_approving_comments">Godkender</string>
+ <string name="dlg_confirm_trash_comments">Smid i papirkurven?</string>
+ <string name="blog_removed_successfully">Websted blev fjernet</string>
+ <string name="delete_draft">Slet kladde</string>
+ <string name="preview_page">Forhåndsvis side</string>
+ <string name="comment_added">Kommentar tilføjet</string>
+ <string name="preview_post">Forhåndsvis indlæg</string>
+ <string name="saving_changes">Gemmer ændringer</string>
+ <string name="hint_comment_content">Kommentar</string>
+ <string name="author_url">Forfatter-URL</string>
+ <string name="author_email">Forfatter-e-mail</string>
+ <string name="author_name">Forfatternavn</string>
+ <string name="trash">Papirkurv</string>
+ <string name="trash_no">Slet ikke</string>
+ <string name="trash_yes">Slet</string>
+ <string name="mnu_comment_trash">Slet</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_unapprove">Fjern godkendelse</string>
+ <string name="mnu_comment_approve">Godkend</string>
+ <string name="edit_comment">Rediger kommentar</string>
+ <string name="comment_status_trash">Slettet</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_unapproved">Afventer</string>
+ <string name="comment_status_approved">Godkendt</string>
+ <string name="delete_page">Slet side</string>
+ <string name="delete_post">Slet indlæg</string>
+ <string name="post_settings">Indstillinger for indlæg</string>
+ <string name="upload_failed">Overførelse fejlede</string>
+ <string name="page_settings">Indstillinger for side</string>
+ <string name="theme_current_theme">Nuværende tema</string>
+ <string name="themes_live_preview">Live forhåndsvisning</string>
+ <string name="learn_more">Lær mere</string>
+ <string name="media_gallery_settings_title">Indstillinger for galleri</string>
+ <string name="connection_error">Forbindelsesfejl</string>
+ <string name="add_comment">Tilføj kommentar</string>
+ <string name="edit_post">Rediger indlæg</string>
+ <string name="account_details">Kontodetaljer</string>
+ <string name="select_categories">Vælg kategorier</string>
+ <string name="create_a_link">Opret et link</string>
+ <string name="site_address">Adresse fra din egen udbyder (URL)</string>
+ <string name="cannot_delete_multi_media_items">Nogle medier kan ikke slettes på nuværende tidspunkt. Prøv igen senere.</string>
+ <string name="media_error_no_permission">Du har ikke tilladelse til at se mediebiblioteket</string>
+ <string name="remove_account">Fjern websted</string>
+ <string name="deleting_page">Sletter side</string>
+ <string name="deleting_post">Sletter indlæg</string>
+ <string name="share_url_post">Del indlæg</string>
+ <string name="share_url_page">Del side</string>
+ <string name="share_link">Del link</string>
+ <string name="creating_your_account">Opretter din konto</string>
+ <string name="creating_your_site">Opretter dit websted</string>
+ <string name="error_refresh_media">Noget gik galt under opdateringen af mediebiblioteket. Prøv igen senere.</string>
+ <string name="reader_empty_posts_in_tag_updating">Henter indlæg...</string>
+ <string name="reader_toast_err_get_comment">Kunne ikke hente denne kommentar</string>
+ <string name="reader_likes_multi">%,d personer synes godt om dette</string>
+ <string name="video">Video</string>
+ <string name="download">Henter medie</string>
+ <string name="comment_spammed">Kommentar markeret som spam</string>
+ <string name="reader_label_reply">Besvar</string>
+ <string name="reader_likes_you_and_multi">Du og %,d andre personer synes godt om dette</string>
+ <string name="cant_share_no_visible_blog">Du kan ikke dele til WordPress uden en synlig blog</string>
+ <string name="reader_toast_err_get_post">Kunne ikke hente dette indlæg</string>
+ <string name="reader_likes_you_and_one">Du og en anden synes godt om dette</string>
+ <string name="validating_user_data">Validerer brugerdata</string>
+ <string name="validating_site_data">Validerer webstedsdata</string>
+ <string name="pick_video">Vælg video</string>
+ <string name="pick_photo">Vælg foto</string>
+ <string name="select_time">Vælg tidspunkt</string>
+ <string name="select_date">Vælg dato</string>
+ <string name="nux_oops_not_selfhosted_blog">Log ind på WordPress.com</string>
+ <string name="nux_tap_continue">Fortsæt</string>
+ <string name="nux_welcome_create_account">Opret konto</string>
+ <string name="signing_in">Logger ind...</string>
+ <string name="nux_add_selfhosted_blog">Tilføj websted fra egen udbyder</string>
+ <string name="password_invalid">Din adgangskode skal være mere sikker. Sørg for at benytte mindst 7 tegn og blande store og små bogstaver, tal eller specielle tegn.</string>
+ <string name="reader_toast_err_tag_exists">Du følger allerede dette tag</string>
+ <string name="username_invalid">Ugyldigt brugernavn</string>
+ <string name="reader_toast_err_comment_failed">Kunne ikke sende din kommentar</string>
+ <string name="reader_toast_err_view_image">Kunne ikke vise billede</string>
+ <string name="reader_toast_err_share_intent">Kunne ikke dele</string>
+ <string name="reader_toast_err_url_intent">Kunne ikke åbne %s</string>
+ <string name="reader_empty_followed_tags">Du følger ikke nogen tags</string>
+ <string name="connecting_wpcom">Forbinder til WordPress.com</string>
+ <string name="create_account_wpcom">Opret en konto på WordPress.com</string>
+ <string name="reader_toast_err_tag_invalid">Dette er ikke et gyldigt tag</string>
+ <string name="jetpack_not_found">Jetpack-pluginnet blev ikke fundet</string>
+ <string name="reader_share_subject">Delt fra %s</string>
+ <string name="reader_untitled_post">(Ingen titel)</string>
+ <string name="media_add_popup_title">Tilføj til mediebibliotek</string>
+ <string name="media_add_new_media_gallery">Opret galleri</string>
+ <string name="empty_list_default">Listen er tom</string>
+ <string name="select_from_media_library">Vælg fra mediebiblioteket</string>
+ <string name="reader_label_added_tag">Tilføjede %s</string>
+ <string name="reader_label_removed_tag">Fjernede %s</string>
+ <string name="reader_likes_only_you">Du synes godt om dette</string>
+ <string name="reader_btn_follow">Følg</string>
+ <string name="reader_btn_unfollow">Følger</string>
+ <string name="reader_btn_share">Del</string>
+ <string name="nux_tutorial_get_started_title">Kom i gang!</string>
+ <string name="reader_likes_one">En person synes godt om dette</string>
+ <string name="reader_hint_comment_on_comment">Besvar kommentar...</string>
+ <string name="stats_view_tags_and_categories">Tags &amp; kategorier</string>
+ <string name="media_edit_caption_hint">Indtast en tekst her</string>
+ <string name="theme_auth_error_title">Kunne ikke hente temaer</string>
+ <string name="stats_entry_referrers">Henviser</string>
+ <string name="media_edit_caption_text">Billedtekst</string>
+ <string name="passcode_manage">Håndter PIN-lås</string>
+ <string name="theme_set_success">Tema indstillet!</string>
+ <string name="media_edit_description_hint">Indtast en beskrivelse her</string>
+ <string name="media_edit_title_hint">Indtast en titel her</string>
+ <string name="media_add_popup_capture_video">Optag video</string>
+ <string name="media_add_popup_capture_photo">Tag billede</string>
+ <string name="unattached">Frigjort</string>
+ <string name="passcode_re_enter_passcode">Genindtast PIN-kode</string>
+ <string name="passcode_enter_passcode">Indtast PIN-kode</string>
+ <string name="passcode_enter_old_passcode">Indtast gammel PIN-kode</string>
+ <string name="custom_date">Egen dato</string>
+ <string name="passcode_turn_on">Aktiver PIN-lås</string>
+ <string name="passcode_preference_title">PIN-lås</string>
+ <string name="passcode_turn_off">Deaktiver PIN-lås</string>
+ <string name="media_gallery_image_order_random">Tilfældig</string>
+ <string name="media_edit_title_text">Titel</string>
+ <string name="media_edit_description_text">Beskrivelse</string>
+ <string name="media_gallery_type_circles">Cirkler</string>
+ <string name="media_gallery_type_squares">Firkanter</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="themes">Temaer</string>
+ <string name="images">Billeder</string>
+ <string name="all">Alle</string>
+ <string name="stats_totals_clicks">Klik</string>
+ <string name="stats_totals_views">Visninger</string>
+ <string name="stats_entry_authors">Forfatter</string>
+ <string name="stats_entry_tags_and_categories">Emne</string>
+ <string name="stats_entry_country">Land</string>
+ <string name="stats_entry_posts_and_pages">Titel</string>
+ <string name="stats_timeframe_today">I dag</string>
+ <string name="stats_timeframe_yesterday">I går</string>
+ <string name="stats_timeframe_days">Dage</string>
+ <string name="stats_timeframe_weeks">Uger</string>
+ <string name="stats_timeframe_months">Måneder</string>
+ <string name="stats_view_clicks">Klik</string>
+ <string name="stats_view_referrers">Henvisninger</string>
+ <string name="stats_view_visitors_and_views">Besøgende og visninger</string>
+ <string name="share_action">Del</string>
+ <string name="stats">Statistikker</string>
+ <string name="media_edit_success">Opdateret</string>
+ <string name="media_edit_failure">Opdatering mislykkedes</string>
+ <string name="themes_details_label">Detaljer</string>
+ <string name="themes_features_label">Funktioner</string>
+ <string name="theme_activate_button">Aktiver</string>
+ <string name="theme_activating_button">Aktiverer</string>
+ <string name="post_excerpt">Uddrag</string>
+ <string name="share_action_title">Tilføj til ...</string>
+ <string name="stats_totals_plays">Afspilninger</string>
+ <string name="passcode_change_passcode">Ændr PIN</string>
+ <string name="passcode_set">PIN ændret</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="upload">Overfør</string>
+ <string name="sign_in">Log ind</string>
+ <string name="notifications">Meddelelser</string>
+ <string name="note_reply_successful">Svar sendt</string>
+ <string name="new_notifications">%d nye notifikationer</string>
+ <string name="more_notifications">og %d mere.</string>
+ <string name="follows">Følger</string>
+ <string name="loading">Indlæser...</string>
+ <string name="httppassword">HTTP-adgangskode</string>
+ <string name="httpuser">HTTP-brugernavn</string>
+ <string name="error_media_upload">Der opstod en fejl under overførsel af medie</string>
+ <string name="post_content">Indhold (tap for at tilføje tekst og medier)</string>
+ <string name="content_description_add_media">Tilføj medier</string>
+ <string name="publish_date">Udgiv</string>
+ <string name="incorrect_credentials">Forkert brugernavn eller adgangskode.</string>
+ <string name="password">Kodeord</string>
+ <string name="username">Brugernavn</string>
+ <string name="reader">Læser</string>
+ <string name="featured_in_post">Inkluder billede i indhold</string>
+ <string name="featured">Brug som fremhævet billede</string>
+ <string name="caption">Billedtekst (valgfri)</string>
+ <string name="no_network_title">Intet netværk tilgængeligt</string>
+ <string name="post">Indlæg</string>
+ <string name="anonymous">Anonym</string>
+ <string name="page">Side</string>
+ <string name="posts">Indlæg</string>
+ <string name="pages">Sider</string>
+ <string name="width">Bredde</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">blogusername</string>
+ <string name="upload_scaled_image">Upload og link til skalerede billede</string>
+ <string name="scaled_image">Bredde på skalerede billede</string>
+ <string name="scheduled">Planlagt</string>
+ <string name="link_enter_url">URL</string>
+ <string name="version">Version</string>
+ <string name="app_title">WordPress til Android</string>
+ <string name="tos">Brugskrav</string>
+ <string name="max_thumbnail_px_width">Standard billedbredde</string>
+ <string name="image_alignment">Justering</string>
+ <string name="refresh">Genindlæs</string>
+ <string name="untitled">Ikke-navngivet</string>
+ <string name="edit">Rediger</string>
+ <string name="post_id">Indlæg</string>
+ <string name="page_id">Side</string>
+ <string name="post_password">Kodeord (valgfrit)</string>
+ <string name="immediately">Øjeblikkeligt</string>
+ <string name="quickpress_add_alert_title">Vælg navn på genvej.</string>
+ <string name="settings">Indstillinger</string>
+ <string name="today">I dag</string>
+ <string name="share_url">Del URL</string>
+ <string name="quickpress_window_title">Vælg blog til QuickPress genvej</string>
+ <string name="quickpress_add_error">Genvejsnavn kan ikke være tomt</string>
+ <string name="publish_post">Udgiv</string>
+ <string name="draft">Kladde</string>
+ <string name="post_private">Privat</string>
+ <string name="upload_full_size_image">Upload og link til hele billedet</string>
+ <string name="title">Titel</string>
+ <string name="categories">Kategorier</string>
+ <string name="tags_separate_with_commas">Tags (adskil tags med komma)</string>
+ <string name="dlg_deleting_comments">Sletter kommentarer</string>
+ <string name="notification_vibrate">Vibrer</string>
+ <string name="notification_blink">Blink beskedlys</string>
+ <string name="status">Status</string>
+ <string name="select_video">Vælg en video fra galleriet</string>
+ <string name="sdcard_title">SD-kort påkrævet</string>
+ <string name="location">Placering</string>
+ <string name="media">Medier</string>
+ <string name="delete">Slet</string>
+ <string name="none">Ingen</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Vælg et billede fra galleri</string>
+ <string name="error">Fejl</string>
+ <string name="save">Gem</string>
+ <string name="add">Tilføj</string>
+ <string name="cancel">Annuller</string>
+ <string name="reply">Svar</string>
+ <string name="no">Nej</string>
+ <string name="yes">Ja</string>
+ <string name="category_refresh_error">Kategori opdateringsfejl</string>
+ <string name="on">på</string>
+ <string name="preview">Forhåndsvis</string>
+</resources>
diff --git a/WordPress/src/main/res/values-de/strings.xml b/WordPress/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..cd6082832
--- /dev/null
+++ b/WordPress/src/main/res/values-de/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Redakteur</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Mitarbeiter</string>
+ <string name="role_follower">Follower</string>
+ <string name="role_viewer">Besucher</string>
+ <string name="error_post_my_profile_no_connection">Keine Verbindung, dein Profil konnte nicht gespeichert werden</string>
+ <string name="alignment_none">Keine</string>
+ <string name="alignment_left">Links</string>
+ <string name="alignment_right">Rechts</string>
+ <string name="site_settings_list_editor_action_mode_title">Ausgewählt %1$d</string>
+ <string name="error_fetch_users_list">Website-Benutzer konnten nicht abgerufen werden</string>
+ <string name="plans_manage">Verwalte deinen Tarif auf\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Du hast noch keine Besucher.</string>
+ <string name="people_fetching">Benutzer werden abgerufen ...</string>
+ <string name="title_follower">Follower</string>
+ <string name="title_email_follower">E-Mail-Follower</string>
+ <string name="people_empty_list_filtered_email_followers">Du hast noch keine E-Mail-Follower.</string>
+ <string name="people_empty_list_filtered_followers">Du hast noch keine Follower.</string>
+ <string name="people_empty_list_filtered_users">Du hast bisher keine Benutzer.</string>
+ <string name="people_dropdown_item_email_followers">E-Mail-Follower</string>
+ <string name="people_dropdown_item_viewers">Besucher</string>
+ <string name="people_dropdown_item_followers">Follower</string>
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="invite_message_usernames_limit">Lade bis zu 10 E-Mail-Adressen und/oder WordPress.com Benutzernamen ein. Diejenigen, welche einen Benutzernamen benötigen, werden eine Anleitung gesendet bekommen, wie sie einen erstellen.</string>
+ <string name="viewer_remove_confirmation_message">Falls du diesen Besucher entfernst, kann er oder sie die Website nicht mehr ansehen.\n\nMöchtest du diesen Besucher noch immer entfernen?</string>
+ <string name="follower_remove_confirmation_message">Nach dem Entfernen bekommt der Follower keine Mitteilungen über dieser Website mehr, bis er wieder folgt.\n\nMöchtest du diesen Follower noch immer entfernen?</string>
+ <string name="follower_subscribed_since">Seit %1$s</string>
+ <string name="reader_label_view_gallery">Galerie anzeigen</string>
+ <string name="error_remove_follower">Konnte Follower nicht entfernen</string>
+ <string name="error_remove_viewer">Konnte Besucher nicht entfernen</string>
+ <string name="error_fetch_email_followers_list">Konnte E-Mail-Follower der Website nicht abrufen</string>
+ <string name="error_fetch_followers_list">Konnte Follower der Website nicht abrufen</string>
+ <string name="editor_failed_uploads_switch_html">Einige Medien konnten nicht hochgeladen werden. Du kannst daher derzeit nicht in den HTML-Modus umschalten. Möchtest du alle fehlgeschlagenen Uploads entfernen und fortfahren?</string>
+ <string name="format_bar_description_html">HTML-Modus</string>
+ <string name="visual_editor">Visueller Editor</string>
+ <string name="image_thumbnail">Vorschaubild</string>
+ <string name="format_bar_description_ul">Ungeordnete Aufzählung</string>
+ <string name="format_bar_description_ol">Geordnete Aufzählung</string>
+ <string name="format_bar_description_more">Weitere einfügen</string>
+ <string name="format_bar_description_media">Medien einfügen</string>
+ <string name="format_bar_description_strike">Durchgestrichen</string>
+ <string name="format_bar_description_quote">Zitatblock</string>
+ <string name="format_bar_description_link">Link einfügen</string>
+ <string name="format_bar_description_italic">Kursiv</string>
+ <string name="format_bar_description_underline">Unterstrichen</string>
+ <string name="image_settings_save_toast">Änderungen gespeichert</string>
+ <string name="image_caption">Bildunterschrift</string>
+ <string name="image_alt_text">Alternativtext</string>
+ <string name="image_link_to">Link zu</string>
+ <string name="image_width">Breite</string>
+ <string name="format_bar_description_bold">Fettschrift</string>
+ <string name="image_settings_dismiss_dialog_title">Ungespeicherte Änderungen verwerfen?</string>
+ <string name="stop_upload_dialog_title">Hochladen stoppen?</string>
+ <string name="stop_upload_button">Upload abbrechen</string>
+ <string name="alert_error_adding_media">Beim Einfügen von Medien ist ein Fehler aufgetreten</string>
+ <string name="alert_action_while_uploading">Derzeit lädst du noch Medien hoch. Bitte warte bis der Vorgang abgeschlossen ist.</string>
+ <string name="alert_insert_image_html_mode">Du kannst keine Medien im HTML-Modus einfügen. Bitte wechsle zurück in den Visuellen Editor und versuche es erneut.</string>
+ <string name="uploading_gallery_placeholder">Galerie wird hochgeladen...</string>
+ <string name="invite_error_some_failed">Einladung gesendet, aber es ist mindestens ein Fehler aufgetreten.</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Einladung erfolgreich gesendet</string>
+ <string name="tap_to_try_again">Tippe, um es erneut zu probieren!</string>
+ <string name="invite_error_sending">Ein Fehler ist während dem Senden der Einladung aufgetreten!</string>
+ <string name="invite_error_invalid_usernames_multiple">Senden fehlgeschlagen: Es wurden ungültige Benutzernamen oder E-Mail-Adressen angegeben</string>
+ <string name="invite_error_invalid_usernames_one">Senden fehlgeschlagen: Benutzername oder E-Mail-Adresse sind ungültig</string>
+ <string name="invite_error_no_usernames">Bitte trage mindestens einen Benutzernamen ein</string>
+ <string name="invite_message_info">(Optional) Du kannst eine individuelle Nachricht mit bis zu 500 Zeichen eintragen, die in der Einladung an die Benutzer mitgesendet wird.</string>
+ <string name="invite_message_remaining_other">Noch %d weitere Zeichen möglich</string>
+ <string name="invite_message_remaining_one">1 weiteres Zeichen möglich</string>
+ <string name="invite_message_remaining_zero">Keine weiteren Zeichen möglich</string>
+ <string name="invite_invalid_email">Die E-Mail-Adresse „%s“ ist ungültig</string>
+ <string name="invite_message_title">Individuelle Nachricht</string>
+ <string name="invite_already_a_member">Es gibt bereits ein Mitglied mit dem Benutzernamen \'%s\'</string>
+ <string name="invite_username_not_found">Es wurde kein Benutzer mit dem Benutzernamen \'%s\' gefunden</string>
+ <string name="invite">Einladen</string>
+ <string name="invite_names_title">Benutzernamen oder E-Mails</string>
+ <string name="signup_succeed_signin_failed">Dein Konto wurde erstellt, aber bei deiner Anmeldung ist ein Fehler\n aufgetreten. Versuche, dich mit deinem neu erstellten Benutzernamen und Passwort anzumelden.</string>
+ <string name="send_link">Link senden</string>
+ <string name="my_site_header_external">Extern</string>
+ <string name="invite_people">Personen einladen</string>
+ <string name="label_clear_search_history">Lösche Suchverlauf</string>
+ <string name="dlg_confirm_clear_search_history">Suchverlauf löschen?</string>
+ <string name="reader_empty_posts_in_search_description">Keine Beiträge zu %s in deiner Sprache gefunden.</string>
+ <string name="reader_label_post_search_running">Suche...</string>
+ <string name="reader_label_related_posts">Ähnliche Beiträge</string>
+ <string name="reader_empty_posts_in_search_title">Keine Beiträge gefunden</string>
+ <string name="reader_label_post_search_explainer">Durchsuche alle öffentlichen WordPress.com-Blogs</string>
+ <string name="reader_hint_post_search">Durchsuche WordPress.com</string>
+ <string name="reader_title_related_post_detail">Ähnliche Beiträge</string>
+ <string name="reader_title_search_results">Suche nach %s</string>
+ <string name="preview_screen_links_disabled">Im Vorschaubildschirm sind Links deaktiviert.</string>
+ <string name="draft_explainer">Dieser Beitrag ist ein Entwurf und wurde noch nicht veröffentlicht</string>
+ <string name="send">Senden</string>
+ <string name="user_remove_confirmation_message">Falls du %1$s entfernst, kann dieser Benutzer nicht länger auf die Website zugreifen, aber alle von %1$s erstellten Beiträge verbleiben auf der Website.\n\nMöchtest du diesen Benutzter noch immer entfernen?</string>
+ <string name="person_removed">\@%1$s erfolgreich entfernt</string>
+ <string name="person_remove_confirmation_title">Entferne %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Die Websites dieser Liste haben keine neuen Beiträge</string>
+ <string name="people">Menschen</string>
+ <string name="edit_user">Benutzer bearbeiten</string>
+ <string name="role">Rolle</string>
+ <string name="error_remove_user">Benutzer konnte nicht entfernt werden</string>
+ <string name="error_fetch_viewers_list">Website-Besucher konnten nicht abgerufen werden</string>
+ <string name="error_update_role">Benutzerrolle konnte nicht aktualisiert werden</string>
+ <string name="gravatar_camera_and_media_permission_required">Zum aufnehmen oder auswählen eines Fotos sind die entsprechenden Rechte erforderlich</string>
+ <string name="error_updating_gravatar">Beim Aktualisieren deines Gravatars ist ein Fehler aufgetreten</string>
+ <string name="error_locating_image">Bei der Lokalisierung des zugeschnittenen Bildes ist ein Fehler aufgetreten.</string>
+ <string name="error_refreshing_gravatar">Beim Neuladen deines Gravatars ist ein Fehler aufgetreten</string>
+ <string name="gravatar_tip">Neu! Tippe deinen Gravatar an, um ihn zu ändern!</string>
+ <string name="error_cropping_image">Beim Zuschneiden des Bildes ist ein Fehler aufgetreten</string>
+ <string name="launch_your_email_app">E-Mail-Programm starten</string>
+ <string name="checking_email">Überprüfe E-Mail-Adresse</string>
+ <string name="not_on_wordpress_com">Nicht bei WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Zur Zeit nicht verfügbar. Bitte gib dein Passwort ein</string>
+ <string name="check_your_email">Überprüfe dein Postfach</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Lass dir einen Link für die sofortige Anmeldung per E-Mail zuschicken</string>
+ <string name="logging_in">Anmeldung läuft</string>
+ <string name="enter_your_password_instead">Gib stattdessen dein Passwort ein</string>
+ <string name="web_address_dialog_hint">Wird mit deinem Kommentar veröffentlicht.</string>
+ <string name="jetpack_not_connected_message">Das Jetpack-Plugin ist installiert, aber nicht mit WordPress.com verbunden. Möchtest du es verbinden?</string>
+ <string name="username_email">E-Mail-Adresse oder Benutzername</string>
+ <string name="jetpack_not_connected">Jetpack-Plugin nicht verbunden</string>
+ <string name="new_editor_reflection_error">Der visuelle Editor ist mit deinem Gerät nicht kompatibel.\nEr wurde automatisch deaktiviert.</string>
+ <string name="stats_insights_latest_post_no_title">(ohne Titel)</string>
+ <string name="capture_or_pick_photo">Foto aufnehmen oder auswählen</string>
+ <string name="plans_post_purchase_text_themes">Jetzt hast du unbegrenzten Zugriff auf Premium-Themes. Sieh dir als ersten Schritt die Vorschau eines beliebigen Themes auf deiner Website an.</string>
+ <string name="plans_post_purchase_button_themes">Themes durchstöbern</string>
+ <string name="plans_post_purchase_title_themes">Finde ein perfektes Premium Theme</string>
+ <string name="plans_post_purchase_button_video">Neuen Beitrag erstellen</string>
+ <string name="plans_post_purchase_text_video">Mit VideoPress und deinem erweiterten Medienspeicher kannst du Videos hochladen und auf deiner Website hosten.</string>
+ <string name="plans_post_purchase_title_video">Lebendigere Beiträge mit Videos</string>
+ <string name="plans_post_purchase_button_customize">Website anpassen</string>
+ <string name="plans_post_purchase_text_customize">Du hast nun Zugang zu individuellen Schriften, individuellen Farben und individuellen CSS-Bearbeitungsmöglichkeiten.</string>
+ <string name="plans_post_purchase_text_intro">Deine Website macht Sprünge vor Begeisterung. Erkunde jetzt die neuen Funktionen deiner Website und wähle wo du beginnen möchtest.</string>
+ <string name="plans_post_purchase_title_customize">Schriften und Farben anpassen.</string>
+ <string name="plans_post_purchase_title_intro">Damit hast du den Tarif! Bestens!</string>
+ <string name="export_your_content_message">Deine Beiträge, Seiten und Einstellungen werden dir per E-Mail an %s gesendet.</string>
+ <string name="plan">Tarif</string>
+ <string name="plans">Tarife</string>
+ <string name="plans_loading_error">Tarife konnten nicht geladen werden</string>
+ <string name="export_your_content">Exportiere deine Inhalte</string>
+ <string name="exporting_content_progress">Inhalt wird exportiert&amp;hellip;</string>
+ <string name="export_email_sent">Exort E-Mail gesendet!</string>
+ <string name="premium_upgrades_message">Du hast aktive Premium Upgrades auf deiner Website. Bitte storniere diese Upgrades, bevor du deine Website löschst.</string>
+ <string name="show_purchases">Käufe anzeigen</string>
+ <string name="checking_purchases">Überprüfe Einkäufe</string>
+ <string name="premium_upgrades_title">Premium Upgrades</string>
+ <string name="purchases_request_error">Es ist ein Fehler aufgetreten. Einkäufe konnten nicht abgefragt werden.</string>
+ <string name="delete_site_progress">Website wird gelöscht &amp;hellip;</string>
+ <string name="delete_site_summary">Diese Aktion kann nicht rückgängig gemacht werden. Wenn du die Website löschst, werden alle Inhalte, Mitarbeiter und Domains von der Website entfernt.</string>
+ <string name="delete_site_hint">Website löschen</string>
+ <string name="export_site_hint">Exportiere deine Website in eine XML-Datei</string>
+ <string name="are_you_sure">Bist du sicher?</string>
+ <string name="export_site_summary">Wenn du dir sicher bist, nimm dir jetzt bitte Zeit zum Exportieren deiner Inhalte. Später kannst du sie nicht mehr wiederherstellen.</string>
+ <string name="keep_your_content">Deine Inhalte behalten</string>
+ <string name="domain_removal_hint">Domain, die nicht mehr funktioniert, sobald deine Website gelöscht wird</string>
+ <string name="domain_removal_summary">Vorsicht! Wenn du deine Website löschst, werden auch deine unten aufgeführten Domains entfernt.</string>
+ <string name="primary_domain">Primäre Domain</string>
+ <string name="domain_removal">Domain-Entfernung</string>
+ <string name="error_deleting_site_summary">Beim Löschen deiner Website ist ein Fehler aufgetreten. Bitte kontaktiere den Support für weiterführende Hilfe.</string>
+ <string name="error_deleting_site">Fehler beim Löschen der Website</string>
+ <string name="confirm_delete_site_prompt">Bitte gib %1$s zur Bestätigung unten in das Feld ein. Dann ist deine Website für immer entfernt.</string>
+ <string name="site_settings_export_content_title">Inhalte exportieren</string>
+ <string name="contact_support">Support kontaktieren</string>
+ <string name="confirm_delete_site">Löschen der Website bestätigen</string>
+ <string name="start_over_text">Wenn du eine Website haben möchtest, aber ohne deine aktuellen Beiträge und Seiten, kann unser Support-Team deine Beiträge, Seiten, Medien und Kommentare für dich löschen.\n\nSo bleiben deine Website und URL aktiv, aber du kannst von Neuem mit der Erstellung von Inhalten beginnen. Kontaktiere uns einfach, damit wir deine aktuellen Inhalte entfernen.</string>
+ <string name="site_settings_start_over_hint">Nochmal mit deiner Website beginnen</string>
+ <string name="let_us_help">Lass uns helfen</string>
+ <string name="me_btn_app_settings">App Einstellungen</string>
+ <string name="start_over">Neu beginnen</string>
+ <string name="editor_remove_failed_uploads">Fehlgeschlagene Uploads entfernen</string>
+ <string name="editor_toast_failed_uploads">Einige Medien-Uploads sind fehlgeschlagen. In diesem Zustand ist das Speichern oder Veröffentlichen deiner Beiträge nicht möglich. Willst du alle fehlgeschlagenen Medien-Uploads löschen?</string>
+ <string name="comments_empty_list_filtered_trashed">Keine gelöschten Kommentare</string>
+ <string name="site_settings_advanced_header">Erweitert</string>
+ <string name="comments_empty_list_filtered_pending">Keine ausstehenden Kommentare</string>
+ <string name="comments_empty_list_filtered_approved">Keine genehmigten Kommentare</string>
+ <string name="button_done">Erledigt</string>
+ <string name="button_skip">Überspringen</string>
+ <string name="site_timeout_error">Konnte nicht zur WordPress-Website verbinden, infolge einer Zeitüberschreitung.</string>
+ <string name="xmlrpc_malformed_response_error">Konnte nicht verbinden. Die WordPress Installation antwortete mit einem ungültigen XML-RPC Dokument.</string>
+ <string name="xmlrpc_missing_method_error">Konnte nicht verbinden. Die benötigte XML-RPC Methoden sind nicht auf dem Server vorhanden.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Zentriert</string>
+ <string name="theme_free">Gratis</string>
+ <string name="theme_all">Alle</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Galerie</string>
+ <string name="post_format_image">Bild</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Zitat</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Informationen zu WordPress.com-Kursen und -Veranstaltungen (online und vor Ort)</string>
+ <string name="post_format_aside">Kurzmitteilung</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Gelegenheiten, an Marktforschung und Befragungen von WordPress.com teilzunehmen</string>
+ <string name="notif_tips">Tipps für die optimale Nutzung von WordPress.com</string>
+ <string name="notif_community">Community</string>
+ <string name="replies_to_my_comments">Antworten auf meine Kommentare</string>
+ <string name="notif_suggestions">Vorschläge</string>
+ <string name="notif_research">Marktforschung</string>
+ <string name="site_achievements">Website-Auszeichnungen</string>
+ <string name="username_mentions">Benutzernamen-Erwähnungen</string>
+ <string name="likes_on_my_posts">Likes für meine Beiträge</string>
+ <string name="site_follows">Website-Follows</string>
+ <string name="likes_on_my_comments">Likes für meine Kommentare</string>
+ <string name="comments_on_my_site">Kommentare auf meiner Website</string>
+ <string name="site_settings_list_editor_summary_other">%d Einträge</string>
+ <string name="site_settings_list_editor_summary_one">1 Eintrag</string>
+ <string name="approve_auto_if_previously_approved">Kommentare bekannter Benutzer</string>
+ <string name="approve_auto">Alle Benutzer</string>
+ <string name="approve_manual">Keine Kommentare</string>
+ <string name="site_settings_paging_summary_other">%d Kommentare pro Seite</string>
+ <string name="site_settings_paging_summary_one">1 Kommentar pro Seite</string>
+ <string name="site_settings_multiple_links_summary_other">Mehr als %d Links müssen genehmigt werden.</string>
+ <string name="site_settings_multiple_links_summary_one">Mehr als 1 Link muss genehmigt werden.</string>
+ <string name="site_settings_multiple_links_summary_zero">Mehr als 0 Links müssen genehmigt werden.</string>
+ <string name="detail_approve_auto">Alle Kommentare automatisch genehmigen</string>
+ <string name="detail_approve_auto_if_previously_approved">Automatisch genehmigen, wenn bereits andere Kommentare des Benutzers genehmigt wurden</string>
+ <string name="detail_approve_manual">Alle Kommentare müssen manuell genehmigt werden.</string>
+ <string name="filter_trashed_posts">Im Papierkorb</string>
+ <string name="days_quantity_one">1 Tag</string>
+ <string name="days_quantity_other">%d Tage</string>
+ <string name="filter_published_posts">Veröffentlicht</string>
+ <string name="filter_draft_posts">Entwürfe</string>
+ <string name="filter_scheduled_posts">Geplant</string>
+ <string name="pending_email_change_snackbar">Klicke auf den Bestätigungslink in der E-Mail, die an %1$s gesendet wurde, um deine neue Adresse zu bestätigen.</string>
+ <string name="primary_site">Primäre Website</string>
+ <string name="web_address">Web-Adresse</string>
+ <string name="editor_toast_uploading_please_wait">Medien werden hochgeladen … Bitte warte, bis der Vorgang abgeschlossen ist.</string>
+ <string name="error_refresh_comments_showing_older">Die Kommentare konnten nicht aktualisiert werden. Es werden ältere Kommentare angezeigt.</string>
+ <string name="editor_post_settings_set_featured_image">Beitragsbild festlegen</string>
+ <string name="editor_post_settings_featured_image">Beitragsbild</string>
+ <string name="new_editor_promo_desc">Die WordPress-App für Android verfügt nun über einen tollen neuen visuellen\n Editor. Probier ihn aus, indem du einen neuen Beitrag erstellst.</string>
+ <string name="new_editor_promo_title">Komplett neuer Editor</string>
+ <string name="new_editor_promo_button_label">Super, danke!</string>
+ <string name="visual_editor_enabled">Visueller Editor aktiviert</string>
+ <string name="editor_content_placeholder">Teile deine Geschichte hier …</string>
+ <string name="editor_page_title_placeholder">Seitentitel</string>
+ <string name="editor_post_title_placeholder">Beitragstitel</string>
+ <string name="email_address">E-Mail-Adresse</string>
+ <string name="preference_show_visual_editor">Visuellen Editor anzeigen</string>
+ <string name="dlg_sure_to_delete_comments">Möchtest du diese Kommentare dauerhaft löschen?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Möchtest du diesen Kommentar dauerhaft löschen?</string>
+ <string name="mnu_comment_delete_permanently">Löschen</string>
+ <string name="comment_deleted_permanently">Kommentar gelöscht</string>
+ <string name="mnu_comment_untrash">Wiederherstellen</string>
+ <string name="comments_empty_list_filtered_spam">Keine Spam-Kommentare</string>
+ <string name="could_not_load_page">Seite konnte nicht geladen werden.</string>
+ <string name="comment_status_all">Alle</string>
+ <string name="interface_language">Sprache der Bedienoberfläche</string>
+ <string name="off">Aus</string>
+ <string name="about_the_app">Über die App</string>
+ <string name="error_post_account_settings">Deine Kontoeinstellungen konnten nicht gespeichert werden.</string>
+ <string name="error_post_my_profile">Konnte dein Profil nicht speichern</string>
+ <string name="error_fetch_account_settings">Deine Kontoeinstellungen konnten nicht abgerufen werden.</string>
+ <string name="error_fetch_my_profile">Dein Profil konnte nicht abgerufen werden.</string>
+ <string name="stats_widget_promo_ok_btn_label">Okay, habe verstanden</string>
+ <string name="stats_widget_promo_desc">Füge das Widget zu deinem Startbildschirm hinzu, um auf deine Statistiken mit einem Klick zugreifen zu können.</string>
+ <string name="stats_widget_promo_title">Statistik-Widget für den Startbildschirm</string>
+ <string name="site_settings_unknown_language_code_error">Sprachcode nicht erkannt</string>
+ <string name="site_settings_threading_dialog_description">Zulassen, dass Kommentare in Threads verschachtelt werden.</string>
+ <string name="site_settings_threading_dialog_header">Verschachteln bis zu</string>
+ <string name="remove">Entfernen</string>
+ <string name="search">Suche</string>
+ <string name="add_category">Kategorie hinzufügen</string>
+ <string name="disabled">Deaktiviert</string>
+ <string name="site_settings_image_original_size">Originalgröße</string>
+ <string name="privacy_private">Deine Website ist nur für dich und freigegebene Nutzer sichtbar.</string>
+ <string name="privacy_public_not_indexed">Deine Website ist für alle sichtbar. Suchmaschinen werden aufgefordert, sie nicht zu indexieren.</string>
+ <string name="privacy_public">Deine Website ist für alle sichtbar. Suchmaschinen dürfen sie indexieren.</string>
+ <string name="about_me_hint">Ein paar Infos über dich ...</string>
+ <string name="public_display_name_hint">Wenn du keinen Anzeigenamen festlegst, wird standardmäßig dein Benutzername verwendet.</string>
+ <string name="about_me">Über mich</string>
+ <string name="public_display_name">Öffentlich angezeigter Name</string>
+ <string name="my_profile">Mein Profil</string>
+ <string name="first_name">Vorname</string>
+ <string name="last_name">Nachname</string>
+ <string name="site_privacy_public_desc">Suchmaschinen erlauben, diese Website zu indexieren</string>
+ <string name="site_privacy_hidden_desc">Suchmaschinen daran hindern, diese Website zu indexieren</string>
+ <string name="site_privacy_private_desc">Meine Website soll privat und nur für von mir ausgewählte Benutzer sichtbar sein.</string>
+ <string name="cd_related_post_preview_image">Vorschaubild für ähnlichen Beitrag</string>
+ <string name="error_post_remote_site_settings">Website-Info konnte nicht gespeichert werden</string>
+ <string name="error_fetch_remote_site_settings">Website-Info konnte nicht abgerufen werden</string>
+ <string name="error_media_upload_connection">Beim Hochladen von Medien ist ein Verbindungsfehler aufgetreten.</string>
+ <string name="site_settings_disconnected_toast">Die Verbindung wurde getrennt, die Bearbeitung ist deaktiviert.</string>
+ <string name="site_settings_unsupported_version_error">Nicht unterstützte WordPress-Version</string>
+ <string name="site_settings_multiple_links_dialog_description">Kommentare mit mehr Links als hier angegeben müssen genehmigt werden.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatisch schließen</string>
+ <string name="site_settings_close_after_dialog_description">Kommentare zu Artikeln automatisch schließen.</string>
+ <string name="site_settings_paging_dialog_description">Kommentar-Threads auf mehrere Seiten aufteilen.</string>
+ <string name="site_settings_paging_dialog_header">Kommentare pro Seite</string>
+ <string name="site_settings_close_after_dialog_title">Kommentarbereich schließen</string>
+ <string name="site_settings_blacklist_description">Wenn ein Kommentar eines dieser Wörter im Inhalt, Namen, in der URL, E-Mail-Adresse oder IP-Adresse enthält, wird er als Spam gekennzeichnet. Du kannst auch Teilbegriffe eingeben. So wird beispielsweise bei Eingabe von „press“ als Ergebnis „WordPress“ angezeigt.</string>
+ <string name="site_settings_hold_for_moderation_description">Wenn ein Kommentar eines dieser Wörter im Inhalt, Namen, in der URL, E-Mail-Adresse oder IP-Adresse enthält, wird er in die Warteschlange zur Freischaltung verschoben. Du kannst auch Teilbegriffe eingeben. So wird beispielsweise bei Eingabe von „press“ als Ergebnis „WordPress“ angezeigt.</string>
+ <string name="site_settings_list_editor_input_hint">Gib ein Wort oder einen Begriff ein.</string>
+ <string name="site_settings_list_editor_no_items_text">Keine Einträge</string>
+ <string name="site_settings_learn_more_caption">Für individuelle Beiträge kannst du die Einstellungen auch ausschalten.</string>
+ <string name="site_settings_rp_preview3_site">unter „Upgrade“</string>
+ <string name="site_settings_rp_preview3_title">Schwerpunkt des Upgrades: VideoPress für Hochzeiten</string>
+ <string name="site_settings_rp_preview2_site">unter „Apps“</string>
+ <string name="site_settings_rp_preview2_title">Die WordPress-App für Android wurde umfassend überarbeitet.</string>
+ <string name="site_settings_rp_preview1_site">unter „Mobil“</string>
+ <string name="site_settings_rp_preview1_title">Umfangreiches iPhone/iPad-Update jetzt verfügbar</string>
+ <string name="site_settings_rp_show_images_title">Bilder anzeigen</string>
+ <string name="site_settings_rp_show_header_title">Header anzeigen</string>
+ <string name="site_settings_rp_switch_summary">Als „Ähnliche Beiträge“ werden relevante Inhalte von deiner Website unter deinen Beiträgen angezeigt.</string>
+ <string name="site_settings_rp_switch_title">„Ähnliche Beiträge“ anzeigen</string>
+ <string name="site_settings_delete_site_hint">Entfernt deine Website-Daten von der App</string>
+ <string name="site_settings_blacklist_hint">Kommentare, die einem Filterkriterium entsprechen, werden als Spam gekennzeichnet.</string>
+ <string name="site_settings_moderation_hold_hint">Kommentare, die einem Filterkriterium entsprechen, werden in die Warteschlange zur Freischaltung verschoben.</string>
+ <string name="site_settings_multiple_links_hint">Bei bekannten Benutzern wird die Beschränkung der Linkanzahl ignoriert.</string>
+ <string name="site_settings_whitelist_hint">Der Autor muss bereits einen genehmigten Kommentar geschrieben haben.</string>
+ <string name="site_settings_user_account_required_hint">Benutzer müssen zum Kommentieren registriert und angemeldet sein.</string>
+ <string name="site_settings_identity_required_hint">Benutzer müssen zum Kommentieren Name und E-Mail-Adresse angeben.</string>
+ <string name="site_settings_manual_approval_hint">Kommentare müssen manuell genehmigt werden.</string>
+ <string name="site_settings_paging_hint">Kommentare in Blöcken mit einer festgelegten Größe anzeigen</string>
+ <string name="site_settings_threading_hint">Verschachtelte Kommentare bis zu einer bestimmten Anzahl zulassen</string>
+ <string name="site_settings_sort_by_hint">Legt fest, in welcher Reihenfolge die Kommentare angezeigt werden</string>
+ <string name="site_settings_close_after_hint">Nach dem angegebenen Zeitpunkt sind keine Kommentare mehr zulässig.</string>
+ <string name="site_settings_receive_pingbacks_hint">Link-Benachrichtigungen von anderen Blogs zulassen</string>
+ <string name="site_settings_send_pingbacks_hint">Versuchen, alle in dem Artikel verlinkten Blogs zu benachrichtigen</string>
+ <string name="site_settings_allow_comments_hint">Lesern erlauben, Kommentare zu veröffentlichen</string>
+ <string name="site_settings_discussion_hint">Diskussionseinstellungen für deine Websites anzeigen und ändern</string>
+ <string name="site_settings_more_hint">Alle verfügbaren Diskussionseinstellungen anzeigen</string>
+ <string name="site_settings_related_posts_hint">Ähnliche Beiträge im Reader anzeigen oder ausblenden</string>
+ <string name="site_settings_upload_and_link_image_hint">Aktivieren, damit Bilder immer in voller Größe hochgeladen werden</string>
+ <string name="site_settings_image_width_hint">Größe von Bildern in Beiträgen auf diese Breite ändern</string>
+ <string name="site_settings_format_hint">Legt ein neues Beitragsformat fest</string>
+ <string name="site_settings_category_hint">Legt eine neue Beitragskategorie fest</string>
+ <string name="site_settings_location_hint">Zu deinen Beiträgen automatisch Ortsangaben hinzufügen</string>
+ <string name="site_settings_password_hint">Ändere dein Passwort</string>
+ <string name="site_settings_username_hint">Aktuelles Benutzerkonto</string>
+ <string name="site_settings_language_hint">Sprache, in der dieses Blog vornehmlich geschrieben wird.</string>
+ <string name="site_settings_privacy_hint">Steuert, wer deine Website sehen kann</string>
+ <string name="site_settings_address_hint">Adressänderungen werden aktuell nicht unterstützt.</string>
+ <string name="site_settings_tagline_hint">Eine kurze Beschreibung oder ein treffender Spruch, um dein Blog zu beschreiben</string>
+ <string name="site_settings_title_hint">Erkläre in ein paar Worten, worum es auf deiner Website geht.</string>
+ <string name="site_settings_whitelist_known_summary">Kommentare von bekannten Benutzern</string>
+ <string name="site_settings_whitelist_all_summary">Kommentare von allen Benutzern</string>
+ <string name="site_settings_threading_summary">%d Ebenen</string>
+ <string name="site_settings_privacy_private_summary">Privat</string>
+ <string name="site_settings_privacy_hidden_summary">Versteckt</string>
+ <string name="site_settings_delete_site_title">Website löschen</string>
+ <string name="site_settings_privacy_public_summary">Öffentlich</string>
+ <string name="site_settings_blacklist_title">Sperrliste</string>
+ <string name="site_settings_moderation_hold_title">Zur Freischaltung überprüfen</string>
+ <string name="site_settings_multiple_links_title">Links in Kommentaren</string>
+ <string name="site_settings_whitelist_title">Automatisch genehmigen</string>
+ <string name="site_settings_threading_title">Verschachtelung</string>
+ <string name="site_settings_paging_title">Seitennummerierung</string>
+ <string name="site_settings_sort_by_title">Sortieren nach</string>
+ <string name="site_settings_account_required_title">Benutzer müssen angemeldet sein.</string>
+ <string name="site_settings_identity_required_title">Name und E-Mail-Adresse erforderlich</string>
+ <string name="site_settings_receive_pingbacks_title">Pingbacks erhalten</string>
+ <string name="site_settings_send_pingbacks_title">Pingbacks senden</string>
+ <string name="site_settings_allow_comments_title">Kommentare zulassen</string>
+ <string name="site_settings_default_format_title">Standardformat</string>
+ <string name="site_settings_default_category_title">Standardkategorie</string>
+ <string name="site_settings_location_title">Ort aktivieren</string>
+ <string name="site_settings_address_title">Adresse</string>
+ <string name="site_settings_title_title">Website-Titel</string>
+ <string name="site_settings_tagline_title">Untertitel</string>
+ <string name="site_settings_this_device_header">Auf diesem Gerät</string>
+ <string name="site_settings_discussion_new_posts_header">Standardeinstellungen für neue Beiträge</string>
+ <string name="site_settings_account_header">Konto</string>
+ <string name="site_settings_writing_header">Schreiben</string>
+ <string name="newest_first">Neuester zuerst</string>
+ <string name="site_settings_general_header">Allgemein</string>
+ <string name="discussion">Diskussionen</string>
+ <string name="privacy">Datenschutz</string>
+ <string name="related_posts">Ähnliche Beiträge</string>
+ <string name="comments">Kommentare</string>
+ <string name="close_after">Schließen nach</string>
+ <string name="oldest_first">Ältester zuerst</string>
+ <string name="media_error_no_permission_upload">Du bist nicht berechtigt, Medien auf diese Website hochzuladen.</string>
+ <string name="never">Nie</string>
+ <string name="unknown">Unbekannt</string>
+ <string name="reader_err_get_post_not_found">Dieser Beitrag ist nicht mehr vorhanden</string>
+ <string name="reader_err_get_post_not_authorized">Du hast nicht die erforderlichen Rechte diesen Beitrag anzusehen</string>
+ <string name="reader_err_get_post_generic">Konnte den Beitrag nicht empfangen</string>
+ <string name="blog_name_no_spaced_allowed">Website-Adresse darf keine Leerzeichen enthalten</string>
+ <string name="invalid_username_no_spaces">Benutzername darf keine Leerzeichen enthalten</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Die Websites, denen du folgst, haben keine aktuellen Beiträge veröffentlicht</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Keine aktuellen Beiträge</string>
+ <string name="media_details_copy_url_toast">URL in Zwischenablage kopiert</string>
+ <string name="edit_media">Medien bearbeiten</string>
+ <string name="media_details_copy_url">Kopiere URL</string>
+ <string name="media_details_label_date_uploaded">Hochgeladen</string>
+ <string name="media_details_label_date_added">Hinzugefügt</string>
+ <string name="selected_theme">Gewähltes Theme</string>
+ <string name="could_not_load_theme">Konnte Theme nicht laden</string>
+ <string name="theme_activation_error">Etwas ist schief gelaufen. Konnte Theme nicht aktivieren</string>
+ <string name="theme_by_author_prompt_append"> von %1$s</string>
+ <string name="theme_prompt">Danke, dass du %1$s gewählt hast</string>
+ <string name="theme_try_and_customize">Ausprobieren &amp; Anpassen</string>
+ <string name="theme_view">Anzeigen</string>
+ <string name="theme_details">Details</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">FERTIG</string>
+ <string name="theme_manage_site">WEBSITE VERWALTEN</string>
+ <string name="title_activity_theme_support">Themes</string>
+ <string name="theme_activate">Aktivieren</string>
+ <string name="date_range_start_date">Startdatum</string>
+ <string name="date_range_end_date">Enddatum</string>
+ <string name="current_theme">Aktuelles Theme</string>
+ <string name="customize">Anpassen</string>
+ <string name="details">Details</string>
+ <string name="support">Support</string>
+ <string name="active">Aktiv</string>
+ <string name="stats_referrers_spam_generic_error">Etwas ist während dieses Vorgangs schief gelaufen. Der Spam-Status wurde nicht geändert.</string>
+ <string name="stats_referrers_marking_not_spam">Als kein Spam markieren</string>
+ <string name="stats_referrers_unspam">Kein Spam</string>
+ <string name="stats_referrers_marking_spam">Als Spam markieren</string>
+ <string name="theme_auth_error_authenticate">Konnte Themes nicht laden: Benutzer konnte nicht authentifiziert werden</string>
+ <string name="post_published">Beitrag veröffentlicht</string>
+ <string name="page_published">Seite veröffentlicht</string>
+ <string name="post_updated">Beitrag aktualisiert</string>
+ <string name="page_updated">Seite aktualisiert</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Leider keine Themes gefunden.</string>
+ <string name="media_file_name">Dateiname: %s</string>
+ <string name="media_uploaded_on">Hochgeladen am: %s</string>
+ <string name="media_dimensions">Größe: %s</string>
+ <string name="upload_queued">In Warteschlange</string>
+ <string name="media_file_type">Dateityp: %s</string>
+ <string name="reader_label_gap_marker">Mehr Beiträge laden</string>
+ <string name="notifications_no_search_results">Die Suche nach \'%s\' ergab keine Ergebnisse</string>
+ <string name="search_sites">Websites durchsuchen</string>
+ <string name="notifications_empty_view_reader">Reader anzeigen</string>
+ <string name="unread">Ungelesen</string>
+ <string name="notifications_empty_action_followers_likes">Mach auf dich aufmerksam: Kommentiere Beiträge, die du gelesen hast.</string>
+ <string name="notifications_empty_action_comments">Nimm an einer Diskussion teil: Kommentiere Beiträge von Blogs denen du folgst.</string>
+ <string name="notifications_empty_action_unread">Belebe eine Diskussion wieder: Schreibe einen neuen Beitrag.</string>
+ <string name="notifications_empty_action_all">Werde aktiv! Kommentiere Beiträge von Blogs denen du folgst.</string>
+ <string name="notifications_empty_likes">Noch keine neuen Likes zum Anzeigen.</string>
+ <string name="notifications_empty_followers">Noch keine neuen Follower anzuzeigen.</string>
+ <string name="notifications_empty_comments">Noch keine neuen Kommentare.</string>
+ <string name="notifications_empty_unread">Keine ungelesenen Nachrichten!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Bitte greife über die App auf die Statistik zu, und versuche das Widget später hinzuzufügen.</string>
+ <string name="stats_widget_error_readd_widget">Bitte entferne das Widget und füge es erneut hinzu</string>
+ <string name="stats_widget_error_no_visible_blog">Ohne sichtbare Blogs kann nicht auf die Statistik zugegriffen werden</string>
+ <string name="stats_widget_error_no_permissions">Dein WordPress.com-Konto kann auf diesem Blog nicht auf Statistiken zugreifen</string>
+ <string name="stats_widget_error_no_account">Bitte bei WordPress anmelden</string>
+ <string name="stats_widget_error_generic">Statistiken konnten nicht geladen werden</string>
+ <string name="stats_widget_loading_data">Lade Daten ...</string>
+ <string name="stats_widget_name_for_blog">Heutige Statistik für %1$s</string>
+ <string name="stats_widget_name">Heutige WordPress-Statistiken</string>
+ <string name="add_location_permission_required">Genehmigung benötigt, um einen Ort hinzuzufügen</string>
+ <string name="add_media_permission_required">Rechte zum Hinzufügen von Medien erforderlich</string>
+ <string name="access_media_permission_required">Rechte zum Zugreifen auf Medien erforderlich</string>
+ <string name="stats_enable_rest_api_in_jetpack">Um deine Statistik anzusehen, aktiviere das JSON API-Modul in Jetpack.</string>
+ <string name="error_open_list_from_notification">Diese(r) Seite / Beitrag wurde auf einer anderen Website veröffentlicht</string>
+ <string name="reader_short_comment_count_multi">%s Kommentare</string>
+ <string name="reader_short_comment_count_one">1 Kommentar</string>
+ <string name="reader_label_submit_comment">SENDEN</string>
+ <string name="reader_hint_comment_on_post">Antwort auf Beitrag…</string>
+ <string name="reader_discover_visit_blog">%s besuchen</string>
+ <string name="reader_discover_attribution_blog">Ursprünglich veröffentlicht auf %s</string>
+ <string name="reader_discover_attribution_author">Ursprünglich veröffentlicht von %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Ursprünglich veröffentlicht von %1$s auf %2$s</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_label_follow_count">%,d Follower</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_menu_tags">Schlagwörter und Blogs bearbeiten</string>
+ <string name="reader_title_post_detail">Reader-Beitrag</string>
+ <string name="local_draft_explainer">Dieser Beitrag ist ein lokaler Entwurf, der noch nicht veröffentlicht wurde</string>
+ <string name="local_changes_explainer">Dieser Beitrag enthält lokale Änderungen, die noch nicht veröffentlicht wurden</string>
+ <string name="notifications_push_summary">Einstellungen für Benachrichtigungen, die auf deinem Gerät angezeigt werden.</string>
+ <string name="notifications_email_summary">Einstellungen für Benachrichtigungen, die an die E-Mail-Adresse für dein Konto gesendet werden.</string>
+ <string name="notifications_tab_summary">Einstellungen für Benachrichtigungen, die auf der Registerkarte „Benachrichtigungen“ angezeigt werden.</string>
+ <string name="notifications_disabled">Anwendungsbenachrichtigungen wurden deaktiviert. Tippe hier, um sie unter „Einstellungen“ zu aktivieren.</string>
+ <string name="notification_types">Benachrichtigungstypen</string>
+ <string name="error_loading_notifications">Konnte Benachrichtungseinstellungen nicht laden</string>
+ <string name="replies_to_your_comments">Antworten auf deine Kommentare</string>
+ <string name="comment_likes">Kommentar-Likes</string>
+ <string name="app_notifications">App-Benachrichtigungen</string>
+ <string name="notifications_tab">Registerkarte „Benachrichtigungen“</string>
+ <string name="email">E-Mail</string>
+ <string name="notifications_comments_other_blogs">Kommentare auf anderen Websites</string>
+ <string name="notifications_wpcom_updates">WordPress.com-Aktualisierungen</string>
+ <string name="notifications_other">Andere</string>
+ <string name="notifications_account_emails">E-Mail von WordPress.com</string>
+ <string name="notifications_account_emails_summary">Wichtige E-Mails zu deinem Konto senden wir dir auf jeden Fall, aber du kannst auch ein paar nützliche Extras dazubekommen.</string>
+ <string name="notifications_sights_and_sounds">Licht und Ton</string>
+ <string name="your_sites">Deine Websites</string>
+ <string name="stats_insights_latest_post_trend">Die Veröffentlichung von %2$s ist %1$s her. Hier siehst du, wie es dem Beitrag bisher ergangen ist ...</string>
+ <string name="stats_insights_latest_post_summary">Übersicht des letzten Beitrags</string>
+ <string name="button_revert">Rückgängig</string>
+ <string name="days_ago">vor %d Tagen</string>
+ <string name="yesterday">Gestern</string>
+ <string name="connectionbar_no_connection">Keine Verbindung</string>
+ <string name="page_trashed">Seite in den Papierkorb verschoben</string>
+ <string name="post_deleted">Beitrag gelöscht</string>
+ <string name="post_trashed">Beitrag in den Papierkorb verschoben</string>
+ <string name="stats_no_activity_this_period">Keine Aktivitäten in diesem Zeitraum</string>
+ <string name="trashed">Im Papierkorb</string>
+ <string name="button_back">Zurück</string>
+ <string name="page_deleted">Seite gelöscht</string>
+ <string name="button_stats">Statistiken</string>
+ <string name="button_trash">In den Papierkorb</string>
+ <string name="button_preview">Vorschau</string>
+ <string name="button_view">Anzeigen</string>
+ <string name="button_edit">Bearbeiten</string>
+ <string name="button_publish">Veröffentlichen</string>
+ <string name="my_site_no_sites_view_subtitle">Möchtest du welche hinzufügen?</string>
+ <string name="my_site_no_sites_view_title">Du hast noch keine WordPress-Websites.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Du bist nicht berechtigt auf diesen Blog zuzugreifen</string>
+ <string name="reader_toast_err_follow_blog_not_found">Dieser Blog konnte nicht gefunden werden</string>
+ <string name="undo">Rückgängig</string>
+ <string name="tabbar_accessibility_label_my_site">Meine Website</string>
+ <string name="tabbar_accessibility_label_me">Ich</string>
+ <string name="passcodelock_prompt_message">Gib deine PIN ein</string>
+ <string name="editor_toast_changes_saved">Änderungen gespeichert</string>
+ <string name="push_auth_expired">Die Anfrage ist nicht mehr gültig. Melde dich bei WordPress.com an, um es erneut zu versuchen.</string>
+ <string name="stats_insights_best_ever">Bisher bestes Aufrufergebnis</string>
+ <string name="ignore">Ignorieren</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% der Aufrufe</string>
+ <string name="stats_insights_most_popular_hour">Beliebteste Uhrzeit</string>
+ <string name="stats_insights_most_popular_day">Beliebtester Tag</string>
+ <string name="stats_insights_popular">Beliebtester Tag und Uhrzeit</string>
+ <string name="stats_insights_today">Heutige Statistiken</string>
+ <string name="stats_insights_all_time">Beiträge, Aufrufe und Besucher für den gesamten Zeitraum</string>
+ <string name="stats_insights">Einsichten</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Um deine Statistiken anzusehen, melde dich bei deinem WordPress.com-Konto an, das du für die Jetpack Verbindung verwendest.</string>
+ <string name="stats_other_recent_stats_moved_label">Suchst du nach deinen anderen, aktuellen Statistiken? Die findest du jetzt auf der Seite Einsichten.</string>
+ <string name="me_disconnect_from_wordpress_com">Verbindung zu WordPress.com trennen</string>
+ <string name="me_connect_to_wordpress_com">Mit WordPress.com verbinden</string>
+ <string name="me_btn_login_logout">Anmelden/Abmelden</string>
+ <string name="account_settings">Kontoeinstellungen</string>
+ <string name="me_btn_support">Hilfe &amp; Support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" wurde nicht ausgeblendet, denn es ist die aktuelle Webseite</string>
+ <string name="site_picker_create_dotcom">WordPress.com Website erstellen</string>
+ <string name="site_picker_add_site">Website hinzufügen</string>
+ <string name="site_picker_add_self_hosted">Selbstgehostete Website hinzufügen</string>
+ <string name="site_picker_edit_visibility">Websites anzeigen/ausblenden</string>
+ <string name="my_site_btn_view_admin">Admin aufrufen</string>
+ <string name="my_site_btn_view_site">Website ansehen</string>
+ <string name="site_picker_title">Website auswählen</string>
+ <string name="my_site_btn_switch_site">Website wechseln</string>
+ <string name="my_site_btn_blog_posts">Blogbeiträge</string>
+ <string name="my_site_btn_site_settings">Einstellungen</string>
+ <string name="my_site_header_look_and_feel">Erscheinungsbild</string>
+ <string name="my_site_header_publish">Veröffentlichen</string>
+ <string name="my_site_header_configuration">Einrichtung</string>
+ <string name="reader_label_new_posts_subtitle">Berühren zum Anzeigen</string>
+ <string name="notifications_account_required">Für Benachrichtigungen bei WordPress.com anmelden</string>
+ <string name="stats_unknown_author">Unbekannter Autor</string>
+ <string name="image_added">Bild hinzugefügt</string>
+ <string name="signout">Trennen</string>
+ <string name="deselect_all">Alle abwählen</string>
+ <string name="show">Anzeigen</string>
+ <string name="hide">Ausblenden</string>
+ <string name="select_all">Alle auswählen</string>
+ <string name="sign_out_wpcom_confirm">Das Trennen deines Benutzerkontos wird alle WordPress.com Daten von @%s von diesem Gerät entfernen, einschließlich aller lokal gespeicherten Entwürfe und Änderungen.</string>
+ <string name="select_from_new_picker">Mehrfach-Auswahl mit dem neuen Auswahlwerkzeug</string>
+ <string name="stats_generic_error">Erforderliche Statistiken konnten nicht geladen werden</string>
+ <string name="no_device_videos">Keine Videos</string>
+ <string name="no_blog_images">Keine Bilder</string>
+ <string name="no_blog_videos">Keine Videos</string>
+ <string name="no_device_images">Keine Bilder</string>
+ <string name="error_loading_blog_images">Konnte Bilder nicht abrufen</string>
+ <string name="error_loading_blog_videos">Konnte Videos nicht abrufen</string>
+ <string name="error_loading_images">Fehler beim Laden der Bilder</string>
+ <string name="error_loading_videos">Fehler beim Laden der Videos</string>
+ <string name="loading_blog_images">Bilder werden abgerufen</string>
+ <string name="loading_blog_videos">Videos werden abgerufen</string>
+ <string name="no_media_sources">Konnte Medien nicht abrufen</string>
+ <string name="loading_videos">Lade Videos</string>
+ <string name="loading_images">Lade Bilder</string>
+ <string name="no_media">Keine Medien</string>
+ <string name="device">Gerät</string>
+ <string name="language">Sprache</string>
+ <string name="add_to_post">Zum Beitrag hinzufügen</string>
+ <string name="media_picker_title">Medien auswählen</string>
+ <string name="take_photo">Schieße ein Foto</string>
+ <string name="take_video">Mache ein Video</string>
+ <string name="tab_title_device_images">Bilder vom Gerät</string>
+ <string name="tab_title_device_videos">Videos vom Gerät</string>
+ <string name="tab_title_site_images">Website-Bilder</string>
+ <string name="tab_title_site_videos">Website-Videos</string>
+ <string name="media_details_label_file_name">Dateiname</string>
+ <string name="media_details_label_file_type">Dateityp</string>
+ <string name="error_publish_no_network">Kann ohne Verbindung nicht veröffentlichen. Als Entwurf gespeichert.</string>
+ <string name="editor_toast_invalid_path">Ungültiger Dateipfad</string>
+ <string name="verification_code">Überprüfungscode</string>
+ <string name="invalid_verification_code">Ungültiger Überprüfungscode</string>
+ <string name="verify">Überprüfen</string>
+ <string name="two_step_footer_label">Gib den Code aus der Authenticator-App ein.</string>
+ <string name="two_step_footer_button">Sende Code via Textnachricht</string>
+ <string name="two_step_sms_sent">Der Überprüfungscode kommt per Textnachricht.</string>
+ <string name="sign_in_jetpack">Melde dich in deinem WordPress.com-Konto an, um dich mit Jetpack zu verbinden.</string>
+ <string name="auth_required">Nochmal anmelden, um fortzufahren.</string>
+ <string name="reader_empty_posts_request_failed">Beiträge können nicht abgerufen werden.</string>
+ <string name="publisher">Herausgeber:</string>
+ <string name="error_notification_open">Benachrichtigung konnte nicht geöffnet werden.</string>
+ <string name="stats_followers_total_email_paged">Es werden %1$d - %2$d von %3$s E-Mail-Followern angezeigt.</string>
+ <string name="stats_search_terms_unknown_search_terms">Unbekannte Suchbegriffe</string>
+ <string name="stats_followers_total_wpcom_paged">Es werden %1$d - %2$d von %3$s WordPress.com Followern angezeigt.</string>
+ <string name="stats_empty_search_terms_desc">Erfahre mehr über deinen Suchverkehr, indem du dir die Begriffe ansiehst, nach denen deine Besucher gesucht haben, um deine Website zu finden.</string>
+ <string name="stats_empty_search_terms">Es wurden keine Suchbegriffe verzeichnet.</string>
+ <string name="stats_entry_search_terms">Suchbegriff</string>
+ <string name="stats_view_authors">Autoren</string>
+ <string name="stats_view_search_terms">Suchbegriffe</string>
+ <string name="comments_fetching">Kommentare werden abgerufen…</string>
+ <string name="pages_fetching">Seiten werden abgerufen…</string>
+ <string name="toast_err_post_uploading">Der Beitrag kann während dem Hochladen nicht geöffnet werden.</string>
+ <string name="posts_fetching">Beiträge werden abgerufen…</string>
+ <string name="media_fetching">Medien werden abgerufen…</string>
+ <string name="post_uploading">Hochladen</string>
+ <string name="stats_total">Insgesamt</string>
+ <string name="stats_overall">Gesamt</string>
+ <string name="stats_period">Zeitraum</string>
+ <string name="logs_copied_to_clipboard">Anwendungsprotokolle wurden in die Zwischenablage kopiert</string>
+ <string name="reader_label_new_posts">Neue Beiträge</string>
+ <string name="reader_empty_posts_in_blog">Dieser Blog ist leer</string>
+ <string name="stats_average_per_day">Durchschnitt pro Tag</string>
+ <string name="stats_recent_weeks">Letzte Wochen</string>
+ <string name="error_copy_to_clipboard">Beim Kopieren von Text in die Zwischenablage ist ein Fehler aufgetreten</string>
+ <string name="reader_page_recommended_blogs">Websites, die dir gefallen könnten</string>
+ <string name="stats_months_and_years">Monate und Jahre</string>
+ <string name="themes_fetching">Themes werden abgerufen…</string>
+ <string name="stats_for">Statistiken für %s</string>
+ <string name="stats_other_recent_stats_label">Weitere aktuelle Statistiken</string>
+ <string name="stats_view_all">Alle anzeigen</string>
+ <string name="stats_view">Anzeigen</string>
+ <string name="stats_followers_months">%1$d Monate</string>
+ <string name="stats_followers_a_year">Ein Jahr</string>
+ <string name="stats_followers_years">%1$d Jahre</string>
+ <string name="stats_followers_a_month">Ein Monat</string>
+ <string name="stats_followers_minutes">%1$d Minuten</string>
+ <string name="stats_followers_an_hour_ago">Vor einer Stunde</string>
+ <string name="stats_followers_hours">%1$d Stunden</string>
+ <string name="stats_followers_a_day">Ein Tag</string>
+ <string name="stats_followers_days">%1$d Tage</string>
+ <string name="stats_followers_a_minute_ago">Vor einer Minute</string>
+ <string name="stats_followers_seconds_ago">Vor Sekunden</string>
+ <string name="stats_followers_total_email">E-Mail-Follower insgesamt: %1$s</string>
+ <string name="stats_followers_wpcom_selector">wordpress.com</string>
+ <string name="stats_followers_email_selector">E-Mail</string>
+ <string name="stats_followers_total_wpcom">WordPress.com Follower insgesamt: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Summe der Beiträge mit Kommentar-Followern: %1$s</string>
+ <string name="stats_comments_by_authors">Nach Autoren</string>
+ <string name="stats_comments_by_posts_and_pages">Nach Beiträgen und Seiten</string>
+ <string name="stats_empty_followers_desc">Informiere dich über die Gesamtzahl deiner Follower und die Zeit, seit der sie dir folgen.</string>
+ <string name="stats_empty_followers">Keine Follower</string>
+ <string name="stats_empty_publicize_desc">Informiere dich mit Publicize über deine Follower aus verschiedenen sozialen Netzwerken.</string>
+ <string name="stats_empty_publicize">Keine Follower in Publicize verzeichnet</string>
+ <string name="stats_empty_video">Keine Videos abgespielt</string>
+ <string name="stats_empty_video_desc">Wenn du Videos mit VideoPress hochgeladen hast, kannst du herausfinden, wie oft sie angesehen wurden.</string>
+ <string name="stats_empty_comments_desc">Wenn du Kommentare auf deiner Website zulässt, kannst du anhand der letzten 1.000 Kommentare herausfinden, welche Personen die meisten Kommentare abgegeben und welche Inhalte die lebhaftesten Diskussionen angestoßen haben.</string>
+ <string name="stats_empty_tags_and_categories_desc">Verschaffe dir anhand deiner Top-Beiträge aus der letzten Woche einen Überblick über die beliebtesten Themen auf deiner Website.</string>
+ <string name="stats_empty_top_authors_desc">Informiere dich über die Aufrufe der Beiträge von einzelnen Mitarbeitern und rufe Details zu den beliebtesten Inhalten der einzelnen Autoren ab.</string>
+ <string name="stats_empty_tags_and_categories">Keine verschlagworteten Beiträge oder Seiten aufgerufen</string>
+ <string name="stats_empty_clicks_desc">Wenn du in deinen Inhalten auf andere Websites verlinkst, kannst du sehen, auf welche Links deine Besucher am häufigsten klicken.</string>
+ <string name="stats_empty_referrers_desc">Informiere dich eingehender über die Sichtbarkeit deiner Website. Dies kannst du an den Websites und Suchmaschinen ablesen, von denen aus die meisten Besucher zu dir finden.</string>
+ <string name="stats_empty_clicks_title">Keine Klicks verzeichnet</string>
+ <string name="stats_empty_referrers_title">Keine Referrer verzeichnet</string>
+ <string name="stats_empty_top_posts_title">Keine Beiträge oder Seiten aufgerufen</string>
+ <string name="stats_empty_top_posts_desc">Informiere dich darüber, welche deiner Inhalte besonders häufig aufgerufen werden, und prüfe, wie sich das Interesse an einzelnen Beiträgen und Seiten im Lauf der Zeit entwickelt.</string>
+ <string name="stats_totals_followers">Seit</string>
+ <string name="stats_empty_geoviews">Keine Länder verzeichnet</string>
+ <string name="stats_empty_geoviews_desc">Blättere in der Liste und informiere dich darüber, aus welchen Ländern und Regionen die meisten Besucher zu deiner Website finden.</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_entry_publicize">Dienst</string>
+ <string name="stats_entry_followers">Follower</string>
+ <string name="stats_totals_publicize">Follower</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_view_top_posts_and_pages">Beiträge &amp; Seiten</string>
+ <string name="stats_view_videos">Videos</string>
+ <string name="stats_view_publicize">Publizieren</string>
+ <string name="stats_view_followers">Follower</string>
+ <string name="stats_view_countries">Länder</string>
+ <string name="stats_likes">Gefällt mir</string>
+ <string name="stats_pagination_label">Seite %1$s von %2$s</string>
+ <string name="stats_timeframe_years">Jahre</string>
+ <string name="stats_views">Aufrufe</string>
+ <string name="stats_visitors">Besucher</string>
+ <string name="ssl_certificate_details">Details</string>
+ <string name="delete_sure_post">Lösche diesen Beitrag</string>
+ <string name="delete_sure">Lösche diesen Entwurf</string>
+ <string name="delete_sure_page">Lösche diese Seite</string>
+ <string name="confirm_delete_multi_media">Ausgewählte Objekte löschen?</string>
+ <string name="confirm_delete_media">Ausgewähltes Objekt löschen?</string>
+ <string name="cab_selected">%d ausgewählt</string>
+ <string name="media_gallery_date_range">Zeige Medien von %1$s bis %2$s</string>
+ <string name="sure_to_remove_account">Diese Website entfernen?</string>
+ <string name="reader_empty_followed_blogs_title">Du folgst bisher noch keiner Website</string>
+ <string name="reader_empty_posts_liked">Du hast bisher noch keine Beiträge mit „gefällt mir“ markiert.</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Die FAQ durchsuchen</string>
+ <string name="nux_help_description">Besuche das Helpcenter um Antworten auf die häufig gestellten Fragen zu erhalten oder besuche die Foren um eine neue Frage zu stellen</string>
+ <string name="agree_terms_of_service">Beim Erstellen eines Kontos akzeptierst du die faszinierenden %1$sNutzungsbedingungen%2$s</string>
+ <string name="create_new_blog_wpcom">Erstelle ein WordPress.com Blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com Blog erstellt!</string>
+ <string name="reader_empty_comments">Keine Kommentare bisher</string>
+ <string name="reader_empty_posts_in_tag">Keine Beiträge mit diesem Tag.</string>
+ <string name="reader_label_comment_count_multi">%,d Kommentare</string>
+ <string name="reader_label_view_original">Zeige Original-Beitrag</string>
+ <string name="reader_label_like">Gefällt mir</string>
+ <string name="reader_label_liked_by">Gefällt</string>
+ <string name="reader_label_comment_count_single">Ein Kommentar</string>
+ <string name="reader_label_comments_closed">Kommentare sind geschlossen</string>
+ <string name="reader_label_comments_on">Kommentare zu</string>
+ <string name="reader_title_photo_viewer">%1$d von %2$d</string>
+ <string name="error_publish_empty_post">Leerer Beitrag kann nicht veröffentlicht werden</string>
+ <string name="error_refresh_unauthorized_posts">Du hast keine Berechtigung Beiträge anzusehen oder zu bearbeiten</string>
+ <string name="error_refresh_unauthorized_pages">Du hast keine Berechtigung Seiten anzusehen oder zu bearbeiten</string>
+ <string name="error_refresh_unauthorized_comments">Du hast keine Berechtigung Kommentare anzusehen oder zu bearbeiten</string>
+ <string name="older_month">Älter als ein Monat</string>
+ <string name="more">Mehr</string>
+ <string name="older_two_days">Älter als zwei Tage</string>
+ <string name="older_last_week">Älter als eine Woche</string>
+ <string name="stats_no_blog">Statistiken konnten nicht geladen werden für den angefragten Blog</string>
+ <string name="select_a_blog">Wähle eine WordPress Site</string>
+ <string name="sending_content">Lade %s Inhalt hoch</string>
+ <string name="uploading_total">Hochladen %1$d von %2$d</string>
+ <string name="mnu_comment_liked">Gefällt</string>
+ <string name="comment">Kommentar</string>
+ <string name="comment_trashed">Kommentar im Papierkorb</string>
+ <string name="posts_empty_list">Keine Beiträge bisher. Warum nicht jetzt einen erstellen?</string>
+ <string name="comment_reply_to_user">Antwort zu %s</string>
+ <string name="pages_empty_list">Keine Seiten bisher. Warum nicht jetzt Eine erstellen?</string>
+ <string name="media_empty_list_custom_date">Keine Medien in diesem Zeitabschnitt</string>
+ <string name="posting_post">Veröffentliche "%s"</string>
+ <string name="signing_out">Abmelden…</string>
+ <string name="reader_toast_err_generic">Konnte diese Aktion nicht ausführen</string>
+ <string name="reader_toast_err_block_blog">Konnte diesen Blog nicht blockieren</string>
+ <string name="reader_toast_blog_blocked">Beiträge von diesem Blog werden nicht länger angezeigt</string>
+ <string name="reader_menu_block_blog">Blockiere diesen Blog</string>
+ <string name="contact_us">Kontaktiere uns</string>
+ <string name="hs__conversation_detail_error">Beschreibe das Problem, das du siehst</string>
+ <string name="hs__new_conversation_header">Support-Chat</string>
+ <string name="hs__conversation_header">Support-Chat</string>
+ <string name="hs__username_blank_error">Gib einen gültigen Namen ein</string>
+ <string name="hs__invalid_email_error">Gib eine gültige E-Mail-Adresse ein</string>
+ <string name="add_location">Standort hinzufügen</string>
+ <string name="current_location">Aktueller Standort</string>
+ <string name="search_location">Suchen</string>
+ <string name="edit_location">Bearbeiten</string>
+ <string name="search_current_location">Lokalisieren</string>
+ <string name="preference_send_usage_stats">Statistiken senden</string>
+ <string name="preference_send_usage_stats_summary">Nutzungsstatistiken automatisch versenden, damit wir WordPress für Android weiter verbessern können</string>
+ <string name="update_verb">Update</string>
+ <string name="schedule_verb">Zeitplan</string>
+ <string name="reader_title_blog_preview">Reader-Blog</string>
+ <string name="reader_title_tag_preview">Reader-Schlagwort</string>
+ <string name="reader_title_subs">Tags &amp; Blogs</string>
+ <string name="reader_page_followed_tags">Abonnierte Tags</string>
+ <string name="reader_page_followed_blogs">Abonnierte Websites</string>
+ <string name="reader_hint_add_tag_or_url">Gib eine URL oder einen Tag zum Folgen ein</string>
+ <string name="reader_label_followed_blog">Blog wird verfolgt</string>
+ <string name="reader_label_tag_preview">Beiträge getaggt mit %s</string>
+ <string name="reader_toast_err_get_blog_info">Dieser Blog kann nicht angezeigt werden.</string>
+ <string name="reader_toast_err_already_follow_blog">Du folgst diesem Blog bereits</string>
+ <string name="reader_toast_err_follow_blog">Blog konnte nicht gefolgt werden </string>
+ <string name="reader_toast_err_unfollow_blog">Blog konnte nicht entfolgt werden</string>
+ <string name="reader_empty_recommended_blogs">Keine empfohlenen Blogs</string>
+ <string name="saving">Wird gespeichert…</string>
+ <string name="media_empty_list">Keine Medien</string>
+ <string name="ptr_tip_message">Tipp: Nach unten ziehen, um die Seite neu zu laden.</string>
+ <string name="help">Hilfe</string>
+ <string name="forgot_password">Passwort vergessen?</string>
+ <string name="forums">Forums</string>
+ <string name="help_center">Hilfecenter</string>
+ <string name="ssl_certificate_error">Ungültiges SSL Zertifikat</string>
+ <string name="ssl_certificate_ask_trust">Wenn du dich üblicherweise ohne Probleme mit dieser Website verbinden kannst, könnte dieser Fehler bedeute, dass sich jemand als diese Website ausgibt und du daher nicht weitermachen solltest. Möchtest du dem Zertifikat trotzdem vertrauen?</string>
+ <string name="out_of_memory">Speicherplatz erschöpft</string>
+ <string name="no_network_message">Es ist kein Netzwerk verfügbar</string>
+ <string name="could_not_remove_account">Website konnte nicht entfernt werden</string>
+ <string name="gallery_error">Das Medien-Objekt konnte nicht abgerufen werden</string>
+ <string name="blog_not_found">Beim Aufrufen des Blogs ist ein Fehler aufgetreten</string>
+ <string name="wait_until_upload_completes">Warte bis der Upload fertig ist</string>
+ <string name="theme_fetch_failed">Abrufen der Themes fehlgeschlagen</string>
+ <string name="theme_set_failed">Theme festlegen fehlgeschlagen</string>
+ <string name="theme_auth_error_message">Stelle sicher, dass du die Berechtigung hast Themes festzulegen</string>
+ <string name="comments_empty_list">Keine Kommentare</string>
+ <string name="mnu_comment_unspam">Kein Spam</string>
+ <string name="no_site_error">Konnte nicht zur WordPress-Website verbinden</string>
+ <string name="adding_cat_failed">Hinzufügen der Kategorie fehlgeschlagen</string>
+ <string name="adding_cat_success">Kategorie erfolgreich hinzugefügt</string>
+ <string name="cat_name_required">Das Feld Kategorie-Name ist erforderlich</string>
+ <string name="category_automatically_renamed">Kategorie-Name %1$s ist ungültig. Er wurde umbenannt zu %2$s.</string>
+ <string name="no_account">Kein WordPress-Konto gefunden, füge ein Konto hinzu und versuche es erneut</string>
+ <string name="sdcard_message">Eine angeschlossene SD-Karte wird für den Medienupload benötigt</string>
+ <string name="stats_empty_comments">Bisher keine Kommentare</string>
+ <string name="stats_bar_graph_empty">Keine Statistiken verfügbar</string>
+ <string name="invalid_url_message">Überprüfe ob die eingegebene Blog-URL gültig ist</string>
+ <string name="reply_failed">Antwort fehlgeschlagen</string>
+ <string name="notifications_empty_list">Keine Benachrichtigungen</string>
+ <string name="error_delete_post">Beim Löschen von %s trat ein Fehler auf</string>
+ <string name="error_refresh_posts">Beiträge konnten nicht aktualisiert werden</string>
+ <string name="error_refresh_pages">Seiten konnten nicht aktualisiert werden</string>
+ <string name="error_refresh_notifications">Benachrichtigungen konnten nicht aktualisiert werden</string>
+ <string name="error_refresh_comments">Kommentare konnten nicht aktualisiert werden</string>
+ <string name="error_refresh_stats">Statistiken konnten nicht aktualisiert werden</string>
+ <string name="error_generic">Es ist ein Fehler aufgetreten</string>
+ <string name="error_moderate_comment">Bei der Moderation trat ein Fehler auf</string>
+ <string name="error_edit_comment">Beim Bearbeiten des Kommentars ist ein Fehler aufgetreten</string>
+ <string name="error_upload">Beim Upload von %s ist ein Fehler aufgetreten</string>
+ <string name="error_load_comment">Kommentar konnte nicht geladen werden</string>
+ <string name="error_downloading_image">Fehler beim Herunterladen des Bildes</string>
+ <string name="passcode_wrong_passcode">Falsche PIN</string>
+ <string name="invalid_email_message">Deine E-Mail-Adresse ist ungültig</string>
+ <string name="invalid_password_message">Passwort muss aus mindestens 4 Zeichen bestehen</string>
+ <string name="invalid_username_too_short">Benutzername muss länger als 4 Zeichen sein</string>
+ <string name="invalid_username_too_long">Benutzername muss kürzer als 61 Zeichen sein</string>
+ <string name="username_only_lowercase_letters_and_numbers">Benutzername darf nur Kleinbuchstaben (a-z) und Nummern enthalten</string>
+ <string name="username_required">Gib einen Benutzernamen ein</string>
+ <string name="username_not_allowed">Benutzername nicht erlaubt</string>
+ <string name="username_must_be_at_least_four_characters">Benutzername muss aus mindestens 4 Zeichen bestehen</string>
+ <string name="username_contains_invalid_characters">Benutzername darf das Zeichen "_" nicht enthalten</string>
+ <string name="username_must_include_letters">Benutzername muss mindestens einen Buchstaben (a-z) enthalten</string>
+ <string name="email_invalid">Gib eine gültige E-Mail-Adresse ein</string>
+ <string name="email_not_allowed">Diese E-Mail-Adresse ist nicht erlaubt</string>
+ <string name="username_exists">Dieser Benutzername existiert bereits</string>
+ <string name="email_exists">Diese E-Mail-Adresse wird bereits verwendet</string>
+ <string name="username_reserved_but_may_be_available">Dieser Benutzername ist momentan reserviert, ist aber vielleicht in ein paar Tagen verfügbar</string>
+ <string name="blog_name_required">Gib eine Website-Adresse ein</string>
+ <string name="blog_name_not_allowed">Diese Website-Adresse ist nicht erlaubt</string>
+ <string name="blog_name_must_be_at_least_four_characters">Website-Adresse muss aus mindestens 4 Zeichen bestehen</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Die Website-Adresse muss kürzer sein als 64 Zeichen</string>
+ <string name="blog_name_contains_invalid_characters">Website-Adresse darf das Zeichen "_" nicht enthalten</string>
+ <string name="blog_name_cant_be_used">Diese Website-Adresse kann nicht genutzt werden</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Website-Adresse darf nur Kleinbuchstaben (a-z) und Nummern enthalten</string>
+ <string name="blog_name_exists">Diese Website existiert bereits</string>
+ <string name="blog_name_reserved">Diese Website ist reserviert</string>
+ <string name="blog_name_reserved_but_may_be_available">Diese Website ist momentan reserviert, aber eventuell in ein paar Tagen verfügbar</string>
+ <string name="username_or_password_incorrect">Der Benutzername oder das Passwort ist falsch</string>
+ <string name="nux_cannot_log_in">Wir können dich nicht anmelden</string>
+ <string name="xmlrpc_error">Konnte nicht verbinden. Gib den vollständigen Pfad zur xmlrpc.php deiner Website ein und versuche es erneut.</string>
+ <string name="select_categories">Kategorien auswählen</string>
+ <string name="account_details">Konto-Details</string>
+ <string name="edit_post">Beitrag bearbeiten</string>
+ <string name="add_comment">Kommentar hinzufügen</string>
+ <string name="connection_error">Fehler beim Verbinden</string>
+ <string name="cancel_edit">Bearbeiten abbrechen</string>
+ <string name="scaled_image_error">Gib eine gültige Breite ein</string>
+ <string name="post_not_found">Beim Abrufen des Beitrags trat ein Fehler auf. Lade deine Beiträge erneut und versuche es nochmal.</string>
+ <string name="learn_more">Mehr erfahren</string>
+ <string name="media_gallery_settings_title">Galerie-Einstellungen</string>
+ <string name="media_gallery_image_order">Bild-Reihenfolge</string>
+ <string name="media_gallery_num_columns">Anzahl Spalten</string>
+ <string name="media_gallery_type_thumbnail_grid">Vorschaubild-Raster</string>
+ <string name="media_gallery_edit">Galerie bearbeiten</string>
+ <string name="media_error_no_permission">Du hast keine Berichtigung die Mediathek zu sehen</string>
+ <string name="cannot_delete_multi_media_items">Einige Mediendateien konnten nicht gelöscht werden. Versuche es später erneut.</string>
+ <string name="themes_live_preview">Live-Vorschau</string>
+ <string name="theme_current_theme">Aktuelles Theme</string>
+ <string name="theme_premium_theme">Premium-Theme</string>
+ <string name="link_enter_url_text">Linktext (optional)</string>
+ <string name="create_a_link">Link erstellen</string>
+ <string name="page_settings">Seiten-Einstellungen</string>
+ <string name="local_draft">Lokaler Entwurf</string>
+ <string name="upload_failed">Upload fehlgeschlagen</string>
+ <string name="horizontal_alignment">Horizontale Ausrichtung</string>
+ <string name="file_not_found">Konnte die Mediendatei für den Upload nicht finden. Wurde sie gelöscht oder verschoben?</string>
+ <string name="post_settings">Beitrags-Einstellungen</string>
+ <string name="delete_post">Beitrag löschen</string>
+ <string name="delete_page">Seite löschen</string>
+ <string name="comment_status_approved">Genehmigt</string>
+ <string name="comment_status_unapproved">Ausstehend</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Gelöscht</string>
+ <string name="edit_comment">Kommentar bearbeiten</string>
+ <string name="mnu_comment_approve">Genehmigen</string>
+ <string name="mnu_comment_unapprove">Ablehnen</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Papierkorb</string>
+ <string name="dlg_approving_comments">Genehmige</string>
+ <string name="dlg_unapproving_comments">Lehne ab</string>
+ <string name="dlg_spamming_comments">Als Spam markieren</string>
+ <string name="dlg_trashing_comments">Verschiebe in Papierkorb</string>
+ <string name="dlg_confirm_trash_comments">In Papierkorb verschieben?</string>
+ <string name="trash_yes">Papierkorb</string>
+ <string name="trash_no">Nicht löschen</string>
+ <string name="trash">Papierkorb</string>
+ <string name="author_name">Autor-Name</string>
+ <string name="author_email">Autor-E-Mail</string>
+ <string name="author_url">Autoren-URL</string>
+ <string name="hint_comment_content">Kommentar</string>
+ <string name="saving_changes">Änderungen speichern</string>
+ <string name="sure_to_cancel_edit_comment">Bearbeiten des Kommentars abbrechen?</string>
+ <string name="content_required">Kommentar wird benötigt</string>
+ <string name="toast_comment_unedited">Kommentar wurde nicht verändert</string>
+ <string name="remove_account">Website entfernen</string>
+ <string name="blog_removed_successfully">Website erfolgreich entfernt</string>
+ <string name="delete_draft">Entwurf löschen</string>
+ <string name="preview_page">Seitenvorschau</string>
+ <string name="preview_post">Beitragsvorschau</string>
+ <string name="comment_added">Kommentar erfolgreich hinzugefügt</string>
+ <string name="post_not_published">Status des Beitrags ist nicht veröffentlicht</string>
+ <string name="page_not_published">Seitenstatus ist nicht veröffentlicht</string>
+ <string name="view_in_browser">Im Browser anschauen</string>
+ <string name="add_new_category">Neue Kategorie hinzufügen</string>
+ <string name="category_name">Kategorie-Name</string>
+ <string name="category_slug">Kategorie-Titelform (optional)</string>
+ <string name="category_desc">Kategorie-Beschreibung (optional)</string>
+ <string name="category_parent">Elternkategorie (optional):</string>
+ <string name="share_action_post">Neuer Beitrag</string>
+ <string name="share_action_media">Mediathek</string>
+ <string name="file_error_create">Konnte keine temporäre Datei für den Medienupload erstellen. Stelle sicher, dass genügend Speicherplatz auf deinem Gerät verfügbar ist.</string>
+ <string name="location_not_found">Unbekannter Standort</string>
+ <string name="open_source_licenses">Open-Source-Lizenzen</string>
+ <string name="invalid_site_url_message">Überprüfe, ob die eingegebene Website-URL gültig ist.</string>
+ <string name="pending_review">Ausstehender Review</string>
+ <string name="http_credentials">HTTP-Zugangsdaten (optional)</string>
+ <string name="http_authorization_required">Autorisierung erforderlich</string>
+ <string name="post_format">Beitragsformat</string>
+ <string name="notifications_empty_all">Keine Benachrichtigung bisher.</string>
+ <string name="new_post">Neuer Beitrag</string>
+ <string name="new_media">Neue Mediendatei</string>
+ <string name="view_site">Website ansehen</string>
+ <string name="privacy_policy">Datenschutzbestimmung</string>
+ <string name="local_changes">Lokale Änderungen</string>
+ <string name="image_settings">Bild-Einstellungen</string>
+ <string name="add_account_blog_url">Blog-Adresse</string>
+ <string name="wordpress_blog">WordPress-Blog</string>
+ <string name="error_blog_hidden">Dieser Blog ist versteckt und konnte nicht geladen werden. Aktivierte ihn erneut in den Einstellungen und versuche es nochmal.</string>
+ <string name="fatal_db_error">Bei der Erstellung der App-Datenbank ist ein Fehler aufgetreten. Versuche die App erneut zu installieren.</string>
+ <string name="jetpack_message_not_admin">Das Jetpack Plugin wird für die Statistiken benötigt. Kontaktiere den Website-Administrator.</string>
+ <string name="reader_title_applog">Anwendungslog</string>
+ <string name="reader_share_link">Link teilen</string>
+ <string name="reader_toast_err_add_tag">Hinzufügen des Tags nicht möglich</string>
+ <string name="reader_toast_err_remove_tag">Tag konnte nicht entfernt werden</string>
+ <string name="required_field">Pflichtfeld</string>
+ <string name="email_hint">E-Mail-Adresse</string>
+ <string name="site_address">Deine selbst-gehostete Adresse (URL)</string>
+ <string name="email_cant_be_used_to_signup">Du kannst diese E-Mail-Adresse nicht zur Registrierung verwenden. Wir haben Probleme mit diesem Anbieter, da er einige unserer E-Mails blockiert. Nutze bitte einen anderen E-Mail-Anbieter.</string>
+ <string name="email_reserved">Diese E-Mail-Adresse wird bereits verwendet. Schaue in deinem Posteingang nach einer Aktivierungs-E-Mail. Falls du nicht aktivierst, kannst du es in ein paar Tagen erneut versuchen.</string>
+ <string name="blog_name_must_include_letters">Website-Adresse muss mindestens einen Buchstaben (a-z) enthalten</string>
+ <string name="blog_name_invalid">Ungültige Website-Adresse</string>
+ <string name="blog_title_invalid">Ungültiger Website-Titel</string>
+ <string name="deleting_page">Lösche Seite</string>
+ <string name="deleting_post">Lösche Beitrag</string>
+ <string name="share_url_post">Beitrag teilen</string>
+ <string name="share_url_page">Seite teilen</string>
+ <string name="share_link">Link teilen</string>
+ <string name="creating_your_account">Erstelle dein Konto</string>
+ <string name="creating_your_site">Erstelle deine Website</string>
+ <string name="reader_empty_posts_in_tag_updating">Beiträge werden abgerufen…</string>
+ <string name="error_refresh_media">Etwas ist schief gelaufen beim Aktualisieren der Mediathek. Bitte versuche es später nochmal.</string>
+ <string name="reader_likes_you_and_multi">Dir und %,d anderen gefällt dies</string>
+ <string name="reader_likes_multi">%d Leuten gefällt dies</string>
+ <string name="reader_toast_err_get_comment">Kommentar konnte nicht geladen werden</string>
+ <string name="reader_label_reply">Antworten</string>
+ <string name="video">Video</string>
+ <string name="download">Lade Medien herunter</string>
+ <string name="comment_spammed">Kommentar als Spam markiert</string>
+ <string name="cant_share_no_visible_blog">Du kannst nicht auf WordPress teilen ohne einen sichtbaren Blog</string>
+ <string name="select_time">Zeit auswählen</string>
+ <string name="reader_likes_you_and_one">Dir und einem anderen gefällt dies</string>
+ <string name="reader_empty_followed_blogs_description">Keine Bange, tippe einfach das Icon rechts oben an um zu stöbern!</string>
+ <string name="select_date">Datum auswählen</string>
+ <string name="pick_photo">Foto auswählen</string>
+ <string name="account_two_step_auth_enabled">Dieses Konto hat Zwei-Schritt-Authentifizierung aktiviert. Besuche deine Sicherheitseinstellungen auf WordPress.com und generiere ein App-spezifisches Passwort.</string>
+ <string name="pick_video">Video auswählen</string>
+ <string name="reader_toast_err_get_post">Beitrag konnte nicht geladen werden</string>
+ <string name="validating_user_data">Validiere Benutzer-Daten</string>
+ <string name="validating_site_data">Validiere Website-Daten</string>
+ <string name="password_invalid">Du benötigst ein sichereres Passwort. Stelle sicher, dass Du 7 oder mehr Zeichen benutzt und Groß- und Kleinbuchstaben enthalten sind, genau so wie Zahlen und Sonderzeichen.</string>
+ <string name="nux_tap_continue">Weiter</string>
+ <string name="nux_welcome_create_account">Konto erstellen</string>
+ <string name="signing_in">Anmelden…</string>
+ <string name="nux_add_selfhosted_blog">Selbst-gehostete Website hinzufügen</string>
+ <string name="nux_oops_not_selfhosted_blog">Bei WordPress.com anmelden</string>
+ <string name="media_add_popup_title">Zur Mediathek hinzufügen</string>
+ <string name="media_add_new_media_gallery">Erstelle Galerie</string>
+ <string name="empty_list_default">Diese Liste ist leer</string>
+ <string name="select_from_media_library">Aus der Mediathek auswählen</string>
+ <string name="jetpack_message">Das Jetpack-Plugin ist erforderlich für Statistiken. Möchtest Du Jetpack installieren?</string>
+ <string name="jetpack_not_found">Jetpack Plugin nicht gefunden</string>
+ <string name="reader_untitled_post">(Ohne Titel)</string>
+ <string name="reader_share_subject">Geteilt von %s</string>
+ <string name="reader_btn_share">Teilen</string>
+ <string name="reader_btn_follow">Folgen</string>
+ <string name="reader_btn_unfollow">Folge ich</string>
+ <string name="reader_hint_comment_on_comment">Antwort auf Kommentar…</string>
+ <string name="reader_label_added_tag">%s hinzugefügt</string>
+ <string name="reader_label_removed_tag">%s entfernt</string>
+ <string name="reader_likes_one">Einer Person gefällt das</string>
+ <string name="reader_likes_only_you">Dir gefällt das</string>
+ <string name="reader_toast_err_comment_failed">Konnte Deinen Kommentar nicht veröffentlichen</string>
+ <string name="reader_toast_err_tag_exists">Du folgt diesem Schlagwort bereits</string>
+ <string name="reader_toast_err_tag_invalid">Das ist kein gültiges Schlagwort</string>
+ <string name="reader_toast_err_share_intent">Teilen nicht möglich</string>
+ <string name="reader_toast_err_view_image">Kann das Bild nicht anzeigen</string>
+ <string name="reader_toast_err_url_intent">Konnte %s nicht öffnen</string>
+ <string name="reader_empty_followed_tags">Du folgst keinen Schlagwörtern</string>
+ <string name="create_account_wpcom">Erstelle ein Konto auf WordPress.com</string>
+ <string name="button_next">Nächster</string>
+ <string name="connecting_wpcom">Verbinde zu WordPress.com</string>
+ <string name="username_invalid">Ungültiger Benutzername</string>
+ <string name="limit_reached">Limit erreicht. Du kannst es in 1 Minute nochmal versuchen. Ein weiterer Versuch vor Ablauf wird nur die Wartezeit verlängern, bis die Sperre aufgehoben wird. Falls du dies für einen Fehler hältst, kontaktiere bitte den Support.</string>
+ <string name="nux_tutorial_get_started_title">Los geht\'s!</string>
+ <string name="themes">Themes</string>
+ <string name="all">Alle</string>
+ <string name="images">Bilder</string>
+ <string name="unattached">Unverknüpft</string>
+ <string name="custom_date">Benutzerdefiniertes Datum</string>
+ <string name="media_add_popup_capture_photo">Foto aufnehmen</string>
+ <string name="media_add_popup_capture_video">Video aufnehmen</string>
+ <string name="media_gallery_image_order_random">Zufall</string>
+ <string name="media_gallery_image_order_reverse">Umgekehrt</string>
+ <string name="media_gallery_type">Typ</string>
+ <string name="media_gallery_type_squares">Quadrate</string>
+ <string name="media_gallery_type_tiled">Gekachelt</string>
+ <string name="media_gallery_type_circles">Kreise</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="media_edit_title_text">Titel</string>
+ <string name="media_edit_caption_text">Beschriftung</string>
+ <string name="media_edit_description_text">Beschreibung</string>
+ <string name="media_edit_title_hint">Gib hier einen Titel ein</string>
+ <string name="media_edit_caption_hint">Gib hier eine Beschriftung ein</string>
+ <string name="media_edit_description_hint">Gib hier eine Beschreibung ein</string>
+ <string name="media_edit_success">Aktualisiert</string>
+ <string name="media_edit_failure">Aktualisierung fehlgeschlagen</string>
+ <string name="themes_details_label">Details</string>
+ <string name="themes_features_label">Features</string>
+ <string name="theme_activate_button">Aktivieren</string>
+ <string name="theme_activating_button">Aktiviere</string>
+ <string name="theme_set_success">Theme erfolgreich festgelegt!</string>
+ <string name="theme_auth_error_title">Abruf der Themes fehlgeschlagen</string>
+ <string name="post_excerpt">Auszug</string>
+ <string name="share_action_title">Hinzufügen zu …</string>
+ <string name="share_action">Teilen</string>
+ <string name="stats">Statistik</string>
+ <string name="stats_view_visitors_and_views">Besucher und Aufrufe</string>
+ <string name="stats_view_clicks">Klicks</string>
+ <string name="stats_view_tags_and_categories">Tags &amp; Kategorien</string>
+ <string name="stats_view_referrers">Referrer</string>
+ <string name="stats_timeframe_today">Heute</string>
+ <string name="stats_timeframe_yesterday">Gestern</string>
+ <string name="stats_timeframe_days">Tage</string>
+ <string name="stats_timeframe_weeks">Wochen</string>
+ <string name="stats_timeframe_months">Monate</string>
+ <string name="stats_entry_country">Land</string>
+ <string name="stats_entry_posts_and_pages">Titel</string>
+ <string name="stats_entry_tags_and_categories">Thema</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_totals_views">Aufrufe</string>
+ <string name="stats_totals_clicks">Klicks</string>
+ <string name="stats_totals_plays">Abgespielt</string>
+ <string name="passcode_manage">PIN-Sperre bearbeiten</string>
+ <string name="passcode_enter_passcode">Gib deine PIN ein</string>
+ <string name="passcode_enter_old_passcode">Gib deine alte PIN ein</string>
+ <string name="passcode_re_enter_passcode">PIN erneut eingeben</string>
+ <string name="passcode_change_passcode">PIN ändern</string>
+ <string name="passcode_set">PIN gesetzt</string>
+ <string name="passcode_preference_title">PIN-Sperre</string>
+ <string name="passcode_turn_off">Stelle PIN-Sperre aus</string>
+ <string name="passcode_turn_on">Stelle PIN-Sperre an</string>
+ <string name="upload">Hochladen</string>
+ <string name="discard">Verwerfen</string>
+ <string name="sign_in">Anmelden</string>
+ <string name="notifications">Benachrichtigungen</string>
+ <string name="note_reply_successful">Antwort veröffentlicht</string>
+ <string name="follows">Folgt</string>
+ <string name="new_notifications">%d neue Benachrichtigungen</string>
+ <string name="more_notifications">und %d weitere.</string>
+ <string name="loading">Lädt...</string>
+ <string name="httpuser">HTTP-Benutzername</string>
+ <string name="httppassword">HTTP-Passwort</string>
+ <string name="error_media_upload">Beim Hochladen von Medien ist ein Fehler aufgetreten</string>
+ <string name="post_content">Inhalt (hier tippen um Text oder Medien hinzuzufügen)</string>
+ <string name="publish_date">Veröffentlichen</string>
+ <string name="content_description_add_media">Medien hinzufügen</string>
+ <string name="incorrect_credentials">Falscher Benutzername oder falsches Passwort.</string>
+ <string name="password">Passwort</string>
+ <string name="username">Benutzername</string>
+ <string name="reader">Reader</string>
+ <string name="featured">Als Beitragsbild nutzen</string>
+ <string name="featured_in_post">Bild im Beitragsinhalt verwenden</string>
+ <string name="no_network_title">Kein Netzwerk verfügbar</string>
+ <string name="pages">Seiten</string>
+ <string name="caption">Bildunterschrift (optional)</string>
+ <string name="width">Breite</string>
+ <string name="posts">Beiträge</string>
+ <string name="anonymous">Anonym</string>
+ <string name="page">Seite</string>
+ <string name="post">Beitrag</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Hochladen und zum skalierten Bild verlinken</string>
+ <string name="scaled_image">Skalierte Bildbreite</string>
+ <string name="scheduled">Geplant</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Lädt hoch…</string>
+ <string name="version">Version</string>
+ <string name="tos">Nutzungsbedingungen</string>
+ <string name="app_title">WordPress für Android</string>
+ <string name="max_thumbnail_px_width">Standardbreite für Bilder</string>
+ <string name="image_alignment">Ausrichtung</string>
+ <string name="refresh">Aktualisieren</string>
+ <string name="untitled">Unbenannt</string>
+ <string name="edit">Bearbeiten</string>
+ <string name="post_id">Beitrag</string>
+ <string name="page_id">Seite</string>
+ <string name="post_password">Passwort (optional)</string>
+ <string name="immediately">Sofort</string>
+ <string name="quickpress_add_alert_title">Namen für Tastaturkürzel festlegen</string>
+ <string name="today">Heute</string>
+ <string name="settings">Einstellungen</string>
+ <string name="share_url">URL weiterempfehlen</string>
+ <string name="quickpress_window_title">Blog für QuickPress Verknüpfung auswählen</string>
+ <string name="quickpress_add_error">Name des Tastaturkürzel darf nicht leer sein</string>
+ <string name="publish_post">Veröffentlichen</string>
+ <string name="draft">Entwurf</string>
+ <string name="post_private">Privater Eintrag</string>
+ <string name="upload_full_size_image">Hochladen und zu Vollbild verlinken</string>
+ <string name="title">Titel</string>
+ <string name="tags_separate_with_commas">Tags (durch Kommas getrennt)</string>
+ <string name="categories">Kategorien</string>
+ <string name="dlg_deleting_comments">Lösche Kommentare</string>
+ <string name="notification_blink">Benachrichtigungslicht blinken</string>
+ <string name="notification_sound">Benachrichtigungston</string>
+ <string name="notification_vibrate">Vibration</string>
+ <string name="status">Status</string>
+ <string name="location">Ortung</string>
+ <string name="sdcard_title">SD-Karte benötigt</string>
+ <string name="select_video">Wähle ein Video von der Galerie</string>
+ <string name="media">Media</string>
+ <string name="delete">Löschen</string>
+ <string name="none">Nichts</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Wähle ein Foto von der Galerie</string>
+ <string name="error">Fehler</string>
+ <string name="cancel">Abbrechen</string>
+ <string name="save">Speichern</string>
+ <string name="add">Hinzufügen</string>
+ <string name="category_refresh_error">Fehler beim Aktualisieren der Kategorien</string>
+ <string name="preview">Vorschau</string>
+ <string name="on">zu</string>
+ <string name="reply">Antworten</string>
+ <string name="notification_settings">Benachrichtigungs-Einstellungen</string>
+ <string name="yes">Ja</string>
+ <string name="no">Nein</string>
+</resources>
diff --git a/WordPress/src/main/res/values-el/strings.xml b/WordPress/src/main/res/values-el/strings.xml
new file mode 100644
index 000000000..e002f9e8b
--- /dev/null
+++ b/WordPress/src/main/res/values-el/strings.xml
@@ -0,0 +1,1102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="editor_failed_uploads_switch_html">Μερικές μεταφορτώσεις πολυμέσων απέτυχαν. Δεν μπορείτε να μεταβείτε σε λειτουργία HTML \n σε αυτή την κατάσταση. Αφαίρεση όλων των αποτυχημένων μεταφορτώσεων και συνέχεια;</string>
+ <string name="format_bar_description_html">Λειτουργία HTML</string>
+ <string name="visual_editor">Κειμενογράφος</string>
+ <string name="image_thumbnail">Μικρογραφία εικόνας</string>
+ <string name="format_bar_description_ul">Λίστα με κουκκίδες</string>
+ <string name="format_bar_description_ol">Αριθμητική λίστα</string>
+ <string name="format_bar_description_more">Εισαγωγή περισσότερων</string>
+ <string name="format_bar_description_media">Εισαγωγή πολυμέσων</string>
+ <string name="format_bar_description_strike">Διεγραμμένα</string>
+ <string name="format_bar_description_quote">Παράθεμα</string>
+ <string name="format_bar_description_link">Εισαγωγή συνδέσμου</string>
+ <string name="format_bar_description_italic">Πλάγια</string>
+ <string name="format_bar_description_underline">Υπογράμμιση</string>
+ <string name="image_settings_save_toast">Οι αλλαγές αποθηκεύτηκαν</string>
+ <string name="image_caption">Λεζάντα</string>
+ <string name="image_alt_text">Εναλλακτικό κείμενο</string>
+ <string name="image_link_to">Σύνδεση στο</string>
+ <string name="image_width">Πλάτος</string>
+ <string name="format_bar_description_bold">Έντονα</string>
+ <string name="image_settings_dismiss_dialog_title">Απόρριψη μη αποθηκευμένων αλλαγών;</string>
+ <string name="stop_upload_dialog_title">Διακοπή μεταφόρτωσης;</string>
+ <string name="stop_upload_button">Διακοπή Μεταφόρτωσης</string>
+ <string name="alert_error_adding_media">Παρουσιάστηκε σφάλμα κατά την εισαγωγή πολυμέσων</string>
+ <string name="alert_action_while_uploading">Αυτή την στιγμή μεταφορτώνετε πολυμέσα. Παρακαλώ περιμένετε μέχρι να ολοκληρωθεί.</string>
+ <string name="alert_insert_image_html_mode">Δεν μπορείτε να προσθέσετε πολυμέσα απευθείας σε λειτουργία HTML. Παρακαλώ επιστρέψτε σε λειτουργία κειμενογράφου.</string>
+ <string name="uploading_gallery_placeholder">Μεταφόρτωση συλλογής...</string>
+ <string name="invite_sent">Η πρόσκληση στάλθηκε επιτυχώς</string>
+ <string name="tap_to_try_again">Πατήστε για να δοκιμάσετε ξανά!</string>
+ <string name="invite_message_info">(Προαιρετικό) Μπορείτε να εισάγετε ένα προσαρμοσμένο μήνυμα με έως 500 χαρακτήρες που θα συμπεριλαμβάνεται στην πρόσκληση των χρηστών.</string>
+ <string name="invite_message_remaining_other">Απομένουν %d χαρακτήρες</string>
+ <string name="invite_message_remaining_one">Απομένει 1 χαρακτήρας</string>
+ <string name="invite_message_remaining_zero">Απομένουν 0 χαρακτήρες</string>
+ <string name="invite_invalid_email">Η διεύθυνση ηλ. ταχυδρομείου \'%s\' δεν είναι έγκυρη</string>
+ <string name="invite_message_title">Προσαρμοσμένο Μήνυμα</string>
+ <string name="invite_already_a_member">Υπάρχει ήδη ένα μέλος με όνομα χρήστη \'%s\'</string>
+ <string name="invite_username_not_found">Δεν βρέθηκε χρήστης με όνομα χρήστη \'%s\'</string>
+ <string name="invite">Πρόσκληση</string>
+ <string name="invite_names_title">Ονόματα Χρήστη ή Ηλ. Διευθύνσεις</string>
+ <string name="signup_succeed_signin_failed">Ο λογαριασμός σας δημιουργήθηκε αλλά παρουσιάστηκε ένα σφάλμα ενώ σας συνδέαμε.\n Προσπαθήστε να συνδεθείτε με το πρόσφατα δημιουργημένο όνομα χρήστη και κωδικό πρόσβασης.</string>
+ <string name="send_link">Αποστολή συνδέσμου</string>
+ <string name="my_site_header_external">Εξωτερική</string>
+ <string name="invite_people">Πρόσκαλέστε Άτομα</string>
+ <string name="label_clear_search_history">Καθαρισμός ιστορικού αναζήτησης</string>
+ <string name="dlg_confirm_clear_search_history">Καθαρισμός ιστορικού αναζήτησης;</string>
+ <string name="reader_empty_posts_in_search_description">Δεν βρέθηκαν άρθρα στην γλώσσα σας για %s</string>
+ <string name="reader_label_post_search_running">Γίνεται αναζήτηση...</string>
+ <string name="reader_label_related_posts">Ανάγνωση Σχετικών</string>
+ <string name="reader_empty_posts_in_search_title">Δεν βρέθηκαν άρθρα</string>
+ <string name="reader_label_post_search_explainer">Αναζήτηση σε όλα τα δημόσια ιστολόγια του WordPress.com</string>
+ <string name="reader_hint_post_search">Αναζήτηση στο WordPress.com</string>
+ <string name="reader_title_related_post_detail">Σχετικό Άρθρο</string>
+ <string name="reader_title_search_results">Αναζήτηση για %s</string>
+ <string name="preview_screen_links_disabled">Οι σύνδεσμοι είναι απενεργοποιημένοι στην οθόνη προεπισκόπησης</string>
+ <string name="draft_explainer">Αυτό το άρθρο είναι ένα προσχέδιο που δεν έχει δημοσιευθεί</string>
+ <string name="send">Αποστολή</string>
+ <string name="person_remove_confirmation_title">Αφαίρεση %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Οι ιστότοποι σε αυτή τη λίστα δεν έχουν δημοσιεύσει κάτι πρόσφατα</string>
+ <string name="people">Άνθρωποι</string>
+ <string name="edit_user">Επεξεργασία Χρήστη</string>
+ <string name="role">Ρόλος</string>
+ <string name="error_remove_user">Αδυναμία αφαίρεσης χρήστη</string>
+ <string name="error_update_role">Αδυναμία ενημέρωσης ρόλου χρήστη</string>
+ <string name="gravatar_camera_and_media_permission_required">Απαιτούνται δικαιώματα πρόσβασης για να επιλέξετε ή να τραβήξετε μια φωτογραφία</string>
+ <string name="error_updating_gravatar">Σφάλμα κατά την ενημέρωση του Gravatar σας</string>
+ <string name="error_locating_image">Σφάλμα εντοπισμού της περικομμένης εικόνας</string>
+ <string name="error_refreshing_gravatar">Σφάλμα κατά την επαναφόρτωση του Gravatar σας</string>
+ <string name="gravatar_tip">Νέο! Πατήστε το Gravatar σας για να το αλλάξετε!</string>
+ <string name="error_cropping_image">Σφάλμα κατά την περικοπή της εικόνας</string>
+ <string name="launch_your_email_app">Εκκίνηση της εφαρμογής ηλ. αλληλογραφίας σας</string>
+ <string name="checking_email">Έλεγχος e-mail</string>
+ <string name="not_on_wordpress_com">Δεν είναι στο WordPress.com;</string>
+ <string name="magic_link_unavailable_error_message">Μη διαθέσιμο αυτή τη στιγμή. Παρακαλούμε εισάγετε το συνθηματικό σας</string>
+ <string name="check_your_email">Ελέγξτε τα e-mail σας</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Λάβετε ένα σύνδεσμο στο e-mail σας για να συνδεθείτε αμέσως</string>
+ <string name="logging_in">Γίνεται σύνδεση</string>
+ <string name="enter_your_password_instead">Εναλλακτικά, εισάγετε το συνθηματικό σας</string>
+ <string name="web_address_dialog_hint">Εμφανίζεται δημοσίως όταν σχολιάζετε.</string>
+ <string name="jetpack_not_connected_message">Το πρόσθετο Jetpack είναι εγκατεστημένο αλλά όχι συνδεδεμένο με το WordPress.com. Θέλετε να συνδέσετε το Jetpack;</string>
+ <string name="username_email">E-mail ή όνομα χρήστη</string>
+ <string name="jetpack_not_connected">Το πρόσθετο Jetpack δεν είναι συνδεδεμένο</string>
+ <string name="new_editor_reflection_error">Ο κειμενογράφος δεν είναι συμβατός με τη συσκευή σας.\nΑπενεργοποιήθηκε αυτόματα.</string>
+ <string name="stats_insights_latest_post_no_title">(δίχως τίτλο)</string>
+ <string name="capture_or_pick_photo">Τραβήξτε ή επιλέξτε φωτογραφία</string>
+ <string name="plans_post_purchase_text_themes">Έχετε πλέον απεριόριστη πρόσβαση σε Premium θέματα. Μπορείτε να κάνετε προεπισκόπηση οποιουδήποτε θέματος στον ιστότοπό σας για να ξεκινήσετε.</string>
+ <string name="plans_post_purchase_button_themes">Περιήγηση στα Θέματα</string>
+ <string name="plans_post_purchase_title_themes">Βρείτε ένα τέλειο, Premium θέμα</string>
+ <string name="plans_post_purchase_button_video">Ξεκινήστε καινούριο άρθρο</string>
+ <string name="plans_post_purchase_text_video">Μπορείτε να αποστείλετε και να φιλοξενήσετε βίντεο στον ιστότοπό σας με το VideoPress και τον αυξημένο αποθηκευτικό χώρο πολυμέσων.</string>
+ <string name="plans_post_purchase_title_video">Ζωντανέψτε τα άρθρα με βίντεο</string>
+ <string name="plans_post_purchase_button_customize">Προσαρμογή του Ιστότοπού μου</string>
+ <string name="plans_post_purchase_text_customize">Έχετε πλέον πρόσβαση σε προσαρμοσμένες γραμματοσειρές, χρώματα και δυνατότητες επεξεργασίας των CSS.</string>
+ <string name="plans_post_purchase_text_intro">Ο ιστότοπός σας κάνει τούμπες από τη χαρά του! Εξερευνήστε τώρα τα νέα χαρακτηριστικά του ιστότοπού σας και επιλέξτε από που θα θέλατε να ξεκινήσετε.</string>
+ <string name="plans_post_purchase_title_customize">Προσαρμόστε Γραμματοσειρές &amp; Χρώματα</string>
+ <string name="plans_post_purchase_title_intro">Όλο δικό σας, έτσι μπράβο!</string>
+ <string name="export_your_content_message">Τα άρθρα, οι σελίδες και οι ρυθμίσεις σας θα σας αποσταλούν με ηλ. ταχυδρομείο στο %s.</string>
+ <string name="plan">Πρόγραμμα</string>
+ <string name="plans">Προγράμματα</string>
+ <string name="plans_loading_error">Αδυναμία φόρτωσης προγραμμάτων</string>
+ <string name="export_your_content">Εξαγωγή του περιεχομένου σας</string>
+ <string name="exporting_content_progress">Εξαγωγή περιεχομένου...</string>
+ <string name="export_email_sent">E-mail εξαγωγής στάλθηκε!</string>
+ <string name="premium_upgrades_message">Έχετε ενεργές premium αναβαθμίσεις στον ιστότοπό σας. Παρακαλούμε ακυρώστε τις αναβαθμίσεις σας πριν διαγράψετε τον ιστότοπό σας</string>
+ <string name="show_purchases">Εμφάνιση αγορών</string>
+ <string name="checking_purchases">Έλεγχος αγορών</string>
+ <string name="premium_upgrades_title">Premium Αναβαθμίσεις</string>
+ <string name="purchases_request_error">Κάτι πήγε στραβά. Αδυναμία αναζήτησης αγορών.</string>
+ <string name="delete_site_progress">Διαγραφή ιστοτόπου...</string>
+ <string name="delete_site_summary">Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. Η διαγραφή του ιστοτόπου σας θα αφαιρέσει όλο το περιεχόμενο, τους συντελεστές και τους τομείς από τον ιστότοπο.</string>
+ <string name="delete_site_hint">Διαγραφή ιστότοπου</string>
+ <string name="export_site_hint">Εξαγωγή του ιστοτόπου σας σε αρχείο XML</string>
+ <string name="are_you_sure">Είστε σίγουροι;</string>
+ <string name="export_site_summary">Αν είστε σίγουροι, παρακαλούμε αφιερώστε λίγο χρόνο να εξάγετε τώρα το περιεχόμενο του ιστοτόπου σας. Δεν μπορεί να ανακτηθεί μελλοντικά.</string>
+ <string name="keep_your_content">Κρατήστε Το Περιεχόμενό Σας</string>
+ <string name="domain_removal_hint">Οι τομείς που δεν θα δουλεύουν μετά την αφαίρεση του ιστοτόπου σας</string>
+ <string name="domain_removal_summary">Προσοχή! Η διαγραφή του ιστοτόπου σας θα αφαιρέσει επίσης τον τομέα(είς) σας που εμφανίζονται παρακάτω.</string>
+ <string name="primary_domain">Πρωτεύων Τομέας</string>
+ <string name="domain_removal">Αφαίρεση Τομέα</string>
+ <string name="error_deleting_site_summary">Παρουσιάστηκε σφάλμα κατά τη διαγραφή του ιστοτόπου σας. Παρακαλούμε επικοινωνήστε με την υποστήριξη για περισσότερη βοήθεια.</string>
+ <string name="error_deleting_site">Σφάλμα διαγραφής ιστοτόπου</string>
+ <string name="confirm_delete_site_prompt">Παρακαλούμε πληκτρολογήστε %1$s στο παρακάτω πεδίο για επιβεβαίωση. Ο ιστότοπός σας μετά θα εξαφανιστεί για πάντα.</string>
+ <string name="site_settings_export_content_title">Εξαγωγή περιεχομένου</string>
+ <string name="contact_support">Επικοινωνία με υποστήριξη</string>
+ <string name="confirm_delete_site">Επιβεβαίωση Διαγραφής Ιστοτόπου</string>
+ <string name="start_over_text">Αν θέλετε να διατηρήσετε έναν ιστότοπο αλλά δεν θέλετε κανένα από τα άρθρα και τις σελίδες που έχετε τώρα, η ομάδα υποστήριξής μας μπορεί να διαγράψει τα άρθρα, τις σελίδες τα πολυμέσα και τα σχόλια για λογαριασμό σας.\n\nΑυτό θα διατηρήσει ενεργά τον ιστότοπο και το URL σας αλλά θα σας επιτρέψει ένα νέο ξεκίνημα στη δημιουργία περιεχομένου. Απλά επικοινωνήστε μαζί μας για να γίνει εκκαθάριση του τρέχοντος περιεχομένου σας.</string>
+ <string name="site_settings_start_over_hint">Ξεκινήστε τον ιστότοπό σας από την αρχή</string>
+ <string name="let_us_help">Αφήστε μας να βοηθήσουμε</string>
+ <string name="me_btn_app_settings">Ρυθμίσεις Εφαρμογής</string>
+ <string name="start_over">Ξεκινήστε από την Αρχή</string>
+ <string name="editor_remove_failed_uploads">Αφαίρεση αποτυχημένων μεταφορτώσεων</string>
+ <string name="editor_toast_failed_uploads">Μερικές μεταφορτώσεις πολυμέσων έχουν αποτύχει. Δεν μπορείτε να αποθηκεύσετε ή να δημοσιεύσετε\nτο άρθρο σας σε αυτή την κατάσταση. Θα θέλατε να αφαιρέσετε όλα τα προβληματικά πολυμέσα;</string>
+ <string name="comments_empty_list_filtered_trashed">Δεν υπάρχουν Διαγραμμένα σχόλια</string>
+ <string name="site_settings_advanced_header">Προχωρημένες</string>
+ <string name="comments_empty_list_filtered_pending">Δεν υπάρχουν Εκκρεμή σχόλια</string>
+ <string name="comments_empty_list_filtered_approved">Δεν υπάρχουν Εγκεκριμένα σχόλια</string>
+ <string name="button_done">Ολοκληρώθηκε</string>
+ <string name="button_skip">Παράβλεψη</string>
+ <string name="site_timeout_error">Αδυναμία σύνδεσης με τον ιστότοπο του WordPress λόγω σφάλματος Timeout.</string>
+ <string name="xmlrpc_malformed_response_error">Αδυναμία σύνδεσης. Η εγκατάσταση του WordPress απάντησε με μη έγκυρο έγγραφο XML-RPC.</string>
+ <string name="xmlrpc_missing_method_error">Αδυναμία σύνδεσης. Οι απαιτούμενες μέθοδοι XML-RPC απουσιάζουν από το διακομιστή.</string>
+ <string name="post_format_status">Κατάσταση</string>
+ <string name="post_format_video">Βίντεο</string>
+ <string name="theme_free">Δωρεάν</string>
+ <string name="theme_all">Όλα</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Συζήτηση</string>
+ <string name="post_format_gallery">Βιβλιοθήκη πολυμέσων</string>
+ <string name="post_format_image">Εικόνα</string>
+ <string name="post_format_link">Σύνδεσμος</string>
+ <string name="post_format_quote">Παράθεση</string>
+ <string name="post_format_standard">Κανονικό</string>
+ <string name="notif_events">Πληροφορίες για μαθήματα και εκδηλώσεις του WordPress.com (διαδικτυακά και φυσικά).</string>
+ <string name="post_format_aside">Πλευρικό</string>
+ <string name="post_format_audio">Ήχος</string>
+ <string name="notif_surveys">Ευκαιρίες συμμετοχής σε έρευνες και δημοσκοπήσεις του WordPress.com.</string>
+ <string name="notif_tips">Συμβουλές για να αξιοποιήσετε στο μέγιστο το WordPress.com.</string>
+ <string name="notif_community">Κοινότητα</string>
+ <string name="replies_to_my_comments">Απαντήσεις στα σχόλιά μου</string>
+ <string name="notif_suggestions">Προτάσεις</string>
+ <string name="notif_research">Έρευνα</string>
+ <string name="site_achievements">Επιτεύγματα του ιστότοπου</string>
+ <string name="username_mentions">Αναφορές του ονόματος χρήστη</string>
+ <string name="likes_on_my_posts">"Μου αρέσει" στα άρθρα μου</string>
+ <string name="site_follows">Ακόλουθοι ιστότοπου</string>
+ <string name="likes_on_my_comments">"Μου αρέσει" στα σχόλιά μου</string>
+ <string name="comments_on_my_site">Σχόλια στον ιστότοπό μου</string>
+ <string name="site_settings_list_editor_summary_other">%d αντικείμενα</string>
+ <string name="site_settings_list_editor_summary_one">1 αντικείμενο</string>
+ <string name="approve_auto_if_previously_approved">Σχόλια γνωστών χρηστών</string>
+ <string name="approve_auto">Όλοι οι χρήστες</string>
+ <string name="approve_manual">Δεν υπάρχουν σχόλια</string>
+ <string name="site_settings_paging_summary_other">%d σχόλια ανά σελίδα</string>
+ <string name="site_settings_paging_summary_one">1 σχόλιο ανά σελίδα</string>
+ <string name="site_settings_multiple_links_summary_other">Απαιτείται έγκριση για περισσότερους από %d συνδέσμους</string>
+ <string name="site_settings_multiple_links_summary_one">Απαιτείται έγκριση για περισσότερους από 1 σύνδεσμο</string>
+ <string name="site_settings_multiple_links_summary_zero">Απαιτείται έγκριση για περισσότερους από 0 συνδέσμους</string>
+ <string name="detail_approve_auto">Αυτόματη έγκριση όλων των σχολίων</string>
+ <string name="detail_approve_auto_if_previously_approved">Αυτόματη έγκριση αν ο χρήστης έχει προηγούμενο εγκεκριμένο σχόλιο</string>
+ <string name="detail_approve_manual">Απαιτείται χειροκίνητη έγκριση για όλα τα σχόλια.</string>
+ <string name="filter_trashed_posts">Διαγραμμένα</string>
+ <string name="days_quantity_one">1 ημέρα</string>
+ <string name="days_quantity_other">%d ημέρες</string>
+ <string name="filter_published_posts">Δημοσιευμένα</string>
+ <string name="filter_draft_posts">Προσχέδια</string>
+ <string name="filter_scheduled_posts">Προγραμματισμένα</string>
+ <string name="pending_email_change_snackbar">Κάντε κλικ στο σύνδεσμο επιβεβαίωσης στο μήνυμα e-mail που στάλθηκε στο %1$s για να επιβεβαιώσετε τη νέα σας διεύθυνση.</string>
+ <string name="primary_site">Κύριος ιστότοπος</string>
+ <string name="web_address">Διεύθυνση Ιστότοπου</string>
+ <string name="editor_toast_uploading_please_wait">Αυτή τη στιγμή μεταφορτώνετε πολυμέσα. Παρακαλώ περιμένετε μέχρι να ολοκληρωθεί.</string>
+ <string name="error_refresh_comments_showing_older">Δεν ήταν δυνατή η ανανέωση των σχολίων αυτή τη στιγμή - εμφάνιση παλαιότερων σχολίων</string>
+ <string name="editor_post_settings_set_featured_image">Ορισμός προβαλλόμενης εικόνας</string>
+ <string name="editor_post_settings_featured_image">Προβαλλόμενη Εικόνα</string>
+ <string name="new_editor_promo_desc">Η εφαρμογή του WordPress για Android περιλαμβάνει τώρα έναν όμορφο νέο\nεπεξεργαστή. Δοκιμάστε τον δημιουργώντας ένα νέο άρθρο.</string>
+ <string name="new_editor_promo_title">Ολοκαίνουριος επεξεργαστής κειμένου</string>
+ <string name="new_editor_promo_button_label">Σπουδαία, ευχαριστούμε!</string>
+ <string name="visual_editor_enabled">Ο οπτικοποιημένος επεξεργαστής ενεργοποιήθηκε</string>
+ <string name="editor_content_placeholder">Μοιραστείτε την ιστορία σας εδώ...</string>
+ <string name="editor_page_title_placeholder">Τίτλος Σελίδας</string>
+ <string name="editor_post_title_placeholder">Τίτλος Άρθρου</string>
+ <string name="email_address">Διεύθυνση email</string>
+ <string name="preference_show_visual_editor">Προβολή οπτικοποιημένου επεξεργαστή</string>
+ <string name="dlg_sure_to_delete_comments">Να διαγραφούν οριστικά αυτά τα σχόλια;</string>
+ <string name="preference_editor">Επεξεργαστής</string>
+ <string name="dlg_sure_to_delete_comment">Οριστική διαγραφή αυτού του σχολίου;</string>
+ <string name="mnu_comment_delete_permanently">Διαγραφή</string>
+ <string name="comment_deleted_permanently">Το σχόλιο διαγράφηκε</string>
+ <string name="mnu_comment_untrash">Αποκατάσταση</string>
+ <string name="comments_empty_list_filtered_spam">Δεν υπάρχουν ανεπιθύμητα σχόλια</string>
+ <string name="could_not_load_page">Αδυναμία φόρτωσης σελίδας</string>
+ <string name="comment_status_all">Όλα</string>
+ <string name="interface_language">Γλώσσα περιβάλλοντος εργασίας</string>
+ <string name="off">Κλειστό</string>
+ <string name="about_the_app">Σχετικά με την εφαρμογή</string>
+ <string name="error_post_account_settings">Αδυναμία αποθήκευσης ρυθμίσεων του λογαριασμού σας</string>
+ <string name="error_post_my_profile">Αδυναμία αποθήκευσης του προφίλ σας</string>
+ <string name="error_fetch_account_settings">Αδυναμία ανάκτησης των ρυθμίσεων του λογαριασμού σας</string>
+ <string name="error_fetch_my_profile">Αδυναμία ανάκτησης του προφίλ σας</string>
+ <string name="stats_widget_promo_ok_btn_label">ΟΚ, το κατάλαβα</string>
+ <string name="stats_widget_promo_desc">Προσθέστε τη μικροεφαρμογή στην αρχική σας οθόνη για έχετε πρόσβαση στα Στατιστικά σας με ένα κλικ.</string>
+ <string name="stats_widget_promo_title">Μικροεφαρμογή Στατιστικών Αρχικής Οθόνης</string>
+ <string name="site_settings_unknown_language_code_error">Ο κώδικας γλώσσας δεν αναγνωρίσθηκε</string>
+ <string name="site_settings_threading_dialog_description">Να επιτρέπεται η κατάταξη των σχολίων σε συζητήσεις.</string>
+ <string name="site_settings_threading_dialog_header">Συζητήσεις μέχρι</string>
+ <string name="remove">Αφαίρεση</string>
+ <string name="search">Αναζήτηση</string>
+ <string name="add_category">Προσθήκη κατηγορίας</string>
+ <string name="disabled">Απενεργοποιημένο</string>
+ <string name="site_settings_image_original_size">Αρχικό Μέγεθος</string>
+ <string name="privacy_private">Ο ιστότοπός σας είναι ορατός μόνο σε σας και τους χρήστες που εγκρίνετε</string>
+ <string name="privacy_public_not_indexed">Ο ιστότοπός σας είναι ορατός σε όλους αλλά ζητά από τις μηχανές αναζήτησης να μην τον ευρετηριάζουν</string>
+ <string name="privacy_public">Ο ιστότοπός σας είναι ορατός σε όλους και οι μηχανές αναζήτησης μπορούν να τον ευρετηριάζουν </string>
+ <string name="about_me_hint">Λίγα λόγια για εσάς...</string>
+ <string name="public_display_name_hint">Αν δεν ορίσετε όνομα που θα εμφανίζεται, από προεπιλογή θα εμφανίζεται το όνομα χρήστη σας.</string>
+ <string name="about_me">Για μένα</string>
+ <string name="public_display_name">Δημόσια εμφανιζόμενο όνομα</string>
+ <string name="my_profile">Το προφίλ μου</string>
+ <string name="first_name">Όνομα</string>
+ <string name="last_name">Επώνυμο</string>
+ <string name="site_privacy_public_desc">Να επιτρέπεται στις μηχανές αναζήτησης να ευρετηριάζουν αυτό τον ιστότοπο</string>
+ <string name="site_privacy_hidden_desc">Να αποθαρρύνονται οι μηχανές αναζήτησης από την ευρετηρίαση αυτού του ιστότοπου</string>
+ <string name="site_privacy_private_desc">Θα ήθελα ο ιστότοπός μου να είναι ιδιωτικός, ορατός μόνο σε χρήστες που επιλέγω</string>
+ <string name="cd_related_post_preview_image">Προεπισκόπηση εικόνας σχετικού άρθρου</string>
+ <string name="error_post_remote_site_settings">Αδυναμία αποθήκευσης πληροφοριών ιστοτόπου</string>
+ <string name="error_fetch_remote_site_settings">Αδυναμία ανάκτησης πληροφοριών ιστοτόπου</string>
+ <string name="error_media_upload_connection">Προέκυψε σφάλμα σύνδεσης κατά τη μεταφόρτωση των πολυμέσων</string>
+ <string name="site_settings_disconnected_toast">Αποσυνδεδεμένος, η επεξεργασία απενεργοποιήθηκε.</string>
+ <string name="site_settings_unsupported_version_error">Μη υποστηριζόμενη έκδοση του WordPress</string>
+ <string name="site_settings_multiple_links_dialog_description">Να απαιτείται έγκριση για σχόλια που περιλαμβάνουν πάνω από τόσους συνδέσμους.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Αυτόματο κλείσιμο</string>
+ <string name="site_settings_close_after_dialog_description">Αυτόματο κλείσιμο των σχολίων στα άρθρα.</string>
+ <string name="site_settings_paging_dialog_description">Σπάσιμο των συζητήσεων με σχόλια σε πολλαπλές σελίδες.</string>
+ <string name="site_settings_paging_dialog_header">Σχόλια ανά σελίδα</string>
+ <string name="site_settings_close_after_dialog_title">Κλείσιμο σχολιασμού</string>
+ <string name="site_settings_blacklist_description">Όταν ένα σχόλιο περιέχει οποιαδήποτε από αυτές τις λέξεις σε περιεχόμενο, όνομα, URL, e-mail ή IP, θα σημειώνεται ως ανεπιθύμητο. Μπορείτε να εισάγετε μέρη λέξεων, έτσι το "press" θα πιάσει και το "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">Όταν ένα σχόλιο περιέχει οποιαδήποτε από αυτές τις λέξεις σε περιεχόμενο, όνομα, URL, e-mail ή IP, θα παραμένει στην αναμονή προς έγκριση. Μπορείτε να εισάγετε μέρη λέξεων, έτσι το "press" θα πιάσει και το "WordPress." </string>
+ <string name="site_settings_list_editor_input_hint">Εισάγετε μια λέξη ή φράση</string>
+ <string name="site_settings_list_editor_no_items_text">Δεν υπάρχουν αντικείμενα</string>
+ <string name="site_settings_learn_more_caption">Μπορείτε να παρακάμψετε αυτές τις ρυθμίσεις για μεμονωμένα άρθρα.</string>
+ <string name="site_settings_rp_preview3_site">σε "Αναβαθμίστε"</string>
+ <string name="site_settings_rp_preview3_title">Εστίαση στην Αναβάθμιση: VideoPress Για Γάμους</string>
+ <string name="site_settings_rp_preview2_site">σε "Εφαρμογές"</string>
+ <string name="site_settings_rp_preview2_title">Στην εφαρμογή του WordPress για το Android γίνεται ένα μεγάλο λίφτινγκ</string>
+ <string name="site_settings_rp_preview1_site">σε "Κινητό"</string>
+ <string name="site_settings_rp_preview1_title">Μεγάλη Αναβάθμιση για iPhone/iPad Διαθέσιμη Τώρα</string>
+ <string name="site_settings_rp_show_images_title">Προβολή Εικόνων</string>
+ <string name="site_settings_rp_show_header_title">Προβολή Κεφαλίδας</string>
+ <string name="site_settings_rp_switch_summary">Στα Σχετικά Άρθρα εμφανίζεται σχετικό περιεχόμενο από τον ιστότοπό σας κάτω από τα άρθρα σας.</string>
+ <string name="site_settings_rp_switch_title">Εμφάνιση Σχετικών Άρθρων</string>
+ <string name="site_settings_delete_site_hint">Αφαιρεί τα δεδομένα του ιστοτόπου σας από την εφαρμογή</string>
+ <string name="site_settings_blacklist_hint">Σχόλια που εμπίπτουν σε ένα φίλτρο σημειώνονται ως ανεπιθύμητα</string>
+ <string name="site_settings_moderation_hold_hint">Σχόλια που εμπίπτουν σε ένα φίλτρο τοποθετούνται στην αναμονή προς έγκριση</string>
+ <string name="site_settings_multiple_links_hint">Αγνόηση του ορίου συνδέσμου για γνωστούς χρήστες</string>
+ <string name="site_settings_whitelist_hint">Ο συντάκτης του σχολίου πρέπει να έχει προηγουμένως εγκεκριμένο σχόλιο</string>
+ <string name="site_settings_user_account_required_hint">Οι χρήστες πρέπει να είναι εγγεγραμμένοι και συνδεδεμένοι για να σχολιάσουν</string>
+ <string name="site_settings_identity_required_hint">Ο συντάκτης του σχολίου θα πρέπει να συμπληρώσει όνομα και e-mail</string>
+ <string name="site_settings_manual_approval_hint">Τα σχόλια θα πρέπει να εγκρίνονται χειροκίνητα</string>
+ <string name="site_settings_paging_hint">Εμφάνιση σχολίων σε ομάδες καθορισμένου μεγέθους</string>
+ <string name="site_settings_threading_hint">Να επιτρέπεται η κατάταξη των σχολίων μέχρι συγκεκριμένου βάθους</string>
+ <string name="site_settings_sort_by_hint">Καθορίζει τη σειρά εμφάνισης των σχολίων</string>
+ <string name="site_settings_close_after_hint">Να μην επιτρέπονται τα σχόλια μετά τον καθορισμένο χρόνο</string>
+ <string name="site_settings_receive_pingbacks_hint">Να επιτρέπονται οι ειδοποιήσεις σύνδεσης από άλλα ιστολόγια</string>
+ <string name="site_settings_send_pingbacks_hint">Να γίνεται προσπάθεια ειδοποίησης ιστολογίων στα οποία γίνεται σύνδεση από το άρθρο</string>
+ <string name="site_settings_allow_comments_hint">Να επιτρέπεται στους αναγνώστες να σχολιάζουν</string>
+ <string name="site_settings_discussion_hint">Προβολή και αλλαγή των ρυθμίσεων συζήτησης των ιστοτόπων σας</string>
+ <string name="site_settings_more_hint">Προβολή όλων των διαθέσιμων ρυθμίσεων Συζητήσεων</string>
+ <string name="site_settings_related_posts_hint">Εμφάνιση ή απόκρυψη σχετικών άρθρων στον αναγνώστη</string>
+ <string name="site_settings_upload_and_link_image_hint">Ενεργοποιήστε για να μεταφορτώνεται πάντα η εικόνα πλήρους μεγέθους</string>
+ <string name="site_settings_image_width_hint">Αλλαγή μεγέθους των εικόνων στα άρθρα σε αυτό το πλάτος</string>
+ <string name="site_settings_format_hint">Ορίζει τη μορφή του νέου άρθρου</string>
+ <string name="site_settings_category_hint">Ορίζει την κατηγορία του νέου άρθρου</string>
+ <string name="site_settings_location_hint">Αυτόματη εισαγωγή δεδομένων τοποθεσίας στα άρθρα σας</string>
+ <string name="site_settings_password_hint">Αλλαγή του συνθηματικού σας</string>
+ <string name="site_settings_username_hint">Τρέχων λογαριασμός χρήστη</string>
+ <string name="site_settings_language_hint">Η γλώσσα στην οποία κυρίως γράφεται αυτό το ιστολόγιο</string>
+ <string name="site_settings_privacy_hint">Καθορίζει ποιος μπορεί να δει τον ιστότοπό σας</string>
+ <string name="site_settings_address_hint">Η αλλαγή της διεύθυνσής σας δεν υποστηρίζεται προς το παρόν</string>
+ <string name="site_settings_tagline_hint">Μια μικρή περιγραφή ή μια πιασάρικη φράση που περιγράφει το ιστολόγιό σας</string>
+ <string name="site_settings_title_hint">Με λίγες λέξεις, εξηγήστε τι αφορά αυτός ο ιστότοπος</string>
+ <string name="site_settings_whitelist_known_summary">Σχόλια από γνωστούς χρήστες</string>
+ <string name="site_settings_whitelist_all_summary">Σχόλια από όλους τους χρήστες</string>
+ <string name="site_settings_threading_summary">%d επίπεδα</string>
+ <string name="site_settings_privacy_private_summary">Ιδιωτικό</string>
+ <string name="site_settings_privacy_hidden_summary">Κρυμμένο</string>
+ <string name="site_settings_delete_site_title">Διαγραφή ιστοτόπου</string>
+ <string name="site_settings_privacy_public_summary">Δημόσιο</string>
+ <string name="site_settings_blacklist_title">Μαύρη λίστα</string>
+ <string name="site_settings_moderation_hold_title">Αναμονή για έλεγχο</string>
+ <string name="site_settings_multiple_links_title">Σύνδεσμοι στα σχόλια</string>
+ <string name="site_settings_whitelist_title">Αυτόματη έγκριση</string>
+ <string name="site_settings_threading_title">Νήματα</string>
+ <string name="site_settings_paging_title">Σελιδοποίηση</string>
+ <string name="site_settings_sort_by_title">Ταξινόμηση κατά</string>
+ <string name="site_settings_account_required_title">Οι χρήστες πρέπει να είναι συνεδεμένοι</string>
+ <string name="site_settings_identity_required_title">Πρέπει να περιλαμβάνεται όνομα και e-mail</string>
+ <string name="site_settings_receive_pingbacks_title">Λήψη Παραθέσεων</string>
+ <string name="site_settings_send_pingbacks_title">Αποστολή Παραθέσεων</string>
+ <string name="site_settings_allow_comments_title">Να επιτρέπονται τα Σχόλια</string>
+ <string name="site_settings_default_format_title">Προεπιλεγμένη Μορφή</string>
+ <string name="site_settings_default_category_title">Προεπιλεγμένη κατηγορία</string>
+ <string name="site_settings_location_title">Ενεργοποίηση Τοποθεσίας</string>
+ <string name="site_settings_address_title">Διεύθυνση</string>
+ <string name="site_settings_title_title">Τίτλος Ιστότοπου</string>
+ <string name="site_settings_tagline_title">Γραμμή ετικετών</string>
+ <string name="site_settings_this_device_header">Αυτή η συσκευή</string>
+ <string name="site_settings_discussion_new_posts_header">Προεπιλογές για νέα άρθρα</string>
+ <string name="site_settings_account_header">Λογαριασμός</string>
+ <string name="site_settings_writing_header">Συγγραφή</string>
+ <string name="newest_first">Τα νεότερα πρώτα</string>
+ <string name="site_settings_general_header">Γενικά</string>
+ <string name="discussion">Συζήτηση</string>
+ <string name="privacy">Ιδιωτικότητα</string>
+ <string name="related_posts">Σχετικά άρθρα</string>
+ <string name="comments">Σχόλια</string>
+ <string name="close_after">Κλείσιμο μετά από</string>
+ <string name="oldest_first">Τα παλιότερα πρώτα</string>
+ <string name="media_error_no_permission_upload">Δεν έχετε άδεια να μεταφορτώσετε πολυμέσα στον ιστότοπο</string>
+ <string name="never">Ποτέ</string>
+ <string name="unknown">Άγνωστο</string>
+ <string name="reader_err_get_post_not_found">Αυτό το άρθρο δεν υπάρχει πια</string>
+ <string name="reader_err_get_post_not_authorized">Δεν έχετε εξουσιοδότηση να δείτε αυτό το άρθρο</string>
+ <string name="reader_err_get_post_generic">Αδυναμία ανάκτησης αυτού του άρθρου</string>
+ <string name="blog_name_no_spaced_allowed">Η διεύθυνση δεν μπορεί να περιέχει κενά</string>
+ <string name="invalid_username_no_spaces">Το όνομα χρήστη δεν μπορεί να περιέχει κενά</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Οι σελίδες που ακολουθείτε δεν έχουν δημοσιεύσει κάτι πρόσφατα</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Καμία πρόσφατη δημοσίευση</string>
+ <string name="media_details_copy_url_toast">Η διεύθυνση αντιγράφτηκε στο πρόχειρο</string>
+ <string name="edit_media">Επεξεργασία πολυμέσου</string>
+ <string name="media_details_copy_url">Αντιγραφή διεύθυνσης</string>
+ <string name="media_details_label_date_uploaded">Ανέβηκε</string>
+ <string name="media_details_label_date_added">Προστέθηκε</string>
+ <string name="selected_theme">Επιλεγμένο Θέμα</string>
+ <string name="could_not_load_theme">Αποτυχία φόρτωσης θέματος</string>
+ <string name="theme_activation_error">Κάτι πήγε στραβά. Δεν ενεργοποιήθηκε το θέμα</string>
+ <string name="theme_by_author_prompt_append">κατά %1$s</string>
+ <string name="theme_prompt">Ευχαριστούμε που επιλέξατε το %1$s</string>
+ <string name="theme_try_and_customize">Δοκιμή &amp; Τροποποίηση</string>
+ <string name="theme_view">Προβολή</string>
+ <string name="theme_details">Λεπτομέριες</string>
+ <string name="theme_support">Υποστήριξη</string>
+ <string name="theme_done">ΕΓΙΝΕ</string>
+ <string name="theme_manage_site">ΔΙΑΧΕΙΡΙΣΗ ΤΟΠΟΘΕΣΙΑΣ</string>
+ <string name="title_activity_theme_support">Θέματα</string>
+ <string name="theme_activate">Ενεργοποίηση</string>
+ <string name="date_range_start_date">Ημερομηνία Έναρξης</string>
+ <string name="date_range_end_date">Ημερομηνία Λήξης</string>
+ <string name="current_theme">Τρέχον Θέμα</string>
+ <string name="customize">Τροποποίηση</string>
+ <string name="details">Λεπτομέριες</string>
+ <string name="support">Υποστήριξη</string>
+ <string name="active">Ενεργό</string>
+ <string name="stats_referrers_spam_generic_error">Κάτι πήγε στραβά κατά την εκτέλεση της λειτουργίας. Η κατάσταση ανεπιθύμητου δεν άλλαξε</string>
+ <string name="stats_referrers_marking_not_spam">Μη επισήμανση ως ανεπιθύμητο</string>
+ <string name="stats_referrers_unspam">Όχι ανεπιθύμητα</string>
+ <string name="stats_referrers_marking_spam">Επισήμανση ως ανεπιθύμητο</string>
+ <string name="theme_auth_error_authenticate">Αποτυχία ανάκτησης θεμάτων: αποτυχία πιστοποίησης αυθεντικότητας χρήστη</string>
+ <string name="post_published">Η δημοσίευση ολοκληρώθηκε</string>
+ <string name="page_published">Η σελίδα δημοσιεύθηκε</string>
+ <string name="post_updated">Η δημοσίευση ενημερώθηκε</string>
+ <string name="page_updated">Η σελίδα ανανεώθηκε</string>
+ <string name="stats_referrers_spam">Ανεπιθύμητα</string>
+ <string name="theme_no_search_result_found">Λυπούμαστε, δε βρέθηκαν θέματα.</string>
+ <string name="media_file_name">Όνομα αρχείου: %s</string>
+ <string name="media_uploaded_on">Μεταφορτώθηκε στο: %s</string>
+ <string name="media_dimensions">Διαστάσεις: %s</string>
+ <string name="upload_queued">Στην ουρά</string>
+ <string name="media_file_type">Τύπος αρχείου: %s</string>
+ <string name="reader_label_gap_marker">Φόρτωμα περισσότερων άρθρων</string>
+ <string name="notifications_no_search_results">Δεν βρέθηκαν ιστότοποι που να ταιριάζουν με την \'%s\'</string>
+ <string name="search_sites">Αναζήτηση ιστότοπων</string>
+ <string name="notifications_empty_view_reader">Προβολή Αναγνώστη</string>
+ <string name="unread">Μη Αναγνωσμένα </string>
+ <string name="notifications_empty_action_followers_likes">Λάβετε ειδοποιήσεις: σχολιάστε δημοσιεύσεις που έχετε διαβάσει.</string>
+ <string name="notifications_empty_action_comments">Συμμετέχετε σε συζήτηση: σχολιάστε δημοσιεύσεις των ιστολογίων που ακολουθείτε.</string>
+ <string name="notifications_empty_action_unread">Αναθερμάνετε τη συζήτηση: γράψτε ένα νέο post.</string>
+ <string name="notifications_empty_action_all">Δραστηριοποιηθείτε! Σχολιάστε σε αναρτήσεις από blogs που ακολουθείτε.</string>
+ <string name="notifications_empty_likes">Δεν υπάρχουν νέα like...ακόμα.</string>
+ <string name="notifications_empty_followers">Δεν έχετε νέους ακόλουθους... ακόμα.</string>
+ <string name="notifications_empty_comments">Δεν υπάρχουν νέα σχόλια...ακόμα.</string>
+ <string name="notifications_empty_unread">Δεν έχετε νέες ενημερώσεις!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Παρακαλούμε όπως προσπελάσετε τα Στατιστικά στην εφαρμογή και στη συνέχεια δοκιμάσετε να προσθέσετε την εφαρμογή-βοηθό</string>
+ <string name="stats_widget_error_readd_widget">Παρακαλούμε αφαιρέστε και προσθέστε ξανά την εφαρμογή</string>
+ <string name="stats_widget_error_no_visible_blog">Δεν μπορεί να γίνει προσπέλαση των στατιστικών χωρίς ένα φανερό ιστολόγιο</string>
+ <string name="stats_widget_error_no_permissions">Ο λογαριασμός σας WordPress.com δεν έχει πρόσβαση στα Στατιστικά σε αυτό το blog</string>
+ <string name="stats_widget_error_no_account">Παρακαλούμε συνδεθείτε στο WordPress</string>
+ <string name="stats_widget_error_generic">Τα στατιστικά δεν μπορούν να φορτωθούν</string>
+ <string name="stats_widget_loading_data">Φόρτωμα δεδομένων...</string>
+ <string name="stats_widget_name_for_blog">Σημερινά Στατιστικά για %1$s</string>
+ <string name="stats_widget_name">Σημερινά Στατιστικά του WordPress</string>
+ <string name="add_location_permission_required">Απαιτούνται δικαιώματα για να προσθέσετε τοποθεσία</string>
+ <string name="add_media_permission_required">Απαιτούνται δικαιώματα για να προσθέσετε πολυμέσα</string>
+ <string name="access_media_permission_required">Απαιτούνται δικαιώματα για να αποκτήσετε πρόσβαση στα πολυμέσα</string>
+ <string name="stats_enable_rest_api_in_jetpack">Για να δείτε τα στατιστικά σας, ενεργοποιήστε το πρόσθετο JSON API στο Jetpack.</string>
+ <string name="error_open_list_from_notification">Αυτό το άρθρο ή η σελίδα δημοσιεύτηκε σε άλλο ιστότοπο</string>
+ <string name="reader_short_comment_count_multi">%s Σχόλια</string>
+ <string name="reader_short_comment_count_one">1 Σχόλιο</string>
+ <string name="reader_label_submit_comment">ΑΠΟΣΤΟΛΗ</string>
+ <string name="reader_hint_comment_on_post">Απάντηση στο άρθρο...</string>
+ <string name="reader_discover_visit_blog">Επισκεφθείτε το %s</string>
+ <string name="reader_discover_attribution_blog">Δημοσιεύτηκε αρχικά στο %s</string>
+ <string name="reader_discover_attribution_author">Δημοσιεύτηκε αρχικά από %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Δημοσιεύτηκε αρχικά από %1$s στο %2$s</string>
+ <string name="reader_short_like_count_multi">%s "Like"</string>
+ <string name="reader_short_like_count_one">1 "Like"</string>
+ <string name="reader_label_follow_count">%,d ακόλουθοι</string>
+ <string name="reader_short_like_count_none">"Like"</string>
+ <string name="reader_menu_tags">Επεξεργασία ετικετών και ιστολογίων</string>
+ <string name="reader_title_post_detail">Άρθρο Αναγνώστη</string>
+ <string name="local_draft_explainer">Αυτό το άρθρο είναι ένα τοπικό προσχέδιο που δεν έχει δημοσιευτεί</string>
+ <string name="local_changes_explainer">Αυτό το άρθρο έχει τοπικές αλλαγές που δεν έχουν δημοσιευθεί</string>
+ <string name="notifications_push_summary">Ρυθμίσεις για τις ειδοποιήσεις που εμφανίζονται στη συσκευή σας.</string>
+ <string name="notifications_email_summary">Ρυθμίσεις για τις ειδοποιήσεις που στέλνονται στο email που είναι συνδεδεμένο με το λογαριασμό σας.</string>
+ <string name="notifications_tab_summary">Ρυθμίσεις για τις ειδοποιήσεις που εμφανίζονται στην καρτέλα Ειδοποιήσεις.</string>
+ <string name="notifications_disabled">Οι ειδοποιήσεις εφαρμογής έχουν απενεργοποιηθεί. Πατήστε εδώ για να τις ενεργοποιήσετε στις Ρυθμίσεις.</string>
+ <string name="notification_types">Τύποι Ειδοποιήσεων</string>
+ <string name="error_loading_notifications">Αδυναμία φόρτωσης ρυθμίσεων ειδοποιήσεων</string>
+ <string name="replies_to_your_comments">Απαντήσεις στα σχόλιά σας</string>
+ <string name="comment_likes">"Like" Σχολίων</string>
+ <string name="app_notifications">Ειδοποιήσεις εφαρμογής</string>
+ <string name="notifications_tab">Καρτέλα ειδοποιήσεων</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Σχόλια σε άλλους ιστότοπους</string>
+ <string name="notifications_wpcom_updates">Ενημερώσεις του WordPress.com</string>
+ <string name="notifications_other">Άλλα</string>
+ <string name="notifications_account_emails">Μήνυμα από το WordPress.com</string>
+ <string name="notifications_account_emails_summary">Θα σας στέλνουμε πάντα σημαντικά email σχετικά με το λογαριασμό σας αλλά μπορείτε να λαμβάνετε και μερικά χρήσιμα επιπλέον.</string>
+ <string name="notifications_sights_and_sounds">Εικόνες και Ήχοι</string>
+ <string name="your_sites">Οι ιστότοποί σας</string>
+ <string name="stats_insights_latest_post_trend">Πέρασαν %1$s από τη δημοσίευση του %2$s. Να πως πήγε το άρθρο μέχρι στιγμής...</string>
+ <string name="stats_insights_latest_post_summary">Πιο πρόσφατη σύνοψη άρθρων</string>
+ <string name="button_revert">Επαναφορά</string>
+ <string name="days_ago">%d μέρες πριν</string>
+ <string name="yesterday">Χθες</string>
+ <string name="connectionbar_no_connection">Δεν υπάρχει σύνδεση</string>
+ <string name="page_trashed">Η σελίδα στάλθηκε στα διαγραμμένα</string>
+ <string name="post_deleted">Το άρθρο διαγράφηκε</string>
+ <string name="post_trashed">Το άρθρο στάλθηκε στα διαγραμμένα</string>
+ <string name="stats_no_activity_this_period">Δεν υπήρξε δραστηριότητα αυτή την περίοδο</string>
+ <string name="trashed">Διαγραμμένα</string>
+ <string name="button_back">Πίσω</string>
+ <string name="page_deleted">Σελίδα διαγράφηκε</string>
+ <string name="button_stats">Στατιστικά</string>
+ <string name="button_trash">Διαγραφή</string>
+ <string name="button_preview">Προεπισκόπηση</string>
+ <string name="button_view">Προβολή</string>
+ <string name="button_edit">Αλλαγή</string>
+ <string name="button_publish">Δημοσίευση</string>
+ <string name="my_site_no_sites_view_subtitle">Θα θέλατε να προσθέσετε ένα;</string>
+ <string name="my_site_no_sites_view_title">Δεν έχετε κανένα ιστότοπο WordPress ακόμα.</string>
+ <string name="my_site_no_sites_view_drake">Απεικόνιση</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Δεν έχετε εξουσιοδότηση για να συνδεθείτε σε αυτό το ιστολόγιο</string>
+ <string name="reader_toast_err_follow_blog_not_found">Αδυναμία εύρεσης αυτού του ιστολογίου</string>
+ <string name="undo">Αναίρεση</string>
+ <string name="tabbar_accessibility_label_my_site">Ο ιστότοπος μου</string>
+ <string name="tabbar_accessibility_label_me">Εγώ</string>
+ <string name="passcodelock_prompt_message">Εισάγετε το PIN σας</string>
+ <string name="editor_toast_changes_saved">Οι αλλαγές αποθηκεύτηκαν</string>
+ <string name="push_auth_expired">Το αίτημα έληξε. Συνδεθείτε στο WordPress.com για να προσπαθήσετε ξανά.</string>
+ <string name="stats_insights_best_ever">Περισσότερες προβολές ποτέ</string>
+ <string name="ignore">Αγνοήστε</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% των προβολών</string>
+ <string name="stats_insights_most_popular_hour">Η πιο δημοφιλής ώρα</string>
+ <string name="stats_insights_most_popular_day">Η πιο δημοφιλής μέρα</string>
+ <string name="stats_insights_popular">Η πιο δημοφιλής ημέρα και ώρα</string>
+ <string name="stats_insights_today">Στατιστικά Ημέρας</string>
+ <string name="stats_insights_all_time">Συνολικά άρθρα, προβολές και επισκέπτες</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Για να δείτε τα στατιστικά σας, συνδεθείτε στο λογαριασμό του WordPress.com που χρησιμοποιήσατε για να συνδέσετε το Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Ψάχνετε για τα Άλλα Πρόσφατα Στατιστικά; Τα μετακινήσαμε στη σελίδα Insights.</string>
+ <string name="me_disconnect_from_wordpress_com">Αποσύνδεση από το WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Συνδεθείτε στο WordPress.com</string>
+ <string name="me_btn_login_logout">Σύνδεση/Αποσύνδεση</string>
+ <string name="account_settings">Ρυθμίσεις Λογαριασμού</string>
+ <string name="me_btn_support">Βοήθεια &amp; Υποστήριξη</string>
+ <string name="site_picker_cant_hide_current_site">Το "%s" δεν κρύφτηκε επειδή είναι ο τρέχων ιστότοπος</string>
+ <string name="site_picker_create_dotcom">Δημιουργία WordPress.com ιστότοπου</string>
+ <string name="site_picker_add_site">Προσθήκη ιστότοπου</string>
+ <string name="site_picker_add_self_hosted">Προσθήκη αυτο-φιλοξενούμενου ιστότοπου</string>
+ <string name="site_picker_edit_visibility">Εμφάνιση/απόκρυψη ιστότοπων</string>
+ <string name="my_site_btn_view_admin">Προβολή Διαχείρισης</string>
+ <string name="my_site_btn_view_site">Προβολή ιστότοπου</string>
+ <string name="site_picker_title">Επιλογή ιστότοπου</string>
+ <string name="my_site_btn_switch_site">Αλλαγή Ιστότοπου</string>
+ <string name="my_site_btn_blog_posts">Ιστολόγημα</string>
+ <string name="my_site_btn_site_settings">Ρυθμίσεις</string>
+ <string name="my_site_header_look_and_feel">Κοίτα και Νιώσε</string>
+ <string name="my_site_header_publish">Δημοσιεύω</string>
+ <string name="my_site_header_configuration">Ρυθμίσεις</string>
+ <string name="reader_label_new_posts_subtitle">Πατήστε για να τα δείτε</string>
+ <string name="notifications_account_required">Συνδεθείτε στο WordPress.com για ειδοποιήσεις</string>
+ <string name="stats_unknown_author">Άγνωστος Συγγραφέας</string>
+ <string name="image_added">Η εικόνα προστέθηκε</string>
+ <string name="signout">Αποσύνδεση</string>
+ <string name="deselect_all">Αποεπιλογή όλων</string>
+ <string name="show">Προβολή</string>
+ <string name="hide">Απόκρυψη</string>
+ <string name="select_all">Επιλογή όλων</string>
+ <string name="sign_out_wpcom_confirm">Η αποσύνδεση του λογαριασμού σας θα αφαιρέσει όλα τα δεδομένα του WordPress.com του @%s από αυτή τη συσκευή, συμπεριλαμβανομένων των τοπικών προσχεδίων και των τοπικών αλλαγών.</string>
+ <string name="select_from_new_picker">Πολλαπλή επιλογή με το νέο επιλογέα</string>
+ <string name="stats_generic_error">Τα Στατιστικά που ζητήθηκαν δεν μπόρεσαν να φορτωθούν</string>
+ <string name="no_device_videos">Δεν υπάρχουν βίντεο</string>
+ <string name="no_blog_images">Δεν υπάρχουν φωτογραφίες</string>
+ <string name="no_blog_videos">Δεν υπάρχουν βίντεο</string>
+ <string name="no_device_images">Δεν υπάρχουν φωτογραφίες</string>
+ <string name="error_loading_blog_images">Αδυναμία λήψης εικόνων</string>
+ <string name="error_loading_blog_videos">Αδυναμία λήψης βίντεο</string>
+ <string name="error_loading_images">Σφάλμα κατά τη φόρτωση εικόνων</string>
+ <string name="error_loading_videos">Σφάλμα κατά τη φόρτωση βίντεο</string>
+ <string name="loading_blog_images">Λήψη εικόνων</string>
+ <string name="loading_blog_videos">Λήψη βίντεο</string>
+ <string name="no_media_sources">Αδυναμία λήψης πολυμέσων</string>
+ <string name="loading_videos">Φόρτωση βίντεο</string>
+ <string name="loading_images">Φόρτωση εικόνων</string>
+ <string name="no_media">Δεν βρέθηκαν πολυμέσα</string>
+ <string name="device">Συσκευή</string>
+ <string name="language">Γλώσσα</string>
+ <string name="add_to_post">Προσθήκη στο Άρθρο</string>
+ <string name="media_picker_title">Επιλογή μέσων</string>
+ <string name="take_photo">Βγάλε μια φωτογραφία</string>
+ <string name="take_video">Βγάλε ένα βίντεο</string>
+ <string name="tab_title_device_images">Εικόνες Συσκευής</string>
+ <string name="tab_title_device_videos">Βίντεο Συσκευής</string>
+ <string name="tab_title_site_images">Εικόνες Ιστότοπου</string>
+ <string name="tab_title_site_videos">Βίντεο Ιστότοπου</string>
+ <string name="media_details_label_file_name">Όνομα αρχείου</string>
+ <string name="media_details_label_file_type">Τύπος αρχείου</string>
+ <string name="error_publish_no_network">Αδυναμία δημοσίευσης ενώ δεν υπάρχει σύνδεση. Αποθηκεύτηκε ως προσχέδιο.</string>
+ <string name="editor_toast_invalid_path">Μη έγκυρη διαδρομή αρχείου</string>
+ <string name="verification_code">Κωδικός επαλήθευσης</string>
+ <string name="invalid_verification_code">Μη έγκυρος κωδικός επαλήθευσης</string>
+ <string name="verify">Επαλήθευση</string>
+ <string name="two_step_footer_label">Εισάγετε τον κωδικό από την εφαρμογή σας πιστοποίησης.</string>
+ <string name="two_step_footer_button">Αποστολή κώδικα μέσω μηνύματος κειμένου</string>
+ <string name="two_step_sms_sent">Ελέγξτε τα μηνύματα κειμένου σας για τον κωδικό επαλήθευσης.</string>
+ <string name="sign_in_jetpack">Συνδεθείτε στο λογαριασμό σας του WordPress.com για να συνδεθείτε στο Jetpack.</string>
+ <string name="auth_required">Συνδεθείτε ξανά για να συνεχίσετε.</string>
+ <string name="reader_empty_posts_request_failed">Αδυναμία φόρτωσης των άρθρων</string>
+ <string name="publisher">Εκδότης:</string>
+ <string name="error_notification_open">Αδύνατον να ανοιχτεί η ειδοποίηση</string>
+ <string name="stats_followers_total_email_paged">Κατάδειξη %1$d - %2$d από %3$s Ακόλουθοι Email</string>
+ <string name="stats_search_terms_unknown_search_terms">Άγνωστοι όροι αναζήτησης</string>
+ <string name="stats_followers_total_wpcom_paged">Κατάδειξη %1$d - %2$d από %3$s Ακόλουθοι WordPress.com</string>
+ <string name="stats_empty_search_terms_desc">Μάθετε περισσότερα για την κίνηση στην αναζήτηση σας εξετάζοντας τους όρους αναζήτησης που χρησιμοποίησαν οι χρήστες για να βρούν το site σας.</string>
+ <string name="stats_empty_search_terms">Δεν καταχωρήθηκαν όροι αναζήτησης</string>
+ <string name="stats_entry_search_terms">Όρος αναζήτησης</string>
+ <string name="stats_view_authors">Συγγραφείς</string>
+ <string name="stats_view_search_terms">Όροι αναζήτησης</string>
+ <string name="comments_fetching">Ανάκτηση σχολίων...</string>
+ <string name="pages_fetching">Ανάκτηση σελίδων...</string>
+ <string name="toast_err_post_uploading">Αδύνατο το άνοιγμα το άρθρου όσο αυτό φορτώνει</string>
+ <string name="posts_fetching">Λήψη άρθρων...</string>
+ <string name="media_fetching">Λήψη πολυμέσων...</string>
+ <string name="post_uploading">Μεταφόρτωση</string>
+ <string name="stats_total">Σύνολα</string>
+ <string name="stats_overall">Συνολικά</string>
+ <string name="stats_period">Περίοδος</string>
+ <string name="logs_copied_to_clipboard">Τα αρχεία καταγραφής της εφαρμογής έχουν αντιγραφεί στο πρόχειρο</string>
+ <string name="reader_label_new_posts">Νέα άρθρα</string>
+ <string name="reader_empty_posts_in_blog">Αυτό το ιστολόγιο είναι κενό</string>
+ <string name="stats_average_per_day">Μέσος όρος ανά Ημέρα</string>
+ <string name="stats_recent_weeks">Πρόσφατες Εβδομάδες</string>
+ <string name="error_copy_to_clipboard">Προέκυψε σφάλμα κατά την αντιγραφή κειμένου στο πρόχειρο</string>
+ <string name="reader_page_recommended_blogs">Ιστολόγια που μπορεί να σας αρέσουν</string>
+ <string name="stats_months_and_years">Μήνες και Χρόνια</string>
+ <string name="themes_fetching">Λήψη θεμάτων...</string>
+ <string name="stats_for">Στατιστικά για %s</string>
+ <string name="stats_other_recent_stats_label">Άλλα Πρόσφατα Στατιστικά</string>
+ <string name="stats_view_all">Προβολή όλων</string>
+ <string name="stats_view">Προβολή</string>
+ <string name="stats_followers_months">%1$d μήνες</string>
+ <string name="stats_followers_a_year">Ένας χρόνος</string>
+ <string name="stats_followers_years">%1$d χρόνια</string>
+ <string name="stats_followers_a_month">Ένας μήνας</string>
+ <string name="stats_followers_minutes">%1$d λεπτά</string>
+ <string name="stats_followers_an_hour_ago">πριν μια ώρα</string>
+ <string name="stats_followers_hours">%1$d ώρες</string>
+ <string name="stats_followers_a_day">Μια ημέρα</string>
+ <string name="stats_followers_days">%1$d ημέρες</string>
+ <string name="stats_followers_a_minute_ago">πριν από ένα λεπτό</string>
+ <string name="stats_followers_seconds_ago">δευτερόλεπτα πρίν</string>
+ <string name="stats_followers_total_email">Συνολικά Ακολουθούν τα Email: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Συνολικοί Ακόλουθοι στο WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Συνολικά άρθρα με ακόλουθους σχολίων: %1$s</string>
+ <string name="stats_comments_by_authors">Κατά Συγγραφέα</string>
+ <string name="stats_comments_by_posts_and_pages">Κατά Άρθρο &amp; Σελίδα</string>
+ <string name="stats_empty_followers_desc">Παρακολουθήστε το συνολικό αριθμό ακολούθων σας καθώς και πόσο καιρό καθένας παρακολουθεί τον ιστότοπό σας.</string>
+ <string name="stats_empty_followers">Δεν υπάρχουν ακόλουθοι</string>
+ <string name="stats_empty_publicize_desc">Παρακολουθήστε τους ακολούθους σας από διάφορες υπηρεσίες κοινωνικών δικτύων με χρήση της δημοσιοποίησης.</string>
+ <string name="stats_empty_publicize">Δεν έχουν καταγραφεί ακόλουθοι δημοσιοποίησης</string>
+ <string name="stats_empty_video">Δεν έχουν αναπαραχθεί βίντεο</string>
+ <string name="stats_empty_video_desc">Αν έχετε μεταφορτώσεις βίντεο με το VideoPress, βρείτε πόσες φορές τα έχουν παρακολουθήσει.</string>
+ <string name="stats_empty_comments_desc">Αν επιτρέπετε σχόλια στην ιστοσελίδα σας, παρακολουθήστε τους κορυφαίους σχολιαστές και ανακαλύψτε τι περιεχόμενο προκαλεί τις πιο ζωηρές συζητήσεις, με βάση τα 1.000 πιο πρόσφατα σχόλια.</string>
+ <string name="stats_empty_tags_and_categories_desc">Λάβετε μια επισκόπηση των πιο δημοφιλών θεμάτων στον ιστότοπό σας όπως απεικονίζονται στα κορυφαία σας άρθρα της προηγούμενης εβδομάδας.</string>
+ <string name="stats_empty_top_authors_desc">Παρακολουθήστε τις προβολές των άρθρων κάθε συμμετέχοντα και εστιάστε για να ανακαλύψετε το πιο δημοφιλές περιεχόμενο κάθε συγγραφέα.</string>
+ <string name="stats_empty_tags_and_categories">Δεν προβλήθηκαν άρθρα ή σελίδες με ετικέτες</string>
+ <string name="stats_empty_clicks_desc">Όταν το περιεχόμενό σας περιλαμβάνει συνδέσμους σε άλλους ιστοτόπους, θα βλέπετε ποιους χρησιμοποιούν περισσότερο οι επισκέπτες σας.</string>
+ <string name="stats_empty_referrers_desc">Μάθετε περισσότερα για την ορατότητα της ιστοσελίδας σας βλέποντας τους ιστότοπους και τις μηχανές αναζήτησης που σας στέλνουν την περισσότερη κίνηση.</string>
+ <string name="stats_empty_clicks_title">Δεν καταγράφηκαν κλικ</string>
+ <string name="stats_empty_referrers_title">Δεν καταγράφηκαν αναφορείς</string>
+ <string name="stats_empty_top_posts_title">Δεν προβλήθηκαν άρθρα ή σελίδες</string>
+ <string name="stats_empty_top_posts_desc">Ανακαλύψτε ποιο περιεχόμενό σας προβάλλεται περισσότερο και δείτε πως συμπεριφέρονται διαχρονικά μεμονωμένα άρθρα και σελίδες.</string>
+ <string name="stats_totals_followers">Από</string>
+ <string name="stats_empty_geoviews">Δεν καταγράφηκαν χώρες</string>
+ <string name="stats_empty_geoviews_desc">Εξερευνήστε τη λίστα για να δείτε ποιες χώρες και περιοχές δημιουργούν την περισσότερη κυκλοφορία στον ιστότοπό σας.</string>
+ <string name="stats_entry_video_plays">Βίντεο</string>
+ <string name="stats_entry_top_commenter">Συγγραφέας</string>
+ <string name="stats_entry_publicize">Υπηρεσία</string>
+ <string name="stats_entry_followers">Ακόλουθος</string>
+ <string name="stats_totals_publicize">Ακόλουθοι</string>
+ <string name="stats_entry_clicks_link">Σύνδεσμος</string>
+ <string name="stats_view_top_posts_and_pages">Άρθρα &amp; Σελίδες</string>
+ <string name="stats_view_videos">Βίντεο</string>
+ <string name="stats_view_publicize">Δημοσιοποίηση</string>
+ <string name="stats_view_followers">Ακόλουθοι</string>
+ <string name="stats_view_countries">Χώρες</string>
+ <string name="stats_likes">Likes</string>
+ <string name="stats_pagination_label">Σελίδα %1$s από %2$s</string>
+ <string name="stats_timeframe_years">Χρόνια</string>
+ <string name="stats_views">Προβολές</string>
+ <string name="stats_visitors">Επισκέπτες</string>
+ <string name="ssl_certificate_details">Λεπτομέρειες</string>
+ <string name="delete_sure_post">Διαγραφή αυτής της ανάρτησης</string>
+ <string name="delete_sure">Διαγραφή αυτού του προσχεδίου</string>
+ <string name="delete_sure_page">Διαγραφή της σελίδας</string>
+ <string name="confirm_delete_multi_media">Διαγραφή επιλεγμένων αντικειμένων;</string>
+ <string name="confirm_delete_media">Διαγραφή επιλεγμένου αντικειμένου;</string>
+ <string name="cab_selected">%d επιλεγμένα</string>
+ <string name="media_gallery_date_range">Εμφάνιση πολυμέσων από %1$s εώς %2$s</string>
+ <string name="sure_to_remove_account">Αφαίρεση αυτού του ιστότοπου;</string>
+ <string name="reader_empty_followed_blogs_title">Δεν ακολουθείτε ιστολόγια ακόμη</string>
+ <string name="reader_empty_posts_liked">Δεν έχετε κάνει "like" σε άρθρα</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Περιηγηθείτε στις Συχνές Ερωτήσεις</string>
+ <string name="nux_help_description">Επισκεφθείτε το κέντρο βοήθειας για να βρείτε απαντήσεις σε κοινές ερωτήσεις ή επισκεφθείτε τα φόρουμ για να ρωτήσετε καινούριες</string>
+ <string name="agree_terms_of_service">Δημιουργώντας λογαριασμό, συμφωνείτε με τους υπέροχους %1$sΌρους της Υπηρεσίας%2$s</string>
+ <string name="create_new_blog_wpcom">Δημιουργίας ιστολογίου στο WordPress.com</string>
+ <string name="new_blog_wpcom_created">Το ιστολόγιο στο WordPress.com δημιουργήθηκε!</string>
+ <string name="reader_empty_comments">Δεν υπάρχουν σχόλια ακόμη</string>
+ <string name="reader_empty_posts_in_tag">Δεν υπάρχουν άρθρα με αυτή την ετικέτα</string>
+ <string name="reader_label_comment_count_multi">%,d σχόλια</string>
+ <string name="reader_label_view_original">Προβολή αρχικού άρθρου</string>
+ <string name="reader_label_like">Like</string>
+ <string name="reader_label_comment_count_single">Ένα σχόλιο</string>
+ <string name="reader_label_comments_closed">Τα σχόλια είναι κλειστά</string>
+ <string name="reader_label_comments_on">Σχόλια ανοιχτά</string>
+ <string name="reader_title_photo_viewer">%1$d από %2$d</string>
+ <string name="error_publish_empty_post">Δεν μπορείτε να δημοσιεύσετε ένα κενό άρθρο</string>
+ <string name="error_refresh_unauthorized_posts">Δεν έχετε άδεια για να δείτε ή να επεξεργαστείτε τα άρθρα</string>
+ <string name="error_refresh_unauthorized_pages">Δεν έχετε άδεια για να δείτε ή να επεξεργαστείτε τις σελίδες</string>
+ <string name="error_refresh_unauthorized_comments">Δεν έχετε άδεια για να δείτε ή να επεξεργαστείτε τα σχόλια</string>
+ <string name="older_month">Παλαιότερο από ένα μήνα</string>
+ <string name="more">Περισσότερα</string>
+ <string name="older_two_days">Παλιότερο από 2 μέρες</string>
+ <string name="older_last_week">Παλιότερο από 1 βδομάδα</string>
+ <string name="stats_no_blog">Αδυναμία φόρτωσης στατιστικών για το ιστολόγιο αυτό</string>
+ <string name="select_a_blog">Επιλέξτε ένα ιστότοπο του WordPress</string>
+ <string name="sending_content">Μεταφόρτωση %s περιεχομένου</string>
+ <string name="uploading_total">Μεταφόρτωση %1$d από %2$d</string>
+ <string name="mnu_comment_liked">Liked</string>
+ <string name="comment">Σχόλιο</string>
+ <string name="comment_trashed">Το σχόλιο διαγράφηκε</string>
+ <string name="posts_empty_list">Δεν υπάρχουν άρθρα ακόμη. Γιατί να μη δημιουργήσετε ένα;</string>
+ <string name="comment_reply_to_user">Απάντηση σε %s</string>
+ <string name="pages_empty_list">Δεν υπάρχουν σελίδες ακόμη. Γιατί να μη δημιουργήσετε μία;</string>
+ <string name="media_empty_list_custom_date">Δεν υπάρχουν πολυμέσα σε αυτό το χρονικό διάστημα</string>
+ <string name="posting_post">Δημοσίευση "%s"</string>
+ <string name="signing_out">Αποσύνδεση...</string>
+ <string name="reader_toast_err_generic">Αδυναμία εκτέλεσης αυτής της ενέργειας</string>
+ <string name="reader_toast_err_block_blog">Αδυναμία μπλοκαρίσματος αυτού του ιστολογίου</string>
+ <string name="reader_toast_blog_blocked">Δεν εμφανίζονται πλέον άρθρα από αυτό το ιστολόγιο</string>
+ <string name="reader_menu_block_blog">Μπλοκάρισμα αυτού του ιστολογίου</string>
+ <string name="contact_us">Επικοινωνήστε μαζί μας</string>
+ <string name="hs__conversation_detail_error">Περιγράψτε το πρόβλημα που βλέπετε</string>
+ <string name="hs__new_conversation_header">Συζήτηση υποστήριξης</string>
+ <string name="hs__conversation_header">Συζήτηση υποστήριξης</string>
+ <string name="hs__username_blank_error">Συμπληρώστε ένα έγκυρο όνομα</string>
+ <string name="hs__invalid_email_error">Εισάγετε μία έγκυρη διεύθυνση email</string>
+ <string name="add_location">Προσθέστε περιοχή</string>
+ <string name="current_location">Τωρινή τοποθεσία</string>
+ <string name="search_location">Αναζήτηση</string>
+ <string name="edit_location">Επεξεργασία</string>
+ <string name="search_current_location">Εντοπισμός</string>
+ <string name="preference_send_usage_stats">Αποστολή στατιστικών</string>
+ <string name="preference_send_usage_stats_summary">Αυτόματη αποστολή στατιστικών στοιχείων χρήσης ώστε να μας βοηθήσετε να βελτιώσουμε το WordPress για το Android</string>
+ <string name="update_verb">Ενημέρωση</string>
+ <string name="schedule_verb">Προγραμματισμός</string>
+ <string name="reader_title_blog_preview">Προεπισκόπηση Ιστολογίου</string>
+ <string name="reader_title_tag_preview">Προεπισκόπηση Ετικέτας</string>
+ <string name="reader_title_subs">Ετικέτες &amp; Ιστολόγια</string>
+ <string name="reader_page_followed_tags">Παρακολουθούμενες ετικέτες</string>
+ <string name="reader_page_followed_blogs">Παρακολουθούμενα ιστολόγια</string>
+ <string name="reader_hint_add_tag_or_url">Εισάγετε μια ετικέτα ή URL για παρακολούθηση</string>
+ <string name="reader_label_followed_blog">Παρακολουθούμενο ιστολόγιο</string>
+ <string name="reader_label_tag_preview">Άρθρα με ετικέτα %s</string>
+ <string name="reader_toast_err_get_blog_info">Αδυναμία εμφάνισης αυτού του ιστολογίου</string>
+ <string name="reader_toast_err_already_follow_blog">Ακολουθείτε ήδη αυτό το ιστολόγιο</string>
+ <string name="reader_toast_err_follow_blog">Αδυναμία παρακολούθησης αυτού του ιστολογίου</string>
+ <string name="reader_toast_err_unfollow_blog">Αδυναμία διακοπής παρακολούθησης αυτού του ιστολογίου</string>
+ <string name="reader_empty_recommended_blogs">Δεν υπάρχουν προτεινόμενα ιστολόγια</string>
+ <string name="saving">Αποθήκευση...</string>
+ <string name="media_empty_list">Δεν υπάρχουν πολυμέσα</string>
+ <string name="ptr_tip_message">Συμβουλή: Τραβήξτε κάτω για ανανέωση</string>
+ <string name="help">Βοήθεια</string>
+ <string name="forgot_password">Έχασες τον κωδικό σου;</string>
+ <string name="forums">Συζητήσεις</string>
+ <string name="help_center">Κέντρο βοήθειας</string>
+ <string name="ssl_certificate_error">Μη έγκυρο πιστοποιητικό SSL</string>
+ <string name="ssl_certificate_ask_trust">Αν συνήθως δεν έχετε προβλήματα σύνδεσης σε αυτό τον ιστότοπο, αυτό το σφάλμα θα μπορούσε να σημαίνει ότι κάποιος προσπαθεί να μιμηθεί τον ιστότοπο και δεν θα πρέπει να συνεχίσετε. Θα θέλατε να εμπιστευτείτε το πιστοποιητικό όπως και να\'χει;</string>
+ <string name="out_of_memory">Η μνήμη της συσκευής εξαντλήθηκε</string>
+ <string name="no_network_message">Δεν υπάρχει διαθέσιμο δίκτυο</string>
+ <string name="could_not_remove_account">Η αφαίρεση της ιστοσελίδας δεν ήταν δυνατή</string>
+ <string name="gallery_error">Αδυναμία λήψης πολυμέσου</string>
+ <string name="blog_not_found">Προέκυψε σφάλμα κατά την πρόσβαση σε αυτό το ιστολόγιο</string>
+ <string name="wait_until_upload_completes">Περιμένετε μέχρι την ολοκλήρωση της μεταφόρτωσης</string>
+ <string name="theme_fetch_failed">Αποτυχία λήψης θεμάτων</string>
+ <string name="theme_set_failed">Αποτυχία ορισμού θέματος</string>
+ <string name="theme_auth_error_message">Βεβαιωθείτε ότι έχετε δικαίωμα ορισμού θεμάτων</string>
+ <string name="comments_empty_list">Δεν υπάρχουν σχόλια</string>
+ <string name="mnu_comment_unspam">Δεν είναι spam</string>
+ <string name="no_site_error">Αδυναμία σύνδεσης στον ιστότοπο του WordPress</string>
+ <string name="adding_cat_failed">Αποτυχία προσθήκης κατηγορίας</string>
+ <string name="adding_cat_success">Η κατηγορία προστέθηκε επιτυχώς</string>
+ <string name="cat_name_required">Απαιτείται το πεδίο ονόματος κατηγορίας</string>
+ <string name="category_automatically_renamed">Το όνομα κατηγορίας %1$s δεν είναι έγκυρο. Έχει μετονομαστεί σε %2$s.</string>
+ <string name="no_account">Δεν βρέθηκε λογαριασμός WordPress, προσθέστε λογαριασμό και δοκιμάστε ξανά</string>
+ <string name="sdcard_message">Απαιτείται προσαρτημένη κάρτα SD για τη μεταφόρτωση πολυμέσων</string>
+ <string name="stats_empty_comments">Δεν υπάρχουν σχόλια ακόμη</string>
+ <string name="stats_bar_graph_empty">Δε υπάρχουν διαθέσιμα στατιστικά</string>
+ <string name="invalid_url_message">Ελέγξτε αν το URL που δώσατε είναι έγκυρο</string>
+ <string name="reply_failed">Αποτυχία απάντησης</string>
+ <string name="notifications_empty_list">Δεν υπάρχουν ειδοποιήσεις</string>
+ <string name="error_delete_post">Προέκυψε σφάλμα κατά τη διαγραφή του %s</string>
+ <string name="error_refresh_posts">Αδυναμία ανανέωσης άρθρων αυτή τη στιγμή</string>
+ <string name="error_refresh_pages">Αδυναμία ανανέωσης σελίδων αυτή τη στιγμή</string>
+ <string name="error_refresh_notifications">Αδυναμία ανανέωσης ειδοποιήσεων αυτή τη στιγμή</string>
+ <string name="error_refresh_comments">Αδυναμία ανανέωσης σχολίων αυτή τη στιγμή</string>
+ <string name="error_refresh_stats">Αδυναμία ανανέωσης στατιστικών αυτή τη στιγμή</string>
+ <string name="error_generic">Προέκυψε σφάλμα</string>
+ <string name="error_moderate_comment">Προέκυψε σφάλμα κατά τη διαχείριση</string>
+ <string name="error_edit_comment">Προέκυψε σφάλμα κατά την επεξεργασία του σχολίου</string>
+ <string name="error_upload">Προέκυψε σφάλμα κατά τη μεταφόρτωση του %s</string>
+ <string name="error_load_comment">Αδυναμία φόρτωσης του σχολίου</string>
+ <string name="error_downloading_image">Σφάλμα κατά την ανάκτηση της εικόνας</string>
+ <string name="passcode_wrong_passcode">Λάθος PIN</string>
+ <string name="invalid_email_message">Η διεύθυνση email σας δεν είναι έγκυρη</string>
+ <string name="invalid_password_message">Το συνθηματικό πρέπει να περιέχει τουλάχιστον 4 χαρακτήρες</string>
+ <string name="invalid_username_too_short">Το όνομα χρήστη πρέπει να είναι μεγαλύτερο από 4 χαρακτήρες</string>
+ <string name="invalid_username_too_long">Το όνομα χρήστη πρέπει να είναι μικρότερο από 61 χαρακτήρες</string>
+ <string name="username_only_lowercase_letters_and_numbers">Το όνομα χρήστη μπορεί να περιέχει μόνο πεζούς χαρακτήρες (a-z) και αριθμούς</string>
+ <string name="username_required">Εισάγετε ένα όνομα χρήστη</string>
+ <string name="username_not_allowed">Το όνομα χρήστη δεν επιτρέπεται</string>
+ <string name="username_must_be_at_least_four_characters">Το όνομα χρήστη πρέπει να είναι τουλάχιστον 4 χαρακτήρες</string>
+ <string name="username_contains_invalid_characters">Το όνομα χρήστη δεν μπορεί να περιέχει το χαρακτήρα “_”</string>
+ <string name="username_must_include_letters">Όνομα Χρήστη πρέπει να έχει τουλάχιστον 1 γράμμα (a-z)</string>
+ <string name="email_invalid">Εισάγετε μια έγκυρη διεύθυνση email</string>
+ <string name="email_not_allowed">Αυτή η διεύθυνση email δεν επιτρέπεται</string>
+ <string name="username_exists">Αυτό το όνομα χρήστη υπάρχει ήδη</string>
+ <string name="email_exists">Αυτή η διεύθυνση email χρησιμοποιείται ήδη</string>
+ <string name="username_reserved_but_may_be_available">Αυτό το όνομα χρήστη είναι δεσμευμένο αυτή τη στιγμή αλλά μπορεί να είναι διαθέσιμο σε μερικές ημέρες</string>
+ <string name="blog_name_required">Πληκτρολογήστε μια διεύθυνση ιστότοπου</string>
+ <string name="blog_name_not_allowed">Αυτή η διεύθυνση ιστότοπου δεν επιτρέπεται</string>
+ <string name="blog_name_must_be_at_least_four_characters">Η διεύθυνση ιστότοπου πρέπει να είναι τουλάχιστον 4 χαρακτήρες</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Η διεύθυνση του ιστοτόπου πρέπει να είναι μικρότερη από 64 χαρακτήρες</string>
+ <string name="blog_name_contains_invalid_characters">Η διεύθυνση του ιστοτόπου δεν μπορεί να περιέχει το χαρακτήρα “_”</string>
+ <string name="blog_name_cant_be_used">Δεν μπορείτε να χρησιμοποιήσετε αυτή τη διεύθυνση ιστοτόπου</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Η διεύθυνση ιστοτόπου μπορεί να περιέχει μόνο πεζά γράμματα (a-z) και αριθμούς</string>
+ <string name="blog_name_exists">Αυτός ο ιστότοπος υπάρχει ήδη</string>
+ <string name="blog_name_reserved">Αυτός ο ιστότοπος είναι δεσμευμένος</string>
+ <string name="blog_name_reserved_but_may_be_available">Αυτός ο ιστότοπος είναι δεσμευμένος αυτή τη στιγμή αλλά μπορεί να είναι διαθέσιμος σε μερικές ημέρες</string>
+ <string name="username_or_password_incorrect">Το όνομα χρήστη ή ο κωδικός που δώσατε είναι εσφαλμένα</string>
+ <string name="nux_cannot_log_in">Δεν μπορούμε να σας συνδέσουμε</string>
+ <string name="xmlrpc_error">Αδυναμία σύνδεσης. Εισάγετε την πλήρη διαδρομή του αρχείου xmlrpc.php στον ιστότοπό σας και δοκιμάστε ξανά.</string>
+ <string name="select_categories">Επιλογή κατηγοριών</string>
+ <string name="account_details">Λεπτομέρειες λογαριασμού</string>
+ <string name="edit_post">Επεξεργασία ανάρτησης</string>
+ <string name="add_comment">Προσθήκη σχόλιου</string>
+ <string name="connection_error">Σφάλμα σύνδεσης</string>
+ <string name="cancel_edit">Ακύρωση επεξεργασίας</string>
+ <string name="scaled_image_error">Εισάγετε μια έγκυρη τιμή κλιμάκωσης πλάτους</string>
+ <string name="post_not_found">Προέκυψε σφάλμα κατά τη φόρτωση του άρθρου. Ανανεώστε τα άρθρα σας και δοκιμάστε ξανά.</string>
+ <string name="learn_more">Μάθετε περισσότερα</string>
+ <string name="media_gallery_settings_title">Ρυθμίσεις Βιβλιοθήκης</string>
+ <string name="media_gallery_image_order">Σειρά εικόνων</string>
+ <string name="media_gallery_num_columns">Αριθμός στηλών</string>
+ <string name="media_gallery_type_thumbnail_grid">Πλέγμα μικρογραφιών</string>
+ <string name="media_gallery_edit">Επεξεργασία της γκαλερί</string>
+ <string name="media_error_no_permission">Δεν έχετε δικαίωμα προβολής της βιβλιοθήκης πολυμέσων</string>
+ <string name="cannot_delete_multi_media_items">Μερικά πολυμέσα δεν ήταν δυνατόν να διαγραφούν αυτή τη στιγμή. Δοκιμάστε ξανά αργότερα.</string>
+ <string name="themes_live_preview">Ζωντανή προεπισκόπηση</string>
+ <string name="theme_current_theme">Τρέχον θέμα</string>
+ <string name="theme_premium_theme">Premium θέμα</string>
+ <string name="link_enter_url_text">Κείμενο συνδέσμου (προαιρετικά)</string>
+ <string name="create_a_link">Δημιουργία συνδέσμου</string>
+ <string name="page_settings">Ρυθμίσεις σελίδας</string>
+ <string name="local_draft">Τοπικό προσχέδιο</string>
+ <string name="upload_failed">Αποτυχία μεταφόρτωσης</string>
+ <string name="horizontal_alignment">Οριζόντια ευθυγράμμιση</string>
+ <string name="file_not_found">Αδυναμία εύρεσης του αρχείου πολυμέσων για μεταφόρτωση. Μήπως διαγράφηκε ή μετακινήθηκε;</string>
+ <string name="post_settings">Ρυθμίσεις άρθρου</string>
+ <string name="delete_post">Διαγραφή ανάρτησης</string>
+ <string name="delete_page">Διαγραφή σελίδας</string>
+ <string name="comment_status_approved">Εγκεκριμένα</string>
+ <string name="comment_status_unapproved">Σε εκκρεμότητα</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Διαγραμμένα</string>
+ <string name="edit_comment">Επεξεργασία σχόλιου</string>
+ <string name="mnu_comment_approve">Έγκριση</string>
+ <string name="mnu_comment_unapprove">Αποέγκριση</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Σκουπίδια</string>
+ <string name="dlg_approving_comments">Έγκριση</string>
+ <string name="dlg_unapproving_comments">Αποέγκριση</string>
+ <string name="dlg_spamming_comments">Σήμανση ως spam</string>
+ <string name="dlg_trashing_comments">Αποστολή στα διαγραμμένα</string>
+ <string name="dlg_confirm_trash_comments">Αποστολή στα σκουπίδια;</string>
+ <string name="trash_yes">Διεγραμμένα</string>
+ <string name="trash_no">Να μη διαγραφεί</string>
+ <string name="trash">Διαγραφή</string>
+ <string name="author_name">Όνομα συγγραφέα</string>
+ <string name="author_email">Email συγγραφέα</string>
+ <string name="author_url">URL συγγραφέα</string>
+ <string name="hint_comment_content">Σχόλιο</string>
+ <string name="saving_changes">Αποθήκευση αλλαγών</string>
+ <string name="sure_to_cancel_edit_comment">Ακύρωση επεξεργασίας αυτού του σχολίου;</string>
+ <string name="content_required">Απαιτείται σχόλιο</string>
+ <string name="toast_comment_unedited">Το σχόλιο δεν έχει αλλάξει</string>
+ <string name="remove_account">Αφαίρεση ιστότοπου</string>
+ <string name="blog_removed_successfully">Ο ιστότοπος αφαιρέθηκε επιτυχώς</string>
+ <string name="delete_draft">Διαγραφή προσχέδιου</string>
+ <string name="preview_page">Προεπισκόπηση σελίδας</string>
+ <string name="preview_post">Προεπισκόπηση άρθρου</string>
+ <string name="comment_added">Το σχόλιο προστέθηκε επιτυχώς</string>
+ <string name="post_not_published">Η κατάσταση του άρθρου δεν είναι δημοσιευμένη</string>
+ <string name="page_not_published">Η κατάσταση σελίδας δεν είναι δημοσιευμένη</string>
+ <string name="view_in_browser">Προβολή στον περιηγητή</string>
+ <string name="add_new_category">Προσθήκη νέας κατηγορίας</string>
+ <string name="category_name">Όνομα κατηγορίας</string>
+ <string name="category_slug">Σύντομο όνομα κατηγορίας (προαιρετικό)</string>
+ <string name="category_desc">Περιγραφή κατηγορίας (προαιρετικό)</string>
+ <string name="category_parent">Γονική κατηγορία (προαιρετικό)</string>
+ <string name="share_action_post">Νέα ανάρτηση</string>
+ <string name="share_action_media">Βιβλιοθήκη πολυμέσων</string>
+ <string name="file_error_create">Αδυναμία δημιουργίας προσωρινού αρχείου για μεταφόρτωση πολυμέσων. Βεβαιωθείτε ότι υπάρχει αρκετός ελεύθερος χώρος στη συσκευή σας.</string>
+ <string name="location_not_found">Άγνωστη τοποθεσία</string>
+ <string name="open_source_licenses">Άδειες ανοιχτού κώδικα</string>
+ <string name="invalid_site_url_message">Βεβαιωθείτε ότι το URL του ιστότοπου που δώσατε είναι έγκυρο</string>
+ <string name="pending_review">Εκκρεμής αναθεώρηση</string>
+ <string name="http_credentials">Πιστοποίηση HTTP (προαιρετικό)</string>
+ <string name="http_authorization_required">Απαιτείται εξουσιοδότηση</string>
+ <string name="post_format">Μορφή άρθρου</string>
+ <string name="notifications_empty_all">Καμία ειδοποίηση...ακόμα.</string>
+ <string name="new_post">Νέα ανάρτηση</string>
+ <string name="new_media">Νέο πολυμέσο</string>
+ <string name="view_site">Προβολή ιστότοπου</string>
+ <string name="privacy_policy">Πολιτική απορρήτου</string>
+ <string name="local_changes">Τοπικές αλλαγές</string>
+ <string name="image_settings">Ρυθμίσεις εικόνας</string>
+ <string name="add_account_blog_url">Διεύθυνση ιστολογίου</string>
+ <string name="wordpress_blog">Ιστολόγιο WordPress</string>
+ <string name="error_blog_hidden">Αυτό το ιστολόγιο είναι κρυφό και δεν μπόρεσε να φορτωθεί. Ενεργοποιήστε το ξανά στις ρυθμίσεις και δοκιμάστε ξανά.</string>
+ <string name="fatal_db_error">Προέκυψε σφάλμα κατά τη δημιουργία της βάσης δεδομένων της εφαρμογής. Δοκιμάστε να την εγκαταστήσετε ξανά.</string>
+ <string name="jetpack_message_not_admin">Το πρόσθετο Jetpack απαιτείται για τα στατιστικά. Επικοινωνήστε με το διαχειριστή του ιστοτόπου.</string>
+ <string name="reader_title_applog">Αρχείο καταγραφής εφαρμογής</string>
+ <string name="reader_share_link">Μοιραστείτε το σύνδεσμο</string>
+ <string name="reader_toast_err_add_tag">Αδυναμία προσθήκης αυτής της ετικέτας</string>
+ <string name="reader_toast_err_remove_tag">Αδυναμία αφαίρεσης αυτής της ετικέτας</string>
+ <string name="required_field">Υποχρεωτικό πεδίο</string>
+ <string name="email_hint">Διεύθυνση email</string>
+ <string name="site_address">Η self-hosted διεύθυνσή σας (URL)</string>
+ <string name="email_cant_be_used_to_signup">Δεν μπορείτε να χρησιμοποιήσετε αυτή τη διεύθυνση email για να συνδεθείτε. Έχουμε προβλήματα με αυτούς γιατί μπλοκάρουν μερικά από τα email μας. Χρησιμοποιήστε άλλο πάροχο email.</string>
+ <string name="email_reserved">Αυτή η διεύθυνση ηλ. ταχυδρομείου χρησιμοποιείται ήδη. Ελέγξτε τα εισερχόμενά σας για email ενεργοποίησης. Αν δεν ενεργοποιήσετε μπορείτε να δοκιμάσετε ξανά σε λίγες μέρες.</string>
+ <string name="blog_name_must_include_letters">Η διεύθυνση του ιστότοπου πρέπει να περιέχει τουλάχιστον 1 γράμμα (a-z)</string>
+ <string name="blog_name_invalid">Μη έγκυρη διεύθυνση ιστότοπου</string>
+ <string name="blog_title_invalid">Μη έγκυρος τίτλος ιστότοπου</string>
+ <string name="deleting_page">Γίνεται διαγραφή σελίδας</string>
+ <string name="deleting_post">Γίνεται διαγραφή άρθρου</string>
+ <string name="share_url_post">Κοινοποίηση άρθρου</string>
+ <string name="share_url_page">Κοινοποίηση σελίδας</string>
+ <string name="share_link">Κοινοποίηση συνδέσμου</string>
+ <string name="creating_your_account">Δημιουργώντας τον λογαριασμό σας</string>
+ <string name="creating_your_site">Δημιουργώντας τον ιστότοπο</string>
+ <string name="reader_empty_posts_in_tag_updating">Γίνεται λήψη άρθρων...</string>
+ <string name="error_refresh_media">Κάτι πήγε στραβά καθώς γινόταν ανανέωση της βιβλιοθήκης πολυμέσων. Δοκιμάστε πάλι σε λίγο.</string>
+ <string name="reader_likes_you_and_multi">Αυτό αρέσει σε εσάς και σε άλλους %,d</string>
+ <string name="reader_likes_multi">Αυτό αρέσει σε %,d ανθρώπους</string>
+ <string name="reader_toast_err_get_comment">Δε στάθηκε δυνατό να ανακτηθεί αυτό το σχόλιο</string>
+ <string name="reader_label_reply">Απάντηση</string>
+ <string name="video">Βίντεο</string>
+ <string name="download">Γίνεται μεταφορά πολυμέσων</string>
+ <string name="comment_spammed">Το σχόλιο αυτό έχει σημειωθεί ως ανεπιθύμητο</string>
+ <string name="cant_share_no_visible_blog">Δε μπορείτε να μοιραστείτε στο WordPress χωρίς ένα ορατό ιστολόγιο</string>
+ <string name="select_time">Επιλέξτε ώρα</string>
+ <string name="reader_likes_you_and_one">Αυτό αρέσει σε εσάς και ακόμη ένα χρήστη</string>
+ <string name="reader_empty_followed_blogs_description">Αλλά μην ανησυχείτε, απλά πατήστε το εικονίδιο επάνω δεξιά για να εξερευνήσετε!</string>
+ <string name="select_date">Επιλέξτε ημερομηνία</string>
+ <string name="pick_photo">Επιλέξτε εικόνα</string>
+ <string name="account_two_step_auth_enabled">Αυτός ο λογαριασμός έχει ενεργοποιημένη τη διαπίστευση σε δύο στάδια. Επισκεφθείτε τις ρυθμίσεις ασφάλειάς σας στο WordPress.com για να παράξετε ένα κωδικό ειδικά για την εφαρμογή.</string>
+ <string name="pick_video">Διάλεξε Βίντεο</string>
+ <string name="reader_toast_err_get_post">Δεν είναι δυνατή η ανάκτηση αυτού του άρθρου.</string>
+ <string name="validating_user_data">Επικύρωση των στοιχείων του χρήστη.</string>
+ <string name="validating_site_data">Επικύρωση στοιχείων του ιστότοπου</string>
+ <string name="password_invalid">Χρειάζεστε έναν πιο ασφαλή κωδικό πρόσβασης. Χρησιμοποιήστε 7 ή περισσότερους χαρακτήρες, μέσα στους οποίους υπάρχουν κεφαλαία και μικρά γράμματα, αριθμοί ή ειδικοί χαρακτήρες.</string>
+ <string name="nux_tap_continue">Συνέχεια</string>
+ <string name="nux_welcome_create_account">Δημιουργία λογαριασμού</string>
+ <string name="signing_in">Σύνδεση...</string>
+ <string name="nux_add_selfhosted_blog">Προσθέστε έναν ιστότοπο που φιλοξενείτε μόνοι σας</string>
+ <string name="nux_oops_not_selfhosted_blog">Είσοδος στο WordPress.com</string>
+ <string name="media_add_popup_title">Προσθήκη στην Βιβλιοθήκη Ππολυμέσων</string>
+ <string name="media_add_new_media_gallery">Δημιουργία έκθεσης</string>
+ <string name="empty_list_default">Αυτή η λίστα είναι κενή</string>
+ <string name="select_from_media_library">Διαλέξτε από την βιβλιοθήκη πολυμέσων</string>
+ <string name="jetpack_message">Το Jetpack plugin είναι αναγκαίο για την συλλογή στατιστικών στοιχείων. Θέλετε να εγκαταστήσετε το Jetpack;</string>
+ <string name="jetpack_not_found">Δεν βρέθηκε το Jetpack plugin</string>
+ <string name="reader_untitled_post">Ανώνυμο</string>
+ <string name="reader_share_subject">Μοιράστηκε από %s</string>
+ <string name="reader_btn_share">Μοιράσου</string>
+ <string name="reader_btn_follow">Ακολουθήστε</string>
+ <string name="reader_btn_unfollow">Ακολουθούν</string>
+ <string name="reader_hint_comment_on_comment">Απάντηση σ\' αυτό το σχόλιο...</string>
+ <string name="reader_label_added_tag">Προστέθηκε η %s</string>
+ <string name="reader_label_removed_tag">Αφαιρέθηκε το %s</string>
+ <string name="reader_likes_one">Σ\'ένα άτομο άρεσε αυτό</string>
+ <string name="reader_likes_only_you">Σας άρεσε αυτό</string>
+ <string name="reader_toast_err_comment_failed">Δεν ήταν δυνατή η προσθήκη του σχόλιού σας</string>
+ <string name="reader_toast_err_tag_exists">Ακολουθείτε ήδη αυτή την ετικέτα</string>
+ <string name="reader_toast_err_tag_invalid">Αυτή η ετικέτα δεν είναι έγκυρη</string>
+ <string name="reader_toast_err_share_intent">Δεν ήταν δυνατός ο διαμοιρασμός</string>
+ <string name="reader_toast_err_view_image">Αδυναμία προβολής της εικόνας</string>
+ <string name="reader_toast_err_url_intent">Δε στάθηκε δυνατό να ανοιχθεί το %s</string>
+ <string name="reader_empty_followed_tags">Δεν ακολουθείτε καμία ετικέτα</string>
+ <string name="create_account_wpcom">Δημιουργήστε έναν λογαριασμό στο WordPress.com</string>
+ <string name="button_next">Επόμενο</string>
+ <string name="connecting_wpcom">Σύνδεση με WordPress.com</string>
+ <string name="username_invalid">Εσφαλμένο Όνομα Χρήστη</string>
+ <string name="limit_reached">Φτάσατε το όριο. Μπορείτε να προσπαθήσετε ξανά σε 1 λεπτό. Αν προσπαθήσετε ξανά πριν περάσει η χρονική περίοδος του 1 λεπτού το μόνο που θα καταφέρετε είναι να αυξήσετε το χρόνο αναμονής σας μέχρι να αρθεί η απαγόρευση. Εάν πιστεύετε ότι αυτό είναι σφάλμα, παρακαλώ επικοινωνήστε με την τεχνική υποστήριξη.</string>
+ <string name="nux_tutorial_get_started_title">Ξεκινήστε!</string>
+ <string name="themes">Θέμα</string>
+ <string name="all">Όλα</string>
+ <string name="images">Εικόνες</string>
+ <string name="unattached">Μη συνδεδεμένα</string>
+ <string name="custom_date">Προσαρμοσμένη ημερομηνία</string>
+ <string name="media_add_popup_capture_photo">Λήψη φωτογραφίας</string>
+ <string name="media_add_popup_capture_video">Λήψη βίντεο</string>
+ <string name="media_gallery_image_order_random">Τυχαία</string>
+ <string name="media_gallery_image_order_reverse">Αντίστροφα</string>
+ <string name="media_gallery_type">Τύπος</string>
+ <string name="media_gallery_type_squares">Τετράγωνα</string>
+ <string name="media_gallery_type_tiled">Πλακίδια</string>
+ <string name="media_gallery_type_circles">Κύκλοι</string>
+ <string name="media_gallery_type_slideshow">Προβολή Διαφανειών</string>
+ <string name="media_edit_title_text">Τίτλος</string>
+ <string name="media_edit_caption_text">Λεζάντα</string>
+ <string name="media_edit_description_text">Περιγραφή</string>
+ <string name="media_edit_title_hint">Εισάγετε τον τίτλο εδώ</string>
+ <string name="media_edit_caption_hint">Προσθέστε μια λεζάντα εδώ</string>
+ <string name="media_edit_description_hint">Εισάγετε την περιγραφή εδώ</string>
+ <string name="media_edit_success">Ενημερώθηκε</string>
+ <string name="media_edit_failure">Αποτυχία ενημέρωσης</string>
+ <string name="themes_details_label">Λεπτομέρειες</string>
+ <string name="themes_features_label">Χαρακτηριστικά</string>
+ <string name="theme_activate_button">Ενεργοποιήση</string>
+ <string name="theme_activating_button">Γίνεται ενεργοποιήση</string>
+ <string name="theme_set_success">Το θέμα ορίστηκε επιτυχώς</string>
+ <string name="theme_auth_error_title">Δε στάθηκε δυνατό να ανακτηθούν τα θέματα.</string>
+ <string name="post_excerpt">Σύνοψη</string>
+ <string name="share_action_title">Προσθήκη στο ...</string>
+ <string name="share_action">Μοιραστείτε</string>
+ <string name="stats">Στατιστικά</string>
+ <string name="stats_view_visitors_and_views">Επισκέπτες και Προβολές</string>
+ <string name="stats_view_clicks">Κλικ</string>
+ <string name="stats_view_tags_and_categories">Ετικέτες και Κατηγορίες</string>
+ <string name="stats_view_referrers">Ιστότοποι που σας Αναφέρουν</string>
+ <string name="stats_timeframe_today">Σήμερα</string>
+ <string name="stats_timeframe_yesterday">Χθες</string>
+ <string name="stats_timeframe_days">Ημέρες</string>
+ <string name="stats_timeframe_weeks">Εβδομάδες</string>
+ <string name="stats_timeframe_months">Μήνες</string>
+ <string name="stats_entry_country">Χώρα</string>
+ <string name="stats_entry_posts_and_pages">Τίτλος</string>
+ <string name="stats_entry_tags_and_categories">Θέμα</string>
+ <string name="stats_entry_authors">Συγγραφέας</string>
+ <string name="stats_entry_referrers">Ιστότοποι που σας Αναφέρουν</string>
+ <string name="stats_totals_views">Εμφανίσεις</string>
+ <string name="stats_totals_clicks">Κλικ</string>
+ <string name="stats_totals_plays">Αναπαραγωγές</string>
+ <string name="passcode_manage">Διαχείριση κλειδώματος PIN</string>
+ <string name="passcode_enter_passcode">Πληκτρολογήστε το PIN σας</string>
+ <string name="passcode_enter_old_passcode">Πληκτρολογήστε το παλιό σας PIN</string>
+ <string name="passcode_re_enter_passcode">Πληκτρολογήστε το PIN σας ξανά</string>
+ <string name="passcode_change_passcode">Αλλάγή PIN</string>
+ <string name="passcode_set">Το PIN ορίστηκε</string>
+ <string name="passcode_preference_title">Κλείδωμα PIN</string>
+ <string name="passcode_turn_off">Απενεργοποιήστε το κλείδωμα PIN</string>
+ <string name="passcode_turn_on">Ενεργοποιήστε το κλείδωμα PIN</string>
+ <string name="upload">Ανέβασμα</string>
+ <string name="discard">Απόρριψη</string>
+ <string name="sign_in">Συνδεθείτε</string>
+ <string name="notifications">Ενημερώσεις</string>
+ <string name="note_reply_successful">Η απάντηση δημοσιεύτηκε</string>
+ <string name="follows">Ακόλουθοι</string>
+ <string name="new_notifications">%d νέες ενημερώσεις</string>
+ <string name="more_notifications">και %d ακόμη.</string>
+ <string name="loading">Φορτώνει...</string>
+ <string name="httpuser">Όνομα χρήστη HTTP</string>
+ <string name="httppassword">Κωδικός χρήστη HTTP</string>
+ <string name="error_media_upload">Ένα σφάλμα συνέβη κατά το ανέβασμα των πολυμέσων</string>
+ <string name="post_content">Περιεχόμενο (πατήστε για να προσθέσετε κείμενο και πολυμέσα)</string>
+ <string name="publish_date">Δημοσίευση</string>
+ <string name="content_description_add_media">Προσθήκη πολυμέσων</string>
+ <string name="incorrect_credentials">Λανθασμένος συνδυασμός ονόματος χρήστη και κωδικού.</string>
+ <string name="password">Κωδικός</string>
+ <string name="username">Ψευδώνυμο</string>
+ <string name="reader">Αναγνώστης</string>
+ <string name="featured">Ορίστε ως χαρακτηριστική εικόνα</string>
+ <string name="featured_in_post">Συμπεριλάβετε την εικόνα στο περιεχόμενο του άρθρου</string>
+ <string name="no_network_title">Το διαδίκτυο δεν είναι διαθέσιμο</string>
+ <string name="pages">Σελίδες</string>
+ <string name="caption">Λεζάντα (προαιρετική)</string>
+ <string name="width">Πλάτος</string>
+ <string name="posts">Άρθρα</string>
+ <string name="anonymous">Ανώνυμος</string>
+ <string name="page">Σελίδα</string>
+ <string name="post">Άρθρο</string>
+ <string name="blogusername">όνομα χρήστη ιστολογίου</string>
+ <string name="ok">ΟΚ</string>
+ <string name="upload_scaled_image">Φορτώστε και συνδέστε με την εικόνα στις νέες της διαστάσεις</string>
+ <string name="scaled_image">Νέο πλάτος εικόνας</string>
+ <string name="scheduled">Προγραμματισμένο</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Γίνεται μεταφόρτωση</string>
+ <string name="version">Έκδοση</string>
+ <string name="tos">Όροι Χρήσης</string>
+ <string name="app_title">WordPress για Android</string>
+ <string name="max_thumbnail_px_width">Προεπιλεγμένο πλάτος εικόνας</string>
+ <string name="image_alignment">Στοίχιση</string>
+ <string name="refresh">Ανανέωση</string>
+ <string name="untitled">Χωρίς τίτλο</string>
+ <string name="edit">Επεξεργασία</string>
+ <string name="post_id">Άρθρο</string>
+ <string name="page_id">Σελίδα</string>
+ <string name="post_password">Κωδικός (προαιρετικός)</string>
+ <string name="immediately">Αμέσως Τώρα</string>
+ <string name="quickpress_add_alert_title">Θέστε συντομευμένο όνομα</string>
+ <string name="today">Σήμερα</string>
+ <string name="settings">Ρυθμίσεις</string>
+ <string name="share_url">Κοινοποίηση URL</string>
+ <string name="quickpress_window_title">Επιλέξτε ιστότοπο για τη συντόμευση του QuickPress</string>
+ <string name="quickpress_add_error">Το όνομα της συντόμευσης δεν επιτρέπεται να είναι κενό</string>
+ <string name="publish_post">Δημοσιεύστε</string>
+ <string name="draft">Πρόχειρο</string>
+ <string name="post_private">Ιδιωτικό</string>
+ <string name="upload_full_size_image">Ανεβάστε και δημιουργήστε σύνδεσμο με την εικόνα στο πλήρες της μέγεθος</string>
+ <string name="title">Τίτλος</string>
+ <string name="tags_separate_with_commas">Ετικέτες (ξεχωριστές ετικέτες με κόμματα)</string>
+ <string name="categories">Κατηγορίες</string>
+ <string name="dlg_deleting_comments">Διαγραφή σχολίων</string>
+ <string name="notification_blink">Να αναβοσβήνει η ένδειξη ενημερώσεων</string>
+ <string name="notification_sound">Ήχος ειδοποιήσης</string>
+ <string name="notification_vibrate">Δόνηση</string>
+ <string name="status">Κατάσταση</string>
+ <string name="location">Τοποθεσία</string>
+ <string name="sdcard_title">Κάρτα SD Είναι Απαραίτητη</string>
+ <string name="select_video">Επιλέξτε ένα video από τη γκαλερί</string>
+ <string name="media">Πολυμέσα</string>
+ <string name="delete">Διαγραφή</string>
+ <string name="none">Κανένα</string>
+ <string name="blogs">Ιστολόγια</string>
+ <string name="select_photo">Επιλέξτε μια φωτογραφία από τη συλλογή</string>
+ <string name="error">Σφάλμα</string>
+ <string name="cancel">Ακύρωση</string>
+ <string name="save">Αποθήκευση</string>
+ <string name="add">Πρόσθεση</string>
+ <string name="category_refresh_error">Σφάλμα ανανέωσης κατηγοριών</string>
+ <string name="preview">Προεπισκόπηση</string>
+ <string name="on">ενεργό</string>
+ <string name="reply">Απάντηση</string>
+ <string name="notification_settings">Ρυθμίσεις Ειδοποιήσεων</string>
+ <string name="yes">Ναι</string>
+ <string name="no">Όχι</string>
+</resources>
diff --git a/WordPress/src/main/res/values-en-rAU/strings.xml b/WordPress/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 000000000..af26e0b14
--- /dev/null
+++ b/WordPress/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Author</string>
+ <string name="role_contributor">Contributor</string>
+ <string name="role_follower">Follower</string>
+ <string name="role_viewer">Viewer</string>
+ <string name="error_post_my_profile_no_connection">No connection, couldn\'t save your profile</string>
+ <string name="alignment_none">None</string>
+ <string name="alignment_left">Left</string>
+ <string name="alignment_right">Right</string>
+ <string name="site_settings_list_editor_action_mode_title">Selected %1$d</string>
+ <string name="error_fetch_users_list">Couldn\'t retrieve site users</string>
+ <string name="plans_manage">Manage your plan at\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">You don\'t have any viewers yet.</string>
+ <string name="people_fetching">Fetching users…</string>
+ <string name="title_follower">Follower</string>
+ <string name="title_email_follower">Email Follower</string>
+ <string name="people_empty_list_filtered_email_followers">You don\'t have any email followers yet.</string>
+ <string name="people_empty_list_filtered_followers">You don\'t have any followers yet.</string>
+ <string name="people_empty_list_filtered_users">You don\'t have any users yet.</string>
+ <string name="people_dropdown_item_email_followers">Email Followers</string>
+ <string name="people_dropdown_item_viewers">Viewers</string>
+ <string name="people_dropdown_item_followers">Followers</string>
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="invite_message_usernames_limit">Invite up to 10 email addresses and/or WordPress.com usernames. Those needing a username will be sent instructions on how to create one.</string>
+ <string name="viewer_remove_confirmation_message">If you remove this viewer, he or she will not be able to visit this site.\n\nWould you still like to remove this viewer?</string>
+ <string name="follower_remove_confirmation_message">If removed, this follower will stop receiving notifications about this site, unless they re-follow.\n\nWould you still like to remove this follower?</string>
+ <string name="follower_subscribed_since">Since %1$s</string>
+ <string name="reader_label_view_gallery">View Gallery</string>
+ <string name="error_remove_follower">Couldn\'t remove follower</string>
+ <string name="error_remove_viewer">Couldn\'t remove viewer</string>
+ <string name="error_fetch_email_followers_list">Couldn\'t retrieve site email followers</string>
+ <string name="error_fetch_followers_list">Couldn\'t retrieve site followers</string>
+ <string name="editor_failed_uploads_switch_html">Some media uploads have failed. You can\'t switch to HTML mode\n in this state. Remove all failed uploads and continue?</string>
+ <string name="format_bar_description_html">HTML mode</string>
+ <string name="visual_editor">Visual editor</string>
+ <string name="image_thumbnail">Image thumbnail</string>
+ <string name="format_bar_description_ul">Unordered list</string>
+ <string name="format_bar_description_ol">Ordered list</string>
+ <string name="format_bar_description_more">Insert more</string>
+ <string name="format_bar_description_media">Insert media</string>
+ <string name="format_bar_description_strike">Strikethrough</string>
+ <string name="format_bar_description_quote">Block quote</string>
+ <string name="format_bar_description_link">Insert link</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Underline</string>
+ <string name="image_settings_save_toast">Changes saved</string>
+ <string name="image_caption">Caption</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Link to</string>
+ <string name="image_width">Width</string>
+ <string name="format_bar_description_bold">Bold</string>
+ <string name="image_settings_dismiss_dialog_title">Discard unsaved changes?</string>
+ <string name="stop_upload_dialog_title">Stop uploading?</string>
+ <string name="stop_upload_button">Stop Upload</string>
+ <string name="alert_error_adding_media">An error occurred while inserting media</string>
+ <string name="alert_action_while_uploading">You are currently uploading media. Please wait until this completes.</string>
+ <string name="alert_insert_image_html_mode">Can\'t insert media directly in HTML mode. Please switch back to visual mode.</string>
+ <string name="uploading_gallery_placeholder">Uploading gallery…</string>
+ <string name="invite_error_some_failed">Invite sent but error(s) occurred!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Invite sent successfully</string>
+ <string name="tap_to_try_again">Tap to try again!</string>
+ <string name="invite_error_sending">An error occurred while trying to send the invite!</string>
+ <string name="invite_error_invalid_usernames_multiple">Cannot send: There are invalid usernames or emails</string>
+ <string name="invite_error_invalid_usernames_one">Cannot send: A username or email is invalid</string>
+ <string name="invite_error_no_usernames">Please add at least one username</string>
+ <string name="invite_message_info">(Optional) You can enter a custom message of up to 500 characters that will be included in the invitation to the user(s).</string>
+ <string name="invite_message_remaining_other">%d characters remaining</string>
+ <string name="invite_message_remaining_one">1 character remaining</string>
+ <string name="invite_message_remaining_zero">0 characters remaining</string>
+ <string name="invite_invalid_email">The email address \'%s\' is invalid</string>
+ <string name="invite_message_title">Custom Message</string>
+ <string name="invite_already_a_member">There\'s already a member with username \'%s\'</string>
+ <string name="invite_username_not_found">No user was found for username \'%s\'</string>
+ <string name="invite">Invite</string>
+ <string name="invite_names_title">Usernames or Emails</string>
+ <string name="signup_succeed_signin_failed">Your account has been created but an error occured while we signed you\n in. Try to sign in with your newly created username and password.</string>
+ <string name="send_link">Send link</string>
+ <string name="my_site_header_external">External</string>
+ <string name="invite_people">Invite People</string>
+ <string name="label_clear_search_history">Clear search history</string>
+ <string name="dlg_confirm_clear_search_history">Clear search history?</string>
+ <string name="reader_empty_posts_in_search_description">No posts found for %s for your language</string>
+ <string name="reader_label_post_search_running">Searching…</string>
+ <string name="reader_label_related_posts">Related Reading</string>
+ <string name="reader_empty_posts_in_search_title">No posts found</string>
+ <string name="reader_label_post_search_explainer">Search all public WordPress.com blogs</string>
+ <string name="reader_hint_post_search">Search WordPress.com</string>
+ <string name="reader_title_related_post_detail">Related Post</string>
+ <string name="reader_title_search_results">Search for %s</string>
+ <string name="preview_screen_links_disabled">Links are disabled on the preview screen</string>
+ <string name="draft_explainer">This post is a draft which hasn\'t been published</string>
+ <string name="send">Send</string>
+ <string name="user_remove_confirmation_message">If you remove %1$s, that user will no longer be able to access this site, but any content that was created by %1$s will remain on the site.\n\nWould you still like to remove this user?</string>
+ <string name="person_removed">Successfully removed %1$s</string>
+ <string name="person_remove_confirmation_title">Remove %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">The sites in this list haven\'t posted anything recently</string>
+ <string name="people">People</string>
+ <string name="edit_user">Edit User</string>
+ <string name="role">Role</string>
+ <string name="error_remove_user">Couldn\'t remove user</string>
+ <string name="error_fetch_viewers_list">Couldn\'t retrieve site viewers</string>
+ <string name="error_update_role">Couldn\'t update user role</string>
+ <string name="gravatar_camera_and_media_permission_required">Permissions required in order to select or capture a photo</string>
+ <string name="error_updating_gravatar">Error updating your Gravatar</string>
+ <string name="error_locating_image">Error locating the cropped image</string>
+ <string name="error_refreshing_gravatar">Error reloading your Gravatar</string>
+ <string name="gravatar_tip">New! Tap your Gravatar to change it!</string>
+ <string name="error_cropping_image">Error cropping the image</string>
+ <string name="launch_your_email_app">Launch your email app</string>
+ <string name="checking_email">Checking email</string>
+ <string name="not_on_wordpress_com">Not on WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Currently unavailable. Please enter your password</string>
+ <string name="check_your_email">Check your email</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Get a link sent to your email to sign in instantly</string>
+ <string name="logging_in">Logging in</string>
+ <string name="enter_your_password_instead">Enter your password instead</string>
+ <string name="web_address_dialog_hint">Shown publicly when you comment.</string>
+ <string name="jetpack_not_connected_message">The Jetpack plugin is installed, but not connected to WordPress.com. Do you want to connect Jetpack?</string>
+ <string name="username_email">Email or username</string>
+ <string name="jetpack_not_connected">Jetpack plugin not connected</string>
+ <string name="new_editor_reflection_error">Visual editor is not compatible with your device. It was\n automatically disabled.</string>
+ <string name="stats_insights_latest_post_no_title">(no title)</string>
+ <string name="capture_or_pick_photo">Capture or select photo</string>
+ <string name="plans_post_purchase_text_themes">You now have unlimited access to Premium themes. Preview any theme on your site to get started.</string>
+ <string name="plans_post_purchase_button_themes">Browse Themes</string>
+ <string name="plans_post_purchase_title_themes">Find a perfect, Premium theme</string>
+ <string name="plans_post_purchase_button_video">Start new post</string>
+ <string name="plans_post_purchase_text_video">You can upload and host videos on your site with VideoPress and your expanded media storage.</string>
+ <string name="plans_post_purchase_title_video">Bring posts to life with video</string>
+ <string name="plans_post_purchase_button_customize">Customise my Site</string>
+ <string name="plans_post_purchase_text_customize">You now have access to custom fonts, custom colours, and custom CSS editing capabilities.</string>
+ <string name="plans_post_purchase_text_intro">Your site is doing somersaults in excitement! Now explore your site\'s new features and choose where you\'d like to begin.</string>
+ <string name="plans_post_purchase_title_customize">Customise Fonts &amp; Colours</string>
+ <string name="plans_post_purchase_title_intro">It\'s all yours, way to go!</string>
+ <string name="export_your_content_message">Your posts, pages, and settings will be emailed to you at %s.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Unable to load plans</string>
+ <string name="export_your_content">Export your content</string>
+ <string name="exporting_content_progress">Exporting content…</string>
+ <string name="export_email_sent">Export email sent!</string>
+ <string name="premium_upgrades_message">You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site.</string>
+ <string name="show_purchases">Show purchases</string>
+ <string name="checking_purchases">Checking purchases</string>
+ <string name="premium_upgrades_title">Premium Upgrades</string>
+ <string name="purchases_request_error">Something went wrong. Could not request purchases.</string>
+ <string name="delete_site_progress">Deleting site…</string>
+ <string name="delete_site_summary">This action can not be undone. Deleting your site will remove all content, contributors, and domains from the site.</string>
+ <string name="delete_site_hint">Delete site</string>
+ <string name="export_site_hint">Export your site to an XML file</string>
+ <string name="are_you_sure">Are You Sure?</string>
+ <string name="export_site_summary">If you are sure, please be sure to take the time and export your content now. It can not be recovered in the future.</string>
+ <string name="keep_your_content">Keep Your Content</string>
+ <string name="domain_removal_hint">The domains that will not work once you remove your site</string>
+ <string name="domain_removal_summary">Be careful! Deleting your site will also remove your domain(s) listed below.</string>
+ <string name="primary_domain">Primary Domain</string>
+ <string name="domain_removal">Domain Removal</string>
+ <string name="error_deleting_site_summary">There was an error in deleting your site. Please contact support for more assistance</string>
+ <string name="error_deleting_site">Error deleting site</string>
+ <string name="confirm_delete_site_prompt">Please type in %1$s in the field below to confirm. Your site will then be gone forever.</string>
+ <string name="site_settings_export_content_title">Export content</string>
+ <string name="contact_support">Contact support</string>
+ <string name="confirm_delete_site">Confirm Delete Site</string>
+ <string name="start_over_text">If you want a site but don\'t want any of the posts and pages you have now, our support team can delete your posts, pages, media and comments for you.\n\nThis will keep your site and URL active, but give you a fresh start on your content creation. Just contact us to have your current content cleared out.</string>
+ <string name="site_settings_start_over_hint">Start your site over</string>
+ <string name="let_us_help">Let Us Help</string>
+ <string name="me_btn_app_settings">App Settings</string>
+ <string name="start_over">Start Over</string>
+ <string name="editor_remove_failed_uploads">Remove failed uploads</string>
+ <string name="editor_toast_failed_uploads">Some media uploads have failed. You can\'t save or publish\n your post in this state. Would you like to remove all failed media?</string>
+ <string name="comments_empty_list_filtered_trashed">No Trashed comments</string>
+ <string name="site_settings_advanced_header">Advanced</string>
+ <string name="comments_empty_list_filtered_pending">No Pending comments</string>
+ <string name="comments_empty_list_filtered_approved">No Approved comments</string>
+ <string name="button_done">Done</string>
+ <string name="button_skip">Skip</string>
+ <string name="site_timeout_error">Couldn\'t connect to the WordPress site due to Timeout error.</string>
+ <string name="xmlrpc_malformed_response_error">Couldn\'t connect. The WordPress installation responded with an invalid XML-RPC document.</string>
+ <string name="xmlrpc_missing_method_error">Couldn\'t connect. Required XML-RPC methods are missing on the server.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Centre</string>
+ <string name="theme_free">Free</string>
+ <string name="theme_all">All</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Gallery</string>
+ <string name="post_format_image">Image</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Quote</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Information on WordPress.com courses and events (online &amp; in-person).</string>
+ <string name="post_format_aside">Aside</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Opportunities to participate in WordPress.com research &amp; surveys.</string>
+ <string name="notif_tips">Tips for getting the most out of WordPress.com.</string>
+ <string name="notif_community">Community</string>
+ <string name="replies_to_my_comments">Replies to my comments</string>
+ <string name="notif_suggestions">Suggestions</string>
+ <string name="notif_research">Research</string>
+ <string name="site_achievements">Site achievements</string>
+ <string name="username_mentions">Username mentions</string>
+ <string name="likes_on_my_posts">Likes on my posts</string>
+ <string name="site_follows">Site follows</string>
+ <string name="likes_on_my_comments">Likes on my comments</string>
+ <string name="comments_on_my_site">Comments on my site</string>
+ <string name="site_settings_list_editor_summary_other">%d items</string>
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="approve_auto_if_previously_approved">Known users\' comments</string>
+ <string name="approve_auto">All users</string>
+ <string name="approve_manual">No comments</string>
+ <string name="site_settings_paging_summary_other">%d comments per page</string>
+ <string name="site_settings_paging_summary_one">1 comment per page</string>
+ <string name="site_settings_multiple_links_summary_other">Require approval for more than %d links</string>
+ <string name="site_settings_multiple_links_summary_one">Require approval for more than 1 link</string>
+ <string name="site_settings_multiple_links_summary_zero">Require approval for more than 0 links</string>
+ <string name="detail_approve_auto">Automatically approve everyone\'s comments.</string>
+ <string name="detail_approve_auto_if_previously_approved">Automatically approve if the user has a previously approved comment</string>
+ <string name="detail_approve_manual">Require manual approval for everyone\'s comments.</string>
+ <string name="filter_trashed_posts">Trashed</string>
+ <string name="days_quantity_one">1 day</string>
+ <string name="days_quantity_other">%d days</string>
+ <string name="filter_published_posts">Published</string>
+ <string name="filter_draft_posts">Drafts</string>
+ <string name="filter_scheduled_posts">Scheduled</string>
+ <string name="pending_email_change_snackbar">Click the verification link in the email sent to %1$s to confirm your new address</string>
+ <string name="primary_site">Primary site</string>
+ <string name="web_address">Web Address</string>
+ <string name="editor_toast_uploading_please_wait">You are currently uploading media. Please wait until this completes.</string>
+ <string name="error_refresh_comments_showing_older">Comments couldn\'t be refreshed at this time - showing older comments</string>
+ <string name="editor_post_settings_set_featured_image">Set Featured Image</string>
+ <string name="editor_post_settings_featured_image">Featured Image</string>
+ <string name="new_editor_promo_desc">The WordPress app for Android now includes a beautiful new visual\n editor. Try it out by creating a new post.</string>
+ <string name="new_editor_promo_title">Brand new editor</string>
+ <string name="new_editor_promo_button_label">Great, thanks!</string>
+ <string name="visual_editor_enabled">Visual Editor enabled</string>
+ <string name="editor_content_placeholder">Share your story here…</string>
+ <string name="editor_page_title_placeholder">Page Title</string>
+ <string name="editor_post_title_placeholder">Post Title</string>
+ <string name="email_address">Email address</string>
+ <string name="preference_show_visual_editor">Show visual editor</string>
+ <string name="dlg_sure_to_delete_comments">Permanently delete these comments?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Permanently delete this comment?</string>
+ <string name="mnu_comment_delete_permanently">Delete</string>
+ <string name="comment_deleted_permanently">Comment deleted</string>
+ <string name="mnu_comment_untrash">Restore</string>
+ <string name="comments_empty_list_filtered_spam">No Spam comments</string>
+ <string name="could_not_load_page">Could not load page</string>
+ <string name="comment_status_all">All</string>
+ <string name="interface_language">Interface Language</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">About the app</string>
+ <string name="error_post_account_settings">Couldn\'t save your account settings</string>
+ <string name="error_post_my_profile">Couldn\'t save your profile</string>
+ <string name="error_fetch_account_settings">Couldn\'t retrieve your account settings</string>
+ <string name="error_fetch_my_profile">Couldn\'t retrieve your profile</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, got it</string>
+ <string name="stats_widget_promo_desc">Add the widget to your home screen to access your Stats in one click.</string>
+ <string name="stats_widget_promo_title">Home Screen Stats Widget</string>
+ <string name="site_settings_unknown_language_code_error">Language code not recognised</string>
+ <string name="site_settings_threading_dialog_description">Allow comments to be nested in threads.</string>
+ <string name="site_settings_threading_dialog_header">Thread up to</string>
+ <string name="remove">Remove</string>
+ <string name="search">Search</string>
+ <string name="add_category">Add category</string>
+ <string name="disabled">Disabled</string>
+ <string name="site_settings_image_original_size">Original Size</string>
+ <string name="privacy_private">Your site is visible only to you and users you approve</string>
+ <string name="privacy_public_not_indexed">Your site is visible to everyone but asks search engines not to index it</string>
+ <string name="privacy_public">Your site is visible to everyone and may be indexed by search engines</string>
+ <string name="about_me_hint">A few words about you…</string>
+ <string name="public_display_name_hint">Display name will default to your username if it is not set</string>
+ <string name="about_me">About me</string>
+ <string name="public_display_name">Public display name</string>
+ <string name="my_profile">My Profile</string>
+ <string name="first_name">First name</string>
+ <string name="last_name">Last name</string>
+ <string name="site_privacy_public_desc">Allow search engines to index this site</string>
+ <string name="site_privacy_hidden_desc">Discourage search engines from indexing this site</string>
+ <string name="site_privacy_private_desc">I would like my site to be private, visible only to users I choose</string>
+ <string name="cd_related_post_preview_image">Related post preview image</string>
+ <string name="error_post_remote_site_settings">Couldn\'t save site info</string>
+ <string name="error_fetch_remote_site_settings">Couldn\'t retrieve site info</string>
+ <string name="error_media_upload_connection">A connection error occurred while uploading media</string>
+ <string name="site_settings_disconnected_toast">Disconnected, editing disabled.</string>
+ <string name="site_settings_unsupported_version_error">Unsupported WordPress version</string>
+ <string name="site_settings_multiple_links_dialog_description">Require approval for comments that include more than this number of links.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatically close</string>
+ <string name="site_settings_close_after_dialog_description">Automatically close comments on articles.</string>
+ <string name="site_settings_paging_dialog_description">Break comment threads into multiple pages.</string>
+ <string name="site_settings_paging_dialog_header">Comments per page</string>
+ <string name="site_settings_close_after_dialog_title">Close commenting</string>
+ <string name="site_settings_blacklist_description">When a comment contains any of these words in its content, name, URL, e-mail, or IP, it will be marked as spam. You can enter partial words, so "press" will match "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">When a comment contains any of these words in its content, name, URL, email, or IP, it will be held in the moderation queue. You can enter partial words, so "press" will match "WordPress."</string>
+ <string name="site_settings_list_editor_input_hint">Enter a word or phrase</string>
+ <string name="site_settings_list_editor_no_items_text">No items</string>
+ <string name="site_settings_learn_more_caption">You can override these settings for individual posts.</string>
+ <string name="site_settings_rp_preview3_site">in "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Upgrade Focus: VideoPress For Weddings</string>
+ <string name="site_settings_rp_preview2_site">in "Apps"</string>
+ <string name="site_settings_rp_preview2_title">The WordPress for Android App Gets a Big Facelift</string>
+ <string name="site_settings_rp_preview1_site">in "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Big iPhone/iPad Update Now Available</string>
+ <string name="site_settings_rp_show_images_title">Show Images</string>
+ <string name="site_settings_rp_show_header_title">Show Header</string>
+ <string name="site_settings_rp_switch_summary">Related Posts displays relevant content from your site below your posts.</string>
+ <string name="site_settings_rp_switch_title">Show Related Posts</string>
+ <string name="site_settings_delete_site_hint">Removes your site data from the app</string>
+ <string name="site_settings_blacklist_hint">Comments that match a filter are marked as spam</string>
+ <string name="site_settings_moderation_hold_hint">Comments that match a filter are put in the moderation queue</string>
+ <string name="site_settings_multiple_links_hint">Ignores link limit from known users</string>
+ <string name="site_settings_whitelist_hint">Comment author must have a previously approved comment</string>
+ <string name="site_settings_user_account_required_hint">Users must be registered and logged in to comment</string>
+ <string name="site_settings_identity_required_hint">Comment author must fill out name and email</string>
+ <string name="site_settings_manual_approval_hint">Comments must be manually approved</string>
+ <string name="site_settings_paging_hint">Display comments in chunks of a specified size</string>
+ <string name="site_settings_threading_hint">Allow nested comments to a certain depth</string>
+ <string name="site_settings_sort_by_hint">Determines the order comments are displayed</string>
+ <string name="site_settings_close_after_hint">Disallow comments after the specified time</string>
+ <string name="site_settings_receive_pingbacks_hint">Allow link notifications from other blogs</string>
+ <string name="site_settings_send_pingbacks_hint">Attempt to notify any blogs linked to from the article</string>
+ <string name="site_settings_allow_comments_hint">Allow readers to post comments</string>
+ <string name="site_settings_discussion_hint">View and change your sites discussion settings</string>
+ <string name="site_settings_more_hint">View all available Discussion settings</string>
+ <string name="site_settings_related_posts_hint">Show or hide related posts in reader</string>
+ <string name="site_settings_upload_and_link_image_hint">Enable to always upload the fullsize image</string>
+ <string name="site_settings_image_width_hint">Resizes images in posts to this width</string>
+ <string name="site_settings_format_hint">Sets new post format</string>
+ <string name="site_settings_category_hint">Sets new post category</string>
+ <string name="site_settings_location_hint">Automatically add location data to your posts</string>
+ <string name="site_settings_password_hint">Change your password</string>
+ <string name="site_settings_username_hint">Current user account</string>
+ <string name="site_settings_language_hint">Language this blog is primarily written in</string>
+ <string name="site_settings_privacy_hint">Controls who can see your site</string>
+ <string name="site_settings_address_hint">Changing your address is not currently supported</string>
+ <string name="site_settings_tagline_hint">A short description or catchy phrase to describe your blog</string>
+ <string name="site_settings_title_hint">In a few words, explain what this site is about</string>
+ <string name="site_settings_whitelist_known_summary">Comments from known users</string>
+ <string name="site_settings_whitelist_all_summary">Comments from all users</string>
+ <string name="site_settings_threading_summary">%d levels</string>
+ <string name="site_settings_privacy_private_summary">Private</string>
+ <string name="site_settings_privacy_hidden_summary">Hidden</string>
+ <string name="site_settings_delete_site_title">Delete Site</string>
+ <string name="site_settings_privacy_public_summary">Public</string>
+ <string name="site_settings_blacklist_title">Blacklist</string>
+ <string name="site_settings_moderation_hold_title">Hold for Moderation</string>
+ <string name="site_settings_multiple_links_title">Links in comments</string>
+ <string name="site_settings_whitelist_title">Automatically approve</string>
+ <string name="site_settings_threading_title">Threading</string>
+ <string name="site_settings_paging_title">Paging</string>
+ <string name="site_settings_sort_by_title">Sort by</string>
+ <string name="site_settings_account_required_title">Users must be signed in</string>
+ <string name="site_settings_identity_required_title">Must include name and email</string>
+ <string name="site_settings_receive_pingbacks_title">Receive Pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Send Pingbacks</string>
+ <string name="site_settings_allow_comments_title">Allow Comments</string>
+ <string name="site_settings_default_format_title">Default Format</string>
+ <string name="site_settings_default_category_title">Default Category</string>
+ <string name="site_settings_location_title">Enable Location</string>
+ <string name="site_settings_address_title">Address</string>
+ <string name="site_settings_title_title">Site Title</string>
+ <string name="site_settings_tagline_title">Tagline</string>
+ <string name="site_settings_this_device_header">This device</string>
+ <string name="site_settings_discussion_new_posts_header">Defaults for new posts</string>
+ <string name="site_settings_account_header">Account</string>
+ <string name="site_settings_writing_header">Writing</string>
+ <string name="newest_first">Newest first</string>
+ <string name="site_settings_general_header">General</string>
+ <string name="discussion">Discussion</string>
+ <string name="privacy">Privacy</string>
+ <string name="related_posts">Related Posts</string>
+ <string name="comments">Comments</string>
+ <string name="close_after">Close after</string>
+ <string name="oldest_first">Oldest first</string>
+ <string name="media_error_no_permission_upload">You don\'t have permission to upload media to the site</string>
+ <string name="never">Never</string>
+ <string name="unknown">Unknown</string>
+ <string name="reader_err_get_post_not_found">This post no longer exists</string>
+ <string name="reader_err_get_post_not_authorized">You\'re not authorised to view this post</string>
+ <string name="reader_err_get_post_generic">Unable to retrieve this post</string>
+ <string name="blog_name_no_spaced_allowed">Site address can\'t contain spaces</string>
+ <string name="invalid_username_no_spaces">Username can\'t contain spaces</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">The sites you follow haven\'t posted anything recently</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">No recent posts</string>
+ <string name="media_details_copy_url_toast">URL copied to clipboard</string>
+ <string name="edit_media">Edit media</string>
+ <string name="media_details_copy_url">Copy URL</string>
+ <string name="media_details_label_date_uploaded">Uploaded</string>
+ <string name="media_details_label_date_added">Added</string>
+ <string name="selected_theme">Selected Theme</string>
+ <string name="could_not_load_theme">Could not load theme</string>
+ <string name="theme_activation_error">Something went wrong. Could not activate theme</string>
+ <string name="theme_by_author_prompt_append"> by %1$s</string>
+ <string name="theme_prompt">Thanks for choosing %1$s</string>
+ <string name="theme_try_and_customize">Try &amp; Customise</string>
+ <string name="theme_view">View</string>
+ <string name="theme_details">Details</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">DONE</string>
+ <string name="theme_manage_site">MANAGE SITE</string>
+ <string name="title_activity_theme_support">Themes</string>
+ <string name="theme_activate">Activate</string>
+ <string name="date_range_start_date">Start Date</string>
+ <string name="date_range_end_date">End Date</string>
+ <string name="current_theme">Current Theme</string>
+ <string name="customize">Customise</string>
+ <string name="details">Details</string>
+ <string name="support">Support</string>
+ <string name="active">Active</string>
+ <string name="stats_referrers_spam_generic_error">Something went wrong during the operation. The spam state wasn\'t changed.</string>
+ <string name="stats_referrers_marking_not_spam">Marking as not spam</string>
+ <string name="stats_referrers_unspam">Not spam</string>
+ <string name="stats_referrers_marking_spam">Marking as spam</string>
+ <string name="theme_auth_error_authenticate">Failed to fetch themes: failed authenticate user</string>
+ <string name="post_published">Post published</string>
+ <string name="page_published">Page published</string>
+ <string name="post_updated">Post updated</string>
+ <string name="page_updated">Page updated</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Sorry, no themes found.</string>
+ <string name="media_file_name">File name: %s</string>
+ <string name="media_uploaded_on">Uploaded on: %s</string>
+ <string name="media_dimensions">Dimensions: %s</string>
+ <string name="upload_queued">Queued</string>
+ <string name="media_file_type">File type: %s</string>
+ <string name="reader_label_gap_marker">Load more posts</string>
+ <string name="notifications_no_search_results">No sites matched \'%s\'</string>
+ <string name="search_sites">Search sites</string>
+ <string name="notifications_empty_view_reader">View Reader</string>
+ <string name="unread">Unread</string>
+ <string name="notifications_empty_action_followers_likes">Get noticed: comment on posts you\'ve read.</string>
+ <string name="notifications_empty_action_comments">Join a conversation: comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_action_unread">Reignite the conversation: write a new post.</string>
+ <string name="notifications_empty_action_all">Get active! Comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_likes">No new likes to show…yet.</string>
+ <string name="notifications_empty_followers">No new followers to report…yet.</string>
+ <string name="notifications_empty_comments">No new comments…yet.</string>
+ <string name="notifications_empty_unread">You\'re all caught up!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Please access the Stats in the app, and try adding the widget later</string>
+ <string name="stats_widget_error_readd_widget">Please remove the widget and re-add it again</string>
+ <string name="stats_widget_error_no_visible_blog">Stats couldn\'t be accessed without a visible blog</string>
+ <string name="stats_widget_error_no_permissions">Your WordPress.com account can\'t access Stats on this blog</string>
+ <string name="stats_widget_error_no_account">Please login into WordPress</string>
+ <string name="stats_widget_error_generic">Stats couldn\'t be loaded</string>
+ <string name="stats_widget_loading_data">Loading data…</string>
+ <string name="stats_widget_name_for_blog">Today\'s Stats for %1$s</string>
+ <string name="stats_widget_name">WordPress Today\'s Stats</string>
+ <string name="add_location_permission_required">Permission required in order to add location</string>
+ <string name="add_media_permission_required">Permissions required in order to add media</string>
+ <string name="access_media_permission_required">Permissions required in order to access media</string>
+ <string name="stats_enable_rest_api_in_jetpack">To view your stats, enable the JSON API module in Jetpack.</string>
+ <string name="error_open_list_from_notification">This post or page was published on another site</string>
+ <string name="reader_short_comment_count_multi">%s Comments</string>
+ <string name="reader_short_comment_count_one">1 Comment</string>
+ <string name="reader_label_submit_comment">SEND</string>
+ <string name="reader_hint_comment_on_post">Reply to post…</string>
+ <string name="reader_discover_visit_blog">Visit %s</string>
+ <string name="reader_discover_attribution_blog">Originally posted on %s</string>
+ <string name="reader_discover_attribution_author">Originally posted by %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originally posted by %1$s on %2$s</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_label_follow_count">%,d followers</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_menu_tags">Edit tags and blogs</string>
+ <string name="reader_title_post_detail">Reader Post</string>
+ <string name="local_draft_explainer">This post is a local draft which hasn\'t been published</string>
+ <string name="local_changes_explainer">This post has local changes which haven\'t been published</string>
+ <string name="notifications_push_summary">Settings for notifications that appear on your device.</string>
+ <string name="notifications_email_summary">Settings for notifications that are sent to the email tied to your account.</string>
+ <string name="notifications_tab_summary">Settings for notifications that appear in the Notifications tab.</string>
+ <string name="notifications_disabled">App notifications have been disabled. Tap here to enable them in Settings.</string>
+ <string name="notification_types">Notification Types</string>
+ <string name="error_loading_notifications">Couldn\'t load notification settings</string>
+ <string name="replies_to_your_comments">Replies to your comments</string>
+ <string name="comment_likes">Comment likes</string>
+ <string name="app_notifications">App notifications</string>
+ <string name="notifications_tab">Notifications tab</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Comments on other sites</string>
+ <string name="notifications_wpcom_updates">WordPress.com Updates</string>
+ <string name="notifications_other">Other</string>
+ <string name="notifications_account_emails">Email from WordPress.com</string>
+ <string name="notifications_account_emails_summary">We\'ll always send important emails regarding your account, but you can get some helpful extras, too.</string>
+ <string name="notifications_sights_and_sounds">Sights and Sounds</string>
+ <string name="your_sites">Your Sites</string>
+ <string name="stats_insights_latest_post_trend">It\'s been %1$s since %2$s was published. Here\'s how the post has performed so far…</string>
+ <string name="stats_insights_latest_post_summary">Latest Post Summary</string>
+ <string name="button_revert">Revert</string>
+ <string name="days_ago">%d days ago</string>
+ <string name="yesterday">Yesterday</string>
+ <string name="connectionbar_no_connection">No connection</string>
+ <string name="page_trashed">Page sent to trash</string>
+ <string name="post_deleted">Post deleted</string>
+ <string name="post_trashed">Post sent to trash</string>
+ <string name="stats_no_activity_this_period">No activity this period</string>
+ <string name="trashed">Trashed</string>
+ <string name="button_back">Back</string>
+ <string name="page_deleted">Page deleted</string>
+ <string name="button_stats">Stats</string>
+ <string name="button_trash">Trash</string>
+ <string name="button_preview">Preview</string>
+ <string name="button_view">View</string>
+ <string name="button_edit">Edit</string>
+ <string name="button_publish">Publish</string>
+ <string name="my_site_no_sites_view_subtitle">Would you like to add one?</string>
+ <string name="my_site_no_sites_view_title">You don\'t have any WordPress sites yet.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">You are not authorised to access this blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">This blog could not be found</string>
+ <string name="undo">Undo</string>
+ <string name="tabbar_accessibility_label_my_site">My Site</string>
+ <string name="tabbar_accessibility_label_me">Me</string>
+ <string name="passcodelock_prompt_message">Enter your PIN</string>
+ <string name="editor_toast_changes_saved">Changes saved</string>
+ <string name="push_auth_expired">The request has expired. Sign in to WordPress.com to try again.</string>
+ <string name="stats_insights_best_ever">Best Views Ever</string>
+ <string name="ignore">Ignore</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% of views</string>
+ <string name="stats_insights_most_popular_hour">Most popular hour</string>
+ <string name="stats_insights_most_popular_day">Most popular day</string>
+ <string name="stats_insights_popular">Most popular day and hour</string>
+ <string name="stats_insights_today">Today\'s Stats</string>
+ <string name="stats_insights_all_time">All-time posts, views, and visitors</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">To view your stats, sign in to the WordPress.com account you used to connect Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Looking for your Other Recent Stats? We\'ve moved them to the Insights page.</string>
+ <string name="me_disconnect_from_wordpress_com">Disconnect from WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Connect to WordPress.com</string>
+ <string name="me_btn_login_logout">Login/Logout</string>
+ <string name="account_settings">Account Settings</string>
+ <string name="me_btn_support">Help &amp; Support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" wasn\'t hidden because it\'s the current site</string>
+ <string name="site_picker_create_dotcom">Create WordPress.com site</string>
+ <string name="site_picker_add_site">Add site</string>
+ <string name="site_picker_add_self_hosted">Add self-hosted site</string>
+ <string name="site_picker_edit_visibility">Show/hide sites</string>
+ <string name="my_site_btn_view_admin">View Admin</string>
+ <string name="my_site_btn_view_site">View Site</string>
+ <string name="site_picker_title">Choose site</string>
+ <string name="my_site_btn_switch_site">Switch Site</string>
+ <string name="my_site_btn_blog_posts">Blog Posts</string>
+ <string name="my_site_btn_site_settings">Settings</string>
+ <string name="my_site_header_look_and_feel">Look and Feel</string>
+ <string name="my_site_header_publish">Publish</string>
+ <string name="my_site_header_configuration">Configuration</string>
+ <string name="reader_label_new_posts_subtitle">Tap to show them</string>
+ <string name="notifications_account_required">Sign in to WordPress.com for notifications</string>
+ <string name="stats_unknown_author">Unknown Author</string>
+ <string name="image_added">Image added</string>
+ <string name="signout">Disconnect</string>
+ <string name="deselect_all">Deselect all</string>
+ <string name="show">Show</string>
+ <string name="hide">Hide</string>
+ <string name="select_all">Select all</string>
+ <string name="sign_out_wpcom_confirm">Disconnecting your account will remove all of @%s’s WordPress.com data from this device, including local drafts and local changes.</string>
+ <string name="select_from_new_picker">Multi-select with the new picker</string>
+ <string name="stats_generic_error">Required Stats couldn\'t be loaded</string>
+ <string name="no_device_videos">No videos</string>
+ <string name="no_blog_images">No images</string>
+ <string name="no_blog_videos">No videos</string>
+ <string name="no_device_images">No images</string>
+ <string name="error_loading_blog_images">Unable to fetch images</string>
+ <string name="error_loading_blog_videos">Unable to fetch videos</string>
+ <string name="error_loading_images">Error loading images</string>
+ <string name="error_loading_videos">Error loading videos</string>
+ <string name="loading_blog_images">Fetching images</string>
+ <string name="loading_blog_videos">Fetching videos</string>
+ <string name="no_media_sources">Couldn\'t fetch media</string>
+ <string name="loading_videos">Loading videos</string>
+ <string name="loading_images">Loading images</string>
+ <string name="no_media">No media</string>
+ <string name="device">Device</string>
+ <string name="language">Language</string>
+ <string name="add_to_post">Add to Post</string>
+ <string name="media_picker_title">Select media</string>
+ <string name="take_photo">Take a photo</string>
+ <string name="take_video">Take a video</string>
+ <string name="tab_title_device_images">Device Images</string>
+ <string name="tab_title_device_videos">Device Videos</string>
+ <string name="tab_title_site_images">Site Images</string>
+ <string name="tab_title_site_videos">Site Videos</string>
+ <string name="media_details_label_file_name">File name</string>
+ <string name="media_details_label_file_type">File type</string>
+ <string name="error_publish_no_network">Can\'t publish while there is no connection. Saved as draft.</string>
+ <string name="editor_toast_invalid_path">Invalid file path</string>
+ <string name="verification_code">Verification code</string>
+ <string name="invalid_verification_code">Invalid verification code</string>
+ <string name="verify">Verify</string>
+ <string name="two_step_footer_label">Enter the code from your authenticator app.</string>
+ <string name="two_step_footer_button">Send code via text message</string>
+ <string name="two_step_sms_sent">Check your text messages for the verification code.</string>
+ <string name="sign_in_jetpack">Sign in to your WordPress.com account to connect to Jetpack.</string>
+ <string name="auth_required">Sign in again to continue.</string>
+ <string name="reader_empty_posts_request_failed">Unable to retrieve posts</string>
+ <string name="publisher">Publisher:</string>
+ <string name="error_notification_open">Could not open notification</string>
+ <string name="stats_followers_total_email_paged">Showing %1$d - %2$d of %3$s Email Followers</string>
+ <string name="stats_search_terms_unknown_search_terms">Unknown Search Terms</string>
+ <string name="stats_followers_total_wpcom_paged">Showing %1$d - %2$d of %3$s WordPress.com Followers</string>
+ <string name="stats_empty_search_terms_desc">Learn more about your search traffic by looking at the terms your visitors searched for to find your site.</string>
+ <string name="stats_empty_search_terms">No search terms recorded</string>
+ <string name="stats_entry_search_terms">Search Term</string>
+ <string name="stats_view_authors">Authors</string>
+ <string name="stats_view_search_terms">Search Terms</string>
+ <string name="comments_fetching">Fetching comments…</string>
+ <string name="pages_fetching">Fetching pages…</string>
+ <string name="toast_err_post_uploading">Unable to open post while it\'s uploading</string>
+ <string name="posts_fetching">Fetching posts…</string>
+ <string name="media_fetching">Fetching media…</string>
+ <string name="post_uploading">Uploading</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">Overall</string>
+ <string name="stats_period">Period</string>
+ <string name="logs_copied_to_clipboard">Application logs have been copied to the clipboard</string>
+ <string name="reader_label_new_posts">New posts</string>
+ <string name="reader_empty_posts_in_blog">This blog is empty</string>
+ <string name="stats_average_per_day">Average per Day</string>
+ <string name="stats_recent_weeks">Recent Weeks</string>
+ <string name="error_copy_to_clipboard">An error occurred while copying text to clipboard</string>
+ <string name="reader_page_recommended_blogs">Sites you may like</string>
+ <string name="stats_months_and_years">Months and Years</string>
+ <string name="themes_fetching">Fetching themes…</string>
+ <string name="stats_for">Stats for %s</string>
+ <string name="stats_other_recent_stats_label">Other Recent Stats</string>
+ <string name="stats_view_all">View all</string>
+ <string name="stats_view">View</string>
+ <string name="stats_followers_months">%1$d months</string>
+ <string name="stats_followers_a_year">A year</string>
+ <string name="stats_followers_years">%1$d years</string>
+ <string name="stats_followers_a_month">A month</string>
+ <string name="stats_followers_minutes">%1$d minutes</string>
+ <string name="stats_followers_an_hour_ago">an hour ago</string>
+ <string name="stats_followers_hours">%1$d hours</string>
+ <string name="stats_followers_a_day">A day</string>
+ <string name="stats_followers_days">%1$d days</string>
+ <string name="stats_followers_a_minute_ago">a minute ago</string>
+ <string name="stats_followers_seconds_ago">seconds ago</string>
+ <string name="stats_followers_total_email">Total Email Followers: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Total WordPress.com Followers: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Total posts with comment followers: %1$s</string>
+ <string name="stats_comments_by_authors">By Authors</string>
+ <string name="stats_comments_by_posts_and_pages">By Posts &amp; Pages</string>
+ <string name="stats_empty_followers_desc">Keep track of your overall number of followers, and how long each one has been following your site.</string>
+ <string name="stats_empty_followers">No followers</string>
+ <string name="stats_empty_publicize_desc">Keep track of your followers from various social networking services using publicize.</string>
+ <string name="stats_empty_publicize">No publicize followers recorded</string>
+ <string name="stats_empty_video">No videos played</string>
+ <string name="stats_empty_video_desc">If you\'ve uploaded videos using VideoPress, find out how many times they’ve been watched.</string>
+ <string name="stats_empty_comments_desc">If you allow comments on your site, track your top commenters and discover what content sparks the liveliest conversations, based on the most recent 1,000 comments.</string>
+ <string name="stats_empty_tags_and_categories_desc">Get an overview of the most popular topics on your site, as reflected in your top posts from the past week.</string>
+ <string name="stats_empty_top_authors_desc">Track the views on each contributor\'s posts, and zoom in to discover the most popular content by each author.</string>
+ <string name="stats_empty_tags_and_categories">No tagged posts or pages viewed</string>
+ <string name="stats_empty_clicks_desc">When your content includes links to other sites, you’ll see which ones your visitors click on the most.</string>
+ <string name="stats_empty_referrers_desc">Learn more about your site’s visibility by looking at the websites and search engines that send the most traffic your way</string>
+ <string name="stats_empty_clicks_title">No clicks recorded</string>
+ <string name="stats_empty_referrers_title">No referrers recorded</string>
+ <string name="stats_empty_top_posts_title">No posts or pages viewed</string>
+ <string name="stats_empty_top_posts_desc">Discover what your most-viewed content is, and check how individual posts and pages perform over time.</string>
+ <string name="stats_totals_followers">Since</string>
+ <string name="stats_empty_geoviews">No countries recorded</string>
+ <string name="stats_empty_geoviews_desc">Explore the list to see which countries and regions generate the most traffic to your site.</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Author</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_followers">Follower</string>
+ <string name="stats_totals_publicize">Followers</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_view_top_posts_and_pages">Posts &amp; Pages</string>
+ <string name="stats_view_videos">Videos</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Followers</string>
+ <string name="stats_view_countries">Countries</string>
+ <string name="stats_likes">Likes</string>
+ <string name="stats_pagination_label">Page %1$s of %2$s</string>
+ <string name="stats_timeframe_years">Years</string>
+ <string name="stats_views">Views</string>
+ <string name="stats_visitors">Visitors</string>
+ <string name="ssl_certificate_details">Details</string>
+ <string name="delete_sure_post">Delete this post</string>
+ <string name="delete_sure">Delete this draft</string>
+ <string name="delete_sure_page">Delete this page</string>
+ <string name="confirm_delete_multi_media">Delete selected items?</string>
+ <string name="confirm_delete_media">Delete selected item?</string>
+ <string name="cab_selected">%d selected</string>
+ <string name="media_gallery_date_range">Displaying media from %1$s to %2$s</string>
+ <string name="sure_to_remove_account">Remove this site?</string>
+ <string name="reader_empty_followed_blogs_title">You\'re not following any sites yet</string>
+ <string name="reader_empty_posts_liked">You haven\'t liked any posts</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Browse our FAQ</string>
+ <string name="nux_help_description">Visit the help center to get answers to common questions or visit the forums to ask new ones</string>
+ <string name="agree_terms_of_service">By creating an account you agree to the fascinating %1$sTerms of Service%2$s</string>
+ <string name="create_new_blog_wpcom">Create WordPress.com blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog created!</string>
+ <string name="reader_empty_comments">No comments yet</string>
+ <string name="reader_empty_posts_in_tag">No posts with this tag</string>
+ <string name="reader_label_comment_count_multi">%,d comments</string>
+ <string name="reader_label_view_original">View original article</string>
+ <string name="reader_label_like">Like</string>
+ <string name="reader_label_liked_by">Liked By</string>
+ <string name="reader_label_comment_count_single">One comment</string>
+ <string name="reader_label_comments_closed">Comments are closed</string>
+ <string name="reader_label_comments_on">Comments on</string>
+ <string name="reader_title_photo_viewer">%1$d of %2$d</string>
+ <string name="error_publish_empty_post">Can\'t publish an empty post</string>
+ <string name="error_refresh_unauthorized_posts">You don\'t have permission to view or edit posts</string>
+ <string name="error_refresh_unauthorized_pages">You don\'t have permission to view or edit pages</string>
+ <string name="error_refresh_unauthorized_comments">You don\'t have permission to view or edit comments</string>
+ <string name="older_month">Older than a month</string>
+ <string name="more">More</string>
+ <string name="older_two_days">Older than 2 days</string>
+ <string name="older_last_week">Older than a week</string>
+ <string name="stats_no_blog">Stats couldn\'t be loaded for the required blog</string>
+ <string name="select_a_blog">Select a WordPress site</string>
+ <string name="sending_content">Uploading %s content</string>
+ <string name="uploading_total">Uploading %1$d of %2$d</string>
+ <string name="mnu_comment_liked">Liked</string>
+ <string name="comment">Comment</string>
+ <string name="comment_trashed">Comment trashed</string>
+ <string name="posts_empty_list">No posts yet. Why not create one?</string>
+ <string name="comment_reply_to_user">Reply to %s</string>
+ <string name="pages_empty_list">No pages yet. Why not create one?</string>
+ <string name="media_empty_list_custom_date">No media in this time interval</string>
+ <string name="posting_post">Posting "%s"</string>
+ <string name="signing_out">Signing out…</string>
+ <string name="reader_toast_err_generic">Unable to perform this action</string>
+ <string name="reader_toast_err_block_blog">Unable to block this blog</string>
+ <string name="reader_toast_blog_blocked">Posts from this blog will no longer be shown</string>
+ <string name="reader_menu_block_blog">Block this blog</string>
+ <string name="contact_us">Contact us</string>
+ <string name="hs__conversation_detail_error">Describe the problem you\'re seeing</string>
+ <string name="hs__new_conversation_header">Support chat</string>
+ <string name="hs__conversation_header">Support chat</string>
+ <string name="hs__username_blank_error">Enter a valid name</string>
+ <string name="hs__invalid_email_error">Enter a valid email address</string>
+ <string name="add_location">Add location</string>
+ <string name="current_location">Current location</string>
+ <string name="search_location">Search</string>
+ <string name="edit_location">Edit</string>
+ <string name="search_current_location">Locate</string>
+ <string name="preference_send_usage_stats">Send statistics</string>
+ <string name="preference_send_usage_stats_summary">Automatically send usage statistics to help us improve WordPress for Android</string>
+ <string name="update_verb">Update</string>
+ <string name="schedule_verb">Schedule</string>
+ <string name="reader_title_blog_preview">Reader Blog</string>
+ <string name="reader_title_tag_preview">Reader Tag</string>
+ <string name="reader_title_subs">Tags &amp; Blogs</string>
+ <string name="reader_page_followed_tags">Followed tags</string>
+ <string name="reader_page_followed_blogs">Followed sites</string>
+ <string name="reader_hint_add_tag_or_url">Enter a URL or tag to follow</string>
+ <string name="reader_label_followed_blog">Blog followed</string>
+ <string name="reader_label_tag_preview">Posts tagged %s</string>
+ <string name="reader_toast_err_get_blog_info">Unable to show this blog</string>
+ <string name="reader_toast_err_already_follow_blog">You already follow this blog</string>
+ <string name="reader_toast_err_follow_blog">Unable to follow this blog</string>
+ <string name="reader_toast_err_unfollow_blog">Unable to unfollow this blog</string>
+ <string name="reader_empty_recommended_blogs">No recommended blogs</string>
+ <string name="saving">Saving…</string>
+ <string name="media_empty_list">No media</string>
+ <string name="ptr_tip_message">Tip: Pull down to refresh</string>
+ <string name="help">Help</string>
+ <string name="forgot_password">Lost your password?</string>
+ <string name="forums">Forums</string>
+ <string name="help_center">Help centre</string>
+ <string name="ssl_certificate_error">Invalid SSL certificate</string>
+ <string name="ssl_certificate_ask_trust">If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn\'t continue. Would you like to trust the certificate anyway?</string>
+ <string name="out_of_memory">Device out of memory</string>
+ <string name="no_network_message">There is no network available</string>
+ <string name="could_not_remove_account">Couldn\'t remove site</string>
+ <string name="gallery_error">The media item couldn\'t be retrieved</string>
+ <string name="blog_not_found">An error occurred when accessing this blog</string>
+ <string name="wait_until_upload_completes">Wait until upload completes</string>
+ <string name="theme_fetch_failed">Failed to fetch themes</string>
+ <string name="theme_set_failed">Failed to set theme</string>
+ <string name="theme_auth_error_message">Ensure you have the privilege to set themes</string>
+ <string name="comments_empty_list">No comments</string>
+ <string name="mnu_comment_unspam">Not spam</string>
+ <string name="no_site_error">Couldn\'t connect to the WordPress site</string>
+ <string name="adding_cat_failed">Adding category failed</string>
+ <string name="adding_cat_success">Category added successfully</string>
+ <string name="cat_name_required">The category name field is required</string>
+ <string name="category_automatically_renamed">Category name %1$s isn\'t valid. It has been renamed to %2$s.</string>
+ <string name="no_account">No WordPress account found, add an account and try again</string>
+ <string name="sdcard_message">A mounted SD card is required to upload media</string>
+ <string name="stats_empty_comments">No comments yet</string>
+ <string name="stats_bar_graph_empty">No stats available</string>
+ <string name="invalid_url_message">Check that the URL entered is valid</string>
+ <string name="reply_failed">Reply failed</string>
+ <string name="notifications_empty_list">No notifications</string>
+ <string name="error_delete_post">An error occurred while deleting the %s</string>
+ <string name="error_refresh_posts">Posts couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_pages">Pages couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_notifications">Notifications couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_comments">Comments couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_stats">Stats couldn\'t be refreshed at this time</string>
+ <string name="error_generic">An error occurred</string>
+ <string name="error_moderate_comment">An error occurred while moderating</string>
+ <string name="error_edit_comment">An error occurred while editing the comment</string>
+ <string name="error_upload">An error occurred while uploading the %s</string>
+ <string name="error_load_comment">Couldn\'t load the comment</string>
+ <string name="error_downloading_image">Error downloading image</string>
+ <string name="passcode_wrong_passcode">Wrong PIN</string>
+ <string name="invalid_email_message">Your email address isn\'t valid</string>
+ <string name="invalid_password_message">Password must contain at least 4 characters</string>
+ <string name="invalid_username_too_short">Username must be longer than 4 characters</string>
+ <string name="invalid_username_too_long">Username must be shorter than 61 characters</string>
+ <string name="username_only_lowercase_letters_and_numbers">Username can only contain lowercase letters (a-z) and numbers</string>
+ <string name="username_required">Enter a username</string>
+ <string name="username_not_allowed">Username not allowed</string>
+ <string name="username_must_be_at_least_four_characters">Username must be at least 4 characters</string>
+ <string name="username_contains_invalid_characters">Username may not contain the character “_”</string>
+ <string name="username_must_include_letters">Username must have a least 1 letter (a-z)</string>
+ <string name="email_invalid">Enter a valid email address</string>
+ <string name="email_not_allowed">That email address isn\'t allowed</string>
+ <string name="username_exists">That username already exists</string>
+ <string name="email_exists">That email address is already being used</string>
+ <string name="username_reserved_but_may_be_available">That username is currently reserved but may be available in a couple of days</string>
+ <string name="blog_name_required">Enter a site address</string>
+ <string name="blog_name_not_allowed">That site address isn\'t allowed</string>
+ <string name="blog_name_must_be_at_least_four_characters">Site address must be at least 4 characters</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">The site address must be shorter than 64 characters</string>
+ <string name="blog_name_contains_invalid_characters">Site address may not contain the character “_”</string>
+ <string name="blog_name_cant_be_used">You may not use that site address</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Site address can only contain lowercase letters (a-z) and numbers</string>
+ <string name="blog_name_exists">That site already exists</string>
+ <string name="blog_name_reserved">That site is reserved</string>
+ <string name="blog_name_reserved_but_may_be_available">That site is currently reserved but may be available in a couple days</string>
+ <string name="username_or_password_incorrect">The username or password you entered is incorrect</string>
+ <string name="nux_cannot_log_in">We can\'t log you in</string>
+ <string name="xmlrpc_error">Couldn\'t connect. Enter the full path to xmlrpc.php on your site and try again.</string>
+ <string name="select_categories">Select categories</string>
+ <string name="account_details">Account details</string>
+ <string name="edit_post">Edit post</string>
+ <string name="add_comment">Add comment</string>
+ <string name="connection_error">Connection error</string>
+ <string name="cancel_edit">Cancel edit</string>
+ <string name="scaled_image_error">Enter a valid scaled width value</string>
+ <string name="post_not_found">An error occurred when loading the post. Refresh your posts and try again.</string>
+ <string name="learn_more">Learn more</string>
+ <string name="media_gallery_settings_title">Gallery settings</string>
+ <string name="media_gallery_image_order">Image order</string>
+ <string name="media_gallery_num_columns">Number of columns</string>
+ <string name="media_gallery_type_thumbnail_grid">Thumbnail grid</string>
+ <string name="media_gallery_edit">Edit gallery</string>
+ <string name="media_error_no_permission">You don\'t have permission to view the media library</string>
+ <string name="cannot_delete_multi_media_items">Some media can\'t be deleted at this time. Try again later.</string>
+ <string name="themes_live_preview">Live preview</string>
+ <string name="theme_current_theme">Current theme</string>
+ <string name="theme_premium_theme">Premium theme</string>
+ <string name="link_enter_url_text">Link text (optional)</string>
+ <string name="create_a_link">Create a link</string>
+ <string name="page_settings">Page settings</string>
+ <string name="local_draft">Local draft</string>
+ <string name="upload_failed">Upload failed</string>
+ <string name="horizontal_alignment">Horizontal alignment</string>
+ <string name="file_not_found">Couldn\'t find the media file for upload. Was it deleted or moved?</string>
+ <string name="post_settings">Post settings</string>
+ <string name="delete_post">Delete post</string>
+ <string name="delete_page">Delete page</string>
+ <string name="comment_status_approved">Approved</string>
+ <string name="comment_status_unapproved">Pending</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Trashed</string>
+ <string name="edit_comment">Edit comment</string>
+ <string name="mnu_comment_approve">Approve</string>
+ <string name="mnu_comment_unapprove">Unapprove</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Trash</string>
+ <string name="dlg_approving_comments">Approving</string>
+ <string name="dlg_unapproving_comments">Unapproving</string>
+ <string name="dlg_spamming_comments">Marking as spam</string>
+ <string name="dlg_trashing_comments">Sending to trash</string>
+ <string name="dlg_confirm_trash_comments">Send to trash?</string>
+ <string name="trash_yes">Trash</string>
+ <string name="trash_no">Don\'t trash</string>
+ <string name="trash">Trash</string>
+ <string name="author_name">Author name</string>
+ <string name="author_email">Author email</string>
+ <string name="author_url">Author URL</string>
+ <string name="hint_comment_content">Comment</string>
+ <string name="saving_changes">Saving changes</string>
+ <string name="sure_to_cancel_edit_comment">Cancel editing this comment?</string>
+ <string name="content_required">Comment is required</string>
+ <string name="toast_comment_unedited">Comment hasn\'t changed</string>
+ <string name="remove_account">Remove site</string>
+ <string name="blog_removed_successfully">Site removed successfully</string>
+ <string name="delete_draft">Delete draft</string>
+ <string name="preview_page">Preview page</string>
+ <string name="preview_post">Preview post</string>
+ <string name="comment_added">Comment added successfully</string>
+ <string name="post_not_published">Post status isn\'t published</string>
+ <string name="page_not_published">Page status isn\'t published</string>
+ <string name="view_in_browser">View in browser</string>
+ <string name="add_new_category">Add new category</string>
+ <string name="category_name">Category name</string>
+ <string name="category_slug">Category slug (optional)</string>
+ <string name="category_desc">Category description (optional)</string>
+ <string name="category_parent">Category parent (optional):</string>
+ <string name="share_action_post">New post</string>
+ <string name="share_action_media">Media library</string>
+ <string name="file_error_create">Couldn\'t create temp file for media upload. Make sure there is enough free space on your device.</string>
+ <string name="location_not_found">Unknown location</string>
+ <string name="open_source_licenses">Open source licences</string>
+ <string name="invalid_site_url_message">Check that the site URL entered is valid</string>
+ <string name="pending_review">Pending review</string>
+ <string name="http_credentials">HTTP credentials (optional)</string>
+ <string name="http_authorization_required">Authorisation required</string>
+ <string name="post_format">Post format</string>
+ <string name="notifications_empty_all">No notifications…yet.</string>
+ <string name="new_post">New post</string>
+ <string name="new_media">New media</string>
+ <string name="view_site">View site</string>
+ <string name="privacy_policy">Privacy policy</string>
+ <string name="local_changes">Local changes</string>
+ <string name="image_settings">Image settings</string>
+ <string name="add_account_blog_url">Blog address</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="error_blog_hidden">This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again.</string>
+ <string name="fatal_db_error">An error occurred while creating the app database. Try reinstalling the app.</string>
+ <string name="jetpack_message_not_admin">The Jetpack plugin is required for stats. Contact the site administrator.</string>
+ <string name="reader_title_applog">Application log</string>
+ <string name="reader_share_link">Share link</string>
+ <string name="reader_toast_err_add_tag">Unable to add this tag</string>
+ <string name="reader_toast_err_remove_tag">Unable to remove this tag</string>
+ <string name="required_field">Required field</string>
+ <string name="email_hint">Email address</string>
+ <string name="site_address">Your self-hosted address (URL)</string>
+ <string name="email_cant_be_used_to_signup">You can\'t use that email address to signup. We are having problems with them blocking some of our email. Use another email provider.</string>
+ <string name="email_reserved">That email address has already been used. Check your inbox for an activation email. If you don\'t activate you can try again in a few days.</string>
+ <string name="blog_name_must_include_letters">Site address must have at least 1 letter (a-z)</string>
+ <string name="blog_name_invalid">Invalid site address</string>
+ <string name="blog_title_invalid">Invalid site title</string>
+ <string name="deleting_page">Deleting page</string>
+ <string name="deleting_post">Deleting post</string>
+ <string name="share_url_post">Share post</string>
+ <string name="share_url_page">Share page</string>
+ <string name="share_link">Share link</string>
+ <string name="creating_your_account">Creating your account</string>
+ <string name="creating_your_site">Creating your site</string>
+ <string name="reader_empty_posts_in_tag_updating">Fetching posts…</string>
+ <string name="error_refresh_media">Something went wrong while refreshing the media library. Try again later.</string>
+ <string name="reader_likes_you_and_multi">You and %,d others like this</string>
+ <string name="reader_likes_multi">%,d people like this</string>
+ <string name="reader_toast_err_get_comment">Unable to retrieve this comment</string>
+ <string name="reader_label_reply">Reply</string>
+ <string name="video">Video</string>
+ <string name="download">Downloading media</string>
+ <string name="comment_spammed">Comment marked as spam</string>
+ <string name="cant_share_no_visible_blog">You can\'t share to WordPress without a visible blog</string>
+ <string name="select_time">Select time</string>
+ <string name="reader_likes_you_and_one">You and one other like this</string>
+ <string name="reader_empty_followed_blogs_description">But don\'t worry, just tap the icon at the top right to start exploring!</string>
+ <string name="select_date">Select date</string>
+ <string name="pick_photo">Select photo</string>
+ <string name="account_two_step_auth_enabled">This account has two step authentication enabled. Visit your security settings on WordPress.com and generate an application-specific password.</string>
+ <string name="pick_video">Select video</string>
+ <string name="reader_toast_err_get_post">Unable to retrieve this post</string>
+ <string name="validating_user_data">Validating user data</string>
+ <string name="validating_site_data">Validating site data</string>
+ <string name="password_invalid">You need a more secure password. Make sure to use 7 or more characters, mix uppercase and lowercase letters, numbers or special characters.</string>
+ <string name="nux_tap_continue">Continue</string>
+ <string name="nux_welcome_create_account">Create account</string>
+ <string name="signing_in">Signing in…</string>
+ <string name="nux_add_selfhosted_blog">Add self-hosted site</string>
+ <string name="nux_oops_not_selfhosted_blog">Sign in to WordPress.com</string>
+ <string name="media_add_popup_title">Add to media library</string>
+ <string name="media_add_new_media_gallery">Create gallery</string>
+ <string name="empty_list_default">This list is empty</string>
+ <string name="select_from_media_library">Select from media library</string>
+ <string name="jetpack_message">The Jetpack plugin is required for stats. Do you want to install Jetpack?</string>
+ <string name="jetpack_not_found">Jetpack plugin not found</string>
+ <string name="reader_untitled_post">(Untitled)</string>
+ <string name="reader_share_subject">Shared from %s</string>
+ <string name="reader_btn_share">Share</string>
+ <string name="reader_btn_follow">Follow</string>
+ <string name="reader_btn_unfollow">Following</string>
+ <string name="reader_hint_comment_on_comment">Reply to comment…</string>
+ <string name="reader_label_added_tag">Added %s</string>
+ <string name="reader_label_removed_tag">Removed %s</string>
+ <string name="reader_likes_one">One person likes this</string>
+ <string name="reader_likes_only_you">You like this</string>
+ <string name="reader_toast_err_comment_failed">Couldn\'t post your comment</string>
+ <string name="reader_toast_err_tag_exists">You already follow this tag</string>
+ <string name="reader_toast_err_tag_invalid">That isn\'t a valid tag</string>
+ <string name="reader_toast_err_share_intent">Unable to share</string>
+ <string name="reader_toast_err_view_image">Unable to view image</string>
+ <string name="reader_toast_err_url_intent">Unable to open %s</string>
+ <string name="reader_empty_followed_tags">You don\'t follow any tags</string>
+ <string name="create_account_wpcom">Create an account on WordPress.com</string>
+ <string name="button_next">Next</string>
+ <string name="connecting_wpcom">Connecting to WordPress.com</string>
+ <string name="username_invalid">Invalid username</string>
+ <string name="limit_reached">Limit reached. You can try again in 1 minute. Trying again before that will only increase the time you have to wait before the ban is lifted. If you think this is in error, contact support.</string>
+ <string name="nux_tutorial_get_started_title">Get started!</string>
+ <string name="themes">Themes</string>
+ <string name="all">All</string>
+ <string name="images">Images</string>
+ <string name="unattached">Unattached</string>
+ <string name="custom_date">Custom Date</string>
+ <string name="media_add_popup_capture_photo">Capture photo</string>
+ <string name="media_add_popup_capture_video">Capture video</string>
+ <string name="media_gallery_image_order_random">Random</string>
+ <string name="media_gallery_image_order_reverse">Reverse</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="media_gallery_type_squares">Squares</string>
+ <string name="media_gallery_type_tiled">Tiled</string>
+ <string name="media_gallery_type_circles">Circles</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="media_edit_title_text">Title</string>
+ <string name="media_edit_caption_text">Caption</string>
+ <string name="media_edit_description_text">Description</string>
+ <string name="media_edit_title_hint">Enter a title here</string>
+ <string name="media_edit_caption_hint">Enter a caption here</string>
+ <string name="media_edit_description_hint">Enter a description here</string>
+ <string name="media_edit_success">Updated</string>
+ <string name="media_edit_failure">Failed to update</string>
+ <string name="themes_details_label">Details</string>
+ <string name="themes_features_label">Features</string>
+ <string name="theme_activate_button">Activate</string>
+ <string name="theme_activating_button">Activating</string>
+ <string name="theme_set_success">Successfully set theme!</string>
+ <string name="theme_auth_error_title">Failed to fetch themes</string>
+ <string name="post_excerpt">Excerpt</string>
+ <string name="share_action_title">Add to …</string>
+ <string name="share_action">Share</string>
+ <string name="stats">Stats</string>
+ <string name="stats_view_visitors_and_views">Visitors and Views</string>
+ <string name="stats_view_clicks">Clicks</string>
+ <string name="stats_view_tags_and_categories">Tags &amp; Categories</string>
+ <string name="stats_view_referrers">Referrers</string>
+ <string name="stats_timeframe_today">Today</string>
+ <string name="stats_timeframe_yesterday">Yesterday</string>
+ <string name="stats_timeframe_days">Days</string>
+ <string name="stats_timeframe_weeks">Weeks</string>
+ <string name="stats_timeframe_months">Months</string>
+ <string name="stats_entry_country">Country</string>
+ <string name="stats_entry_posts_and_pages">Title</string>
+ <string name="stats_entry_tags_and_categories">Topic</string>
+ <string name="stats_entry_authors">Author</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_totals_views">Views</string>
+ <string name="stats_totals_clicks">Clicks</string>
+ <string name="stats_totals_plays">Plays</string>
+ <string name="passcode_manage">Manage PIN lock</string>
+ <string name="passcode_enter_passcode">Enter your PIN</string>
+ <string name="passcode_enter_old_passcode">Enter your old PIN</string>
+ <string name="passcode_re_enter_passcode">Re-enter your PIN</string>
+ <string name="passcode_change_passcode">Change PIN</string>
+ <string name="passcode_set">PIN set</string>
+ <string name="passcode_preference_title">PIN lock</string>
+ <string name="passcode_turn_off">Turn PIN lock off</string>
+ <string name="passcode_turn_on">Turn PIN lock on</string>
+ <string name="upload">Upload</string>
+ <string name="discard">Discard</string>
+ <string name="sign_in">Sign in</string>
+ <string name="notifications">Notifications</string>
+ <string name="note_reply_successful">Reply published</string>
+ <string name="follows">Follows</string>
+ <string name="new_notifications">%d new notifications</string>
+ <string name="more_notifications">and %d more.</string>
+ <string name="loading">Loading…</string>
+ <string name="httpuser">HTTP username</string>
+ <string name="httppassword">HTTP password</string>
+ <string name="error_media_upload">An error occurred while uploading media</string>
+ <string name="post_content">Content (tap to add text and media)</string>
+ <string name="publish_date">Publish</string>
+ <string name="content_description_add_media">Add media</string>
+ <string name="incorrect_credentials">Incorrect username or password.</string>
+ <string name="password">Password</string>
+ <string name="username">Username</string>
+ <string name="reader">Reader</string>
+ <string name="featured">Use as featured image</string>
+ <string name="featured_in_post">Include image in post content</string>
+ <string name="no_network_title">No network available</string>
+ <string name="pages">Pages</string>
+ <string name="caption">Caption (optional)</string>
+ <string name="width">Width</string>
+ <string name="posts">Posts</string>
+ <string name="anonymous">Anonymous</string>
+ <string name="page">Page</string>
+ <string name="post">Post</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Upload and link to scaled image</string>
+ <string name="scaled_image">Scaled image width</string>
+ <string name="scheduled">Scheduled</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Uploading…</string>
+ <string name="version">Version</string>
+ <string name="tos">Terms of Service</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">Default Image Width</string>
+ <string name="image_alignment">Alignment</string>
+ <string name="refresh">Refresh</string>
+ <string name="untitled">Untitled</string>
+ <string name="edit">Edit</string>
+ <string name="post_id">Post</string>
+ <string name="page_id">Page</string>
+ <string name="post_password">Password (optional)</string>
+ <string name="immediately">Immediately</string>
+ <string name="quickpress_add_alert_title">Set shortcut name</string>
+ <string name="today">Today</string>
+ <string name="settings">Settings</string>
+ <string name="share_url">Share URL</string>
+ <string name="quickpress_window_title">Select blog for QuickPress shortcut</string>
+ <string name="quickpress_add_error">Shortcut name can\'t be empty</string>
+ <string name="publish_post">Publish</string>
+ <string name="draft">Draft</string>
+ <string name="post_private">Private</string>
+ <string name="upload_full_size_image">Upload and link to full image</string>
+ <string name="title">Title</string>
+ <string name="tags_separate_with_commas">Tags (separate tags with commas)</string>
+ <string name="categories">Categories</string>
+ <string name="dlg_deleting_comments">Deleting comments</string>
+ <string name="notification_blink">Blink notification light</string>
+ <string name="notification_sound">Notification sound</string>
+ <string name="notification_vibrate">Vibrate</string>
+ <string name="status">Status</string>
+ <string name="location">Location</string>
+ <string name="sdcard_title">SD Card Required</string>
+ <string name="select_video">Select a video from gallery</string>
+ <string name="media">Media</string>
+ <string name="delete">Delete</string>
+ <string name="none">None</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Select a photo from gallery</string>
+ <string name="error">Error</string>
+ <string name="cancel">Cancel</string>
+ <string name="save">Save</string>
+ <string name="add">Add</string>
+ <string name="category_refresh_error">Category refresh error</string>
+ <string name="preview">Preview</string>
+ <string name="on">on</string>
+ <string name="reply">Reply</string>
+ <string name="notification_settings">Notification Settings</string>
+ <string name="yes">Yes</string>
+ <string name="no">No</string>
+</resources>
diff --git a/WordPress/src/main/res/values-en-rCA/strings.xml b/WordPress/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 000000000..7f619d777
--- /dev/null
+++ b/WordPress/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Author</string>
+ <string name="role_contributor">Contributor</string>
+ <string name="role_follower">Follower</string>
+ <string name="role_viewer">Viewer</string>
+ <string name="error_post_my_profile_no_connection">No connection, couldn\'t save your profile</string>
+ <string name="alignment_none">None</string>
+ <string name="alignment_left">Left</string>
+ <string name="alignment_right">Right</string>
+ <string name="site_settings_list_editor_action_mode_title">Selected %1$d</string>
+ <string name="error_fetch_users_list">Couldn\'t retrieve site users</string>
+ <string name="plans_manage">Manage your plan at\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">You don\'t have any viewers yet.</string>
+ <string name="people_fetching">Fetching users…</string>
+ <string name="title_follower">Follower</string>
+ <string name="title_email_follower">Email Follower</string>
+ <string name="people_empty_list_filtered_email_followers">You don\'t have any email followers yet.</string>
+ <string name="people_empty_list_filtered_followers">You don\'t have any followers yet.</string>
+ <string name="people_empty_list_filtered_users">You don\'t have any users yet.</string>
+ <string name="people_dropdown_item_email_followers">Email Followers</string>
+ <string name="people_dropdown_item_viewers">Viewers</string>
+ <string name="people_dropdown_item_followers">Followers</string>
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="invite_message_usernames_limit">Invite up to 10 email addresses and/or WordPress.com usernames. Those needing a username will be sent instructions on how to create one.</string>
+ <string name="viewer_remove_confirmation_message">If you remove this viewer, he or she will not be able to visit this site.\n\nWould you still like to remove this viewer?</string>
+ <string name="follower_remove_confirmation_message">If removed, this follower will stop receiving notifications about this site, unless they re-follow.\n\nWould you still like to remove this follower?</string>
+ <string name="follower_subscribed_since">Since %1$s</string>
+ <string name="reader_label_view_gallery">View Gallery</string>
+ <string name="error_remove_follower">Couldn\'t remove follower</string>
+ <string name="error_remove_viewer">Couldn\'t remove viewer</string>
+ <string name="error_fetch_email_followers_list">Couldn\'t retrieve site email followers</string>
+ <string name="error_fetch_followers_list">Couldn\'t retrieve site followers</string>
+ <string name="editor_failed_uploads_switch_html">Some media uploads have failed. You can\'t switch to HTML mode\n in this state. Remove all failed uploads and continue?</string>
+ <string name="format_bar_description_html">HTML mode</string>
+ <string name="visual_editor">Visual editor</string>
+ <string name="image_thumbnail">Image thumbnail</string>
+ <string name="format_bar_description_ul">Unordered list</string>
+ <string name="format_bar_description_ol">Ordered list</string>
+ <string name="format_bar_description_more">Insert more</string>
+ <string name="format_bar_description_media">Insert media</string>
+ <string name="format_bar_description_strike">Strikethrough</string>
+ <string name="format_bar_description_quote">Block quote</string>
+ <string name="format_bar_description_link">Insert link</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Underline</string>
+ <string name="image_settings_save_toast">Changes saved</string>
+ <string name="image_caption">Caption</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Link to</string>
+ <string name="image_width">Width</string>
+ <string name="format_bar_description_bold">Bold</string>
+ <string name="image_settings_dismiss_dialog_title">Discard unsaved changes?</string>
+ <string name="stop_upload_dialog_title">Stop uploading?</string>
+ <string name="stop_upload_button">Stop Upload</string>
+ <string name="alert_error_adding_media">An error occurred while inserting media</string>
+ <string name="alert_action_while_uploading">You are currently uploading media. Please wait until this completes.</string>
+ <string name="alert_insert_image_html_mode">Can\'t insert media directly in HTML mode. Please switch back to visual mode.</string>
+ <string name="uploading_gallery_placeholder">Uploading gallery…</string>
+ <string name="invite_error_some_failed">Invite sent but error(s) occurred!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Invite sent successfully</string>
+ <string name="tap_to_try_again">Tap to try again!</string>
+ <string name="invite_error_sending">An error occurred while trying to send the invite!</string>
+ <string name="invite_error_invalid_usernames_multiple">Cannot send: There are invalid usernames or emails</string>
+ <string name="invite_error_invalid_usernames_one">Cannot send: A username or email is invalid</string>
+ <string name="invite_error_no_usernames">Please add at least one username</string>
+ <string name="invite_message_info">(Optional) You can enter a custom message of up to 500 characters that will be included in the invitation to the user(s).</string>
+ <string name="invite_message_remaining_other">%d characters remaining</string>
+ <string name="invite_message_remaining_one">1 character remaining</string>
+ <string name="invite_message_remaining_zero">0 characters remaining</string>
+ <string name="invite_invalid_email">The email address \'%s\' is invalid</string>
+ <string name="invite_message_title">Custom Message</string>
+ <string name="invite_already_a_member">There\'s already a member with username \'%s\'</string>
+ <string name="invite_username_not_found">No user was found for username \'%s\'</string>
+ <string name="invite">Invite</string>
+ <string name="invite_names_title">Usernames or Emails</string>
+ <string name="signup_succeed_signin_failed">Your account has been created but an error occured while we signed you\n in. Try to sign in with your newly created username and password.</string>
+ <string name="send_link">Send link</string>
+ <string name="my_site_header_external">External</string>
+ <string name="invite_people">Invite People</string>
+ <string name="label_clear_search_history">Clear search history</string>
+ <string name="dlg_confirm_clear_search_history">Clear search history?</string>
+ <string name="reader_empty_posts_in_search_description">No posts found for %s for your language</string>
+ <string name="reader_label_post_search_running">Searching…</string>
+ <string name="reader_label_related_posts">Related Reading</string>
+ <string name="reader_empty_posts_in_search_title">No posts found</string>
+ <string name="reader_label_post_search_explainer">Search all public WordPress.com blogs</string>
+ <string name="reader_hint_post_search">Search WordPress.com</string>
+ <string name="reader_title_related_post_detail">Related Post</string>
+ <string name="reader_title_search_results">Search for %s</string>
+ <string name="preview_screen_links_disabled">Links are disabled on the preview screen</string>
+ <string name="draft_explainer">This post is a draft which hasn\'t been published</string>
+ <string name="send">Send</string>
+ <string name="user_remove_confirmation_message">If you remove %1$s, that user will no longer be able to access this site, but any content that was created by %1$s will remain on the site.\n\nWould you still like to remove this user?</string>
+ <string name="person_removed">Successfully removed %1$s</string>
+ <string name="person_remove_confirmation_title">Remove %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">The sites in this list haven\'t posted anything recently</string>
+ <string name="people">People</string>
+ <string name="edit_user">Edit User</string>
+ <string name="role">Role</string>
+ <string name="error_remove_user">Couldn\'t remove user</string>
+ <string name="error_fetch_viewers_list">Couldn\'t retrieve site viewers</string>
+ <string name="error_update_role">Couldn\'t update user role</string>
+ <string name="gravatar_camera_and_media_permission_required">Permissions required in order to select or capture a photo</string>
+ <string name="error_updating_gravatar">Error updating your Gravatar</string>
+ <string name="error_locating_image">Error locating the cropped image</string>
+ <string name="error_refreshing_gravatar">Error reloading your Gravatar</string>
+ <string name="gravatar_tip">New! Tap your Gravatar to change it!</string>
+ <string name="error_cropping_image">Error cropping the image</string>
+ <string name="launch_your_email_app">Launch your email app</string>
+ <string name="checking_email">Checking email</string>
+ <string name="not_on_wordpress_com">Not on WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Currently unavailable. Please enter your password</string>
+ <string name="check_your_email">Check your email</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Get a link sent to your email to sign in instantly</string>
+ <string name="logging_in">Logging in</string>
+ <string name="enter_your_password_instead">Enter your password instead</string>
+ <string name="web_address_dialog_hint">Shown publicly when you comment.</string>
+ <string name="jetpack_not_connected_message">The Jetpack plugin is installed, but not connected to WordPress.com. Do you want to connect Jetpack?</string>
+ <string name="username_email">Email or username</string>
+ <string name="jetpack_not_connected">Jetpack plugin not connected</string>
+ <string name="new_editor_reflection_error">Visual editor is not compatible with your device. It was\n automatically disabled.</string>
+ <string name="stats_insights_latest_post_no_title">(no title)</string>
+ <string name="capture_or_pick_photo">Capture or select photo</string>
+ <string name="plans_post_purchase_text_themes">You now have unlimited access to Premium themes. Preview any theme on your site to get started.</string>
+ <string name="plans_post_purchase_button_themes">Browse Themes</string>
+ <string name="plans_post_purchase_title_themes">Find a perfect, Premium theme</string>
+ <string name="plans_post_purchase_button_video">Start new post</string>
+ <string name="plans_post_purchase_text_video">You can upload and host videos on your site with VideoPress and your expanded media storage.</string>
+ <string name="plans_post_purchase_title_video">Bring posts to life with video</string>
+ <string name="plans_post_purchase_button_customize">Customize my Site</string>
+ <string name="plans_post_purchase_text_customize">You now have access to custom fonts, custom colours, and custom CSS editing capabilities.</string>
+ <string name="plans_post_purchase_text_intro">Your site is doing somersaults in excitement! Now explore your site\'s new features and choose where you\'d like to begin.</string>
+ <string name="plans_post_purchase_title_customize">Customize Fonts &amp; Colours</string>
+ <string name="plans_post_purchase_title_intro">It\'s all yours, way to go!</string>
+ <string name="export_your_content_message">Your posts, pages, and settings will be emailed to you at %s.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Unable to load plans</string>
+ <string name="export_your_content">Export your content</string>
+ <string name="exporting_content_progress">Exporting content…</string>
+ <string name="export_email_sent">Export email sent!</string>
+ <string name="premium_upgrades_message">You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site.</string>
+ <string name="show_purchases">Show purchases</string>
+ <string name="checking_purchases">Checking purchases</string>
+ <string name="premium_upgrades_title">Premium Upgrades</string>
+ <string name="purchases_request_error">Something went wrong. Could not request purchases.</string>
+ <string name="delete_site_progress">Deleting site…</string>
+ <string name="delete_site_summary">This action can not be undone. Deleting your site will remove all content, contributors, and domains from the site.</string>
+ <string name="delete_site_hint">Delete site</string>
+ <string name="export_site_hint">Export your site to an XML file</string>
+ <string name="are_you_sure">Are You Sure?</string>
+ <string name="export_site_summary">If you are sure, please be sure to take the time and export your content now. It can not be recovered in the future.</string>
+ <string name="keep_your_content">Keep Your Content</string>
+ <string name="domain_removal_hint">The domains that will not work once you remove your site</string>
+ <string name="domain_removal_summary">Be careful! Deleting your site will also remove your domain(s) listed below.</string>
+ <string name="primary_domain">Primary Domain</string>
+ <string name="domain_removal">Domain Removal</string>
+ <string name="error_deleting_site_summary">There was an error in deleting your site. Please contact support for more assistance</string>
+ <string name="error_deleting_site">Error deleting site</string>
+ <string name="confirm_delete_site_prompt">Please type in %1$s in the field below to confirm. Your site will then be gone forever.</string>
+ <string name="site_settings_export_content_title">Export content</string>
+ <string name="contact_support">Contact support</string>
+ <string name="confirm_delete_site">Confirm Delete Site</string>
+ <string name="start_over_text">If you want a site but don\'t want any of the posts and pages you have now, our support team can delete your posts, pages, media and comments for you.\n\nThis will keep your site and URL active, but give you a fresh start on your content creation. Just contact us to have your current content cleared out.</string>
+ <string name="site_settings_start_over_hint">Start your site over</string>
+ <string name="let_us_help">Let Us Help</string>
+ <string name="me_btn_app_settings">App Settings</string>
+ <string name="start_over">Start Over</string>
+ <string name="editor_remove_failed_uploads">Remove failed uploads</string>
+ <string name="editor_toast_failed_uploads">Some media uploads have failed. You can\'t save or publish\n your post in this state. Would you like to remove all failed media?</string>
+ <string name="comments_empty_list_filtered_trashed">No Trashed comments</string>
+ <string name="site_settings_advanced_header">Advanced</string>
+ <string name="comments_empty_list_filtered_pending">No Pending comments</string>
+ <string name="comments_empty_list_filtered_approved">No Approved comments</string>
+ <string name="button_done">Done</string>
+ <string name="button_skip">Skip</string>
+ <string name="site_timeout_error">Couldn\'t connect to the WordPress site due to Timeout error.</string>
+ <string name="xmlrpc_malformed_response_error">Couldn\'t connect. The WordPress installation responded with an invalid XML-RPC document.</string>
+ <string name="xmlrpc_missing_method_error">Couldn\'t connect. Required XML-RPC methods are missing on the server.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Centre</string>
+ <string name="theme_free">Free</string>
+ <string name="theme_all">All</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Gallery</string>
+ <string name="post_format_image">Image</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Quote</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Information on WordPress.com courses and events (online &amp; in-person).</string>
+ <string name="post_format_aside">Aside</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Opportunities to participate in WordPress.com research &amp; surveys.</string>
+ <string name="notif_tips">Tips for getting the most out of WordPress.com.</string>
+ <string name="notif_community">Community</string>
+ <string name="replies_to_my_comments">Replies to my comments</string>
+ <string name="notif_suggestions">Suggestions</string>
+ <string name="notif_research">Research</string>
+ <string name="site_achievements">Site achievements</string>
+ <string name="username_mentions">Username mentions</string>
+ <string name="likes_on_my_posts">Likes on my posts</string>
+ <string name="site_follows">Site follows</string>
+ <string name="likes_on_my_comments">Likes on my comments</string>
+ <string name="comments_on_my_site">Comments on my site</string>
+ <string name="site_settings_list_editor_summary_other">%d items</string>
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="approve_auto_if_previously_approved">Known users\' comments</string>
+ <string name="approve_auto">All users</string>
+ <string name="approve_manual">No comments</string>
+ <string name="site_settings_paging_summary_other">%d comments per page</string>
+ <string name="site_settings_paging_summary_one">1 comment per page</string>
+ <string name="site_settings_multiple_links_summary_other">Require approval for more than %d links</string>
+ <string name="site_settings_multiple_links_summary_one">Require approval for more than 1 link</string>
+ <string name="site_settings_multiple_links_summary_zero">Require approval for more than 0 links</string>
+ <string name="detail_approve_auto">Automatically approve everyone\'s comments.</string>
+ <string name="detail_approve_auto_if_previously_approved">Automatically approve if the user has a previously approved comment</string>
+ <string name="detail_approve_manual">Require manual approval for everyone\'s comments.</string>
+ <string name="filter_trashed_posts">Trashed</string>
+ <string name="days_quantity_one">1 day</string>
+ <string name="days_quantity_other">%d days</string>
+ <string name="filter_published_posts">Published</string>
+ <string name="filter_draft_posts">Drafts</string>
+ <string name="filter_scheduled_posts">Scheduled</string>
+ <string name="pending_email_change_snackbar">Click the verification link in the email sent to %1$s to confirm your new address</string>
+ <string name="primary_site">Primary site</string>
+ <string name="web_address">Web Address</string>
+ <string name="editor_toast_uploading_please_wait">You are currently uploading media. Please wait until this completes.</string>
+ <string name="error_refresh_comments_showing_older">Comments couldn\'t be refreshed at this time - showing older comments</string>
+ <string name="editor_post_settings_set_featured_image">Set Featured Image</string>
+ <string name="editor_post_settings_featured_image">Featured Image</string>
+ <string name="new_editor_promo_desc">The WordPress app for Android now includes a beautiful new visual\n editor. Try it out by creating a new post.</string>
+ <string name="new_editor_promo_title">Brand new editor</string>
+ <string name="new_editor_promo_button_label">Great, thanks!</string>
+ <string name="visual_editor_enabled">Visual Editor enabled</string>
+ <string name="editor_content_placeholder">Share your story here…</string>
+ <string name="editor_page_title_placeholder">Page Title</string>
+ <string name="editor_post_title_placeholder">Post Title</string>
+ <string name="email_address">Email address</string>
+ <string name="preference_show_visual_editor">Show visual editor</string>
+ <string name="dlg_sure_to_delete_comments">Permanently delete these comments?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Permanently delete this comment?</string>
+ <string name="mnu_comment_delete_permanently">Delete</string>
+ <string name="comment_deleted_permanently">Comment deleted</string>
+ <string name="mnu_comment_untrash">Restore</string>
+ <string name="comments_empty_list_filtered_spam">No Spam comments</string>
+ <string name="could_not_load_page">Could not load page</string>
+ <string name="comment_status_all">All</string>
+ <string name="interface_language">Interface Language</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">About the app</string>
+ <string name="error_post_account_settings">Couldn\'t save your account settings</string>
+ <string name="error_post_my_profile">Couldn\'t save your profile</string>
+ <string name="error_fetch_account_settings">Couldn\'t retrieve your account settings</string>
+ <string name="error_fetch_my_profile">Couldn\'t retrieve your profile</string>
+ <string name="stats_widget_promo_ok_btn_label">OK, got it</string>
+ <string name="stats_widget_promo_desc">Add the widget to your home screen to access your Stats in one click.</string>
+ <string name="stats_widget_promo_title">Home Screen Stats Widget</string>
+ <string name="site_settings_unknown_language_code_error">Language code not recognised</string>
+ <string name="site_settings_threading_dialog_description">Allow comments to be nested in threads.</string>
+ <string name="site_settings_threading_dialog_header">Thread up to</string>
+ <string name="remove">Remove</string>
+ <string name="search">Search</string>
+ <string name="add_category">Add category</string>
+ <string name="disabled">Disabled</string>
+ <string name="site_settings_image_original_size">Original Size</string>
+ <string name="privacy_private">Your site is visible only to you and users you approve</string>
+ <string name="privacy_public_not_indexed">Your site is visible to everyone but asks search engines not to index it</string>
+ <string name="privacy_public">Your site is visible to everyone and may be indexed by search engines</string>
+ <string name="about_me_hint">A few words about you…</string>
+ <string name="public_display_name_hint">Display name will default to your username if it is not set</string>
+ <string name="about_me">About me</string>
+ <string name="public_display_name">Public display name</string>
+ <string name="my_profile">My Profile</string>
+ <string name="first_name">First name</string>
+ <string name="last_name">Last name</string>
+ <string name="site_privacy_public_desc">Allow search engines to index this site</string>
+ <string name="site_privacy_hidden_desc">Discourage search engines from indexing this site</string>
+ <string name="site_privacy_private_desc">I would like my site to be private, visible only to users I choose</string>
+ <string name="cd_related_post_preview_image">Related post preview image</string>
+ <string name="error_post_remote_site_settings">Couldn\'t save site info</string>
+ <string name="error_fetch_remote_site_settings">Couldn\'t retrieve site info</string>
+ <string name="error_media_upload_connection">A connection error occurred while uploading media</string>
+ <string name="site_settings_disconnected_toast">Disconnected, editing disabled.</string>
+ <string name="site_settings_unsupported_version_error">Unsupported WordPress version</string>
+ <string name="site_settings_multiple_links_dialog_description">Require approval for comments that include more than this number of links.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatically close</string>
+ <string name="site_settings_close_after_dialog_description">Automatically close comments on articles.</string>
+ <string name="site_settings_paging_dialog_description">Break comment threads into multiple pages.</string>
+ <string name="site_settings_paging_dialog_header">Comments per page</string>
+ <string name="site_settings_close_after_dialog_title">Close commenting</string>
+ <string name="site_settings_blacklist_description">When a comment contains any of these words in its content, name, URL, email, or IP, it will be marked as spam. You can enter partial words, so "press" will match "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">When a comment contains any of these words in its content, name, URL, email, or IP, it will be held in the moderation queue. You can enter partial words, so "press" will match "WordPress."</string>
+ <string name="site_settings_list_editor_input_hint">Enter a word or phrase</string>
+ <string name="site_settings_list_editor_no_items_text">No items</string>
+ <string name="site_settings_learn_more_caption">You can override these settings for individual posts.</string>
+ <string name="site_settings_rp_preview3_site">in "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Upgrade Focus: VideoPress For Weddings</string>
+ <string name="site_settings_rp_preview2_site">in "Apps"</string>
+ <string name="site_settings_rp_preview2_title">The WordPress for Android App Gets a Big Facelift</string>
+ <string name="site_settings_rp_preview1_site">in "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Big iPhone/iPad Update Now Available</string>
+ <string name="site_settings_rp_show_images_title">Show Images</string>
+ <string name="site_settings_rp_show_header_title">Show Header</string>
+ <string name="site_settings_rp_switch_summary">Related Posts displays relevant content from your site below your posts.</string>
+ <string name="site_settings_rp_switch_title">Show Related Posts</string>
+ <string name="site_settings_delete_site_hint">Removes your site data from the app</string>
+ <string name="site_settings_blacklist_hint">Comments that match a filter are marked as spam</string>
+ <string name="site_settings_moderation_hold_hint">Comments that match a filter are put in the moderation queue</string>
+ <string name="site_settings_multiple_links_hint">Ignores link limit from known users</string>
+ <string name="site_settings_whitelist_hint">Comment author must have a previously approved comment</string>
+ <string name="site_settings_user_account_required_hint">Users must be registered and logged in to comment</string>
+ <string name="site_settings_identity_required_hint">Comment author must fill out name and e-mail</string>
+ <string name="site_settings_manual_approval_hint">Comments must be manually approved</string>
+ <string name="site_settings_paging_hint">Display comments in chunks of a specified size</string>
+ <string name="site_settings_threading_hint">Allow nested comments to a certain depth</string>
+ <string name="site_settings_sort_by_hint">Determines the order comments are displayed</string>
+ <string name="site_settings_close_after_hint">Disallow comments after the specified time</string>
+ <string name="site_settings_receive_pingbacks_hint">Allow link notifications from other blogs</string>
+ <string name="site_settings_send_pingbacks_hint">Attempt to notify any blogs linked to from the article</string>
+ <string name="site_settings_allow_comments_hint">Allow readers to post comments</string>
+ <string name="site_settings_discussion_hint">View and change your sites discussion settings</string>
+ <string name="site_settings_more_hint">View all available Discussion settings</string>
+ <string name="site_settings_related_posts_hint">Show or hide related posts in reader</string>
+ <string name="site_settings_upload_and_link_image_hint">Enable to always upload the fullsize image</string>
+ <string name="site_settings_image_width_hint">Resizes images in posts to this width</string>
+ <string name="site_settings_format_hint">Sets new post format</string>
+ <string name="site_settings_category_hint">Sets new post category</string>
+ <string name="site_settings_location_hint">Automatically add location data to your posts</string>
+ <string name="site_settings_password_hint">Change your password</string>
+ <string name="site_settings_username_hint">Current user account</string>
+ <string name="site_settings_language_hint">Language this blog is primarily written in</string>
+ <string name="site_settings_privacy_hint">Controls who can see your site</string>
+ <string name="site_settings_address_hint">Changing your address is not currently supported</string>
+ <string name="site_settings_tagline_hint">A short description or catchy phrase to describe your blog</string>
+ <string name="site_settings_title_hint">In a few words, explain what this site is about</string>
+ <string name="site_settings_whitelist_known_summary">Comments from known users</string>
+ <string name="site_settings_whitelist_all_summary">Comments from all users</string>
+ <string name="site_settings_threading_summary">%d levels</string>
+ <string name="site_settings_privacy_private_summary">Private</string>
+ <string name="site_settings_privacy_hidden_summary">Hidden</string>
+ <string name="site_settings_delete_site_title">Delete Site</string>
+ <string name="site_settings_privacy_public_summary">Public</string>
+ <string name="site_settings_blacklist_title">Blacklist</string>
+ <string name="site_settings_moderation_hold_title">Hold for Moderation</string>
+ <string name="site_settings_multiple_links_title">Links in comments</string>
+ <string name="site_settings_whitelist_title">Automatically approve</string>
+ <string name="site_settings_threading_title">Threading</string>
+ <string name="site_settings_paging_title">Paging</string>
+ <string name="site_settings_sort_by_title">Sort by</string>
+ <string name="site_settings_account_required_title">Users must be signed in</string>
+ <string name="site_settings_identity_required_title">Must include name and email</string>
+ <string name="site_settings_receive_pingbacks_title">Receive Pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Send Pingbacks</string>
+ <string name="site_settings_allow_comments_title">Allow Comments</string>
+ <string name="site_settings_default_format_title">Default Format</string>
+ <string name="site_settings_default_category_title">Default Category</string>
+ <string name="site_settings_location_title">Enable Location</string>
+ <string name="site_settings_address_title">Address</string>
+ <string name="site_settings_title_title">Site Title</string>
+ <string name="site_settings_tagline_title">Tagline</string>
+ <string name="site_settings_this_device_header">This device</string>
+ <string name="site_settings_discussion_new_posts_header">Defaults for new posts</string>
+ <string name="site_settings_account_header">Account</string>
+ <string name="site_settings_writing_header">Writing</string>
+ <string name="newest_first">Newest first</string>
+ <string name="site_settings_general_header">General</string>
+ <string name="discussion">Discussion</string>
+ <string name="privacy">Privacy</string>
+ <string name="related_posts">Related Posts</string>
+ <string name="comments">Comments</string>
+ <string name="close_after">Close after</string>
+ <string name="oldest_first">Oldest first</string>
+ <string name="media_error_no_permission_upload">You don\'t have permission to upload media to the site</string>
+ <string name="never">Never</string>
+ <string name="unknown">Unknown</string>
+ <string name="reader_err_get_post_not_found">This post no longer exists</string>
+ <string name="reader_err_get_post_not_authorized">You\'re not authorized to view this post</string>
+ <string name="reader_err_get_post_generic">Unable to retrieve this post</string>
+ <string name="blog_name_no_spaced_allowed">Site address can\'t contain spaces</string>
+ <string name="invalid_username_no_spaces">Username can\'t contain spaces</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">The sites you follow haven\'t posted anything recently</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">No recent posts</string>
+ <string name="media_details_copy_url_toast">URL copied to clipboard</string>
+ <string name="edit_media">Edit media</string>
+ <string name="media_details_copy_url">Copy URL</string>
+ <string name="media_details_label_date_uploaded">Uploaded</string>
+ <string name="media_details_label_date_added">Added</string>
+ <string name="selected_theme">Selected Theme</string>
+ <string name="could_not_load_theme">Could not load theme</string>
+ <string name="theme_activation_error">Something went wrong. Could not activate theme</string>
+ <string name="theme_by_author_prompt_append"> by %1$s</string>
+ <string name="theme_prompt">Thanks for choosing %1$s</string>
+ <string name="theme_try_and_customize">Try &amp; Customize</string>
+ <string name="theme_view">View</string>
+ <string name="theme_details">Details</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">DONE</string>
+ <string name="theme_manage_site">MANAGE SITE</string>
+ <string name="title_activity_theme_support">Themes</string>
+ <string name="theme_activate">Activate</string>
+ <string name="date_range_start_date">Start Date</string>
+ <string name="date_range_end_date">End Date</string>
+ <string name="current_theme">Current Theme</string>
+ <string name="customize">Customize</string>
+ <string name="details">Details</string>
+ <string name="support">Support</string>
+ <string name="active">Active</string>
+ <string name="stats_referrers_spam_generic_error">Something went wrong during the operation. The spam state wasn\'t changed.</string>
+ <string name="stats_referrers_marking_not_spam">Marking as not spam</string>
+ <string name="stats_referrers_unspam">Not spam</string>
+ <string name="stats_referrers_marking_spam">Marking as spam</string>
+ <string name="theme_auth_error_authenticate">Failed to fetch themes: failed authenticating user</string>
+ <string name="post_published">Post published</string>
+ <string name="page_published">Page published</string>
+ <string name="post_updated">Post updated</string>
+ <string name="page_updated">Page updated</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Sorry, no themes found.</string>
+ <string name="media_file_name">File name: %s</string>
+ <string name="media_uploaded_on">Uploaded on: %s</string>
+ <string name="media_dimensions">Dimensions: %s</string>
+ <string name="upload_queued">Queued</string>
+ <string name="media_file_type">File type: %s</string>
+ <string name="reader_label_gap_marker">Load more posts</string>
+ <string name="notifications_no_search_results">No sites matched \'%s\'</string>
+ <string name="search_sites">Search sites</string>
+ <string name="notifications_empty_view_reader">View Reader</string>
+ <string name="unread">Unread</string>
+ <string name="notifications_empty_action_followers_likes">Get noticed: comment on posts you\'ve read.</string>
+ <string name="notifications_empty_action_comments">Join a conversation: comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_action_unread">Reignite the conversation: write a new post.</string>
+ <string name="notifications_empty_action_all">Get active! Comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_likes">No new likes to show…yet.</string>
+ <string name="notifications_empty_followers">No new followers to report…yet.</string>
+ <string name="notifications_empty_comments">No new comments…yet.</string>
+ <string name="notifications_empty_unread">You\'re all caught up!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Please access the Stats in the app, and try adding the widget later</string>
+ <string name="stats_widget_error_readd_widget">Please remove the widget and re-add it again</string>
+ <string name="stats_widget_error_no_visible_blog">Stats couldn\'t be accessed without a visible blog</string>
+ <string name="stats_widget_error_no_permissions">Your WordPress.com account can\'t access Stats on this blog</string>
+ <string name="stats_widget_error_no_account">Please login into WordPress</string>
+ <string name="stats_widget_error_generic">Stats couldn\'t be loaded</string>
+ <string name="stats_widget_loading_data">Loading data…</string>
+ <string name="stats_widget_name_for_blog">Today\'s Stats for %1$s</string>
+ <string name="stats_widget_name">WordPress Today\'s Stats</string>
+ <string name="add_location_permission_required">Permission required in order to add location</string>
+ <string name="add_media_permission_required">Permissions required in order to add media</string>
+ <string name="access_media_permission_required">Permissions required in order to access media</string>
+ <string name="stats_enable_rest_api_in_jetpack">To view your stats, enable the JSON API module in Jetpack.</string>
+ <string name="error_open_list_from_notification">This post or page was published on another site</string>
+ <string name="reader_short_comment_count_multi">%s Comments</string>
+ <string name="reader_short_comment_count_one">1 Comment</string>
+ <string name="reader_label_submit_comment">SEND</string>
+ <string name="reader_hint_comment_on_post">Reply to post…</string>
+ <string name="reader_discover_visit_blog">Visit %s</string>
+ <string name="reader_discover_attribution_blog">Originally posted on %s</string>
+ <string name="reader_discover_attribution_author">Originally posted by %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originally posted by %1$s on %2$s</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_label_follow_count">%,d followers</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_menu_tags">Edit tags and blogs</string>
+ <string name="reader_title_post_detail">Reader Post</string>
+ <string name="local_draft_explainer">This post is a local draft which hasn\'t been published</string>
+ <string name="local_changes_explainer">This post has local changes which haven\'t been published</string>
+ <string name="notifications_push_summary">Settings for notifications that appear on your device.</string>
+ <string name="notifications_email_summary">Settings for notifications that are sent to the email tied to your account.</string>
+ <string name="notifications_tab_summary">Settings for notifications that appear in the Notifications tab.</string>
+ <string name="notifications_disabled">App notifications have been disabled. Tap here to enable them in Settings.</string>
+ <string name="notification_types">Notification Types</string>
+ <string name="error_loading_notifications">Couldn\'t load notification settings</string>
+ <string name="replies_to_your_comments">Replies to your comments</string>
+ <string name="comment_likes">Comment likes</string>
+ <string name="app_notifications">App notifications</string>
+ <string name="notifications_tab">Notifications tab</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Comments on other sites</string>
+ <string name="notifications_wpcom_updates">WordPress.com Updates</string>
+ <string name="notifications_other">Other</string>
+ <string name="notifications_account_emails">Email from WordPress.com</string>
+ <string name="notifications_account_emails_summary">We\'ll always send important emails regarding your account, but you can get some helpful extras, too.</string>
+ <string name="notifications_sights_and_sounds">Sights and Sounds</string>
+ <string name="your_sites">Your Sites</string>
+ <string name="stats_insights_latest_post_trend">It\'s been %1$s since %2$s was published. Here\'s how the post has performed so far…</string>
+ <string name="stats_insights_latest_post_summary">Latest Post Summary</string>
+ <string name="button_revert">Revert</string>
+ <string name="days_ago">%d days ago</string>
+ <string name="yesterday">Yesterday</string>
+ <string name="connectionbar_no_connection">No connection</string>
+ <string name="page_trashed">Page sent to trash</string>
+ <string name="post_deleted">Post deleted</string>
+ <string name="post_trashed">Post sent to trash</string>
+ <string name="stats_no_activity_this_period">No activity this period</string>
+ <string name="trashed">Trashed</string>
+ <string name="button_back">Back</string>
+ <string name="page_deleted">Page deleted</string>
+ <string name="button_stats">Stats</string>
+ <string name="button_trash">Trash</string>
+ <string name="button_preview">Preview</string>
+ <string name="button_view">View</string>
+ <string name="button_edit">Edit</string>
+ <string name="button_publish">Publish</string>
+ <string name="my_site_no_sites_view_subtitle">Would you like to add one?</string>
+ <string name="my_site_no_sites_view_title">You don\'t have any WordPress sites yet.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">You are not authorized to access this blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">This blog could not be found</string>
+ <string name="undo">Undo</string>
+ <string name="tabbar_accessibility_label_my_site">My Site</string>
+ <string name="tabbar_accessibility_label_me">Me</string>
+ <string name="passcodelock_prompt_message">Enter your PIN</string>
+ <string name="editor_toast_changes_saved">Changes saved</string>
+ <string name="push_auth_expired">The request has expired. Sign in to WordPress.com to try again.</string>
+ <string name="stats_insights_best_ever">Best Views Ever</string>
+ <string name="ignore">Ignore</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% of views</string>
+ <string name="stats_insights_most_popular_hour">Most popular hour</string>
+ <string name="stats_insights_most_popular_day">Most popular day</string>
+ <string name="stats_insights_popular">Most popular day and hour</string>
+ <string name="stats_insights_today">Today\'s Stats</string>
+ <string name="stats_insights_all_time">All-time posts, views, and visitors</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">To view your stats, sign in to the WordPress.com account you used to connect Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Looking for your Other Recent Stats? We\'ve moved them to the Insights page.</string>
+ <string name="me_disconnect_from_wordpress_com">Disconnect from WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Connect to WordPress.com</string>
+ <string name="me_btn_login_logout">Login/Logout</string>
+ <string name="account_settings">Account Settings</string>
+ <string name="me_btn_support">Help &amp; Support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" wasn\'t hidden because it\'s the current site</string>
+ <string name="site_picker_create_dotcom">Create WordPress.com site</string>
+ <string name="site_picker_add_site">Add site</string>
+ <string name="site_picker_add_self_hosted">Add self-hosted site</string>
+ <string name="site_picker_edit_visibility">Show/hide sites</string>
+ <string name="my_site_btn_view_admin">View Admin</string>
+ <string name="my_site_btn_view_site">View Site</string>
+ <string name="site_picker_title">Choose site</string>
+ <string name="my_site_btn_switch_site">Switch Site</string>
+ <string name="my_site_btn_blog_posts">Blog Posts</string>
+ <string name="my_site_btn_site_settings">Settings</string>
+ <string name="my_site_header_look_and_feel">Look and Feel</string>
+ <string name="my_site_header_publish">Publish</string>
+ <string name="my_site_header_configuration">Configuration</string>
+ <string name="reader_label_new_posts_subtitle">Tap to show them</string>
+ <string name="notifications_account_required">Sign in to WordPress.com for notifications</string>
+ <string name="stats_unknown_author">Unknown Author</string>
+ <string name="image_added">Image added</string>
+ <string name="signout">Disconnect</string>
+ <string name="deselect_all">Deselect all</string>
+ <string name="show">Show</string>
+ <string name="hide">Hide</string>
+ <string name="select_all">Select all</string>
+ <string name="sign_out_wpcom_confirm">Disconnecting your account will remove all of @%s’s WordPress.com data from this device, including local drafts and local changes.</string>
+ <string name="select_from_new_picker">Multi-select with the new picker</string>
+ <string name="stats_generic_error">Required Stats couldn\'t be loaded</string>
+ <string name="no_device_videos">No videos</string>
+ <string name="no_blog_images">No images</string>
+ <string name="no_blog_videos">No videos</string>
+ <string name="no_device_images">No images</string>
+ <string name="error_loading_blog_images">Unable to fetch images</string>
+ <string name="error_loading_blog_videos">Unable to fetch videos</string>
+ <string name="error_loading_images">Error loading images</string>
+ <string name="error_loading_videos">Error loading videos</string>
+ <string name="loading_blog_images">Fetching images</string>
+ <string name="loading_blog_videos">Fetching videos</string>
+ <string name="no_media_sources">Couldn\'t fetch media</string>
+ <string name="loading_videos">Loading videos</string>
+ <string name="loading_images">Loading images</string>
+ <string name="no_media">No media</string>
+ <string name="device">Device</string>
+ <string name="language">Language</string>
+ <string name="add_to_post">Add to Post</string>
+ <string name="media_picker_title">Select media</string>
+ <string name="take_photo">Take a photo</string>
+ <string name="take_video">Take a video</string>
+ <string name="tab_title_device_images">Device Images</string>
+ <string name="tab_title_device_videos">Device Videos</string>
+ <string name="tab_title_site_images">Site Images</string>
+ <string name="tab_title_site_videos">Site Videos</string>
+ <string name="media_details_label_file_name">File name</string>
+ <string name="media_details_label_file_type">File type</string>
+ <string name="error_publish_no_network">Can\'t publish while there is no connection. Saved as draft.</string>
+ <string name="editor_toast_invalid_path">Invalid file path</string>
+ <string name="verification_code">Verification code</string>
+ <string name="invalid_verification_code">Invalid verification code</string>
+ <string name="verify">Verify</string>
+ <string name="two_step_footer_label">Enter the code from your authenticator app.</string>
+ <string name="two_step_footer_button">Send code via text message</string>
+ <string name="two_step_sms_sent">Check your text messages for the verification code.</string>
+ <string name="sign_in_jetpack">Sign in to your WordPress.com account to connect to Jetpack.</string>
+ <string name="auth_required">Sign in again to continue.</string>
+ <string name="reader_empty_posts_request_failed">Unable to retrieve posts</string>
+ <string name="publisher">Publisher:</string>
+ <string name="error_notification_open">Could not open notification</string>
+ <string name="stats_followers_total_email_paged">Showing %1$d - %2$d of %3$s Email Followers</string>
+ <string name="stats_search_terms_unknown_search_terms">Unknown Search Terms</string>
+ <string name="stats_followers_total_wpcom_paged">Showing %1$d - %2$d of %3$s WordPress.com Followers</string>
+ <string name="stats_empty_search_terms_desc">Learn more about your search traffic by looking at the terms your visitors searched for to find your site.</string>
+ <string name="stats_empty_search_terms">No search terms recorded</string>
+ <string name="stats_entry_search_terms">Search Term</string>
+ <string name="stats_view_authors">Authors</string>
+ <string name="stats_view_search_terms">Search Terms</string>
+ <string name="comments_fetching">Fetching comments…</string>
+ <string name="pages_fetching">Fetching pages…</string>
+ <string name="toast_err_post_uploading">Unable to open post while it\'s uploading</string>
+ <string name="posts_fetching">Fetching posts…</string>
+ <string name="media_fetching">Fetching media…</string>
+ <string name="post_uploading">Uploading</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">Overall</string>
+ <string name="stats_period">Period</string>
+ <string name="logs_copied_to_clipboard">Application logs have been copied to the clipboard</string>
+ <string name="reader_label_new_posts">New posts</string>
+ <string name="reader_empty_posts_in_blog">This blog is empty</string>
+ <string name="stats_average_per_day">Average per Day</string>
+ <string name="stats_recent_weeks">Recent Weeks</string>
+ <string name="error_copy_to_clipboard">An error occurred while copying text to clipboard</string>
+ <string name="reader_page_recommended_blogs">Sites you may like</string>
+ <string name="stats_months_and_years">Months and Years</string>
+ <string name="themes_fetching">Fetching themes…</string>
+ <string name="stats_for">Stats for %s</string>
+ <string name="stats_other_recent_stats_label">Other Recent Stats</string>
+ <string name="stats_view_all">View all</string>
+ <string name="stats_view">View</string>
+ <string name="stats_followers_months">%1$d months</string>
+ <string name="stats_followers_a_year">A year</string>
+ <string name="stats_followers_years">%1$d years</string>
+ <string name="stats_followers_a_month">A month</string>
+ <string name="stats_followers_minutes">%1$d minutes</string>
+ <string name="stats_followers_an_hour_ago">an hour ago</string>
+ <string name="stats_followers_hours">%1$d hours</string>
+ <string name="stats_followers_a_day">A day</string>
+ <string name="stats_followers_days">%1$d days</string>
+ <string name="stats_followers_a_minute_ago">a minute ago</string>
+ <string name="stats_followers_seconds_ago">seconds ago</string>
+ <string name="stats_followers_total_email">Total Email Followers: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Total WordPress.com Followers: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Total posts with comment followers: %1$s</string>
+ <string name="stats_comments_by_authors">By Authors</string>
+ <string name="stats_comments_by_posts_and_pages">By Posts &amp; Pages</string>
+ <string name="stats_empty_followers_desc">Keep track of your overall number of followers, and how long each one has been following your site.</string>
+ <string name="stats_empty_followers">No followers</string>
+ <string name="stats_empty_publicize_desc">Keep track of your followers from various social networking services using publicize.</string>
+ <string name="stats_empty_publicize">No publicize followers recorded</string>
+ <string name="stats_empty_video">No videos played</string>
+ <string name="stats_empty_video_desc">If you\'ve uploaded videos using VideoPress, find out how many times they’ve been watched.</string>
+ <string name="stats_empty_comments_desc">If you allow comments on your site, track your top commenters and discover what content sparks the liveliest conversations, based on the most recent 1,000 comments.</string>
+ <string name="stats_empty_tags_and_categories_desc">Get an overview of the most popular topics on your site, as reflected in your top posts from the past week.</string>
+ <string name="stats_empty_top_authors_desc">Track the views on each contributor\'s posts, and zoom in to discover the most popular content by each author.</string>
+ <string name="stats_empty_tags_and_categories">No tagged posts or pages viewed</string>
+ <string name="stats_empty_clicks_desc">When your content includes links to other sites, you’ll see which ones your visitors click on the most.</string>
+ <string name="stats_empty_referrers_desc">Learn more about your site’s visibility by looking at the websites and search engines that send the most traffic your way</string>
+ <string name="stats_empty_clicks_title">No clicks recorded</string>
+ <string name="stats_empty_referrers_title">No referrers recorded</string>
+ <string name="stats_empty_top_posts_title">No posts or pages viewed</string>
+ <string name="stats_empty_top_posts_desc">Discover what your most-viewed content is, and check how individual posts and pages perform over time.</string>
+ <string name="stats_totals_followers">Since</string>
+ <string name="stats_empty_geoviews">No countries recorded</string>
+ <string name="stats_empty_geoviews_desc">Explore the list to see which countries and regions generate the most traffic to your site.</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Author</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_followers">Follower</string>
+ <string name="stats_totals_publicize">Followers</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_view_top_posts_and_pages">Posts &amp; Pages</string>
+ <string name="stats_view_videos">Videos</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Followers</string>
+ <string name="stats_view_countries">Countries</string>
+ <string name="stats_likes">Likes</string>
+ <string name="stats_pagination_label">Page %1$s of %2$s</string>
+ <string name="stats_timeframe_years">Years</string>
+ <string name="stats_views">Views</string>
+ <string name="stats_visitors">Visitors</string>
+ <string name="ssl_certificate_details">Details</string>
+ <string name="delete_sure_post">Delete this post</string>
+ <string name="delete_sure">Delete this draft</string>
+ <string name="delete_sure_page">Delete this page</string>
+ <string name="confirm_delete_multi_media">Delete selected items?</string>
+ <string name="confirm_delete_media">Delete selected item?</string>
+ <string name="cab_selected">%d selected</string>
+ <string name="media_gallery_date_range">Displaying media from %1$s to %2$s</string>
+ <string name="sure_to_remove_account">Remove this site?</string>
+ <string name="reader_empty_followed_blogs_title">You\'re not following any sites yet</string>
+ <string name="reader_empty_posts_liked">You haven\'t liked any posts</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Browse our FAQ</string>
+ <string name="nux_help_description">Visit the help centre to get answers to common questions or visit the forums to ask new ones</string>
+ <string name="agree_terms_of_service">By creating an account you agree to the fascinating %1$sTerms of Service%2$s</string>
+ <string name="create_new_blog_wpcom">Create WordPress.com blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog created!</string>
+ <string name="reader_empty_comments">No comments yet</string>
+ <string name="reader_empty_posts_in_tag">No posts with this tag</string>
+ <string name="reader_label_comment_count_multi">%,d comments</string>
+ <string name="reader_label_view_original">View original article</string>
+ <string name="reader_label_like">Like</string>
+ <string name="reader_label_liked_by">Liked By</string>
+ <string name="reader_label_comment_count_single">One comment</string>
+ <string name="reader_label_comments_closed">Comments are closed</string>
+ <string name="reader_label_comments_on">Comments on</string>
+ <string name="reader_title_photo_viewer">%1$d of %2$d</string>
+ <string name="error_publish_empty_post">Can\'t publish an empty post</string>
+ <string name="error_refresh_unauthorized_posts">You don\'t have permission to view or edit posts</string>
+ <string name="error_refresh_unauthorized_pages">You don\'t have permission to view or edit pages</string>
+ <string name="error_refresh_unauthorized_comments">You don\'t have permission to view or edit comments</string>
+ <string name="older_month">Older than a month</string>
+ <string name="more">More</string>
+ <string name="older_two_days">Older than 2 days</string>
+ <string name="older_last_week">Older than a week</string>
+ <string name="stats_no_blog">Stats couldn\'t be loaded for the required blog</string>
+ <string name="select_a_blog">Select a WordPress site</string>
+ <string name="sending_content">Uploading %s content</string>
+ <string name="uploading_total">Uploading %1$d of %2$d</string>
+ <string name="mnu_comment_liked">Liked</string>
+ <string name="comment">Comment</string>
+ <string name="comment_trashed">Comment trashed</string>
+ <string name="posts_empty_list">No posts yet. Why not create one?</string>
+ <string name="comment_reply_to_user">Reply to %s</string>
+ <string name="pages_empty_list">No pages yet. Why not create one?</string>
+ <string name="media_empty_list_custom_date">No media in this time interval</string>
+ <string name="posting_post">Posting "%s"</string>
+ <string name="signing_out">Signing out…</string>
+ <string name="reader_toast_err_generic">Unable to perform this action</string>
+ <string name="reader_toast_err_block_blog">Unable to block this blog</string>
+ <string name="reader_toast_blog_blocked">Posts from this blog will no longer be shown</string>
+ <string name="reader_menu_block_blog">Block this blog</string>
+ <string name="contact_us">Contact us</string>
+ <string name="hs__conversation_detail_error">Describe the problem you\'re seeing</string>
+ <string name="hs__new_conversation_header">Support chat</string>
+ <string name="hs__conversation_header">Support chat</string>
+ <string name="hs__username_blank_error">Enter a valid name</string>
+ <string name="hs__invalid_email_error">Enter a valid email address</string>
+ <string name="add_location">Add location</string>
+ <string name="current_location">Current location</string>
+ <string name="search_location">Search</string>
+ <string name="edit_location">Edit</string>
+ <string name="search_current_location">Locate</string>
+ <string name="preference_send_usage_stats">Send statistics</string>
+ <string name="preference_send_usage_stats_summary">Automatically send usage statistics to help us improve WordPress for Android</string>
+ <string name="update_verb">Update</string>
+ <string name="schedule_verb">Schedule</string>
+ <string name="reader_title_blog_preview">Reader Blog</string>
+ <string name="reader_title_tag_preview">Reader Tag</string>
+ <string name="reader_title_subs">Tags &amp; Blogs</string>
+ <string name="reader_page_followed_tags">Followed tags</string>
+ <string name="reader_page_followed_blogs">Followed sites</string>
+ <string name="reader_hint_add_tag_or_url">Enter a URL or tag to follow</string>
+ <string name="reader_label_followed_blog">Blog followed</string>
+ <string name="reader_label_tag_preview">Posts tagged %s</string>
+ <string name="reader_toast_err_get_blog_info">Unable to show this blog</string>
+ <string name="reader_toast_err_already_follow_blog">You already follow this blog</string>
+ <string name="reader_toast_err_follow_blog">Unable to follow this blog</string>
+ <string name="reader_toast_err_unfollow_blog">Unable to unfollow this blog</string>
+ <string name="reader_empty_recommended_blogs">No recommended blogs</string>
+ <string name="saving">Saving…</string>
+ <string name="media_empty_list">No media</string>
+ <string name="ptr_tip_message">Tip: Pull down to refresh</string>
+ <string name="help">Help</string>
+ <string name="forgot_password">Lost your password?</string>
+ <string name="forums">Forums</string>
+ <string name="help_center">Help centre</string>
+ <string name="ssl_certificate_error">Invalid SSL certificate</string>
+ <string name="ssl_certificate_ask_trust">If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn\'t continue. Would you like to trust the certificate anyway?</string>
+ <string name="out_of_memory">Device out of memory</string>
+ <string name="no_network_message">There is no network available</string>
+ <string name="could_not_remove_account">Couldn\'t remove site</string>
+ <string name="gallery_error">The media item couldn\'t be retrieved</string>
+ <string name="blog_not_found">An error occurred when accessing this blog</string>
+ <string name="wait_until_upload_completes">Wait until upload completes</string>
+ <string name="theme_fetch_failed">Failed to fetch themes</string>
+ <string name="theme_set_failed">Failed to set theme</string>
+ <string name="theme_auth_error_message">Ensure you have the privilege to set themes</string>
+ <string name="comments_empty_list">No comments</string>
+ <string name="mnu_comment_unspam">Not spam</string>
+ <string name="no_site_error">Couldn\'t connect to the WordPress site</string>
+ <string name="adding_cat_failed">Adding category failed</string>
+ <string name="adding_cat_success">Category added successfully</string>
+ <string name="cat_name_required">The category name field is required</string>
+ <string name="category_automatically_renamed">Category name %1$s isn\'t valid. It has been renamed to %2$s.</string>
+ <string name="no_account">No WordPress account found, add an account and try again</string>
+ <string name="sdcard_message">A mounted SD card is required to upload media</string>
+ <string name="stats_empty_comments">No comments yet</string>
+ <string name="stats_bar_graph_empty">No stats available</string>
+ <string name="invalid_url_message">Check that the URL entered is valid</string>
+ <string name="reply_failed">Reply failed</string>
+ <string name="notifications_empty_list">No notifications</string>
+ <string name="error_delete_post">An error occurred while deleting the %s</string>
+ <string name="error_refresh_posts">Posts couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_pages">Pages couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_notifications">Notifications couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_comments">Comments couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_stats">Stats couldn\'t be refreshed at this time</string>
+ <string name="error_generic">An error occurred</string>
+ <string name="error_moderate_comment">An error occurred while moderating</string>
+ <string name="error_edit_comment">An error occurred while editing the comment</string>
+ <string name="error_upload">An error occurred while uploading the %s</string>
+ <string name="error_load_comment">Couldn\'t load the comment</string>
+ <string name="error_downloading_image">Error downloading image</string>
+ <string name="passcode_wrong_passcode">Wrong PIN</string>
+ <string name="invalid_email_message">Your email address isn\'t valid</string>
+ <string name="invalid_password_message">Password must contain at least 4 characters</string>
+ <string name="invalid_username_too_short">Username must be longer than 4 characters</string>
+ <string name="invalid_username_too_long">Username must be shorter than 61 characters</string>
+ <string name="username_only_lowercase_letters_and_numbers">Username can only contain lowercase letters (a-z) and numbers</string>
+ <string name="username_required">Enter a username</string>
+ <string name="username_not_allowed">Username not allowed</string>
+ <string name="username_must_be_at_least_four_characters">Username must be at least 4 characters</string>
+ <string name="username_contains_invalid_characters">Username may not contain the character “_”</string>
+ <string name="username_must_include_letters">Username must have a least 1 letter (a-z)</string>
+ <string name="email_invalid">Enter a valid email address</string>
+ <string name="email_not_allowed">That email address isn\'t allowed</string>
+ <string name="username_exists">That username already exists</string>
+ <string name="email_exists">That email address is already being used</string>
+ <string name="username_reserved_but_may_be_available">That username is currently reserved but may be available in a couple of days</string>
+ <string name="blog_name_required">Enter a site address</string>
+ <string name="blog_name_not_allowed">That site address isn\'t allowed</string>
+ <string name="blog_name_must_be_at_least_four_characters">Site address must be at least 4 characters</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">The site address must be shorter than 64 characters</string>
+ <string name="blog_name_contains_invalid_characters">Site address may not contain the character “_”</string>
+ <string name="blog_name_cant_be_used">You may not use that site address</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Site address can only contain lowercase letters (a-z) and numbers</string>
+ <string name="blog_name_exists">That site already exists</string>
+ <string name="blog_name_reserved">That site is reserved</string>
+ <string name="blog_name_reserved_but_may_be_available">That site is currently reserved but may be available in a couple days</string>
+ <string name="username_or_password_incorrect">The username or password you entered is incorrect</string>
+ <string name="nux_cannot_log_in">We can\'t log you in</string>
+ <string name="xmlrpc_error">Couldn\'t connect. Enter the full path to xmlrpc.php on your site and try again.</string>
+ <string name="select_categories">Select categories</string>
+ <string name="account_details">Account details</string>
+ <string name="edit_post">Edit post</string>
+ <string name="add_comment">Add comment</string>
+ <string name="connection_error">Connection error</string>
+ <string name="cancel_edit">Cancel edit</string>
+ <string name="scaled_image_error">Enter a valid scaled width value</string>
+ <string name="post_not_found">An error occurred when loading the post. Refresh your posts and try again.</string>
+ <string name="learn_more">Learn more</string>
+ <string name="media_gallery_settings_title">Gallery settings</string>
+ <string name="media_gallery_image_order">Image order</string>
+ <string name="media_gallery_num_columns">Number of columns</string>
+ <string name="media_gallery_type_thumbnail_grid">Thumbnail grid</string>
+ <string name="media_gallery_edit">Edit gallery</string>
+ <string name="media_error_no_permission">You don\'t have permission to view the media library</string>
+ <string name="cannot_delete_multi_media_items">Some media can\'t be deleted at this time. Try again later.</string>
+ <string name="themes_live_preview">Live preview</string>
+ <string name="theme_current_theme">Current theme</string>
+ <string name="theme_premium_theme">Premium theme</string>
+ <string name="link_enter_url_text">Link text (optional)</string>
+ <string name="create_a_link">Create a link</string>
+ <string name="page_settings">Page settings</string>
+ <string name="local_draft">Local draft</string>
+ <string name="upload_failed">Upload failed</string>
+ <string name="horizontal_alignment">Horizontal alignment</string>
+ <string name="file_not_found">Couldn\'t find the media file for upload. Was it deleted or moved?</string>
+ <string name="post_settings">Post settings</string>
+ <string name="delete_post">Delete post</string>
+ <string name="delete_page">Delete page</string>
+ <string name="comment_status_approved">Approved</string>
+ <string name="comment_status_unapproved">Pending</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Trashed</string>
+ <string name="edit_comment">Edit comment</string>
+ <string name="mnu_comment_approve">Approve</string>
+ <string name="mnu_comment_unapprove">Unapprove</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Trash</string>
+ <string name="dlg_approving_comments">Approving</string>
+ <string name="dlg_unapproving_comments">Unapproving</string>
+ <string name="dlg_spamming_comments">Marking as spam</string>
+ <string name="dlg_trashing_comments">Sending to trash</string>
+ <string name="dlg_confirm_trash_comments">Send to trash?</string>
+ <string name="trash_yes">Trash</string>
+ <string name="trash_no">Don\'t trash</string>
+ <string name="trash">Trash</string>
+ <string name="author_name">Author name</string>
+ <string name="author_email">Author email</string>
+ <string name="author_url">Author URL</string>
+ <string name="hint_comment_content">Comment</string>
+ <string name="saving_changes">Saving changes</string>
+ <string name="sure_to_cancel_edit_comment">Cancel editing this comment?</string>
+ <string name="content_required">Comment is required</string>
+ <string name="toast_comment_unedited">Comment hasn\'t changed</string>
+ <string name="remove_account">Remove site</string>
+ <string name="blog_removed_successfully">Site removed successfully</string>
+ <string name="delete_draft">Delete draft</string>
+ <string name="preview_page">Preview page</string>
+ <string name="preview_post">Preview post</string>
+ <string name="comment_added">Comment added successfully</string>
+ <string name="post_not_published">Post status isn\'t published</string>
+ <string name="page_not_published">Page status isn\'t published</string>
+ <string name="view_in_browser">View in browser</string>
+ <string name="add_new_category">Add new category</string>
+ <string name="category_name">Category name</string>
+ <string name="category_slug">Category slug (optional)</string>
+ <string name="category_desc">Category description (optional)</string>
+ <string name="category_parent">Category parent (optional):</string>
+ <string name="share_action_post">New post</string>
+ <string name="share_action_media">Media library</string>
+ <string name="file_error_create">Couldn\'t create temp file for media upload. Make sure there is enough free space on your device.</string>
+ <string name="location_not_found">Unknown location</string>
+ <string name="open_source_licenses">Open source licenses</string>
+ <string name="invalid_site_url_message">Check that the site URL entered is valid</string>
+ <string name="pending_review">Pending review</string>
+ <string name="http_credentials">HTTP credentials (optional)</string>
+ <string name="http_authorization_required">Authorization required</string>
+ <string name="post_format">Post format</string>
+ <string name="notifications_empty_all">No notifications…yet.</string>
+ <string name="new_post">New post</string>
+ <string name="new_media">New media</string>
+ <string name="view_site">View site</string>
+ <string name="privacy_policy">Privacy policy</string>
+ <string name="local_changes">Local changes</string>
+ <string name="image_settings">Image settings</string>
+ <string name="add_account_blog_url">Blog address</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="error_blog_hidden">This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again.</string>
+ <string name="fatal_db_error">An error occurred while creating the app database. Try reinstalling the app.</string>
+ <string name="jetpack_message_not_admin">The Jetpack plugin is required for stats. Contact the site administrator.</string>
+ <string name="reader_title_applog">Application log</string>
+ <string name="reader_share_link">Share link</string>
+ <string name="reader_toast_err_add_tag">Unable to add this tag</string>
+ <string name="reader_toast_err_remove_tag">Unable to remove this tag</string>
+ <string name="required_field">Required field</string>
+ <string name="email_hint">Email address</string>
+ <string name="site_address">Your self-hosted address (URL)</string>
+ <string name="email_cant_be_used_to_signup">You can\'t use that email address to signup. We are having problems with them blocking some of our email. Use another email provider.</string>
+ <string name="email_reserved">That email address has already been used. Check your inbox for an activation email. If you don\'t activate you can try again in a few days.</string>
+ <string name="blog_name_must_include_letters">Site address must have at least 1 letter (a-z)</string>
+ <string name="blog_name_invalid">Invalid site address</string>
+ <string name="blog_title_invalid">Invalid site title</string>
+ <string name="deleting_page">Deleting page</string>
+ <string name="deleting_post">Deleting post</string>
+ <string name="share_url_post">Share post</string>
+ <string name="share_url_page">Share page</string>
+ <string name="share_link">Share link</string>
+ <string name="creating_your_account">Creating your account</string>
+ <string name="creating_your_site">Creating your site</string>
+ <string name="reader_empty_posts_in_tag_updating">Fetching posts…</string>
+ <string name="error_refresh_media">Something went wrong while refreshing the media library. Try again later.</string>
+ <string name="reader_likes_you_and_multi">You and %,d others like this</string>
+ <string name="reader_likes_multi">%,d people like this</string>
+ <string name="reader_toast_err_get_comment">Unable to retrieve this comment</string>
+ <string name="reader_label_reply">Reply</string>
+ <string name="video">Video</string>
+ <string name="download">Downloading media</string>
+ <string name="comment_spammed">Comment marked as spam</string>
+ <string name="cant_share_no_visible_blog">You can\'t share to WordPress without a visible blog</string>
+ <string name="select_time">Select time</string>
+ <string name="reader_likes_you_and_one">You and one other like this</string>
+ <string name="reader_empty_followed_blogs_description">But don\'t worry, just tap the icon at the top right to start exploring!</string>
+ <string name="select_date">Select date</string>
+ <string name="pick_photo">Select photo</string>
+ <string name="account_two_step_auth_enabled">This account has two step authentication enabled. Visit your security settings on WordPress.com and generate an application-specific password.</string>
+ <string name="pick_video">Select video</string>
+ <string name="reader_toast_err_get_post">Unable to retrieve this post</string>
+ <string name="validating_user_data">Validating user data</string>
+ <string name="validating_site_data">Validating site data</string>
+ <string name="password_invalid">You need a more secure password. Make sure to use 7 or more characters, mix uppercase and lowercase letters, numbers or special characters.</string>
+ <string name="nux_tap_continue">Continue</string>
+ <string name="nux_welcome_create_account">Create account</string>
+ <string name="signing_in">Signing in…</string>
+ <string name="nux_add_selfhosted_blog">Add self-hosted site</string>
+ <string name="nux_oops_not_selfhosted_blog">Sign in to WordPress.com</string>
+ <string name="media_add_popup_title">Add to media library</string>
+ <string name="media_add_new_media_gallery">Create gallery</string>
+ <string name="empty_list_default">This list is empty</string>
+ <string name="select_from_media_library">Select from media library</string>
+ <string name="jetpack_message">The Jetpack plugin is required for stats. Do you want to install Jetpack?</string>
+ <string name="jetpack_not_found">Jetpack plugin not found</string>
+ <string name="reader_untitled_post">(Untitled)</string>
+ <string name="reader_share_subject">Shared from %s</string>
+ <string name="reader_btn_share">Share</string>
+ <string name="reader_btn_follow">Follow</string>
+ <string name="reader_btn_unfollow">Following</string>
+ <string name="reader_hint_comment_on_comment">Reply to comment…</string>
+ <string name="reader_label_added_tag">Added %s</string>
+ <string name="reader_label_removed_tag">Removed %s</string>
+ <string name="reader_likes_one">One person likes this</string>
+ <string name="reader_likes_only_you">You like this</string>
+ <string name="reader_toast_err_comment_failed">Couldn\'t post your comment</string>
+ <string name="reader_toast_err_tag_exists">You already follow this tag</string>
+ <string name="reader_toast_err_tag_invalid">That isn\'t a valid tag</string>
+ <string name="reader_toast_err_share_intent">Unable to share</string>
+ <string name="reader_toast_err_view_image">Unable to view image</string>
+ <string name="reader_toast_err_url_intent">Unable to open %s</string>
+ <string name="reader_empty_followed_tags">You don\'t follow any tags</string>
+ <string name="create_account_wpcom">Create an account on WordPress.com</string>
+ <string name="button_next">Next</string>
+ <string name="connecting_wpcom">Connecting to WordPress.com</string>
+ <string name="username_invalid">Invalid username</string>
+ <string name="limit_reached">Limit reached. You can try again in 1 minute. Trying again before that will only increase the time you have to wait before the ban is lifted. If you think this is in error, contact support.</string>
+ <string name="nux_tutorial_get_started_title">Get started!</string>
+ <string name="themes">Themes</string>
+ <string name="all">All</string>
+ <string name="images">Images</string>
+ <string name="unattached">Unattached</string>
+ <string name="custom_date">Custom Date</string>
+ <string name="media_add_popup_capture_photo">Capture photo</string>
+ <string name="media_add_popup_capture_video">Capture video</string>
+ <string name="media_gallery_image_order_random">Random</string>
+ <string name="media_gallery_image_order_reverse">Reverse</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="media_gallery_type_squares">Squares</string>
+ <string name="media_gallery_type_tiled">Tiled</string>
+ <string name="media_gallery_type_circles">Circles</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="media_edit_title_text">Title</string>
+ <string name="media_edit_caption_text">Caption</string>
+ <string name="media_edit_description_text">Description</string>
+ <string name="media_edit_title_hint">Enter a title here</string>
+ <string name="media_edit_caption_hint">Enter a caption here</string>
+ <string name="media_edit_description_hint">Enter a description here</string>
+ <string name="media_edit_success">Updated</string>
+ <string name="media_edit_failure">Failed to update</string>
+ <string name="themes_details_label">Details</string>
+ <string name="themes_features_label">Features</string>
+ <string name="theme_activate_button">Activate</string>
+ <string name="theme_activating_button">Activating</string>
+ <string name="theme_set_success">Successfully set theme!</string>
+ <string name="theme_auth_error_title">Failed to fetch themes</string>
+ <string name="post_excerpt">Excerpt</string>
+ <string name="share_action_title">Add to …</string>
+ <string name="share_action">Share</string>
+ <string name="stats">Stats</string>
+ <string name="stats_view_visitors_and_views">Visitors and Views</string>
+ <string name="stats_view_clicks">Clicks</string>
+ <string name="stats_view_tags_and_categories">Tags &amp; Categoriess</string>
+ <string name="stats_view_referrers">Referrers</string>
+ <string name="stats_timeframe_today">Today</string>
+ <string name="stats_timeframe_yesterday">Yesterday</string>
+ <string name="stats_timeframe_days">Days</string>
+ <string name="stats_timeframe_weeks">Weeks</string>
+ <string name="stats_timeframe_months">Months</string>
+ <string name="stats_entry_country">Country</string>
+ <string name="stats_entry_posts_and_pages">Title</string>
+ <string name="stats_entry_tags_and_categories">Topic</string>
+ <string name="stats_entry_authors">Author</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_totals_views">Views</string>
+ <string name="stats_totals_clicks">Clicks</string>
+ <string name="stats_totals_plays">Plays</string>
+ <string name="passcode_manage">Manage PIN lock</string>
+ <string name="passcode_enter_passcode">Enter your PIN</string>
+ <string name="passcode_enter_old_passcode">Enter your old PIN</string>
+ <string name="passcode_re_enter_passcode">Re-enter your PIN</string>
+ <string name="passcode_change_passcode">Change PIN</string>
+ <string name="passcode_set">PIN set</string>
+ <string name="passcode_preference_title">PIN lock</string>
+ <string name="passcode_turn_off">Turn PIN lock off</string>
+ <string name="passcode_turn_on">Turn PIN lock on</string>
+ <string name="upload">Upload</string>
+ <string name="discard">Discard</string>
+ <string name="sign_in">Sign in</string>
+ <string name="notifications">Notifications</string>
+ <string name="note_reply_successful">Reply published</string>
+ <string name="follows">Follows</string>
+ <string name="new_notifications">%d new notifications</string>
+ <string name="more_notifications">and %d more.</string>
+ <string name="loading">Loading…</string>
+ <string name="httpuser">HTTP username</string>
+ <string name="httppassword">HTTP password</string>
+ <string name="error_media_upload">An error occurred while uploading media</string>
+ <string name="post_content">Content (tap to add text and media)</string>
+ <string name="publish_date">Publish</string>
+ <string name="content_description_add_media">Add media</string>
+ <string name="incorrect_credentials">Incorrect username or password.</string>
+ <string name="password">Password</string>
+ <string name="username">Username</string>
+ <string name="reader">Reader</string>
+ <string name="featured">Use as featured image</string>
+ <string name="featured_in_post">Include image in post content</string>
+ <string name="no_network_title">No network available</string>
+ <string name="pages">Pages</string>
+ <string name="caption">Caption (optional)</string>
+ <string name="width">Width</string>
+ <string name="posts">Posts</string>
+ <string name="anonymous">Anonymous</string>
+ <string name="page">Page</string>
+ <string name="post">Post</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Upload and link to scaled image</string>
+ <string name="scaled_image">Scaled image width</string>
+ <string name="scheduled">Scheduled</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Uploading…</string>
+ <string name="version">Version</string>
+ <string name="tos">Terms of Service</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">Default Image Width</string>
+ <string name="image_alignment">Alignment</string>
+ <string name="refresh">Refresh</string>
+ <string name="untitled">Untitled</string>
+ <string name="edit">Edit</string>
+ <string name="post_id">Post</string>
+ <string name="page_id">Page</string>
+ <string name="post_password">Password (optional)</string>
+ <string name="immediately">Immediately</string>
+ <string name="quickpress_add_alert_title">Set shortcut name</string>
+ <string name="today">Today</string>
+ <string name="settings">Settings</string>
+ <string name="share_url">Share URL</string>
+ <string name="quickpress_window_title">Select blog for QuickPress shortcut</string>
+ <string name="quickpress_add_error">Shortcut name can\'t be empty</string>
+ <string name="publish_post">Publish</string>
+ <string name="draft">Draft</string>
+ <string name="post_private">Private</string>
+ <string name="upload_full_size_image">Upload and link to full image</string>
+ <string name="title">Title</string>
+ <string name="tags_separate_with_commas">Tags (separate tags with commas)</string>
+ <string name="categories">Categories</string>
+ <string name="dlg_deleting_comments">Deleting comments</string>
+ <string name="notification_blink">Blink notification light</string>
+ <string name="notification_sound">Notification sound</string>
+ <string name="notification_vibrate">Vibrate</string>
+ <string name="status">Status</string>
+ <string name="location">Location</string>
+ <string name="sdcard_title">SD Card Required</string>
+ <string name="select_video">Select a video from gallery</string>
+ <string name="media">Media</string>
+ <string name="delete">Delete</string>
+ <string name="none">None</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Select a photo from gallery</string>
+ <string name="error">Error</string>
+ <string name="cancel">Cancel</string>
+ <string name="save">Save</string>
+ <string name="add">Add</string>
+ <string name="category_refresh_error">Category refresh error</string>
+ <string name="preview">Preview</string>
+ <string name="on">on</string>
+ <string name="reply">Reply</string>
+ <string name="notification_settings">Notification Settings</string>
+ <string name="yes">Yes</string>
+ <string name="no">No</string>
+</resources>
diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 000000000..97ddbf428
--- /dev/null
+++ b/WordPress/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Author</string>
+ <string name="role_contributor">Contributor</string>
+ <string name="role_follower">Follower</string>
+ <string name="role_viewer">Viewer</string>
+ <string name="error_post_my_profile_no_connection">No connection, couldn\'t save your profile</string>
+ <string name="alignment_none">None</string>
+ <string name="alignment_left">Left</string>
+ <string name="alignment_right">Right</string>
+ <string name="site_settings_list_editor_action_mode_title">Selected %1$d</string>
+ <string name="error_fetch_users_list">Couldn\'t retrieve site users</string>
+ <string name="plans_manage">Manage your plan at\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">You don\'t have any viewers yet.</string>
+ <string name="people_fetching">Fetching users…</string>
+ <string name="title_follower">Follower</string>
+ <string name="title_email_follower">Email Follower</string>
+ <string name="people_empty_list_filtered_email_followers">You don\'t have any email followers yet.</string>
+ <string name="people_empty_list_filtered_followers">You don\'t have any followers yet.</string>
+ <string name="people_empty_list_filtered_users">You don\'t have any users yet.</string>
+ <string name="people_dropdown_item_email_followers">Email Followers</string>
+ <string name="people_dropdown_item_viewers">Viewers</string>
+ <string name="people_dropdown_item_followers">Followers</string>
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="invite_message_usernames_limit">Invite up to 10 email addresses and/or WordPress.com usernames. Those needing a username will be sent instructions on how to create one.</string>
+ <string name="viewer_remove_confirmation_message">If you remove this viewer, he or she will not be able to visit this site.\n\nWould you still like to remove this viewer?</string>
+ <string name="follower_remove_confirmation_message">If removed, this follower will stop receiving notifications about this site, unless they re-follow.\n\nWould you still like to remove this follower?</string>
+ <string name="follower_subscribed_since">Since %1$s</string>
+ <string name="reader_label_view_gallery">View Gallery</string>
+ <string name="error_remove_follower">Couldn\'t remove follower</string>
+ <string name="error_remove_viewer">Couldn\'t remove viewer</string>
+ <string name="error_fetch_email_followers_list">Couldn\'t retrieve site email followers</string>
+ <string name="error_fetch_followers_list">Couldn\'t retrieve site followers</string>
+ <string name="editor_failed_uploads_switch_html">Some media uploads have failed. You can\'t switch to HTML mode\n in this state. Remove all failed uploads and continue?</string>
+ <string name="format_bar_description_html">HTML mode</string>
+ <string name="visual_editor">Visual editor</string>
+ <string name="image_thumbnail">Image thumbnail</string>
+ <string name="format_bar_description_ul">Unordered list</string>
+ <string name="format_bar_description_ol">Ordered list</string>
+ <string name="format_bar_description_more">Insert more</string>
+ <string name="format_bar_description_media">Insert media</string>
+ <string name="format_bar_description_strike">Strikethrough</string>
+ <string name="format_bar_description_quote">Block quote</string>
+ <string name="format_bar_description_link">Insert link</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Underline</string>
+ <string name="image_settings_save_toast">Changes saved</string>
+ <string name="image_caption">Caption</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Link to</string>
+ <string name="image_width">Width</string>
+ <string name="format_bar_description_bold">Bold</string>
+ <string name="image_settings_dismiss_dialog_title">Discard unsaved changes?</string>
+ <string name="stop_upload_dialog_title">Stop uploading?</string>
+ <string name="stop_upload_button">Stop Upload</string>
+ <string name="alert_error_adding_media">An error occurred while inserting media</string>
+ <string name="alert_action_while_uploading">You are currently uploading media. Please wait until this completes.</string>
+ <string name="alert_insert_image_html_mode">Can\'t insert media directly in HTML mode. Please switch back to visual mode.</string>
+ <string name="uploading_gallery_placeholder">Uploading gallery…</string>
+ <string name="invite_error_some_failed">Invite sent but error(s) occurred!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Invite sent successfully</string>
+ <string name="tap_to_try_again">Tap to try again!</string>
+ <string name="invite_error_sending">An error occurred while trying to send the invite!</string>
+ <string name="invite_error_invalid_usernames_multiple">Cannot send: There are invalid usernames or emails</string>
+ <string name="invite_error_invalid_usernames_one">Cannot send: A username or email is invalid</string>
+ <string name="invite_error_no_usernames">Please add at least one username</string>
+ <string name="invite_message_info">(Optional) You can enter a custom message of up to 500 characters that will be included in the invitation to the user(s).</string>
+ <string name="invite_message_remaining_other">%d characters remaining</string>
+ <string name="invite_message_remaining_one">1 character remaining</string>
+ <string name="invite_message_remaining_zero">0 characters remaining</string>
+ <string name="invite_invalid_email">The email address \'%s\' is invalid</string>
+ <string name="invite_message_title">Custom Message</string>
+ <string name="invite_already_a_member">There\'s already a member with username \'%s\'</string>
+ <string name="invite_username_not_found">No user was found for username \'%s\'</string>
+ <string name="invite">Invite</string>
+ <string name="invite_names_title">Usernames or Emails</string>
+ <string name="signup_succeed_signin_failed">Your account has been created but an error occurred while we signed you\n in. Try to sign in with your newly created username and password.</string>
+ <string name="send_link">Send link</string>
+ <string name="my_site_header_external">External</string>
+ <string name="invite_people">Invite People</string>
+ <string name="label_clear_search_history">Clear search history</string>
+ <string name="dlg_confirm_clear_search_history">Clear search history?</string>
+ <string name="reader_empty_posts_in_search_description">No posts found for %s for your language</string>
+ <string name="reader_label_post_search_running">Searching…</string>
+ <string name="reader_label_related_posts">Related Reading</string>
+ <string name="reader_empty_posts_in_search_title">No posts found</string>
+ <string name="reader_label_post_search_explainer">Search all public WordPress.com blogs</string>
+ <string name="reader_hint_post_search">Search WordPress.com</string>
+ <string name="reader_title_related_post_detail">Related Post</string>
+ <string name="reader_title_search_results">Search for %s</string>
+ <string name="preview_screen_links_disabled">Links are disabled on the preview screen</string>
+ <string name="draft_explainer">This post is a draft which hasn\'t been published</string>
+ <string name="send">Send</string>
+ <string name="user_remove_confirmation_message">If you remove %1$s, that user will no longer be able to access this site, but any content that was created by %1$s will remain on the site.\n\nWould you still like to remove this user?</string>
+ <string name="person_removed">Successfully removed %1$s</string>
+ <string name="person_remove_confirmation_title">Remove %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">The sites in this list haven\'t posted anything recently</string>
+ <string name="people">People</string>
+ <string name="edit_user">Edit User</string>
+ <string name="role">Role</string>
+ <string name="error_remove_user">Couldn\'t remove user</string>
+ <string name="error_fetch_viewers_list">Couldn\'t retrieve site viewers</string>
+ <string name="error_update_role">Couldn\'t update user role</string>
+ <string name="gravatar_camera_and_media_permission_required">Permissions required in order to select or capture a photo</string>
+ <string name="error_updating_gravatar">Error updating your Gravatar</string>
+ <string name="error_locating_image">Error locating the cropped image</string>
+ <string name="error_refreshing_gravatar">Error reloading your Gravatar</string>
+ <string name="gravatar_tip">New! Tap your Gravatar to change it!</string>
+ <string name="error_cropping_image">Error cropping the image</string>
+ <string name="launch_your_email_app">Launch your email app</string>
+ <string name="checking_email">Checking email</string>
+ <string name="not_on_wordpress_com">Not on WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Currently unavailable. Please enter your password</string>
+ <string name="check_your_email">Check your email</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Get a link sent to your email to sign in instantly</string>
+ <string name="logging_in">Logging in</string>
+ <string name="enter_your_password_instead">Enter your password instead</string>
+ <string name="web_address_dialog_hint">Shown publicly when you comment.</string>
+ <string name="jetpack_not_connected_message">The Jetpack plugin is installed, but not connected to WordPress.com. Do you want to connect Jetpack?</string>
+ <string name="username_email">Email or username</string>
+ <string name="jetpack_not_connected">Jetpack plugin not connected</string>
+ <string name="new_editor_reflection_error">Visual editor is not compatible with your device. It was\n automatically disabled.</string>
+ <string name="stats_insights_latest_post_no_title">(no title)</string>
+ <string name="capture_or_pick_photo">Capture or select photo</string>
+ <string name="plans_post_purchase_text_themes">You now have unlimited access to Premium themes. Preview any theme on your site to get started.</string>
+ <string name="plans_post_purchase_button_themes">Browse Themes</string>
+ <string name="plans_post_purchase_title_themes">Find a perfect, Premium theme</string>
+ <string name="plans_post_purchase_button_video">Start new post</string>
+ <string name="plans_post_purchase_text_video">You can upload and host videos on your site with VideoPress and your expanded media storage.</string>
+ <string name="plans_post_purchase_title_video">Bring posts to life with video</string>
+ <string name="plans_post_purchase_button_customize">Customise my Site</string>
+ <string name="plans_post_purchase_text_customize">You now have access to custom fonts, custom colours, and custom CSS editing capabilities.</string>
+ <string name="plans_post_purchase_text_intro">Your site is doing somersaults in excitement! Now explore your site\'s new features and choose where you\'d like to begin.</string>
+ <string name="plans_post_purchase_title_customize">Customise Fonts &amp; Colours</string>
+ <string name="plans_post_purchase_title_intro">It\'s all yours, way to go!</string>
+ <string name="export_your_content_message">Your posts, pages, and settings will be emailed to you at %s.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Unable to load plans</string>
+ <string name="export_your_content">Export your content</string>
+ <string name="exporting_content_progress">Exporting content…</string>
+ <string name="export_email_sent">Export email sent!</string>
+ <string name="premium_upgrades_message">You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site.</string>
+ <string name="show_purchases">Show purchases</string>
+ <string name="checking_purchases">Checking purchases</string>
+ <string name="premium_upgrades_title">Premium Upgrades</string>
+ <string name="purchases_request_error">Something went wrong. Could not request purchases.</string>
+ <string name="delete_site_progress">Deleting site…</string>
+ <string name="delete_site_summary">This action cannot be undone. Deleting your site will remove all content, contributors, and domains from the site.</string>
+ <string name="delete_site_hint">Delete site</string>
+ <string name="export_site_hint">Export your site to an XML file</string>
+ <string name="are_you_sure">Are You Sure?</string>
+ <string name="export_site_summary">If you are sure, please be sure to take the time and export your content now. It cannot be recovered in the future.</string>
+ <string name="keep_your_content">Keep Your Content</string>
+ <string name="domain_removal_hint">The domains that will not work once you remove your site</string>
+ <string name="domain_removal_summary">Be careful! Deleting your site will also remove your domain(s) listed below.</string>
+ <string name="primary_domain">Primary Domain</string>
+ <string name="domain_removal">Domain Removal</string>
+ <string name="error_deleting_site_summary">There was an error in deleting your site. Please contact support for more assistance</string>
+ <string name="error_deleting_site">Error deleting site</string>
+ <string name="confirm_delete_site_prompt">Please type %1$s in the field below to confirm. Your site will then be gone forever.</string>
+ <string name="site_settings_export_content_title">Export content</string>
+ <string name="contact_support">Contact support</string>
+ <string name="confirm_delete_site">Confirm Delete Site</string>
+ <string name="start_over_text">If you want a site but don\'t want any of the posts and pages you have now, our support team can delete your posts, pages, media and comments for you.\n\nThis will keep your site and URL active, but give you a fresh start on your content creation. Just contact us to have your current content cleared out.</string>
+ <string name="site_settings_start_over_hint">Start your site again</string>
+ <string name="let_us_help">Let Us Help</string>
+ <string name="me_btn_app_settings">App Settings</string>
+ <string name="start_over">Start Over</string>
+ <string name="editor_remove_failed_uploads">Remove failed uploads</string>
+ <string name="editor_toast_failed_uploads">Some media uploads have failed. You can\'t save or publish\n your post in this state. Would you like to remove all failed media?</string>
+ <string name="comments_empty_list_filtered_trashed">No Binned comments</string>
+ <string name="site_settings_advanced_header">Advanced</string>
+ <string name="comments_empty_list_filtered_pending">No Pending comments</string>
+ <string name="comments_empty_list_filtered_approved">No Approved comments</string>
+ <string name="button_done">Done</string>
+ <string name="button_skip">Skip</string>
+ <string name="site_timeout_error">Couldn\'t connect to the WordPress site due to Timeout error.</string>
+ <string name="xmlrpc_malformed_response_error">Couldn\'t connect. The WordPress installation responded with an invalid XML-RPC document.</string>
+ <string name="xmlrpc_missing_method_error">Couldn\'t connect. Required XML-RPC methods are missing on the server.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Centre</string>
+ <string name="theme_free">Free</string>
+ <string name="theme_all">All</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Gallery</string>
+ <string name="post_format_image">Image</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Quote</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Information on WordPress.com courses and events (online &amp; in-person).</string>
+ <string name="post_format_aside">Aside</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Opportunities to participate in WordPress.com research &amp; surveys.</string>
+ <string name="notif_tips">Tips for getting the most out of WordPress.com.</string>
+ <string name="notif_community">Community</string>
+ <string name="replies_to_my_comments">Replies to my comments</string>
+ <string name="notif_suggestions">Suggestions</string>
+ <string name="notif_research">Research</string>
+ <string name="site_achievements">Site achievements</string>
+ <string name="username_mentions">Username mentions</string>
+ <string name="likes_on_my_posts">Likes on my posts</string>
+ <string name="site_follows">Site follows</string>
+ <string name="likes_on_my_comments">Likes on my comments</string>
+ <string name="comments_on_my_site">Comments on my site</string>
+ <string name="site_settings_list_editor_summary_other">%d items</string>
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="approve_auto_if_previously_approved">Known users\' comments</string>
+ <string name="approve_auto">All users</string>
+ <string name="approve_manual">No comments</string>
+ <string name="site_settings_paging_summary_other">%d comments per page</string>
+ <string name="site_settings_paging_summary_one">1 comment per page</string>
+ <string name="site_settings_multiple_links_summary_other">Require approval for more than %d links</string>
+ <string name="site_settings_multiple_links_summary_one">Require approval for more than 1 link</string>
+ <string name="site_settings_multiple_links_summary_zero">Require approval for more than 0 links</string>
+ <string name="detail_approve_auto">Automatically approve everyone\'s comments.</string>
+ <string name="detail_approve_auto_if_previously_approved">Automatically approve if the user has a previously approved comment</string>
+ <string name="detail_approve_manual">Require manual approval for everyone\'s comments.</string>
+ <string name="filter_trashed_posts">Binned</string>
+ <string name="days_quantity_one">1 day</string>
+ <string name="days_quantity_other">%d days</string>
+ <string name="filter_published_posts">Published</string>
+ <string name="filter_draft_posts">Drafts</string>
+ <string name="filter_scheduled_posts">Scheduled</string>
+ <string name="pending_email_change_snackbar">Click the verification link in the email sent to %1$s to confirm your new address</string>
+ <string name="primary_site">Primary site</string>
+ <string name="web_address">Web Address</string>
+ <string name="editor_toast_uploading_please_wait">You are currently uploading media. Please wait until this completes.</string>
+ <string name="error_refresh_comments_showing_older">Comments couldn\'t be refreshed at this time - showing older comments</string>
+ <string name="editor_post_settings_set_featured_image">Set Featured Image</string>
+ <string name="editor_post_settings_featured_image">Featured Image</string>
+ <string name="new_editor_promo_desc">The WordPress app for Android now includes a beautiful new visual\n editor. Try it out by creating a new post.</string>
+ <string name="new_editor_promo_title">Brand new editor</string>
+ <string name="new_editor_promo_button_label">Great, thanks!</string>
+ <string name="visual_editor_enabled">Visual Editor enabled</string>
+ <string name="editor_content_placeholder">Share your story here…</string>
+ <string name="editor_page_title_placeholder">Page Title</string>
+ <string name="editor_post_title_placeholder">Post Title</string>
+ <string name="email_address">Email address</string>
+ <string name="preference_show_visual_editor">Show visual editor</string>
+ <string name="dlg_sure_to_delete_comments">Permanently delete these comments?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Permanently delete this comment?</string>
+ <string name="mnu_comment_delete_permanently">Delete</string>
+ <string name="comment_deleted_permanently">Comment deleted</string>
+ <string name="mnu_comment_untrash">Restore</string>
+ <string name="comments_empty_list_filtered_spam">No Spam comments</string>
+ <string name="could_not_load_page">Could not load page</string>
+ <string name="comment_status_all">All</string>
+ <string name="interface_language">Interface Language</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">About the app</string>
+ <string name="error_post_account_settings">Couldn\'t save your account settings</string>
+ <string name="error_post_my_profile">Couldn\'t save your profile</string>
+ <string name="error_fetch_account_settings">Couldn\'t retrieve your account settings</string>
+ <string name="error_fetch_my_profile">Couldn\'t retrieve your profile</string>
+ <string name="stats_widget_promo_ok_btn_label">OK, got it</string>
+ <string name="stats_widget_promo_desc">Add the widget to your home screen to access your Stats in one click.</string>
+ <string name="stats_widget_promo_title">Home Screen Stats Widget</string>
+ <string name="site_settings_unknown_language_code_error">Language code not recognised</string>
+ <string name="site_settings_threading_dialog_description">Allow comments to be nested in threads.</string>
+ <string name="site_settings_threading_dialog_header">Thread up to</string>
+ <string name="remove">Remove</string>
+ <string name="search">Search</string>
+ <string name="add_category">Add category</string>
+ <string name="disabled">Disabled</string>
+ <string name="site_settings_image_original_size">Original Size</string>
+ <string name="privacy_private">Your site is visible only to you and users you approve</string>
+ <string name="privacy_public_not_indexed">Your site is visible to everyone but asks search engines not to index it</string>
+ <string name="privacy_public">Your site is visible to everyone and may be indexed by search engines</string>
+ <string name="about_me_hint">A few words about you…</string>
+ <string name="public_display_name_hint">Display name will default to your username if it is not set</string>
+ <string name="about_me">About me</string>
+ <string name="public_display_name">Public display name</string>
+ <string name="my_profile">My Profile</string>
+ <string name="first_name">First name</string>
+ <string name="last_name">Last name</string>
+ <string name="site_privacy_public_desc">Allow search engines to index this site</string>
+ <string name="site_privacy_hidden_desc">Discourage search engines from indexing this site</string>
+ <string name="site_privacy_private_desc">I would like my site to be private, visible only to users I choose</string>
+ <string name="cd_related_post_preview_image">Related post preview image</string>
+ <string name="error_post_remote_site_settings">Couldn\'t save site info</string>
+ <string name="error_fetch_remote_site_settings">Couldn\'t retrieve site info</string>
+ <string name="error_media_upload_connection">A connection error occurred while uploading media</string>
+ <string name="site_settings_disconnected_toast">Disconnected, editing disabled.</string>
+ <string name="site_settings_unsupported_version_error">Unsupported WordPress version</string>
+ <string name="site_settings_multiple_links_dialog_description">Require approval for comments that include more than this number of links.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatically close</string>
+ <string name="site_settings_close_after_dialog_description">Automatically close comments on articles.</string>
+ <string name="site_settings_paging_dialog_description">Break comment threads into multiple pages.</string>
+ <string name="site_settings_paging_dialog_header">Comments per page</string>
+ <string name="site_settings_close_after_dialog_title">Close commenting</string>
+ <string name="site_settings_blacklist_description">When a comment contains any of these words in its content, name, URL, email, or IP, it will be marked as spam. You can enter partial words, so "press" will match "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">When a comment contains any of these words in its content, name, URL, email, or IP, it will be held in the moderation queue. You can enter partial words, so "press" will match "WordPress."</string>
+ <string name="site_settings_list_editor_input_hint">Enter a word or phrase</string>
+ <string name="site_settings_list_editor_no_items_text">No items</string>
+ <string name="site_settings_learn_more_caption">You can override these settings for individual posts.</string>
+ <string name="site_settings_rp_preview3_site">in "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Upgrade Focus: VideoPress For Weddings</string>
+ <string name="site_settings_rp_preview2_site">in "Apps"</string>
+ <string name="site_settings_rp_preview2_title">The WordPress for Android App Gets a Big Facelift</string>
+ <string name="site_settings_rp_preview1_site">in "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Big iPhone/iPad Update Now Available</string>
+ <string name="site_settings_rp_show_images_title">Show Images</string>
+ <string name="site_settings_rp_show_header_title">Show Header</string>
+ <string name="site_settings_rp_switch_summary">Related Posts displays relevant content from your site below your posts.</string>
+ <string name="site_settings_rp_switch_title">Show Related Posts</string>
+ <string name="site_settings_delete_site_hint">Removes your site data from the app</string>
+ <string name="site_settings_blacklist_hint">Comments that match a filter are marked as spam</string>
+ <string name="site_settings_moderation_hold_hint">Comments that match a filter are put in the moderation queue</string>
+ <string name="site_settings_multiple_links_hint">Ignores link limit from known users</string>
+ <string name="site_settings_whitelist_hint">Comment author must have a previously approved comment</string>
+ <string name="site_settings_user_account_required_hint">Users must be registered and logged in to comment</string>
+ <string name="site_settings_identity_required_hint">Comment author must fill out name and e-mail</string>
+ <string name="site_settings_manual_approval_hint">Comments must be manually approved</string>
+ <string name="site_settings_paging_hint">Display comments in chunks of a specified size</string>
+ <string name="site_settings_threading_hint">Allow nested comments to a certain depth</string>
+ <string name="site_settings_sort_by_hint">Determines the order comments are displayed</string>
+ <string name="site_settings_close_after_hint">Disallow comments after the specified time</string>
+ <string name="site_settings_receive_pingbacks_hint">Allow link notifications from other blogs</string>
+ <string name="site_settings_send_pingbacks_hint">Attempt to notify any blogs linked to from the article</string>
+ <string name="site_settings_allow_comments_hint">Allow readers to post comments</string>
+ <string name="site_settings_discussion_hint">View and change your sites discussion settings</string>
+ <string name="site_settings_more_hint">View all available Discussion settings</string>
+ <string name="site_settings_related_posts_hint">Show or hide related posts in reader</string>
+ <string name="site_settings_upload_and_link_image_hint">Enable to always upload the fullsize image</string>
+ <string name="site_settings_image_width_hint">Resizes images in posts to this width</string>
+ <string name="site_settings_format_hint">Sets new post format</string>
+ <string name="site_settings_category_hint">Sets new post category</string>
+ <string name="site_settings_location_hint">Automatically add location data to your posts</string>
+ <string name="site_settings_password_hint">Change your password</string>
+ <string name="site_settings_username_hint">Current user account</string>
+ <string name="site_settings_language_hint">Language this blog is primarily written in</string>
+ <string name="site_settings_privacy_hint">Controls who can see your site</string>
+ <string name="site_settings_address_hint">Changing your address is not currently supported</string>
+ <string name="site_settings_tagline_hint">A short description or catchy phrase to describe your blog</string>
+ <string name="site_settings_title_hint">In a few words, explain what this site is about</string>
+ <string name="site_settings_whitelist_known_summary">Comments from known users</string>
+ <string name="site_settings_whitelist_all_summary">Comments from all users</string>
+ <string name="site_settings_threading_summary">%d levels</string>
+ <string name="site_settings_privacy_private_summary">Private</string>
+ <string name="site_settings_privacy_hidden_summary">Hidden</string>
+ <string name="site_settings_delete_site_title">Delete Site</string>
+ <string name="site_settings_privacy_public_summary">Public</string>
+ <string name="site_settings_blacklist_title">Blacklist</string>
+ <string name="site_settings_moderation_hold_title">Hold for Moderation</string>
+ <string name="site_settings_multiple_links_title">Links in comments</string>
+ <string name="site_settings_whitelist_title">Automatically approve</string>
+ <string name="site_settings_threading_title">Threading</string>
+ <string name="site_settings_paging_title">Paging</string>
+ <string name="site_settings_sort_by_title">Sort by</string>
+ <string name="site_settings_account_required_title">Users must be signed in</string>
+ <string name="site_settings_identity_required_title">Must include name and email</string>
+ <string name="site_settings_receive_pingbacks_title">Receive Pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Send Pingbacks</string>
+ <string name="site_settings_allow_comments_title">Allow Comments</string>
+ <string name="site_settings_default_format_title">Default Format</string>
+ <string name="site_settings_default_category_title">Default Category</string>
+ <string name="site_settings_location_title">Enable Location</string>
+ <string name="site_settings_address_title">Address</string>
+ <string name="site_settings_title_title">Site Title</string>
+ <string name="site_settings_tagline_title">Tagline</string>
+ <string name="site_settings_this_device_header">This device</string>
+ <string name="site_settings_discussion_new_posts_header">Defaults for new posts</string>
+ <string name="site_settings_account_header">Account</string>
+ <string name="site_settings_writing_header">Writing</string>
+ <string name="newest_first">Newest first</string>
+ <string name="site_settings_general_header">General</string>
+ <string name="discussion">Discussion</string>
+ <string name="privacy">Privacy</string>
+ <string name="related_posts">Related Posts</string>
+ <string name="comments">Comments</string>
+ <string name="close_after">Close after</string>
+ <string name="oldest_first">Oldest first</string>
+ <string name="media_error_no_permission_upload">You don\'t have permission to upload media to the site</string>
+ <string name="never">Never</string>
+ <string name="unknown">Unknown</string>
+ <string name="reader_err_get_post_not_found">This post no longer exists</string>
+ <string name="reader_err_get_post_not_authorized">You\'re not authorised to view this post</string>
+ <string name="reader_err_get_post_generic">Unable to retrieve this post</string>
+ <string name="blog_name_no_spaced_allowed">Site address can\'t contain spaces</string>
+ <string name="invalid_username_no_spaces">Username can\'t contain spaces</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">The sites you follow haven\'t posted anything recently</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">No recent posts</string>
+ <string name="media_details_copy_url_toast">URL copied to clipboard</string>
+ <string name="edit_media">Edit media</string>
+ <string name="media_details_copy_url">Copy URL</string>
+ <string name="media_details_label_date_uploaded">Uploaded</string>
+ <string name="media_details_label_date_added">Added</string>
+ <string name="selected_theme">Selected Theme</string>
+ <string name="could_not_load_theme">Could not load theme</string>
+ <string name="theme_activation_error">Something went wrong. Could not activate theme</string>
+ <string name="theme_by_author_prompt_append"> by %1$s</string>
+ <string name="theme_prompt">Thanks for choosing %1$s</string>
+ <string name="theme_try_and_customize">Try &amp; Customise</string>
+ <string name="theme_view">View</string>
+ <string name="theme_details">Details</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">DONE</string>
+ <string name="theme_manage_site">MANAGE SITE</string>
+ <string name="title_activity_theme_support">Themes</string>
+ <string name="theme_activate">Activate</string>
+ <string name="date_range_start_date">Start Date</string>
+ <string name="date_range_end_date">End Date</string>
+ <string name="current_theme">Current Theme</string>
+ <string name="customize">Customise</string>
+ <string name="details">Details</string>
+ <string name="support">Support</string>
+ <string name="active">Active</string>
+ <string name="stats_referrers_spam_generic_error">Something went wrong during the operation. The spam state wasn\'t changed.</string>
+ <string name="stats_referrers_marking_not_spam">Marking as not spam</string>
+ <string name="stats_referrers_unspam">Not spam</string>
+ <string name="stats_referrers_marking_spam">Marking as spam</string>
+ <string name="theme_auth_error_authenticate">Failed to fetch themes: failed authenticating user</string>
+ <string name="post_published">Post published</string>
+ <string name="page_published">Page published</string>
+ <string name="post_updated">Post updated</string>
+ <string name="page_updated">Page updated</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Sorry, no themes found.</string>
+ <string name="media_file_name">File name: %s</string>
+ <string name="media_uploaded_on">Uploaded on: %s</string>
+ <string name="media_dimensions">Dimensions: %s</string>
+ <string name="upload_queued">Queued</string>
+ <string name="media_file_type">File type: %s</string>
+ <string name="reader_label_gap_marker">Load more posts</string>
+ <string name="notifications_no_search_results">No sites matched \'%s\'</string>
+ <string name="search_sites">Search sites</string>
+ <string name="notifications_empty_view_reader">View Reader</string>
+ <string name="unread">Unread</string>
+ <string name="notifications_empty_action_followers_likes">Get noticed: comment on posts you\'ve read.</string>
+ <string name="notifications_empty_action_comments">Join a conversation: comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_action_unread">Reignite the conversation: write a new post.</string>
+ <string name="notifications_empty_action_all">Get active! Comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_likes">No new likes to show…yet.</string>
+ <string name="notifications_empty_followers">No new followers to report…yet.</string>
+ <string name="notifications_empty_comments">No new comments…yet.</string>
+ <string name="notifications_empty_unread">You\'re all caught up!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Please access the Stats in the app, and try adding the widget later</string>
+ <string name="stats_widget_error_readd_widget">Please remove the widget and re-add it again</string>
+ <string name="stats_widget_error_no_visible_blog">Stats couldn\'t be accessed without a visible blog</string>
+ <string name="stats_widget_error_no_permissions">Your WordPress.com account can\'t access Stats on this blog</string>
+ <string name="stats_widget_error_no_account">Please login into WordPress</string>
+ <string name="stats_widget_error_generic">Stats couldn\'t be loaded</string>
+ <string name="stats_widget_loading_data">Loading data…</string>
+ <string name="stats_widget_name_for_blog">Today\'s Stats for %1$s</string>
+ <string name="stats_widget_name">WordPress Today\'s Stats</string>
+ <string name="add_location_permission_required">Permission required in order to add location</string>
+ <string name="add_media_permission_required">Permissions required in order to add media</string>
+ <string name="access_media_permission_required">Permissions required in order to access media</string>
+ <string name="stats_enable_rest_api_in_jetpack">To view your stats, enable the JSON API module in Jetpack.</string>
+ <string name="error_open_list_from_notification">This post or page was published on another site</string>
+ <string name="reader_short_comment_count_multi">%s Comments</string>
+ <string name="reader_short_comment_count_one">1 Comment</string>
+ <string name="reader_label_submit_comment">SEND</string>
+ <string name="reader_hint_comment_on_post">Reply to post…</string>
+ <string name="reader_discover_visit_blog">Visit %s</string>
+ <string name="reader_discover_attribution_blog">Originally posted on %s</string>
+ <string name="reader_discover_attribution_author">Originally posted by %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originally posted by %1$s on %2$s</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_label_follow_count">%,d followers</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_menu_tags">Edit tags and blogs</string>
+ <string name="reader_title_post_detail">Reader Post</string>
+ <string name="local_draft_explainer">This post is a local draft which hasn\'t been published</string>
+ <string name="local_changes_explainer">This post has local changes which haven\'t been published</string>
+ <string name="notifications_push_summary">Settings for notifications that appear on your device.</string>
+ <string name="notifications_email_summary">Settings for notifications that are sent to the email tied to your account.</string>
+ <string name="notifications_tab_summary">Settings for notifications that appear in the Notifications tab.</string>
+ <string name="notifications_disabled">App notifications have been disabled. Tap here to enable them in Settings.</string>
+ <string name="notification_types">Notification Types</string>
+ <string name="error_loading_notifications">Couldn\'t load notification settings</string>
+ <string name="replies_to_your_comments">Replies to your comments</string>
+ <string name="comment_likes">Comment likes</string>
+ <string name="app_notifications">App notifications</string>
+ <string name="notifications_tab">Notifications tab</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Comments on other sites</string>
+ <string name="notifications_wpcom_updates">WordPress.com Updates</string>
+ <string name="notifications_other">Other</string>
+ <string name="notifications_account_emails">Email from WordPress.com</string>
+ <string name="notifications_account_emails_summary">We\'ll always send important emails regarding your account, but you can get some helpful extras, too.</string>
+ <string name="notifications_sights_and_sounds">Sights and Sounds</string>
+ <string name="your_sites">Your Sites</string>
+ <string name="stats_insights_latest_post_trend">It\'s been %1$s since %2$s was published. Here\'s how the post has performed so far…</string>
+ <string name="stats_insights_latest_post_summary">Latest Post Summary</string>
+ <string name="button_revert">Revert</string>
+ <string name="days_ago">%d days ago</string>
+ <string name="yesterday">Yesterday</string>
+ <string name="connectionbar_no_connection">No connection</string>
+ <string name="page_trashed">Page sent to bin</string>
+ <string name="post_deleted">Post deleted</string>
+ <string name="post_trashed">Post sent to bin</string>
+ <string name="stats_no_activity_this_period">No activity this period</string>
+ <string name="trashed">Binned</string>
+ <string name="button_back">Back</string>
+ <string name="page_deleted">Page deleted</string>
+ <string name="button_stats">Stats</string>
+ <string name="button_trash">Bin</string>
+ <string name="button_preview">Preview</string>
+ <string name="button_view">View</string>
+ <string name="button_edit">Edit</string>
+ <string name="button_publish">Publish</string>
+ <string name="my_site_no_sites_view_subtitle">Would you like to add one?</string>
+ <string name="my_site_no_sites_view_title">You don\'t have any WordPress sites yet.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">You are not authorised to access this blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">This blog could not be found</string>
+ <string name="undo">Undo</string>
+ <string name="tabbar_accessibility_label_my_site">My Site</string>
+ <string name="tabbar_accessibility_label_me">Me</string>
+ <string name="passcodelock_prompt_message">Enter your PIN</string>
+ <string name="editor_toast_changes_saved">Changes saved</string>
+ <string name="push_auth_expired">The request has expired. Sign in to WordPress.com to try again.</string>
+ <string name="stats_insights_best_ever">Best Views Ever</string>
+ <string name="ignore">Ignore</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% of views</string>
+ <string name="stats_insights_most_popular_hour">Most popular hour</string>
+ <string name="stats_insights_most_popular_day">Most popular day</string>
+ <string name="stats_insights_popular">Most popular day and hour</string>
+ <string name="stats_insights_today">Today\'s Stats</string>
+ <string name="stats_insights_all_time">All-time posts, views, and visitors</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">To view your stats, sign in to the WordPress.com account you used to connect Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Looking for your Other Recent Stats? We\'ve moved them to the Insights page.</string>
+ <string name="me_disconnect_from_wordpress_com">Disconnect from WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Connect to WordPress.com</string>
+ <string name="me_btn_login_logout">Login/Logout</string>
+ <string name="account_settings">Account Settings</string>
+ <string name="me_btn_support">Help &amp; Support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" wasn\'t hidden because it\'s the current site</string>
+ <string name="site_picker_create_dotcom">Create WordPress.com site</string>
+ <string name="site_picker_add_site">Add site</string>
+ <string name="site_picker_add_self_hosted">Add self-hosted site</string>
+ <string name="site_picker_edit_visibility">Show/hide sites</string>
+ <string name="my_site_btn_view_admin">View Admin</string>
+ <string name="my_site_btn_view_site">View Site</string>
+ <string name="site_picker_title">Choose site</string>
+ <string name="my_site_btn_switch_site">Switch Site</string>
+ <string name="my_site_btn_blog_posts">Blog Posts</string>
+ <string name="my_site_btn_site_settings">Settings</string>
+ <string name="my_site_header_look_and_feel">Look and Feel</string>
+ <string name="my_site_header_publish">Publish</string>
+ <string name="my_site_header_configuration">Configuration</string>
+ <string name="reader_label_new_posts_subtitle">Tap to show them</string>
+ <string name="notifications_account_required">Sign in to WordPress.com for notifications</string>
+ <string name="stats_unknown_author">Unknown Author</string>
+ <string name="image_added">Image added</string>
+ <string name="signout">Disconnect</string>
+ <string name="deselect_all">Deselect all</string>
+ <string name="show">Show</string>
+ <string name="hide">Hide</string>
+ <string name="select_all">Select all</string>
+ <string name="sign_out_wpcom_confirm">Disconnecting your account will remove all of @%s’s WordPress.com data from this device, including local drafts and local changes.</string>
+ <string name="select_from_new_picker">Multi-select with the new picker</string>
+ <string name="stats_generic_error">Required Stats couldn\'t be loaded</string>
+ <string name="no_device_videos">No videos</string>
+ <string name="no_blog_images">No images</string>
+ <string name="no_blog_videos">No videos</string>
+ <string name="no_device_images">No images</string>
+ <string name="error_loading_blog_images">Unable to fetch images</string>
+ <string name="error_loading_blog_videos">Unable to fetch videos</string>
+ <string name="error_loading_images">Error loading images</string>
+ <string name="error_loading_videos">Error loading videos</string>
+ <string name="loading_blog_images">Fetching images</string>
+ <string name="loading_blog_videos">Fetching videos</string>
+ <string name="no_media_sources">Couldn\'t fetch media</string>
+ <string name="loading_videos">Loading videos</string>
+ <string name="loading_images">Loading images</string>
+ <string name="no_media">No media</string>
+ <string name="device">Device</string>
+ <string name="language">Language</string>
+ <string name="add_to_post">Add to Post</string>
+ <string name="media_picker_title">Select media</string>
+ <string name="take_photo">Take a photo</string>
+ <string name="take_video">Take a video</string>
+ <string name="tab_title_device_images">Device Images</string>
+ <string name="tab_title_device_videos">Device Videos</string>
+ <string name="tab_title_site_images">Site Images</string>
+ <string name="tab_title_site_videos">Site Videos</string>
+ <string name="media_details_label_file_name">File name</string>
+ <string name="media_details_label_file_type">File type</string>
+ <string name="error_publish_no_network">Can\'t publish while there is no connection. Saved as draft.</string>
+ <string name="editor_toast_invalid_path">Invalid file path</string>
+ <string name="verification_code">Verification code</string>
+ <string name="invalid_verification_code">Invalid verification code</string>
+ <string name="verify">Verify</string>
+ <string name="two_step_footer_label">Enter the code from your authenticator app.</string>
+ <string name="two_step_footer_button">Send code via text message</string>
+ <string name="two_step_sms_sent">Check your text messages for the verification code.</string>
+ <string name="sign_in_jetpack">Sign in to your WordPress.com account to connect to Jetpack.</string>
+ <string name="auth_required">Sign in again to continue.</string>
+ <string name="reader_empty_posts_request_failed">Unable to retrieve posts</string>
+ <string name="publisher">Publisher:</string>
+ <string name="error_notification_open">Could not open notification</string>
+ <string name="stats_followers_total_email_paged">Showing %1$d - %2$d of %3$s Email Followers</string>
+ <string name="stats_search_terms_unknown_search_terms">Unknown Search Terms</string>
+ <string name="stats_followers_total_wpcom_paged">Showing %1$d - %2$d of %3$s WordPress.com Followers</string>
+ <string name="stats_empty_search_terms_desc">Learn more about your search traffic by looking at the terms your visitors searched for to find your site.</string>
+ <string name="stats_empty_search_terms">No search terms recorded</string>
+ <string name="stats_entry_search_terms">Search Term</string>
+ <string name="stats_view_authors">Authors</string>
+ <string name="stats_view_search_terms">Search Terms</string>
+ <string name="comments_fetching">Fetching comments…</string>
+ <string name="pages_fetching">Fetching pages…</string>
+ <string name="toast_err_post_uploading">Unable to open post while it\'s uploading</string>
+ <string name="posts_fetching">Fetching posts…</string>
+ <string name="media_fetching">Fetching media…</string>
+ <string name="post_uploading">Uploading</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">Overall</string>
+ <string name="stats_period">Period</string>
+ <string name="logs_copied_to_clipboard">Application logs have been copied to the clipboard</string>
+ <string name="reader_label_new_posts">New posts</string>
+ <string name="reader_empty_posts_in_blog">This blog is empty</string>
+ <string name="stats_average_per_day">Average per Day</string>
+ <string name="stats_recent_weeks">Recent Weeks</string>
+ <string name="error_copy_to_clipboard">An error occurred while copying text to clipboard</string>
+ <string name="reader_page_recommended_blogs">Sites you may like</string>
+ <string name="stats_months_and_years">Months and Years</string>
+ <string name="themes_fetching">Fetching themes…</string>
+ <string name="stats_for">Stats for %s</string>
+ <string name="stats_other_recent_stats_label">Other Recent Stats</string>
+ <string name="stats_view_all">View all</string>
+ <string name="stats_view">View</string>
+ <string name="stats_followers_months">%1$d months</string>
+ <string name="stats_followers_a_year">A year</string>
+ <string name="stats_followers_years">%1$d years</string>
+ <string name="stats_followers_a_month">A month</string>
+ <string name="stats_followers_minutes">%1$d minutes</string>
+ <string name="stats_followers_an_hour_ago">an hour ago</string>
+ <string name="stats_followers_hours">%1$d hours</string>
+ <string name="stats_followers_a_day">A day</string>
+ <string name="stats_followers_days">%1$d days</string>
+ <string name="stats_followers_a_minute_ago">a minute ago</string>
+ <string name="stats_followers_seconds_ago">seconds ago</string>
+ <string name="stats_followers_total_email">Total Email Followers: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Total WordPress.com Followers: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Total posts with comment followers: %1$s</string>
+ <string name="stats_comments_by_authors">By Authors</string>
+ <string name="stats_comments_by_posts_and_pages">By Posts &amp; Pages</string>
+ <string name="stats_empty_followers_desc">Keep track of your overall number of followers, and how long each one has been following your site.</string>
+ <string name="stats_empty_followers">No followers</string>
+ <string name="stats_empty_publicize_desc">Keep track of your followers from various social networking services using publicise.</string>
+ <string name="stats_empty_publicize">No publicise followers recorded</string>
+ <string name="stats_empty_video">No videos played</string>
+ <string name="stats_empty_video_desc">If you\'ve uploaded videos using VideoPress, find out how many times they’ve been watched.</string>
+ <string name="stats_empty_comments_desc">If you allow comments on your site, track your top commenters and discover what content sparks the liveliest conversations, based on the most recent 1,000 comments.</string>
+ <string name="stats_empty_tags_and_categories_desc">Get an overview of the most popular topics on your site, as reflected in your top posts from the past week.</string>
+ <string name="stats_empty_top_authors_desc">Track the views on each contributor\'s posts, and zoom in to discover the most popular content by each author.</string>
+ <string name="stats_empty_tags_and_categories">No tagged posts or pages viewed</string>
+ <string name="stats_empty_clicks_desc">When your content includes links to other sites, you’ll see which ones your visitors click on the most.</string>
+ <string name="stats_empty_referrers_desc">Learn more about your site’s visibility by looking at the websites and search engines that send the most traffic your way</string>
+ <string name="stats_empty_clicks_title">No clicks recorded</string>
+ <string name="stats_empty_referrers_title">No referrers recorded</string>
+ <string name="stats_empty_top_posts_title">No posts or pages viewed</string>
+ <string name="stats_empty_top_posts_desc">Discover what your most-viewed content is, and check how individual posts and pages perform over time.</string>
+ <string name="stats_totals_followers">Since</string>
+ <string name="stats_empty_geoviews">No countries recorded</string>
+ <string name="stats_empty_geoviews_desc">Explore the list to see which countries and regions generate the most traffic to your site.</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Author</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_followers">Follower</string>
+ <string name="stats_totals_publicize">Followers</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_view_top_posts_and_pages">Posts &amp; Pages</string>
+ <string name="stats_view_videos">Videos</string>
+ <string name="stats_view_publicize">Publicise</string>
+ <string name="stats_view_followers">Followers</string>
+ <string name="stats_view_countries">Countries</string>
+ <string name="stats_likes">Likes</string>
+ <string name="stats_pagination_label">Page %1$s of %2$s</string>
+ <string name="stats_timeframe_years">Years</string>
+ <string name="stats_views">Views</string>
+ <string name="stats_visitors">Visitors</string>
+ <string name="ssl_certificate_details">Details</string>
+ <string name="delete_sure_post">Delete this post</string>
+ <string name="delete_sure">Delete this draft</string>
+ <string name="delete_sure_page">Delete this page</string>
+ <string name="confirm_delete_multi_media">Delete selected items?</string>
+ <string name="confirm_delete_media">Delete selected item?</string>
+ <string name="cab_selected">%d selected</string>
+ <string name="media_gallery_date_range">Displaying media from %1$s to %2$s</string>
+ <string name="sure_to_remove_account">Remove this site?</string>
+ <string name="reader_empty_followed_blogs_title">You\'re not following any sites yet</string>
+ <string name="reader_empty_posts_liked">You haven\'t liked any posts</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Browse our FAQ</string>
+ <string name="nux_help_description">Visit the help centre to get answers to common questions or visit the forums to ask new ones</string>
+ <string name="agree_terms_of_service">By creating an account you agree to the fascinating %1$sTerms of Service%2$s</string>
+ <string name="create_new_blog_wpcom">Create WordPress.com blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog created!</string>
+ <string name="reader_empty_comments">No comments yet</string>
+ <string name="reader_empty_posts_in_tag">No posts with this tag</string>
+ <string name="reader_label_comment_count_multi">%,d comments</string>
+ <string name="reader_label_view_original">View original article</string>
+ <string name="reader_label_like">Like</string>
+ <string name="reader_label_liked_by">Liked By</string>
+ <string name="reader_label_comment_count_single">One comment</string>
+ <string name="reader_label_comments_closed">Comments are closed</string>
+ <string name="reader_label_comments_on">Comments on</string>
+ <string name="reader_title_photo_viewer">%1$d of %2$d</string>
+ <string name="error_publish_empty_post">Can\'t publish an empty post</string>
+ <string name="error_refresh_unauthorized_posts">You don\'t have permission to view or edit posts</string>
+ <string name="error_refresh_unauthorized_pages">You don\'t have permission to view or edit pages</string>
+ <string name="error_refresh_unauthorized_comments">You don\'t have permission to view or edit comments</string>
+ <string name="older_month">Older than a month</string>
+ <string name="more">More</string>
+ <string name="older_two_days">Older than 2 days</string>
+ <string name="older_last_week">Older than a week</string>
+ <string name="stats_no_blog">Stats couldn\'t be loaded for the required blog</string>
+ <string name="select_a_blog">Select a WordPress site</string>
+ <string name="sending_content">Uploading %s content</string>
+ <string name="uploading_total">Uploading %1$d of %2$d</string>
+ <string name="mnu_comment_liked">Liked</string>
+ <string name="comment">Comment</string>
+ <string name="comment_trashed">Comment binned</string>
+ <string name="posts_empty_list">No posts yet. Why not create one?</string>
+ <string name="comment_reply_to_user">Reply to %s</string>
+ <string name="pages_empty_list">No pages yet. Why not create one?</string>
+ <string name="media_empty_list_custom_date">No media in this time interval</string>
+ <string name="posting_post">Posting "%s"</string>
+ <string name="signing_out">Signing out…</string>
+ <string name="reader_toast_err_generic">Unable to perform this action</string>
+ <string name="reader_toast_err_block_blog">Unable to block this blog</string>
+ <string name="reader_toast_blog_blocked">Posts from this blog will no longer be shown</string>
+ <string name="reader_menu_block_blog">Block this blog</string>
+ <string name="contact_us">Contact us</string>
+ <string name="hs__conversation_detail_error">Describe the problem you\'re seeing</string>
+ <string name="hs__new_conversation_header">Support chat</string>
+ <string name="hs__conversation_header">Support chat</string>
+ <string name="hs__username_blank_error">Enter a valid name</string>
+ <string name="hs__invalid_email_error">Enter a valid email address</string>
+ <string name="add_location">Add location</string>
+ <string name="current_location">Current location</string>
+ <string name="search_location">Search</string>
+ <string name="edit_location">Edit</string>
+ <string name="search_current_location">Locate</string>
+ <string name="preference_send_usage_stats">Send statistics</string>
+ <string name="preference_send_usage_stats_summary">Automatically send usage statistics to help us improve WordPress for Android</string>
+ <string name="update_verb">Update</string>
+ <string name="schedule_verb">Schedule</string>
+ <string name="reader_title_blog_preview">Reader Blog</string>
+ <string name="reader_title_tag_preview">Reader Tag</string>
+ <string name="reader_title_subs">Tags &amp; Blogs</string>
+ <string name="reader_page_followed_tags">Followed tags</string>
+ <string name="reader_page_followed_blogs">Followed sites</string>
+ <string name="reader_hint_add_tag_or_url">Enter a URL or tag to follow</string>
+ <string name="reader_label_followed_blog">Blog followed</string>
+ <string name="reader_label_tag_preview">Posts tagged %s</string>
+ <string name="reader_toast_err_get_blog_info">Unable to show this blog</string>
+ <string name="reader_toast_err_already_follow_blog">You already follow this blog</string>
+ <string name="reader_toast_err_follow_blog">Unable to follow this blog</string>
+ <string name="reader_toast_err_unfollow_blog">Unable to unfollow this blog</string>
+ <string name="reader_empty_recommended_blogs">No recommended blogs</string>
+ <string name="saving">Saving…</string>
+ <string name="media_empty_list">No media</string>
+ <string name="ptr_tip_message">Tip: Pull down to refresh</string>
+ <string name="help">Help</string>
+ <string name="forgot_password">Lost your password?</string>
+ <string name="forums">Forums</string>
+ <string name="help_center">Help centre</string>
+ <string name="ssl_certificate_error">Invalid SSL certificate</string>
+ <string name="ssl_certificate_ask_trust">If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn\'t continue. Would you like to trust the certificate anyway?</string>
+ <string name="out_of_memory">Device out of memory</string>
+ <string name="no_network_message">There is no network available</string>
+ <string name="could_not_remove_account">Couldn\'t remove site</string>
+ <string name="gallery_error">The media item couldn\'t be retrieved</string>
+ <string name="blog_not_found">An error occurred when accessing this blog</string>
+ <string name="wait_until_upload_completes">Wait until upload completes</string>
+ <string name="theme_fetch_failed">Failed to fetch themes</string>
+ <string name="theme_set_failed">Failed to set theme</string>
+ <string name="theme_auth_error_message">Ensure you have the privilege to set themes</string>
+ <string name="comments_empty_list">No comments</string>
+ <string name="mnu_comment_unspam">Not spam</string>
+ <string name="no_site_error">Couldn\'t connect to the WordPress site</string>
+ <string name="adding_cat_failed">Adding category failed</string>
+ <string name="adding_cat_success">Category added successfully</string>
+ <string name="cat_name_required">The category name field is required</string>
+ <string name="category_automatically_renamed">Category name %1$s isn\'t valid. It has been renamed to %2$s.</string>
+ <string name="no_account">No WordPress account found, add an account and try again</string>
+ <string name="sdcard_message">A mounted SD card is required to upload media</string>
+ <string name="stats_empty_comments">No comments yet</string>
+ <string name="stats_bar_graph_empty">No stats available</string>
+ <string name="invalid_url_message">Check that the URL entered is valid</string>
+ <string name="reply_failed">Reply failed</string>
+ <string name="notifications_empty_list">No notifications</string>
+ <string name="error_delete_post">An error occurred while deleting the %s</string>
+ <string name="error_refresh_posts">Posts couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_pages">Pages couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_notifications">Notifications couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_comments">Comments couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_stats">Stats couldn\'t be refreshed at this time</string>
+ <string name="error_generic">An error occurred</string>
+ <string name="error_moderate_comment">An error occurred while moderating</string>
+ <string name="error_edit_comment">An error occurred while editing the comment</string>
+ <string name="error_upload">An error occurred while uploading the %s</string>
+ <string name="error_load_comment">Couldn\'t load the comment</string>
+ <string name="error_downloading_image">Error downloading image</string>
+ <string name="passcode_wrong_passcode">Wrong PIN</string>
+ <string name="invalid_email_message">Your email address isn\'t valid</string>
+ <string name="invalid_password_message">Password must contain at least 4 characters</string>
+ <string name="invalid_username_too_short">Username must be longer than 4 characters</string>
+ <string name="invalid_username_too_long">Username must be shorter than 61 characters</string>
+ <string name="username_only_lowercase_letters_and_numbers">Username can only contain lowercase letters (a-z) and numbers</string>
+ <string name="username_required">Enter a username</string>
+ <string name="username_not_allowed">Username not allowed</string>
+ <string name="username_must_be_at_least_four_characters">Username must be at least 4 characters</string>
+ <string name="username_contains_invalid_characters">Username may not contain the character “_”</string>
+ <string name="username_must_include_letters">Username must have a least 1 letter (a-z)</string>
+ <string name="email_invalid">Enter a valid email address</string>
+ <string name="email_not_allowed">That email address isn\'t allowed</string>
+ <string name="username_exists">That username already exists</string>
+ <string name="email_exists">That email address is already being used</string>
+ <string name="username_reserved_but_may_be_available">That username is currently reserved but may be available in a couple of days</string>
+ <string name="blog_name_required">Enter a site address</string>
+ <string name="blog_name_not_allowed">That site address isn\'t allowed</string>
+ <string name="blog_name_must_be_at_least_four_characters">Site address must be at least 4 characters</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">The site address must be shorter than 64 characters</string>
+ <string name="blog_name_contains_invalid_characters">Site address may not contain the character “_”</string>
+ <string name="blog_name_cant_be_used">You may not use that site address</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Site address can only contain lowercase letters (a-z) and numbers</string>
+ <string name="blog_name_exists">That site already exists</string>
+ <string name="blog_name_reserved">That site is reserved</string>
+ <string name="blog_name_reserved_but_may_be_available">That site is currently reserved but may be available in a couple days</string>
+ <string name="username_or_password_incorrect">The username or password you entered is incorrect</string>
+ <string name="nux_cannot_log_in">We can\'t log you in</string>
+ <string name="xmlrpc_error">Couldn\'t connect. Enter the full path to xmlrpc.php on your site and try again.</string>
+ <string name="select_categories">Select categories</string>
+ <string name="account_details">Account details</string>
+ <string name="edit_post">Edit post</string>
+ <string name="add_comment">Add comment</string>
+ <string name="connection_error">Connection error</string>
+ <string name="cancel_edit">Cancel edit</string>
+ <string name="scaled_image_error">Enter a valid scaled width value</string>
+ <string name="post_not_found">An error occurred when loading the post. Refresh your posts and try again.</string>
+ <string name="learn_more">Learn more</string>
+ <string name="media_gallery_settings_title">Gallery settings</string>
+ <string name="media_gallery_image_order">Image order</string>
+ <string name="media_gallery_num_columns">Number of columns</string>
+ <string name="media_gallery_type_thumbnail_grid">Thumbnail grid</string>
+ <string name="media_gallery_edit">Edit gallery</string>
+ <string name="media_error_no_permission">You don\'t have permission to view the media library</string>
+ <string name="cannot_delete_multi_media_items">Some media can\'t be deleted at this time. Try again later.</string>
+ <string name="themes_live_preview">Live preview</string>
+ <string name="theme_current_theme">Current theme</string>
+ <string name="theme_premium_theme">Premium theme</string>
+ <string name="link_enter_url_text">Link text (optional)</string>
+ <string name="create_a_link">Create a link</string>
+ <string name="page_settings">Page settings</string>
+ <string name="local_draft">Local draft</string>
+ <string name="upload_failed">Upload failed</string>
+ <string name="horizontal_alignment">Horizontal alignment</string>
+ <string name="file_not_found">Couldn\'t find the media file for upload. Was it deleted or moved?</string>
+ <string name="post_settings">Post settings</string>
+ <string name="delete_post">Delete post</string>
+ <string name="delete_page">Delete page</string>
+ <string name="comment_status_approved">Approved</string>
+ <string name="comment_status_unapproved">Pending</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Binned</string>
+ <string name="edit_comment">Edit comment</string>
+ <string name="mnu_comment_approve">Approve</string>
+ <string name="mnu_comment_unapprove">Unapprove</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Bin</string>
+ <string name="dlg_approving_comments">Approving</string>
+ <string name="dlg_unapproving_comments">Unapproving</string>
+ <string name="dlg_spamming_comments">Marking as spam</string>
+ <string name="dlg_trashing_comments">Sending to bin</string>
+ <string name="dlg_confirm_trash_comments">Send to bin?</string>
+ <string name="trash_yes">Bin</string>
+ <string name="trash_no">Don\'t bin</string>
+ <string name="trash">Bin</string>
+ <string name="author_name">Author name</string>
+ <string name="author_email">Author email</string>
+ <string name="author_url">Author URL</string>
+ <string name="hint_comment_content">Comment</string>
+ <string name="saving_changes">Saving changes</string>
+ <string name="sure_to_cancel_edit_comment">Cancel editing this comment?</string>
+ <string name="content_required">Comment is required</string>
+ <string name="toast_comment_unedited">Comment hasn\'t changed</string>
+ <string name="remove_account">Remove site</string>
+ <string name="blog_removed_successfully">Site removed successfully</string>
+ <string name="delete_draft">Delete draft</string>
+ <string name="preview_page">Preview page</string>
+ <string name="preview_post">Preview post</string>
+ <string name="comment_added">Comment added successfully</string>
+ <string name="post_not_published">Post status isn\'t published</string>
+ <string name="page_not_published">Page status isn\'t published</string>
+ <string name="view_in_browser">View in browser</string>
+ <string name="add_new_category">Add new category</string>
+ <string name="category_name">Category name</string>
+ <string name="category_slug">Category slug (optional)</string>
+ <string name="category_desc">Category description (optional)</string>
+ <string name="category_parent">Category parent (optional):</string>
+ <string name="share_action_post">New post</string>
+ <string name="share_action_media">Media library</string>
+ <string name="file_error_create">Couldn\'t create temp file for media upload. Make sure there is enough free space on your device.</string>
+ <string name="location_not_found">Unknown location</string>
+ <string name="open_source_licenses">Open source licenses</string>
+ <string name="invalid_site_url_message">Check that the site URL entered is valid</string>
+ <string name="pending_review">Pending review</string>
+ <string name="http_credentials">HTTP credentials (optional)</string>
+ <string name="http_authorization_required">Authorisation required</string>
+ <string name="post_format">Post format</string>
+ <string name="notifications_empty_all">No notifications…yet.</string>
+ <string name="new_post">New post</string>
+ <string name="new_media">New media</string>
+ <string name="view_site">View site</string>
+ <string name="privacy_policy">Privacy policy</string>
+ <string name="local_changes">Local changes</string>
+ <string name="image_settings">Image settings</string>
+ <string name="add_account_blog_url">Blog address</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="error_blog_hidden">This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again.</string>
+ <string name="fatal_db_error">An error occurred while creating the app database. Try reinstalling the app.</string>
+ <string name="jetpack_message_not_admin">The Jetpack plugin is required for stats. Contact the site administrator.</string>
+ <string name="reader_title_applog">Application log</string>
+ <string name="reader_share_link">Share link</string>
+ <string name="reader_toast_err_add_tag">Unable to add this tag</string>
+ <string name="reader_toast_err_remove_tag">Unable to remove this tag</string>
+ <string name="required_field">Required field</string>
+ <string name="email_hint">Email address</string>
+ <string name="site_address">Your self-hosted address (URL)</string>
+ <string name="email_cant_be_used_to_signup">You can\'t use that email address to signup. We are having problems with them blocking some of our email. Use another email provider.</string>
+ <string name="email_reserved">That email address has already been used. Check your inbox for an activation email. If you don\'t activate you can try again in a few days.</string>
+ <string name="blog_name_must_include_letters">Site address must have at least 1 letter (a-z)</string>
+ <string name="blog_name_invalid">Invalid site address</string>
+ <string name="blog_title_invalid">Invalid site title</string>
+ <string name="deleting_page">Deleting page</string>
+ <string name="deleting_post">Deleting post</string>
+ <string name="share_url_post">Share post</string>
+ <string name="share_url_page">Share page</string>
+ <string name="share_link">Share link</string>
+ <string name="creating_your_account">Creating your account</string>
+ <string name="creating_your_site">Creating your site</string>
+ <string name="reader_empty_posts_in_tag_updating">Fetching posts…</string>
+ <string name="error_refresh_media">Something went wrong while refreshing the media library. Try again later.</string>
+ <string name="reader_likes_you_and_multi">You and %,d others like this</string>
+ <string name="reader_likes_multi">%,d people like this</string>
+ <string name="reader_toast_err_get_comment">Unable to retrieve this comment</string>
+ <string name="reader_label_reply">Reply</string>
+ <string name="video">Video</string>
+ <string name="download">Downloading media</string>
+ <string name="comment_spammed">Comment marked as spam</string>
+ <string name="cant_share_no_visible_blog">You can\'t share to WordPress without a visible blog</string>
+ <string name="select_time">Select time</string>
+ <string name="reader_likes_you_and_one">You and one other like this</string>
+ <string name="reader_empty_followed_blogs_description">But don\'t worry, just tap the icon at the top right to start exploring!</string>
+ <string name="select_date">Select date</string>
+ <string name="pick_photo">Select photo</string>
+ <string name="account_two_step_auth_enabled">This account has two step authentication enabled. Visit your security settings on WordPress.com and generate an application-specific password.</string>
+ <string name="pick_video">Select video</string>
+ <string name="reader_toast_err_get_post">Unable to retrieve this post</string>
+ <string name="validating_user_data">Validating user data</string>
+ <string name="validating_site_data">Validating site data</string>
+ <string name="password_invalid">You need a more secure password. Make sure to use 7 or more characters, mix uppercase and lowercase letters, numbers or special characters.</string>
+ <string name="nux_tap_continue">Continue</string>
+ <string name="nux_welcome_create_account">Create account</string>
+ <string name="signing_in">Signing in…</string>
+ <string name="nux_add_selfhosted_blog">Add self-hosted site</string>
+ <string name="nux_oops_not_selfhosted_blog">Sign in to WordPress.com</string>
+ <string name="media_add_popup_title">Add to media library</string>
+ <string name="media_add_new_media_gallery">Create gallery</string>
+ <string name="empty_list_default">This list is empty</string>
+ <string name="select_from_media_library">Select from media library</string>
+ <string name="jetpack_message">The Jetpack plugin is required for stats. Do you want to install Jetpack?</string>
+ <string name="jetpack_not_found">Jetpack plugin not found</string>
+ <string name="reader_untitled_post">(Untitled)</string>
+ <string name="reader_share_subject">Shared from %s</string>
+ <string name="reader_btn_share">Share</string>
+ <string name="reader_btn_follow">Follow</string>
+ <string name="reader_btn_unfollow">Following</string>
+ <string name="reader_hint_comment_on_comment">Reply to comment…</string>
+ <string name="reader_label_added_tag">Added %s</string>
+ <string name="reader_label_removed_tag">Removed %s</string>
+ <string name="reader_likes_one">One person likes this</string>
+ <string name="reader_likes_only_you">You like this</string>
+ <string name="reader_toast_err_comment_failed">Couldn\'t post your comment</string>
+ <string name="reader_toast_err_tag_exists">You already follow this tag</string>
+ <string name="reader_toast_err_tag_invalid">That isn\'t a valid tag</string>
+ <string name="reader_toast_err_share_intent">Unable to share</string>
+ <string name="reader_toast_err_view_image">Unable to view image</string>
+ <string name="reader_toast_err_url_intent">Unable to open %s</string>
+ <string name="reader_empty_followed_tags">You don\'t follow any tags</string>
+ <string name="create_account_wpcom">Create an account on WordPress.com</string>
+ <string name="button_next">Next</string>
+ <string name="connecting_wpcom">Connecting to WordPress.com</string>
+ <string name="username_invalid">Invalid username</string>
+ <string name="limit_reached">Limit reached. You can try again in 1 minute. Trying again before that will only increase the time you have to wait before the ban is lifted. If you think this is in error, contact support.</string>
+ <string name="nux_tutorial_get_started_title">Get started!</string>
+ <string name="themes">Themes</string>
+ <string name="all">All</string>
+ <string name="images">Images</string>
+ <string name="unattached">Unattached</string>
+ <string name="custom_date">Custom Date</string>
+ <string name="media_add_popup_capture_photo">Capture photo</string>
+ <string name="media_add_popup_capture_video">Capture video</string>
+ <string name="media_gallery_image_order_random">Random</string>
+ <string name="media_gallery_image_order_reverse">Reverse</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="media_gallery_type_squares">Squares</string>
+ <string name="media_gallery_type_tiled">Tiled</string>
+ <string name="media_gallery_type_circles">Circles</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="media_edit_title_text">Title</string>
+ <string name="media_edit_caption_text">Caption</string>
+ <string name="media_edit_description_text">Description</string>
+ <string name="media_edit_title_hint">Enter a title here</string>
+ <string name="media_edit_caption_hint">Enter a caption here</string>
+ <string name="media_edit_description_hint">Enter a description here</string>
+ <string name="media_edit_success">Updated</string>
+ <string name="media_edit_failure">Failed to update</string>
+ <string name="themes_details_label">Details</string>
+ <string name="themes_features_label">Features</string>
+ <string name="theme_activate_button">Activate</string>
+ <string name="theme_activating_button">Activating</string>
+ <string name="theme_set_success">Successfully set theme!</string>
+ <string name="theme_auth_error_title">Failed to fetch themes</string>
+ <string name="post_excerpt">Excerpt</string>
+ <string name="share_action_title">Add to …</string>
+ <string name="share_action">Share</string>
+ <string name="stats">Stats</string>
+ <string name="stats_view_visitors_and_views">Visitors and Views</string>
+ <string name="stats_view_clicks">Clicks</string>
+ <string name="stats_view_tags_and_categories">Tags &amp; Categoriess</string>
+ <string name="stats_view_referrers">Referrers</string>
+ <string name="stats_timeframe_today">Today</string>
+ <string name="stats_timeframe_yesterday">Yesterday</string>
+ <string name="stats_timeframe_days">Days</string>
+ <string name="stats_timeframe_weeks">Weeks</string>
+ <string name="stats_timeframe_months">Months</string>
+ <string name="stats_entry_country">Country</string>
+ <string name="stats_entry_posts_and_pages">Title</string>
+ <string name="stats_entry_tags_and_categories">Topic</string>
+ <string name="stats_entry_authors">Author</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_totals_views">Views</string>
+ <string name="stats_totals_clicks">Clicks</string>
+ <string name="stats_totals_plays">Plays</string>
+ <string name="passcode_manage">Manage PIN lock</string>
+ <string name="passcode_enter_passcode">Enter your PIN</string>
+ <string name="passcode_enter_old_passcode">Enter your old PIN</string>
+ <string name="passcode_re_enter_passcode">Re-enter your PIN</string>
+ <string name="passcode_change_passcode">Change PIN</string>
+ <string name="passcode_set">PIN set</string>
+ <string name="passcode_preference_title">PIN lock</string>
+ <string name="passcode_turn_off">Turn PIN lock off</string>
+ <string name="passcode_turn_on">Turn PIN lock on</string>
+ <string name="upload">Upload</string>
+ <string name="discard">Discard</string>
+ <string name="sign_in">Sign in</string>
+ <string name="notifications">Notifications</string>
+ <string name="note_reply_successful">Reply published</string>
+ <string name="follows">Follows</string>
+ <string name="new_notifications">%d new notifications</string>
+ <string name="more_notifications">and %d more.</string>
+ <string name="loading">Loading…</string>
+ <string name="httpuser">HTTP username</string>
+ <string name="httppassword">HTTP password</string>
+ <string name="error_media_upload">An error occurred while uploading media</string>
+ <string name="post_content">Content (tap to add text and media)</string>
+ <string name="publish_date">Publish</string>
+ <string name="content_description_add_media">Add media</string>
+ <string name="incorrect_credentials">Incorrect username or password.</string>
+ <string name="password">Password</string>
+ <string name="username">Username</string>
+ <string name="reader">Reader</string>
+ <string name="featured">Use as featured image</string>
+ <string name="featured_in_post">Include image in post content</string>
+ <string name="no_network_title">No network available</string>
+ <string name="pages">Pages</string>
+ <string name="caption">Caption (optional)</string>
+ <string name="width">Width</string>
+ <string name="posts">Posts</string>
+ <string name="anonymous">Anonymous</string>
+ <string name="page">Page</string>
+ <string name="post">Post</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Upload and link to scaled image</string>
+ <string name="scaled_image">Scaled image width</string>
+ <string name="scheduled">Scheduled</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Uploading…</string>
+ <string name="version">Version</string>
+ <string name="tos">Terms of Service</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">Default Image Width</string>
+ <string name="image_alignment">Alignment</string>
+ <string name="refresh">Refresh</string>
+ <string name="untitled">Untitled</string>
+ <string name="edit">Edit</string>
+ <string name="post_id">Post</string>
+ <string name="page_id">Page</string>
+ <string name="post_password">Password (optional)</string>
+ <string name="immediately">Immediately</string>
+ <string name="quickpress_add_alert_title">Set shortcut name</string>
+ <string name="today">Today</string>
+ <string name="settings">Settings</string>
+ <string name="share_url">Share URL</string>
+ <string name="quickpress_window_title">Select blog for QuickPress shortcut</string>
+ <string name="quickpress_add_error">Shortcut name can\'t be empty</string>
+ <string name="publish_post">Publish</string>
+ <string name="draft">Draft</string>
+ <string name="post_private">Private</string>
+ <string name="upload_full_size_image">Upload and link to full image</string>
+ <string name="title">Title</string>
+ <string name="tags_separate_with_commas">Tags (separate tags with commas)</string>
+ <string name="categories">Categories</string>
+ <string name="dlg_deleting_comments">Deleting comments</string>
+ <string name="notification_blink">Blink notification light</string>
+ <string name="notification_sound">Notification sound</string>
+ <string name="notification_vibrate">Vibrate</string>
+ <string name="status">Status</string>
+ <string name="location">Location</string>
+ <string name="sdcard_title">SD Card Required</string>
+ <string name="select_video">Select a video from gallery</string>
+ <string name="media">Media</string>
+ <string name="delete">Delete</string>
+ <string name="none">None</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Select a photo from gallery</string>
+ <string name="error">Error</string>
+ <string name="cancel">Cancel</string>
+ <string name="save">Save</string>
+ <string name="add">Add</string>
+ <string name="category_refresh_error">Category refresh error</string>
+ <string name="preview">Preview</string>
+ <string name="on">on</string>
+ <string name="reply">Reply</string>
+ <string name="notification_settings">Notification Settings</string>
+ <string name="yes">Yes</string>
+ <string name="no">No</string>
+</resources>
diff --git a/WordPress/src/main/res/values-es-rCL/strings.xml b/WordPress/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 000000000..f2d77a687
--- /dev/null
+++ b/WordPress/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,728 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="editor_failed_uploads_switch_html">Algunas cargas de archivos fallaron. Puedes cambiar a modo HTML en este estado. ¿Eliminar los archivos que fallaron y continuar?</string>
+ <string name="format_bar_description_html">Modo HTML</string>
+ <string name="visual_editor">Editor Visual</string>
+ <string name="image_thumbnail">Miniatura de imagen</string>
+ <string name="format_bar_description_ul">Lista desordenada</string>
+ <string name="format_bar_description_ol">Lista ordenada</string>
+ <string name="format_bar_description_more">Insertar etiqueta Más</string>
+ <string name="format_bar_description_media">Insertar Media</string>
+ <string name="format_bar_description_strike">Texto tachado</string>
+ <string name="format_bar_description_quote">Bloque de Cita</string>
+ <string name="format_bar_description_link">Insertar Enlace</string>
+ <string name="format_bar_description_italic">Cursiva</string>
+ <string name="format_bar_description_underline">Subrayado</string>
+ <string name="image_settings_save_toast">Cambios guardados</string>
+ <string name="image_caption">Leyenda</string>
+ <string name="image_alt_text">Texto alternativo</string>
+ <string name="image_link_to">Enlace a</string>
+ <string name="image_width">Ancho</string>
+ <string name="format_bar_description_bold">Negrita</string>
+ <string name="image_settings_dismiss_dialog_title">¿Descartar los cambios no guardados?</string>
+ <string name="stop_upload_dialog_title">¿Detener la cargar?</string>
+ <string name="stop_upload_button">Detener la Carga</string>
+ <string name="alert_error_adding_media">Se produjo un error al cargar los archivos</string>
+ <string name="alert_action_while_uploading">Actualmente estás subiendo archivos. Por favor, espera hasta que termine.</string>
+ <string name="alert_insert_image_html_mode">No se puede insertar medios directamente en modo HTML. Por favor, cambia al modo visual.</string>
+ <string name="uploading_gallery_placeholder">Cargando Galería...</string>
+ <string name="invite_sent">Invitación enviada exitosamente</string>
+ <string name="tap_to_try_again">¡Toca para volver a intentarlo!</string>
+ <string name="invite_message_info">(Opcional) Puedes escribir un mensaje personalizado de hasta 500 caracteres que serán incluidos en la invitación a lo(s) usuario(s).</string>
+ <string name="invite_message_remaining_other">%d caracteres restantes</string>
+ <string name="invite_message_remaining_one">1 caracter restantes</string>
+ <string name="invite_message_remaining_zero">0 caracteres restantes</string>
+ <string name="invite_invalid_email">La dirección de correo electrónico \'%s\' no es válida</string>
+ <string name="invite_message_title">Mensaje Personalizado</string>
+ <string name="invite_already_a_member">Ya hay un miembro con nombre de usuario \'%s\'</string>
+ <string name="invite_username_not_found">No se encontró ningún usuario para el nombre de usuario \'%s\'</string>
+ <string name="invite">Invitar</string>
+ <string name="invite_names_title">Nombres de usuario o correo electrónico</string>
+ <string name="interface_language">Idioma de la interfaz</string>
+ <string name="off">Desactivado</string>
+ <string name="about_the_app">Acerca de la aplicación</string>
+ <string name="error_post_account_settings">No pude guardar la configuración de la cuenta</string>
+ <string name="error_post_my_profile">No pude guardar tu perfil</string>
+ <string name="add_media_permission_required">Permisos necesarios para añadir archivos multimedia</string>
+ <string name="access_media_permission_required">Permisos necesarios para acceder a archivos multimedia</string>
+ <string name="stats_enable_rest_api_in_jetpack">Para ver tus estadísticas debes activar el módulo JSON API en Jetpack.</string>
+ <string name="error_open_list_from_notification">Esta entrada o página fue publicada en otro sitio</string>
+ <string name="reader_short_comment_count_multi">%s Comentarios</string>
+ <string name="reader_short_comment_count_one">1 Comentario</string>
+ <string name="reader_label_submit_comment">ENVIAR</string>
+ <string name="reader_hint_comment_on_post">Responder a la entrada...</string>
+ <string name="reader_discover_visit_blog">Visita %s</string>
+ <string name="reader_discover_attribution_blog">Originalmente publicado en %s</string>
+ <string name="reader_discover_attribution_author">Originalmente publicado por %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originalmente publicado por %1$s en %2$s</string>
+ <string name="reader_short_like_count_multi">%s Me gusta</string>
+ <string name="reader_short_like_count_one">1 Me gusta</string>
+ <string name="reader_label_follow_count">%,d seguidores</string>
+ <string name="reader_short_like_count_none">Me gusta</string>
+ <string name="reader_menu_tags">Editar etiquetas y blogs</string>
+ <string name="reader_title_post_detail">Entrada del lector</string>
+ <string name="local_draft_explainer">Esta entrada tiene un borrador local que no ha sido publicado</string>
+ <string name="local_changes_explainer">Esta entrada tiene cambios en local que no han sido publicados</string>
+ <string name="notifications_push_summary">Opciones para las notificaciones que aparecen en tu dispositivo.</string>
+ <string name="notifications_email_summary">Opciones para las notificaciones que se envían al correo electrónico ligado a tu cuenta.</string>
+ <string name="notifications_tab_summary">Opciones para las notificaciones que aparecen en la pestaña de notificaciones.</string>
+ <string name="notifications_disabled">Las notificaciones de la App han sido desativadas. Pulsa aquí para activarlas en Opciones.</string>
+ <string name="notification_types">Tipos de notificaciones</string>
+ <string name="error_loading_notifications">No se han podido cargar los ajustes de avisos</string>
+ <string name="replies_to_your_comments">Respuestas a tus comentarios</string>
+ <string name="comment_likes">Me gusta al comentario</string>
+ <string name="app_notifications">Avisos de la aplicación</string>
+ <string name="notifications_tab">Pestaña de avisos</string>
+ <string name="email">Correo electrónico</string>
+ <string name="notifications_comments_other_blogs">Comentarios en otros sitios</string>
+ <string name="notifications_wpcom_updates">Actualizaciones de WordPress.com</string>
+ <string name="notifications_other">Otras</string>
+ <string name="notifications_account_emails">Correo de WordPress.com</string>
+ <string name="notifications_account_emails_summary">Siempre enviamos correos electrónicos importantes relativos a tu cuenta, pero también obtendrás extras útiles.</string>
+ <string name="notifications_sights_and_sounds">Vistas y sonidos</string>
+ <string name="your_sites">Tus Sitios</string>
+ <string name="stats_insights_latest_post_trend">Ha pasado %1$s desde que se publicó %2$s. Aquí tienes el rendimiento de la entrada desde entonces…</string>
+ <string name="stats_insights_latest_post_summary">Sumario de la Última Entrada</string>
+ <string name="button_revert">Deshacer</string>
+ <string name="days_ago">Hace %d días</string>
+ <string name="yesterday">Ayer</string>
+ <string name="connectionbar_no_connection">Sin conexión</string>
+ <string name="page_trashed">Página enviada a la papelera</string>
+ <string name="post_deleted">Entrada borrada</string>
+ <string name="post_trashed">Entrada enviada a la papelera</string>
+ <string name="stats_no_activity_this_period">Sin actividad en este periodo</string>
+ <string name="trashed">En la papelera</string>
+ <string name="button_back">Atrás</string>
+ <string name="page_deleted">Página borrada</string>
+ <string name="button_stats">Estadísticas</string>
+ <string name="button_trash">Papelera</string>
+ <string name="button_preview">Vista previa</string>
+ <string name="button_view">Ver</string>
+ <string name="button_edit">Editar</string>
+ <string name="button_publish">Publicar</string>
+ <string name="my_site_no_sites_view_subtitle">¿Quieres añadir uno?</string>
+ <string name="my_site_no_sites_view_title">Aún no tienes ningún sitio WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustración</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">No tienes autorización para acceder a este blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">No se pudo encontrar dicho blog</string>
+ <string name="undo">Deshacer</string>
+ <string name="tabbar_accessibility_label_my_site">Mi sitio</string>
+ <string name="tabbar_accessibility_label_me">Yo</string>
+ <string name="passcodelock_prompt_message">Escribe tu PIN</string>
+ <string name="editor_toast_changes_saved">Cambios guardados</string>
+ <string name="push_auth_expired">La solicitud ha expirado. Inicia sesión en WordPress.com para volver a intentarlo.</string>
+ <string name="ignore">Ignorar</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de vistas</string>
+ <string name="stats_insights_best_ever">Mejores Visualizaciones</string>
+ <string name="stats_insights_most_popular_hour">Hora más popular</string>
+ <string name="stats_insights_most_popular_day">Día más popular</string>
+ <string name="stats_insights_popular">Hora y día más populares</string>
+ <string name="stats_insights_today">Estadísticas de hoy</string>
+ <string name="stats_insights_all_time">Entradas, vistas y visitantes de todos los tiempos</string>
+ <string name="stats_insights">Detalles</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Para ver tus estadísticas, inicia sesión con la cuenta de WordPress.com que has usado para efectuar la conexión con Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">¿Buscas tus otras estadísticas recientes? Las hemos trasladado a la página "Detalles".</string>
+ <string name="me_disconnect_from_wordpress_com">Desconectar de WordPress.com</string>
+ <string name="me_btn_login_logout">Iniciar/Cerrar sesión</string>
+ <string name="me_connect_to_wordpress_com">Conectar a WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">"%s" no se ocultó porque es el sitio actual</string>
+ <string name="me_btn_support">Ayuda y Soporte</string>
+ <string name="account_settings">Preferencias de la Cuenta</string>
+ <string name="site_picker_create_dotcom">Crear sitio en WordPress.com</string>
+ <string name="site_picker_add_site">Añadir sitio</string>
+ <string name="site_picker_add_self_hosted">Añadir sitio autoalojado</string>
+ <string name="site_picker_edit_visibility">Mostrar/Ocultar sitios</string>
+ <string name="my_site_btn_view_site">Ver sitio</string>
+ <string name="site_picker_title">Elegir sitio</string>
+ <string name="my_site_btn_switch_site">Cambiar sitio</string>
+ <string name="my_site_btn_view_admin">Ver Administrador</string>
+ <string name="my_site_btn_blog_posts">Entradas del blog</string>
+ <string name="my_site_btn_site_settings">Preferencias</string>
+ <string name="my_site_header_look_and_feel">Aspecto</string>
+ <string name="my_site_header_publish">Publicar</string>
+ <string name="my_site_header_configuration">Configuración</string>
+ <string name="reader_label_new_posts_subtitle">Toca para mostrarlos</string>
+ <string name="notifications_account_required">Inicia sesión en WordPress.com para recibir notificaciones</string>
+ <string name="stats_unknown_author">Autor Desconocido</string>
+ <string name="image_added">Imagen añadida</string>
+ <string name="signout">Desconectar</string>
+ <string name="deselect_all">Anular todas las selecciones</string>
+ <string name="show">Mostrar</string>
+ <string name="hide">Ocultar</string>
+ <string name="select_all">Seleccionar todo</string>
+ <string name="sign_out_wpcom_confirm">Si desconectas tu cuenta, se eliminarán de este dispositivo todos los datos de WordPress.com de @%s, incluidos los borradores y los cambios locales.</string>
+ <string name="select_from_new_picker">Selección múltiple con el nuevo selector</string>
+ <string name="stats_generic_error">No se pudieron cargar las estadísticas solicitadas.</string>
+ <string name="no_device_videos">No hay vídeos</string>
+ <string name="no_blog_images">No hay imágenes</string>
+ <string name="no_blog_videos">No hay vídeos</string>
+ <string name="no_device_images">No hay imágenes</string>
+ <string name="error_loading_blog_images">No se pudieron traer las imágenes</string>
+ <string name="error_loading_blog_videos">No se pudieron traer los vídeos</string>
+ <string name="error_loading_images">Error cargando imágenes</string>
+ <string name="error_loading_videos">Error cargando vídeos</string>
+ <string name="loading_blog_images">Trayendo imágenes</string>
+ <string name="loading_blog_videos">Trayendo vídeos</string>
+ <string name="no_media_sources">No se pudieron traer los archivos multimedia</string>
+ <string name="loading_videos">Cargando vídeos</string>
+ <string name="loading_images">Cargando imágenes</string>
+ <string name="no_media">No hay archivos multimedia</string>
+ <string name="device">Dispositivo</string>
+ <string name="language">Idioma</string>
+ <string name="media_picker_title">Selecciona medio</string>
+ <string name="take_photo">Sacar una foto</string>
+ <string name="take_video">Grabar un vídeo</string>
+ <string name="media_details_label_file_name">Nombre del archivo:</string>
+ <string name="media_details_label_file_type">Tipo de archivo:</string>
+ <string name="error_publish_no_network">No se puede publicar mientras no haya conexión. Guardado como borrador.</string>
+ <string name="editor_toast_invalid_path">Ruta de archivo no válida</string>
+ <string name="verification_code">Código de verificación</string>
+ <string name="invalid_verification_code">Código de verificación no válido</string>
+ <string name="verify">Verificar</string>
+ <string name="two_step_footer_label">Introduce el código de tu app de autentificación.</string>
+ <string name="two_step_footer_button">Enviar código vía mensaje de texto</string>
+ <string name="two_step_sms_sent">Revisa tus mensajes de texto para el código de verificación.</string>
+ <string name="sign_in_jetpack">Inicia sesión en tu cuenta de WordPress.com para conectarte con Jetpack.</string>
+ <string name="auth_required">Inicia sesión de nuevo para continuar.</string>
+ <string name="add_to_post">Añadir a la Entrada</string>
+ <string name="tab_title_device_images">Imágenes del Dispositivo</string>
+ <string name="tab_title_device_videos">Vídeos del Dispositivo</string>
+ <string name="tab_title_site_images">Imágenes del Sitio</string>
+ <string name="tab_title_site_videos">Vídeos del Sitio</string>
+ <string name="reader_empty_posts_request_failed">No fue posible cargar las entradas</string>
+ <string name="publisher">Autor:</string>
+ <string name="error_notification_open">No se pudo abrir la notificación</string>
+ <string name="stats_followers_total_email_paged">Mostrando del %1$d al %2$d de %3$s suscriptores por correo electrónico</string>
+ <string name="stats_followers_total_wpcom_paged">Mostrando del %1$d al %2$d de %3$s suscriptores de WordPress.com</string>
+ <string name="stats_empty_search_terms_desc">Aprende más sobre el tráfico que te traen las búsquedas viendo los términos que buscaron tus lectores para llegar a tu sitio.</string>
+ <string name="stats_empty_search_terms">No se han grabado los términos de búsqueda</string>
+ <string name="stats_entry_search_terms">Término de búsqueda</string>
+ <string name="stats_view_authors">Autores</string>
+ <string name="stats_view_search_terms">Términos de búsqueda</string>
+ <string name="comments_fetching">Recuperando comentarios…</string>
+ <string name="pages_fetching">Recuperando páginas…</string>
+ <string name="toast_err_post_uploading">No se puede abrir la entrada durante la carga</string>
+ <string name="posts_fetching">Recuperando entradas…</string>
+ <string name="media_fetching">Recuperando medios…</string>
+ <string name="stats_search_terms_unknown_search_terms">Términos de Búsqueda Desconocidos</string>
+ <string name="post_uploading">Cargando</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">Global</string>
+ <string name="stats_period">Periodo</string>
+ <string name="logs_copied_to_clipboard">Los informes de la aplicación se han copiado al portapapeles</string>
+ <string name="reader_label_new_posts">Nuevas entradas</string>
+ <string name="reader_empty_posts_in_blog">Este blog está vacío</string>
+ <string name="stats_average_per_day">Media diaria</string>
+ <string name="stats_recent_weeks">Últimas Semanas</string>
+ <string name="error_copy_to_clipboard">Ocurrió un error al copiar el texto en el portapapeles</string>
+ <string name="reader_page_recommended_blogs">Sitios que te podrían gustar</string>
+ <string name="stats_months_and_years">Meses y Años</string>
+ <string name="themes_fetching">Obteniendo temas…</string>
+ <string name="stats_for">Estadísticas del %s</string>
+ <string name="stats_view_all">Ver todo</string>
+ <string name="stats_view">Vista</string>
+ <string name="stats_followers_months">%1$d meses</string>
+ <string name="stats_followers_a_year">Un año</string>
+ <string name="stats_followers_years">%1$d años</string>
+ <string name="stats_followers_a_month">Un mes</string>
+ <string name="stats_followers_minutes">%1$d minutos</string>
+ <string name="stats_followers_an_hour_ago">hace una hora</string>
+ <string name="stats_followers_hours">%1$d horas</string>
+ <string name="stats_followers_a_day">Un día</string>
+ <string name="stats_followers_days">%1$d días</string>
+ <string name="stats_followers_a_minute_ago">hace un minuto</string>
+ <string name="stats_followers_seconds_ago">hace unos segundos</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Correo electrónico</string>
+ <string name="stats_comments_total_comments_followers">Total de entradas con comentarios de seguidores: %1$s</string>
+ <string name="stats_empty_followers_desc">Realiza un seguimiento del número total de seguidores y del tiempo que cada uno de ellos lleva siguiendo tu sitio.</string>
+ <string name="stats_empty_followers">Sin seguidores</string>
+ <string name="stats_empty_publicize_desc">Realiza un seguimiento de los seguidores provenientes de distintos servicios de redes sociales con ayuda de Publicize.</string>
+ <string name="stats_empty_publicize">No se han registrado seguidores de Publicize</string>
+ <string name="stats_empty_video">No se han reproducido vídeos</string>
+ <string name="stats_empty_video_desc">Si has subido vídeos con VideoPress, descubre cuántas veces se han visto.</string>
+ <string name="stats_empty_comments_desc">Si permites comentarios en tu sitio, realiza un seguimiento de los principales autores de comentarios y descubre, a partir de los 1000 comentarios más recientes, qué parte de tu contenido desata las conversaciones más animadas.</string>
+ <string name="stats_empty_tags_and_categories_desc">Obtén una visión general de los temas más populares de tu sitio a partir de las principales entradas publicadas la semana pasada.</string>
+ <string name="stats_empty_top_authors_desc">Realiza un seguimiento de las visitas que reciben las entradas de cada colaborador y haz zoom para descubrir cuál es el contenido más popular de cada autor.</string>
+ <string name="stats_empty_tags_and_categories">No se han visto páginas ni entradas etiquetadas</string>
+ <string name="stats_empty_clicks_desc">Si tu contenido incluye enlaces a otros sitios, verás cuáles son aquellos en los que los visitantes hacen clic de forma más habitual.</string>
+ <string name="stats_empty_referrers_desc">Obtén más información acerca de la visibilidad de tu sitio; para ello, echa un vistazo a los sitios web y los motores de búsqueda que envían la mayor parte del tráfico a tu sitio</string>
+ <string name="stats_empty_clicks_title">No se han registrado clics</string>
+ <string name="stats_empty_referrers_title">No se han registrado referencias</string>
+ <string name="stats_empty_top_posts_title">No se han visto entradas ni páginas</string>
+ <string name="stats_empty_top_posts_desc">Descubre qué parte de tu contenido se ha visto más y consulta cómo rinden las entradas y páginas individuales a lo largo del tiempo.</string>
+ <string name="stats_totals_followers">Desde</string>
+ <string name="stats_empty_geoviews">No se han registrado países</string>
+ <string name="stats_empty_geoviews_desc">Explora la lista para saber cuáles son los países y las regiones que generan más tráfico hasta tu sitio.</string>
+ <string name="stats_entry_video_plays">Vídeo</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_entry_publicize">Servicio</string>
+ <string name="stats_entry_followers">Seguidor</string>
+ <string name="stats_totals_publicize">Seguidores</string>
+ <string name="stats_entry_clicks_link">Enlace</string>
+ <string name="stats_view_videos">Vídeos</string>
+ <string name="stats_view_publicize">Difundir</string>
+ <string name="stats_view_followers">Seguidores</string>
+ <string name="stats_view_countries">Países</string>
+ <string name="stats_likes">Me gusta</string>
+ <string name="stats_pagination_label">Página %1$s de %2$s</string>
+ <string name="stats_timeframe_years">Años</string>
+ <string name="stats_views">Vistas</string>
+ <string name="stats_visitors">Visitantes</string>
+ <string name="stats_other_recent_stats_label">Otras Estadísticas Recientes</string>
+ <string name="stats_followers_total_email">Total de Seguidores por Correo Electrónico: %1$s</string>
+ <string name="stats_followers_total_wpcom">Total de Seguidores de WordPress.com: %1$s</string>
+ <string name="stats_comments_by_authors">Por Autores</string>
+ <string name="stats_comments_by_posts_and_pages">Por Entradas y Páginas</string>
+ <string name="stats_view_top_posts_and_pages">Entradas y Páginas</string>
+ <string name="ssl_certificate_details">Detalles</string>
+ <string name="delete_sure_post">Eliminar esta entrada</string>
+ <string name="delete_sure">Eliminar este borrador</string>
+ <string name="delete_sure_page">Eliminar esta página</string>
+ <string name="confirm_delete_multi_media">¿Quieres eliminar los elementos seleccionados?</string>
+ <string name="confirm_delete_media">¿Quieres eliminar el elemento seleccionado?</string>
+ <string name="cab_selected">%d seleccionados</string>
+ <string name="media_gallery_date_range">Mostrando elementos multimedia desde %1$s a %2$s</string>
+ <string name="sure_to_remove_account">¿Deseas eliminar este sitio?</string>
+ <string name="reader_empty_followed_blogs_title">Todavía no sigues a ningún sitio.</string>
+ <string name="reader_empty_posts_liked">No te ha gustado ninguna entrada.</string>
+ <string name="faq_button">Preguntas frecuentes</string>
+ <string name="nux_help_description">Visita el centro de ayuda para obtener respuestas a las preguntas habituales o visita los foros para hacer preguntas nuevas.</string>
+ <string name="agree_terms_of_service">Al crear una cuenta, aceptas los fascinantes %1$sTérminos de servicio%2$s.</string>
+ <string name="create_new_blog_wpcom">Crear un blog de WordPress.com</string>
+ <string name="new_blog_wpcom_created">Se ha creado un blog de WordPress.com.</string>
+ <string name="reader_empty_comments">Aún no hay comentarios</string>
+ <string name="reader_empty_posts_in_tag">No hay entradas con esta etiqueta</string>
+ <string name="reader_label_comment_count_multi">%,d comentarios</string>
+ <string name="reader_label_view_original">Ver artículo original</string>
+ <string name="reader_label_like">Me gusta</string>
+ <string name="reader_label_comment_count_single">Un comentario</string>
+ <string name="reader_label_comments_closed">Los comentarios están cerrados</string>
+ <string name="reader_label_comments_on">Comentarios en</string>
+ <string name="reader_title_photo_viewer">%1$d de %2$d</string>
+ <string name="error_publish_empty_post">No se puede publicar una entrada vacía.</string>
+ <string name="error_refresh_unauthorized_posts">No tienes permiso para ver o editar entradas.</string>
+ <string name="error_refresh_unauthorized_pages">No tienes permiso para ver o editar páginas.</string>
+ <string name="error_refresh_unauthorized_comments">No tienes permiso para ver o editar comentarios.</string>
+ <string name="older_month">Hace más de 1 mes</string>
+ <string name="older_two_days">Hace más de 2 días</string>
+ <string name="older_last_week">Hace más de 1 semana</string>
+ <string name="stats_no_blog">No se han podido cargar las estadísticas del blog solicitado</string>
+ <string name="select_a_blog">Elegir un sitio de WordPress</string>
+ <string name="sending_content">Cargando el contenido de %s</string>
+ <string name="uploading_total">Cargando %1$d de %2$d</string>
+ <string name="mnu_comment_liked">Me gustó</string>
+ <string name="comment">Comentario</string>
+ <string name="comment_trashed">El comentario se ha enviado a la papelera.</string>
+ <string name="posts_empty_list">Aún no se han publicado entradas. ¿Por qué no crear una?</string>
+ <string name="comment_reply_to_user">Responder a %s</string>
+ <string name="pages_empty_list">Sin páginas todavía. ¿Por qué no crear una?</string>
+ <string name="media_empty_list_custom_date">Sin multimedia durante este intervalo de tiempo.</string>
+ <string name="posting_post">Publicando "%s"</string>
+ <string name="signing_out">Cerrando sesión…</string>
+ <string name="browse_our_faq_button">Revisa nuestras Preguntas Más Frecuentes.</string>
+ <string name="reader_toast_err_generic">No es posible realizar esta acción</string>
+ <string name="reader_toast_err_block_blog">No es posible bloquear este blog</string>
+ <string name="reader_toast_blog_blocked">Las entradas de este blog no volverán a mostrarse</string>
+ <string name="reader_menu_block_blog">Bloquear este blog</string>
+ <string name="contact_us">Contacta con nosotros</string>
+ <string name="hs__conversation_detail_error">Describe el problema que estás teniendo</string>
+ <string name="hs__new_conversation_header">Chat de soporte</string>
+ <string name="hs__conversation_header">Chat de soporte</string>
+ <string name="hs__username_blank_error">Escribe un nombre válido</string>
+ <string name="hs__invalid_email_error">Escribe una dirección de correo electrónico válida</string>
+ <string name="add_location">Añadir localización</string>
+ <string name="current_location">Localización actual</string>
+ <string name="search_location">Búsqueda</string>
+ <string name="edit_location">Editar</string>
+ <string name="search_current_location">Localizar</string>
+ <string name="preference_send_usage_stats">Enviar estadísticas</string>
+ <string name="preference_send_usage_stats_summary">Enviar automáticamente estadísticas de uso para ayudarnos a mejorar WordPress para Android</string>
+ <string name="update_verb">Actualizar</string>
+ <string name="schedule_verb">Programación</string>
+ <string name="reader_title_blog_preview">Blog del lector</string>
+ <string name="reader_title_tag_preview">Etiqueta del lector</string>
+ <string name="reader_title_subs">Etiquetas y blogs</string>
+ <string name="reader_page_followed_tags">Etiquetas que se siguen</string>
+ <string name="reader_page_followed_blogs">Sitios que sigues</string>
+ <string name="reader_hint_add_tag_or_url">Introduce la URL o etiqueta que quieras seguir</string>
+ <string name="reader_label_followed_blog">Blog que se sigue</string>
+ <string name="reader_label_tag_preview">Entradas etiquetadas %s</string>
+ <string name="reader_toast_err_get_blog_info">No se puede mostrar este blog</string>
+ <string name="reader_toast_err_already_follow_blog">Ya estás siguiendo este blog</string>
+ <string name="reader_toast_err_follow_blog">No se puede seguir este blog</string>
+ <string name="reader_toast_err_unfollow_blog">No se puede dejar de seguir este blog</string>
+ <string name="reader_empty_recommended_blogs">Sin blogs recomendados</string>
+ <string name="saving">Guardando</string>
+ <string name="media_empty_list">Sin elementos multimedia</string>
+ <string name="ptr_tip_message">Consejo: Arrastra hacia abajo para recargar</string>
+ <string name="help">Ayuda</string>
+ <string name="forgot_password">¿Has olvidado la contraseña?</string>
+ <string name="forums">Foros</string>
+ <string name="help_center">Centro de Ayuda</string>
+ <string name="ssl_certificate_error">Certificado SSL no válido</string>
+ <string name="ssl_certificate_ask_trust">SI normalmente se conecta sin problemas a este sitio sin problemas, este error puede significar que alguien están intentando suplantar el sitio, por lo que no deberías continuar. ¿Quieres, de todas formas, confiar en el certificado?</string>
+ <string name="out_of_memory">Memoria del dispositivo agotada</string>
+ <string name="gallery_error">El elemento multimedia no ha podido ser recuperado</string>
+ <string name="blog_not_found">Ha ocurrido un error mientras se accedía a este blog</string>
+ <string name="theme_fetch_failed">Ocurrió un error al obtener los temas.</string>
+ <string name="theme_set_failed">No se pudo establecer el tema</string>
+ <string name="theme_auth_error_message">Asegúrate que tienes autorización para elegir temas</string>
+ <string name="comments_empty_list">No hay comentarios.</string>
+ <string name="mnu_comment_unspam">No es spam</string>
+ <string name="no_site_error">No se pudo conectar con el sitio WordPress</string>
+ <string name="adding_cat_failed">No se pudo añadir la categoría</string>
+ <string name="adding_cat_success">Categoría agregada correctamente.</string>
+ <string name="cat_name_required">El campo nombre de categoría es necesario</string>
+ <string name="category_automatically_renamed">El nombre de categoría %1$s no es válido. Fue renombrado a %2$s.</string>
+ <string name="sdcard_message">Se necesita una tarjeta SD montada para subir medios</string>
+ <string name="stats_empty_comments">Aún sin comentarios</string>
+ <string name="stats_bar_graph_empty">Estadísticas no disponibles.</string>
+ <string name="reply_failed">Respuesta fallida</string>
+ <string name="notifications_empty_list">Sin notificaciones</string>
+ <string name="error_delete_post">Ocurrió un error al eliminar el %s</string>
+ <string name="error_refresh_posts">Las entradas no pueden ser actualizadas en este momento</string>
+ <string name="error_refresh_pages">Las páginas no pueden ser actualizadas en este momento.</string>
+ <string name="error_refresh_comments">No se pudieron actualizar los comentarios</string>
+ <string name="error_refresh_stats">No se pudieron actualizar las estadísticas</string>
+ <string name="error_generic">Ocurrió un error</string>
+ <string name="error_moderate_comment">Ocurrió un error al moderar el comentario</string>
+ <string name="error_edit_comment">Ocurrió un error al editar el comentario</string>
+ <string name="error_upload">Ocurrió un error mientras se subía el %s</string>
+ <string name="error_load_comment">No se pudo cargar el comentario</string>
+ <string name="error_downloading_image">Error al descargar la imagen</string>
+ <string name="passcode_wrong_passcode">PIN erróneo</string>
+ <string name="invalid_email_message">Tu dirección de correo electrónico no es válida</string>
+ <string name="invalid_password_message">La contraseña debe contener al menos 4 caracteres</string>
+ <string name="invalid_username_too_short">Nombre de usuario debe ser superior a 4 caracteres</string>
+ <string name="invalid_username_too_long">Nombre de usuario debe ser inferior a 61 caracteres</string>
+ <string name="username_only_lowercase_letters_and_numbers">Nombre de usuario solo puede contener letras minúsculas (a-z) y números</string>
+ <string name="username_required">Ingresa un nombre de usuario</string>
+ <string name="username_not_allowed">Nombre de usuario no permitido</string>
+ <string name="username_must_be_at_least_four_characters">El nombre de usuario debe tener, al menos, 4 caracteres</string>
+ <string name="username_contains_invalid_characters">El nombre de usuario no puede incluir el caracter “_”</string>
+ <string name="username_must_include_letters">El nombre de usuario debe tener, al menos, 1 letra (a-z)</string>
+ <string name="email_invalid">Ingresa una dirección de correo electrónico válida</string>
+ <string name="email_not_allowed">Esa dirección de correo electrónico no está permitida</string>
+ <string name="username_exists">Ese nombre de usuario ya existe</string>
+ <string name="email_exists">Esa dirección de correo electrónico ya está siendo usada</string>
+ <string name="username_reserved_but_may_be_available">Ese nombre de usuario actualmente está reservado pero puede estar disponible en un par de días</string>
+ <string name="blog_name_required">Ingresa una dirección para tu sitio</string>
+ <string name="blog_name_not_allowed">Esa dirección de sitio no está disponible</string>
+ <string name="blog_name_must_be_at_least_four_characters">La dirección del sitio debe tener al menos 4 caracteres</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">La dirección del sitio debe ser más corta que 64 caracteres</string>
+ <string name="blog_name_contains_invalid_characters">La dirección del sitio no puede tener el caracter “_”</string>
+ <string name="blog_name_cant_be_used">No puedes usar esa dirección de sitio</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">La dirección solo puede contener letras minúsculas (a-z) y números.</string>
+ <string name="blog_name_exists">Ese sitio ya existe</string>
+ <string name="blog_name_reserved">Ese sitio está reservado</string>
+ <string name="blog_name_reserved_but_may_be_available">Este sitio está reservado actualmente pero es posible que quede libre en un par de días.</string>
+ <string name="username_or_password_incorrect">El usuario o contraseña que ingresaste no son correctos.</string>
+ <string name="nux_cannot_log_in">No se pudo acceder</string>
+ <string name="no_network_message">No hay conexiones de red disponible</string>
+ <string name="could_not_remove_account">No pudimos eliminar el sitio</string>
+ <string name="wait_until_upload_completes">Espera mientras se complete la carga</string>
+ <string name="no_account">No entramos cuentas WordPress, añade una cuenta e intenta nuevamente</string>
+ <string name="error_refresh_notifications">No se pudieron actualizar las notificaciones</string>
+ <string name="invalid_url_message">Comprueba que la URL de blog ingresada es válida</string>
+ <string name="xmlrpc_error">No se pudo conectar. Ingresa la ruta completa a xmlrpc.php en tu sitio e intenta de nuevo.</string>
+ <string name="select_categories">Seleccionar categorías</string>
+ <string name="account_details">Detalles de la cuenta</string>
+ <string name="edit_post">Editar entrada</string>
+ <string name="add_comment">Añadir comentario</string>
+ <string name="connection_error">Error de conexión</string>
+ <string name="cancel_edit">Cancelar edición</string>
+ <string name="scaled_image_error">Introduce un valor válido de ancho a escala</string>
+ <string name="post_not_found">Ocurrió un error al cargar la entrada. Actualiza tus entradas e intenta nuevamente.</string>
+ <string name="learn_more">Aprender más</string>
+ <string name="media_gallery_settings_title">Ajustes de galería</string>
+ <string name="media_gallery_image_order">Orden de imágenes</string>
+ <string name="media_gallery_num_columns">Número de columnas</string>
+ <string name="media_gallery_type_thumbnail_grid">Malla (red) de miniaturas</string>
+ <string name="media_gallery_edit">Editar galería</string>
+ <string name="media_error_no_permission">No tienes permiso para ver la librería multimedia</string>
+ <string name="cannot_delete_multi_media_items">Algunos elementos multimedia no pudieron ser borrados. Prueba más tarde</string>
+ <string name="themes_live_preview">Vista previa</string>
+ <string name="theme_current_theme">Tema actual</string>
+ <string name="theme_premium_theme">Tema premium</string>
+ <string name="link_enter_url_text">Texto del enlace (opcional)</string>
+ <string name="create_a_link">Crear un enlace</string>
+ <string name="page_settings">Ajustes de página</string>
+ <string name="local_draft">Borrador local</string>
+ <string name="upload_failed">Falló la subida</string>
+ <string name="horizontal_alignment">Alineación horizontal</string>
+ <string name="file_not_found">No ha sido posible encontrar el archivo multimedia para cargar. ¿Se ha borrado o cambiado de ubicación?</string>
+ <string name="post_settings">Ajustes de entrada</string>
+ <string name="delete_post">Borrar entrada</string>
+ <string name="delete_page">Borrar página</string>
+ <string name="comment_status_approved">Aprobado</string>
+ <string name="comment_status_unapproved">Pendiente</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">En la papelera</string>
+ <string name="edit_comment">Editar comentario</string>
+ <string name="mnu_comment_approve">Aprobar</string>
+ <string name="mnu_comment_unapprove">Rechazar</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Enviar a la papelera</string>
+ <string name="dlg_approving_comments">Aprobando</string>
+ <string name="dlg_unapproving_comments">Rechazando</string>
+ <string name="dlg_spamming_comments">Marcando como spam</string>
+ <string name="dlg_trashing_comments">Enviando a la papelera</string>
+ <string name="dlg_confirm_trash_comments">¿Enviar a la papelera?</string>
+ <string name="trash_yes">Enviar a la papelera</string>
+ <string name="trash_no">No enviar a la papelera</string>
+ <string name="trash">Papelera</string>
+ <string name="author_name">Nombre del autor</string>
+ <string name="author_email">Email del autor</string>
+ <string name="author_url">URL del autor</string>
+ <string name="hint_comment_content">Comentario</string>
+ <string name="saving_changes">Guardando cambios</string>
+ <string name="sure_to_cancel_edit_comment">¿Cancelar la edición de este comentario?</string>
+ <string name="content_required">Comentario obligatorio</string>
+ <string name="toast_comment_unedited">El comentario no ha cambiado</string>
+ <string name="remove_account">Eliminar sitio</string>
+ <string name="blog_removed_successfully">El sitio se ha eliminado correctamente</string>
+ <string name="delete_draft">Eliminar borrador</string>
+ <string name="preview_page">Vista previa de página</string>
+ <string name="preview_post">Vista previa de entrada</string>
+ <string name="comment_added">Comentado añadido con éxito</string>
+ <string name="post_not_published">No se ha publicado el estatus de la entrada</string>
+ <string name="page_not_published">No se ha publicado el estatus de la página</string>
+ <string name="view_in_browser">Ver en el navegador</string>
+ <string name="add_new_category">Añadir nueva categoría</string>
+ <string name="category_name">Nombre de la categoría</string>
+ <string name="category_slug">Slug de la categoría (opcional)</string>
+ <string name="category_desc">Descripción de la categoría (opcional)</string>
+ <string name="category_parent">Categoría padre (opcional):</string>
+ <string name="share_action_post">Nueva entrada</string>
+ <string name="share_action_media">Librería multimedia</string>
+ <string name="file_error_create">No se pudo crear un archivo temporal para subir el archivo multimedia. Asegúrate que haya suficiente espacio libre en tu dispositivo.</string>
+ <string name="location_not_found">Localización desconocida</string>
+ <string name="open_source_licenses">Licencias Open source</string>
+ <string name="pending_review">Pendiente de revisión</string>
+ <string name="http_credentials">Credenciales HTTP (opcional)</string>
+ <string name="http_authorization_required">Se necesita autorización</string>
+ <string name="post_format">Tipo de entrada</string>
+ <string name="new_post">Nueva entrada</string>
+ <string name="new_media">Nuevo elemento multimedia</string>
+ <string name="view_site">Ver sitio</string>
+ <string name="privacy_policy">Política de privacidad</string>
+ <string name="local_changes">Cambios locales</string>
+ <string name="image_settings">Ajustes de imagen</string>
+ <string name="add_account_blog_url">Dirección del blog</string>
+ <string name="wordpress_blog">Blog de WordPress</string>
+ <string name="error_blog_hidden">Este blog está oculto y no se puede cargar. Actívalo de nuevo en ajustes y prueba de nuevo.</string>
+ <string name="fatal_db_error">Hubo un error al crear la base de datos de la app. Por favor, intenta reinstalar la app. </string>
+ <string name="jetpack_message_not_admin">Se necesita el plugin Jetpack para tener estadísticas. Contacta con el administrador del sitio para activarlo.</string>
+ <string name="reader_title_applog">Registro de la aplicación</string>
+ <string name="reader_share_link">Compartir enlace</string>
+ <string name="reader_toast_err_add_tag">No se pudo añadir esta etiqueta</string>
+ <string name="reader_toast_err_remove_tag">No se pudo quitar esta etiqueta</string>
+ <string name="required_field">Campo obligatorio</string>
+ <string name="email_hint">Correo electrónico</string>
+ <string name="site_address">La dirección (URL) de tu sitio autoalojado</string>
+ <string name="email_cant_be_used_to_signup">No puedes usar esta dirección de correo electrónico para registrarte, ya que bloquea algunos de nuestros correos. Utiliza otro proveedor de correo.</string>
+ <string name="email_reserved">Esta dirección de correo electrónico ya se ha utilizado. Comprueba si tienes en tu bandeja de entrada un correo de activación. Si no activas ahora el correo, puedes intentarlo dentro de unos días.</string>
+ <string name="blog_name_must_include_letters">La dirección de los sitios debe tener, al menos, 1 letra (a-z)</string>
+ <string name="blog_title_invalid">Título de sitio no válido</string>
+ <string name="blog_name_invalid">Dirección de sitio no válida</string>
+ <string name="deleting_page">Borrando página</string>
+ <string name="deleting_post">Borrando entrada</string>
+ <string name="share_url_post">Compartir entrada</string>
+ <string name="share_url_page">Compartir página</string>
+ <string name="share_link">Compartir enlace</string>
+ <string name="creating_your_account">Creando tu cuenta</string>
+ <string name="creating_your_site">Creando tu sitio</string>
+ <string name="reader_empty_posts_in_tag_updating">Recuperando entradas...</string>
+ <string name="error_refresh_media">Algo ha ido mal mientras actualizábamos la biblioteca multimedia. Inténtalo más tarde.</string>
+ <string name="reader_likes_you_and_multi">A ti, y a %,d personas más les gusta esto</string>
+ <string name="reader_likes_multi">A %,d personas les gusta esto</string>
+ <string name="reader_toast_err_get_comment">No ha sido posible cargar este comentario</string>
+ <string name="reader_label_reply">Responder</string>
+ <string name="video">Vídeo</string>
+ <string name="download">Descargando elemento multimedia</string>
+ <string name="comment_spammed">Comentado marcado como spam</string>
+ <string name="cant_share_no_visible_blog">No puedes compartir en WordPress sin un blog visible</string>
+ <string name="reader_empty_followed_blogs_description">No te preocupes, ¡solo tienes que pulsar el icono para empezar a explorar!</string>
+ <string name="select_date">Elegir fecha</string>
+ <string name="pick_photo">Elige una foto</string>
+ <string name="account_two_step_auth_enabled">Esta cuenta tiene activado el proceso de autentificación en dos pasos. Visita los ajustes de seguridad de WordPress.com y genera una contraseña específica para la aplicación.</string>
+ <string name="pick_video">Elige un vídeo</string>
+ <string name="reader_toast_err_get_post">No fue posible cargar esta entrada</string>
+ <string name="validating_user_data">Datos de validación del usuario</string>
+ <string name="validating_site_data">Validando datos del sitio</string>
+ <string name="select_time">Elige hora</string>
+ <string name="reader_likes_you_and_one">A ti y a otra persona les gusta esto.</string>
+ <string name="nux_tap_continue">Continuar</string>
+ <string name="nux_welcome_create_account">Crear cuenta</string>
+ <string name="signing_in">Iniciando sesión...</string>
+ <string name="nux_oops_not_selfhosted_blog">Regístrate en WordPress.com</string>
+ <string name="password_invalid">Necesitas una contraseña más segura. Asegúrate de utilizar 7 ó más caracteres, mezclar letras mayúsculas y minúsculas, número y caracteres especiales.</string>
+ <string name="nux_add_selfhosted_blog">Añade un sitio auto hospedado</string>
+ <string name="media_add_popup_title">Añadir a biblioteca multimedia</string>
+ <string name="media_add_new_media_gallery">Crear galería</string>
+ <string name="empty_list_default">Esta lista esta vacía</string>
+ <string name="select_from_media_library">Seleccionar de la biblioteca multimedia</string>
+ <string name="jetpack_message">Necesitas el pluging Jetpack para tener estadísticas estadísticas. ¿Quieres instalar Jetpack?</string>
+ <string name="jetpack_not_found">No se ha encontrado el plugin Jetpack</string>
+ <string name="reader_untitled_post">Sin título</string>
+ <string name="reader_share_subject">Compartido desde %s</string>
+ <string name="reader_btn_share">Compartir</string>
+ <string name="reader_btn_follow">Seguir</string>
+ <string name="reader_btn_unfollow">Siguiendo</string>
+ <string name="reader_label_removed_tag">Eliminado %s</string>
+ <string name="reader_likes_one">A una persona le gusta esto</string>
+ <string name="reader_likes_only_you">Te gusta esto</string>
+ <string name="reader_toast_err_comment_failed">No se pudo publicar tu comentario </string>
+ <string name="reader_toast_err_tag_exists">Ya estás siguiendo esta etiqueta</string>
+ <string name="reader_empty_followed_tags">No sigues ninguna etiqueta</string>
+ <string name="create_account_wpcom">Crear una cuenta de WordPress.com</string>
+ <string name="connecting_wpcom">Conectando a WordPress.com</string>
+ <string name="username_invalid">Nombre de usuario no válido</string>
+ <string name="limit_reached">Límite alcanzado. Puedes probar en 1 minuto. Probar de nuevo antes solamente incrementará el tiempo de espera hasta que la prohibición sea suprimida. Si crees que es un error, contacta con soporte. </string>
+ <string name="reader_hint_comment_on_comment">Contestar al comentario...</string>
+ <string name="reader_label_added_tag">Agregado %s</string>
+ <string name="reader_toast_err_tag_invalid">Esa no es una etiqueta válida</string>
+ <string name="reader_toast_err_share_intent">No se pudo compartir</string>
+ <string name="reader_toast_err_view_image">No se pudo ver la imágen</string>
+ <string name="reader_toast_err_url_intent">No se pudo abrir %s</string>
+ <string name="nux_tutorial_get_started_title">¡Comencemos!</string>
+ <string name="themes">Temas</string>
+ <string name="all">Todas</string>
+ <string name="images">Imágenes</string>
+ <string name="unattached">Sin adjuntar</string>
+ <string name="media_gallery_image_order_random">Aleatorio</string>
+ <string name="media_gallery_image_order_reverse">Inverso</string>
+ <string name="media_gallery_type">Tipo</string>
+ <string name="media_gallery_type_squares">Cuadrados</string>
+ <string name="media_gallery_type_tiled">Mosaico</string>
+ <string name="media_gallery_type_circles">Círculos</string>
+ <string name="media_gallery_type_slideshow">Pase de diapositivas </string>
+ <string name="media_edit_title_text">Título</string>
+ <string name="media_edit_caption_text">Leyenda</string>
+ <string name="media_edit_description_text">Descripción</string>
+ <string name="media_edit_title_hint">Escribir un título aquí</string>
+ <string name="media_edit_caption_hint">Escribir una leyenda aquí</string>
+ <string name="media_edit_description_hint">Escribe una descripción aquí</string>
+ <string name="media_edit_success">Actualizado</string>
+ <string name="media_edit_failure">No se pudo actualizar</string>
+ <string name="themes_details_label">Detalles</string>
+ <string name="themes_features_label">Características</string>
+ <string name="theme_activate_button">Activar</string>
+ <string name="theme_activating_button">Activando</string>
+ <string name="theme_set_success">¡Tema aplicado con exito!</string>
+ <string name="theme_auth_error_title">Error al recibir temas</string>
+ <string name="post_excerpt">Extracto</string>
+ <string name="share_action_title">Añadir a...</string>
+ <string name="share_action">Compartir</string>
+ <string name="stats">Estadísticas</string>
+ <string name="stats_view_clicks">Clics</string>
+ <string name="stats_view_tags_and_categories">Etiquetas y categorías</string>
+ <string name="stats_view_referrers">Referentes</string>
+ <string name="stats_timeframe_today">Hoy</string>
+ <string name="stats_timeframe_yesterday">Ayer</string>
+ <string name="stats_timeframe_days">Días</string>
+ <string name="stats_timeframe_weeks">Semanas</string>
+ <string name="stats_timeframe_months">Meses</string>
+ <string name="stats_entry_country">País</string>
+ <string name="stats_entry_posts_and_pages">Título</string>
+ <string name="stats_entry_tags_and_categories">Tema</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Referencias</string>
+ <string name="stats_totals_views">Vistas</string>
+ <string name="stats_totals_clicks">Clics</string>
+ <string name="stats_totals_plays">Juegos</string>
+ <string name="passcode_manage">Administrar el PIN de bloqueo</string>
+ <string name="passcode_enter_passcode">Introduce tu PIN</string>
+ <string name="passcode_re_enter_passcode">Vuelve a introducir el PIN</string>
+ <string name="passcode_change_passcode">Cambiar PIN</string>
+ <string name="passcode_set">Introducir PIN</string>
+ <string name="passcode_preference_title">PIN bloqueado</string>
+ <string name="passcode_turn_off">Desbloquear PIN</string>
+ <string name="passcode_turn_on">Bloquear PIN</string>
+ <string name="custom_date">Fecha Personalizada</string>
+ <string name="media_add_popup_capture_photo">Tomar Foto</string>
+ <string name="media_add_popup_capture_video">Capturar Vídeo</string>
+ <string name="stats_view_visitors_and_views">Visitantes y Visualizaciones</string>
+ <string name="passcode_enter_old_passcode">Introduce tu PIN anterior</string>
+ <string name="upload">Subir</string>
+ <string name="sign_in">Acceder</string>
+ <string name="notifications">Notificaciones</string>
+ <string name="note_reply_successful">Respuesta publicada</string>
+ <string name="new_notifications">%d nuevas notificaciones</string>
+ <string name="more_notifications">y %d más.</string>
+ <string name="loading">Cargando...</string>
+ <string name="httpuser">Usuario HTTP</string>
+ <string name="httppassword">Contraseña HTTP</string>
+ <string name="error_media_upload">Se ha producido un error al cargar los archivos</string>
+ <string name="post_content">Contenido (pulsa para añadir texto y multimedia)</string>
+ <string name="publish_date">Publicar</string>
+ <string name="content_description_add_media">Añadir media</string>
+ <string name="incorrect_credentials">Nombre de usuario o contraseña incorrecta</string>
+ <string name="password">Contraseña</string>
+ <string name="username">Nombre de usuario</string>
+ <string name="reader">Lector</string>
+ <string name="featured">Usar como imagen destacada</string>
+ <string name="featured_in_post">Incluír imagen en el contenido del mensaje</string>
+ <string name="no_network_title">No hay red disponible</string>
+ <string name="pages">Páginas</string>
+ <string name="caption">Leyenda (opcional)</string>
+ <string name="width">Ancho</string>
+ <string name="posts">Entradas</string>
+ <string name="anonymous">Anónimo</string>
+ <string name="page">Página</string>
+ <string name="post">Entrada</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">Nombre de Usuario del blog</string>
+ <string name="upload_scaled_image">Subir y enlazar a la imagen escalada</string>
+ <string name="scaled_image">Ancho de la imagen en escala</string>
+ <string name="scheduled">Programado</string>
+ <string name="link_enter_url">URL</string>
+ <string name="version">Versión</string>
+ <string name="app_title">WordPress para Android</string>
+ <string name="tos">Términos de Servicio</string>
+ <string name="max_thumbnail_px_width">Ancho de Imagen por Defecto</string>
+ <string name="image_alignment">Alineamiento</string>
+ <string name="refresh">Refrescar</string>
+ <string name="untitled">Sin Título</string>
+ <string name="post_id">Entrada</string>
+ <string name="page_id">Página</string>
+ <string name="post_password">Contraseña (opcional)</string>
+ <string name="immediately">Inmediatamente</string>
+ <string name="quickpress_add_alert_title">Configura el nombre del acceso directo</string>
+ <string name="today">Hoy</string>
+ <string name="settings">Ajustes</string>
+ <string name="share_url">Compartir URL</string>
+ <string name="quickpress_window_title">Selecciona un blog para el acceso directo a la Publicación Rápida</string>
+ <string name="quickpress_add_error">El nombre del acceso directo no puede ser vacío</string>
+ <string name="publish_post">Publicar</string>
+ <string name="draft">Borrador</string>
+ <string name="post_private">Privado</string>
+ <string name="upload_full_size_image">Subir y enlazar a la imagen completa</string>
+ <string name="title">Título</string>
+ <string name="tags_separate_with_commas">Etiquetas (separa las etiquetas con comas)</string>
+ <string name="categories">Categorías</string>
+ <string name="notification_blink">Notificación luminosa</string>
+ <string name="notification_vibrate">Vibrar</string>
+ <string name="notification_sound">Sonido de Notificación</string>
+ <string name="status">Estado</string>
+ <string name="location">Ubicación</string>
+ <string name="sdcard_title">Requiere tarjeta SD</string>
+ <string name="select_video">Selecciona un video de la galería</string>
+ <string name="media">Multimedia</string>
+ <string name="delete">Eliminar</string>
+ <string name="none">Ninguno</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Selecciona una foto de la galería</string>
+ <string name="error">Error</string>
+ <string name="cancel">Cancelar</string>
+ <string name="save">Guardar</string>
+ <string name="add">Añadir</string>
+ <string name="category_refresh_error">Error de actualización de categorías</string>
+ <string name="preview">Vista previa</string>
+ <string name="on">en</string>
+ <string name="reply">Responder</string>
+ <string name="notification_settings">Ajustes de avisos</string>
+ <string name="yes">Sí</string>
+ <string name="no">No</string>
+</resources>
diff --git a/WordPress/src/main/res/values-es-rVE/strings.xml b/WordPress/src/main/res/values-es-rVE/strings.xml
new file mode 100644
index 000000000..a948cb0fe
--- /dev/null
+++ b/WordPress/src/main/res/values-es-rVE/strings.xml
@@ -0,0 +1,1040 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="gravatar_camera_and_media_permission_required">Permisos necesarios para seleccionar o capturar una imagen</string>
+ <string name="error_updating_gravatar">Error al subir tu Gravatar</string>
+ <string name="error_refreshing_gravatar">Error al recargar tu Gravatar</string>
+ <string name="error_locating_image">Error al localizar la imagen recortada</string>
+ <string name="error_cropping_image">Error al recortar la imagen</string>
+ <string name="gravatar_tip">¡Nuevo! ¡Haz clic en tu Gravatar para cambiarlo!</string>
+ <string name="checking_email">Comprobando correo electrónico</string>
+ <string name="not_on_wordpress_com">¿No estás en WordPress.com?</string>
+ <string name="launch_your_email_app">Abre tu app de correo electrónico</string>
+ <string name="check_your_email">Comprueba tu correo electrónico</string>
+ <string name="magic_link_unavailable_error_message">Actualmente no disponible. Por favor, introduce tu contraseña</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Haz que se envíe un enlace a tu correo electrónico inmediatamente</string>
+ <string name="logging_in">Accediendo</string>
+ <string name="enter_your_password_instead">Introduce tu contraseña</string>
+ <string name="web_address_dialog_hint">Cuando comentes se hará público</string>
+ <string name="jetpack_not_connected_message">El plugin Jetpack se ha instalado, pero no se ha conectado con WordPress.com. ¿Quieres conectar Jetpack?</string>
+ <string name="username_email">Correo electrónico o nombre de usuario</string>
+ <string name="jetpack_not_connected">Plugin Jetpack no conectado</string>
+ <string name="new_editor_reflection_error">Editor visual no compatible con tu dispositivo. Se ha\ndesactivado automáticamente</string>
+ <string name="stats_insights_latest_post_no_title">(sin título)</string>
+ <string name="capture_or_pick_photo">Captura o elige imagen</string>
+ <string name="plans_post_purchase_button_themes">Ver Temas</string>
+ <string name="plans_post_purchase_text_themes">Ahora tienes acceso ilimitado a temas Premium. Haz una vista previa de cualquier tema en tu sitio para comenzar</string>
+ <string name="plans_post_purchase_title_themes">Encuentra un tema premium perfecto</string>
+ <string name="plans_post_purchase_button_video">Comenzar una entrada nueva</string>
+ <string name="plans_post_purchase_text_video">Puedes subir y alojar vídeos en tu sitio con VideoPress y tu almacenamiento extendido de medios.</string>
+ <string name="plans_post_purchase_title_video">Dale vida a tus entradas con un vídeo.</string>
+ <string name="plans_post_purchase_button_customize">Personalizar mi sitio</string>
+ <string name="plans_post_purchase_text_customize">Ahora tienes acceso a fuentes personalizadas, colores personalizados, y permisos de edición de CSS personalizado.</string>
+ <string name="plans_post_purchase_title_customize">Personalizar fuentes y colores</string>
+ <string name="plans_post_purchase_text_intro">¡Tu sitio está dando saltos de la emoción! Ahora explora las nuevas características de tu sitio y elige por dónde quieres empezar. </string>
+ <string name="plans_post_purchase_title_intro">Todo tuyo. ¡Adelante!</string>
+ <string name="export_your_content_message">Tus entradas, páginas y ajustes te serán enviadas por correo electrónico a %s.</string>
+ <string name="plans">Planes</string>
+ <string name="plan">Plan</string>
+ <string name="plans_loading_error">No se pueden cargar los planes</string>
+ <string name="export_your_content">Exportar tu contenido</string>
+ <string name="exporting_content_progress">Exportando contenido...</string>
+ <string name="export_email_sent">¡Correo electrónico de exportación enviado!</string>
+ <string name="premium_upgrades_message">Tienes mejoras premium en tu sitio. Por favor, cancela tus mejoras antes de eliminar tu sitio. </string>
+ <string name="show_purchases">Mostrar compras</string>
+ <string name="checking_purchases">Comprobando compras</string>
+ <string name="premium_upgrades_title">Mejoras premium</string>
+ <string name="purchases_request_error">Algo fue mal. No se pudo realizar la compra.</string>
+ <string name="delete_site_progress">Borrando sitio...</string>
+ <string name="delete_site_summary">Esta acción no se puede deshacer. Eliminar tu sitio eliminará todo el contenido, contribuidores y dominios de tu sitio.</string>
+ <string name="delete_site_hint">Borrar tu sitio</string>
+ <string name="export_site_hint">Exportar tu sitio a un archivo XML</string>
+ <string name="are_you_sure">¿Estás seguro?</string>
+ <string name="export_site_summary">Si estás seguro, por favor asegúrate de invertir tiempo en exportar tu contenido ahora. No podrá ser recuperado en el futuro.</string>
+ <string name="keep_your_content">Manten tu contenido</string>
+ <string name="domain_removal_hint">Una vez que hayas eliminado tu sitio, los dominios no funcionarán.</string>
+ <string name="domain_removal_summary">¡Ten cuidado! La eliminación de su sitio también eliminará su dominio(s) enumerado(s) a continuación.</string>
+ <string name="domain_removal">Eliminación de Dominio</string>
+ <string name="primary_domain">Dominio principal</string>
+ <string name="error_deleting_site_summary">Hubo un error eliminando tu sitio. Por favor, contacta con el soporte para más asistencia</string>
+ <string name="error_deleting_site">Error borrando sitio</string>
+ <string name="confirm_delete_site_prompt">Por favor, escribe en %1$s en el campo arriba para confirmar. Tu sitio desaparecerá para siempre.</string>
+ <string name="site_settings_export_content_title">Exportar contenido</string>
+ <string name="contact_support">Contactar con el soporte</string>
+ <string name="confirm_delete_site">Confirmar borrado del sitio</string>
+ <string name="start_over_text">Si quieres un sitio, pero no quieres ninguna de las entradas y las páginas que tiene ahora, nuestro equipo de soporte puede borrar sus mensajes, páginas, archivos multimedia y tus comentarios.\n\nEsto mantendrá su sitio y la URL activos, pero tendrás un nuevo comienzo en la creación de contenidos. Sólo tienes que contactar con nosotros para limpiar tu contenido actual..</string>
+ <string name="let_us_help">Déjanos ayudarte</string>
+ <string name="site_settings_start_over_hint">Comienza tu sitio encima</string>
+ <string name="start_over">Comenzar de nuevo</string>
+ <string name="me_btn_app_settings">Ajustes de la app</string>
+ <string name="editor_remove_failed_uploads">Eliminar subidas fallidas</string>
+ <string name="editor_toast_failed_uploads">La subida de algunos archivos multimedia han fallado. No se puede guardar o publicar\n tu entrada tiene este estado. ¿Quieres eliminar todos los archivos multimedia fallidos?</string>
+ <string name="comments_empty_list_filtered_trashed">No hay comentarios en la papelera</string>
+ <string name="site_settings_advanced_header">Avanzado</string>
+ <string name="comments_empty_list_filtered_pending">No hay comentarios pendientes</string>
+ <string name="comments_empty_list_filtered_approved">No hay comentarios aprobados</string>
+ <string name="button_done">Hecho</string>
+ <string name="button_skip">Saltar</string>
+ <string name="site_timeout_error">No se pudo conectar con WordPress debido a un error de Timeout.</string>
+ <string name="xmlrpc_malformed_response_error">No se pudo conectar. La instalación de WordPress está respondiendo con un documento XML-RPC no válido.</string>
+ <string name="xmlrpc_missing_method_error">No se pudo conectar. Los métodos requeridos en el XML-RPC faltan en el servidor.</string>
+ <string name="post_format_video">Video</string>
+ <string name="theme_all">Todo</string>
+ <string name="theme_premium">Premium</string>
+ <string name="theme_free">Gratis</string>
+ <string name="post_format_status">Estado</string>
+ <string name="alignment_center">Centrado</string>
+ <string name="post_format_image">Imagen</string>
+ <string name="post_format_gallery">Galería</string>
+ <string name="post_format_standard">Estándar</string>
+ <string name="post_format_link">Enlace</string>
+ <string name="post_format_quote">Cita</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_aside">Minientrada</string>
+ <string name="notif_events">Información sobre cursos y eventos de WordPress.com (online y presenciales).</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Oportunidades para participar en investigaciones y encuestas en WordPress.</string>
+ <string name="notif_tips">Consejos para sacar el máximo partido a WordPress.com</string>
+ <string name="notif_community">Comunidad</string>
+ <string name="notif_suggestions">Sugerencias</string>
+ <string name="replies_to_my_comments">Respuestas a mis comentarios</string>
+ <string name="notif_research">Investigación</string>
+ <string name="username_mentions">Menciones del nombre de usuario</string>
+ <string name="site_achievements">Logros del sitio</string>
+ <string name="site_follows">Seguidores del sitio</string>
+ <string name="likes_on_my_posts">"Me gusta" en mis entradas</string>
+ <string name="likes_on_my_comments">"Me gusta" en mis comentarios</string>
+ <string name="comments_on_my_site">Comentarios en mi sitio</string>
+ <string name="site_settings_list_editor_summary_other">%d elementos</string>
+ <string name="site_settings_list_editor_summary_one">1 elemento</string>
+ <string name="approve_auto">Todos los usuarios</string>
+ <string name="approve_auto_if_previously_approved">Comentarios de usuarios conocidos</string>
+ <string name="approve_manual">Sin comentarios</string>
+ <string name="site_settings_paging_summary_other">%d comentarios por página</string>
+ <string name="site_settings_paging_summary_one">1 comentario por página</string>
+ <string name="site_settings_multiple_links_summary_other">Se necesita aprobación para más de %d enlaces</string>
+ <string name="site_settings_multiple_links_summary_one">Se necesita aprobación para más de 1 enlace</string>
+ <string name="site_settings_multiple_links_summary_zero">Se requiere aprobación para más de 0 enlaces</string>
+ <string name="detail_approve_auto">Aprobar automáticamente los comentarios de todo el mundo.</string>
+ <string name="detail_approve_auto_if_previously_approved">Aprobar automáticamente si el usuario tiene un comentario previamente aprobado</string>
+ <string name="detail_approve_manual">Se requiere aprobación manual de los comentarios de todos.</string>
+ <string name="filter_trashed_posts">En la papelera</string>
+ <string name="days_quantity_one">1 día</string>
+ <string name="days_quantity_other">%d días</string>
+ <string name="filter_published_posts">Publicados</string>
+ <string name="filter_scheduled_posts">Planificados</string>
+ <string name="filter_draft_posts">Borradores</string>
+ <string name="primary_site">Sitio principal</string>
+ <string name="pending_email_change_snackbar">Haz click en el enlace de verificación del correo electrónica enviado a %1$s para confirmar tu nueva dirección</string>
+ <string name="web_address">Dirección web</string>
+ <string name="editor_toast_uploading_please_wait">Actualmente estás subiendo archivos multimedia. Por favor, espera hasta que se complete.</string>
+ <string name="error_refresh_comments_showing_older">No se pudieron actualizar los comentarios ahora mismo - se muestran comentarios antiguos</string>
+ <string name="editor_post_settings_set_featured_image">Fijar la imagen destacada</string>
+ <string name="editor_post_settings_featured_image">Imagen destacada</string>
+ <string name="new_editor_promo_desc">La app de WordPress para Android ahora incluye un nuevo editor visual\nPruébalo creando una nueva entrada.</string>
+ <string name="new_editor_promo_title">El nuevo editor</string>
+ <string name="new_editor_promo_button_label">¡Genial, gracias!</string>
+ <string name="visual_editor_enabled">Editor visual activado</string>
+ <string name="editor_content_placeholder">Comparte tu historia aquí...</string>
+ <string name="editor_page_title_placeholder">Título página</string>
+ <string name="editor_post_title_placeholder">Título entrada</string>
+ <string name="email_address">Correo electrónico</string>
+ <string name="preference_show_visual_editor">Mostrar editor visual</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comments">¿Eliminar de forma permanente estos comentarios?</string>
+ <string name="dlg_sure_to_delete_comment">¿Eliminar de forma permanente este comentario?</string>
+ <string name="mnu_comment_delete_permanently">Eliminar</string>
+ <string name="mnu_comment_untrash">Restaurar</string>
+ <string name="comment_deleted_permanently">Comentario eliminado</string>
+ <string name="comments_empty_list_filtered_spam">Sin comentarios de spam</string>
+ <string name="comment_status_all">Todos</string>
+ <string name="could_not_load_page">No se pudo cargar la página</string>
+ <string name="off">Off</string>
+ <string name="interface_language">Idioma del interface</string>
+ <string name="about_the_app">Sobre la app</string>
+ <string name="error_post_account_settings">No se pudieron guardar los ajustes de la cuenta</string>
+ <string name="error_post_my_profile">No se pudo guardar tu perfil</string>
+ <string name="error_fetch_account_settings">No se pudieron recuperar los ajustes de la cuenta</string>
+ <string name="error_fetch_my_profile">No se pudo obtener tu perfil</string>
+ <string name="stats_widget_promo_ok_btn_label">Bien, lo tenemos</string>
+ <string name="stats_widget_promo_desc">Añade el widget a tu pantalla de inicio para acceder a tus estadísticas en un clic.</string>
+ <string name="stats_widget_promo_title">Widget de estadísticas en portada</string>
+ <string name="site_settings_unknown_language_code_error">No se pudo reconocer el código del idioma</string>
+ <string name="site_settings_threading_dialog_description">Permite los comentarios anidados.</string>
+ <string name="site_settings_threading_dialog_header">Anidar hasta</string>
+ <string name="remove">Eliminar</string>
+ <string name="disabled">Desactivado</string>
+ <string name="search">Buscar</string>
+ <string name="add_category">Añadir categoría</string>
+ <string name="site_settings_image_original_size">Tamaño original</string>
+ <string name="privacy_private">Tu sitio es visible únicamente por ti y por lo usuarios que apruebes</string>
+ <string name="privacy_public_not_indexed">Tu sitio es visible para todos pero pide a los motores de búsqueda no ser indexado</string>
+ <string name="privacy_public">Tu sitio es visible para todos y puede ser indexado por motores de búsqueda</string>
+ <string name="about_me_hint">Algunas palabras sobre ti...</string>
+ <string name="about_me">Sobre mí</string>
+ <string name="public_display_name_hint">El nombre público mostrará por defecto el nombre de usuario si no está establecido</string>
+ <string name="public_display_name">Nombre que se mostrará públicamente</string>
+ <string name="first_name">Nombre</string>
+ <string name="my_profile">Mi perfil</string>
+ <string name="last_name">Apellido</string>
+ <string name="site_privacy_public_desc">Permitir a los motores de búsqueda indexar este sitio</string>
+ <string name="site_privacy_hidden_desc">Disuadir a los motores de búsqueda de indexar este sitio</string>
+ <string name="site_privacy_private_desc">Me gustaría que mi sitio fuese privado, visible únicamente a los usuarios que yo elija</string>
+ <string name="cd_related_post_preview_image">Imagen de vista previa de entradas relacionadas</string>
+ <string name="error_post_remote_site_settings">No pudo guardar la información del sitio</string>
+ <string name="error_fetch_remote_site_settings">No pudo recuperar la información del sitio</string>
+ <string name="error_media_upload_connection">Se ha producido un error de conexión mientras subía multimedia</string>
+ <string name="site_settings_disconnected_toast">Desconectado, la edición se ha desactivado</string>
+ <string name="site_settings_unsupported_version_error">Versión de WordPress no soportada</string>
+ <string name="site_settings_multiple_links_dialog_description">Requiere aprobación para los comentarios que incluyen más de este número de enlaces.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Cierra automáticamente</string>
+ <string name="site_settings_close_after_dialog_description">Cierra automáticamente los comentarios en artículos</string>
+ <string name="site_settings_paging_dialog_description">Rompe los hilos de comentarios en múltiples páginas</string>
+ <string name="site_settings_paging_dialog_header">Comentarios por página</string>
+ <string name="site_settings_close_after_dialog_title">Cerrar los comentarios</string>
+ <string name="site_settings_blacklist_description">Cuando un comentario contenga alguna de estas palabras en su contenido, nombre, URL, correo electrónico o IP, será marcado como spam. Puedes introducir palabras parciales, así "press" coincidirá con "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">Cuando un comentario contenga alguna de las estas palabras en su contenido, nombre, URL, correo electrónico o IP, será puesto en la cola para moderar. Puedes introducir palabras parciales, así "press" coincidirá como "Wordpress"</string>
+ <string name="site_settings_list_editor_input_hint">Introduce una palabra o frase</string>
+ <string name="site_settings_list_editor_no_items_text">Sin elementos</string>
+ <string name="site_settings_learn_more_caption">Puedes sobreescribir esta configuración para entradas individuales</string>
+ <string name="site_settings_rp_preview3_site">en "Mejora"</string>
+ <string name="site_settings_rp_preview3_title">Novedades: VideoPress para bodas</string>
+ <string name="site_settings_rp_preview2_site">en "Aplicaciones"</string>
+ <string name="site_settings_rp_preview2_title">La aplicación WordPress para Android consigue un gran lavado de cara</string>
+ <string name="site_settings_rp_preview1_site">en "Móvil"</string>
+ <string name="site_settings_rp_preview1_title">Gran actualización disponible ahora para iPhone/iPad</string>
+ <string name="site_settings_rp_show_images_title">Mostrar imágenes</string>
+ <string name="site_settings_rp_show_header_title">Mostrar cabecera</string>
+ <string name="site_settings_rp_switch_summary">Las entradas relacionadas muestran contenido relevante de tu sitio debajo de las entradas.</string>
+ <string name="site_settings_rp_switch_title">Mostrar entradas relacionadas</string>
+ <string name="site_settings_delete_site_hint">Elimina los datos de tu sitio de la aplicación</string>
+ <string name="site_settings_blacklist_hint">Los comentarios que coinciden con un filtro, son marcados como spam</string>
+ <string name="site_settings_moderation_hold_hint">Los comentarios que coinciden con un filtro, son puestos en la cola para moderar</string>
+ <string name="site_settings_multiple_links_hint">Ignorar límite de enlaces de usuarios conocidos</string>
+ <string name="site_settings_whitelist_hint">El autor de un comentario debe tener aprobado un comentario previo</string>
+ <string name="site_settings_user_account_required_hint">Los usuarios deben estar registrados e iniciar sesión para comentar</string>
+ <string name="site_settings_identity_required_hint">El autor de un comentario debe rellenar el nombre y correo eléctronico</string>
+ <string name="site_settings_manual_approval_hint">Los comentarios deben aprobarse manualmente</string>
+ <string name="site_settings_paging_hint">Mostrar los comentarios en pedazos de un tamaño especificado</string>
+ <string name="site_settings_threading_hint">Permitir comentarios anidados hasta cierto nivel</string>
+ <string name="site_settings_sort_by_hint">Determina el orden de los comentarios mostrados</string>
+ <string name="site_settings_close_after_hint">No permitir los comentarios después de un tiempo</string>
+ <string name="site_settings_receive_pingbacks_hint">Permitir notificaciones de enlace de otros sitios</string>
+ <string name="site_settings_send_pingbacks_hint">Intenta notificar cualquier sitio enlazado desde el artículo</string>
+ <string name="site_settings_allow_comments_hint">Permitir a los lectores publicar comentarios</string>
+ <string name="site_settings_discussion_hint">Ver y cambiar la configuración de discusiones de tu sitio</string>
+ <string name="site_settings_more_hint">Ver todos los ajustes disponibles de las discusiones</string>
+ <string name="site_settings_related_posts_hint">Mostrar u ocultar entradas relacionadas en el lector</string>
+ <string name="site_settings_upload_and_link_image_hint">Habilitar que siempre suba la imagen a tamaño completo</string>
+ <string name="site_settings_image_width_hint">Redimensionar imágenes en las entradas a esta anchura</string>
+ <string name="site_settings_format_hint">Establece formato para nuevas entradas</string>
+ <string name="site_settings_category_hint">Establece categoría para nuevas entradas</string>
+ <string name="site_settings_location_hint">Añadir ubicación a tus entradas automáticamente</string>
+ <string name="site_settings_password_hint">Cambiar tu contraseña</string>
+ <string name="site_settings_username_hint">Cuenta del usuario actual</string>
+ <string name="site_settings_language_hint">Idioma en el que está escrito este blog</string>
+ <string name="site_settings_privacy_hint">Controla quien puede ver tu sitio</string>
+ <string name="site_settings_address_hint">El cambio de dirección no está soportado en este momento</string>
+ <string name="site_settings_tagline_hint">Una descripción corta o frase ingeniosa que describa tu blog</string>
+ <string name="site_settings_title_hint">En pocas palabras, explica sobre que es este sitio</string>
+ <string name="site_settings_whitelist_known_summary">Comentarios de usuarios conocidos</string>
+ <string name="site_settings_whitelist_all_summary">Comentarios de todos los usuarios</string>
+ <string name="site_settings_threading_summary">%d niveles</string>
+ <string name="site_settings_privacy_private_summary">Privado</string>
+ <string name="site_settings_privacy_hidden_summary">Oculto</string>
+ <string name="site_settings_privacy_public_summary">Público</string>
+ <string name="site_settings_delete_site_title">Borrar el sitio</string>
+ <string name="site_settings_blacklist_title">Lista negra</string>
+ <string name="site_settings_moderation_hold_title">Mantener para moderación</string>
+ <string name="site_settings_multiple_links_title">Enlaces en comentarios</string>
+ <string name="site_settings_whitelist_title">Aprobar automáticamente</string>
+ <string name="site_settings_paging_title">Paginación</string>
+ <string name="site_settings_threading_title">Comentarios anidados</string>
+ <string name="site_settings_sort_by_title">Ordenar por</string>
+ <string name="site_settings_account_required_title">Los usuarios deben iniciar sesión</string>
+ <string name="site_settings_identity_required_title">Deben ser incluidos el nombre y correo electrónico</string>
+ <string name="site_settings_receive_pingbacks_title">Recibe pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Envía pingbacks</string>
+ <string name="site_settings_allow_comments_title">Permitir comentarios</string>
+ <string name="site_settings_default_format_title">Formato por defecto</string>
+ <string name="site_settings_default_category_title">Categoría por defecto</string>
+ <string name="site_settings_location_title">Permitir la ubicación</string>
+ <string name="site_settings_address_title">Dirección</string>
+ <string name="site_settings_title_title">Título del sitio</string>
+ <string name="site_settings_tagline_title">Lema</string>
+ <string name="site_settings_this_device_header">Este dispositivo</string>
+ <string name="site_settings_discussion_new_posts_header">Por defecto para nuevas entradas</string>
+ <string name="site_settings_writing_header">Escribiendo</string>
+ <string name="site_settings_account_header">Cuenta</string>
+ <string name="newest_first">Nuevos primero</string>
+ <string name="site_settings_general_header">General</string>
+ <string name="privacy">Privacidad</string>
+ <string name="related_posts">Entradas relacionadas</string>
+ <string name="discussion">Discusión</string>
+ <string name="comments">Comentarios</string>
+ <string name="close_after">Cerrar después de</string>
+ <string name="oldest_first">Antiguos primero</string>
+ <string name="media_error_no_permission_upload">No tienes permisos para subir multimedia al sitio</string>
+ <string name="never">Nunca</string>
+ <string name="unknown">Desconocido</string>
+ <string name="reader_err_get_post_not_found">Esta entrada ya no existe</string>
+ <string name="reader_err_get_post_not_authorized">No estás autorizado a ver esta entrada</string>
+ <string name="reader_err_get_post_generic">No se pudo cargar esta entrada</string>
+ <string name="blog_name_no_spaced_allowed">La dirección del sitio no puede tener espacios</string>
+ <string name="invalid_username_no_spaces">El nombre de usuario no puede tener espacios</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Los sitios que estás siguiendo no han publicado nada recientemente</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">No hay entradas recientes</string>
+ <string name="media_details_copy_url_toast">URL copiada al portapapeles</string>
+ <string name="edit_media">Editar multimedia</string>
+ <string name="media_details_copy_url">Copiar URL</string>
+ <string name="media_details_label_date_uploaded">Subido</string>
+ <string name="media_details_label_date_added">Añadido</string>
+ <string name="selected_theme">Tema seleccionado</string>
+ <string name="could_not_load_theme">No se ha podido cargar el tema</string>
+ <string name="theme_activation_error">Se ha producido un error. No se ha podido activar el tema</string>
+ <string name="theme_by_author_prompt_append"> de %1$s</string>
+ <string name="theme_prompt">Gracias por elegir %1$s</string>
+ <string name="theme_view">Vista</string>
+ <string name="theme_try_and_customize">Probar y personalizar</string>
+ <string name="theme_done">HECHO</string>
+ <string name="theme_manage_site">GESTIONAR SITIO</string>
+ <string name="theme_details">Detalles</string>
+ <string name="theme_support">Ayuda</string>
+ <string name="title_activity_theme_support">Temas</string>
+ <string name="theme_activate">Activar</string>
+ <string name="current_theme">Tema actual</string>
+ <string name="customize">Personalizar</string>
+ <string name="date_range_start_date">Fecha de inicio</string>
+ <string name="date_range_end_date">Fecha de finalización</string>
+ <string name="details">Detalles</string>
+ <string name="support">Ayuda</string>
+ <string name="active">Activo</string>
+ <string name="stats_referrers_spam_generic_error">Se ha producido un error durante la operación. No se ha cambiado el estado del spam.</string>
+ <string name="stats_referrers_marking_not_spam">Marcando como no spam</string>
+ <string name="stats_referrers_unspam">No spam</string>
+ <string name="stats_referrers_marking_spam">Marcando como spam</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="page_published">Página publicada</string>
+ <string name="page_updated">Página actualizada</string>
+ <string name="theme_auth_error_authenticate">Error al recibir temas: error al autenticar el usuario</string>
+ <string name="post_published">Entrada publicada</string>
+ <string name="post_updated">Entrada actualizada</string>
+ <string name="theme_no_search_result_found">Lo sentimos, no se han encontrados temas.</string>
+ <string name="media_uploaded_on">Subido el: %s</string>
+ <string name="media_file_name">Nombre del archivo: %s</string>
+ <string name="media_dimensions">Dimensiones: %s</string>
+ <string name="media_file_type">Tipo de archivo: %s</string>
+ <string name="upload_queued">En cola</string>
+ <string name="reader_label_gap_marker">Cargar más entradas</string>
+ <string name="notifications_no_search_results">Ningún sitio coincide con "%s"</string>
+ <string name="search_sites">Buscar sitios</string>
+ <string name="notifications_empty_view_reader">Ver Lector</string>
+ <string name="unread">Sin leer</string>
+ <string name="notifications_empty_action_followers_likes">Recibir avisos: comentar en entradas que has leído.</string>
+ <string name="notifications_empty_action_comments">Unirse a una conversación: comentar en entradas de blogs que sigues.</string>
+ <string name="notifications_empty_action_unread">Reiniciar la conversación: escribir una entrada nueva.</string>
+ <string name="notifications_empty_action_all">¡Actívate! Comentario en entradas de blogs que sigues.</string>
+ <string name="notifications_empty_likes">No hay nuevos Me gusta que mostrar... todavía.</string>
+ <string name="notifications_empty_followers">No hay nuevos seguidores de los que informar... todavía.</string>
+ <string name="notifications_empty_comments">No hay comentarios nuevos... todavía.</string>
+ <string name="notifications_empty_unread">¡Te has puesto al día!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Accede a las estadísticas de la aplicación e intenta agregar un widget más tarde</string>
+ <string name="stats_widget_error_readd_widget">Elimina el widget y vuelve a agregarlo</string>
+ <string name="stats_widget_error_no_visible_blog">No se pudo acceder a las estadísticas sin un blog visible</string>
+ <string name="stats_widget_error_no_permissions">Tu cuenta de WordPress.com no puede acceder a las estadísticas de este blog</string>
+ <string name="stats_widget_error_no_account">Accede a WordPress</string>
+ <string name="stats_widget_error_generic">No se han podido cargar las estadísticas</string>
+ <string name="stats_widget_loading_data">Cargando datos...</string>
+ <string name="stats_widget_name_for_blog">Las estadísticas de hoy de %1$s</string>
+ <string name="stats_widget_name">Las estadísticas de hoy de WordPress</string>
+ <string name="add_location_permission_required">Permiso necesario para añadir la ubicación</string>
+ <string name="add_media_permission_required">Permisos necesarios para añadir archivos multimedia</string>
+ <string name="access_media_permission_required">Permisos necesarios para acceder a archivos multimedia</string>
+ <string name="stats_enable_rest_api_in_jetpack">Para ver tus estadísticas debes activar el módulo JSON API en Jetpack.</string>
+ <string name="error_open_list_from_notification">Esta entrada o página fue publicada en otro sitio</string>
+ <string name="reader_short_comment_count_multi">%s Comentarios</string>
+ <string name="reader_short_comment_count_one">1 Comentario</string>
+ <string name="reader_label_submit_comment">ENVIAR</string>
+ <string name="reader_hint_comment_on_post">Responder a la entrada...</string>
+ <string name="reader_discover_visit_blog">Visita %s</string>
+ <string name="reader_discover_attribution_blog">Originalmente publicado en %s</string>
+ <string name="reader_discover_attribution_author">Originalmente publicado por %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originalmente publicado por %1$s en %2$s</string>
+ <string name="reader_short_like_count_multi">%s Me gusta</string>
+ <string name="reader_short_like_count_none">Me gusta</string>
+ <string name="reader_short_like_count_one">1 Me gusta</string>
+ <string name="reader_label_follow_count">%,d seguidores</string>
+ <string name="reader_menu_tags">Editar etiquetas y blogs</string>
+ <string name="reader_title_post_detail">Entrada del lector</string>
+ <string name="local_draft_explainer">Esta entrada tiene un borrador local que no ha sido publicado</string>
+ <string name="local_changes_explainer">Esta entrada tiene cambios en local que no han sido publicados</string>
+ <string name="notifications_push_summary">Opciones para las notificaciones que aparecen en tu dispositivo.</string>
+ <string name="notifications_email_summary">Opciones para las notificaciones que se envían al correo electrónico ligado a tu cuenta.</string>
+ <string name="notifications_tab_summary">Opciones para las notificaciones que aparecen en la pestaña de notificaciones.</string>
+ <string name="notifications_disabled">Las notificaciones de la App han sido desativadas. Pulsa aquí para activarlas en Opciones.</string>
+ <string name="notification_types">Tipos de notificaciones</string>
+ <string name="error_loading_notifications">No se han podido cargar los ajustes de avisos</string>
+ <string name="replies_to_your_comments">Respuestas a tus comentarios</string>
+ <string name="comment_likes">Me gusta al comentario</string>
+ <string name="notifications_tab">Pestaña de avisos</string>
+ <string name="email">Correo electrónico</string>
+ <string name="app_notifications">Avisos de la aplicación</string>
+ <string name="notifications_comments_other_blogs">Comentarios en otros sitios</string>
+ <string name="notifications_other">Otras</string>
+ <string name="notifications_wpcom_updates">Actualizaciones de WordPress.com</string>
+ <string name="notifications_account_emails">Correo de WordPress.com</string>
+ <string name="notifications_account_emails_summary">Siempre mandamos correos electrónicos importantes relativos a tu cuenta, pero también obtendrás extras útiles.</string>
+ <string name="notifications_sights_and_sounds">Vistas y sonidos</string>
+ <string name="your_sites">Tus sitios</string>
+ <string name="stats_insights_latest_post_trend">Ha pasado %1$s desde que se publicó %2$s. Aquí tienes el rendimiento de la entrada desde entonces…</string>
+ <string name="stats_insights_latest_post_summary">Sumario de la última entrada</string>
+ <string name="button_revert">Volver</string>
+ <string name="days_ago">Hace %d días</string>
+ <string name="yesterday">Ayer</string>
+ <string name="connectionbar_no_connection">Sin conexión</string>
+ <string name="button_preview">Vista previa</string>
+ <string name="button_view">Ver</string>
+ <string name="stats_no_activity_this_period">Sin actividad en este periodo</string>
+ <string name="button_publish">Publicar</string>
+ <string name="page_trashed">Página enviada a la papelera</string>
+ <string name="page_deleted">Página borrada</string>
+ <string name="button_trash">Papelera</string>
+ <string name="button_stats">Estadísticas</string>
+ <string name="post_trashed">Entrada enviada a la papelera</string>
+ <string name="post_deleted">Entrada borrada</string>
+ <string name="trashed">En la papelera</string>
+ <string name="button_edit">Editar</string>
+ <string name="button_back">Atrás</string>
+ <string name="my_site_no_sites_view_subtitle">¿Quieres añadir uno?</string>
+ <string name="my_site_no_sites_view_title">Aún no tienes ningún sitio WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustración</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">No tienes autorización para acceder a este blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">No se pudo encontrar este blog</string>
+ <string name="undo">Deshacer</string>
+ <string name="tabbar_accessibility_label_me">Yo mismo</string>
+ <string name="tabbar_accessibility_label_my_site">Mi sitio</string>
+ <string name="passcodelock_prompt_message">Escribe tu PIN</string>
+ <string name="editor_toast_changes_saved">Cambios guardados</string>
+ <string name="push_auth_expired">La solicitud ha expirado. Inicia sesión en WordPress.com para volver a intentarlo.</string>
+ <string name="ignore">Ignorar</string>
+ <string name="stats_insights_best_ever">El mejor día</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de vistas</string>
+ <string name="stats_insights_most_popular_hour">Hora más popular</string>
+ <string name="stats_insights_most_popular_day">Día más popular</string>
+ <string name="stats_insights_popular">Hora y día más populares</string>
+ <string name="stats_insights_today">Estadísticas de hoy</string>
+ <string name="stats_insights_all_time">Entradas, vistas y visitantes de todos los tiempos</string>
+ <string name="stats_insights">Detalles</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Para ver tus estadísticas, inicia sesión en la cuenta de WordPress.com que has usado para efectuar la conexión con Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">¿Buscas tus otras estadísticas recientes? Las hemos trasladado a la página "Detalles".</string>
+ <string name="me_disconnect_from_wordpress_com">Desconectar de WordPress.com</string>
+ <string name="me_btn_login_logout">Iniciar/Cerrar sesión</string>
+ <string name="me_connect_to_wordpress_com">Conectar a WordPress.com</string>
+ <string name="account_settings">Configuración de la cuenta</string>
+ <string name="me_btn_support">Ayuda y soporte</string>
+ <string name="site_picker_cant_hide_current_site">"%s" no se ocultó porque es el sitio actual</string>
+ <string name="site_picker_create_dotcom">Crear sitio en WordPress.com</string>
+ <string name="site_picker_edit_visibility">Mostrar/Ocultar sitios</string>
+ <string name="site_picker_add_self_hosted">Añadir sitio autoalojado</string>
+ <string name="site_picker_add_site">Añadir sitio</string>
+ <string name="my_site_btn_view_site">Ver sitio</string>
+ <string name="my_site_btn_view_admin">Ver Administrador</string>
+ <string name="site_picker_title">Elegir sitio</string>
+ <string name="my_site_btn_switch_site">Cambiar sitio</string>
+ <string name="my_site_header_publish">Publicar</string>
+ <string name="my_site_btn_site_settings">Preferencias</string>
+ <string name="my_site_btn_blog_posts">Entradas del blog</string>
+ <string name="my_site_header_look_and_feel">Aspecto</string>
+ <string name="reader_label_new_posts_subtitle">Toca para mostrarlos</string>
+ <string name="my_site_header_configuration">Configuración</string>
+ <string name="notifications_account_required">Inicia sesión en WordPress.com para recibir notificaciones</string>
+ <string name="stats_unknown_author">Autor desconocido</string>
+ <string name="image_added">Imagen añadida</string>
+ <string name="signout">Desconectar</string>
+ <string name="sign_out_wpcom_confirm">Si desconectas tu cuenta, se eliminarán de este dispositivo todos los datos de WordPress.com de @%s, incluidos los borradores y los cambios locales.</string>
+ <string name="select_all">Seleccionar todo</string>
+ <string name="hide">Ocultar</string>
+ <string name="show">Mostrar</string>
+ <string name="deselect_all">Anular todas las selecciones</string>
+ <string name="select_from_new_picker">Selección múltiple con el nuevo selector</string>
+ <string name="loading_blog_videos">Trayendo vídeos</string>
+ <string name="loading_blog_images">Trayendo imágenes</string>
+ <string name="error_loading_blog_videos">No se pudieron traer los vídeos</string>
+ <string name="no_media_sources">No se pudieron traer los archivos multimedia</string>
+ <string name="error_loading_blog_images">No se pudieron traer las imágenes</string>
+ <string name="stats_generic_error">No se pudieron cargar las estadísticas solicitadas.</string>
+ <string name="no_device_videos">No hay vídeos</string>
+ <string name="no_blog_videos">No hay vídeos</string>
+ <string name="no_blog_images">No hay imágenes</string>
+ <string name="no_device_images">No hay imágenes</string>
+ <string name="error_loading_videos">Error cargando vídeos</string>
+ <string name="error_loading_images">Error cargando imágenes</string>
+ <string name="no_media">No hay archivos multimedia</string>
+ <string name="loading_videos">Cargando vídeos</string>
+ <string name="loading_images">Cargando imágenes</string>
+ <string name="verify">Verificar</string>
+ <string name="tab_title_site_videos">Vídeos del sitio</string>
+ <string name="tab_title_device_videos">Vídeos del dispositivo</string>
+ <string name="media_details_label_file_type">Tipo de archivo</string>
+ <string name="media_picker_title">Selecciona medio</string>
+ <string name="take_photo">Sacar una foto</string>
+ <string name="editor_toast_invalid_path">Ruta de archivo no válida</string>
+ <string name="two_step_sms_sent">Revisa tus mensajes de texto para el código de verificación.</string>
+ <string name="media_details_label_file_name">Nombre del archivo</string>
+ <string name="error_publish_no_network">No se puede publicar mientras no haya conexión. Guardado como borrador.</string>
+ <string name="two_step_footer_label">Introduce el código de tu app de autentificación.</string>
+ <string name="sign_in_jetpack">Inicia sesión en tu cuenta de WordPress.com para conectarte con Jetpack.</string>
+ <string name="auth_required">Inicia sesión de nuevo para continuar.</string>
+ <string name="tab_title_site_images">Imágenes del sitio</string>
+ <string name="tab_title_device_images">Imágenes del dispositivo</string>
+ <string name="language">Idioma</string>
+ <string name="take_video">Grabar un vídeo</string>
+ <string name="two_step_footer_button">Enviar código vía mensaje de texto</string>
+ <string name="device">Dispositivo</string>
+ <string name="invalid_verification_code">Código de verificación no válido</string>
+ <string name="verification_code">Código de verificación</string>
+ <string name="add_to_post">Añadir a la entrada</string>
+ <string name="stats_search_terms_unknown_search_terms">Términos de búsqueda desconocidos</string>
+ <string name="stats_view_search_terms">Términos de búsqueda</string>
+ <string name="stats_entry_search_terms">Término de búsqueda</string>
+ <string name="pages_fetching">Recuperando páginas…</string>
+ <string name="media_fetching">Recuperando medios…</string>
+ <string name="posts_fetching">Recuperando entradas…</string>
+ <string name="comments_fetching">Recuperando comentarios…</string>
+ <string name="toast_err_post_uploading">No se puede abrir la entrada durante la carga</string>
+ <string name="error_notification_open">No se pudo abrir la notificación</string>
+ <string name="reader_empty_posts_request_failed">No se pudieron cargar las entradas</string>
+ <string name="stats_empty_search_terms">No se han grabado los términos de búsqueda</string>
+ <string name="stats_followers_total_email_paged">Mostrando del %1$d al %2$d de %3$s suscriptores por correo electrónico</string>
+ <string name="stats_followers_total_wpcom_paged">Mostrando del %1$d al %2$d de %3$s suscriptores de WordPress.com</string>
+ <string name="stats_view_authors">Autores</string>
+ <string name="publisher">Autor:</string>
+ <string name="stats_empty_search_terms_desc">Aprende más sobre el tráfico que te traen las búsquedas viendo los términos que buscaron tus lectores para llegar a tu sitio.</string>
+ <string name="stats_total">Total</string>
+ <string name="reader_page_recommended_blogs">Sitios que te podrían gustar</string>
+ <string name="stats_period">Periodo</string>
+ <string name="error_copy_to_clipboard">Ocurrió un error al copiar el texto en el portapapeles</string>
+ <string name="reader_label_new_posts">Nuevas entradas</string>
+ <string name="stats_months_and_years">Meses y años</string>
+ <string name="stats_average_per_day">Media diaria</string>
+ <string name="logs_copied_to_clipboard">Los informes de la aplicación se han copiado al portapapeles</string>
+ <string name="stats_overall">Global</string>
+ <string name="reader_empty_posts_in_blog">Este blog está vacío</string>
+ <string name="post_uploading">Cargando</string>
+ <string name="stats_recent_weeks">Últimas Semanas</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_views">Vistas</string>
+ <string name="stats_view">Vista</string>
+ <string name="stats_visitors">Visitantes</string>
+ <string name="stats_view_all">Ver todo</string>
+ <string name="stats_view_videos">Vídeos</string>
+ <string name="stats_entry_video_plays">Vídeo</string>
+ <string name="stats_followers_a_month">Un mes</string>
+ <string name="stats_followers_a_day">Un día</string>
+ <string name="stats_followers_a_year">Un año</string>
+ <string name="stats_comments_total_comments_followers">Total de entradas con comentarios de seguidores: %1$s</string>
+ <string name="stats_empty_followers">Sin seguidores</string>
+ <string name="stats_empty_clicks_desc">Si tu contenido incluye enlaces a otros sitios, verás cuáles son aquellos en los que los visitantes hacen clic de forma más habitual.</string>
+ <string name="stats_empty_comments_desc">Si permites comentarios en tu sitio, realiza un seguimiento de los principales autores de comentarios y descubre, a partir de los 1000 comentarios más recientes, qué parte de tu contenido desata las conversaciones más animadas.</string>
+ <string name="stats_empty_video_desc">Si has subido vídeos con VideoPress, descubre cuántas veces se han visto.</string>
+ <string name="stats_entry_publicize">Servicio</string>
+ <string name="stats_followers_total_email">Seguidores totales por correo electrónico: %1$s</string>
+ <string name="stats_followers_total_wpcom">Seguidores totales de WordPress.com: %1$s</string>
+ <string name="stats_totals_publicize">Seguidores</string>
+ <string name="stats_view_followers">Seguidores</string>
+ <string name="stats_entry_followers">Seguidor</string>
+ <string name="stats_empty_followers_desc">Realiza un seguimiento del número total de seguidores y del tiempo que cada uno de ellos lleva siguiendo tu sitio.</string>
+ <string name="stats_empty_publicize_desc">Realiza un seguimiento de los seguidores provenientes de distintos servicios de redes sociales con ayuda de Publicize.</string>
+ <string name="stats_empty_top_authors_desc">Realiza un seguimiento de las visitas que reciben las entradas de cada colaborador y haz zoom para descubrir cuál es el contenido más popular de cada autor.</string>
+ <string name="stats_comments_by_posts_and_pages">Por entradas y páginas</string>
+ <string name="stats_comments_by_authors">Por autores</string>
+ <string name="stats_pagination_label">Página %1$s de %2$s</string>
+ <string name="stats_view_countries">Países</string>
+ <string name="stats_other_recent_stats_label">Otras estadísticas recientes</string>
+ <string name="themes_fetching">Obteniendo temas…</string>
+ <string name="stats_empty_tags_and_categories_desc">Obtén una visión general de los temas más populares de tu sitio a partir de las principales entradas publicadas la semana pasada.</string>
+ <string name="stats_empty_referrers_desc">Obtén más información acerca de la visibilidad de tu sitio; para ello, echa un vistazo a los sitios web y los motores de búsqueda que envían la mayor parte del tráfico a tu sitio</string>
+ <string name="stats_empty_tags_and_categories">No se han visto páginas ni entradas etiquetadas</string>
+ <string name="stats_empty_top_posts_title">No se han visto entradas ni páginas</string>
+ <string name="stats_empty_video">No se han reproducido vídeos</string>
+ <string name="stats_empty_publicize">No se han registrado seguidores de Publicize</string>
+ <string name="stats_empty_referrers_title">No se han registrado referencias</string>
+ <string name="stats_empty_geoviews">No se han registrado países</string>
+ <string name="stats_empty_clicks_title">No se han registrado clics</string>
+ <string name="stats_likes">Me gusta</string>
+ <string name="stats_followers_seconds_ago">hace unos segundos</string>
+ <string name="stats_followers_an_hour_ago">hace una hora</string>
+ <string name="stats_followers_a_minute_ago">hace un minuto</string>
+ <string name="stats_empty_geoviews_desc">Explora la lista para saber cuáles son los países y las regiones que generan más tráfico hasta tu sitio.</string>
+ <string name="stats_for">Estadísticas del %s</string>
+ <string name="stats_view_top_posts_and_pages">Entradas y páginas</string>
+ <string name="stats_entry_clicks_link">Enlace</string>
+ <string name="stats_view_publicize">Difundir</string>
+ <string name="stats_totals_followers">Desde</string>
+ <string name="stats_empty_top_posts_desc">Descubre qué parte de tu contenido se ha visto más y consulta cómo rinden las entradas y páginas individuales a lo largo del tiempo.</string>
+ <string name="stats_followers_email_selector">Correo electrónico</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_timeframe_years">Años</string>
+ <string name="stats_followers_minutes">%1$d minutos</string>
+ <string name="stats_followers_months">%1$d meses</string>
+ <string name="stats_followers_hours">%1$d horas</string>
+ <string name="stats_followers_days">%1$d días</string>
+ <string name="stats_followers_years">%1$d años</string>
+ <string name="ssl_certificate_details">Detalles</string>
+ <string name="media_gallery_date_range">Mostrando elementos multimedia desde %1$s a %2$s</string>
+ <string name="delete_sure">Eliminar este borrador</string>
+ <string name="delete_sure_page">Eliminar esta página</string>
+ <string name="delete_sure_post">Eliminar esta entrada</string>
+ <string name="confirm_delete_multi_media">¿Quieres eliminar los elementos seleccionados?</string>
+ <string name="confirm_delete_media">¿Quieres eliminar el elemento seleccionado?</string>
+ <string name="sure_to_remove_account">¿Deseas eliminar este sitio?</string>
+ <string name="cab_selected">%d seleccionados</string>
+ <string name="nux_help_description">Visita el centro de ayuda para obtener respuestas a las preguntas habituales o visita los foros para hacer preguntas nuevas.</string>
+ <string name="reader_label_view_original">Ver artículo original</string>
+ <string name="reader_label_comment_count_single">Un comentario</string>
+ <string name="reader_empty_followed_blogs_title">Todavía no sigues a ningún sitio.</string>
+ <string name="pages_empty_list">Sin páginas todavía. ¿Por qué no crear una?</string>
+ <string name="media_empty_list_custom_date">Sin multimedia durante este intervalo de tiempo.</string>
+ <string name="new_blog_wpcom_created">Se ha creado un blog de WordPress.com.</string>
+ <string name="comment_reply_to_user">Responder a %s</string>
+ <string name="posting_post">Publicando "%s"</string>
+ <string name="faq_button">Preguntas frecuentes</string>
+ <string name="error_refresh_unauthorized_pages">No tienes permiso para ver o editar páginas.</string>
+ <string name="error_refresh_unauthorized_posts">No tienes permiso para ver o editar entradas.</string>
+ <string name="error_refresh_unauthorized_comments">No tienes permiso para ver o editar comentarios.</string>
+ <string name="reader_empty_posts_liked">No te ha gustado ninguna entrada.</string>
+ <string name="error_publish_empty_post">No se puede publicar una entrada vacía.</string>
+ <string name="stats_no_blog">No se han podido cargar las estadísticas del blog solicitado</string>
+ <string name="reader_empty_posts_in_tag">No hay entradas con esta etiqueta</string>
+ <string name="mnu_comment_liked">Me gustó</string>
+ <string name="reader_label_like">Me gusta</string>
+ <string name="more">Más</string>
+ <string name="reader_label_comments_closed">Los comentarios están cerrados</string>
+ <string name="older_two_days">Hace más de 2 días</string>
+ <string name="older_last_week">Hace más de 1 semana</string>
+ <string name="older_month">Hace más de 1 mes</string>
+ <string name="browse_our_faq_button">Explora nuestras Preguntas frecuentes.</string>
+ <string name="select_a_blog">Elegir un sitio de WordPress</string>
+ <string name="comment_trashed">El comentario se ha enviado a la papelera.</string>
+ <string name="create_new_blog_wpcom">Crear un blog de WordPress.com</string>
+ <string name="reader_label_comments_on">Comentarios en</string>
+ <string name="comment">Comentario</string>
+ <string name="signing_out">Cerrando sesión…</string>
+ <string name="sending_content">Cargando el contenido de %s</string>
+ <string name="uploading_total">Cargando %1$d de %2$d</string>
+ <string name="agree_terms_of_service">Al crear una cuenta, aceptas los fascinantes %1$sTérminos de servicio%2$s.</string>
+ <string name="posts_empty_list">Aún no se han publicado entradas. ¿Por qué no crear una?</string>
+ <string name="reader_empty_comments">Aún no hay comentarios</string>
+ <string name="reader_title_photo_viewer">%1$d de %2$d</string>
+ <string name="reader_label_comment_count_multi">%,d comentarios</string>
+ <string name="reader_toast_err_generic">No es posible realizar esta acción</string>
+ <string name="reader_toast_err_block_blog">No es posible bloquear este blog</string>
+ <string name="reader_toast_blog_blocked">Las entradas de este blog no volverán a mostrarse</string>
+ <string name="reader_menu_block_blog">Bloquear este blog</string>
+ <string name="contact_us">Contacta con nosotros</string>
+ <string name="hs__conversation_detail_error">Describe el problema que estás teniendo</string>
+ <string name="hs__new_conversation_header">Chat de soporte</string>
+ <string name="hs__conversation_header">Chat de soporte</string>
+ <string name="hs__username_blank_error">Escribe un nombre válido</string>
+ <string name="hs__invalid_email_error">Escribe una dirección de correo electrónico válida</string>
+ <string name="add_location">Añadir localización</string>
+ <string name="current_location">Localización actual</string>
+ <string name="search_location">Búsqueda</string>
+ <string name="edit_location">Editar</string>
+ <string name="search_current_location">Localizar</string>
+ <string name="preference_send_usage_stats">Enviar estadísticas</string>
+ <string name="preference_send_usage_stats_summary">Enviar automáticamente estadísticas de uso para ayudarnos a mejorar WordPress para Android</string>
+ <string name="update_verb">Actualizar</string>
+ <string name="schedule_verb">Programación</string>
+ <string name="reader_title_subs">Etiquetas y blogs</string>
+ <string name="reader_page_followed_tags">Etiquetas que se siguen</string>
+ <string name="reader_label_followed_blog">Blog que se sigue</string>
+ <string name="reader_label_tag_preview">Entradas etiquetadas %s</string>
+ <string name="reader_toast_err_get_blog_info">No se puede mostrar este blog</string>
+ <string name="reader_toast_err_already_follow_blog">Ya estás siguiendo este blog</string>
+ <string name="reader_toast_err_follow_blog">No se puede seguir este blog</string>
+ <string name="reader_toast_err_unfollow_blog">No se puede dejar de seguir este blog</string>
+ <string name="reader_empty_recommended_blogs">Sin blogs recomendados</string>
+ <string name="reader_page_followed_blogs">Sitios que sigues</string>
+ <string name="reader_hint_add_tag_or_url">Introduce la URL o etiqueta que quieras seguir</string>
+ <string name="reader_title_tag_preview">Etiqueta del lector</string>
+ <string name="reader_title_blog_preview">Blog del lector</string>
+ <string name="saving">Guardando</string>
+ <string name="media_empty_list">Sin elementos multimedia</string>
+ <string name="ptr_tip_message">Consejo: Arrastra hacia abajo para recargar</string>
+ <string name="help">Ayuda</string>
+ <string name="forgot_password">¿Has olvidado la contraseña?</string>
+ <string name="forums">Foros</string>
+ <string name="help_center">Centro de Ayuda</string>
+ <string name="ssl_certificate_error">Certificado SSL no válido</string>
+ <string name="ssl_certificate_ask_trust">SI normalmente se conecta sin problemas a este sitio sin problemas, este error puede significar que alguien están intentando suplantar el sitio, por lo que no deberías continuar. ¿Quieres, de todas formas, confiar en el certificado?</string>
+ <string name="out_of_memory">Memoria del dispositivo agotada</string>
+ <string name="no_network_message">No hay ninguna conexión de red disponible</string>
+ <string name="gallery_error">El elemento multimedia no ha podido ser recuperado</string>
+ <string name="blog_not_found">Ha ocurrido un error mientras se accedía a este blog</string>
+ <string name="wait_until_upload_completes">Espere mientras se complete la carga</string>
+ <string name="theme_fetch_failed">Ocurrió un error al obtener los temas.</string>
+ <string name="theme_set_failed">No se pudo establecer el tema</string>
+ <string name="theme_auth_error_message">Asegúrate que tienes autorización para elegir temas</string>
+ <string name="comments_empty_list">No hay comentarios.</string>
+ <string name="mnu_comment_unspam">No es spam</string>
+ <string name="no_site_error">No se pudo conectar con el sitio WordPress</string>
+ <string name="adding_cat_failed">No se pudo añadir la categoría</string>
+ <string name="adding_cat_success">Categoría agregada correctamente.</string>
+ <string name="cat_name_required">El campo nombre de categoría es necesario</string>
+ <string name="category_automatically_renamed">El nombre de categoría %1$s no es válido. Fue renombrado a %2$s.</string>
+ <string name="no_account">No se encontró una cuenta WordPress, añade una cuenta e intenta nuevamente</string>
+ <string name="sdcard_message">Se necesita una tarjeta SD montada para subir medios</string>
+ <string name="stats_empty_comments">Aún sin comentarios</string>
+ <string name="stats_bar_graph_empty">Estadísticas no disponibles.</string>
+ <string name="reply_failed">Respuesta fallida</string>
+ <string name="notifications_empty_list">Sin notificaciones</string>
+ <string name="error_delete_post">Ocurrió un error al eliminar el %s</string>
+ <string name="error_refresh_posts">Las entradas no pueden ser actualizadas en este momento</string>
+ <string name="error_refresh_pages">Las páginas no pueden ser actualizadas en este momento.</string>
+ <string name="error_refresh_notifications">No se pudo actualizar las notificaciones</string>
+ <string name="error_refresh_comments">No se pudieron actualizar los comentarios</string>
+ <string name="error_refresh_stats">No se pudieron actualizar las estadísticas</string>
+ <string name="error_generic">Ocurrió un error</string>
+ <string name="error_moderate_comment">Ocurrió un error al moderar el comentario</string>
+ <string name="error_edit_comment">Ocurrió un error al editar el comentario</string>
+ <string name="error_upload">Ocurrió un error mientras se subía el %s</string>
+ <string name="error_load_comment">No se pudo cargar el comentario</string>
+ <string name="error_downloading_image">Error al descargar la imagen</string>
+ <string name="passcode_wrong_passcode">PIN erróneo</string>
+ <string name="invalid_email_message">Tu dirección de correo electrónico no es válida</string>
+ <string name="invalid_password_message">La contraseña debe contener al menos 4 caracteres</string>
+ <string name="invalid_username_too_short">Nombre de usuario debe ser superior a 4 caracteres</string>
+ <string name="invalid_username_too_long">Nombre de usuario debe ser inferior a 61 caracteres</string>
+ <string name="username_only_lowercase_letters_and_numbers">Nombre de usuario solo puede contener letras minúsculas (a-z) y números</string>
+ <string name="username_required">Ingresa un nombre de usuario</string>
+ <string name="username_not_allowed">Nombre de usuario no permitido</string>
+ <string name="username_must_be_at_least_four_characters">El nombre de usuario debe tener, al menos, 4 caracteres</string>
+ <string name="username_contains_invalid_characters">El nombre de usuario no puede incluir el caracter “_”</string>
+ <string name="username_must_include_letters">El nombre de usuario debe tener, al menos, 1 letra (a-z)</string>
+ <string name="email_invalid">Ingresa una dirección de correo electrónico válida</string>
+ <string name="email_not_allowed">Esa dirección de correo electrónico no está permitida</string>
+ <string name="username_exists">Ese nombre de usuario ya existe</string>
+ <string name="email_exists">Esa dirección de correo electrónico ya está siendo usada</string>
+ <string name="username_reserved_but_may_be_available">Ese nombre de usuario actualmente está reservado pero puede estar disponible en un par de días</string>
+ <string name="blog_name_required">Ingresa una dirección para tu sitio</string>
+ <string name="blog_name_not_allowed">Esa dirección de sitio no está disponible</string>
+ <string name="blog_name_must_be_at_least_four_characters">La dirección del sitio debe tener al menos 4 caracteres</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">La dirección del sitio debe ser más corta que 64 caracteres</string>
+ <string name="blog_name_contains_invalid_characters">La dirección del sitio no puede tener el caracter “_”</string>
+ <string name="blog_name_cant_be_used">No puedes usar esa dirección de sitio</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">La dirección solo puede contener letras minúsculas (a-z) y números.</string>
+ <string name="blog_name_exists">Ese sitio ya existe</string>
+ <string name="blog_name_reserved">Ese sitio está reservado</string>
+ <string name="blog_name_reserved_but_may_be_available">Este sitio está reservado actualmente pero es posible que quede libre en un par de días.</string>
+ <string name="username_or_password_incorrect">El usuario o contraseña que ingresaste no son correctos.</string>
+ <string name="nux_cannot_log_in">No se pudo acceder</string>
+ <string name="could_not_remove_account">No se ha podido eliminar el sitio</string>
+ <string name="invalid_url_message">Comprueba que la URL introducida sea válida</string>
+ <string name="xmlrpc_error">No se pudo conectar. Ingresa la ruta completa a xmlrpc.php en tu sitio e intenta de nuevo.</string>
+ <string name="select_categories">Seleccionar categorías</string>
+ <string name="account_details">Detalles de la cuenta</string>
+ <string name="edit_post">Editar entrada</string>
+ <string name="add_comment">Añadir comentario</string>
+ <string name="connection_error">Error de conexión</string>
+ <string name="cancel_edit">Cancelar edición</string>
+ <string name="scaled_image_error">Introduce un valor válido de ancho a escala</string>
+ <string name="post_not_found">Ocurrió un error al cargar la entrada. Actualiza tus entradas e intenta nuevamente.</string>
+ <string name="learn_more">Aprender más</string>
+ <string name="media_gallery_settings_title">Ajustes de galería</string>
+ <string name="media_gallery_image_order">Orden de imágenes</string>
+ <string name="media_gallery_num_columns">Número de columnas</string>
+ <string name="media_gallery_type_thumbnail_grid">Malla (red) de miniaturas</string>
+ <string name="media_gallery_edit">Editar galería</string>
+ <string name="media_error_no_permission">No tienes permiso para ver la librería multimedia</string>
+ <string name="cannot_delete_multi_media_items">Algunos elementos multimedia no pudieron ser borrados. Prueba más tarde</string>
+ <string name="themes_live_preview">Vista previa</string>
+ <string name="theme_current_theme">Tema actual</string>
+ <string name="theme_premium_theme">Tema premium</string>
+ <string name="link_enter_url_text">Texto del enlace (opcional)</string>
+ <string name="create_a_link">Crear un enlace</string>
+ <string name="page_settings">Ajustes de página</string>
+ <string name="local_draft">Borrador local</string>
+ <string name="upload_failed">Falló la subida</string>
+ <string name="horizontal_alignment">Alineación horizontal</string>
+ <string name="file_not_found">No ha sido posible encontrar el archivo multimedia para cargar. ¿Se ha borrado o cambiado de ubicación?</string>
+ <string name="post_settings">Ajustes de entrada</string>
+ <string name="delete_post">Borrar entrada</string>
+ <string name="delete_page">Borrar página</string>
+ <string name="comment_status_approved">Aprobado</string>
+ <string name="comment_status_unapproved">Pendiente</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">En la papelera</string>
+ <string name="edit_comment">Editar comentario</string>
+ <string name="mnu_comment_approve">Aprobar</string>
+ <string name="mnu_comment_unapprove">Rechazar</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Enviar a la papelera</string>
+ <string name="dlg_approving_comments">Aprobando</string>
+ <string name="dlg_unapproving_comments">Rechazando</string>
+ <string name="dlg_spamming_comments">Marcando como spam</string>
+ <string name="dlg_trashing_comments">Enviando a la papelera</string>
+ <string name="dlg_confirm_trash_comments">¿Enviar a la papelera?</string>
+ <string name="trash_yes">Enviar a la papelera</string>
+ <string name="trash_no">No enviar a la papelera</string>
+ <string name="trash">Papelera</string>
+ <string name="author_name">Nombre del autor</string>
+ <string name="author_email">Email del autor</string>
+ <string name="author_url">URL del autor</string>
+ <string name="hint_comment_content">Comentario</string>
+ <string name="saving_changes">Guardando cambios</string>
+ <string name="sure_to_cancel_edit_comment">¿Cancelar la edición de este comentario?</string>
+ <string name="content_required">Comentario obligatorio</string>
+ <string name="toast_comment_unedited">El comentario no ha cambiado</string>
+ <string name="delete_draft">Eliminar borrador</string>
+ <string name="preview_page">Vista previa de página</string>
+ <string name="preview_post">Vista previa de entrada</string>
+ <string name="comment_added">Comentado añadido con éxito</string>
+ <string name="post_not_published">No se ha publicado el estatus de la entrada</string>
+ <string name="page_not_published">No se ha publicado el estatus de la página</string>
+ <string name="view_in_browser">Ver en el navegador</string>
+ <string name="add_new_category">Añadir nueva categoría</string>
+ <string name="category_name">Nombre de la categoría</string>
+ <string name="category_slug">Slug de la categoría (opcional)</string>
+ <string name="category_desc">Descripción de la categoría (opcional)</string>
+ <string name="category_parent">Categoría padre (opcional):</string>
+ <string name="share_action_post">Nueva entrada</string>
+ <string name="share_action_media">Librería multimedia</string>
+ <string name="file_error_create">No se pudo crear un archivo temporal para subir el archivo multimedia. Asegúrate que haya suficiente espacio libre en tu dispositivo.</string>
+ <string name="location_not_found">Localización desconocida</string>
+ <string name="open_source_licenses">Licencias Open source</string>
+ <string name="pending_review">Pendiente de revisión</string>
+ <string name="http_credentials">Credenciales HTTP (opcional)</string>
+ <string name="http_authorization_required">Se necesita autorización</string>
+ <string name="post_format">Tipo de entrada</string>
+ <string name="new_post">Nueva entrada</string>
+ <string name="new_media">Nuevo elemento multimedia</string>
+ <string name="view_site">Ver sitio</string>
+ <string name="privacy_policy">Política de privacidad</string>
+ <string name="local_changes">Cambios locales</string>
+ <string name="image_settings">Ajustes de imagen</string>
+ <string name="add_account_blog_url">Dirección del blog</string>
+ <string name="wordpress_blog">Blog de WordPress</string>
+ <string name="error_blog_hidden">Este blog está oculto y no se puede cargar. Actívalo de nuevo en ajustes y prueba de nuevo.</string>
+ <string name="fatal_db_error">Hubo un error al crear la base de datos de la app. Por favor, intenta reinstalar la app. </string>
+ <string name="jetpack_message_not_admin">Se necesita el plugin Jetpack para tener estadísticas. Contacta con el administrador del sitio para activarlo.</string>
+ <string name="reader_title_applog">Registro de la aplicación</string>
+ <string name="reader_share_link">Compartir enlace</string>
+ <string name="reader_toast_err_add_tag">No se pudo añadir esta etiqueta</string>
+ <string name="required_field">Campo obligatorio</string>
+ <string name="email_hint">Correo electrónico</string>
+ <string name="site_address">La dirección (URL) de tu sitio autoalojado</string>
+ <string name="email_cant_be_used_to_signup">No puedes usar esta dirección de correo electrónico para registrarte, ya que bloquea algunos de nuestros correos. Utiliza otro proveedor de correo.</string>
+ <string name="email_reserved">Esta dirección de correo electrónico ya se ha utilizado. Comprueba si tienes en tu bandeja de entrada un correo de activación. Si no activas ahora el correo, puedes intentarlo dentro de unos días.</string>
+ <string name="blog_name_must_include_letters">La dirección de los sitios debe tener, al menos, 1 letra (a-z)</string>
+ <string name="blog_name_invalid">Dirección de sitio inválida</string>
+ <string name="blog_title_invalid">Título de sitio no válido</string>
+ <string name="reader_toast_err_remove_tag">No se pudo eliminar esta etiqueta</string>
+ <string name="notifications_empty_all">No hay notificaciones... todavía.</string>
+ <string name="remove_account">Eliminar sitio</string>
+ <string name="blog_removed_successfully">El sitio se ha eliminado correctamente</string>
+ <string name="invalid_site_url_message">Comprueba que la URL del sitio introducida es válida</string>
+ <string name="deleting_page">Borrando página</string>
+ <string name="deleting_post">Borrando entrada</string>
+ <string name="share_url_post">Compartir entrada</string>
+ <string name="share_url_page">Compartir página</string>
+ <string name="share_link">Compartir enlace</string>
+ <string name="creating_your_account">Creando tu cuenta</string>
+ <string name="creating_your_site">Creando tu sitio</string>
+ <string name="reader_empty_posts_in_tag_updating">Recuperando entradas...</string>
+ <string name="error_refresh_media">Algo ha ido mal mientras actualizábamos la biblioteca multimedia. Inténtalo más tarde.</string>
+ <string name="reader_likes_you_and_multi">A ti, y a %,d personas más les gusta esto</string>
+ <string name="reader_likes_multi">A %,d personas les gusta esto</string>
+ <string name="reader_label_reply">Responder</string>
+ <string name="video">Vídeo</string>
+ <string name="download">Descargando elemento multimedia</string>
+ <string name="cant_share_no_visible_blog">No se puede compartir en WordPress si no tienes un blog visible</string>
+ <string name="reader_toast_err_get_comment">No ha sido posible cargar este comentario</string>
+ <string name="comment_spammed">Comentado marcado como spam</string>
+ <string name="select_time">Elige tiempo</string>
+ <string name="reader_likes_you_and_one">A ti y a otra persona os gusta esto.</string>
+ <string name="select_date">Elegir fecha</string>
+ <string name="pick_photo">Elige una foto</string>
+ <string name="account_two_step_auth_enabled">Esta cuenta tiene activado el proceso de autentificación en dos pasos. Visita los ajustes de seguridad de WordPress.com y genera una contraseña específica para la aplicación.</string>
+ <string name="pick_video">Elige un vídeo</string>
+ <string name="reader_toast_err_get_post">No fue posible cargar esta entrada</string>
+ <string name="validating_user_data">Datos de validación del usuario</string>
+ <string name="validating_site_data">Validando datos del sitio</string>
+ <string name="reader_empty_followed_blogs_description">No te preocupes, ¡solo tienes que pulsar el icono para empezar a explorar!</string>
+ <string name="password_invalid">Necesitas una contraseña más segura. Asegúrate de utilizar 7 o más caracteres, mezclar letras mayúsculas y minúsculas, número y caracteres especiales.</string>
+ <string name="nux_tap_continue">Continuar</string>
+ <string name="nux_welcome_create_account">Crear cuenta</string>
+ <string name="nux_add_selfhosted_blog">Añade un sitio autoalojado</string>
+ <string name="nux_oops_not_selfhosted_blog">Regístrate en WordPress.com</string>
+ <string name="signing_in">Iniciando sesión...</string>
+ <string name="media_add_popup_title">Añadir a biblioteca multimedia</string>
+ <string name="media_add_new_media_gallery">Crear galería</string>
+ <string name="empty_list_default">Esta lista esta vacía</string>
+ <string name="select_from_media_library">Seleccionar de la biblioteca multimedia</string>
+ <string name="jetpack_message">Necesitas el pluging Jetpack para tener estadísticas estadísticas. ¿Quieres instalar Jetpack?</string>
+ <string name="jetpack_not_found">No se ha encontrado el plugin Jetpack</string>
+ <string name="reader_untitled_post">Sin título</string>
+ <string name="reader_share_subject">Compartido desde %s</string>
+ <string name="reader_btn_share">Compartir</string>
+ <string name="reader_btn_follow">Seguir</string>
+ <string name="reader_label_added_tag">Añadido %s</string>
+ <string name="reader_label_removed_tag">Eliminado %s</string>
+ <string name="reader_likes_one">A una persona le gusta esto</string>
+ <string name="reader_likes_only_you">Te gusta esto</string>
+ <string name="reader_toast_err_comment_failed">No se pudo publicar tu comentario </string>
+ <string name="reader_toast_err_tag_exists">Ya estás siguiendo esta etiqueta</string>
+ <string name="reader_toast_err_tag_invalid">Esta no es una etiqueta válida</string>
+ <string name="reader_toast_err_share_intent">Imposible compartir</string>
+ <string name="reader_toast_err_view_image">Imposible ver la imágen</string>
+ <string name="reader_toast_err_url_intent">Imposible abrir %s</string>
+ <string name="connecting_wpcom">Conectando a WordPress.com</string>
+ <string name="username_invalid">Nombre de usuario no válido</string>
+ <string name="limit_reached">Límite alcanzado. Puedes probar en 1 minuto. Probar de nuevo antes solamente incrementará el tiempo de espera hasta que la prohibición sea suprimida. Si crees que es un error, contacta con soporte. </string>
+ <string name="nux_tutorial_get_started_title">¡Empezar!</string>
+ <string name="reader_btn_unfollow">Siguiendo</string>
+ <string name="button_next">Seguiente</string>
+ <string name="reader_empty_followed_tags">No sigues ninguna etiqueta</string>
+ <string name="create_account_wpcom">Crear una cuenta de WordPress.com</string>
+ <string name="reader_hint_comment_on_comment">Contestar al comentario </string>
+ <string name="themes">Temas</string>
+ <string name="all">Todas</string>
+ <string name="images">Imágenes</string>
+ <string name="unattached">Sin adjuntar</string>
+ <string name="custom_date">Fecha personalizada</string>
+ <string name="media_add_popup_capture_photo">Tomar foto</string>
+ <string name="media_add_popup_capture_video">Tomar vídeo</string>
+ <string name="media_gallery_image_order_random">Aleatorio</string>
+ <string name="media_gallery_image_order_reverse">Inverso</string>
+ <string name="media_gallery_type">Tipo</string>
+ <string name="media_gallery_type_squares">Cuadrados</string>
+ <string name="media_gallery_type_tiled">Mosaico</string>
+ <string name="media_gallery_type_circles">Círculos</string>
+ <string name="media_gallery_type_slideshow">Pase de diapositivas </string>
+ <string name="media_edit_title_text">Título</string>
+ <string name="media_edit_caption_text">Leyenda</string>
+ <string name="media_edit_description_text">Descripción</string>
+ <string name="media_edit_title_hint">Escribir un título aquí</string>
+ <string name="media_edit_caption_hint">Escribir una leyenda aquí</string>
+ <string name="media_edit_description_hint">Escribe una descripción aquí</string>
+ <string name="media_edit_success">Actualizado</string>
+ <string name="media_edit_failure">No se pudo actualizar</string>
+ <string name="themes_details_label">Detalles</string>
+ <string name="themes_features_label">Características</string>
+ <string name="theme_activate_button">Activar</string>
+ <string name="theme_activating_button">Activando</string>
+ <string name="theme_set_success">¡Tema aplicado con exito!</string>
+ <string name="theme_auth_error_title">Error al recibir temas</string>
+ <string name="post_excerpt">Extracto</string>
+ <string name="share_action_title">Añadir a...</string>
+ <string name="share_action">Compartir</string>
+ <string name="stats">Estadísticas</string>
+ <string name="stats_view_visitors_and_views">Visitantes y Vistas</string>
+ <string name="stats_view_clicks">Clics</string>
+ <string name="stats_view_referrers">Referentes</string>
+ <string name="stats_timeframe_today">Hoy</string>
+ <string name="stats_timeframe_yesterday">Ayer</string>
+ <string name="stats_timeframe_days">Días</string>
+ <string name="stats_timeframe_weeks">Semanas</string>
+ <string name="stats_timeframe_months">Meses</string>
+ <string name="stats_entry_country">País</string>
+ <string name="stats_entry_posts_and_pages">Título</string>
+ <string name="stats_entry_tags_and_categories">Tema</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Referencias</string>
+ <string name="stats_totals_views">Vistas</string>
+ <string name="stats_totals_clicks">Clics</string>
+ <string name="stats_totals_plays">Juegos</string>
+ <string name="passcode_manage">Administrar el PIN de bloqueo</string>
+ <string name="passcode_enter_passcode">Introduce tu PIN</string>
+ <string name="passcode_enter_old_passcode">Introduce tu viejo PIN</string>
+ <string name="passcode_re_enter_passcode">Vuelve a introducir el PIN</string>
+ <string name="passcode_change_passcode">Cambiar PIN</string>
+ <string name="passcode_set">Introducir PIN</string>
+ <string name="passcode_preference_title">PIN bloqueado</string>
+ <string name="passcode_turn_off">Desbloquear PIN</string>
+ <string name="passcode_turn_on">Bloquear PIN</string>
+ <string name="stats_view_tags_and_categories">Etiquetas y categorías</string>
+ <string name="upload">Subir</string>
+ <string name="sign_in">Acceder</string>
+ <string name="notifications">Notificaciones</string>
+ <string name="note_reply_successful">Respuesta publicada</string>
+ <string name="new_notifications">%d nuevas notificaciones</string>
+ <string name="more_notifications">y %d más.</string>
+ <string name="follows">Seguimientos</string>
+ <string name="loading">Cargando...</string>
+ <string name="httpuser">Usuario HTTP</string>
+ <string name="httppassword">Contraseña HTTP</string>
+ <string name="error_media_upload">Se ha producido un error al cargar los archivos</string>
+ <string name="post_content">Contenido (pulsa para añadir texto y multimedia)</string>
+ <string name="publish_date">Publicar</string>
+ <string name="content_description_add_media">Añadir media</string>
+ <string name="incorrect_credentials">Nombre de usuario o contraseña incorrecta</string>
+ <string name="password">Contraseña</string>
+ <string name="username">Nombre de usuario</string>
+ <string name="reader">Lector</string>
+ <string name="featured">Usar como imagen destacada</string>
+ <string name="featured_in_post">Incluír imagen en el contenido del mensaje</string>
+ <string name="no_network_title">No hay red disponible</string>
+ <string name="pages">Páginas</string>
+ <string name="caption">Leyenda (opcional)</string>
+ <string name="width">Ancho</string>
+ <string name="posts">Entradas</string>
+ <string name="anonymous">Anónimo</string>
+ <string name="page">Página</string>
+ <string name="post">Entrada</string>
+ <string name="blogusername">Usuario del blog</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Subir y enlazar a la imagen escalada</string>
+ <string name="scaled_image">Ancho de la imagen en escala</string>
+ <string name="scheduled">Programado</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Subiendo</string>
+ <string name="version">Versión</string>
+ <string name="tos">Términos del servicio</string>
+ <string name="app_title">WordPress para Android</string>
+ <string name="max_thumbnail_px_width">Ancho predeterminado de la imagen </string>
+ <string name="image_alignment">Alineamiento</string>
+ <string name="refresh">Refrescar</string>
+ <string name="untitled">Sin Título</string>
+ <string name="edit">Editar</string>
+ <string name="post_id">Entrada</string>
+ <string name="page_id">Página</string>
+ <string name="post_password">Contraseña (opcional)</string>
+ <string name="immediately">Inmediatamente</string>
+ <string name="quickpress_add_alert_title">Nombre del Acceso Directo</string>
+ <string name="settings">Ajustes</string>
+ <string name="today">Hoy</string>
+ <string name="share_url">Compartir URL</string>
+ <string name="quickpress_window_title">Selecciona un blog para el acceso directo a la Publicación Rápida</string>
+ <string name="quickpress_add_error">El nombre del acceso directo no puede ser vacío.</string>
+ <string name="publish_post">Publicar</string>
+ <string name="draft">Borrador</string>
+ <string name="post_private">Privado</string>
+ <string name="upload_full_size_image">Subir y enlazar a la imagen completa.</string>
+ <string name="title">Título</string>
+ <string name="tags_separate_with_commas">Etiquetas (separa las etiquetas con comas)</string>
+ <string name="categories">Categorías</string>
+ <string name="dlg_deleting_comments">Eliminando comentarios</string>
+ <string name="notification_blink">Notificación luminosa</string>
+ <string name="notification_sound">Notificación sonora</string>
+ <string name="notification_vibrate">Vibrar</string>
+ <string name="status">Estado</string>
+ <string name="location">Ubicación</string>
+ <string name="sdcard_title">Requiere tarjeta SD</string>
+ <string name="select_video">Selecciona un video de la galería</string>
+ <string name="media">Multimedia</string>
+ <string name="delete">Eliminar</string>
+ <string name="none">Ninguno</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Selecciona una foto de la galería</string>
+ <string name="error">Error</string>
+ <string name="cancel">Cancelar</string>
+ <string name="save">Guardar</string>
+ <string name="add">Añadir</string>
+ <string name="category_refresh_error">Error de actualización de categorías</string>
+ <string name="preview">Vista previa</string>
+ <string name="on">en</string>
+ <string name="reply">Responder</string>
+ <string name="yes">Sí</string>
+ <string name="no">No</string>
+ <string name="notification_settings">Ajustes de avisos</string>
+</resources>
diff --git a/WordPress/src/main/res/values-es/strings.xml b/WordPress/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..e28c99028
--- /dev/null
+++ b/WordPress/src/main/res/values-es/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrador</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Colaborador</string>
+ <string name="role_follower">Seguidor</string>
+ <string name="role_viewer">Espectador</string>
+ <string name="error_post_my_profile_no_connection">Sin conexión, no se pudo guardar tu perfil</string>
+ <string name="alignment_none">Ninguna</string>
+ <string name="alignment_left">Izquierda</string>
+ <string name="alignment_right">Derecha</string>
+ <string name="site_settings_list_editor_action_mode_title">Elegido %1$d</string>
+ <string name="error_fetch_users_list">No se pudieron recuperar los usuarios del sitio</string>
+ <string name="plans_manage">Gestiona tu plan en\nWordPress.com/plans</string>
+ <string name="title_follower">Seguidor</string>
+ <string name="title_email_follower">Correo electrónico del seguidor</string>
+ <string name="people_empty_list_filtered_viewers">Todavía no tienes ningún espectador.</string>
+ <string name="people_fetching">Recuperando usuarios…</string>
+ <string name="people_empty_list_filtered_email_followers">Todavía no tienes el correno electrónico de ningún seguidor.</string>
+ <string name="people_empty_list_filtered_followers">Todavía no tienes ningún seguidor.</string>
+ <string name="people_empty_list_filtered_users">Todavía no tienes ningún usuario.</string>
+ <string name="people_dropdown_item_email_followers">Suscriptores por correo electrónico</string>
+ <string name="people_dropdown_item_viewers">Espectadores</string>
+ <string name="people_dropdown_item_followers">Seguidores</string>
+ <string name="people_dropdown_item_team">Equipo</string>
+ <string name="invite_message_usernames_limit">Invita como máximo a 10 personas con sus correos electrónicos o nombre de usuarios de WordPress.com. A aquellos que necesiten un nombre de usuario se le enviará instrucciones sobre cómo hacerlo.</string>
+ <string name="viewer_remove_confirmation_message">Si eliminas a este espectador, no podrá visitar tu sitio\n\n¿Todavía quieres eliminar a este espectador?</string>
+ <string name="follower_remove_confirmation_message">Si lo eliminas, este seguidor dejará de recibir informaciones de tu sitio, a no ser que vuelta a seguirte. \n\n¿Todavía quieres eliminar a este seguidor? </string>
+ <string name="follower_subscribed_since">Desde %1$s</string>
+ <string name="reader_label_view_gallery">Ver galería</string>
+ <string name="error_remove_follower">No se pudo quitar al seguidor</string>
+ <string name="error_remove_viewer">No se pudo quitar el espectador</string>
+ <string name="error_fetch_email_followers_list">No se pudieron recuperar los correos electrónicos de los seguidores del sitio</string>
+ <string name="error_fetch_followers_list">No se pudieron recuperar los seguidores del sitio</string>
+ <string name="editor_failed_uploads_switch_html">Algunas subidas de medios han fallado. Puedes cambiar al modo HTML \ncuando esto pasa. ¿Borramos todas las subidas fallidas y seguimos?</string>
+ <string name="format_bar_description_html">Modo HTML</string>
+ <string name="visual_editor">Editor visual</string>
+ <string name="image_thumbnail">Miniatura de la imagen</string>
+ <string name="format_bar_description_ul">Lista no ordenada</string>
+ <string name="format_bar_description_ol">Lista ordenada</string>
+ <string name="format_bar_description_more">Insertar más</string>
+ <string name="format_bar_description_media">Insertar archivo multimeda</string>
+ <string name="format_bar_description_link">Insertar enlace</string>
+ <string name="format_bar_description_quote">Cita</string>
+ <string name="format_bar_description_strike">Tachado</string>
+ <string name="format_bar_description_italic">Cursiva</string>
+ <string name="format_bar_description_underline">Subrayado</string>
+ <string name="image_settings_save_toast">Cambios guardados</string>
+ <string name="format_bar_description_bold">Negrita</string>
+ <string name="image_width">Ancho</string>
+ <string name="image_caption">Leyenda</string>
+ <string name="image_alt_text">Texto alternativo</string>
+ <string name="image_link_to">Enlazado a</string>
+ <string name="image_settings_dismiss_dialog_title">¿Descartar cambios sin guardar?</string>
+ <string name="stop_upload_button">Para la subida</string>
+ <string name="stop_upload_dialog_title">¿Parar la subida?</string>
+ <string name="alert_error_adding_media">Hubo un error al insertar el archivo multimedia</string>
+ <string name="alert_action_while_uploading">Ahora mismo estás subiendo medios. Por favor, espera hasta que termine.</string>
+ <string name="alert_insert_image_html_mode">No se pueden insertar medios directamente en el modo HTML. Por favor, cambia al modo visual.</string>
+ <string name="uploading_gallery_placeholder">Subiendo galería...</string>
+ <string name="invite_sent">Invitación enviada con éxito</string>
+ <string name="invite_error_some_failed">¡Se han enviado las invitaciones pero ha habido errores!</string>
+ <string name="tap_to_try_again">¡Toca para probar de nuevo!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">¡Hubo un error al tratar de enviar la invitación!</string>
+ <string name="invite_error_invalid_usernames_multiple">No se pudo enviar: Hay nombres de usuario o correos electrónicos no válidos.</string>
+ <string name="invite_error_invalid_usernames_one">No se pudo enviar: Un nombre de usuario o correo electrónico no es válido</string>
+ <string name="invite_error_no_usernames">Por favor, añade al menos un nombre de usuario</string>
+ <string name="invite_message_info">(Opcional) Puedes introducir un mensaje personalizado de hasta 500 caracteres que se incluirá en la invitación.</string>
+ <string name="invite_message_remaining_other">%d caracteres restantes</string>
+ <string name="invite_message_remaining_one">Queda 1 carácter</string>
+ <string name="invite_message_remaining_zero">Quedan 0 caracteres</string>
+ <string name="invite_message_title">Mensaje personalizado</string>
+ <string name="invite_invalid_email">La dirección de correo electrónico \'%s\' no es válida</string>
+ <string name="invite_already_a_member">Ya existe un miembro con el nombre de usuario \'%s\'</string>
+ <string name="invite_username_not_found">No se han encontrado usuarios con el nombre \'%s\'</string>
+ <string name="invite">Invitar</string>
+ <string name="invite_names_title">Nombres de usuario o correos electrónicos</string>
+ <string name="send_link">Enviar enlace</string>
+ <string name="invite_people">Invitar gente</string>
+ <string name="signup_succeed_signin_failed">Tu cuenta se ha creado pero ocurrió un error mientras te dábamos acceso \n \nTrata de acceder con tu usuario y contraseña recién creados.</string>
+ <string name="my_site_header_external">Externo</string>
+ <string name="label_clear_search_history">Borrar historial de búsqueda</string>
+ <string name="dlg_confirm_clear_search_history">¿Borrar historial de búsqueda?</string>
+ <string name="reader_empty_posts_in_search_description">No se han encontrado entradas con %s para tu idioma</string>
+ <string name="reader_label_post_search_running">Buscando...</string>
+ <string name="reader_empty_posts_in_search_title">No se han encontrado entradas</string>
+ <string name="reader_label_related_posts">Lecturas relacionadas</string>
+ <string name="reader_label_post_search_explainer">Buscar en todos los blogs públicos de WordPress.com</string>
+ <string name="reader_hint_post_search">Buscar en WordPress.com</string>
+ <string name="reader_title_related_post_detail">Entrada relacionada</string>
+ <string name="reader_title_search_results">Buscar %s</string>
+ <string name="preview_screen_links_disabled">Los enlaces están inhabilitados en la pantalla de vista previa</string>
+ <string name="draft_explainer">Esta entrada es un borrador que aún no ha sido publicado</string>
+ <string name="send">Enviar</string>
+ <string name="user_remove_confirmation_message">Si eliminas %1$s, ese usuario ya no será capaz de acceder a este sitio, pero cualquier contenido que fuera creado por %1$s permanecerá en el sitio.\n\n¿Aún te gustaría eliminar este usuario?</string>
+ <string name="person_removed">\@%1$s eliminado correctamente.</string>
+ <string name="person_remove_confirmation_title">Eliminar %1$s</string>
+ <string name="people">Gente</string>
+ <string name="edit_user">Editar usuario</string>
+ <string name="role">Rol</string>
+ <string name="reader_empty_posts_in_custom_list">Los sitios de esta lista no han publicado nada últimamente.</string>
+ <string name="error_remove_user">No se pudo eliminar el usuario</string>
+ <string name="error_update_role">No se ha podido actualizar el rol del usuario</string>
+ <string name="error_fetch_viewers_list">No se pudieron recuperar los espectadores del sitio</string>
+ <string name="gravatar_camera_and_media_permission_required">Permisos necesarios para seleccionar o capturar una imagen</string>
+ <string name="error_updating_gravatar">Error al subir tu Gravatar</string>
+ <string name="error_locating_image">Error al localizar la imagen recortada</string>
+ <string name="error_refreshing_gravatar">Error al recargar tu Gravatar</string>
+ <string name="gravatar_tip">¡Nuevo! ¡Haz clic en tu Gravatar para cambiarlo!</string>
+ <string name="error_cropping_image">Error al recortar la imagen</string>
+ <string name="launch_your_email_app">Abre tu app de correo electrónico</string>
+ <string name="checking_email">Comprobando correo electrónico</string>
+ <string name="not_on_wordpress_com">¿No estás en WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Actualmente no disponible. Por favor, introduce tu contraseña</string>
+ <string name="check_your_email">Comprueba tu correo electrónico</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Haz que se envíe un enlace a tu correo electrónico inmediatamente</string>
+ <string name="logging_in">Accediendo</string>
+ <string name="enter_your_password_instead">Introduce tu contraseña</string>
+ <string name="web_address_dialog_hint">Cuando comentes se hará público</string>
+ <string name="jetpack_not_connected_message">El plugin Jetpack se ha instalado, pero no se ha conectado con WordPress.com. ¿Quieres conectar Jetpack?</string>
+ <string name="username_email">Correo electrónico o nombre de usuario</string>
+ <string name="jetpack_not_connected">Plugin Jetpack no conectado</string>
+ <string name="new_editor_reflection_error">Editor visual no compatible con tu dispositivo. Se ha\ndesactivado automáticamente</string>
+ <string name="stats_insights_latest_post_no_title">(sin título)</string>
+ <string name="capture_or_pick_photo">Captura o elige imagen</string>
+ <string name="plans_post_purchase_button_themes">Ver Temas</string>
+ <string name="plans_post_purchase_text_themes">Ahora tienes acceso ilimitado a temas Premium. Haz una vista previa de cualquier tema en tu sitio para comenzar</string>
+ <string name="plans_post_purchase_title_themes">Encuentra un tema premium perfecto</string>
+ <string name="plans_post_purchase_button_video">Comenzar una entrada nueva</string>
+ <string name="plans_post_purchase_text_video">Puedes subir y alojar vídeos en tu sitio con VideoPress y tu almacenamiento extendido de medios.</string>
+ <string name="plans_post_purchase_title_video">Dale vida a tus entradas con un vídeo.</string>
+ <string name="plans_post_purchase_button_customize">Personalizar mi sitio</string>
+ <string name="plans_post_purchase_text_customize">Ahora tienes acceso a fuentes personalizadas, colores personalizados, y permisos de edición de CSS personalizado.</string>
+ <string name="plans_post_purchase_title_customize">Personalizar fuentes y colores</string>
+ <string name="plans_post_purchase_text_intro">¡Tu sitio está dando saltos de la emoción! Ahora explora las nuevas características de tu sitio y elige por dónde quieres empezar. </string>
+ <string name="plans_post_purchase_title_intro">Todo tuyo. ¡Adelante!</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Planes</string>
+ <string name="export_your_content_message">Tus entradas, páginas y ajustes te serán enviadas por correo electrónico a %s.</string>
+ <string name="plans_loading_error">No se pueden cargar los planes</string>
+ <string name="export_your_content">Exportar tu contenido</string>
+ <string name="exporting_content_progress">Exportando contenido...</string>
+ <string name="export_email_sent">¡Correo electrónico de exportación enviado!</string>
+ <string name="show_purchases">Mostrar compras</string>
+ <string name="premium_upgrades_message">Tienes mejoras premium en tu sitio. Por favor, cancela tus mejoras antes de eliminar tu sitio. </string>
+ <string name="checking_purchases">Comprobando compras</string>
+ <string name="premium_upgrades_title">Mejoras premium</string>
+ <string name="purchases_request_error">Algo fue mal. No se pudo realizar la compra.</string>
+ <string name="delete_site_progress">Borrando sitio...</string>
+ <string name="delete_site_summary">Esta acción no se puede deshacer. Eliminar tu sitio eliminará todo el contenido, contribuidores y dominios de tu sitio.</string>
+ <string name="delete_site_hint">Eliminar el sitio</string>
+ <string name="export_site_hint">Exportar tu sitio a un archivo XML</string>
+ <string name="are_you_sure">¿Estás seguro?</string>
+ <string name="export_site_summary">Si estás seguro, por favor asegúrate de invertir tiempo en exportar tu contenido ahora. No podrá ser recuperado en el futuro.</string>
+ <string name="keep_your_content">Manten tu contenido</string>
+ <string name="domain_removal_hint">Una vez que hayas eliminado tu sitio, los dominios no funcionarán.</string>
+ <string name="domain_removal_summary">¡Ten cuidado! La eliminación de su sitio también eliminará su dominio(s) enumerado(s) a continuación.</string>
+ <string name="primary_domain">Dominio principal</string>
+ <string name="domain_removal">Eliminación de Dominio</string>
+ <string name="error_deleting_site_summary">Hubo un error eliminando tu sitio. Por favor, contacta con el soporte para más asistencia</string>
+ <string name="error_deleting_site">Error borrando sitio</string>
+ <string name="site_settings_export_content_title">Exportar contenido</string>
+ <string name="confirm_delete_site_prompt">Por favor, escribe en %1$s en el campo arriba para confirmar. Tu sitio desaparecerá para siempre.</string>
+ <string name="confirm_delete_site">Confirmar borrado del sitio</string>
+ <string name="contact_support">Contactar con el soporte</string>
+ <string name="start_over_text">Si quieres un sitio, pero no quieres ninguna de las entradas y las páginas que tiene ahora, nuestro equipo de soporte puede borrar sus mensajes, páginas, archivos multimedia y tus comentarios.\n\nEsto mantendrá su sitio y la URL activos, pero tendrás un nuevo comienzo en la creación de contenidos. Sólo tienes que contactar con nosotros para limpiar tu contenido actual..</string>
+ <string name="let_us_help">Déjanos ayudarte</string>
+ <string name="site_settings_start_over_hint">Comienza tu sitio encima</string>
+ <string name="me_btn_app_settings">Ajustes de la app</string>
+ <string name="start_over">Comenzar de nuevo</string>
+ <string name="editor_remove_failed_uploads">Eliminar subidas fallidas</string>
+ <string name="editor_toast_failed_uploads">La subida de algunos archivos multimedia han fallado. No se puede guardar o publicar\n tu entrada tiene este estado. ¿Quieres eliminar todos los archivos multimedia fallidos?</string>
+ <string name="site_settings_advanced_header">Avanzado</string>
+ <string name="comments_empty_list_filtered_trashed">No hay comentarios en la papelera</string>
+ <string name="comments_empty_list_filtered_pending">No hay comentarios pendientes</string>
+ <string name="comments_empty_list_filtered_approved">No hay comentarios aprobados</string>
+ <string name="button_done">Hecho</string>
+ <string name="button_skip">Saltar</string>
+ <string name="site_timeout_error">No se pudo conectar con WordPress debido a un error de Timeout.</string>
+ <string name="xmlrpc_malformed_response_error">No se pudo conectar. La instalación de WordPress está respondiendo con un documento XML-RPC no válido.</string>
+ <string name="xmlrpc_missing_method_error">No se pudo conectar. Los métodos requeridos en el XML-RPC faltan en el servidor.</string>
+ <string name="post_format_status">Estado</string>
+ <string name="post_format_video">Video</string>
+ <string name="theme_all">Todo</string>
+ <string name="theme_premium">Premium</string>
+ <string name="theme_free">Gratis</string>
+ <string name="alignment_center">Centrado</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Galería</string>
+ <string name="post_format_image">Imagen</string>
+ <string name="post_format_link">Enlace</string>
+ <string name="post_format_quote">Cita</string>
+ <string name="post_format_standard">Estándar</string>
+ <string name="notif_events">Información sobre cursos y eventos de WordPress.com (online y presenciales).</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="post_format_aside">Minientrada</string>
+ <string name="notif_surveys">Oportunidades para participar en investigaciones y encuestas en WordPress.</string>
+ <string name="notif_tips">Consejos para sacar el máximo partido a WordPress.com</string>
+ <string name="notif_community">Comunidad</string>
+ <string name="replies_to_my_comments">Respuestas a mis comentarios</string>
+ <string name="notif_suggestions">Sugerencias</string>
+ <string name="notif_research">Investigación</string>
+ <string name="site_achievements">Logros del sitio</string>
+ <string name="username_mentions">Menciones del nombre de usuario</string>
+ <string name="likes_on_my_posts">"Me gusta" en mis entradas</string>
+ <string name="site_follows">Seguidores del sitio</string>
+ <string name="likes_on_my_comments">"Me gusta" en mis comentarios</string>
+ <string name="comments_on_my_site">Comentarios en mi sitio</string>
+ <string name="site_settings_list_editor_summary_other">%d elementos</string>
+ <string name="site_settings_list_editor_summary_one">1 elemento</string>
+ <string name="approve_auto_if_previously_approved">Comentarios de usuarios conocidos</string>
+ <string name="approve_auto">Todos los usuarios</string>
+ <string name="approve_manual">Sin comentarios</string>
+ <string name="site_settings_paging_summary_other">%d comentarios por página</string>
+ <string name="site_settings_paging_summary_one">1 comentario por página</string>
+ <string name="site_settings_multiple_links_summary_other">Se necesita aprobación para más de %d enlaces</string>
+ <string name="site_settings_multiple_links_summary_one">Se necesita aprobación para más de 1 enlace</string>
+ <string name="site_settings_multiple_links_summary_zero">Se requiere aprobación para más de 0 enlaces</string>
+ <string name="detail_approve_auto">Aprobar automáticamente los comentarios de todo el mundo.</string>
+ <string name="detail_approve_auto_if_previously_approved">Aprobar automáticamente si el usuario tiene un comentario previamente aprobado</string>
+ <string name="detail_approve_manual">Se requiere aprobación manual de los comentarios de todos.</string>
+ <string name="days_quantity_one">1 día</string>
+ <string name="days_quantity_other">%d días</string>
+ <string name="filter_trashed_posts">En la papelera</string>
+ <string name="filter_published_posts">Publicados</string>
+ <string name="filter_draft_posts">Borradores</string>
+ <string name="filter_scheduled_posts">Planificados</string>
+ <string name="primary_site">Sitio principal</string>
+ <string name="web_address">Dirección web</string>
+ <string name="pending_email_change_snackbar">Haz click en el enlace de verificación del correo electrónica enviado a %1$s para confirmar tu nueva dirección</string>
+ <string name="editor_toast_uploading_please_wait">Actualmente estás subiendo archivos multimedia. Por favor, espera hasta que se complete.</string>
+ <string name="error_refresh_comments_showing_older">No se pudieron actualizar los comentarios ahora mismo - se muestran comentarios antiguos</string>
+ <string name="editor_post_settings_set_featured_image">Fijar la imagen destacada</string>
+ <string name="editor_post_settings_featured_image">Imagen destacada</string>
+ <string name="new_editor_promo_desc">La app de WordPress para Android ahora incluye un nuevo editor visual\nPruébalo creando una nueva entrada.</string>
+ <string name="new_editor_promo_title">El nuevo editor</string>
+ <string name="new_editor_promo_button_label">¡Genial, gracias!</string>
+ <string name="visual_editor_enabled">Editor visual activado</string>
+ <string name="editor_content_placeholder">Comparte tu historia aquí...</string>
+ <string name="editor_page_title_placeholder">Título página</string>
+ <string name="editor_post_title_placeholder">Título entrada</string>
+ <string name="email_address">Correo electrónico</string>
+ <string name="preference_show_visual_editor">Mostrar editor visual</string>
+ <string name="dlg_sure_to_delete_comments">¿Eliminar de forma permanente estos comentarios?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">¿Eliminar de forma permanente este comentario?</string>
+ <string name="mnu_comment_delete_permanently">Eliminar</string>
+ <string name="comment_deleted_permanently">Comentario eliminado</string>
+ <string name="mnu_comment_untrash">Restaurar</string>
+ <string name="comments_empty_list_filtered_spam">Sin comentarios de spam</string>
+ <string name="could_not_load_page">No se pudo cargar la página</string>
+ <string name="comment_status_all">Todos</string>
+ <string name="interface_language">Idioma del interface</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">Sobre la app</string>
+ <string name="error_post_account_settings">No se pudieron guardar los ajustes de la cuenta</string>
+ <string name="error_post_my_profile">No se pudo guardar tu perfil</string>
+ <string name="error_fetch_account_settings">No se pudieron recuperar los ajustes de tu cuenta</string>
+ <string name="error_fetch_my_profile">No se pudo recuperar tu perfil</string>
+ <string name="stats_widget_promo_ok_btn_label">Bien, lo tenemos</string>
+ <string name="stats_widget_promo_desc">Añade el widget a tu pantalla de inicio para acceder a tus estadísticas en un clic.</string>
+ <string name="stats_widget_promo_title">Widget de estadísticas en portada</string>
+ <string name="site_settings_unknown_language_code_error">No se pudo reconocer el código del idioma</string>
+ <string name="site_settings_threading_dialog_description">Permite los comentarios anidados.</string>
+ <string name="site_settings_threading_dialog_header">Anidar hasta</string>
+ <string name="remove">Eliminar</string>
+ <string name="disabled">Desactivado</string>
+ <string name="search">Buscar</string>
+ <string name="add_category">Añadir categoría</string>
+ <string name="site_settings_image_original_size">Tamaño original</string>
+ <string name="privacy_private">Tu sitio es visible únicamente por ti y por lo usuarios que apruebes</string>
+ <string name="privacy_public_not_indexed">Tu sitio es visible para todos pero pide a los motores de búsqueda no ser indexado</string>
+ <string name="privacy_public">Tu sitio es visible para todos y puede ser indexado por motores de búsqueda</string>
+ <string name="about_me_hint">Algunas palabras sobre ti...</string>
+ <string name="about_me">Sobre mí</string>
+ <string name="public_display_name_hint">El nombre público mostrará por defecto el nombre de usuario si no está establecido</string>
+ <string name="public_display_name">Nombre que se mostrará públicamente</string>
+ <string name="my_profile">Mi perfil</string>
+ <string name="first_name">Nombre</string>
+ <string name="last_name">Apellido</string>
+ <string name="site_privacy_public_desc">Permitir a los motores de búsqueda indexar este sitio</string>
+ <string name="site_privacy_hidden_desc">Disuadir a los motores de búsqueda de indexar este sitio</string>
+ <string name="site_privacy_private_desc">Me gustaría que mi sitio fuese privado, visible únicamente a los usuarios que yo elija</string>
+ <string name="cd_related_post_preview_image">Imagen de vista previa de entradas relacionadas</string>
+ <string name="error_post_remote_site_settings">No pudo guardar la información del sitio</string>
+ <string name="error_fetch_remote_site_settings">No pudo recuperar la información del sitio</string>
+ <string name="error_media_upload_connection">Se ha producido un error de conexión mientras subía multimedia</string>
+ <string name="site_settings_disconnected_toast">Desconectado, la edición se ha desactivado</string>
+ <string name="site_settings_unsupported_version_error">Versión de WordPress no soportada</string>
+ <string name="site_settings_multiple_links_dialog_description">Requiere aprobación para los comentarios que incluyen más de este número de enlaces.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Cierra automáticamente</string>
+ <string name="site_settings_close_after_dialog_description">Cierra automáticamente los comentarios en artículos</string>
+ <string name="site_settings_paging_dialog_description">Rompe los hilos de comentarios en múltiples páginas</string>
+ <string name="site_settings_paging_dialog_header">Comentarios por página</string>
+ <string name="site_settings_close_after_dialog_title">Cerrar los comentarios</string>
+ <string name="site_settings_blacklist_description">Cuando un comentario contenga alguna de estas palabras en su contenido, nombre, URL, correo electrónico o IP, será marcado como spam. Puedes introducir palabras parciales, así "press" coincidirá con "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">Cuando un comentario contenga alguna de las estas palabras en su contenido, nombre, URL, correo electrónico o IP, será puesto en la cola para moderar. Puedes introducir palabras parciales, así "press" coincidirá como "Wordpress"</string>
+ <string name="site_settings_list_editor_input_hint">Introduce una palabra o frase</string>
+ <string name="site_settings_list_editor_no_items_text">Sin elementos</string>
+ <string name="site_settings_learn_more_caption">Puedes sobreescribir esta configuración para entradas individuales</string>
+ <string name="site_settings_rp_preview3_site">en "Mejora"</string>
+ <string name="site_settings_rp_preview3_title">Novedades: VideoPress para bodas</string>
+ <string name="site_settings_rp_preview2_site">en "Aplicaciones"</string>
+ <string name="site_settings_rp_preview2_title">La aplicación WordPress para Android consigue un gran lavado de cara</string>
+ <string name="site_settings_rp_preview1_site">en "Móvil"</string>
+ <string name="site_settings_rp_preview1_title">Gran actualización disponible ahora para iPhone/iPad</string>
+ <string name="site_settings_rp_show_images_title">Mostrar imágenes</string>
+ <string name="site_settings_rp_show_header_title">Mostrar cabecera</string>
+ <string name="site_settings_rp_switch_summary">Las entradas relacionadas muestran contenido relevante de tu sitio debajo de las entradas.</string>
+ <string name="site_settings_rp_switch_title">Mostrar entradas relacionadas</string>
+ <string name="site_settings_delete_site_hint">Elimina los datos de tu sitio de la aplicación</string>
+ <string name="site_settings_blacklist_hint">Los comentarios que coinciden con un filtro, son marcados como spam</string>
+ <string name="site_settings_moderation_hold_hint">Los comentarios que coinciden con un filtro, son puestos en la cola para moderar</string>
+ <string name="site_settings_multiple_links_hint">Ignorar límite de enlaces de usuarios conocidos</string>
+ <string name="site_settings_whitelist_hint">El autor de un comentario debe tener aprobado un comentario previo</string>
+ <string name="site_settings_user_account_required_hint">Los usuarios deben estar registrados e iniciar sesión para comentar</string>
+ <string name="site_settings_identity_required_hint">El autor de un comentario debe rellenar el nombre y correo eléctronico</string>
+ <string name="site_settings_manual_approval_hint">Los comentarios deben aprobarse manualmente</string>
+ <string name="site_settings_paging_hint">Mostrar los comentarios en pedazos de un tamaño especificado</string>
+ <string name="site_settings_threading_hint">Permitir comentarios anidados hasta cierto nivel</string>
+ <string name="site_settings_sort_by_hint">Determina el orden de los comentarios mostrados</string>
+ <string name="site_settings_close_after_hint">No permitir los comentarios después de un tiempo</string>
+ <string name="site_settings_receive_pingbacks_hint">Permitir notificaciones de enlace de otros sitios</string>
+ <string name="site_settings_send_pingbacks_hint">Intenta notificar cualquier sitio enlazado desde el artículo</string>
+ <string name="site_settings_allow_comments_hint">Permitir a los lectores publicar comentarios</string>
+ <string name="site_settings_discussion_hint">Ver y cambiar la configuración de discusiones de tu sitio</string>
+ <string name="site_settings_more_hint">Ver todos los ajustes disponibles de las discusiones</string>
+ <string name="site_settings_related_posts_hint">Mostrar u ocultar entradas relacionadas en el lector</string>
+ <string name="site_settings_upload_and_link_image_hint">Habilitar que siempre suba la imagen a tamaño completo</string>
+ <string name="site_settings_image_width_hint">Redimensionar imágenes en las entradas a esta anchura</string>
+ <string name="site_settings_format_hint">Establece formato para nuevas entradas</string>
+ <string name="site_settings_category_hint">Establece categoría para nuevas entradas</string>
+ <string name="site_settings_location_hint">Añadir ubicación a tus entradas automáticamente</string>
+ <string name="site_settings_password_hint">Cambiar tu contraseña</string>
+ <string name="site_settings_username_hint">Cuenta del usuario actual</string>
+ <string name="site_settings_language_hint">Idioma en el que está escrito este blog</string>
+ <string name="site_settings_privacy_hint">Controla quien puede ver tu sitio</string>
+ <string name="site_settings_address_hint">El cambio de dirección no está soportado en este momento</string>
+ <string name="site_settings_tagline_hint">Una descripción corta o frase ingeniosa que describa tu blog</string>
+ <string name="site_settings_title_hint">En pocas palabras, explica sobre que es este sitio</string>
+ <string name="site_settings_whitelist_known_summary">Comentarios de usuarios conocidos</string>
+ <string name="site_settings_whitelist_all_summary">Comentarios de todos los usuarios</string>
+ <string name="site_settings_threading_summary">%d niveles</string>
+ <string name="site_settings_privacy_private_summary">Privado</string>
+ <string name="site_settings_privacy_hidden_summary">Oculto</string>
+ <string name="site_settings_privacy_public_summary">Público</string>
+ <string name="site_settings_delete_site_title">Borrar el sitio</string>
+ <string name="site_settings_blacklist_title">Lista negra</string>
+ <string name="site_settings_moderation_hold_title">Mantener para moderación</string>
+ <string name="site_settings_multiple_links_title">Enlaces en comentarios</string>
+ <string name="site_settings_whitelist_title">Aprobar automáticamente</string>
+ <string name="site_settings_paging_title">Paginación</string>
+ <string name="site_settings_threading_title">Comentarios anidados</string>
+ <string name="site_settings_sort_by_title">Ordenar por</string>
+ <string name="site_settings_account_required_title">Los usuarios deben iniciar sesión</string>
+ <string name="site_settings_identity_required_title">Deben ser incluidos el nombre y correo electrónico</string>
+ <string name="site_settings_receive_pingbacks_title">Recibe pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Envía pingbacks</string>
+ <string name="site_settings_allow_comments_title">Permitir comentarios</string>
+ <string name="site_settings_default_format_title">Formato por defecto</string>
+ <string name="site_settings_default_category_title">Categoría por defecto</string>
+ <string name="site_settings_location_title">Permitir la ubicación</string>
+ <string name="site_settings_address_title">Dirección</string>
+ <string name="site_settings_title_title">Título del sitio</string>
+ <string name="site_settings_tagline_title">Lema</string>
+ <string name="site_settings_this_device_header">Este dispositivo</string>
+ <string name="site_settings_discussion_new_posts_header">Por defecto para nuevas entradas</string>
+ <string name="site_settings_account_header">Cuenta</string>
+ <string name="site_settings_writing_header">Escribiendo</string>
+ <string name="site_settings_general_header">General</string>
+ <string name="newest_first">Nuevos primero</string>
+ <string name="comments">Comentarios</string>
+ <string name="privacy">Privacidad</string>
+ <string name="oldest_first">Antiguos primero</string>
+ <string name="discussion">Discusión</string>
+ <string name="related_posts">Entradas relacionadas</string>
+ <string name="close_after">Cerrar después de</string>
+ <string name="media_error_no_permission_upload">No tienes permisos para subir multimedia al sitio</string>
+ <string name="never">Nunca</string>
+ <string name="unknown">Desconocido</string>
+ <string name="reader_err_get_post_not_found">Esta entrada ya no existe</string>
+ <string name="reader_err_get_post_not_authorized">No estás autorizado a ver esta entrada</string>
+ <string name="reader_err_get_post_generic">No se pudo recuperar esta entrada</string>
+ <string name="blog_name_no_spaced_allowed">La dirección del sitio no puede tener espacios</string>
+ <string name="invalid_username_no_spaces">El nombre de usuario no puede tener espacios</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Los sitios que estás siguiendo no han publicado nada recientemente</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">No hay entradas recientes</string>
+ <string name="media_details_copy_url_toast">URL copiada al portapapeles</string>
+ <string name="edit_media">Editar multimedia</string>
+ <string name="media_details_copy_url">Copiar URL</string>
+ <string name="media_details_label_date_uploaded">Subido</string>
+ <string name="media_details_label_date_added">Añadido</string>
+ <string name="selected_theme">Tema seleccionado</string>
+ <string name="could_not_load_theme">No se ha podido cargar el tema</string>
+ <string name="theme_activation_error">Se ha producido un error. No se ha podido activar el tema</string>
+ <string name="theme_by_author_prompt_append"> de %1$s</string>
+ <string name="theme_prompt">Gracias por elegir %1$s</string>
+ <string name="theme_try_and_customize">Probar y personalizar</string>
+ <string name="theme_view">Vista</string>
+ <string name="theme_details">Detalles</string>
+ <string name="theme_support">Ayuda</string>
+ <string name="theme_done">HECHO</string>
+ <string name="theme_manage_site">GESTIONAR SITIO</string>
+ <string name="title_activity_theme_support">Temas</string>
+ <string name="theme_activate">Activar</string>
+ <string name="date_range_start_date">Fecha de inicio</string>
+ <string name="date_range_end_date">Fecha de finalización</string>
+ <string name="current_theme">Tema actual</string>
+ <string name="customize">Personalizar</string>
+ <string name="details">Detalles</string>
+ <string name="support">Ayuda</string>
+ <string name="active">Activo</string>
+ <string name="stats_referrers_spam_generic_error">Se ha producido un error durante la operación. No se ha cambiado el estado del spam.</string>
+ <string name="stats_referrers_marking_not_spam">Marcando como no spam</string>
+ <string name="stats_referrers_unspam">No spam</string>
+ <string name="stats_referrers_marking_spam">Marcando como spam</string>
+ <string name="theme_auth_error_authenticate">Error al recibir temas: error al autenticar el usuario</string>
+ <string name="post_published">Entrada publicada</string>
+ <string name="page_published">Página publicada</string>
+ <string name="post_updated">Entrada actualizada</string>
+ <string name="page_updated">Página actualizada</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Lo sentimos, no se han encontrados temas.</string>
+ <string name="media_file_name">Nombre del archivo: %s</string>
+ <string name="media_dimensions">Dimensiones: %s</string>
+ <string name="media_uploaded_on">Subido el: %s</string>
+ <string name="upload_queued">En cola</string>
+ <string name="media_file_type">Tipo de archivo: %s</string>
+ <string name="reader_label_gap_marker">Cargar más entradas</string>
+ <string name="notifications_no_search_results">Ningún sitio coincide con "%s"</string>
+ <string name="search_sites">Buscar sitios</string>
+ <string name="notifications_empty_view_reader">Ver Lector</string>
+ <string name="unread">Sin leer</string>
+ <string name="notifications_empty_action_followers_likes">Recibir avisos: comentar en entradas que has leído.</string>
+ <string name="notifications_empty_action_comments">Unirse a una conversación: comentar en entradas de blogs que sigues.</string>
+ <string name="notifications_empty_action_unread">Reiniciar la conversación: escribir una entrada nueva.</string>
+ <string name="notifications_empty_action_all">¡Actívate! Comentario en entradas de blogs que sigues.</string>
+ <string name="notifications_empty_likes">No hay nuevos Me gusta que mostrar... todavía.</string>
+ <string name="notifications_empty_followers">No hay nuevos seguidores de los que informar... todavía.</string>
+ <string name="notifications_empty_comments">No hay comentarios nuevos... todavía.</string>
+ <string name="notifications_empty_unread">¡Te has puesto al día!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Accede a las estadísticas de la aplicación e intenta agregar un widget más tarde</string>
+ <string name="stats_widget_error_readd_widget">Elimina el widget y vuelve a agregarlo</string>
+ <string name="stats_widget_error_no_visible_blog">No se pudo acceder a las estadísticas sin un blog visible</string>
+ <string name="stats_widget_error_no_permissions">Tu cuenta de WordPress.com no puede acceder a las estadísticas de este blog</string>
+ <string name="stats_widget_error_no_account">Accede a WordPress</string>
+ <string name="stats_widget_error_generic">No se han podido cargar las estadísticas</string>
+ <string name="stats_widget_loading_data">Cargando datos...</string>
+ <string name="stats_widget_name_for_blog">Las estadísticas de hoy de %1$s</string>
+ <string name="stats_widget_name">Las estadísticas de hoy de WordPress</string>
+ <string name="add_location_permission_required">Permiso necesario para añadir la ubicación</string>
+ <string name="add_media_permission_required">Permisos necesarios para añadir archivos multimedia</string>
+ <string name="access_media_permission_required">Permisos necesarios para acceder a archivos multimedia</string>
+ <string name="stats_enable_rest_api_in_jetpack">Para ver tus estadísticas debes activar el módulo JSON API en Jetpack.</string>
+ <string name="error_open_list_from_notification">Esta entrada o página fue publicada en otro sitio</string>
+ <string name="reader_short_comment_count_multi">%s Comentarios</string>
+ <string name="reader_short_comment_count_one">1 Comentario</string>
+ <string name="reader_label_submit_comment">ENVIAR</string>
+ <string name="reader_hint_comment_on_post">Responder a la entrada...</string>
+ <string name="reader_discover_visit_blog">Visita %s</string>
+ <string name="reader_discover_attribution_blog">Originalmente publicado en %s</string>
+ <string name="reader_discover_attribution_author">Originalmente publicado por %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originalmente publicado por %1$s en %2$s</string>
+ <string name="reader_short_like_count_multi">%s Me gusta</string>
+ <string name="reader_short_like_count_one">1 Me gusta</string>
+ <string name="reader_label_follow_count">%,d seguidores</string>
+ <string name="reader_short_like_count_none">Me gusta</string>
+ <string name="reader_menu_tags">Editar etiquetas y blogs</string>
+ <string name="reader_title_post_detail">Entrada del lector</string>
+ <string name="local_draft_explainer">Esta entrada tiene un borrador local que no ha sido publicado</string>
+ <string name="local_changes_explainer">Esta entrada tiene cambios en local que no han sido publicados</string>
+ <string name="notifications_push_summary">Opciones para las notificaciones que aparecen en tu dispositivo.</string>
+ <string name="notifications_email_summary">Opciones para las notificaciones que se envían al correo electrónico ligado a tu cuenta.</string>
+ <string name="notifications_tab_summary">Opciones para las notificaciones que aparecen en la pestaña de notificaciones.</string>
+ <string name="notifications_disabled">Las notificaciones de la App han sido desativadas. Pulsa aquí para activarlas en Opciones.</string>
+ <string name="notification_types">Tipos de notificaciones</string>
+ <string name="error_loading_notifications">No se han podido cargar los ajustes de avisos</string>
+ <string name="replies_to_your_comments">Respuestas a tus comentarios</string>
+ <string name="comment_likes">Me gusta al comentario</string>
+ <string name="email">Correo electrónico</string>
+ <string name="app_notifications">Avisos de la aplicación</string>
+ <string name="notifications_tab">Pestaña de avisos</string>
+ <string name="notifications_comments_other_blogs">Comentarios en otros sitios</string>
+ <string name="notifications_wpcom_updates">Actualizaciones de WordPress.com</string>
+ <string name="notifications_other">Otras</string>
+ <string name="notifications_account_emails">Correo de WordPress.com</string>
+ <string name="notifications_account_emails_summary">Siempre mandamos correos electrónicos importantes relativos a tu cuenta, pero también obtendrás extras útiles.</string>
+ <string name="your_sites">Tus sitios</string>
+ <string name="notifications_sights_and_sounds">Vistas y sonidos</string>
+ <string name="stats_insights_latest_post_trend">Ha pasado %1$s desde que se publicó %2$s. Aquí tienes el rendimiento de la entrada desde entonces…</string>
+ <string name="stats_insights_latest_post_summary">Sumario de la última entrada</string>
+ <string name="button_revert">Volver</string>
+ <string name="days_ago">Hace %d días</string>
+ <string name="yesterday">Ayer</string>
+ <string name="connectionbar_no_connection">Sin conexión</string>
+ <string name="page_trashed">Página enviada a la papelera</string>
+ <string name="post_deleted">Entrada borrada</string>
+ <string name="post_trashed">Entrada enviada a la papelera</string>
+ <string name="stats_no_activity_this_period">Sin actividad en este periodo</string>
+ <string name="trashed">En la papelera</string>
+ <string name="button_back">Atrás</string>
+ <string name="page_deleted">Página borrada</string>
+ <string name="button_stats">Estadísticas</string>
+ <string name="button_trash">Papelera</string>
+ <string name="button_preview">Vista previa</string>
+ <string name="button_view">Ver</string>
+ <string name="button_edit">Editar</string>
+ <string name="button_publish">Publicar</string>
+ <string name="my_site_no_sites_view_subtitle">¿Quieres añadir uno?</string>
+ <string name="my_site_no_sites_view_title">Aún no tienes ningún sitio WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustración</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">No tienes autorización para acceder a este blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">No se pudo encontrar este blog</string>
+ <string name="undo">Deshacer</string>
+ <string name="tabbar_accessibility_label_me">Yo mismo</string>
+ <string name="tabbar_accessibility_label_my_site">Mi sitio</string>
+ <string name="passcodelock_prompt_message">Escribe tu PIN</string>
+ <string name="editor_toast_changes_saved">Cambios guardados</string>
+ <string name="push_auth_expired">La solicitud ha expirado. Inicia sesión en WordPress.com para volver a intentarlo.</string>
+ <string name="ignore">Ignorar</string>
+ <string name="stats_insights_best_ever">El mejor día</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de vistas</string>
+ <string name="stats_insights_most_popular_hour">Hora más popular</string>
+ <string name="stats_insights_most_popular_day">Día más popular</string>
+ <string name="stats_insights_today">Estadísticas de hoy</string>
+ <string name="stats_insights_popular">Hora y día más populares</string>
+ <string name="stats_insights_all_time">Entradas, vistas y visitantes de todos los tiempos</string>
+ <string name="stats_insights">Detalles</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Para ver tus estadísticas, inicia sesión en la cuenta de WordPress.com que has usado para efectuar la conexión con Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">¿Buscas tus otras estadísticas recientes? Las hemos trasladado a la página "Detalles".</string>
+ <string name="me_disconnect_from_wordpress_com">Desconectar de WordPress.com</string>
+ <string name="me_btn_login_logout">Iniciar/Cerrar sesión</string>
+ <string name="me_connect_to_wordpress_com">Conectar a WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">"%s" no se ocultó porque es el sitio actual</string>
+ <string name="me_btn_support">Ayuda y soporte</string>
+ <string name="account_settings">Configuración de la cuenta</string>
+ <string name="site_picker_create_dotcom">Crear sitio en WordPress.com</string>
+ <string name="site_picker_edit_visibility">Mostrar/Ocultar sitios</string>
+ <string name="site_picker_add_site">Añadir sitio</string>
+ <string name="site_picker_add_self_hosted">Añadir sitio autoalojado</string>
+ <string name="my_site_btn_view_site">Ver sitio</string>
+ <string name="my_site_btn_view_admin">Ver Administrador</string>
+ <string name="my_site_btn_switch_site">Cambiar sitio</string>
+ <string name="site_picker_title">Elegir sitio</string>
+ <string name="my_site_header_look_and_feel">Aspecto</string>
+ <string name="my_site_btn_blog_posts">Entradas del blog</string>
+ <string name="my_site_btn_site_settings">Preferencias</string>
+ <string name="my_site_header_publish">Publicar</string>
+ <string name="reader_label_new_posts_subtitle">Toca para mostrarlos</string>
+ <string name="my_site_header_configuration">Configuración</string>
+ <string name="notifications_account_required">Inicia sesión en WordPress.com para recibir notificaciones</string>
+ <string name="stats_unknown_author">Autor desconocido</string>
+ <string name="signout">Desconectar</string>
+ <string name="image_added">Imagen añadida</string>
+ <string name="deselect_all">Anular todas las selecciones</string>
+ <string name="sign_out_wpcom_confirm">Si desconectas tu cuenta, se eliminarán de este dispositivo todos los datos de WordPress.com de @%s, incluidos los borradores y los cambios locales.</string>
+ <string name="select_all">Seleccionar todo</string>
+ <string name="show">Mostrar</string>
+ <string name="hide">Ocultar</string>
+ <string name="select_from_new_picker">Selección múltiple con el nuevo selector</string>
+ <string name="no_media_sources">No se pudieron traer los archivos multimedia</string>
+ <string name="loading_blog_videos">Trayendo vídeos</string>
+ <string name="loading_blog_images">Trayendo imágenes</string>
+ <string name="error_loading_images">Error cargando imágenes</string>
+ <string name="error_loading_videos">Error cargando vídeos</string>
+ <string name="error_loading_blog_videos">No se pudieron traer los vídeos</string>
+ <string name="error_loading_blog_images">No se pudieron traer las imágenes</string>
+ <string name="stats_generic_error">No se pudieron cargar las estadísticas solicitadas.</string>
+ <string name="no_blog_videos">No hay vídeos</string>
+ <string name="no_device_images">No hay imágenes</string>
+ <string name="no_device_videos">No hay vídeos</string>
+ <string name="no_blog_images">No hay imágenes</string>
+ <string name="no_media">No hay archivos multimedia</string>
+ <string name="loading_videos">Cargando vídeos</string>
+ <string name="loading_images">Cargando imágenes</string>
+ <string name="sign_in_jetpack">Inicia sesión en tu cuenta de WordPress.com para conectarte con Jetpack.</string>
+ <string name="auth_required">Inicia sesión de nuevo para continuar.</string>
+ <string name="tab_title_device_images">Imágenes del dispositivo</string>
+ <string name="tab_title_device_videos">Vídeos del dispositivo</string>
+ <string name="tab_title_site_images">Imágenes del sitio</string>
+ <string name="tab_title_site_videos">Vídeos del sitio</string>
+ <string name="add_to_post">Añadir a la entrada</string>
+ <string name="editor_toast_invalid_path">Ruta de archivo no válida</string>
+ <string name="verification_code">Código de verificación</string>
+ <string name="two_step_sms_sent">Revisa tus mensajes de texto para el código de verificación.</string>
+ <string name="two_step_footer_button">Enviar código vía mensaje de texto</string>
+ <string name="two_step_footer_label">Introduce el código de tu app de autentificación.</string>
+ <string name="verify">Verificar</string>
+ <string name="invalid_verification_code">Código de verificación no válido</string>
+ <string name="error_publish_no_network">No se puede publicar mientras no haya conexión. Guardado como borrador.</string>
+ <string name="take_video">Grabar un vídeo</string>
+ <string name="take_photo">Sacar una foto</string>
+ <string name="media_picker_title">Selecciona medio</string>
+ <string name="language">Idioma</string>
+ <string name="device">Dispositivo</string>
+ <string name="media_details_label_file_name">Nombre del archivo</string>
+ <string name="media_details_label_file_type">Tipo de archivo</string>
+ <string name="publisher">Autor:</string>
+ <string name="stats_empty_search_terms">No se han grabado los términos de búsqueda</string>
+ <string name="comments_fetching">Recuperando comentarios…</string>
+ <string name="pages_fetching">Recuperando páginas…</string>
+ <string name="toast_err_post_uploading">No se puede abrir la entrada durante la carga</string>
+ <string name="posts_fetching">Recuperando entradas…</string>
+ <string name="media_fetching">Recuperando medios…</string>
+ <string name="stats_view_search_terms">Términos de búsqueda</string>
+ <string name="stats_empty_search_terms_desc">Aprende más sobre el tráfico que te traen las búsquedas viendo los términos que buscaron tus lectores para llegar a tu sitio.</string>
+ <string name="stats_entry_search_terms">Término de búsqueda</string>
+ <string name="stats_view_authors">Autores</string>
+ <string name="stats_search_terms_unknown_search_terms">Términos de búsqueda desconocidos</string>
+ <string name="stats_followers_total_wpcom_paged">Mostrando del %1$d al %2$d de %3$s suscriptores de WordPress.com</string>
+ <string name="error_notification_open">No se pudo abrir la notificación</string>
+ <string name="stats_followers_total_email_paged">Mostrando del %1$d al %2$d de %3$s suscriptores por correo electrónico</string>
+ <string name="reader_empty_posts_request_failed">No se pudieron recuperar las entradas</string>
+ <string name="stats_months_and_years">Meses y años</string>
+ <string name="stats_average_per_day">Media diaria</string>
+ <string name="stats_overall">Global</string>
+ <string name="logs_copied_to_clipboard">Los informes de la aplicación se han copiado al portapapeles</string>
+ <string name="error_copy_to_clipboard">Ocurrió un error al copiar el texto en el portapapeles</string>
+ <string name="reader_empty_posts_in_blog">Este blog está vacío</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_recent_weeks">Últimas Semanas</string>
+ <string name="stats_period">Periodo</string>
+ <string name="reader_label_new_posts">Nuevas entradas</string>
+ <string name="post_uploading">Cargando</string>
+ <string name="reader_page_recommended_blogs">Sitios que te podrían gustar</string>
+ <string name="stats_comments_total_comments_followers">Total de entradas con comentarios de seguidores: %1$s</string>
+ <string name="stats_visitors">Visitantes</string>
+ <string name="stats_views">Vistas</string>
+ <string name="stats_timeframe_years">Años</string>
+ <string name="stats_pagination_label">Página %1$s de %2$s</string>
+ <string name="stats_view_countries">Países</string>
+ <string name="stats_likes">Me gusta</string>
+ <string name="stats_view_followers">Seguidores</string>
+ <string name="stats_view_videos">Vídeos</string>
+ <string name="stats_view_publicize">Difundir</string>
+ <string name="stats_entry_clicks_link">Enlace</string>
+ <string name="stats_view_top_posts_and_pages">Entradas y páginas</string>
+ <string name="stats_entry_followers">Seguidor</string>
+ <string name="stats_totals_publicize">Seguidores</string>
+ <string name="stats_entry_publicize">Servicio</string>
+ <string name="stats_totals_followers">Desde</string>
+ <string name="stats_empty_geoviews">No se han registrado países</string>
+ <string name="stats_empty_geoviews_desc">Explora la lista para saber cuáles son los países y las regiones que generan más tráfico hasta tu sitio.</string>
+ <string name="stats_entry_video_plays">Vídeo</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_empty_clicks_title">No se han registrado clics</string>
+ <string name="stats_empty_referrers_title">No se han registrado referencias</string>
+ <string name="stats_empty_top_posts_title">No se han visto entradas ni páginas</string>
+ <string name="stats_empty_top_posts_desc">Descubre qué parte de tu contenido se ha visto más y consulta cómo rinden las entradas y páginas individuales a lo largo del tiempo.</string>
+ <string name="stats_empty_tags_and_categories">No se han visto páginas ni entradas etiquetadas</string>
+ <string name="stats_empty_clicks_desc">Si tu contenido incluye enlaces a otros sitios, verás cuáles son aquellos en los que los visitantes hacen clic de forma más habitual.</string>
+ <string name="stats_empty_referrers_desc">Obtén más información acerca de la visibilidad de tu sitio; para ello, echa un vistazo a los sitios web y los motores de búsqueda que envían la mayor parte del tráfico a tu sitio</string>
+ <string name="stats_empty_tags_and_categories_desc">Obtén una visión general de los temas más populares de tu sitio a partir de las principales entradas publicadas la semana pasada.</string>
+ <string name="stats_empty_top_authors_desc">Realiza un seguimiento de las visitas que reciben las entradas de cada colaborador y haz zoom para descubrir cuál es el contenido más popular de cada autor.</string>
+ <string name="stats_empty_video_desc">Si has subido vídeos con VideoPress, descubre cuántas veces se han visto.</string>
+ <string name="stats_empty_comments_desc">Si permites comentarios en tu sitio, realiza un seguimiento de los principales autores de comentarios y descubre, a partir de los 1000 comentarios más recientes, qué parte de tu contenido desata las conversaciones más animadas.</string>
+ <string name="stats_empty_video">No se han reproducido vídeos</string>
+ <string name="stats_empty_publicize">No se han registrado seguidores de Publicize</string>
+ <string name="stats_empty_publicize_desc">Realiza un seguimiento de los seguidores provenientes de distintos servicios de redes sociales con ayuda de Publicize.</string>
+ <string name="stats_empty_followers">Sin seguidores</string>
+ <string name="stats_empty_followers_desc">Realiza un seguimiento del número total de seguidores y del tiempo que cada uno de ellos lleva siguiendo tu sitio.</string>
+ <string name="stats_comments_by_authors">Por autores</string>
+ <string name="stats_comments_by_posts_and_pages">Por entradas y páginas</string>
+ <string name="stats_followers_total_wpcom">Seguidores totales de WordPress.com: %1$s</string>
+ <string name="stats_followers_seconds_ago">hace unos segundos</string>
+ <string name="stats_followers_total_email">Seguidores totales por correo electrónico: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Correo electrónico</string>
+ <string name="stats_followers_a_minute_ago">hace un minuto</string>
+ <string name="stats_followers_days">%1$d días</string>
+ <string name="stats_followers_a_day">Un día</string>
+ <string name="stats_followers_hours">%1$d horas</string>
+ <string name="stats_followers_minutes">%1$d minutos</string>
+ <string name="stats_followers_an_hour_ago">hace una hora</string>
+ <string name="stats_followers_a_month">Un mes</string>
+ <string name="stats_followers_a_year">Un año</string>
+ <string name="stats_followers_years">%1$d años</string>
+ <string name="stats_view">Vista</string>
+ <string name="stats_followers_months">%1$d meses</string>
+ <string name="stats_view_all">Ver todo</string>
+ <string name="themes_fetching">Obteniendo temas…</string>
+ <string name="stats_for">Estadísticas del %s</string>
+ <string name="stats_other_recent_stats_label">Otras estadísticas recientes</string>
+ <string name="ssl_certificate_details">Detalles</string>
+ <string name="sure_to_remove_account">¿Deseas eliminar este sitio?</string>
+ <string name="delete_sure_post">Eliminar esta entrada</string>
+ <string name="delete_sure">Eliminar este borrador</string>
+ <string name="delete_sure_page">Eliminar esta página</string>
+ <string name="confirm_delete_multi_media">¿Quieres eliminar los elementos seleccionados?</string>
+ <string name="confirm_delete_media">¿Quieres eliminar el elemento seleccionado?</string>
+ <string name="cab_selected">%d seleccionados</string>
+ <string name="media_gallery_date_range">Mostrando elementos multimedia desde %1$s a %2$s</string>
+ <string name="reader_empty_posts_liked">No te ha gustado ninguna entrada.</string>
+ <string name="nux_help_description">Visita el centro de ayuda para obtener respuestas a las preguntas habituales o visita los foros para hacer preguntas nuevas.</string>
+ <string name="agree_terms_of_service">Al crear una cuenta, aceptas los fascinantes %1$sTérminos de servicio%2$s.</string>
+ <string name="create_new_blog_wpcom">Crear un blog de WordPress.com</string>
+ <string name="new_blog_wpcom_created">Se ha creado un blog de WordPress.com.</string>
+ <string name="reader_empty_comments">Aún no hay comentarios</string>
+ <string name="reader_empty_posts_in_tag">No hay entradas con esta etiqueta</string>
+ <string name="reader_label_comment_count_multi">%,d comentarios</string>
+ <string name="reader_label_view_original">Ver artículo original</string>
+ <string name="reader_label_like">Me gusta</string>
+ <string name="reader_label_comment_count_single">Un comentario</string>
+ <string name="reader_label_comments_closed">Los comentarios están cerrados</string>
+ <string name="reader_label_comments_on">Comentarios en</string>
+ <string name="reader_title_photo_viewer">%1$d de %2$d</string>
+ <string name="error_publish_empty_post">No se puede publicar una entrada vacía.</string>
+ <string name="error_refresh_unauthorized_posts">No tienes permiso para ver o editar entradas.</string>
+ <string name="error_refresh_unauthorized_pages">No tienes permiso para ver o editar páginas.</string>
+ <string name="error_refresh_unauthorized_comments">No tienes permiso para ver o editar comentarios.</string>
+ <string name="older_month">Hace más de 1 mes</string>
+ <string name="more">Más</string>
+ <string name="older_two_days">Hace más de 2 días</string>
+ <string name="older_last_week">Hace más de 1 semana</string>
+ <string name="stats_no_blog">No se han podido cargar las estadísticas del blog solicitado</string>
+ <string name="select_a_blog">Elegir un sitio de WordPress</string>
+ <string name="uploading_total">Cargando %1$d de %2$d</string>
+ <string name="mnu_comment_liked">Me gustó</string>
+ <string name="comment">Comentario</string>
+ <string name="comment_trashed">El comentario se ha enviado a la papelera.</string>
+ <string name="comment_reply_to_user">Responder a %s</string>
+ <string name="pages_empty_list">Sin páginas todavía. ¿Por qué no crear una?</string>
+ <string name="media_empty_list_custom_date">Sin multimedia durante este intervalo de tiempo.</string>
+ <string name="posting_post">Publicando "%s"</string>
+ <string name="signing_out">Cerrando sesión…</string>
+ <string name="faq_button">Preguntas frecuentes</string>
+ <string name="browse_our_faq_button">Explora nuestras Preguntas frecuentes.</string>
+ <string name="sending_content">Cargando el contenido de %s</string>
+ <string name="posts_empty_list">Aún no se han publicado entradas. ¿Por qué no crear una?</string>
+ <string name="reader_empty_followed_blogs_title">Todavía no sigues a ningún sitio.</string>
+ <string name="reader_label_liked_by">Le gusta a</string>
+ <string name="reader_toast_err_generic">No es posible realizar esta acción</string>
+ <string name="reader_toast_err_block_blog">No es posible bloquear este blog</string>
+ <string name="reader_toast_blog_blocked">Las entradas de este blog no volverán a mostrarse</string>
+ <string name="reader_menu_block_blog">Bloquear este blog</string>
+ <string name="contact_us">Contacta con nosotros</string>
+ <string name="hs__conversation_detail_error">Describe el problema que estás teniendo</string>
+ <string name="hs__new_conversation_header">Chat de soporte</string>
+ <string name="hs__conversation_header">Chat de soporte</string>
+ <string name="hs__username_blank_error">Escribe un nombre válido</string>
+ <string name="hs__invalid_email_error">Escribe una dirección de correo electrónico válida</string>
+ <string name="add_location">Añadir localización</string>
+ <string name="current_location">Localización actual</string>
+ <string name="search_location">Búsqueda</string>
+ <string name="edit_location">Editar</string>
+ <string name="search_current_location">Localizar</string>
+ <string name="preference_send_usage_stats">Enviar estadísticas</string>
+ <string name="preference_send_usage_stats_summary">Enviar automáticamente estadísticas de uso para ayudarnos a mejorar WordPress para Android</string>
+ <string name="update_verb">Actualizar</string>
+ <string name="schedule_verb">Programación</string>
+ <string name="reader_title_subs">Etiquetas y blogs</string>
+ <string name="reader_toast_err_already_follow_blog">Ya estás siguiendo este blog</string>
+ <string name="reader_page_followed_tags">Etiquetas que se siguen</string>
+ <string name="reader_label_followed_blog">Blog que se sigue</string>
+ <string name="reader_label_tag_preview">Entradas etiquetadas %s</string>
+ <string name="reader_toast_err_get_blog_info">No se puede mostrar este blog</string>
+ <string name="reader_toast_err_follow_blog">No se puede seguir este blog</string>
+ <string name="reader_toast_err_unfollow_blog">No se puede dejar de seguir este blog</string>
+ <string name="reader_empty_recommended_blogs">Sin blogs recomendados</string>
+ <string name="reader_title_blog_preview">Blog del lector</string>
+ <string name="reader_title_tag_preview">Etiqueta del lector</string>
+ <string name="reader_page_followed_blogs">Sitios que sigues</string>
+ <string name="reader_hint_add_tag_or_url">Introduce la URL o etiqueta que quieras seguir</string>
+ <string name="saving">Guardando</string>
+ <string name="media_empty_list">Sin elementos multimedia</string>
+ <string name="ptr_tip_message">Consejo: Arrastra hacia abajo para recargar</string>
+ <string name="help">Ayuda</string>
+ <string name="help_center">Centro de Ayuda</string>
+ <string name="forums">Foros</string>
+ <string name="ssl_certificate_error">Certificado SSL no válido</string>
+ <string name="forgot_password">¿Has olvidado la contraseña?</string>
+ <string name="ssl_certificate_ask_trust">SI normalmente se conecta sin problemas a este sitio sin problemas, este error puede significar que alguien están intentando suplantar el sitio, por lo que no deberías continuar. ¿Quieres, de todas formas, confiar en el certificado?</string>
+ <string name="passcode_wrong_passcode">PIN erróneo</string>
+ <string name="invalid_password_message">La contraseña debe contener al menos 4 caracteres</string>
+ <string name="invalid_username_too_short">Nombre de usuario debe ser superior a 4 caracteres</string>
+ <string name="invalid_username_too_long">Nombre de usuario debe ser inferior a 61 caracteres</string>
+ <string name="username_only_lowercase_letters_and_numbers">Nombre de usuario solo puede contener letras minúsculas (a-z) y números</string>
+ <string name="theme_fetch_failed">Ocurrió un error al obtener los temas.</string>
+ <string name="comments_empty_list">No hay comentarios.</string>
+ <string name="adding_cat_success">Categoría agregada correctamente.</string>
+ <string name="stats_bar_graph_empty">Estadísticas no disponibles.</string>
+ <string name="error_refresh_posts">Las entradas no pueden ser actualizadas en este momento</string>
+ <string name="error_refresh_pages">Las páginas no pueden ser actualizadas en este momento.</string>
+ <string name="out_of_memory">Memoria del dispositivo agotada</string>
+ <string name="mnu_comment_unspam">No es spam</string>
+ <string name="no_site_error">No se pudo conectar con el sitio WordPress</string>
+ <string name="adding_cat_failed">No se pudo añadir la categoría</string>
+ <string name="theme_set_failed">No se pudo establecer el tema</string>
+ <string name="notifications_empty_list">Sin notificaciones</string>
+ <string name="error_generic">Ocurrió un error</string>
+ <string name="error_edit_comment">Ocurrió un error al editar el comentario</string>
+ <string name="error_moderate_comment">Ocurrió un error al moderar el comentario</string>
+ <string name="invalid_email_message">Tu dirección de correo electrónico no es válida</string>
+ <string name="username_required">Ingresa un nombre de usuario</string>
+ <string name="username_not_allowed">Nombre de usuario no permitido</string>
+ <string name="username_must_be_at_least_four_characters">El nombre de usuario debe tener, al menos, 4 caracteres</string>
+ <string name="username_contains_invalid_characters">El nombre de usuario no puede incluir el caracter “_”</string>
+ <string name="username_must_include_letters">El nombre de usuario debe tener, al menos, 1 letra (a-z)</string>
+ <string name="email_invalid">Ingresa una dirección de correo electrónico válida</string>
+ <string name="email_not_allowed">Esa dirección de correo electrónico no está permitida</string>
+ <string name="username_exists">Ese nombre de usuario ya existe</string>
+ <string name="email_exists">Esa dirección de correo electrónico ya está siendo usada</string>
+ <string name="username_reserved_but_may_be_available">Ese nombre de usuario actualmente está reservado pero puede estar disponible en un par de días</string>
+ <string name="blog_name_not_allowed">Esa dirección de sitio no está disponible</string>
+ <string name="blog_name_required">Ingresa una dirección para tu sitio</string>
+ <string name="theme_auth_error_message">Asegúrate que tienes autorización para elegir temas</string>
+ <string name="cat_name_required">El campo nombre de categoría es necesario</string>
+ <string name="category_automatically_renamed">El nombre de categoría %1$s no es válido. Fue renombrado a %2$s.</string>
+ <string name="no_account">No se encontró una cuenta WordPress, añade una cuenta e intenta nuevamente</string>
+ <string name="error_delete_post">Ocurrió un error al eliminar el %s</string>
+ <string name="error_refresh_notifications">No se pudo actualizar las notificaciones</string>
+ <string name="error_load_comment">No se pudo cargar el comentario</string>
+ <string name="error_downloading_image">Error al descargar la imagen</string>
+ <string name="blog_name_must_be_at_least_four_characters">La dirección del sitio debe tener al menos 4 caracteres</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">La dirección del sitio debe ser más corta que 64 caracteres</string>
+ <string name="blog_name_contains_invalid_characters">La dirección del sitio no puede tener el caracter “_”</string>
+ <string name="blog_name_cant_be_used">No puedes usar esa dirección de sitio</string>
+ <string name="blog_name_exists">Ese sitio ya existe</string>
+ <string name="blog_name_reserved">Ese sitio está reservado</string>
+ <string name="stats_empty_comments">Aún sin comentarios</string>
+ <string name="sdcard_message">Se necesita una tarjeta SD montada para subir medios</string>
+ <string name="error_upload">Ocurrió un error mientras se subía el %s</string>
+ <string name="no_network_message">No hay ninguna conexión de red disponible</string>
+ <string name="gallery_error">El elemento multimedia no ha podido ser recuperado</string>
+ <string name="wait_until_upload_completes">Espere mientras se complete la carga</string>
+ <string name="blog_not_found">Ha ocurrido un error mientras se accedía a este blog</string>
+ <string name="username_or_password_incorrect">El usuario o contraseña que ingresaste no son correctos.</string>
+ <string name="reply_failed">Respuesta fallida</string>
+ <string name="error_refresh_comments">No se pudieron actualizar los comentarios</string>
+ <string name="error_refresh_stats">No se pudieron actualizar las estadísticas</string>
+ <string name="could_not_remove_account">No se ha podido eliminar el sitio</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">La dirección solo puede contener letras minúsculas (a-z) y números.</string>
+ <string name="blog_name_reserved_but_may_be_available">Este sitio está reservado actualmente pero es posible que quede libre en un par de días.</string>
+ <string name="nux_cannot_log_in">No se pudo acceder</string>
+ <string name="invalid_url_message">Comprueba que la URL introducida sea válida</string>
+ <string name="edit_post">Editar entrada</string>
+ <string name="add_comment">Añadir comentario</string>
+ <string name="connection_error">Error de conexión</string>
+ <string name="cancel_edit">Cancelar edición</string>
+ <string name="media_gallery_edit">Editar galería</string>
+ <string name="delete_post">Borrar entrada</string>
+ <string name="delete_page">Borrar página</string>
+ <string name="comment_status_approved">Aprobado</string>
+ <string name="comment_status_unapproved">Pendiente</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">En la papelera</string>
+ <string name="edit_comment">Editar comentario</string>
+ <string name="mnu_comment_approve">Aprobar</string>
+ <string name="mnu_comment_unapprove">Rechazar</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Enviar a la papelera</string>
+ <string name="dlg_approving_comments">Aprobando</string>
+ <string name="dlg_unapproving_comments">Rechazando</string>
+ <string name="dlg_spamming_comments">Marcando como spam</string>
+ <string name="dlg_trashing_comments">Enviando a la papelera</string>
+ <string name="dlg_confirm_trash_comments">¿Enviar a la papelera?</string>
+ <string name="trash">Papelera</string>
+ <string name="author_name">Nombre del autor</string>
+ <string name="author_email">Email del autor</string>
+ <string name="author_url">URL del autor</string>
+ <string name="hint_comment_content">Comentario</string>
+ <string name="saving_changes">Guardando cambios</string>
+ <string name="delete_draft">Eliminar borrador</string>
+ <string name="preview_page">Vista previa de página</string>
+ <string name="preview_post">Vista previa de entrada</string>
+ <string name="share_action_post">Nueva entrada</string>
+ <string name="share_action_media">Librería multimedia</string>
+ <string name="media_gallery_num_columns">Número de columnas</string>
+ <string name="new_post">Nueva entrada</string>
+ <string name="account_details">Detalles de la cuenta</string>
+ <string name="add_new_category">Añadir nueva categoría</string>
+ <string name="category_name">Nombre de la categoría</string>
+ <string name="category_slug">Slug de la categoría (opcional)</string>
+ <string name="category_desc">Descripción de la categoría (opcional)</string>
+ <string name="view_site">Ver sitio</string>
+ <string name="privacy_policy">Política de privacidad</string>
+ <string name="local_changes">Cambios locales</string>
+ <string name="wordpress_blog">Blog de WordPress</string>
+ <string name="email_hint">Correo electrónico</string>
+ <string name="blog_name_invalid">Dirección de sitio inválida</string>
+ <string name="blog_title_invalid">Título de sitio no válido</string>
+ <string name="media_gallery_settings_title">Ajustes de galería</string>
+ <string name="page_settings">Ajustes de página</string>
+ <string name="image_settings">Ajustes de imagen</string>
+ <string name="post_settings">Ajustes de entrada</string>
+ <string name="select_categories">Seleccionar categorías</string>
+ <string name="theme_current_theme">Tema actual</string>
+ <string name="theme_premium_theme">Tema premium</string>
+ <string name="link_enter_url_text">Texto del enlace (opcional)</string>
+ <string name="create_a_link">Crear un enlace</string>
+ <string name="file_error_create">No se pudo crear un archivo temporal para subir el archivo multimedia. Asegúrate que haya suficiente espacio libre en tu dispositivo.</string>
+ <string name="post_not_found">Ocurrió un error al cargar la entrada. Actualiza tus entradas e intenta nuevamente.</string>
+ <string name="media_gallery_image_order">Orden de imágenes</string>
+ <string name="local_draft">Borrador local</string>
+ <string name="view_in_browser">Ver en el navegador</string>
+ <string name="open_source_licenses">Licencias Open source</string>
+ <string name="reader_share_link">Compartir enlace</string>
+ <string name="reader_title_applog">Registro de la aplicación</string>
+ <string name="reader_toast_err_add_tag">No se pudo añadir esta etiqueta</string>
+ <string name="http_credentials">Credenciales HTTP (opcional)</string>
+ <string name="xmlrpc_error">No se pudo conectar. Ingresa la ruta completa a xmlrpc.php en tu sitio e intenta de nuevo.</string>
+ <string name="trash_no">No enviar a la papelera</string>
+ <string name="trash_yes">Enviar a la papelera</string>
+ <string name="pending_review">Pendiente de revisión</string>
+ <string name="http_authorization_required">Se necesita autorización</string>
+ <string name="add_account_blog_url">Dirección del blog</string>
+ <string name="required_field">Campo obligatorio</string>
+ <string name="site_address">La dirección (URL) de tu sitio autoalojado</string>
+ <string name="upload_failed">Falló la subida</string>
+ <string name="themes_live_preview">Vista previa</string>
+ <string name="learn_more">Aprender más</string>
+ <string name="comment_added">Comentado añadido con éxito</string>
+ <string name="sure_to_cancel_edit_comment">¿Cancelar la edición de este comentario?</string>
+ <string name="content_required">Comentario obligatorio</string>
+ <string name="toast_comment_unedited">El comentario no ha cambiado</string>
+ <string name="jetpack_message_not_admin">Se necesita el plugin Jetpack para tener estadísticas. Contacta con el administrador del sitio para activarlo.</string>
+ <string name="blog_name_must_include_letters">La dirección de los sitios debe tener, al menos, 1 letra (a-z)</string>
+ <string name="location_not_found">Localización desconocida</string>
+ <string name="cannot_delete_multi_media_items">Algunos elementos multimedia no pudieron ser borrados. Prueba más tarde</string>
+ <string name="fatal_db_error">Hubo un error al crear la base de datos de la app. Por favor, intenta reinstalar la app. </string>
+ <string name="blog_removed_successfully">El sitio se ha eliminado correctamente</string>
+ <string name="remove_account">Eliminar sitio</string>
+ <string name="post_not_published">No se ha publicado el estatus de la entrada</string>
+ <string name="page_not_published">No se ha publicado el estatus de la página</string>
+ <string name="category_parent">Categoría padre (opcional):</string>
+ <string name="horizontal_alignment">Alineación horizontal</string>
+ <string name="media_gallery_type_thumbnail_grid">Malla (red) de miniaturas</string>
+ <string name="media_error_no_permission">No tienes permiso para ver la librería multimedia</string>
+ <string name="error_blog_hidden">Este blog está oculto y no se puede cargar. Actívalo de nuevo en ajustes y prueba de nuevo.</string>
+ <string name="post_format">Tipo de entrada</string>
+ <string name="new_media">Nuevo elemento multimedia</string>
+ <string name="scaled_image_error">Introduce un valor válido de ancho a escala</string>
+ <string name="file_not_found">No ha sido posible encontrar el archivo multimedia para cargar. ¿Se ha borrado o cambiado de ubicación?</string>
+ <string name="email_cant_be_used_to_signup">No puedes usar esta dirección de correo electrónico para registrarte, ya que bloquea algunos de nuestros correos. Utiliza otro proveedor de correo.</string>
+ <string name="email_reserved">Esta dirección de correo electrónico ya se ha utilizado. Comprueba si tienes en tu bandeja de entrada un correo de activación. Si no activas ahora el correo, puedes intentarlo dentro de unos días.</string>
+ <string name="notifications_empty_all">No hay notificaciones... todavía.</string>
+ <string name="reader_toast_err_remove_tag">No se pudo eliminar esta etiqueta</string>
+ <string name="invalid_site_url_message">Comprueba que la URL del sitio introducida es válida</string>
+ <string name="deleting_post">Borrando entrada</string>
+ <string name="share_url_post">Compartir entrada</string>
+ <string name="share_url_page">Compartir página</string>
+ <string name="share_link">Compartir enlace</string>
+ <string name="deleting_page">Borrando página</string>
+ <string name="creating_your_account">Creando tu cuenta</string>
+ <string name="creating_your_site">Creando tu sitio</string>
+ <string name="reader_empty_posts_in_tag_updating">Recuperando entradas...</string>
+ <string name="error_refresh_media">Algo ha ido mal mientras actualizábamos la biblioteca multimedia. Inténtalo más tarde.</string>
+ <string name="reader_likes_you_and_multi">A ti, y a %,d personas más les gusta esto</string>
+ <string name="reader_likes_multi">A %,d personas les gusta esto</string>
+ <string name="reader_label_reply">Responder</string>
+ <string name="video">Vídeo</string>
+ <string name="download">Descargando elemento multimedia</string>
+ <string name="comment_spammed">Comentado marcado como spam</string>
+ <string name="cant_share_no_visible_blog">No se puede compartir en WordPress si no tienes un blog visible</string>
+ <string name="reader_toast_err_get_comment">No ha sido posible recuperar este comentario</string>
+ <string name="reader_likes_you_and_one">A ti y a otra persona os gusta esto.</string>
+ <string name="select_date">Elegir fecha</string>
+ <string name="pick_photo">Elige una foto</string>
+ <string name="pick_video">Elige un vídeo</string>
+ <string name="validating_user_data">Datos de validación del usuario</string>
+ <string name="validating_site_data">Validando datos del sitio</string>
+ <string name="account_two_step_auth_enabled">Esta cuenta tiene activado el proceso de autentificación en dos pasos. Visita los ajustes de seguridad de WordPress.com y genera una contraseña específica para la aplicación.</string>
+ <string name="select_time">Elige tiempo</string>
+ <string name="reader_empty_followed_blogs_description">No te preocupes, ¡solo tienes que pulsar el icono para empezar a explorar!</string>
+ <string name="reader_toast_err_get_post">No fue posible recuperar esta entrada</string>
+ <string name="nux_welcome_create_account">Crear cuenta</string>
+ <string name="password_invalid">Necesitas una contraseña más segura. Asegúrate de utilizar 7 o más caracteres, mezclar letras mayúsculas y minúsculas, número y caracteres especiales.</string>
+ <string name="nux_tap_continue">Continuar</string>
+ <string name="nux_add_selfhosted_blog">Añade un sitio autoalojado</string>
+ <string name="nux_oops_not_selfhosted_blog">Regístrate en WordPress.com</string>
+ <string name="signing_in">Iniciando sesión...</string>
+ <string name="reader_hint_comment_on_comment">Contestar al comentario </string>
+ <string name="reader_label_added_tag">Añadido %s</string>
+ <string name="button_next">Seguiente</string>
+ <string name="connecting_wpcom">Conectando a WordPress.com</string>
+ <string name="media_add_popup_title">Añadir a biblioteca multimedia</string>
+ <string name="select_from_media_library">Seleccionar de la biblioteca multimedia</string>
+ <string name="jetpack_message">Necesitas el pluging Jetpack para tener estadísticas estadísticas. ¿Quieres instalar Jetpack?</string>
+ <string name="jetpack_not_found">No se ha encontrado el plugin Jetpack</string>
+ <string name="reader_toast_err_tag_invalid">Esta no es una etiqueta válida</string>
+ <string name="reader_toast_err_share_intent">Imposible compartir</string>
+ <string name="limit_reached">Límite alcanzado. Puedes probar en 1 minuto. Probar de nuevo antes solamente incrementará el tiempo de espera hasta que la prohibición sea suprimida. Si crees que es un error, contacta con soporte. </string>
+ <string name="reader_toast_err_comment_failed">No se pudo publicar tu comentario </string>
+ <string name="reader_toast_err_view_image">Imposible ver la imágen</string>
+ <string name="reader_toast_err_url_intent">Imposible abrir %s</string>
+ <string name="nux_tutorial_get_started_title">¡Empezar!</string>
+ <string name="reader_toast_err_tag_exists">Ya estás siguiendo esta etiqueta</string>
+ <string name="reader_label_removed_tag">Eliminado %s</string>
+ <string name="media_add_new_media_gallery">Crear galería</string>
+ <string name="empty_list_default">Esta lista esta vacía</string>
+ <string name="reader_untitled_post">Sin título</string>
+ <string name="reader_share_subject">Compartido desde %s</string>
+ <string name="reader_btn_share">Compartir</string>
+ <string name="reader_btn_follow">Seguir</string>
+ <string name="reader_likes_one">A una persona le gusta esto</string>
+ <string name="reader_likes_only_you">Te gusta esto</string>
+ <string name="username_invalid">Nombre de usuario no válido</string>
+ <string name="reader_btn_unfollow">Siguiendo</string>
+ <string name="reader_empty_followed_tags">No sigues ninguna etiqueta</string>
+ <string name="create_account_wpcom">Crear una cuenta de WordPress.com</string>
+ <string name="themes">Temas</string>
+ <string name="all">Todas</string>
+ <string name="images">Imágenes</string>
+ <string name="unattached">Sin adjuntar</string>
+ <string name="custom_date">Fecha personalizada</string>
+ <string name="media_add_popup_capture_photo">Tomar foto</string>
+ <string name="media_add_popup_capture_video">Tomar vídeo</string>
+ <string name="media_gallery_type">Tipo</string>
+ <string name="stats_view_tags_and_categories">Etiquetas y categorías</string>
+ <string name="stats_timeframe_today">Hoy</string>
+ <string name="stats_timeframe_yesterday">Ayer</string>
+ <string name="stats_timeframe_days">Días</string>
+ <string name="stats_timeframe_weeks">Semanas</string>
+ <string name="stats_timeframe_months">Meses</string>
+ <string name="stats_entry_country">País</string>
+ <string name="stats_entry_posts_and_pages">Título</string>
+ <string name="theme_activate_button">Activar</string>
+ <string name="theme_activating_button">Activando</string>
+ <string name="share_action_title">Añadir a...</string>
+ <string name="media_edit_title_text">Título</string>
+ <string name="media_edit_description_text">Descripción</string>
+ <string name="media_edit_success">Actualizado</string>
+ <string name="media_gallery_image_order_random">Aleatorio</string>
+ <string name="media_edit_title_hint">Escribir un título aquí</string>
+ <string name="themes_details_label">Detalles</string>
+ <string name="post_excerpt">Extracto</string>
+ <string name="share_action">Compartir</string>
+ <string name="stats_view_clicks">Clics</string>
+ <string name="stats_view_referrers">Referentes</string>
+ <string name="stats_totals_plays">Juegos</string>
+ <string name="stats_entry_tags_and_categories">Tema</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Referencias</string>
+ <string name="passcode_manage">Administrar el PIN de bloqueo</string>
+ <string name="passcode_enter_passcode">Introduce tu PIN</string>
+ <string name="passcode_enter_old_passcode">Introduce tu viejo PIN</string>
+ <string name="passcode_re_enter_passcode">Vuelve a introducir el PIN</string>
+ <string name="passcode_change_passcode">Cambiar PIN</string>
+ <string name="passcode_set">Introducir PIN</string>
+ <string name="passcode_preference_title">PIN bloqueado</string>
+ <string name="passcode_turn_off">Desbloquear PIN</string>
+ <string name="passcode_turn_on">Bloquear PIN</string>
+ <string name="media_gallery_type_slideshow">Pase de diapositivas </string>
+ <string name="media_gallery_type_tiled">Mosaico</string>
+ <string name="media_gallery_type_circles">Círculos</string>
+ <string name="stats_view_visitors_and_views">Visitantes y Vistas</string>
+ <string name="theme_set_success">¡Tema aplicado con exito!</string>
+ <string name="theme_auth_error_title">Error al recibir temas</string>
+ <string name="stats_totals_clicks">Clics</string>
+ <string name="media_gallery_type_squares">Cuadrados</string>
+ <string name="media_edit_caption_text">Leyenda</string>
+ <string name="media_edit_caption_hint">Escribir una leyenda aquí</string>
+ <string name="media_edit_description_hint">Escribe una descripción aquí</string>
+ <string name="media_edit_failure">No se pudo actualizar</string>
+ <string name="themes_features_label">Características</string>
+ <string name="stats_totals_views">Vistas</string>
+ <string name="media_gallery_image_order_reverse">Inverso</string>
+ <string name="stats">Estadísticas</string>
+ <string name="upload">Subir</string>
+ <string name="discard">Descartar</string>
+ <string name="notifications">Notificaciones</string>
+ <string name="note_reply_successful">Respuesta publicada</string>
+ <string name="new_notifications">%d nuevas notificaciones</string>
+ <string name="more_notifications">y %d más.</string>
+ <string name="sign_in">Acceder</string>
+ <string name="follows">Seguimientos</string>
+ <string name="loading">Cargando...</string>
+ <string name="httpuser">Usuario HTTP</string>
+ <string name="httppassword">Contraseña HTTP</string>
+ <string name="error_media_upload">Se ha producido un error al cargar los archivos</string>
+ <string name="publish_date">Publicar</string>
+ <string name="content_description_add_media">Añadir media</string>
+ <string name="post_content">Contenido (pulsa para añadir texto y multimedia)</string>
+ <string name="incorrect_credentials">Nombre de usuario o contraseña incorrecta</string>
+ <string name="username">Nombre de usuario</string>
+ <string name="password">Contraseña</string>
+ <string name="reader">Lector</string>
+ <string name="featured">Usar como imagen destacada</string>
+ <string name="posts">Entradas</string>
+ <string name="width">Ancho</string>
+ <string name="pages">Páginas</string>
+ <string name="anonymous">Anónimo</string>
+ <string name="page">Página</string>
+ <string name="post">Entrada</string>
+ <string name="featured_in_post">Incluír imagen en el contenido del mensaje</string>
+ <string name="no_network_title">No hay red disponible</string>
+ <string name="caption">Leyenda (opcional)</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">Usuario del blog</string>
+ <string name="upload_scaled_image">Subir y enlazar a la imagen escalada</string>
+ <string name="scaled_image">Ancho de la imagen en escala</string>
+ <string name="scheduled">Programado</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Subiendo…</string>
+ <string name="app_title">WordPress para Android</string>
+ <string name="version">Versión</string>
+ <string name="tos">Términos del servicio</string>
+ <string name="max_thumbnail_px_width">Ancho predeterminado de la imagen </string>
+ <string name="image_alignment">Alineamiento</string>
+ <string name="refresh">Refrescar</string>
+ <string name="untitled">Sin Título</string>
+ <string name="edit">Editar</string>
+ <string name="page_id">Página</string>
+ <string name="post_id">Entrada</string>
+ <string name="immediately">Inmediatamente</string>
+ <string name="post_password">Contraseña (opcional)</string>
+ <string name="quickpress_add_alert_title">Nombre del Acceso Directo</string>
+ <string name="settings">Ajustes</string>
+ <string name="today">Hoy</string>
+ <string name="share_url">Compartir URL</string>
+ <string name="quickpress_add_error">El nombre del acceso directo no puede ser vacío.</string>
+ <string name="quickpress_window_title">Selecciona un blog para el acceso directo a la Publicación Rápida</string>
+ <string name="publish_post">Publicar</string>
+ <string name="draft">Borrador</string>
+ <string name="post_private">Privado</string>
+ <string name="upload_full_size_image">Subir y enlazar a la imagen completa.</string>
+ <string name="categories">Categorías</string>
+ <string name="title">Título</string>
+ <string name="tags_separate_with_commas">Etiquetas (separa las etiquetas con comas)</string>
+ <string name="dlg_deleting_comments">Eliminando comentarios</string>
+ <string name="notification_vibrate">Vibrar</string>
+ <string name="notification_blink">Notificación luminosa</string>
+ <string name="notification_sound">Sonido de aviso</string>
+ <string name="status">Estado</string>
+ <string name="select_video">Selecciona un video de la galería</string>
+ <string name="location">Ubicación</string>
+ <string name="sdcard_title">Requiere tarjeta SD</string>
+ <string name="media">Multimedia</string>
+ <string name="delete">Eliminar</string>
+ <string name="none">Ninguno</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Selecciona una foto de la galería</string>
+ <string name="error">Error</string>
+ <string name="cancel">Cancelar</string>
+ <string name="save">Guardar</string>
+ <string name="add">Añadir</string>
+ <string name="category_refresh_error">Error de actualización de categorías</string>
+ <string name="preview">Vista previa</string>
+ <string name="reply">Responder</string>
+ <string name="yes">Sí</string>
+ <string name="no">No</string>
+ <string name="on">en</string>
+ <string name="notification_settings">Ajustes de avisos</string>
+</resources>
diff --git a/WordPress/src/main/res/values-eu/strings.xml b/WordPress/src/main/res/values-eu/strings.xml
new file mode 100644
index 000000000..604310475
--- /dev/null
+++ b/WordPress/src/main/res/values-eu/strings.xml
@@ -0,0 +1,793 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administratzailea</string>
+ <string name="role_editor">Editorea</string>
+ <string name="role_author">Egilea</string>
+ <string name="role_contributor">Laguntzailea</string>
+ <string name="role_follower">Jarraitzailea</string>
+ <string name="role_viewer">Ikuslea</string>
+ <string name="error_post_my_profile_no_connection">Konexiorik ez, ezin izan da profila gorde</string>
+ <string name="alignment_none">Bat ere ez</string>
+ <string name="alignment_left">Ezkerrera</string>
+ <string name="alignment_right">Eskuinera</string>
+ <string name="error_fetch_users_list">Ezin izan dira guneko erabiltzaileak eskuratu</string>
+ <string name="plans_manage">Kudeatu zure plana\nWordPress.com/plans orrialdean</string>
+ <string name="title_follower">Jarraitzailea</string>
+ <string name="title_email_follower">E-posta jarraitzailea</string>
+ <string name="people_empty_list_filtered_viewers">Oraindik ez daukazu ikuslerik.</string>
+ <string name="people_fetching">Erabiltzaileak eskuratzen...</string>
+ <string name="people_empty_list_filtered_email_followers">Ez daukazu e-posta jarraitzailerik oraindik.</string>
+ <string name="people_empty_list_filtered_followers">Ez daukazu jarraitzailerik oraindik.</string>
+ <string name="people_empty_list_filtered_users">Ez daukazu erabiltzailerik oraindik.</string>
+ <string name="people_dropdown_item_email_followers">E-posta jarraitzaileak</string>
+ <string name="people_dropdown_item_viewers">Ikusleak</string>
+ <string name="people_dropdown_item_followers">Jarraitzialeak</string>
+ <string name="people_dropdown_item_team">Taldea</string>
+ <string name="follower_subscribed_since">%1$s-(e)tik</string>
+ <string name="reader_label_view_gallery">Ikusi galeria</string>
+ <string name="error_remove_follower">Ezin izan da jarraitzailea kendu</string>
+ <string name="error_remove_viewer">Ezin izan da ikuslea kendu</string>
+ <string name="error_fetch_email_followers_list">Ezin izan dira guneko e-posta jarraitzaileak eskuratu</string>
+ <string name="error_fetch_followers_list">Ezin izan dira guneko jarraitzaileak eskuratu</string>
+ <string name="visual_editor">Editore bisuala</string>
+ <string name="format_bar_description_html">HTML modua</string>
+ <string name="format_bar_description_ul">Ordenarik gabeko zerrenda</string>
+ <string name="format_bar_description_ol">Zerrenda ordenatua</string>
+ <string name="format_bar_description_more">Txertatu gehiago</string>
+ <string name="format_bar_description_media">Txertatu media</string>
+ <string name="format_bar_description_link">Txertatu esteka</string>
+ <string name="format_bar_description_underline">Azpimarra</string>
+ <string name="format_bar_description_italic">Etzana</string>
+ <string name="image_settings_save_toast">Aldaketak gorde dira</string>
+ <string name="image_alt_text">Testu alternatiboa</string>
+ <string name="image_link_to">Estekatu:</string>
+ <string name="uploading_gallery_placeholder">Galeria igotzen...</string>
+ <string name="invite_sent">Gonbidapena ondo bidali da</string>
+ <string name="tap_to_try_again">Egin tap berriro saiatzeko!</string>
+ <string name="invite_error_some_failed">Gonbidapena bidalita baina errorea(k) gertatu da/dira!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">Errore bat gertatu da gonbidapena bidaltzen saiatzean!</string>
+ <string name="invite_error_invalid_usernames_multiple">Ezin bidali: erabiltzaile-izen edo e-posta baliogabeak daude</string>
+ <string name="invite_error_invalid_usernames_one">Ezin bidali: erabiltzaile-izen edo e-posta bat baliogabea da</string>
+ <string name="invite_error_no_usernames">Mesedez, gehitu gutxienez erabiltzaile-izen bat</string>
+ <string name="invite_message_info">(Hautazkoa) Erabiltzaileari/ei bidaliko zaion/en 500 hizki arteko mezu pertsonalizatu bat idatz dezakezu.</string>
+ <string name="invite_message_remaining_other">%d hizki falta dira</string>
+ <string name="invite_message_remaining_one">Hizki 1 falta da</string>
+ <string name="invite_message_remaining_zero">0 hizki falta dira</string>
+ <string name="invite_invalid_email">\'%s\' e-posta helbidea baliogabea da</string>
+ <string name="invite_message_title">Mezu pertsonalizatua</string>
+ <string name="invite_already_a_member">Dagoeneko badago \'%s\' izeneko erabiltzaile bat</string>
+ <string name="invite_username_not_found">Ez da \'%s\' izeneko erabiltzailerik aurkitu</string>
+ <string name="invite">Gonbidatu</string>
+ <string name="invite_names_title">Erabiltzaile-izenak edo e-postak</string>
+ <string name="my_site_header_external">Kanpokoak</string>
+ <string name="send_link">Bidali esteka</string>
+ <string name="invite_people">Gonbidatu jendea</string>
+ <string name="label_clear_search_history">Garbitu bilaketen historia</string>
+ <string name="dlg_confirm_clear_search_history">Bilaketen historia garbitu?</string>
+ <string name="reader_label_post_search_running">Bilatzen...</string>
+ <string name="send">Bidali</string>
+ <string name="people">Jendea</string>
+ <string name="edit_user">Editatu erabiltzailea</string>
+ <string name="role">Rola</string>
+ <string name="error_remove_user">Ezin izan da erabiltzailea ezabatu</string>
+ <string name="error_update_role">Ezin izan da erabiltzailearen rola eguneratu</string>
+ <string name="checking_email">E-posta egiaztatzen</string>
+ <string name="not_on_wordpress_com">Ez dago WordPress.com-en?</string>
+ <string name="check_your_email">Egiaztatu zure e-posta</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Jaso esteka bat zure e-postan segituan saioa hasteko</string>
+ <string name="logging_in">Saioa hasten</string>
+ <string name="enter_your_password_instead">Sartu zure pasahitza ordez</string>
+ <string name="web_address_dialog_hint">Publikoki erakusten da iruzkinak egitean.</string>
+ <string name="username_email">E-posta edo erabiltzailea</string>
+ <string name="jetpack_not_connected_message">Jetpack plugina instalatuta dago baina ez dago WordPress.com-era konektatuta. Jetpack konektatu nahi al duzu?</string>
+ <string name="jetpack_not_connected">Jetpack plugina konektatu gabe</string>
+ <string name="stats_insights_latest_post_no_title">(izenbururik gabe)</string>
+ <string name="plans_post_purchase_button_themes">Arakatu itxurak</string>
+ <string name="plans_post_purchase_button_video">Hasi bidalketa berria</string>
+ <string name="plans_loading_error">Ezin izan dira planak kargatu</string>
+ <string name="plan">Plana</string>
+ <string name="plans">Planak</string>
+ <string name="export_your_content">Esportatu zure edukia</string>
+ <string name="exporting_content_progress">Edukia esportatzen...</string>
+ <string name="export_email_sent">Esportazio e-posta bidalita!</string>
+ <string name="delete_site_progress">Gunea ezabatzen...</string>
+ <string name="delete_site_summary">Ekintza hau ezin da desegin. Zure gunea ezabatzean guneko eduki, parte-hartzaile eta domeinu guztiak kenduko dira.</string>
+ <string name="delete_site_hint">Ezabatu gunea</string>
+ <string name="are_you_sure">Ziur zaude?</string>
+ <string name="export_site_hint">Esportatu zure gunea XML fitxategi batera</string>
+ <string name="export_site_summary">Ziur bazaude, har ezazu mesedez denbora bat zure edukia oraintxe esportatzeko. Etorkizunean ezingo duzu berreskuratu.</string>
+ <string name="keep_your_content">Mantendu zure edukia</string>
+ <string name="me_btn_app_settings">App-aren ezarpenak</string>
+ <string name="site_settings_advanced_header">Aurreratua</string>
+ <string name="button_done">Eginda</string>
+ <string name="post_format_status">Egoera</string>
+ <string name="post_format_video">Bideoa</string>
+ <string name="alignment_center">Erdira</string>
+ <string name="theme_all">Guztiak</string>
+ <string name="theme_free">Doakoak</string>
+ <string name="theme_premium">Ordainpekoak</string>
+ <string name="post_format_link">Esteka</string>
+ <string name="post_format_quote">Aipua</string>
+ <string name="post_format_image">Irudia</string>
+ <string name="post_format_chat">Txata</string>
+ <string name="post_format_gallery">Galeria</string>
+ <string name="post_format_standard">Estandarra</string>
+ <string name="notif_suggestions">Iradokizunak</string>
+ <string name="site_settings_list_editor_summary_other">%d item</string>
+ <string name="site_settings_list_editor_summary_one">Item 1</string>
+ <string name="approve_auto">Erabiltzaile guztiek</string>
+ <string name="approve_manual">Iruzkinik ez</string>
+ <string name="site_settings_paging_summary_other">%d iruzkin orrialdeko</string>
+ <string name="site_settings_paging_summary_one">Iruzkin 1 orrialdeko</string>
+ <string name="days_quantity_one">Egun 1</string>
+ <string name="days_quantity_other">%d egun</string>
+ <string name="filter_draft_posts">Zirriborroak</string>
+ <string name="filter_scheduled_posts">Programatuak</string>
+ <string name="filter_published_posts">Argitaratuta</string>
+ <string name="new_editor_promo_button_label">Bikain, eskerrik asko!</string>
+ <string name="visual_editor_enabled">Editore bisuala gaituta</string>
+ <string name="editor_content_placeholder">Partekatu zure istorioa hemen...</string>
+ <string name="editor_page_title_placeholder">Orrialdearen izenburua</string>
+ <string name="editor_post_title_placeholder">Bidalketaren izenburua</string>
+ <string name="email_address">E-posta helbidea</string>
+ <string name="preference_show_visual_editor">Erakutsi editore bisuala</string>
+ <string name="preference_editor">Editorea</string>
+ <string name="mnu_comment_delete_permanently">Ezabatu</string>
+ <string name="comment_deleted_permanently">Iruzkina ezabatu da</string>
+ <string name="mnu_comment_untrash">Berreskuratu</string>
+ <string name="comments_empty_list_filtered_spam">Ez dago zabor iruzkinik</string>
+ <string name="could_not_load_page">Ezin izan da orrialde kargatu</string>
+ <string name="comment_status_all">Guztiak</string>
+ <string name="interface_language">Interfazearen hizkuntza</string>
+ <string name="about_the_app">App-ari buruz</string>
+ <string name="error_post_account_settings">Ezin izan dira zure kontu ezarpenak gorde</string>
+ <string name="stats_widget_promo_ok_btn_label">Ados, ulertuta</string>
+ <string name="remove">Kendu</string>
+ <string name="search">Bilatu</string>
+ <string name="add_category">Gehitu kategoria</string>
+ <string name="disabled">Ezgaitu</string>
+ <string name="site_settings_image_original_size">Jatorrizko neurria</string>
+ <string name="about_me">Niri buruz</string>
+ <string name="public_display_name">Publikoki erakusten den izena</string>
+ <string name="my_profile">Nire profila</string>
+ <string name="first_name">Izena</string>
+ <string name="last_name">Abizena</string>
+ <string name="site_privacy_public_desc">Baimendu bilaketa motorrei gune hau indexatzen</string>
+ <string name="site_privacy_hidden_desc">Asmoa kendu bilaketa motorrei gune hau indexatzeaz</string>
+ <string name="site_privacy_private_desc">Nire gunea pribatua izatea nahi dut, nik hautatutako erabiltzaileek bakarrik ikusi ahal izatea</string>
+ <string name="site_settings_unsupported_version_error">Euskarririk gabeko WordPress bertsioa</string>
+ <string name="site_settings_paging_dialog_header">Iruzkinak orrialdeko</string>
+ <string name="site_settings_list_editor_input_hint">Sartu hitz edo esaldi bat</string>
+ <string name="site_settings_list_editor_no_items_text">Itemik ez</string>
+ <string name="site_settings_rp_show_images_title">Erakutsi irudiak</string>
+ <string name="site_settings_rp_show_header_title">Erakutsi goiburua</string>
+ <string name="site_settings_password_hint">Aldatu zure pasahitza</string>
+ <string name="site_settings_username_hint">Oraingo erabiltzaile kontua</string>
+ <string name="site_settings_tagline_hint">Deskripzio labur bat edo esaldi burutsu bat zure bloga deskribatzeko</string>
+ <string name="site_settings_title_hint">Azaldu hitz gutxitan gune hau zertaz doan</string>
+ <string name="site_settings_whitelist_known_summary">Erabiltzaile ezagunen iruzkinak</string>
+ <string name="site_settings_whitelist_all_summary">Erabiltzaile guztien iruzkinak</string>
+ <string name="site_settings_privacy_private_summary">Pribatua</string>
+ <string name="site_settings_privacy_hidden_summary">Ezkutua</string>
+ <string name="site_settings_delete_site_title">Ezabatu gunea</string>
+ <string name="site_settings_privacy_public_summary">Publikoa</string>
+ <string name="site_settings_blacklist_title">Zerrenda beltza</string>
+ <string name="site_settings_moderation_hold_title">Moderatzeko gelditu</string>
+ <string name="site_settings_multiple_links_title">Estekak iruzkinetan</string>
+ <string name="site_settings_whitelist_title">Automatikoki onartu</string>
+ <string name="site_settings_threading_title">Hariztaketa</string>
+ <string name="site_settings_paging_title">Orrikatzea</string>
+ <string name="site_settings_allow_comments_title">Baimendu iruzkinak</string>
+ <string name="site_settings_default_format_title">Formatu lehenetsia</string>
+ <string name="site_settings_default_category_title">Kategoria lehenetsia</string>
+ <string name="site_settings_address_title">Helbidea</string>
+ <string name="site_settings_title_title">Gunearen izenburua</string>
+ <string name="site_settings_this_device_header">Gailu hau</string>
+ <string name="site_settings_discussion_new_posts_header">Lehenetsitakoak bidalketa berrientzako</string>
+ <string name="site_settings_account_header">Kontua</string>
+ <string name="site_settings_writing_header">Idazketa</string>
+ <string name="newest_first">Berriena lehenengoa</string>
+ <string name="site_settings_general_header">Orokorra</string>
+ <string name="discussion">Eztabaida</string>
+ <string name="privacy">Pribatutasuna</string>
+ <string name="comments">Iruzkinak</string>
+ <string name="never">Inoiz ez</string>
+ <string name="unknown">Ezezaguna</string>
+ <string name="reader_err_get_post_not_found">Bidalketa hau jada ez da existitzen</string>
+ <string name="reader_err_get_post_not_authorized">Ez daukazu bidalketa hau ikusteko baimenik</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Ez dago bidalketa berririk</string>
+ <string name="media_details_copy_url_toast">URL-a arbelera kopiatu da</string>
+ <string name="edit_media">Editatu media</string>
+ <string name="media_details_copy_url">Kopiatu URL-a</string>
+ <string name="media_details_label_date_uploaded">Igota</string>
+ <string name="media_details_label_date_added">Gehituta</string>
+ <string name="selected_theme">Hautatutako itxura</string>
+ <string name="could_not_load_theme">Ezin da itxura kargatu</string>
+ <string name="theme_activation_error">Zerbait gaizki atera da. Ezin izan da itxura gaitu</string>
+ <string name="theme_view">Ikusi</string>
+ <string name="theme_details">Xehetasunak</string>
+ <string name="theme_support">Euskarria</string>
+ <string name="theme_done">EGINDA</string>
+ <string name="theme_manage_site">Kudeatu gunea</string>
+ <string name="title_activity_theme_support">Itxurak</string>
+ <string name="theme_activate">Aktibatu</string>
+ <string name="date_range_start_date">Hasiera data</string>
+ <string name="date_range_end_date">Amaiera data</string>
+ <string name="current_theme">Oraingo itxura</string>
+ <string name="customize">Pertsonalizatu</string>
+ <string name="details">Xehetasunak</string>
+ <string name="support">Euskarria</string>
+ <string name="active">Aktiboa</string>
+ <string name="post_published">Bidalketa argitaratuta</string>
+ <string name="page_published">Orrialdea argitaratuta</string>
+ <string name="post_updated">Bidalketa eguneratuta</string>
+ <string name="page_updated">Orrialdea eguneratu da</string>
+ <string name="theme_no_search_result_found">Barkatu, ez da itxurarik aurkitu.</string>
+ <string name="media_file_name">Fitxategi izena: %s</string>
+ <string name="notifications_empty_followers">Ez dago jarraitzaile berririk... oraindik.</string>
+ <string name="notifications_empty_comments">Ez dago iruzkin berririk... oraindik.</string>
+ <string name="stats_widget_loading_data">Datuak kargatzen...</string>
+ <string name="stats_widget_name_for_blog">Gaurko estatistikak %1$s blogerako</string>
+ <string name="stats_widget_name">WordPress-eko gaurko estatistikak</string>
+ <string name="reader_short_comment_count_multi">%s iruzkin</string>
+ <string name="reader_short_comment_count_one">Iruzkin 1</string>
+ <string name="reader_label_submit_comment">BIDALI</string>
+ <string name="reader_hint_comment_on_post">Bidalketari erantzun...</string>
+ <string name="reader_discover_visit_blog">Bisitatu %s</string>
+ <string name="notification_types">Jakinarazpen motak</string>
+ <string name="replies_to_your_comments">Zure iruzkinen erantzunak</string>
+ <string name="app_notifications">App-aren jakinarazpenak</string>
+ <string name="notifications_tab">Jakinarazpenen erlaitza</string>
+ <string name="email">E-posta</string>
+ <string name="notifications_wpcom_updates">WordPress.com eguneraketak</string>
+ <string name="your_sites">Zure guneak</string>
+ <string name="stats_insights_latest_post_summary">Azken bidalketaren laburpena</string>
+ <string name="days_ago">Duela %d egun</string>
+ <string name="yesterday">Atzo</string>
+ <string name="connectionbar_no_connection">Konexiorik ez</string>
+ <string name="page_trashed">Orrialdea zaborretara bidalia</string>
+ <string name="post_trashed">Bidalketa zaborretara bidalia</string>
+ <string name="post_deleted">Bidalketa ezabatua</string>
+ <string name="button_back">Itzuli</string>
+ <string name="page_deleted">Orrialdea ezabatua</string>
+ <string name="button_stats">Estatistikak</string>
+ <string name="button_trash">Zaborra</string>
+ <string name="button_preview">Aurreikusi</string>
+ <string name="button_view">Ikusi</string>
+ <string name="button_edit">Editatu</string>
+ <string name="button_publish">Argitaratu</string>
+ <string name="my_site_no_sites_view_subtitle">Gehitu nahi diozu bat?</string>
+ <string name="my_site_no_sites_view_title">Oraindik ez duzu WordPress gunerik.</string>
+ <string name="reader_toast_err_follow_blog_not_found">Ezin izan da blog hori aurkitu</string>
+ <string name="undo">Desegin</string>
+ <string name="tabbar_accessibility_label_my_site">Nire gunea</string>
+ <string name="tabbar_accessibility_label_me">Ni</string>
+ <string name="passcodelock_prompt_message">Sartu zure PINa</string>
+ <string name="editor_toast_changes_saved">Aldaketak gorde dira</string>
+ <string name="push_auth_expired">Eskaria iraungi da. Hasi saioa WordPress.com-en berriro saiatzeko.</string>
+ <string name="ignore">Ezikusi</string>
+ <string name="stats_insights_most_popular_hour">Ordurik jendetsuena</string>
+ <string name="stats_insights_most_popular_day">Egunik jendetsuena</string>
+ <string name="stats_insights_today">Gaurko estatistikak</string>
+ <string name="stats_insights_popular">Egun eta ordu jendetsuena</string>
+ <string name="stats_insights_all_time">Inoizko bidalketa, ikustaldi eta bisitari guztiak</string>
+ <string name="stats_insights">Ezagutzak</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Zure estatistikak ikusteko, hasi saioa Jetpack-era konektatzeko erabili zenuen WordPress.com kontuarekin.</string>
+ <string name="stats_other_recent_stats_moved_label">\'Bestelako estatistika berrien\' bila zabiltza? Ezagutzak orrialdera mugitu dugu.</string>
+ <string name="me_disconnect_from_wordpress_com">WordPress.com-etik deskonektatu</string>
+ <string name="me_connect_to_wordpress_com">WordPress.com-era konektatu</string>
+ <string name="me_btn_login_logout">Hasi/itxi saioa</string>
+ <string name="account_settings">Kontuaren ezarpenak</string>
+ <string name="me_btn_support">Laguntza eta euskarria</string>
+ <string name="site_picker_add_site">Gehitu gunea</string>
+ <string name="site_picker_add_self_hosted">Gehitu zuk ostatatutako gunea</string>
+ <string name="site_picker_edit_visibility">Erakutsi/ezkutatu guneak</string>
+ <string name="my_site_btn_view_site">Ikusi gunea</string>
+ <string name="site_picker_title">Aukeratu gunea</string>
+ <string name="my_site_btn_switch_site">Aldatu gunez</string>
+ <string name="my_site_btn_view_admin">Ikusi Admin</string>
+ <string name="my_site_header_look_and_feel">Itxura</string>
+ <string name="my_site_header_publish">Argitaratu</string>
+ <string name="my_site_btn_site_settings">Ezarpenak</string>
+ <string name="my_site_btn_blog_posts">Blogeko bidalketak</string>
+ <string name="my_site_header_configuration">Konfigurazioa</string>
+ <string name="reader_label_new_posts_subtitle">Egin tap berauek erakusteko</string>
+ <string name="notifications_account_required">Hasi saioa WordPress.com-en jakinarazpenentzako</string>
+ <string name="stats_unknown_author">Egile ezezaguna</string>
+ <string name="signout">Deskonektatu</string>
+ <string name="image_added">Irudia gehitua</string>
+ <string name="hide">Ezkutatu</string>
+ <string name="show">Ikusi</string>
+ <string name="select_all">Aukeratu denak</string>
+ <string name="no_device_videos">Bideorik ez</string>
+ <string name="no_blog_images">Irudirik ez</string>
+ <string name="no_blog_videos">Bideorik ez</string>
+ <string name="no_device_images">Irudirik ez</string>
+ <string name="error_loading_images">Errorea irudiak kargatzean</string>
+ <string name="error_loading_videos">Errorea bideoak kargatzean</string>
+ <string name="loading_blog_images">Irudiak eskuratzen</string>
+ <string name="loading_blog_videos">Bideoak eskuratzen</string>
+ <string name="no_media_sources">Ezin izan da media eskuratu</string>
+ <string name="error_loading_blog_images">Ezin dira irudiak eskuratu</string>
+ <string name="error_loading_blog_videos">Ezin dira bideoak eskuratu</string>
+ <string name="no_media">Mediarik ez</string>
+ <string name="take_photo">Atera argazkia</string>
+ <string name="language">Hizkuntza</string>
+ <string name="device">Gailua</string>
+ <string name="media_picker_title">Aukeratu media</string>
+ <string name="media_details_label_file_name">Fitxategi izena</string>
+ <string name="media_details_label_file_type">Fitxategi mota</string>
+ <string name="verification_code">Egiaztaketa kodea</string>
+ <string name="invalid_verification_code">Egiaztaketa kode okerra</string>
+ <string name="verify">Egiaztatu</string>
+ <string name="sign_in_jetpack">Hasi saioa WordPress.com-eko zure kontuan Jetpack-era konektatzeko.</string>
+ <string name="auth_required">Hasi saioa berriro jarraitzeko.</string>
+ <string name="editor_toast_invalid_path">Fitxategiaren bide okerra</string>
+ <string name="reader_empty_posts_request_failed">Huts egin du bidalketak eskuratzeak</string>
+ <string name="stats_view_authors">Egileak</string>
+ <string name="comments_fetching">Iruzkinak eskuratzen...</string>
+ <string name="pages_fetching">Orrialdeak eskuratzen...</string>
+ <string name="toast_err_post_uploading">Ezin da bidalketa ireki kargatzen dagoen bitartean</string>
+ <string name="posts_fetching">Bidalketak eskuratzen...</string>
+ <string name="media_fetching">Media eskuratzen...</string>
+ <string name="publisher">Argitaratzailea:</string>
+ <string name="stats_entry_search_terms">Bilaketa terminoa</string>
+ <string name="stats_view_search_terms">Bilaketa terminoak</string>
+ <string name="post_uploading">Kargatzen</string>
+ <string name="stats_total">Guztira</string>
+ <string name="reader_label_new_posts">Bidalketa berriak</string>
+ <string name="reader_empty_posts_in_blog">Blog hau hutsik dago</string>
+ <string name="stats_average_per_day">Eguneko bataz bestekoa</string>
+ <string name="stats_recent_weeks">Azken asteak</string>
+ <string name="error_copy_to_clipboard">Errore bat gertatu da testua arbelera kopiatzean</string>
+ <string name="reader_page_recommended_blogs">Gogoko izan ditzakezun guneak</string>
+ <string name="stats_months_and_years">Hilabeteak eta urteak</string>
+ <string name="stats_empty_video">Ez da bideorik erreproduzitu</string>
+ <string name="stats_entry_video_plays">Bideoak</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">E-posta</string>
+ <string name="stats_entry_top_commenter">Egilea</string>
+ <string name="stats_entry_publicize">Zerbitzua</string>
+ <string name="stats_entry_followers">Jarraitzailea</string>
+ <string name="stats_totals_publicize">Jarraitzaileak</string>
+ <string name="stats_entry_clicks_link">Esteka</string>
+ <string name="stats_view_top_posts_and_pages">Bidalketak eta orrialdeak</string>
+ <string name="stats_view_videos">Bideoak</string>
+ <string name="stats_view_followers">Jarraitzaileak</string>
+ <string name="stats_pagination_label">%2$setik %1$s. orrialdea</string>
+ <string name="stats_visitors">Bisitariak</string>
+ <string name="stats_other_recent_stats_label">Bestelako estatistika berriak</string>
+ <string name="stats_view_publicize">Publizitatea egin</string>
+ <string name="stats_empty_publicize_desc">Publizitatea erabiliz, egiezu jarraipena zure jarraitzaileei hainbat sare sozialetan.</string>
+ <string name="stats_empty_publicize">Ez dago publizitate bidezko jarraitzailerik</string>
+ <string name="stats_followers_months">%1$d hilabete</string>
+ <string name="stats_followers_a_minute_ago">duela minutu bat</string>
+ <string name="stats_followers_seconds_ago">duela segundo batzuk</string>
+ <string name="stats_timeframe_years">Urteak</string>
+ <string name="stats_view_countries">Estatuak</string>
+ <string name="ssl_certificate_details">Xehetasunak</string>
+ <string name="cab_selected">%d aukeratuta</string>
+ <string name="delete_sure_post">Ezabatu bidalketa hau</string>
+ <string name="delete_sure">Ezabatu zirriborro hau</string>
+ <string name="delete_sure_page">Ezabatu orrialde hau</string>
+ <string name="confirm_delete_multi_media">Ezabatu aukeratutako itemak?</string>
+ <string name="confirm_delete_media">Ezabatu aukeratutako itema?</string>
+ <string name="media_gallery_date_range">Erakutsi mediak %1$s(e)tik %2$s(e)ra</string>
+ <string name="sure_to_remove_account">Ezabatu gune hau?</string>
+ <string name="signing_out">Saioa ixten...</string>
+ <string name="more">Gehiago</string>
+ <string name="reader_label_comments_closed">Iruzkinak itxita daude</string>
+ <string name="reader_label_comment_count_single">Iruzkin bat</string>
+ <string name="reader_label_comment_count_multi">%,d iruzkin</string>
+ <string name="reader_empty_posts_in_tag">Ez dago bidalketarik etiketa honekin</string>
+ <string name="new_blog_wpcom_created">WordPress.com bloga sortuta!</string>
+ <string name="reader_empty_posts_liked">Ez duzu gustuko bidalketarik</string>
+ <string name="create_new_blog_wpcom">Sortu WordPress.com bloga</string>
+ <string name="reader_empty_comments">Oraindik ez dago iruzkinik</string>
+ <string name="reader_label_view_original">Ikusi artikulu originala</string>
+ <string name="reader_label_like">Gustukoa</string>
+ <string name="select_a_blog">Aukeratu WordPress gune bat</string>
+ <string name="reader_empty_followed_blogs_title">Oraindik ez duzu webgunetik jarraitzen</string>
+ <string name="posts_empty_list">Bidalketarik ez oraingoz. Zergatik ez duzu bat sortzen?</string>
+ <string name="pages_empty_list">Orrialderik ez oraingoz. Zergatik ez duzu bat sortzen?</string>
+ <string name="browse_our_faq_button">Arakatu Galdera eta Erantzunak</string>
+ <string name="nux_help_description">Bisitatu laguntza zentroa galdera ohikoenen erantzunak lortzeko edo bisitatu foroak galdera berriak egiteko</string>
+ <string name="faq_button">Ohiko galderak</string>
+ <string name="error_publish_empty_post">Ezin da bidalketa huts bat argitaratu</string>
+ <string name="older_two_days">2 egun baino zaharragoa</string>
+ <string name="older_last_week">Aste bat baino zaharragoa</string>
+ <string name="contact_us">Kontaktatu gurekin</string>
+ <string name="hs__username_blank_error">Sartu izen egoki bat</string>
+ <string name="hs__invalid_email_error">Sartu e-posta egoki bat</string>
+ <string name="hs__conversation_header">Laguntza txata</string>
+ <string name="add_location">Gehitu kokapena</string>
+ <string name="current_location">Egungo kokapena</string>
+ <string name="edit_location">Editatu</string>
+ <string name="search_location">Bilatu</string>
+ <string name="search_current_location">Kokatu</string>
+ <string name="preference_send_usage_stats">Bidali estatistikak</string>
+ <string name="preference_send_usage_stats_summary">Bidali erabilera estatistikak automatikoki WordPress Android-erako hobetzen laguntzeko</string>
+ <string name="update_verb">Eguneratu</string>
+ <string name="schedule_verb">Programatu</string>
+ <string name="reader_page_followed_blogs">Jarraitutako guneak</string>
+ <string name="reader_empty_recommended_blogs">Ez dago gomendatutako blogik</string>
+ <string name="reader_title_subs">Etiketak eta blogak</string>
+ <string name="saving">Gordetzen...</string>
+ <string name="media_empty_list">Ez dago mediarik</string>
+ <string name="help">Laguntza</string>
+ <string name="forums">Foroak</string>
+ <string name="ssl_certificate_error">SSL ziurtagiri okerra</string>
+ <string name="help_center">Laguntza zentroa</string>
+ <string name="forgot_password">Pasahitza galdu duzu?</string>
+ <string name="ssl_certificate_ask_trust">Normalean gune honetara arazo gabe konektatzen bazara, errore honek norbait zu ordezkatu nahian dabilela adieraz lezake eta ez zenuke aurrera jarraitu beharko. Hala ere ziurtagiriaz fidatu nahi duzu?</string>
+ <string name="passcode_wrong_passcode">PIN okerra</string>
+ <string name="invalid_password_message">Pasahitzak gutxienez 4 karaktere eduki behar ditu</string>
+ <string name="comments_empty_list">Ez dago iruzkinik.</string>
+ <string name="adding_cat_success">Kategoria zuzen gehitu da.</string>
+ <string name="stats_bar_graph_empty">Ez dago estatistikarik eskuragarri.</string>
+ <string name="error_refresh_pages">Orrialdeak ezin dira momentu honetan eguneratu.</string>
+ <string name="out_of_memory">Gailuaren memoria agortu da</string>
+ <string name="mnu_comment_unspam">Ez da spama</string>
+ <string name="no_site_error">Ezin izan da WordPressekin konektatu</string>
+ <string name="adding_cat_failed">Ezin izan da kategoria gehitu</string>
+ <string name="notifications_empty_list">Jakinarazpenik gabe</string>
+ <string name="error_generic">Errore bat gertatu da</string>
+ <string name="error_edit_comment">Iruzkina editatzean errore bat gertatu da</string>
+ <string name="error_moderate_comment">Iruzkina moderatzean errore bat gertatu da</string>
+ <string name="invalid_email_message">Zure posta helbidea ez da zuzena</string>
+ <string name="email_invalid">Sartu posta helbide zuzen bat</string>
+ <string name="email_not_allowed">Posta elektroniko helbide hau ez dago baimenduta</string>
+ <string name="email_exists">Posta elektroniko helbide hori dagoeneko erabilia da</string>
+ <string name="cat_name_required">Kategoria izena beharrezkoa da</string>
+ <string name="category_automatically_renamed">%1$s kategoria izena ez da zuzena. %2$s bezala berrizendatua izan da.</string>
+ <string name="no_account">Ez da Wordpress konturik aurkitu, gehitu kontu bat eta saiatu berriz</string>
+ <string name="error_delete_post">%s ezabatzean errore bat gertatu da</string>
+ <string name="error_refresh_notifications">Jakinarazpenak ezin izan dira eguneratu</string>
+ <string name="error_load_comment">Ezin izan da iruzkina kargatu</string>
+ <string name="error_downloading_image">Irudia deskargatzean errorea</string>
+ <string name="stats_empty_comments">Oraindik iruzkin gabe</string>
+ <string name="error_upload">%s igotzean errore bat gertatu da</string>
+ <string name="no_network_message">Ez dago sarerako konexiorik eskuragarri</string>
+ <string name="wait_until_upload_completes">Itxaron karga osatu bitartean</string>
+ <string name="blog_not_found">Blog honetara sartzean errore bat gertatu da</string>
+ <string name="nux_cannot_log_in">Ezin zaitugu sartu</string>
+ <string name="reply_failed">Erantzunak huts egin du</string>
+ <string name="error_refresh_comments">Iruzkinak ezin izan dira une honetan freskatu</string>
+ <string name="error_refresh_stats">Estatistikak ezin izan dira une honetan freskatu </string>
+ <string name="gallery_error">Ezin izan da media itema eskuratu</string>
+ <string name="sdcard_message">Mediak igotzeko muntatutako SD txartel bat beharrezkoa da</string>
+ <string name="blog_name_required">Sartu gune helbide bat</string>
+ <string name="blog_name_not_allowed">Gune helbide hori ez da onartzen</string>
+ <string name="blog_name_must_be_at_least_four_characters">Gunearen helbideak gutxienez 4 karaktere eduki behar ditu</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Gunearen helbideak 64 karaktere baino gutxiago eduki behar ditu</string>
+ <string name="blog_name_contains_invalid_characters">Gunearen helbideak ezin du “_” karaktererik eduki</string>
+ <string name="blog_name_cant_be_used">Ezin duzu gune helbide hori erabili</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Gunearen helbideak letra xeheak (a-z) eta zenbakiak bakarrik izan ditzake</string>
+ <string name="blog_name_exists">Gune hori dagoeneko existitzen da</string>
+ <string name="blog_name_reserved">Gune hori erreserbatuta dago</string>
+ <string name="blog_name_reserved_but_may_be_available">Gune hori momentu honetan erreserbatuta dago baina egun batzuk barru eskuragarri egon liteke</string>
+ <string name="theme_set_failed">Huts egin du itxura ezartzeak</string>
+ <string name="theme_auth_error_message">Ziurtatu zaitez itxurak aukeratzeko baimena duzula</string>
+ <string name="theme_fetch_failed">Huts egin du itxurak eskuratzeak</string>
+ <string name="error_refresh_posts">Bidalketak ezin dira momentu honetan eguneratu</string>
+ <string name="could_not_remove_account">Ezin izan da gunea ezabatu</string>
+ <string name="invalid_username_too_short">Erabiltzaile-izenak gutxienez 4 karaktere eduki behar ditu</string>
+ <string name="invalid_username_too_long">Erabiltzaile-izenak 64 karaktere baino gutxiago eduki behar ditu</string>
+ <string name="username_only_lowercase_letters_and_numbers">Erabiltzaile-izenak letra minuskulak (a-z) eta zenbakiak bakarrik eduki ditzake</string>
+ <string name="username_required">Sartu erabiltzaile-izen bat</string>
+ <string name="username_not_allowed">Erabiltzaile-izena ez dago baimenduta</string>
+ <string name="username_must_be_at_least_four_characters">Erabiltzaile-izenak gutxienez 4 karaktere izan behar ditu</string>
+ <string name="username_contains_invalid_characters">Erabiltzaile-izenak ezin du “_” karakterea eduki</string>
+ <string name="username_must_include_letters">Erabiltzaile-izenak gutxienez letra (a-z) eduki behar du</string>
+ <string name="username_exists">Erabiltzaile-izen hori dagoeneko existitzen da</string>
+ <string name="username_reserved_but_may_be_available">Momentu honetan erabiltzaile-izen hori erreserbatuta dago baina pare bat egunetan eskuragarri egon daiteke</string>
+ <string name="username_or_password_incorrect">Sartutako erabiltzaile-izena edo pasahitza ez dira zuzenak</string>
+ <string name="invalid_url_message">Ziurtatu sartutako URLa zuzena dela</string>
+ <string name="add_comment">Gehitu iruzkina</string>
+ <string name="comment_status_approved">Onartua</string>
+ <string name="comment_status_unapproved">Onartzeko zain</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Zakarrontzian</string>
+ <string name="edit_comment">Editatu iruzkina</string>
+ <string name="mnu_comment_approve">Onartu</string>
+ <string name="mnu_comment_unapprove">Baztertu</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Bidali zakarrontzira</string>
+ <string name="dlg_approving_comments">Onartua</string>
+ <string name="dlg_unapproving_comments">Baztertua</string>
+ <string name="dlg_spamming_comments">Spam bezala markatzen</string>
+ <string name="dlg_trashing_comments">Zakarrontzira bidaltzen</string>
+ <string name="dlg_confirm_trash_comments">Zakarrontzira bidali?</string>
+ <string name="trash">Zakarrontzia</string>
+ <string name="hint_comment_content">Iruzkina</string>
+ <string name="delete_draft">Ezabatu zirriborroa</string>
+ <string name="media_gallery_num_columns">Zutabe kopurua</string>
+ <string name="account_details">Kontuaren xehetasunak</string>
+ <string name="add_new_category">Gehitu kategoria berria</string>
+ <string name="category_name">Kategoriaren izena</string>
+ <string name="privacy_policy">Pribatutasun politika</string>
+ <string name="local_changes">Aldaketa lokalak</string>
+ <string name="wordpress_blog">WordPress-eko bloga</string>
+ <string name="select_categories">Kategoriak aukeratu</string>
+ <string name="create_a_link">Esteka sortu</string>
+ <string name="media_gallery_image_order">Irudien ordena</string>
+ <string name="local_draft">Zirriborro lokala</string>
+ <string name="view_in_browser">Nabigatzailean ikusi</string>
+ <string name="reader_toast_err_add_tag">Ezin izan da etiketa hau gehitu</string>
+ <string name="reader_toast_err_remove_tag">Ezin izna da etiketa hau kendu</string>
+ <string name="upload_failed">Igoerak huts egin du</string>
+ <string name="horizontal_alignment">Lerrokatze horizontala</string>
+ <string name="file_not_found">Ezin izan da igotzeko fitxategia aurkitu. Mugitua edo ezabatua izan da?</string>
+ <string name="trash_yes">Zakarrontzira bidali</string>
+ <string name="trash_no">Ez bidali zakarrontzira</string>
+ <string name="content_required">Iruzkina beharrezkoa da</string>
+ <string name="toast_comment_unedited">Iruzkina ez da aldatu</string>
+ <string name="comment_added">Iruzkina ondo gehitu da</string>
+ <string name="page_not_published">Orrialdearen egoera ez dago argitaratuta</string>
+ <string name="required_field">Beharrezko eremua</string>
+ <string name="scaled_image_error">Sartu eskalatutako zabalera balio bat</string>
+ <string name="learn_more">Gehiago jakin</string>
+ <string name="media_gallery_type_thumbnail_grid">Miniaturen sareta</string>
+ <string name="themes_live_preview">Zuzeneko aurrebista</string>
+ <string name="location_not_found">Kokaleku ezezaguna</string>
+ <string name="pending_review">Berrikuspenaren zain</string>
+ <string name="http_authorization_required">Baimena beharrezkoa</string>
+ <string name="add_account_blog_url">Blogaren helbidea</string>
+ <string name="connection_error">Errorea konektatzean</string>
+ <string name="cancel_edit">Ezeztatu editatutakoa</string>
+ <string name="media_gallery_edit">Editatu galeria</string>
+ <string name="media_error_no_permission">Ez duzu media bilduma ikusteko baimenik</string>
+ <string name="delete_post">Ezabatu bidalketa</string>
+ <string name="delete_page">Ezabatu orrialdea</string>
+ <string name="remove_account">Ezabatu gunea</string>
+ <string name="blog_removed_successfully">Gunea ondo ezabatu da</string>
+ <string name="saving_changes">Aldaketak gordetzen</string>
+ <string name="sure_to_cancel_edit_comment">Iruzkin honen edizioa ezeztatu?</string>
+ <string name="preview_page">Aurreikusi orrialdea</string>
+ <string name="preview_post">Aurreikusi bidalketa</string>
+ <string name="share_action_post">Bidalketa berria</string>
+ <string name="share_action_media">Media bilduma</string>
+ <string name="file_error_create">Ezin izan da media fitxategia igotzeko behin-behineko fitxategia sortu. Ziurtatu zaitez gailuak behar haina toki duela.</string>
+ <string name="open_source_licenses">Kode irekiko lizentziak</string>
+ <string name="post_format">Bidalketaren formatua</string>
+ <string name="new_post">Bidalketa berria</string>
+ <string name="new_media">Media berria</string>
+ <string name="view_site">Ikusi gunea</string>
+ <string name="error_blog_hidden">Blog hau ezkutatuta dago eta ezin izan da kargatu. Hobespenetan gaitu eta berriz saiatu.</string>
+ <string name="reader_share_link">Partekatu esteka</string>
+ <string name="email_hint">E-posta helbidea</string>
+ <string name="fatal_db_error">Errorea gertatu da aplikazioaren datu-basea sortzean. Saiatu aplikazioa berrinstalatzen.</string>
+ <string name="reader_title_applog">Aplikazioaren loga</string>
+ <string name="blog_name_must_include_letters">Gunearen helbideak gutxienez hizki bat (a-z) izan behar du</string>
+ <string name="blog_name_invalid">Gune helbide okerra</string>
+ <string name="blog_title_invalid">Gune izenburu okerra</string>
+ <string name="cannot_delete_multi_media_items">Mediaren bat ezin izan da orain ezabatu. Saiatu zaitez berriz beranduago.</string>
+ <string name="email_cant_be_used_to_signup">Ezin duzu e-posta helbide hori erabili izena emateko. Hornitzaile honekin arazoak edukitzen ari gara gure e-posta batzuk blokeatzen dituelako. Erabili beste hornitzaile bat.</string>
+ <string name="xmlrpc_error">Ezin izan da konektatu. Sartu xmlrpc.php-ra bide guztia zure gunean eta saiatu berriz.</string>
+ <string name="jetpack_message_not_admin">Jetpack plugina beharrezkoa da estatistikentzako. Jarri harremanetan guneko administratzailearekin.</string>
+ <string name="theme_current_theme">Egungo itxura</string>
+ <string name="theme_premium_theme">Premium itxura</string>
+ <string name="edit_post">Editatu bidalketa</string>
+ <string name="post_not_found">Errore bat gertatu da bidalketa kargatzean. Eguneratu bidalketak eta saiatu berriz.</string>
+ <string name="post_not_published">Bidalketaren egoera ez dago argitaratuta</string>
+ <string name="email_reserved">E-posta helbide hori dagoeneko erabilia izan da. Begiratu zure sarrera ontzia gaitze e-posta bilatzeko. Ez baduzu gaitzen egun batzuk barru berriz saia zaitezke.</string>
+ <string name="category_desc">Kategoriaren deskripzioa (hautazkoa)</string>
+ <string name="author_name">Egilearen izena</string>
+ <string name="author_email">Egilearen e-posta</string>
+ <string name="author_url">Egilearen URLa</string>
+ <string name="link_enter_url_text">Estekaren testua (hautazkoa)</string>
+ <string name="category_slug">Karegoriaren slug-a (hautazkoa)</string>
+ <string name="category_parent">Kategoriaren gurasoa (hautazkoa)</string>
+ <string name="http_credentials">HTTP egiaztagiria (hautazkoa)</string>
+ <string name="site_address">Zuk ostatatutako helbidea (URL)</string>
+ <string name="notifications_empty_all">Jakinarazpenik ez... oraingoz.</string>
+ <string name="media_gallery_settings_title">Galeriaren ezarpenak</string>
+ <string name="page_settings">Orrialdearen ezarpenak</string>
+ <string name="post_settings">Bidalketaren ezarpenak</string>
+ <string name="image_settings">Irudiaren ezarpenak</string>
+ <string name="invalid_site_url_message">Egiaztatu sartutako URL gunea baliozkoa dela</string>
+ <string name="share_url_page">Partekatu orrialdea</string>
+ <string name="share_link">Partekatu esteka</string>
+ <string name="deleting_page">Orrialdea ezabatzen</string>
+ <string name="deleting_post">Bidalketa ezabatzen</string>
+ <string name="share_url_post">Partekatu bidalketa</string>
+ <string name="creating_your_account">Zure kontua sortzen</string>
+ <string name="creating_your_site">Zure gunea sortzen</string>
+ <string name="reader_empty_posts_in_tag_updating">Bidalketak eskuratzen...</string>
+ <string name="error_refresh_media">Zerbait gaizki irten da media bilduma eguneratzean. Saiatu berriz beranduago.</string>
+ <string name="reader_label_reply">Erantzun</string>
+ <string name="video">Bideoa</string>
+ <string name="cant_share_no_visible_blog">Ezin da WordPress-en partekatu blog ikusgai bat ez baduzu</string>
+ <string name="comment_spammed">Iruzkina spam bezala markatuta</string>
+ <string name="download">Media deskargatzen</string>
+ <string name="reader_likes_multi">%,d pertsonak hau gustuko dute</string>
+ <string name="reader_toast_err_get_comment">Ezin izan da iruzkin hau eskuratu</string>
+ <string name="reader_likes_you_and_multi">Zuk eta %,d pertsonak hau gustuko duzue</string>
+ <string name="select_time">Aukeratu ordua</string>
+ <string name="reader_likes_you_and_one">Zuk eta beste batek hau gustuko duzue.</string>
+ <string name="select_date">Aukeratu eguna</string>
+ <string name="pick_photo">Aukeratu argazkia</string>
+ <string name="account_two_step_auth_enabled">Kontu honek bi pausotako autentikazioa gaiturik dauka. Joan WordPress.com-eko hobespenetara eta aplikazioarentzat pasahitz bat sortu.</string>
+ <string name="pick_video">Aukeratu bideoa</string>
+ <string name="reader_toast_err_get_post">Ezin izan da bidalketa hau eskuratu</string>
+ <string name="validating_user_data">Erabiltzaile-datuak egiaztatzen</string>
+ <string name="validating_site_data">Gunearen datuak egiaztatzen</string>
+ <string name="nux_welcome_create_account">Sortu kontua</string>
+ <string name="password_invalid">Pasahitz seguruago bat behar duzu. Ziurtatu gutxienez 7 karaktere erabiltzeaz, letra larri eta xeheak, zenbakiak eta karaktere bereziak nahasteaz.</string>
+ <string name="nux_tap_continue">Jarraitu</string>
+ <string name="signing_in">Saioa hasten...</string>
+ <string name="nux_add_selfhosted_blog">Gehitu zuk ostatatutako gunea</string>
+ <string name="nux_oops_not_selfhosted_blog">Hasi saioa WordPress.com-en</string>
+ <string name="reader_label_added_tag">%s gehitua</string>
+ <string name="connecting_wpcom">WordPress.com-era konektatzen</string>
+ <string name="reader_toast_err_tag_invalid">Hau ez da etiketa zuzen bat</string>
+ <string name="reader_toast_err_share_intent">Ezin izan da partekatu</string>
+ <string name="reader_toast_err_comment_failed">Ezin izan da zure iruzkina argitaratu </string>
+ <string name="reader_toast_err_view_image">Ezin izan da irudia ikusi</string>
+ <string name="reader_toast_err_url_intent">Ezin izan da %s ireki</string>
+ <string name="nux_tutorial_get_started_title">Hasi!</string>
+ <string name="empty_list_default">Zerrenda hau hutsik dago</string>
+ <string name="reader_share_subject">%s-tik partekatua</string>
+ <string name="reader_btn_share">Partekatu</string>
+ <string name="reader_btn_follow">Jarraitu</string>
+ <string name="media_add_popup_title">Gehitu media bildumara</string>
+ <string name="media_add_new_media_gallery">Sortu bilduma</string>
+ <string name="select_from_media_library">Aukeratu media bildumatik</string>
+ <string name="jetpack_not_found">Ez da Jetpack plugina aurkitu</string>
+ <string name="jetpack_message">Jetpack plugina ezinbestekoa da estatistikentzako. Jetpack instalatu nahi duzu?</string>
+ <string name="reader_untitled_post">(Izenbururik gabe)</string>
+ <string name="reader_btn_unfollow">Jarraitzen</string>
+ <string name="reader_label_removed_tag">%s ezabatua</string>
+ <string name="reader_likes_one">Pertsona batek hau gustuko du</string>
+ <string name="reader_likes_only_you">Hau gustuko duzu</string>
+ <string name="reader_toast_err_tag_exists">Dagoeneko etiketa hau jarraitzen duzu</string>
+ <string name="reader_empty_followed_tags">Ez duzu etiketatik jarraitzen</string>
+ <string name="create_account_wpcom">Sortu kontu bat WordPress.com-en</string>
+ <string name="username_invalid">Erabiltzaile-izen okerra</string>
+ <string name="reader_hint_comment_on_comment">Erantzun iruzkinari...</string>
+ <string name="button_next">Hurrengoa</string>
+ <string name="limit_reached">Mugara iritsi zara. Minutu bat barru berriz proba dezakezu. Lehenago saiatuz gero debekua luzatzea besterik ez duzu lortuko. Hau errore bat dela baderitzozu, kontaktatu laguntza zerbitzua. </string>
+ <string name="images">Irudiak</string>
+ <string name="custom_date">Data pertsonalizatua</string>
+ <string name="media_gallery_type">Mota</string>
+ <string name="media_gallery_type_squares">Karratuak</string>
+ <string name="media_edit_title_text">Izenburua</string>
+ <string name="share_action">Partekatu</string>
+ <string name="stats">Estatistikak</string>
+ <string name="stats_timeframe_days">Egunak</string>
+ <string name="stats_timeframe_weeks">Asteak</string>
+ <string name="stats_timeframe_months">Hilabeteak</string>
+ <string name="stats_entry_country">Herrialdea</string>
+ <string name="stats_entry_posts_and_pages">Izenburua</string>
+ <string name="stats_entry_tags_and_categories">Gaia</string>
+ <string name="passcode_enter_passcode">Sartu zure PINa</string>
+ <string name="all">Denak</string>
+ <string name="unattached">Txertatu gabe</string>
+ <string name="media_add_popup_capture_video">Bideoa grabatu</string>
+ <string name="stats_timeframe_today">Gaur</string>
+ <string name="stats_timeframe_yesterday">Atzo</string>
+ <string name="theme_activate_button">Gaitu</string>
+ <string name="theme_activating_button">Gaitzen</string>
+ <string name="share_action_title">Gehitu ...</string>
+ <string name="media_gallery_image_order_random">Ausazkoa</string>
+ <string name="themes_details_label">Xehetasunak</string>
+ <string name="post_excerpt">Laburpena</string>
+ <string name="stats_view_clicks">Klikak</string>
+ <string name="stats_view_referrers">Erreferentziak</string>
+ <string name="passcode_change_passcode">Aldatu PINa</string>
+ <string name="media_gallery_type_slideshow">Diapositiba pasea</string>
+ <string name="media_gallery_type_tiled">Mosaikoa</string>
+ <string name="media_gallery_type_circles">Zirkuluak</string>
+ <string name="media_gallery_image_order_reverse">Alderantzikatu</string>
+ <string name="stats_view_visitors_and_views">Bisitariak eta ikuspegiak</string>
+ <string name="stats_totals_clicks">Klikak</string>
+ <string name="themes_features_label">Ezaugarriak</string>
+ <string name="stats_totals_views">Ikuspegiak</string>
+ <string name="themes">Itxurak</string>
+ <string name="media_add_popup_capture_photo">Argazkia atera</string>
+ <string name="media_edit_caption_text">Legenda</string>
+ <string name="media_edit_description_text">Deskripzioa</string>
+ <string name="media_edit_title_hint">Sartu izenburu bat hemen</string>
+ <string name="media_edit_caption_hint">Sartu legenda bat hemen</string>
+ <string name="media_edit_description_hint">Sartu deskripzio bat hemen</string>
+ <string name="media_edit_success">Eguneratua</string>
+ <string name="theme_set_success">Itxura ondo ezarri da!</string>
+ <string name="stats_view_tags_and_categories">Etiketak eta kategoriak</string>
+ <string name="stats_entry_referrers">Erreferentzia</string>
+ <string name="stats_entry_authors">Egilea</string>
+ <string name="passcode_manage">Kudeatu PIN blokeatzea</string>
+ <string name="stats_totals_plays">Erreprodukzioak</string>
+ <string name="media_edit_failure">Huts egin du eguneratzeak</string>
+ <string name="theme_auth_error_title">Huts egin du itxurak eskuratzeak</string>
+ <string name="passcode_enter_old_passcode">Sartu zure PIN zaharra</string>
+ <string name="passcode_re_enter_passcode">Sartu PINa berriro</string>
+ <string name="passcode_set">PINaren ezarpena</string>
+ <string name="passcode_preference_title">PINaren blokeoa</string>
+ <string name="passcode_turn_off">Desblokeatu PINa</string>
+ <string name="passcode_turn_on">Blokeatu PINa</string>
+ <string name="upload">Igo</string>
+ <string name="discard">Baztertu</string>
+ <string name="new_notifications">%d jakinarazpen berri</string>
+ <string name="notifications">Jakinarazpenak</string>
+ <string name="more_notifications">eta %d gehiago.</string>
+ <string name="note_reply_successful">Erantzuna argitaratuta.</string>
+ <string name="sign_in">Hasi saioa</string>
+ <string name="loading">Kargatzen...</string>
+ <string name="httppassword">HTTP pasahitza</string>
+ <string name="httpuser">HTTP erabiltzaile-izena</string>
+ <string name="error_media_upload">Errore bat gertatu da mediak kargatzean</string>
+ <string name="publish_date">Argitaratu</string>
+ <string name="post_content">Edukia (egin tap testua eta multimedia gehitzeko)</string>
+ <string name="content_description_add_media">Gehitu media</string>
+ <string name="incorrect_credentials">Erabiltzaile-izen edo pasahitz okerra.</string>
+ <string name="password">Pasahitza</string>
+ <string name="username">Erabiltzaile-izena</string>
+ <string name="reader">Irakurlea</string>
+ <string name="featured">Nabarmendutako irudi bezala erabili</string>
+ <string name="no_network_title">Ez dago sarerik eskuragarri</string>
+ <string name="pages">Orrialdeak</string>
+ <string name="width">Zabalera</string>
+ <string name="page">Orrialdea</string>
+ <string name="anonymous">Anonimoa</string>
+ <string name="featured_in_post">Txertatu irudia mezuaren edukian</string>
+ <string name="caption">Legenda (hautazkoa)</string>
+ <string name="posts">Bidalketak</string>
+ <string name="post">Bidalketa</string>
+ <string name="blogusername">Blogeko erabiltzaile-izena</string>
+ <string name="ok">Ados</string>
+ <string name="upload_scaled_image">Igo eta estekatu eskalatutako irudiari</string>
+ <string name="scaled_image">Irudiaren zabalera eskalan</string>
+ <string name="scheduled">Programatuta</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Igotzen...</string>
+ <string name="app_title">WordPress Android-erako</string>
+ <string name="tos">Zerbitzuaren baldintzak</string>
+ <string name="version">Bertsioa:</string>
+ <string name="max_thumbnail_px_width">Lehenetsitako irudi zabalera</string>
+ <string name="image_alignment">Lerrokatzea</string>
+ <string name="refresh">Freskatu</string>
+ <string name="untitled">Izenbururik gabe</string>
+ <string name="edit">Editatu</string>
+ <string name="page_id">Orrialdea</string>
+ <string name="post_id">Bidalketa</string>
+ <string name="immediately">Berehala</string>
+ <string name="post_password">Pasahitza (hautazkoa)</string>
+ <string name="quickpress_add_alert_title">Sarbide zuzenaren izena</string>
+ <string name="today">Gaur</string>
+ <string name="settings">Ezarpenak</string>
+ <string name="share_url">Partekatu URLa</string>
+ <string name="quickpress_add_error">Sarbide zuzenaren izena ezin da hutsik egon.</string>
+ <string name="quickpress_window_title">Aukeratu Argitaratze Azkarra erabiltzeko blog bat</string>
+ <string name="publish_post">Argitaratu</string>
+ <string name="draft">Zirriborroa</string>
+ <string name="post_private">Pribatua</string>
+ <string name="upload_full_size_image">Igo eta irudi osora estekatu</string>
+ <string name="title">Izenburua</string>
+ <string name="categories">Kategoriak</string>
+ <string name="tags_separate_with_commas">Etiketak (banandu etiketak komak erabiliz)</string>
+ <string name="dlg_deleting_comments">Iruzkinak ezabatzen</string>
+ <string name="notification_vibrate">Bibratu</string>
+ <string name="notification_blink">Jakinarazpen keinukariaren argia</string>
+ <string name="notification_sound">Jakinarazpen soinua</string>
+ <string name="status">Egoera</string>
+ <string name="select_video">Aukeratu bideo bat bildumatik</string>
+ <string name="location">Kokalekua</string>
+ <string name="sdcard_title">SD txartela ezinbestekoa</string>
+ <string name="media">Media</string>
+ <string name="delete">Ezabatu</string>
+ <string name="none">Bat ere ez</string>
+ <string name="blogs">Blogak</string>
+ <string name="select_photo">Aukeratu argazki bat bildumatik</string>
+ <string name="error">Errorea</string>
+ <string name="cancel">Ezeztatu</string>
+ <string name="save">Gorde</string>
+ <string name="add">Gehitu</string>
+ <string name="reply">Erantzun</string>
+ <string name="no">Ez</string>
+ <string name="yes">Bai</string>
+ <string name="preview">Aurrebista</string>
+ <string name="category_refresh_error">Kategoria eguneraketa errorea</string>
+ <string name="on">en</string>
+ <string name="notification_settings">Jakinarazpen ezarpenak</string>
+</resources>
diff --git a/WordPress/src/main/res/values-fr/strings.xml b/WordPress/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..b215e3b40
--- /dev/null
+++ b/WordPress/src/main/res/values-fr/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrateur</string>
+ <string name="role_editor">Éditeur</string>
+ <string name="role_author">Auteur</string>
+ <string name="role_contributor">Contributeur</string>
+ <string name="role_follower">Abonné</string>
+ <string name="role_viewer">Lecteur</string>
+ <string name="error_post_my_profile_no_connection">Aucune connexion, impossible d\'enregistrer le profil</string>
+ <string name="alignment_none">Aucun</string>
+ <string name="alignment_left">Gauche</string>
+ <string name="alignment_right">Droit</string>
+ <string name="site_settings_list_editor_action_mode_title">Sélectionné %1$d</string>
+ <string name="error_fetch_users_list">Impossible de récupérer les utilisateurs du site</string>
+ <string name="plans_manage">Gérez votre plan sur\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Vous n\'avez pas encore de lecteur.</string>
+ <string name="people_fetching">Récupération des utilisateurs...</string>
+ <string name="title_follower">Abonné</string>
+ <string name="title_email_follower">E-mail de l’abonné</string>
+ <string name="people_empty_list_filtered_email_followers">Vous n’avez encore aucun e-mail d’abonné.</string>
+ <string name="people_empty_list_filtered_followers">Vous n’avez encore aucun abonné.</string>
+ <string name="people_empty_list_filtered_users">Vous n\'avez pas encore d\'utilisateur.</string>
+ <string name="people_dropdown_item_email_followers">E-mails des abonnés</string>
+ <string name="people_dropdown_item_viewers">Lecteurs</string>
+ <string name="people_dropdown_item_followers">Abonnés</string>
+ <string name="people_dropdown_item_team">Équipe</string>
+ <string name="invite_message_usernames_limit">Invitez jusqu’à 10 adresses de messagerie ou noms d’utilisateurs WordPress.com. Ceux nécessitant un nom d’utilisateur recevront les indications pour le créer.</string>
+ <string name="viewer_remove_confirmation_message">Si vous supprimez ce compte de lecteur/trice, il ou elle ne pourra plus se rendre sur ce site.\n\nVoulez-vous toujours supprimer ce compte ?</string>
+ <string name="follower_remove_confirmation_message">Une fois supprimé, cet abonné ne recevra plus de notifications à propos de ce site, à moins de se réabonner.\n\nVoulez-vous vraiment supprimer cet abonné ?</string>
+ <string name="follower_subscribed_since">Depuis le %1$s</string>
+ <string name="reader_label_view_gallery">Afficher la galerie</string>
+ <string name="error_remove_follower">Impossible de supprimer l\'abonné</string>
+ <string name="error_remove_viewer">Impossible de supprimer le lecteur</string>
+ <string name="error_fetch_email_followers_list">Impossible de récupérer les abonnés par e-mail</string>
+ <string name="error_fetch_followers_list">Impossible de récupérer les abonnés au site</string>
+ <string name="editor_failed_uploads_switch_html">La mise en ligne de certains fichiers a échoué. Vous ne pouvez pas \nrepasser en mode HTML dans l\'état actuel des choses.\nSupprimer toutes les mises en ligne échouées et continuer ?</string>
+ <string name="format_bar_description_html">Mode HTML</string>
+ <string name="visual_editor">Éditeur visuel</string>
+ <string name="image_thumbnail">Image miniature</string>
+ <string name="format_bar_description_ul">Liste non-ordonnée</string>
+ <string name="format_bar_description_ol">Liste ordonnée</string>
+ <string name="format_bar_description_more">Insérer séparateur More</string>
+ <string name="format_bar_description_media">Insérer un média</string>
+ <string name="format_bar_description_strike">Barré</string>
+ <string name="format_bar_description_quote">Bloc de citation</string>
+ <string name="format_bar_description_link">Insérer un lien</string>
+ <string name="format_bar_description_italic">Italique</string>
+ <string name="format_bar_description_underline">Souligné</string>
+ <string name="image_settings_save_toast">Modifications enregistrées</string>
+ <string name="image_caption">Légende</string>
+ <string name="image_alt_text">Texte alternatif</string>
+ <string name="image_link_to">Lien vers</string>
+ <string name="image_width">Largeur</string>
+ <string name="format_bar_description_bold">Gras</string>
+ <string name="image_settings_dismiss_dialog_title">Rejeter les modifications non enregistrées ?</string>
+ <string name="stop_upload_dialog_title">Arrêter la mise en ligne ?</string>
+ <string name="stop_upload_button">Arrêter la mise en ligne</string>
+ <string name="alert_error_adding_media">Erreur pendant l\'insertion d\'un média</string>
+ <string name="alert_action_while_uploading">Nous sommes en train de mettre des médias en ligne. Veuillez patienter jusqu\'à complétion des transferts.</string>
+ <string name="alert_insert_image_html_mode">Impossible d\'insérer directement un média en mode HTML. Veuillez passer en mode visuel.</string>
+ <string name="uploading_gallery_placeholder">Mise en ligne de la galerie...</string>
+ <string name="invite_error_some_failed">L\'invitation a été envoyée mais il y a eu des problèmes !</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Les invitations ont bien été envoyées</string>
+ <string name="tap_to_try_again">Tapez pour réessayer !</string>
+ <string name="invite_error_sending">Une erreur est survenue lors de l\'envoi de l\'invitation !</string>
+ <string name="invite_error_invalid_usernames_multiple">Envoi impossible : il y a des identifiants ou adresses de messagerie non valable</string>
+ <string name="invite_error_invalid_usernames_one">Envoi impossible : un identifiant ou une adresse de messagerie n\'est pas valable</string>
+ <string name="invite_error_no_usernames">Veuillez ajouter au moins un identifiant</string>
+ <string name="invite_message_info">(Facultatif) Vous pouvez saisir un message personnalisé, faisant jusqu\'à 500 caractères, qui sera inclus dans l\'invitation envoyée.</string>
+ <string name="invite_message_remaining_other">Restent %d caractères</string>
+ <string name="invite_message_remaining_one">Reste 1 caractère</string>
+ <string name="invite_message_remaining_zero">Reste 0 caractère</string>
+ <string name="invite_invalid_email">L\'adresse de messagerie «%s» n\'est pas valable</string>
+ <string name="invite_message_title">Message personnalisé</string>
+ <string name="invite_already_a_member">Un membre utilise déjà le nom d\'utilisateur «%s»</string>
+ <string name="invite_username_not_found">Aucun utilisateur trouvé pour le nom «%s» </string>
+ <string name="invite">Invitation</string>
+ <string name="invite_names_title">Noms d\'utilisateurs ou adresses de messagerie</string>
+ <string name="signup_succeed_signin_failed">Votre compte a été créé mais une erreur est survenue lors de votre connexion. Essayez de vous connecter avec votre nouvel identifiant et son mot de passe.</string>
+ <string name="send_link">Envoyer le lien</string>
+ <string name="my_site_header_external">Externe</string>
+ <string name="invite_people">Inviter des personnes</string>
+ <string name="label_clear_search_history">Supprimer l\'historique de recherche</string>
+ <string name="dlg_confirm_clear_search_history">Supprimer l\'historique de recherche ?</string>
+ <string name="reader_empty_posts_in_search_description">Aucune article trouvé pour %s dans votre langue</string>
+ <string name="reader_label_post_search_running">Recherche en cours...</string>
+ <string name="reader_label_related_posts">Lectures connexes</string>
+ <string name="reader_empty_posts_in_search_title">Aucun article trouvé</string>
+ <string name="reader_label_post_search_explainer">Recherche dans tous les blogs publics de WordPress.com</string>
+ <string name="reader_hint_post_search">Recherche dans WordPress.com</string>
+ <string name="reader_title_related_post_detail">Article connexe</string>
+ <string name="reader_title_search_results">Résultats pour %s</string>
+ <string name="preview_screen_links_disabled">Les liens sont désactivés sur l\'écran de prévisualisation</string>
+ <string name="draft_explainer">Cet article est un brouillon qui n\'a pas été publié</string>
+ <string name="send">Envoyer</string>
+ <string name="user_remove_confirmation_message">Si vous supprimez %1$s, cet utilisateur ne pourras plus accéder à ce site mais tout le contenu ayant été crée par %1$s restera sur le site.\n\nVoulez vous toujours supprimer cet utilisateur ?</string>
+ <string name="person_removed">%1$s a bien été supprimé</string>
+ <string name="person_remove_confirmation_title">Supprimer %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Les sites dans cette liste n’ont rien publié récemment</string>
+ <string name="people">Personnes</string>
+ <string name="edit_user">Modifier l’utilisateur</string>
+ <string name="role">Rôle</string>
+ <string name="error_remove_user">Impossible de supprimer l’utilisateur</string>
+ <string name="error_fetch_viewers_list">Impossible de récupérer les utilisateurs du site</string>
+ <string name="error_update_role">Impossible de mettre à jour le rôle utilisateur</string>
+ <string name="gravatar_camera_and_media_permission_required">Des permissions sont requises pour sélectionner une photo ou en prendre une</string>
+ <string name="error_updating_gravatar">Erreur lors de la mise à jour de votre Gravatar</string>
+ <string name="error_locating_image">Erreur de la localisation de l\'image recadrée</string>
+ <string name="error_refreshing_gravatar">Erreur lors de la mise en ligne de votre Gravatar</string>
+ <string name="gravatar_tip">Nouveau ! Tapez sur votre Gravatar pour le modifier !</string>
+ <string name="error_cropping_image">Erreur lors du recadrage de l\'image</string>
+ <string name="launch_your_email_app">Lancez votre application d\'e-mail</string>
+ <string name="checking_email">Vérification de l\'adresse e-mail</string>
+ <string name="not_on_wordpress_com">Pas de compte WordPress.com ?</string>
+ <string name="magic_link_unavailable_error_message">Actuellement indisponible. Veuillez saisir votre mot de passe</string>
+ <string name="check_your_email">Vérifiez vos e-mails</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Faites-vous envoyer par e-mail un lien pour vous connecter instantanément</string>
+ <string name="logging_in">Connexion</string>
+ <string name="enter_your_password_instead">Saisissez votre mot de passe à la place</string>
+ <string name="web_address_dialog_hint">Affiché publiquement quand vous commentez.</string>
+ <string name="jetpack_not_connected_message">L\'extension Jetpack est installée, mais pas connecté à WordPress.com. Voulez-vous connecter Jetpack ?</string>
+ <string name="username_email">Adresse e-mail ou identifiant</string>
+ <string name="jetpack_not_connected">L\'extension Jetpack n\'est pas connectée</string>
+ <string name="new_editor_reflection_error">L\'éditeur visuel n\'est pas compatible avec votre appareil.\nIl a été automatiquement désactivé.</string>
+ <string name="stats_insights_latest_post_no_title">(pas de titre)</string>
+ <string name="capture_or_pick_photo">Prenez une photo ou choisissez-en une</string>
+ <string name="plans_post_purchase_text_themes">Vous avez maintenant un accès illimité aux thèmes premium. Visualisez n\'importe quel thème sur votre thème pour commencer.</string>
+ <string name="plans_post_purchase_button_themes">Parcourez les thèmes</string>
+ <string name="plans_post_purchase_title_themes">Trouvez un thème premium parfait</string>
+ <string name="plans_post_purchase_button_video">Commencez un nouvel article</string>
+ <string name="plans_post_purchase_text_video">Vous pouvez mettre en ligne et héberger des vidéos sur votre site avec VideoPress et votre espace de stockage de média élargi.</string>
+ <string name="plans_post_purchase_title_video">Donnez vie à vos articles avec des vidéos </string>
+ <string name="plans_post_purchase_button_customize">Personnaliser mon site</string>
+ <string name="plans_post_purchase_text_customize">Vous avez désormais accès aux polices de caractères et couleurs personnalisées, ainsi qu\'à l\'éditeur CSS.</string>
+ <string name="plans_post_purchase_text_intro">Votre site trépigne d\'impatience ! Explorez maintenant les nouvelles fonctionnalités de votre site, et choisissez où vous souhaitez commencer.</string>
+ <string name="plans_post_purchase_title_customize">Personnalisez les polices et couleurs</string>
+ <string name="plans_post_purchase_title_intro">Félicitations ! À vous de jouer !</string>
+ <string name="export_your_content_message">Vos articles, pages, et réglages vont vous être envoyés par e-mail à l\'adresse %s.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Impossible de charger les plans</string>
+ <string name="export_your_content">Exporter votre contenu</string>
+ <string name="exporting_content_progress">Export en cours...</string>
+ <string name="export_email_sent">Message d’export envoyé !</string>
+ <string name="premium_upgrades_message">Vous avez des mises à jour premium actives sur votre site. Veuillez annuler vos mises à jour avant de supprimer votre site</string>
+ <string name="show_purchases">Voir les achats</string>
+ <string name="checking_purchases">Vérification des achats</string>
+ <string name="premium_upgrades_title">Mises à jour premium</string>
+ <string name="purchases_request_error">Quelque chose s’est mal passé. Impossible de lancer l’achat.</string>
+ <string name="delete_site_progress">Suppression du site...</string>
+ <string name="delete_site_summary">Cette action est irréversible. La suppression de votre site supprimera tout le contenu, les contributeurs et les domaines du site.</string>
+ <string name="delete_site_hint">Supprimer le site</string>
+ <string name="export_site_hint">Exportez votre site vers un fichier XML</string>
+ <string name="are_you_sure">Êtes vous sur ?</string>
+ <string name="export_site_summary">Si vous êtes sur, veuillez vous assurer de prendre le temps d\'exporter votre contenu maintenant. Il ne pourras pas être récupéré plus tard.</string>
+ <string name="keep_your_content">Gardez votre contenu</string>
+ <string name="domain_removal_hint">Les domaines qui ne fonctionneront plus une fois que vous aurez supprimé votre site.</string>
+ <string name="domain_removal_summary">Soyez prudents ! La suppression de votre site supprimera aussi le(s) domaine(s) listés ci-dessous.</string>
+ <string name="primary_domain">Domaine principal</string>
+ <string name="domain_removal">Retrait de domaine</string>
+ <string name="error_deleting_site_summary">Il y a eu une erreur durant la suppression de votre site. Veuillez contacter le support pour de l\'assistance.</string>
+ <string name="error_deleting_site">Erreur de suppression de site</string>
+ <string name="confirm_delete_site_prompt">Veuillez saisir %1$s dans le champ ci-dessous pour confirmer. Votre site partira à tout jamais.</string>
+ <string name="site_settings_export_content_title">Export du contenu</string>
+ <string name="contact_support">Contacter le support</string>
+ <string name="confirm_delete_site">Confirmez la suppression du site</string>
+ <string name="start_over_text">Si vous voulez un site mais ne voulez plus aucun de vos articles ou vos pages, notre équipe de support peut supprimer vos articles, pages, médias et commentaires pour vous.\n\nCeci gardera votre site et votre URL actifs, mais vous donnera un nouveau départ pour votre création de contenu. Contactez nous pour avoir votre contenu nettoyé.</string>
+ <string name="site_settings_start_over_hint">Recommencez votre site</string>
+ <string name="let_us_help">Laissez nous vous aider</string>
+ <string name="me_btn_app_settings">Réglages de l’app</string>
+ <string name="start_over">Recommencer</string>
+ <string name="editor_remove_failed_uploads">Retirer les mise en ligne ayant échoué</string>
+ <string name="editor_toast_failed_uploads">Certains envois de fichiers ont échoué. Vous ne pouvez pas\nenregistrer ou publier votre article dans cet état.\nSouhaitez-vous enlever toutes les mises en ligne ayant échoué ?</string>
+ <string name="comments_empty_list_filtered_trashed">Aucun commentaire dans la corbeille</string>
+ <string name="site_settings_advanced_header">Avancé</string>
+ <string name="comments_empty_list_filtered_pending">Aucun commentaire en attente</string>
+ <string name="comments_empty_list_filtered_approved">Aucun commentaire approuvé</string>
+ <string name="button_done">Terminé</string>
+ <string name="button_skip">Sauter</string>
+ <string name="site_timeout_error">Impossible de se connecter au site WordPress suite à un trop long délai d\'attente.</string>
+ <string name="xmlrpc_malformed_response_error">Impossible de se connecter. L\'installation de WordPress a répondu avec un document XML-RPC non valide.</string>
+ <string name="xmlrpc_missing_method_error">Impossible de se connecter. Le serveur ne dispose pas des méthodes XML-RPC requises.</string>
+ <string name="post_format_status">État</string>
+ <string name="post_format_video">Vidéo</string>
+ <string name="alignment_center">Centré</string>
+ <string name="theme_free">Gratuit</string>
+ <string name="theme_all">Tous</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Discussion</string>
+ <string name="post_format_gallery">Galerie</string>
+ <string name="post_format_image">Image</string>
+ <string name="post_format_link">Lien</string>
+ <string name="post_format_quote">Citation</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Information sur les formations et évènements WordPress.com (en ligne ou en personne).</string>
+ <string name="post_format_aside">En passant</string>
+ <string name="post_format_audio">Son</string>
+ <string name="notif_surveys">Opportunités de participer aux recherches et sondages de WordPress.com.</string>
+ <string name="notif_tips">Conseils pour tirer au mieux parti de WordPress.com</string>
+ <string name="notif_community">Communauté</string>
+ <string name="replies_to_my_comments">Réponses à mes commentaires</string>
+ <string name="notif_suggestions">Suggestions</string>
+ <string name="notif_research">Recherche</string>
+ <string name="site_achievements">Accomplissements du site</string>
+ <string name="username_mentions">Mentions de l\'identifiant</string>
+ <string name="likes_on_my_posts">Mentions "J\'aime" sur mes articles</string>
+ <string name="site_follows">Abonnements du site</string>
+ <string name="likes_on_my_comments">Mentions "J\'aime" sur mes commentaires</string>
+ <string name="comments_on_my_site">Commentaires sur mon site</string>
+ <string name="site_settings_list_editor_summary_other">%d éléments</string>
+ <string name="site_settings_list_editor_summary_one">Un élément</string>
+ <string name="approve_auto_if_previously_approved">Commentaires d’utilisateurs connus</string>
+ <string name="approve_auto">Tous les utilisateurs</string>
+ <string name="approve_manual">Aucun commentaire</string>
+ <string name="site_settings_paging_summary_other">%d commentaires par page</string>
+ <string name="site_settings_paging_summary_one">Un commentaire par page</string>
+ <string name="site_settings_multiple_links_summary_other">Requiert une approbation pour plus de %d liens</string>
+ <string name="site_settings_multiple_links_summary_one">Requiert une approbation pour plus d\'un lien</string>
+ <string name="site_settings_multiple_links_summary_zero">Requiert une approbation pour plus de 0 lien</string>
+ <string name="detail_approve_auto">Approuver automatiquement les commentaires de tout le monde.</string>
+ <string name="detail_approve_auto_if_previously_approved">Approuver automatiquement les commentaires de tous ceux ayant déjà eu un commentaire approuvé</string>
+ <string name="detail_approve_manual">Requiert l\'approbation manuelle des commentaires de tout le monde.</string>
+ <string name="filter_trashed_posts">Mis à la Corbeille</string>
+ <string name="days_quantity_one">Un jour</string>
+ <string name="days_quantity_other">%d jours</string>
+ <string name="filter_published_posts">Publiés</string>
+ <string name="filter_draft_posts">Brouillons</string>
+ <string name="filter_scheduled_posts">Programmés</string>
+ <string name="pending_email_change_snackbar">Cliquez sur le lien de vérification de l\'e-mail envoyé à %1$s pour confirmer votre nouvelle adresse.</string>
+ <string name="primary_site">Site principal</string>
+ <string name="web_address">Adresse web</string>
+ <string name="editor_toast_uploading_please_wait">Vous êtes actuellement en train de mettre en ligne un média. Veuillez patienter jusqu\'à ce que soit terminé.</string>
+ <string name="error_refresh_comments_showing_older">Les commentaires ne peuvent pas être actualisés en ce moment - affichage des commentaires plus anciens</string>
+ <string name="editor_post_settings_set_featured_image">Définir une image mise à la une</string>
+ <string name="editor_post_settings_featured_image">Image à la une</string>
+ <string name="new_editor_promo_desc">L\'application WordPress for Android inclus un nouveau et superbe éditeur visuel. Essayez-le en créant un nouvel article !</string>
+ <string name="new_editor_promo_title">Tout nouvel éditeur</string>
+ <string name="new_editor_promo_button_label">Super, merci !</string>
+ <string name="visual_editor_enabled">Éditeur visuel activé</string>
+ <string name="editor_content_placeholder">Partagez ici votre histoire...</string>
+ <string name="editor_page_title_placeholder">Titre de la page</string>
+ <string name="editor_post_title_placeholder">Titre de l\'article</string>
+ <string name="email_address">Adresse de messagerie</string>
+ <string name="preference_show_visual_editor">Afficher l\'éditeur visuel</string>
+ <string name="dlg_sure_to_delete_comments">Supprimer définitivement ces commentaires ?</string>
+ <string name="preference_editor">Éditeur</string>
+ <string name="dlg_sure_to_delete_comment">Supprimer définitivement ce commentaire ? </string>
+ <string name="mnu_comment_delete_permanently">Supprimer</string>
+ <string name="comment_deleted_permanently">Commentaire supprimé</string>
+ <string name="mnu_comment_untrash">Restaurer</string>
+ <string name="comments_empty_list_filtered_spam">Pas de commentaire indésirable</string>
+ <string name="could_not_load_page">Impossible de charger la page</string>
+ <string name="comment_status_all">Tout</string>
+ <string name="interface_language">Langue de l\'interface</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">À propos de l\'application</string>
+ <string name="error_post_account_settings">Impossible d\'enregistrer vos réglages de compte</string>
+ <string name="error_post_my_profile">Impossible d\'enregistrer votre profil</string>
+ <string name="error_fetch_account_settings">Impossible de récupérer vos réglages de compte</string>
+ <string name="error_fetch_my_profile">Impossible de récupérer votre profil</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, j\'ai compris</string>
+ <string name="stats_widget_promo_desc">Ajoutez le widget à votre écran d\'accueil afin d\'accéder à vos statistiques en un clic.</string>
+ <string name="stats_widget_promo_title">Widget Stats pour Écran d\'Accueil</string>
+ <string name="site_settings_unknown_language_code_error">Code de langue non reconnu</string>
+ <string name="site_settings_threading_dialog_description">Autoriser les commentaires à se faire en discussion imbriquées.</string>
+ <string name="site_settings_threading_dialog_header">Imbriquer jusqu\'à</string>
+ <string name="remove">Enlever</string>
+ <string name="search">Rechercher</string>
+ <string name="add_category">Ajouter la catégorie</string>
+ <string name="disabled">Désactivé(e)</string>
+ <string name="site_settings_image_original_size">Taille originale</string>
+ <string name="privacy_private">Votre site est visible uniquement pour vous et les utilisateurs que vous approuvez</string>
+ <string name="privacy_public_not_indexed">Votre site est visible par tous, mais demande aux moteurs de recherche de ne pas l\'indexer</string>
+ <string name="privacy_public">Votre site est visible par tous et peut être indexé par les moteurs de recherche</string>
+ <string name="about_me_hint">Quelques mots à propos de vous...</string>
+ <string name="public_display_name_hint">Le nom d\'affichage par défaut est votre nom d\'utilisateur s\'il n\'est pas défini.</string>
+ <string name="about_me">À propos de moi</string>
+ <string name="public_display_name">Public</string>
+ <string name="my_profile">Mon profil</string>
+ <string name="first_name">Prénom</string>
+ <string name="last_name">Nom</string>
+ <string name="site_privacy_public_desc">Permettre aux moteurs de recherche d’indexer votre site</string>
+ <string name="site_privacy_hidden_desc">Demander aux moteurs de recherche de ne pas indexer ce site</string>
+ <string name="site_privacy_private_desc">Je veux que mon site soit privé, visible seulement aux utilisateurs choisis.</string>
+ <string name="cd_related_post_preview_image">Prévisualisation de l\'image de l\'article connexe</string>
+ <string name="error_post_remote_site_settings">Impossible d\'enregistrer les infos du site</string>
+ <string name="error_fetch_remote_site_settings">Impossible de retrouver les infos du site</string>
+ <string name="error_media_upload_connection">Désolé, une erreur est survenue pendant l\'envoi du média</string>
+ <string name="site_settings_disconnected_toast">Déconnecté, modification désactivée.</string>
+ <string name="site_settings_unsupported_version_error">Version de WordPress non supportée</string>
+ <string name="site_settings_multiple_links_dialog_description">Approbation requise pour les commentaires incluant plus de liens que ce nombre.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Fermer automatiquement</string>
+ <string name="site_settings_close_after_dialog_description">Fermer automatiquement les commentaires.</string>
+ <string name="site_settings_paging_dialog_description">Scinder les fils de commentaires en pages multiples.</string>
+ <string name="site_settings_paging_dialog_header">Commentaires par page</string>
+ <string name="site_settings_close_after_dialog_title">Fermer les commentaires</string>
+ <string name="site_settings_blacklist_description">Quand un commentaire contient un de ces mots dans son contenu, nom, URL, email ou IP, il sera marqué en spam. Vous pouvez saisir des mots partiels, ainsi "press" correspondra à "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">Quand un commentaire contient un de ces mots dans son contenu, nom, URL, email ou IP, il sera déposé dans la file de modération. Vous pouvez saisir des mots partiels, ainsi "press" correspondra à "WordPress". </string>
+ <string name="site_settings_list_editor_input_hint">Entrez un mot ou une phrase</string>
+ <string name="site_settings_list_editor_no_items_text">0 éléments</string>
+ <string name="site_settings_learn_more_caption">Vous pouvez substituer ces paramètres pour les articles individuels.</string>
+ <string name="site_settings_rp_preview3_site">dans "Mise à jour"</string>
+ <string name="site_settings_rp_preview3_title">Mise en avant de l\'option : VideoPress pour les mariages</string>
+ <string name="site_settings_rp_preview2_site">dans "Apps"</string>
+ <string name="site_settings_rp_preview2_title">WordPress pour Android App a subi un gros lifting</string>
+ <string name="site_settings_rp_preview1_site">Dans "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Grosse mise à jour iPhone/iPad disponible.</string>
+ <string name="site_settings_rp_show_images_title">Afficher les images</string>
+ <string name="site_settings_rp_show_header_title">Afficher l\'en-tête</string>
+ <string name="site_settings_rp_switch_summary">Les articles similaires affichent du contenu pertinent de votre site sous les articles.</string>
+ <string name="site_settings_rp_switch_title">Afficher les articles similaires</string>
+ <string name="site_settings_delete_site_hint">Supprime les données de votre site de l\'application</string>
+ <string name="site_settings_blacklist_hint">Les commentaires qui correspondent à un filtrer son marqués comme indésirables</string>
+ <string name="site_settings_moderation_hold_hint">Les commentaires qui correspondent à un filtre sont mis dans la file d\'attente de modération</string>
+ <string name="site_settings_multiple_links_hint">Ignore la limite de liens pour les utilisateurs connus</string>
+ <string name="site_settings_whitelist_hint">L\'auteur d\'un commentaire doit avoir déjà au moins un commentaire approuvé</string>
+ <string name="site_settings_user_account_required_hint">Les utilisateurs doivent être enregistrés et connectés pour commenter</string>
+ <string name="site_settings_identity_required_hint">L\'auteur d\'un commentaire doit renseigner son nom et son adresse de messagerie</string>
+ <string name="site_settings_manual_approval_hint">Les commentaires doivent être approuvés manuellement</string>
+ <string name="site_settings_paging_hint">Afficher les commentaires en morceaux d\'une taille spécifiée.</string>
+ <string name="site_settings_threading_hint">Permettre les commentaires imbriqués à un certain niveau</string>
+ <string name="site_settings_sort_by_hint">Détermine l\'ordre d\'affichage des commentaires</string>
+ <string name="site_settings_close_after_hint">Interdire les commentaires après l\'heure indiquée</string>
+ <string name="site_settings_receive_pingbacks_hint">Autoriser les liens de notifications depuis les autres sites</string>
+ <string name="site_settings_send_pingbacks_hint">Tenter de notifier les sites liés depuis le contenu des articles</string>
+ <string name="site_settings_allow_comments_hint">Autoriser les lecteurs à publier des commentaires</string>
+ <string name="site_settings_discussion_hint">Voir et modifier les réglages de discussion de votre site</string>
+ <string name="site_settings_more_hint">Voir tous les réglages de discussion disponibles</string>
+ <string name="site_settings_related_posts_hint">Afficher ou cacher les articles similaires dans le lecteur</string>
+ <string name="site_settings_upload_and_link_image_hint">Activez pour toujours mettre en ligne l\'image en pleine grandeur</string>
+ <string name="site_settings_image_width_hint">Redimensionner les images dans les articles dans cette largeur. </string>
+ <string name="site_settings_format_hint">Configure le nouveau format d\'article</string>
+ <string name="site_settings_category_hint">Configure la nouvelle catégorie d\'articles</string>
+ <string name="site_settings_location_hint">Automatiquement ajouter les données de localisation pour vos articles.</string>
+ <string name="site_settings_password_hint">Changez votre mot de passe</string>
+ <string name="site_settings_username_hint">Compte utilisateur actuel</string>
+ <string name="site_settings_language_hint">La langue d\'écriture de ce blog est principalement en</string>
+ <string name="site_settings_privacy_hint">Contrôler qui peut voir votre site.</string>
+ <string name="site_settings_address_hint">Changer votre adresse n\'est actuellement pas supporté.</string>
+ <string name="site_settings_tagline_hint">Une brève description ou une phrase accrocheuse pour décrire votre blog</string>
+ <string name="site_settings_title_hint">En quelques mots, décrivez la raison d\'être de ce site.</string>
+ <string name="site_settings_whitelist_known_summary">Commentaires des utilisateurs anonymes</string>
+ <string name="site_settings_whitelist_all_summary">Commentaires de tous les utilisateurs</string>
+ <string name="site_settings_threading_summary">%d niveaux</string>
+ <string name="site_settings_privacy_private_summary">Privé</string>
+ <string name="site_settings_privacy_hidden_summary">Caché</string>
+ <string name="site_settings_delete_site_title">Supprimer le site</string>
+ <string name="site_settings_privacy_public_summary">Public</string>
+ <string name="site_settings_blacklist_title">Liste noire</string>
+ <string name="site_settings_moderation_hold_title">Garder en modération</string>
+ <string name="site_settings_multiple_links_title">Liens dans les commentaires</string>
+ <string name="site_settings_whitelist_title">Approuver automatiquement</string>
+ <string name="site_settings_threading_title">Fil de discussions</string>
+ <string name="site_settings_paging_title">Pagination</string>
+ <string name="site_settings_sort_by_title">Trier par</string>
+ <string name="site_settings_account_required_title">Les utilisateurs doivent être connectés</string>
+ <string name="site_settings_identity_required_title">Doit inclure le nom et l\'email</string>
+ <string name="site_settings_receive_pingbacks_title">Recevoir les pings</string>
+ <string name="site_settings_send_pingbacks_title">Envoi des pings</string>
+ <string name="site_settings_allow_comments_title">Autoriser les commentaires</string>
+ <string name="site_settings_default_format_title">Format par défaut</string>
+ <string name="site_settings_default_category_title">Catégorie par défaut</string>
+ <string name="site_settings_location_title">Activer la localisation</string>
+ <string name="site_settings_address_title">Adresse</string>
+ <string name="site_settings_title_title">Titre du site</string>
+ <string name="site_settings_tagline_title">Slogan</string>
+ <string name="site_settings_this_device_header">Cet appareil</string>
+ <string name="site_settings_discussion_new_posts_header">Par défaut pour les nouveaux articles</string>
+ <string name="site_settings_account_header">Compte</string>
+ <string name="site_settings_writing_header">Écriture</string>
+ <string name="newest_first">Récents en premier</string>
+ <string name="site_settings_general_header">Général</string>
+ <string name="discussion">Discussion</string>
+ <string name="privacy">Vie privée</string>
+ <string name="related_posts">Articles similaires</string>
+ <string name="comments">Commentaires</string>
+ <string name="close_after">Fermer après</string>
+ <string name="oldest_first"> Anciens en Premier</string>
+ <string name="media_error_no_permission_upload">Vous n\'avez pas l\'autorisation de mettre en ligne des fichiers média sur ce site</string>
+ <string name="never">Jamais</string>
+ <string name="unknown">Inconnu</string>
+ <string name="reader_err_get_post_not_found">Cet article n’existe plus</string>
+ <string name="reader_err_get_post_not_authorized">Vous n’avez pas les droits nécessaires pour voir cet article</string>
+ <string name="reader_err_get_post_generic">Impossible de retrouver cet article</string>
+ <string name="blog_name_no_spaced_allowed">L\'adresse du site ne peut pas contenir d\'espaces.</string>
+ <string name="invalid_username_no_spaces">Le nom d\'utilisateur ne peut pas contenir d\'espaces.</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Les sites que vous suivez n\'ont rien publié récemment.</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Pas d\'articles récents.</string>
+ <string name="media_details_copy_url_toast">URL copiée dans le presse-papiers.</string>
+ <string name="edit_media">Modifier le média.</string>
+ <string name="media_details_copy_url">Copier l\'URL.</string>
+ <string name="media_details_label_date_uploaded">Envoyé</string>
+ <string name="media_details_label_date_added">Ajouté</string>
+ <string name="selected_theme">Thème sélectionné</string>
+ <string name="could_not_load_theme">Impossible de charger le thème</string>
+ <string name="theme_activation_error">Quelque chose s\'est mal passé. Impossible d\'activer le thème.</string>
+ <string name="theme_by_author_prompt_append">par %1$s</string>
+ <string name="theme_prompt">Merci d\'avoir choisi %1$s</string>
+ <string name="theme_try_and_customize">Essayer et personnaliser</string>
+ <string name="theme_view">Afficher</string>
+ <string name="theme_details">Détails</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">FAIT</string>
+ <string name="theme_manage_site">GÉRER LE SITE</string>
+ <string name="title_activity_theme_support">Thèmes</string>
+ <string name="theme_activate">Activer</string>
+ <string name="date_range_start_date">Date de début</string>
+ <string name="date_range_end_date">Date de fin</string>
+ <string name="current_theme">Thème actuel</string>
+ <string name="customize">Personnaliser</string>
+ <string name="details">Détails</string>
+ <string name="support">Support</string>
+ <string name="active">Actif</string>
+ <string name="stats_referrers_spam_generic_error">Une erreur a eu lieu durant l\'opération. L\'élément n\'a pas été marqué comme indésirable.</string>
+ <string name="stats_referrers_marking_not_spam">Retirer l\'état d\'indésirable</string>
+ <string name="stats_referrers_unspam">Pas un indésirable</string>
+ <string name="stats_referrers_marking_spam">Marquer comme indésirable</string>
+ <string name="theme_auth_error_authenticate">Impossible de récupérer les thèmes : échec de l\'authentification de l\'utilisateur</string>
+ <string name="post_published">Article publié</string>
+ <string name="page_published">Page publiée</string>
+ <string name="post_updated">Article mis à jour</string>
+ <string name="page_updated">Page mise à jour</string>
+ <string name="stats_referrers_spam">Indésirable</string>
+ <string name="theme_no_search_result_found">Désolé, pas de thème trouvé.</string>
+ <string name="media_file_name">Nom du fichier : %s</string>
+ <string name="media_uploaded_on">Mis en ligne le : %s</string>
+ <string name="media_dimensions">Dimensions : %s</string>
+ <string name="upload_queued">En file d\'attente</string>
+ <string name="media_file_type">Type du fichier : %s</string>
+ <string name="reader_label_gap_marker">Charger plus d\'artilces</string>
+ <string name="notifications_no_search_results">Aucun site ne correspond à \'%s\'</string>
+ <string name="search_sites">Rechercher les sites</string>
+ <string name="notifications_empty_view_reader">Voir le Lecteur</string>
+ <string name="unread">Non lus</string>
+ <string name="notifications_empty_action_followers_likes">Soyez remarqué : commentez les articles que vous avez lus.</string>
+ <string name="notifications_empty_action_comments">Rejoignez une conversation : commentez les articles des sites que vous suivez.</string>
+ <string name="notifications_empty_action_unread">Rallumez la conversation : écrivez un nouvel article.</string>
+ <string name="notifications_empty_action_all">Soyez actif! Commentez les articles des sites que vous suivez.</string>
+ <string name="notifications_empty_likes">Pas (encore) de nouveaux "j\'aime" à afficher.</string>
+ <string name="notifications_empty_followers">Pas (encore) de nouveaux followers.</string>
+ <string name="notifications_empty_comments">Pas (encore) de nouveaux commentaires.</string>
+ <string name="notifications_empty_unread">Vous êtes à jour!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Vous devez d\'abord aller sur l\'écran Stats dans l\'appli, réessayez d\'ajouter le Widget ensuite.</string>
+ <string name="stats_widget_error_readd_widget">Veuillez retirer le widget et le rajouter ensuite.</string>
+ <string name="stats_widget_error_no_visible_blog">Les Stats ne sont pas accessibles sans un Site visible</string>
+ <string name="stats_widget_error_no_permissions">Votre compte WordPress.com n\'a pas accès aux Stats sur ce site.</string>
+ <string name="stats_widget_error_no_account">Veuillez vous connecter à WordPress</string>
+ <string name="stats_widget_error_generic">Les Stats ne peuvent pas être chargées</string>
+ <string name="stats_widget_loading_data">Chargement des données…</string>
+ <string name="stats_widget_name_for_blog">Stats du jour pour %1$s</string>
+ <string name="stats_widget_name">Stats du jour WordPress</string>
+ <string name="add_location_permission_required">Permission nécessaire pour ajouter la Geolocalisation</string>
+ <string name="add_media_permission_required">Droits requis pour ajouter du contenu multimédia</string>
+ <string name="access_media_permission_required">Droits requis pour accéder au contenu multimédia</string>
+ <string name="stats_enable_rest_api_in_jetpack">Pour voir vos stats, vous devez d\'abord activer le module Jetpack nommé JSON API.</string>
+ <string name="error_open_list_from_notification">Cet article ou cette page à été publié sur un autre site</string>
+ <string name="reader_short_comment_count_multi">%s Commentaires</string>
+ <string name="reader_short_comment_count_one">1 Commentaire</string>
+ <string name="reader_label_submit_comment">ENVOYER</string>
+ <string name="reader_hint_comment_on_post">Répondre à l\'article...</string>
+ <string name="reader_discover_visit_blog">Visiter %s</string>
+ <string name="reader_discover_attribution_blog">Publié à l\'origine sur %s</string>
+ <string name="reader_discover_attribution_author">Publié à l\'origine par %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Publié à l\'origine par %1$s sur %2$s</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_label_follow_count">%,d abonnés</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_menu_tags">Éditer les tags et les blogs</string>
+ <string name="reader_title_post_detail">Article du Reader</string>
+ <string name="local_draft_explainer">Cet article est un brouillon local qui n\'a pas été publié</string>
+ <string name="local_changes_explainer">Cet article contient des modifications locales qui n\'ont pas été publiées</string>
+ <string name="notifications_push_summary">Paramètres de notifications qui apparaissent sur votre appareil.</string>
+ <string name="notifications_email_summary">Paramètres de notifications qui vous sont envoyé par email.</string>
+ <string name="notifications_tab_summary">Paramètres de notifications qui apparaissent dans le tab Notifications.</string>
+ <string name="notifications_disabled">Les notifications de l\'Appli ont été désactivées. Toucher ici pour les activer dans les Paramètres.</string>
+ <string name="notification_types">Types de notifications</string>
+ <string name="error_loading_notifications">Les paramètres de notifications ne peuvent pas être chargés.</string>
+ <string name="replies_to_your_comments">Réponses à vos commentaires</string>
+ <string name="comment_likes">Likes de commentaires</string>
+ <string name="app_notifications">Notifications de l\'Appli</string>
+ <string name="notifications_tab">Tab de notifications</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Commentaires sur d\'autres sites</string>
+ <string name="notifications_wpcom_updates">Mis à jour WordPress.com</string>
+ <string name="notifications_other">Autre</string>
+ <string name="notifications_account_emails">Email de WordPress.com</string>
+ <string name="notifications_account_emails_summary">Nous vous enverrons des emails importants concernant votre compte, mais vous pouvez aussi recevoir des extras très utiles.</string>
+ <string name="notifications_sights_and_sounds">Vues et Sons</string>
+ <string name="your_sites">Vos Sites</string>
+ <string name="stats_insights_latest_post_trend">Cela fait %1$s que %2$s a été publié. Comment l\'article a évolué jusqu\'à maintenant…</string>
+ <string name="stats_insights_latest_post_summary">Résumé du dernier article</string>
+ <string name="button_revert">Annuler modifications</string>
+ <string name="days_ago">il y a %d jours</string>
+ <string name="yesterday">Hier</string>
+ <string name="connectionbar_no_connection">Pas de connexion</string>
+ <string name="page_trashed">Page envoyée à la corbeille</string>
+ <string name="post_deleted">Article supprimé</string>
+ <string name="post_trashed">Article envoyé à la corbeille</string>
+ <string name="stats_no_activity_this_period">Pas d\'activité durant cette période</string>
+ <string name="trashed">À la corbeille</string>
+ <string name="button_back">Retour</string>
+ <string name="page_deleted">Page supprimée</string>
+ <string name="button_stats">Stats</string>
+ <string name="button_trash">Corbeille</string>
+ <string name="button_preview">Aperçu</string>
+ <string name="button_view">Vue</string>
+ <string name="button_edit">Editer</string>
+ <string name="button_publish">Publier</string>
+ <string name="my_site_no_sites_view_subtitle">Voulez-vous en ajouter un ?</string>
+ <string name="my_site_no_sites_view_title">Vous n\'avez aucun site WordPress pour le moment.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Vous n\'êtes pas autorisé à accéder à ce blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">Ce blog est introuvable</string>
+ <string name="undo">Annuler</string>
+ <string name="tabbar_accessibility_label_my_site">Mon site</string>
+ <string name="tabbar_accessibility_label_me">Moi</string>
+ <string name="passcodelock_prompt_message">Entrer votre PIN</string>
+ <string name="editor_toast_changes_saved">Modifications enregistrées</string>
+ <string name="push_auth_expired">La requête a expiré. Inscrivez-vous sur WordPress.com et réessayez.</string>
+ <string name="stats_insights_best_ever">Record de vues absolu</string>
+ <string name="ignore">Ignorer</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de vues</string>
+ <string name="stats_insights_most_popular_hour">Heure la plus prisée</string>
+ <string name="stats_insights_most_popular_day">Jour le plus prisé</string>
+ <string name="stats_insights_popular">Jour et heure les plus prisés</string>
+ <string name="stats_insights_today">Statistiques du jour</string>
+ <string name="stats_insights_all_time">Articles, vues et visiteurs depuis la création</string>
+ <string name="stats_insights">Tendances</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Pour afficher vos statistiques, connectez-vous au compte WordPress.com que vous avez utilisé pour vous connecter à Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Vous recherchez vos autres statistiques récentes ? Elles se trouvent désormais sur la page Tendances.</string>
+ <string name="me_disconnect_from_wordpress_com">Se déconnecter de WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Se connecter à WordPress.com</string>
+ <string name="me_btn_login_logout">Se Connecter/Déconnecter</string>
+ <string name="account_settings">Paramètres du compte</string>
+ <string name="me_btn_support">Aide &amp; Support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" n\'a pas été caché, c\'est le site selectionné</string>
+ <string name="site_picker_create_dotcom">Créer un site WordPress.com</string>
+ <string name="site_picker_add_site">Ajouter un site</string>
+ <string name="site_picker_add_self_hosted">Ajouter un site auto-hébergé</string>
+ <string name="site_picker_edit_visibility">Montrer/cacher les sites</string>
+ <string name="my_site_btn_view_admin">Afficher l\'Admin</string>
+ <string name="my_site_btn_view_site">Afficher le site</string>
+ <string name="site_picker_title">Choisir un site</string>
+ <string name="my_site_btn_switch_site">Changer de Site</string>
+ <string name="my_site_btn_blog_posts">Articles</string>
+ <string name="my_site_btn_site_settings">Réglages</string>
+ <string name="my_site_header_look_and_feel">Personnaliser</string>
+ <string name="my_site_header_publish">Publier</string>
+ <string name="my_site_header_configuration">Configuration</string>
+ <string name="reader_label_new_posts_subtitle">Toucher pour les afficher</string>
+ <string name="notifications_account_required">Se connecter à WordPress.com pour voir vos notifications</string>
+ <string name="stats_unknown_author">Auteur inconnu</string>
+ <string name="image_added">Image ajoutée</string>
+ <string name="signout">Se déconnecter</string>
+ <string name="deselect_all">Tout déselectionner</string>
+ <string name="show">Montrer</string>
+ <string name="hide">Cacher</string>
+ <string name="select_all">Tout selectionner</string>
+ <string name="sign_out_wpcom_confirm">Se déconnecter de votre compte supprimera toutes les données liées à votre compte WordPress.com @%s’s sur cet appareil. Cela inclu les changements locaux et les brouillons.</string>
+ <string name="select_from_new_picker">Multi selection avec le nouveau selecteur</string>
+ <string name="stats_generic_error">Les stats n\'ont pas pu être chargées</string>
+ <string name="no_device_videos">Aucune vidéo</string>
+ <string name="no_blog_images">Aucune image</string>
+ <string name="no_blog_videos">Aucune vidéo</string>
+ <string name="no_device_images">Aucune image</string>
+ <string name="error_loading_blog_images">Impossible de récupérer les images</string>
+ <string name="error_loading_blog_videos">Impossible de récupérer les vidéos</string>
+ <string name="error_loading_images">Erreur pendant le chargement des images</string>
+ <string name="error_loading_videos">Erreur pendant le chargement des vidéos</string>
+ <string name="loading_blog_images">Récupération des images</string>
+ <string name="loading_blog_videos">Récupération des vidéos</string>
+ <string name="no_media_sources">Impossible de récupérer les medias</string>
+ <string name="loading_videos">Chargement des vidéos</string>
+ <string name="loading_images">Chargement des images</string>
+ <string name="no_media">Aucun média</string>
+ <string name="device">Appareil</string>
+ <string name="language">Langue</string>
+ <string name="add_to_post">Ajouter à l\'article</string>
+ <string name="media_picker_title">Sélectionner un média</string>
+ <string name="take_photo">Prendre une photo</string>
+ <string name="take_video">Prendre une vidéo</string>
+ <string name="tab_title_device_images">Images de l\'appareil</string>
+ <string name="tab_title_device_videos">Vidéos de l\'appareil</string>
+ <string name="tab_title_site_images">Images du site</string>
+ <string name="tab_title_site_videos">Videos du site</string>
+ <string name="media_details_label_file_name">Nom de fichier</string>
+ <string name="media_details_label_file_type">Type de fichier</string>
+ <string name="error_publish_no_network">Impossible de publier quand il n\'y a pas de connexion. Brouillon sauvegardé.</string>
+ <string name="editor_toast_invalid_path">Chemin de fichier invalide</string>
+ <string name="verification_code">Code de vérification</string>
+ <string name="invalid_verification_code">Code de vérification invalide</string>
+ <string name="verify">Vérifier</string>
+ <string name="two_step_footer_label">Entrer le code depuis votre application d\'authentification.</string>
+ <string name="two_step_footer_button">Code envoyé par SMS</string>
+ <string name="two_step_sms_sent">Vérifier que le code de vérification est dans vos SMS.</string>
+ <string name="sign_in_jetpack">Se connecter à votre compte WordPress.com pour vous connecter à Jetpack.</string>
+ <string name="auth_required">Se reconnecter pour continuer.</string>
+ <string name="reader_empty_posts_request_failed">Impossible de récupérer les articles</string>
+ <string name="publisher">Editeur:</string>
+ <string name="error_notification_open">Impossible d\'ouvrir la notification</string>
+ <string name="stats_followers_total_email_paged">Affichage de %1$d - %2$d sur %3$s Email Followers</string>
+ <string name="stats_search_terms_unknown_search_terms">Termes de recherche inconnus</string>
+ <string name="stats_followers_total_wpcom_paged">Affichage de %1$d - %2$d sur %3$s WordPress.com Followers</string>
+ <string name="stats_empty_search_terms_desc">En savoir plus sur votre trafic de recherche en regardant les termes utilisés par vos visiteurs pour trouver votre site.</string>
+ <string name="stats_empty_search_terms">Pas de termes de recherche enregistré</string>
+ <string name="stats_entry_search_terms">Terme de Recherche</string>
+ <string name="stats_view_authors">Auteurs</string>
+ <string name="stats_view_search_terms">Termes de Recherche</string>
+ <string name="comments_fetching">Récupération des commentaires…</string>
+ <string name="pages_fetching">Récupération des pages…</string>
+ <string name="toast_err_post_uploading">Impossible d\'ouvrir l\'article lorsqu\'il est en cours d\'upload</string>
+ <string name="posts_fetching">Récupération des articles…</string>
+ <string name="media_fetching">Récupération des médias…</string>
+ <string name="post_uploading">En cours d\'upload</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">Global</string>
+ <string name="stats_period">Période</string>
+ <string name="logs_copied_to_clipboard">Les logs de l\'application ont été copiés dans le presse papier</string>
+ <string name="reader_label_new_posts">Nouveaux articles</string>
+ <string name="reader_empty_posts_in_blog">Ce blog est vide</string>
+ <string name="stats_average_per_day">Moyenne par jour</string>
+ <string name="stats_recent_weeks">Semaines récentes</string>
+ <string name="error_copy_to_clipboard">Une erreur s\'est produite lors de la copie dans le presse papier</string>
+ <string name="reader_page_recommended_blogs">Sites que vous aimerez</string>
+ <string name="stats_months_and_years">Mois et années</string>
+ <string name="themes_fetching">Récupération des thèmes…</string>
+ <string name="stats_for">Statistiques de %s</string>
+ <string name="stats_other_recent_stats_label">Autres statistiques récentes</string>
+ <string name="stats_view_all">Afficher tout</string>
+ <string name="stats_view">Afficher</string>
+ <string name="stats_followers_months">%1$d mois</string>
+ <string name="stats_followers_a_year">Un an</string>
+ <string name="stats_followers_years">%1$d années</string>
+ <string name="stats_followers_a_month">Un mois</string>
+ <string name="stats_followers_minutes">%1$d minutes</string>
+ <string name="stats_followers_an_hour_ago">il y a une heure</string>
+ <string name="stats_followers_hours">%1$d heures</string>
+ <string name="stats_followers_a_day">Un jour</string>
+ <string name="stats_followers_days">%1$d jours</string>
+ <string name="stats_followers_a_minute_ago">il y a une minute</string>
+ <string name="stats_followers_seconds_ago">il y a quelques secondes</string>
+ <string name="stats_followers_total_email">Total des abonnés par email : %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Total des abonnés WordPress.com : %1$s</string>
+ <string name="stats_comments_total_comments_followers">Nombre d’articles commentés par les abonnés : %1$s</string>
+ <string name="stats_comments_by_authors">Par auteurs</string>
+ <string name="stats_comments_by_posts_and_pages">Par articles et pages</string>
+ <string name="stats_empty_followers_desc">Suivez le nombre total de vos abonnés ainsi que depuis combien de temps chacun suit votre site.</string>
+ <string name="stats_empty_followers">Aucun abonné</string>
+ <string name="stats_empty_publicize_desc">Publicize vous permet de suivre vos abonnés qui utilisent différents réseaux sociaux.</string>
+ <string name="stats_empty_publicize">Aucun abonné Publicize enregistré</string>
+ <string name="stats_empty_video">Aucune vidéo lue</string>
+ <string name="stats_empty_video_desc">Si vous avez mis en ligne des vidéos via VideoPress, découvrez combien de fois elles ont été regardées.</string>
+ <string name="stats_empty_comments_desc">Si vous autorisez les commentaires sur votre site, suivez les personnes qui commentent le plus et découvrez quel contenu déclenche les conversations les plus animées, selon les 1 000 derniers commentaires.</string>
+ <string name="stats_empty_tags_and_categories_desc">Obtenez un aperçu des sujets les plus populaires sur votre site en fonction des articles phares de la semaine écoulée.</string>
+ <string name="stats_empty_top_authors_desc">Suivez le nombre de consultations des articles de chaque contributeur et découvrez le contenu le plus populaire pour chaque auteur.</string>
+ <string name="stats_empty_tags_and_categories">Aucun article ou page tagué vu</string>
+ <string name="stats_empty_clicks_desc">Lorsque votre contenu inclut des liens vers d\'autres sites, vous découvrirez sur lesquels vos visiteurs cliquent le plus.</string>
+ <string name="stats_empty_referrers_desc">Apprenez-en davantage sur la visibilité de votre site en analysant les sites Web et les moteurs de recherche d\'où proviennent la majorité de vos visiteurs</string>
+ <string name="stats_empty_clicks_title">Aucun clic enregistré</string>
+ <string name="stats_empty_referrers_title">Aucun référant enregistré</string>
+ <string name="stats_empty_top_posts_title">Aucun article ou page vu</string>
+ <string name="stats_empty_top_posts_desc">Découvrez quel contenu a été le plus regardé et consultez les performances de chaque article et chaque page au fil du temps.</string>
+ <string name="stats_totals_followers">Depuis</string>
+ <string name="stats_empty_geoviews">Aucun pays enregistré</string>
+ <string name="stats_empty_geoviews_desc">Cette liste vous permet de connaître les pays et les régions d\'où viennent la majorité de vos visiteurs.</string>
+ <string name="stats_entry_video_plays">Vidéo</string>
+ <string name="stats_entry_top_commenter">Auteur</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_followers">Abonné</string>
+ <string name="stats_totals_publicize">Abonnés</string>
+ <string name="stats_entry_clicks_link">Lien</string>
+ <string name="stats_view_top_posts_and_pages">Articles et pages</string>
+ <string name="stats_view_videos">Vidéos</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Abonnés</string>
+ <string name="stats_view_countries">Pays</string>
+ <string name="stats_likes">Aime</string>
+ <string name="stats_pagination_label">Page %1$s de %2$s</string>
+ <string name="stats_timeframe_years">Années</string>
+ <string name="stats_views">Visites</string>
+ <string name="stats_visitors">Visiteurs</string>
+ <string name="ssl_certificate_details">Détails</string>
+ <string name="delete_sure_post">Supprimer cet article</string>
+ <string name="delete_sure">Supprimer ce brouillon</string>
+ <string name="delete_sure_page">Supprimer cette page</string>
+ <string name="confirm_delete_multi_media">Supprimer les elements sélectionnés ?</string>
+ <string name="confirm_delete_media">Supprimer l\'element sélectionné ?</string>
+ <string name="cab_selected">%d sélectionnés</string>
+ <string name="media_gallery_date_range">Affiche les médias entre %1$s et %2$s</string>
+ <string name="sure_to_remove_account">Supprimer ce site ?</string>
+ <string name="reader_empty_followed_blogs_title">Vous ne suivez aucun site pour le moment</string>
+ <string name="reader_empty_posts_liked">Vous n\'aimez aucun article</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Feuilleter notre FAQ</string>
+ <string name="nux_help_description">Visitez le centre d\'aide pour obtenir des réponses aux questions les plus communes ou bien visitez le forum pour en poser de nouvelles.</string>
+ <string name="agree_terms_of_service">En créant un compte, vous acceptez les %1$sconditions d\'utilisations%2$s</string>
+ <string name="create_new_blog_wpcom">Créer votre blog WordPress.com</string>
+ <string name="new_blog_wpcom_created">Blog WordPress.com créé!</string>
+ <string name="reader_empty_comments">Pas encore de commentaires</string>
+ <string name="reader_empty_posts_in_tag">Pas d\'articles avec ce tag</string>
+ <string name="reader_label_comment_count_multi">%,d commentaires</string>
+ <string name="reader_label_view_original">Voir l\'article original</string>
+ <string name="reader_label_like">Aime</string>
+ <string name="reader_label_liked_by">Aimé par</string>
+ <string name="reader_label_comment_count_single">Un commentaire</string>
+ <string name="reader_label_comments_closed">Les commentaires sont fermés</string>
+ <string name="reader_label_comments_on">Commentaires sur</string>
+ <string name="reader_title_photo_viewer">%1$d sur %2$d</string>
+ <string name="error_publish_empty_post">Impossible de publier un article vide</string>
+ <string name="error_refresh_unauthorized_posts">Vous n\'avez pas la permission de voir ou éditer les articles</string>
+ <string name="error_refresh_unauthorized_pages">Vous n\'avez pas la permission de voir ou éditer les pages</string>
+ <string name="error_refresh_unauthorized_comments">Vous n\'avez pas la permission de voir ou éditer les commentaires</string>
+ <string name="older_month">Plus d\'un mois</string>
+ <string name="more">Plus</string>
+ <string name="older_two_days">Plus de 2 jours</string>
+ <string name="older_last_week">Plus d\'une semaine</string>
+ <string name="stats_no_blog">Les statistiques ne peuvent pas être chargée pour ce blog</string>
+ <string name="select_a_blog">Sélectionner un site WordPress</string>
+ <string name="sending_content">Upload du contenu de %s</string>
+ <string name="uploading_total">Upload %1$d sur %2$d</string>
+ <string name="mnu_comment_liked">Aimé</string>
+ <string name="comment">Commentaire</string>
+ <string name="comment_trashed">Commentaire supprimé</string>
+ <string name="posts_empty_list">Pas encore d\'article. Pourquoi ne pas en créer un?</string>
+ <string name="comment_reply_to_user">Réponse à %s</string>
+ <string name="pages_empty_list">Pas encore de page. Pourquoi ne pas en créer une?</string>
+ <string name="media_empty_list_custom_date">Pas de média dans cet interval de temps</string>
+ <string name="posting_post">Publication de "%s"</string>
+ <string name="signing_out">Déconnexion…</string>
+ <string name="reader_toast_err_generic">Impossible d\'effectuer cette action</string>
+ <string name="reader_toast_err_block_blog">Impossible de bloquer ce blog</string>
+ <string name="reader_toast_blog_blocked">Les articles de ce blog ne seront plus affichés</string>
+ <string name="reader_menu_block_blog">Bloquer ce blog</string>
+ <string name="contact_us">Contactez nous</string>
+ <string name="hs__conversation_detail_error">Décrivez le problème rencontré</string>
+ <string name="hs__new_conversation_header">Discussion support</string>
+ <string name="hs__conversation_header">Discussion support</string>
+ <string name="hs__username_blank_error">Entrer un nom valide</string>
+ <string name="hs__invalid_email_error">Entrer une adresse email valide</string>
+ <string name="add_location">Ajouter localisation</string>
+ <string name="current_location">Localisation courante</string>
+ <string name="search_location">Rechercher</string>
+ <string name="edit_location">Editer</string>
+ <string name="search_current_location">Localiser</string>
+ <string name="preference_send_usage_stats">Envoyer statistiques</string>
+ <string name="preference_send_usage_stats_summary">Envoyer automatiquement des statistiques d\'utilisation pour nous aider à améliorer WordPress pour Android</string>
+ <string name="update_verb">Mettre à jour</string>
+ <string name="schedule_verb">Programmer</string>
+ <string name="reader_title_blog_preview">Blog du Reader</string>
+ <string name="reader_title_tag_preview">Tag du Reader</string>
+ <string name="reader_title_subs">Tags &amp; blogs</string>
+ <string name="reader_page_followed_tags">Tags suivis</string>
+ <string name="reader_page_followed_blogs">Sites suivis</string>
+ <string name="reader_hint_add_tag_or_url">Saisissez une étiquette ou une adresse web à suivre</string>
+ <string name="reader_label_followed_blog">Abonné au blog</string>
+ <string name="reader_label_tag_preview">Posts tagués %s</string>
+ <string name="reader_toast_err_get_blog_info">Impossible d\'afficher ce blog</string>
+ <string name="reader_toast_err_already_follow_blog">Vous êtes déjà abonné à ce blog</string>
+ <string name="reader_toast_err_follow_blog">Impossible de s\'abonner à ce blog</string>
+ <string name="reader_toast_err_unfollow_blog">Impossible de se désabonner de ce blog</string>
+ <string name="reader_empty_recommended_blogs">Pas de blogs recommandés</string>
+ <string name="saving">Sauvegarde...</string>
+ <string name="media_empty_list">Pas de media</string>
+ <string name="ptr_tip_message">Tip: Tirer vers le bas pour rafraichir</string>
+ <string name="help">Aide</string>
+ <string name="forgot_password">Mot de passe perdu ?</string>
+ <string name="forums">Forums</string>
+ <string name="help_center">Centre d\'aide</string>
+ <string name="ssl_certificate_error">Certificat SSL invalide</string>
+ <string name="ssl_certificate_ask_trust">Si vous vous connectez habituellement à ce site sans problème, cette erreur peut signifier que quelqu\'un essai de se faire passer pour vous et vous ne devriez pas continuer. Souhaitez-vous faire confiance à ce certificat ?</string>
+ <string name="out_of_memory">L\'appareil n\'a plus assez de mémoire</string>
+ <string name="no_network_message">Aucun réseau disponible</string>
+ <string name="could_not_remove_account">Impossible de supprimer le site</string>
+ <string name="gallery_error">Le média ne peut pas être récupéré</string>
+ <string name="blog_not_found">Une erreur s\'est produite en accédant à ce blog</string>
+ <string name="wait_until_upload_completes">Patientez jusqu\'au chargement complet</string>
+ <string name="theme_fetch_failed">Échec de chargement des thèmes</string>
+ <string name="theme_set_failed">Échec de définition du thème</string>
+ <string name="theme_auth_error_message">Vérifiez que vous disposez des droits pour définir les thèmes</string>
+ <string name="comments_empty_list">Aucun commentaire</string>
+ <string name="mnu_comment_unspam">Non indésirable</string>
+ <string name="no_site_error">Impossible de se connecter au site WordPress</string>
+ <string name="adding_cat_failed">Échec de l\'ajout de catégorie</string>
+ <string name="adding_cat_success">Catégorie ajoutée avec succès</string>
+ <string name="cat_name_required">Le champ du nom de catégorie est requis</string>
+ <string name="category_automatically_renamed">Le nom de la catégorie %1$s est invalide. Il a été renommé en %2$s.</string>
+ <string name="no_account">Aucun compte WordPress n\'a été trouvé, ajoutez un compte et réessayez</string>
+ <string name="sdcard_message">Une carte SD est nécessaire pour charger les médias</string>
+ <string name="stats_empty_comments">Aucun commentaire</string>
+ <string name="stats_bar_graph_empty">Aucune statistique disponible</string>
+ <string name="invalid_url_message">Vérifiez que l\'URL du blog est valide</string>
+ <string name="reply_failed">Échec de la réponse</string>
+ <string name="notifications_empty_list">Aucune notifications</string>
+ <string name="error_delete_post">Une erreur s\'est produite lors de la suppression de %s</string>
+ <string name="error_refresh_posts">Les articles ne peuvent pas être actualisés pour l\'instant</string>
+ <string name="error_refresh_pages">Les pages ne peuvent pas être actualisées pour l\'instant </string>
+ <string name="error_refresh_notifications">Les notifications ne peuvent être actualisées pour l\'instant</string>
+ <string name="error_refresh_comments">Les commentaires ne peuvent être actualisés pour l\'instant</string>
+ <string name="error_refresh_stats">Les statistiques ne peuvent être actualisées pour l\'instant</string>
+ <string name="error_generic">Une erreur s\'est produite</string>
+ <string name="error_moderate_comment">Une erreur s\'est produite lors de la modération</string>
+ <string name="error_edit_comment">Une erreur s\'est produite lors de la modification du commentaire</string>
+ <string name="error_upload">Une erreur s\'est produite lors du chargement de %s</string>
+ <string name="error_load_comment">Ne peut pas charger le commentaire</string>
+ <string name="error_downloading_image">Erreur du téléchargement d\'image</string>
+ <string name="passcode_wrong_passcode">Mauvais code PIN</string>
+ <string name="invalid_email_message">Votre adresse e-mail est invalide</string>
+ <string name="invalid_password_message">Le mot de passe doit contenir au moins 4 caractères</string>
+ <string name="invalid_username_too_short">L\'identifiant doit doit posséder plus de 4 caractères</string>
+ <string name="invalid_username_too_long">L\'identifiant doit être plus court que 61 caractères</string>
+ <string name="username_only_lowercase_letters_and_numbers">L\'identifiant ne doit contenir que des lettres minuscules (a-z) et des nombres</string>
+ <string name="username_required">Entrez un identifiant</string>
+ <string name="username_not_allowed">L\'identifiant n\'est pas autorisé</string>
+ <string name="username_must_be_at_least_four_characters">L\'identifiant doit posséder au moins 4 caractères</string>
+ <string name="username_contains_invalid_characters">L\'identifiant ne doit pas contenir "_"</string>
+ <string name="username_must_include_letters">L\'identifiant doit posséder au moins 1 caractère (a-z)</string>
+ <string name="email_invalid">Entrez une adresse e-mail valide</string>
+ <string name="email_not_allowed">Cet adresse e-mail n\'est pas autorisée</string>
+ <string name="username_exists">Cet identifiant existe déjà</string>
+ <string name="email_exists">Cette adresse e-mail est déjà utilisée</string>
+ <string name="username_reserved_but_may_be_available">Cet identifiant est déjà réservé mais pourrait être disponible dans quelques jours</string>
+ <string name="blog_name_required">Entrer une adresse de site</string>
+ <string name="blog_name_not_allowed">Cette adresse n\'est pas autorisée</string>
+ <string name="blog_name_must_be_at_least_four_characters">L\'adresse du site doit avoir au moins 4 caractères</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">L\'adresse du site doit avoir moins de 64 caractères</string>
+ <string name="blog_name_contains_invalid_characters">L\'adresse du site ne peut pas contenir le caractère "_"</string>
+ <string name="blog_name_cant_be_used">Vous ne devriez pas utiliser cette adresse</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">L\'adresse du site peut uniquement contenir des minuscules (a-z) et des nombres</string>
+ <string name="blog_name_exists">Ce site existe déjà</string>
+ <string name="blog_name_reserved">Ce site est réservé</string>
+ <string name="blog_name_reserved_but_may_be_available">Ce site est réservé mais pourrait être disponible dans quelques jours</string>
+ <string name="username_or_password_incorrect">L\'identifiant ou le mot de passe saisi est incorrect</string>
+ <string name="nux_cannot_log_in">Nous ne pouvons pas vous connecter</string>
+ <string name="xmlrpc_error">Connexion impossible. Entrer le chemin complet vers xmlrpc.php de votre site et essayez à nouveau.</string>
+ <string name="select_categories">Sélectionner les catégories</string>
+ <string name="account_details">Détails du compte</string>
+ <string name="edit_post">Modifier l\'article</string>
+ <string name="add_comment">Ajouter un commentaire</string>
+ <string name="connection_error">Erreur de connexion</string>
+ <string name="cancel_edit">Annuler la modification</string>
+ <string name="scaled_image_error">Saisir une largeur valide d\'image à l\'échelle</string>
+ <string name="post_not_found">Une erreur s\'est produite en chargeant cet article. Actualisez vos articles et essayez de nouveau.</string>
+ <string name="learn_more">En savoir plus</string>
+ <string name="media_gallery_settings_title">Paramètres de la galerie</string>
+ <string name="media_gallery_image_order">Ordre des images</string>
+ <string name="media_gallery_num_columns">Nombre de colonnes</string>
+ <string name="media_gallery_type_thumbnail_grid">Grille de vignettes</string>
+ <string name="media_gallery_edit">Modifier la galerie</string>
+ <string name="media_error_no_permission">Vous n\'avez pas la permission de voir la bibliothèque de média</string>
+ <string name="cannot_delete_multi_media_items">Certains média ne peuvent pas être supprimés pour l\'instant. Veuillez réessayer plus tard.</string>
+ <string name="themes_live_preview">Prévisualisation instantanée</string>
+ <string name="theme_current_theme">Thème actuel</string>
+ <string name="theme_premium_theme">Thème premium</string>
+ <string name="link_enter_url_text">Texte du lien (optionnel)</string>
+ <string name="create_a_link">Créer un lien</string>
+ <string name="page_settings">Paramètres de page</string>
+ <string name="local_draft">Brouillon local</string>
+ <string name="upload_failed">Échec du chargement</string>
+ <string name="horizontal_alignment">Alignement horizontal</string>
+ <string name="file_not_found">Impossible de trouver le fichier pour le chargement. Aurait-il été supprimé ou déplacé ?</string>
+ <string name="post_settings">Paramètres d\'article</string>
+ <string name="delete_post">Article supprimé</string>
+ <string name="delete_page">Page supprimée</string>
+ <string name="comment_status_approved">Approuvé</string>
+ <string name="comment_status_unapproved">En attente</string>
+ <string name="comment_status_spam">Indésirable</string>
+ <string name="comment_status_trash">À la Corbeille</string>
+ <string name="edit_comment">Modifier le commentaire</string>
+ <string name="mnu_comment_approve">Approuver</string>
+ <string name="mnu_comment_unapprove">Désapprouver</string>
+ <string name="mnu_comment_spam">Indésirable</string>
+ <string name="mnu_comment_trash">Supprimer</string>
+ <string name="dlg_approving_comments">Approbation</string>
+ <string name="dlg_unapproving_comments">Désapprobation</string>
+ <string name="dlg_spamming_comments">Marquage comme indésirable</string>
+ <string name="dlg_trashing_comments">Envoie à la corbeille</string>
+ <string name="dlg_confirm_trash_comments">Mettre à la corbeille ?</string>
+ <string name="trash_yes">Supprimer</string>
+ <string name="trash_no">Ne pas supprimer</string>
+ <string name="trash">Corbeille</string>
+ <string name="author_name">Nom de l\'auteur</string>
+ <string name="author_email">E-mail de l\'auteur</string>
+ <string name="author_url">URL de l\'auteur</string>
+ <string name="hint_comment_content">Commentaire</string>
+ <string name="saving_changes">Enregistrement des modifications</string>
+ <string name="sure_to_cancel_edit_comment">Annuler l\'édition de ce commentaire ?</string>
+ <string name="content_required">Commentaire requis</string>
+ <string name="toast_comment_unedited">Le commentaire n\'a pas été modifié</string>
+ <string name="remove_account">Supprimer le site</string>
+ <string name="blog_removed_successfully">Site supprimé avec succès</string>
+ <string name="delete_draft">Effacer le brouillon</string>
+ <string name="preview_page">Prévisualiser la page</string>
+ <string name="preview_post">Prévisualisation l\'article</string>
+ <string name="comment_added">Commentaire correctement ajouté</string>
+ <string name="post_not_published">L\'état de l\'article n\'est pas publié</string>
+ <string name="page_not_published">L\'état de la page n\'est pas publiée</string>
+ <string name="view_in_browser">Voir dans le navigateur</string>
+ <string name="add_new_category">Ajouter une nouvelle catégorie</string>
+ <string name="category_name">Nom de la catégorie</string>
+ <string name="category_slug">Identifiant de la catégorie (optionnel)</string>
+ <string name="category_desc">Description de la catégorie (optionnel)</string>
+ <string name="category_parent">Catégorie mère (optionnel)</string>
+ <string name="share_action_post">Nouvel article</string>
+ <string name="share_action_media">Bibliothèque de médias</string>
+ <string name="file_error_create">Impossible de créer un fichier temporaire pendant l\'envoi de média. Veuillez vérifier que vous disposez d\'assez d\'espace libre sur votre appareil.</string>
+ <string name="location_not_found">Localisation inconnue</string>
+ <string name="open_source_licenses">Licences open sources</string>
+ <string name="invalid_site_url_message">Vérifiez que l\'URL saisie est valide</string>
+ <string name="pending_review">En attente de relecture</string>
+ <string name="http_credentials">Identification HTTP (facultatif)</string>
+ <string name="http_authorization_required">Autorisation requise</string>
+ <string name="post_format">Format d\'article</string>
+ <string name="notifications_empty_all">Pas (encore) de notifications.</string>
+ <string name="new_post">Nouvel article</string>
+ <string name="new_media">Nouveau média</string>
+ <string name="view_site">Voir le site</string>
+ <string name="privacy_policy">Vie privée</string>
+ <string name="local_changes">Changements locaux</string>
+ <string name="image_settings">Paramètres d\'image</string>
+ <string name="add_account_blog_url">Adresse du blog</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="error_blog_hidden">Ce blog est caché et ne peut être chargé. Activez-le dans les paramètres et réessayez.</string>
+ <string name="fatal_db_error">Une erreur s\'est produite lors de la création de la base de données de l\'app. Essayez de réinstaller l\'app.</string>
+ <string name="jetpack_message_not_admin">L\'extension Jetpack est requise pour les stats. Contactez l\'administrateur du site.</string>
+ <string name="reader_title_applog">Journal de l\'application</string>
+ <string name="reader_share_link">Lien partagé</string>
+ <string name="reader_toast_err_add_tag">Impossible d\'ajouter ce mot-clé</string>
+ <string name="reader_toast_err_remove_tag">Impossible de supprimer ce mot-clé </string>
+ <string name="required_field">Champs requis</string>
+ <string name="email_hint">Adresse e-mail</string>
+ <string name="site_address">Votre adresse auto-hébergée (URL)</string>
+ <string name="email_cant_be_used_to_signup">Vous ne pouvez pas utiliser cette adresse de messagerie pour vous inscrire. Nous avons des problèmes avec ce fournisseur de messagerie. Utilisez un autre fournisseur de messagerie.</string>
+ <string name="email_reserved">Cette adresse de messagerie a déjà été utilisée. Vérifiez dans votre boite de réception si un message d\'activation s\'y trouve. Si vous n\'activez pas, vous pouvez essayer de nouveau dans quelques jours.</string>
+ <string name="blog_name_must_include_letters">L\'adresse du site doit avoir au moins 1 lettre (a-z)</string>
+ <string name="blog_name_invalid">Adresse de site invalide</string>
+ <string name="blog_title_invalid">Titre de site invalide</string>
+ <string name="deleting_page">Suppression de la page</string>
+ <string name="deleting_post">Suppression de l\'article</string>
+ <string name="share_url_post">Partager l\'article</string>
+ <string name="share_url_page">Partager la page</string>
+ <string name="share_link">Lien de partage</string>
+ <string name="creating_your_account">Création de votre compte</string>
+ <string name="creating_your_site">Création de votre site</string>
+ <string name="reader_empty_posts_in_tag_updating">Récupération des articles…</string>
+ <string name="error_refresh_media">Une erreur est survenue pendant le chargement de la bibliothèque média. Veuillez réessayer.</string>
+ <string name="reader_likes_you_and_multi">Vous et %,d personnes aiment ça</string>
+ <string name="reader_likes_multi">%,d personnes aiment ça</string>
+ <string name="reader_toast_err_get_comment">Impossible de récupérer ce commentaire</string>
+ <string name="reader_label_reply">Répondre</string>
+ <string name="video">Vidéo</string>
+ <string name="download">Téléchargement du média en cours</string>
+ <string name="comment_spammed">Commentaire marqué comme indésirable</string>
+ <string name="cant_share_no_visible_blog">Vous ne pouvez pas partager vers WordPress sans avoir publié au moins un blog.</string>
+ <string name="select_time">Sélectionner l\'heure</string>
+ <string name="reader_likes_you_and_one">Vous et un autre avez aimé ceci</string>
+ <string name="reader_empty_followed_blogs_description">Mais ne vous inquiétez pas, appuyer sur l\'icône en haut à droite pour commencer a explorer!</string>
+ <string name="select_date">Sélectionner la date</string>
+ <string name="pick_photo">Sélectionner une photo</string>
+ <string name="account_two_step_auth_enabled">Ce compte a activé la vérification en deux étapes. Rendez-vous sur les réglages de sécurité sur WordPress.com et générez un mot de passe propre à cette application.</string>
+ <string name="pick_video">Sélectionner une vidéo</string>
+ <string name="reader_toast_err_get_post">Impossible de récupérer cet article</string>
+ <string name="validating_user_data">Validation des données utilisateur</string>
+ <string name="validating_site_data">Validation des données du site</string>
+ <string name="password_invalid">Il vous faut un mot de passe plus sûr. Faites en sorte qu\'il comprenne au moins 7 caractères, mélangez les lettres capitales et bas-de-casse, les chiffres ou les caractères spéciaux.</string>
+ <string name="nux_tap_continue">Continuer</string>
+ <string name="nux_welcome_create_account">Créer un compte</string>
+ <string name="signing_in">Connexion…</string>
+ <string name="nux_add_selfhosted_blog">Ajouter un site auto-hébergé</string>
+ <string name="nux_oops_not_selfhosted_blog">Se connecter à WordPress.com</string>
+ <string name="media_add_popup_title">Ajouter à la bibliothèque média</string>
+ <string name="media_add_new_media_gallery">Créer une galerie</string>
+ <string name="empty_list_default">Cette liste est vide</string>
+ <string name="select_from_media_library">Sélectionner depuis la bibliothèque média</string>
+ <string name="jetpack_message">Le module Jetpack est requis pour afficher les statistiques. Voulez-vous installer Jetpack ?</string>
+ <string name="jetpack_not_found">Le module Jetpack est introuvable</string>
+ <string name="reader_untitled_post">(Sans Titre)</string>
+ <string name="reader_share_subject">Partagé depuis %s</string>
+ <string name="reader_btn_share">Partager</string>
+ <string name="reader_btn_follow">Suivre</string>
+ <string name="reader_btn_unfollow">Abonné</string>
+ <string name="reader_hint_comment_on_comment">Répondre au commentaire…</string>
+ <string name="reader_label_added_tag">%s Ajouté</string>
+ <string name="reader_label_removed_tag">%s Retiré</string>
+ <string name="reader_likes_one">Une personne aime ça</string>
+ <string name="reader_likes_only_you">Vous aimez ça</string>
+ <string name="reader_toast_err_comment_failed">Impossible d\'envoyer votre commentaire</string>
+ <string name="reader_toast_err_tag_exists">Vous suivez déjà cette étiquette</string>
+ <string name="reader_toast_err_tag_invalid">Ce n’est pas une étiquette valide</string>
+ <string name="reader_toast_err_share_intent">Impossible de partager</string>
+ <string name="reader_toast_err_view_image">Impossible de voir l\'image</string>
+ <string name="reader_toast_err_url_intent">Impossible d\'ouvrir %s</string>
+ <string name="reader_empty_followed_tags">Vous ne suivez aucun tag</string>
+ <string name="create_account_wpcom">Créer un compte sur WordPress.com</string>
+ <string name="button_next">Suivant</string>
+ <string name="connecting_wpcom">Connexion à WordPress.com</string>
+ <string name="username_invalid">Nom d\'utilisateur invalide</string>
+ <string name="limit_reached">Limite atteinte. Vous pouvez réessayer dans 1 minute. Si vous pensez que c\'est une erreur, contacter le support.</string>
+ <string name="nux_tutorial_get_started_title">Démarrez!</string>
+ <string name="themes">Thèmes</string>
+ <string name="all">Tout</string>
+ <string name="images">Images</string>
+ <string name="unattached">Non attachés</string>
+ <string name="custom_date">Date Personnalisée</string>
+ <string name="media_add_popup_capture_photo">Capturer photo</string>
+ <string name="media_add_popup_capture_video">Capturer vidéo</string>
+ <string name="media_gallery_image_order_random">Aléatoire</string>
+ <string name="media_gallery_image_order_reverse">Inversé</string>
+ <string name="media_gallery_type">Catégorie</string>
+ <string name="media_gallery_type_squares">Carrés</string>
+ <string name="media_gallery_type_tiled">Mosaïque</string>
+ <string name="media_gallery_type_circles">Cercles</string>
+ <string name="media_gallery_type_slideshow">Diaporama</string>
+ <string name="media_edit_title_text">Titre</string>
+ <string name="media_edit_caption_text">Légende</string>
+ <string name="media_edit_description_text">Description</string>
+ <string name="media_edit_title_hint">Entrer un titre</string>
+ <string name="media_edit_caption_hint">Entrer une légende</string>
+ <string name="media_edit_description_hint">Entrer une description</string>
+ <string name="media_edit_success">Mis à jour</string>
+ <string name="media_edit_failure">Échec de mise à jour</string>
+ <string name="themes_details_label">Détails</string>
+ <string name="themes_features_label">Fonctionnalités</string>
+ <string name="theme_activate_button">Activer</string>
+ <string name="theme_activating_button">Activation</string>
+ <string name="theme_set_success">Le thème a bien été sélectionné !</string>
+ <string name="theme_auth_error_title">Impossible de récupérer les thèmes</string>
+ <string name="post_excerpt">Extrait</string>
+ <string name="share_action_title">Ajouter à ...</string>
+ <string name="share_action">Partager</string>
+ <string name="stats">Stats</string>
+ <string name="stats_view_visitors_and_views">Visiteurs et Vues</string>
+ <string name="stats_view_clicks">Clics</string>
+ <string name="stats_view_tags_and_categories">Étiquettes et Catégories</string>
+ <string name="stats_view_referrers">Référents</string>
+ <string name="stats_timeframe_today">Aujourd\'hui</string>
+ <string name="stats_timeframe_yesterday">Hier</string>
+ <string name="stats_timeframe_days">Jours</string>
+ <string name="stats_timeframe_weeks">Semaines</string>
+ <string name="stats_timeframe_months">Mois</string>
+ <string name="stats_entry_country">Pays</string>
+ <string name="stats_entry_posts_and_pages">Titre</string>
+ <string name="stats_entry_tags_and_categories">Sujet</string>
+ <string name="stats_entry_authors">Auteur</string>
+ <string name="stats_entry_referrers">Référant</string>
+ <string name="stats_totals_views">Vues</string>
+ <string name="stats_totals_clicks">Clicks</string>
+ <string name="stats_totals_plays">Vues</string>
+ <string name="passcode_manage">Gérer le code PIN</string>
+ <string name="passcode_enter_passcode">Entrer le code PIN</string>
+ <string name="passcode_enter_old_passcode">Entrer l\'ancien code PIN</string>
+ <string name="passcode_re_enter_passcode">Re-entrer votre code PIN</string>
+ <string name="passcode_change_passcode">Changer le code PIN</string>
+ <string name="passcode_set">Code PIN changé</string>
+ <string name="passcode_preference_title">code PIN</string>
+ <string name="passcode_turn_off">Désactiver le code PIN</string>
+ <string name="passcode_turn_on">Activer le code PIN</string>
+ <string name="upload">Envoyer</string>
+ <string name="discard">Rejeter</string>
+ <string name="sign_in">Se connecter</string>
+ <string name="notifications">Notifications</string>
+ <string name="note_reply_successful">Réponse envoyée</string>
+ <string name="follows">Suit</string>
+ <string name="new_notifications">%d nouvelles notifications</string>
+ <string name="more_notifications">et %d autres.</string>
+ <string name="loading">Chargement…</string>
+ <string name="httpuser">Nom d’utilisateur HTTP</string>
+ <string name="httppassword">Mot de passe HTTP</string>
+ <string name="error_media_upload">Désolé, une erreur est survenue pendant l\'envoi du média</string>
+ <string name="post_content">Contenu (touchez pour ajouter du texte et des médias)</string>
+ <string name="publish_date">Publié le</string>
+ <string name="content_description_add_media">Ajouter média</string>
+ <string name="incorrect_credentials">Le nom d\'utilisateur ou le mot de passe ne sont pas valides.</string>
+ <string name="password">Mot de passe</string>
+ <string name="username">Identifiant</string>
+ <string name="reader">Lecteur</string>
+ <string name="featured">Utiliser comme image à la une</string>
+ <string name="featured_in_post">Inclure une image dans le contenu de la publication</string>
+ <string name="no_network_title">Pas de réseau disponible</string>
+ <string name="pages">Pages</string>
+ <string name="caption">Légende (optionnel)</string>
+ <string name="width">Largeur</string>
+ <string name="posts">Articles</string>
+ <string name="anonymous">Anonyme</string>
+ <string name="page">Page</string>
+ <string name="post">Article</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Envoyer et lier l\'image à l\'échelle</string>
+ <string name="scaled_image">Largeur de l\'image à l\'échelle</string>
+ <string name="scheduled">Programmé</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Mise en ligne...</string>
+ <string name="version">Version</string>
+ <string name="tos">Conditions d\'utilisation</string>
+ <string name="app_title">WordPress pour Android</string>
+ <string name="max_thumbnail_px_width">Largeur d\'iImage par défaut</string>
+ <string name="image_alignment">Alignement</string>
+ <string name="refresh">Rafraichir</string>
+ <string name="untitled">Sans titre</string>
+ <string name="edit">Modifier</string>
+ <string name="post_id">Article</string>
+ <string name="page_id">Page</string>
+ <string name="post_password">Mot de passe (optionnel)</string>
+ <string name="immediately">Immédiatement</string>
+ <string name="quickpress_add_alert_title">Choisir le nom du raccourci</string>
+ <string name="today">Aujourd\'hui</string>
+ <string name="settings">Paramètres</string>
+ <string name="share_url">Partager cette URL</string>
+ <string name="quickpress_window_title">Sélectionner le blog pour le raccourci QuickPress</string>
+ <string name="quickpress_add_error">Le nom du raccourci ne peut pas être vide</string>
+ <string name="publish_post">Publier</string>
+ <string name="draft">Brouillon</string>
+ <string name="post_private">Privé</string>
+ <string name="upload_full_size_image">Envoyer l\'image et ajouter un lien vers la taille originale</string>
+ <string name="title">Titre</string>
+ <string name="tags_separate_with_commas">Étiquettes (séparez les par des virgules)</string>
+ <string name="categories">Catégories</string>
+ <string name="dlg_deleting_comments">Effacement de commentaires en cours</string>
+ <string name="notification_blink">Faire clignoter le voyant de notification</string>
+ <string name="notification_sound">Son de notification</string>
+ <string name="notification_vibrate">Vibrer</string>
+ <string name="status">Avancement</string>
+ <string name="location">Localisation</string>
+ <string name="sdcard_title">Carte SD requise</string>
+ <string name="select_video">Choisissez une vidéo dans la galerie</string>
+ <string name="media">Multimedia</string>
+ <string name="delete">Effacer</string>
+ <string name="none">Aucun</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Sélectionnez une photo dans la galerie</string>
+ <string name="error">Erreur</string>
+ <string name="cancel">Annuler</string>
+ <string name="save">Enregistrer</string>
+ <string name="add">Ajouter</string>
+ <string name="category_refresh_error">Erreur de rafraîchissement de catégorie</string>
+ <string name="preview">Prévisualisation</string>
+ <string name="on">sur</string>
+ <string name="reply">Répondre</string>
+ <string name="notification_settings">Paramètres de Notification</string>
+ <string name="yes">Oui</string>
+ <string name="no">Non</string>
+</resources>
diff --git a/WordPress/src/main/res/values-gd/strings.xml b/WordPress/src/main/res/values-gd/strings.xml
new file mode 100644
index 000000000..7c531f49b
--- /dev/null
+++ b/WordPress/src/main/res/values-gd/strings.xml
@@ -0,0 +1,685 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="error_fetch_users_list">Cha b’ urrainn dhuinn cleachdaichean na làraich fhaighinn</string>
+ <string name="plans_manage">Stiùirich am plana agad aig\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Cha dug duine sam bith sùil air dad fhathast.</string>
+ <string name="title_follower">Neach-leantainn</string>
+ <string name="title_email_follower">Neach-leantainn puist-d</string>
+ <string name="people_empty_list_filtered_email_followers">Chan eil duine sam bith ’gad leantainn fhathast slighe puist-d.</string>
+ <string name="people_empty_list_filtered_followers">Chan eil duine sam bith ’gad leantainn fhathast.</string>
+ <string name="people_empty_list_filtered_users">Chan eil cleachdaiche sam bith agad fhathast.</string>
+ <string name="people_dropdown_item_email_followers">Luchd-leantainn puist-d</string>
+ <string name="people_dropdown_item_viewers">Luchd-coimhid</string>
+ <string name="people_dropdown_item_followers">Luchd-leantainn</string>
+ <string name="people_dropdown_item_team">Sgioba</string>
+ <string name="invite_message_usernames_limit">Thoir cuireadh do suas ri 10 puist-d agus/no ainmean-cleachdaichean WordPress.com. Mur eile ainm-cleachdaiche aig cuideigin, innsidh sinn dhaibh mar a gheibh iad fear.</string>
+ <string name="viewer_remove_confirmation_message">Ma bheir thu air falbh an neach-coimhid seo, chan urrainn dhaibh tuilleadh tadhal air an làrach.\n\nA bheil thu airson a thoirt air falbh fhathast?</string>
+ <string name="follower_remove_confirmation_message">Ma bheir thu an cleachdaiche seo air falbh, chan fhaigh iad brathan mun làrach seo tuilleadh ach ma nì iad leantainn às ùr.\n\nA bheil thu cinnteach gu bheil thu airson an neach-leantainn seo a thoirt air falbh?</string>
+ <string name="follower_subscribed_since">A-mach o %1$s</string>
+ <string name="reader_label_view_gallery">Seall an gailearaidh</string>
+ <string name="error_remove_follower">Cha b’ urrainn dhuinn an neach-leantainn a thoirt air falbh</string>
+ <string name="error_remove_viewer">Cha b’ urrainn dhuinn an neach-coimhid a thoirt air falbh</string>
+ <string name="error_fetch_email_followers_list">Cha b’ urrainn dhuinn na daoine a leanas an làrach slighe puist-d fhaighinn</string>
+ <string name="error_fetch_followers_list">Cha b’ urrainn dhuinn luchd-leantainn na làraich fhaighinn</string>
+ <string name="editor_failed_uploads_switch_html">Cha deach gach meadhan a luchdadh suas. Chan urrainn dhut leum gun mhodh\n HTML is cùisean mar seo. A bheil thu airson na dh’fhàillig a thoirt air falbh is leantainn air adhart?</string>
+ <string name="format_bar_description_html">Am modh HTML</string>
+ <string name="visual_editor">An deasaiche lèirsinneach</string>
+ <string name="image_thumbnail">Dealbhagan deilbh</string>
+ <string name="format_bar_description_ul">Liosta gun seòrsachadh</string>
+ <string name="format_bar_description_ol">Liosta air a sheòrsachadh</string>
+ <string name="format_bar_description_more">Cuir a-steach barrachd</string>
+ <string name="format_bar_description_media">Am modh cuir a-steach</string>
+ <string name="format_bar_description_strike">Loidhne troimhe</string>
+ <string name="format_bar_description_quote">Bloc-luaidh</string>
+ <string name="format_bar_description_link">Cuir a-steach ceangal</string>
+ <string name="format_bar_description_italic">Eadailteach</string>
+ <string name="format_bar_description_underline">Fo-loidhne</string>
+ <string name="image_settings_save_toast">Chaidh na h-atharraichean a shàbhaladh</string>
+ <string name="image_caption">Caipsean</string>
+ <string name="image_alt_text">Roghainn teacsa</string>
+ <string name="image_link_to">Ceangal ri</string>
+ <string name="image_width">Leud</string>
+ <string name="format_bar_description_bold">Trom</string>
+ <string name="image_settings_dismiss_dialog_title">A bheil thu airson na h-atharraichean nach deach a shàbhaladh a thilgeil air falbh?</string>
+ <string name="stop_upload_dialog_title">A bheil thu airson sgur dhen luchdadh suas?</string>
+ <string name="stop_upload_button">Sguir dhen luchdadh suas</string>
+ <string name="alert_error_adding_media">Thachair mearachd fhad ’s a bha sinn a’ cur a-steach a’ mheadhain</string>
+ <string name="alert_action_while_uploading">Tha thu a’ luchdadh suas meadhanan. Fuirich ort gus am bi sin deiseil.</string>
+ <string name="alert_insert_image_html_mode">Chan urrainn dhut meadhanan a chur a-steach mar sin sa mhodh HTML. Cleachd am modh lèirsinneach airson sin.</string>
+ <string name="uploading_gallery_placeholder">A’ luchdadh suas a’ ghailearaidh…</string>
+ <string name="invite_error_some_failed">Chaidh an cuireadh a chur ach thachair co-dhiù aon mhearachd!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Chaidh an cuireadh a chur</string>
+ <string name="tap_to_try_again">Thoir gnogag airson fheuchainn ris a-rithist!</string>
+ <string name="invite_error_sending">Thachair mearachd nuair a dh’fheuch sinn ris an cuireadh a chur!</string>
+ <string name="invite_error_invalid_usernames_multiple">Cha ghabh a chur: Tha ainmean no puist-d ann nach eil dligheach</string>
+ <string name="invite_error_invalid_usernames_one">Cha ghabh a chur: Tha ainm no post-d ann nach eil dligheach</string>
+ <string name="tabbar_accessibility_label_my_site">An làrach agam</string>
+ <string name="tabbar_accessibility_label_me">Mise</string>
+ <string name="editor_toast_changes_saved">Chaidh na h-atharraichean a shàbhaladh</string>
+ <string name="passcodelock_prompt_message">Cuir a-steach am PIN agad</string>
+ <string name="push_auth_expired">Dh’fhalbh an ùine air an iarrtas. Clàraich a-steach gu WordPress.com a dh’fheuchainn ris a-rithist.</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de sheallaidhean</string>
+ <string name="ignore">Leig seachad</string>
+ <string name="stats_insights_best_ever">Na seallaidhean as fhearr a-riamh</string>
+ <string name="stats_insights_most_popular_hour">An uair as fhèillmhoire</string>
+ <string name="stats_insights_most_popular_day">An latha as fhèillmhoire</string>
+ <string name="stats_insights_today">Stats an latha</string>
+ <string name="stats_insights_popular">An latha ’s uair as fhèillmhoire</string>
+ <string name="stats_insights_all_time">Puist, seallaidhean is aoighean air fad</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Clàraich a-steach gun chunntas WordPress.com leis an do cheangail thu Jetpack ris gus sùil a thoirt air na stats agad.</string>
+ <string name="stats_other_recent_stats_moved_label">A bheil an corr dhe na stats a bha agad o chionn goirid a dhìth ort? Chaidh an gluasad gu duilleag nan Insights.</string>
+ <string name="me_disconnect_from_wordpress_com">Dì-cheangail o WordPress.com</string>
+ <string name="me_btn_login_logout">Clàraich a-steach/a-mach</string>
+ <string name="me_connect_to_wordpress_com">Ceangail ri WordPress.com</string>
+ <string name="me_btn_support">Cobhair ⁊ taic</string>
+ <string name="site_picker_cant_hide_current_site">Cha deach “%s” a chur am falach a chionn ’s gur e sin an làrach làithreach</string>
+ <string name="account_settings">Roghainnean a\' chunntais</string>
+ <string name="site_picker_create_dotcom">Cruthaich làrach WordPress.com</string>
+ <string name="site_picker_edit_visibility">Seall/falaich làraichean</string>
+ <string name="site_picker_add_self_hosted">Cuir làrach air fèin-òstaireachd ris</string>
+ <string name="site_picker_add_site">Cuir làrach ris</string>
+ <string name="my_site_btn_view_admin">Seall an rianachd</string>
+ <string name="my_site_btn_switch_site">Leum gu làrach eile</string>
+ <string name="my_site_btn_view_site">Seall an làrach agam</string>
+ <string name="site_picker_title">Tagh làrach</string>
+ <string name="my_site_header_publish">Foillsich</string>
+ <string name="my_site_header_look_and_feel">Coltas</string>
+ <string name="my_site_btn_site_settings">Roghainnean</string>
+ <string name="my_site_btn_blog_posts">Puist bloga</string>
+ <string name="my_site_header_configuration">Rèiteachadh</string>
+ <string name="reader_label_new_posts_subtitle">Thoir gnogag gus an sealltainn</string>
+ <string name="notifications_account_required">Clàraich a-steach gu WordPress.com airson nam brathan</string>
+ <string name="stats_unknown_author">Ùghdar neo-aithnichte</string>
+ <string name="signout">Dì-cheangail</string>
+ <string name="image_added">Chaidh an dealbh a chur ris</string>
+ <string name="sign_out_wpcom_confirm">Ma dhì-cheanglas tu an cunntas agad, thèid an dàta WordPress.com air fad aig @%s a thoirt air falbh on uidheam seo, a’ gabhail a-steach dhreachdan is atharraichean ionadail.</string>
+ <string name="select_all">Tagh na h-uile</string>
+ <string name="hide">Falaich</string>
+ <string name="show">Seall</string>
+ <string name="deselect_all">Dì-thagh na h-uile</string>
+ <string name="select_from_new_picker">Dèan ioma-thaghadh leis an roghnaichear ùr</string>
+ <string name="no_media_sources">Cha b’ urrainn dhuinn na meadhanan fhaighinn</string>
+ <string name="loading_blog_images">A’ faighinn nan dealbhan</string>
+ <string name="loading_blog_videos">A’ faighinn nam videothan</string>
+ <string name="error_loading_videos">Mearachd le luchdadh nan videothan</string>
+ <string name="error_loading_images">Mearachd le luchdadh nan dealbhan</string>
+ <string name="error_loading_blog_videos">Chan urrainn dhuinn na videothan fhaighinn</string>
+ <string name="error_loading_blog_images">Chan urrainn dhuinn na dealbhan fhaighinn</string>
+ <string name="no_blog_videos">Gun videothan</string>
+ <string name="no_device_images">Gun dealbhan</string>
+ <string name="no_device_videos">Gun videothan</string>
+ <string name="no_blog_images">Gun dealbhan</string>
+ <string name="stats_generic_error">Cha b’ urrainn dhuinn na stats a dh’iarr thu a luchdadh</string>
+ <string name="no_media">Chan eil meadhanan ann</string>
+ <string name="loading_images">A’ luchdadh nan dealbhan</string>
+ <string name="loading_videos">A’ luchdadh nan videothan</string>
+ <string name="sign_in_jetpack">Clàraich a-steach dhan chunntas WordPress.com agad airson ceangal ri Jetpack.</string>
+ <string name="auth_required">Clàraich a-steach a-rithist airson leantainn air adhart.</string>
+ <string name="two_step_sms_sent">Thoir sùil air na teachdaireachdan teacsa agad airson a’ chòd dearbhaidh.</string>
+ <string name="two_step_footer_button">Cuir an còd slighe teachdaireachd teacsa</string>
+ <string name="two_step_footer_label">Cuir a-steach on aplacaid dearbhaidh agad.</string>
+ <string name="verify">Dearbh</string>
+ <string name="invalid_verification_code">Còd dearbhadh mì-dhligheach</string>
+ <string name="verification_code">Còd dearbhaidh</string>
+ <string name="editor_toast_invalid_path">Tha slighe an fhaidhle mì-dhligheach</string>
+ <string name="error_publish_no_network">Chan urrainn dhut foillseachadh mur eil ceangal ann. Chaidh dreachd dheth a shàbhaladh.</string>
+ <string name="tab_title_site_videos">Videothan na làraich</string>
+ <string name="tab_title_device_videos">Videothan an uidheim</string>
+ <string name="tab_title_site_images">Dealbhan na làraich</string>
+ <string name="take_video">Tog video</string>
+ <string name="tab_title_device_images">Dealbhan an uidheim</string>
+ <string name="take_photo">Tog dealbh</string>
+ <string name="media_picker_title">Tagh meadhan</string>
+ <string name="add_to_post">Cuir ris a’ phost</string>
+ <string name="language">Cànan</string>
+ <string name="device">Uidheam</string>
+ <string name="media_details_label_file_name">Ainm an fhaidhle</string>
+ <string name="media_details_label_file_type">Seòrsa an fhaidhle</string>
+ <string name="media_fetching">A’ faighinn nam meadhanan…</string>
+ <string name="posts_fetching">A’ faighinn nam post…</string>
+ <string name="pages_fetching">A’ faighinn nan duilleagan…</string>
+ <string name="toast_err_post_uploading">Cha ghabh post fhosgladh nuair a tha e ’ga luchdadh</string>
+ <string name="stats_view_search_terms">Faclan-luirg</string>
+ <string name="comments_fetching">A’ faighinn nam beachdan…</string>
+ <string name="stats_entry_search_terms">Facal-luirg</string>
+ <string name="stats_view_authors">Ùghdaran</string>
+ <string name="stats_empty_search_terms">Cha deach facal-luirg sam bith a chlàradh</string>
+ <string name="stats_empty_search_terms_desc">Fàs eòlach air trafaig nan lorg le bhith a’ toirt sùil air na faclan a lorg daoine ’s a thug iad dhan làrach agad.</string>
+ <string name="stats_followers_total_wpcom_paged">A’ sealltainn %1$d - %2$d à %3$s luchd-leantainn WordPress.com</string>
+ <string name="stats_followers_total_email_paged">A’ sealltainn %1$d - %2$d à %3$s luchd-leantainn puist-d</string>
+ <string name="stats_search_terms_unknown_search_terms">Faclan-luirg nach aithnichear</string>
+ <string name="publisher">Foillsichear:</string>
+ <string name="error_notification_open">Cha b’ urrainn dhuinn am brath fhosgladh</string>
+ <string name="reader_empty_posts_request_failed">Chan urrainn dhuinn na puist fhaighinn</string>
+ <string name="stats_months_and_years">Mìosan is bliadhnaichean</string>
+ <string name="stats_recent_weeks">Na seachdainean seo chaidh</string>
+ <string name="error_copy_to_clipboard">Thachair mearachd nuair a chaidh lethbhreac dha na faidhlichean a chur air an stòr-bhòrd</string>
+ <string name="stats_average_per_day">An cuibheas làitheil</string>
+ <string name="reader_label_new_posts">Puist ùra</string>
+ <string name="reader_empty_posts_in_blog">Tha am bloga seo falamh</string>
+ <string name="stats_period">Ùine</string>
+ <string name="logs_copied_to_clipboard">Chaidh lethbhreac de logaichean na h-aplacaid a chur air an stòr-bhòrd</string>
+ <string name="stats_total">Iomlan</string>
+ <string name="stats_overall">San fharsaingeachd</string>
+ <string name="post_uploading">’Ga luchdadh suas</string>
+ <string name="reader_page_recommended_blogs">Bidh ùidh agad sna leanas is dòcha</string>
+ <string name="stats_comments_total_comments_followers">Puist air fad aig a bheil luchd-leantainn phost: %1$s</string>
+ <string name="stats_visitors">Aoighean</string>
+ <string name="stats_views">Seallaidhean</string>
+ <string name="stats_pagination_label">Duilleag %1$s à %2$s</string>
+ <string name="stats_timeframe_years">Bliadhna</string>
+ <string name="stats_view_countries">Dùthchannan</string>
+ <string name="stats_likes">Daoine as toil leotha</string>
+ <string name="stats_view_followers">Luchd-leantainn</string>
+ <string name="stats_entry_clicks_link">Ceangal</string>
+ <string name="stats_view_top_posts_and_pages">Puist ⁊ duilleagan</string>
+ <string name="stats_view_videos">Videothan</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Ùghdar</string>
+ <string name="stats_entry_publicize">Seirbheis</string>
+ <string name="stats_entry_followers">Neach-leantainn</string>
+ <string name="stats_totals_publicize">Luchd-leantainn</string>
+ <string name="stats_empty_geoviews_desc">Fidir an liosta airson fiosrachadh dè na dùthchannan is roinnean-dùthcha on a thig an àireamh as motha de dhaoine thugad.</string>
+ <string name="stats_totals_followers">A-mach o</string>
+ <string name="stats_empty_geoviews">Cha deach dùthaich sam bith a chlàradh</string>
+ <string name="stats_empty_clicks_title">Cha deach briogadh sam bith a chlàradh</string>
+ <string name="stats_empty_referrers_title">Cha deach ath-threòraiche a chlàradh</string>
+ <string name="stats_empty_top_posts_title">Cha deach coimhead air duilleag no post</string>
+ <string name="stats_empty_top_posts_desc">Fiosraich dè an t-susbaint agad a tha mòran a’ coimhead air agus dè cho math ’s a tha puist is duilleagan àraidh a’ dèanamh thairis air ùine.</string>
+ <string name="stats_empty_referrers_desc">Faigh barrachd fiosrachaidh air dè cho faicsinneach ’s a tha an làrach agad ’s tu a’ coimhead air na làraichean-lìn ’s na h-einnseanan-luirg on a thig mòran dhaoine</string>
+ <string name="stats_empty_tags_and_categories">Cha deach coimhead air duilleag no post tagaichte</string>
+ <string name="stats_empty_clicks_desc">Ma tha an t-susbaint agad a’ ceangal ri làraichean eile, chì thu dè an fheadhainn air am briog daoine as trice.</string>
+ <string name="stats_empty_top_authors_desc">Cum sùil air co mheud duine a choimheadas air puist nan com-pàirtichean is fiosraich dè an t-susbaint air a bheil fèill mhòr aig gach ùghdar.</string>
+ <string name="stats_empty_tags_and_categories_desc">Faigh foir-shealladh air na cuspairean air a bheil fèill mhòr air an làrach agad a-rèir nam post as trainge san t-seachdain seo chaidh.</string>
+ <string name="stats_empty_comments_desc">Ma cheadaicheas tu beachdan air an làrach agad, ’s urrainn dhut sùil a chumail air cò na daoine a sgrìobhas an àireamh as motha de bheachdan agus dè an t-susbaint a bhrosnaicheas na deasbadan as beòthaile a-rèir a’ 1,000 beachd as ùire.</string>
+ <string name="stats_empty_video_desc">Ma luchdaich thu suas videothan slighe VideoPress, faic co mheud turas a choimhead daoine orra.</string>
+ <string name="stats_empty_video">Cha deach video a chluich</string>
+ <string name="stats_empty_publicize">Cha deach neach-leantainn Publicize a chlàradh</string>
+ <string name="stats_empty_publicize_desc">Cum sùil air cò tha ’gad leantainn o dhiofar sheirbheisean`sòisealta slighe Publicize.</string>
+ <string name="stats_empty_followers">Gun luchd-leantainn</string>
+ <string name="stats_empty_followers_desc">Cum sùil air co mheud neach-leantainn a tha agad san fharsaingeachd agus dè cho fad ’s a tha iad air a bhith ’nan luchd-leantainn agad.</string>
+ <string name="stats_comments_by_posts_and_pages">A-rèir puist ⁊ duilleige</string>
+ <string name="stats_comments_by_authors">A-rèir ùghdair</string>
+ <string name="stats_followers_total_wpcom">Luchd-leantainn WordPress.com uile gu lèir: %1$s</string>
+ <string name="stats_followers_email_selector">Post-d</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_a_minute_ago">mionaid air ais</string>
+ <string name="stats_followers_seconds_ago">diog(an) air ais</string>
+ <string name="stats_followers_total_email">Luchd-leantainn puist-d uile gu lèir %1$s</string>
+ <string name="stats_followers_days">%1$d là(ithean)</string>
+ <string name="stats_followers_a_day">Latha</string>
+ <string name="stats_followers_hours">%1$d uair(ean) a thìde</string>
+ <string name="stats_followers_minutes">%1$d mionaid(ean)</string>
+ <string name="stats_followers_an_hour_ago">uair a thìde air ais</string>
+ <string name="stats_followers_a_month">Mìos</string>
+ <string name="stats_followers_years">%1$d bliadhna</string>
+ <string name="stats_view">Seall</string>
+ <string name="stats_followers_months">%1$d mìos(an)</string>
+ <string name="stats_followers_a_year">Bliadhna</string>
+ <string name="stats_view_all">Seall na h-uile</string>
+ <string name="stats_for">Na stats airson %s</string>
+ <string name="stats_other_recent_stats_label">Stats ùra eile</string>
+ <string name="themes_fetching">A’ faighinn nan ùrlaran…</string>
+ <string name="ssl_certificate_details">Mion-fhiosrachadh</string>
+ <string name="sure_to_remove_account">A bheil thu airson an làrach seo a thoirt air falbh?</string>
+ <string name="media_gallery_date_range">A’ sealltainn nam meadhanan %1$s gu %2$s</string>
+ <string name="cab_selected">%d air a thaghadh</string>
+ <string name="confirm_delete_media">A bheil thu airson an rud a thagh thu a sguabadh às?</string>
+ <string name="delete_sure">Sguab às an dreachd seo</string>
+ <string name="delete_sure_page">Sguab às an duilleag seo</string>
+ <string name="confirm_delete_multi_media">A bheil thu airson na rudan a thagh thu a sguabadh às?</string>
+ <string name="delete_sure_post">Sguab às am post seo</string>
+ <string name="signing_out">’Gad chlàradh a-mach…</string>
+ <string name="posting_post">A’ postadh “%s”</string>
+ <string name="media_empty_list_custom_date">Cha robh meadhan ann san eadaramh ùine seo</string>
+ <string name="comment_trashed">Chaidh am beachd a chur dhan sgudal</string>
+ <string name="posts_empty_list">Chan eil post agad fhathast. Carson nach cruthaich thu fear?</string>
+ <string name="comment_reply_to_user">Freagair %s</string>
+ <string name="pages_empty_list">Chan eil duilleag agad fhathast. Carson nach cruthaich thu tè?</string>
+ <string name="mnu_comment_liked">’S toil leat seo</string>
+ <string name="comment">Beachd</string>
+ <string name="sending_content">A’ luchdadh suas susbaint %s</string>
+ <string name="uploading_total">A’ luchdadh suas %1$d de %2$d</string>
+ <string name="select_a_blog">Tagh làrach WordPress</string>
+ <string name="stats_no_blog">Cha b’ urrainn dhuinn na stats aig a’ bhloga sin a luchdadh</string>
+ <string name="older_last_week">Nas sine na seachdain</string>
+ <string name="older_two_days">Nas sine na dà latha</string>
+ <string name="older_month">Nas sine na mìos</string>
+ <string name="error_refresh_unauthorized_comments">Chan eil cead agad coimhead beachdan fhaicinn no a dheasachadh</string>
+ <string name="error_refresh_unauthorized_pages">Chan eil cead agad coimhead duilleagan fhaicinn no a dheasachadh</string>
+ <string name="error_refresh_unauthorized_posts">Chan eil cead agad coimhead puist fhaicinn no a dheasachadh</string>
+ <string name="error_publish_empty_post">Cha ghabh post bàn fhoillseachadh</string>
+ <string name="reader_title_photo_viewer">%1$d de %2$d</string>
+ <string name="reader_label_comments_on">Beachdan air</string>
+ <string name="reader_label_comments_closed">Tha na beachdan dùinte</string>
+ <string name="reader_label_comment_count_single">Aon bheachd</string>
+ <string name="reader_label_like">’S toil</string>
+ <string name="reader_label_view_original">Seall an t-artaigil tùsail</string>
+ <string name="reader_label_comment_count_multi">%,d beachd(an)</string>
+ <string name="reader_empty_posts_in_tag">Chan eil an taga seo aig post sam bith</string>
+ <string name="reader_empty_comments">Chan eil beachd ann fhathast</string>
+ <string name="create_new_blog_wpcom">Cruthaich bloga WordPress.com</string>
+ <string name="new_blog_wpcom_created">Chaidh bloga WordPress.com a chruthachadh!</string>
+ <string name="agree_terms_of_service">Ma chruthaicheas tu cunntas, bidh thu ag aontachadh ri %1$stermichean na seirbheise%2$s</string>
+ <string name="nux_help_description">Tadhail air aonad na cobharach far am faigh thu freagairtean air ceistean cumanta no tadhail air na fòraman gus ceist ùr a chur.</string>
+ <string name="browse_our_faq_button">Rùraich na CÀBHA agam</string>
+ <string name="faq_button">CÀBHA</string>
+ <string name="reader_empty_posts_liked">Cha duirt thu gur toil leat post sam bith fhathast</string>
+ <string name="reader_empty_followed_blogs_title">Chan eil thu a’ leantainn làrach sam bith fhathast</string>
+ <string name="more">Barrachd</string>
+ <string name="reader_menu_block_blog">Bac am bloga seo</string>
+ <string name="reader_toast_blog_blocked">Cha dèid puist on bhloga seo a shealltainn tuilleadh</string>
+ <string name="reader_toast_err_block_blog">Cha ghabh am bloga seo a bhacadh</string>
+ <string name="reader_toast_err_generic">Cha ghabh seo a dhèanamh</string>
+ <string name="hs__invalid_email_error">Cuir a-steach post-d dligheach</string>
+ <string name="hs__username_blank_error">Cuir a-steach ainm dligheach</string>
+ <string name="hs__conversation_header">Cabadaich taice</string>
+ <string name="hs__new_conversation_header">Cabadaich taice</string>
+ <string name="hs__conversation_detail_error">Mìnich dè an duilgheadas a tha thu a’ faicinn</string>
+ <string name="contact_us">Cuir fios thugainn</string>
+ <string name="current_location">An t-ionad làithreach</string>
+ <string name="add_location">Cuir ionad ris</string>
+ <string name="search_current_location">Lorg</string>
+ <string name="edit_location">Deasaich</string>
+ <string name="search_location">Lorg</string>
+ <string name="preference_send_usage_stats_summary">Cuir stats cleachdaidh thugainn ach an urrainn dhuinn WordPress airson Android a leasachadh</string>
+ <string name="preference_send_usage_stats">Cuir na stats</string>
+ <string name="schedule_verb">Cuir air an sgeideal</string>
+ <string name="update_verb">Ùraich</string>
+ <string name="reader_toast_err_already_follow_blog">Tha thu a’ leantainn a’ bhloga seo mu thràth</string>
+ <string name="reader_toast_err_follow_blog">Cha ghabh am bloga seo a leantainn</string>
+ <string name="reader_toast_err_unfollow_blog">Cha ghabh am bloga seo a dhì-leantainn</string>
+ <string name="reader_empty_recommended_blogs">Chan eil bloga ann a mholamaid</string>
+ <string name="reader_label_followed_blog">A’ leantainn a’ bhloga</string>
+ <string name="reader_label_tag_preview">Puist ris a bheil taga %s</string>
+ <string name="reader_toast_err_get_blog_info">Cha ghabh am bloga seo a shealltainn</string>
+ <string name="reader_title_subs">Tagaichean ⁊ blogaichean</string>
+ <string name="reader_page_followed_tags">Tagaichean a tha thu a’ leantainn</string>
+ <string name="reader_title_blog_preview">Bloga leughadair</string>
+ <string name="reader_title_tag_preview">Taga leughadair</string>
+ <string name="reader_page_followed_blogs">Làraichean a tha thu a’ leantainn</string>
+ <string name="reader_hint_add_tag_or_url">Cuir a-steach URL no taga gus a leantainn</string>
+ <string name="media_empty_list">Chan eil meadhanan ann</string>
+ <string name="ptr_tip_message">Gliocas: Tarraing a-nuas gus ath-nuadhachadh</string>
+ <string name="saving">’Ga shàbhaladh…</string>
+ <string name="ssl_certificate_ask_trust">Ma gheibh thu cothrom air an làrach seo gun duilgheadas sam bith a ghnàth, dh’fhaoidte gu bheil a’ mhearachd seo a’ ciallachadh gu bheil cuideigin a’ leigeil orra gur iad-san an làrach cheart is cha bu chòir dhut leantainn air adhart. A bheil thu airson gabhail ris an teisteanas co-dhiù?</string>
+ <string name="forums">Fòraman</string>
+ <string name="help_center">Aonad na cobharach</string>
+ <string name="ssl_certificate_error">Teisteanas SSL mì-dhligheach</string>
+ <string name="forgot_password">Na chaill thu am facal-faire agad?</string>
+ <string name="help">Cobhair</string>
+ <string name="nux_cannot_log_in">Chan urrainn dhuinn do chlàradh a-steach</string>
+ <string name="username_or_password_incorrect">Tha an t-ainm-cleachdaiche no am facal-faire a chuir thu a-steach cearr</string>
+ <string name="blog_name_reserved_but_may_be_available">Tha an làrach sin air a ghlèidheadh ach faodaidh gum bi e ri fhaighinn ann am beagan làithean</string>
+ <string name="blog_name_reserved">Tha an làrach seo glèidhte aig cuideigin eile</string>
+ <string name="blog_name_exists">Tha an làrach seo ann mu thràth</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Chan fhaod ach litrichean beaga (a-z) agus àireamhan a bhith ann an seòladh làraich</string>
+ <string name="blog_name_cant_be_used">Chan urrainn dhut an seòladh làraich seo a chleachdadh</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Feumaidh seòladh làraich a bhith nas giorra na 64 caractar</string>
+ <string name="blog_name_contains_invalid_characters">Chan fhaod “_” a bhith ann an seòladh làraich</string>
+ <string name="blog_name_must_be_at_least_four_characters">Feumaidh seòladh làraich a bhith 4 caractaran a dh’fhaid air a’ char as lugha</string>
+ <string name="blog_name_not_allowed">Chan eil an seòladh làraich seo ceadaichte</string>
+ <string name="blog_name_required">Cuir a-steach seòladh làraich</string>
+ <string name="username_reserved_but_may_be_available">Tha an t-ainm-cleachdaiche sin air a chaomhnadh ach faodaidh gum bi e ri fhaighinn ann am beagan làithean</string>
+ <string name="email_exists">Tha an seòladh puist-d sin ’ga chleachdadh mu thràth</string>
+ <string name="email_invalid">Cuir a-steach post-d dligheach</string>
+ <string name="email_not_allowed">Chan eil an seòladh puist-d seo ceadaichte</string>
+ <string name="username_exists">Tha an t-ainm-cleachdaiche seo ann mu thràth</string>
+ <string name="username_must_include_letters">Feumaidh co-dhiù aon litir (a-z) a bhith ann an ainm-cleachdaiche</string>
+ <string name="username_must_be_at_least_four_characters">Feumaidh ainm cleachdaiche a bhith 4 caractaran a dh\'fhaid air a’ char as lugha</string>
+ <string name="username_contains_invalid_characters">Chan fhaod “_” a bhith ann an ainm-cleachdaiche</string>
+ <string name="username_not_allowed">Chan eil an t-ainm-cleachdaiche seo ceadaichte</string>
+ <string name="invalid_username_too_long">Feumaidh ainm-cleachdaiche a bhith nas giorra na 61 caractar</string>
+ <string name="username_only_lowercase_letters_and_numbers">Chan fhaod ach litrichean beaga (a-z) agus àireamhan a bhith ann an ainm-cleachdaiche</string>
+ <string name="username_required">Cuir a-steach ainm-cleachdaiche</string>
+ <string name="invalid_username_too_short">Feumaidh ainm-cleachdaiche a bhith nas fhaide na 4 caractaran</string>
+ <string name="invalid_password_message">Feumaidh facal-faire a bhith 4 caractaran a dh’fhaid air a’ char as lugha</string>
+ <string name="invalid_email_message">Chan eil am post-d agad dligheach</string>
+ <string name="passcode_wrong_passcode">PIN cearr</string>
+ <string name="error_downloading_image">Mearachd le luchdadh a-nuas an deilbh</string>
+ <string name="error_load_comment">Cha b’ urrainn dhuinn am beachd a luchdadh</string>
+ <string name="error_refresh_comments">Cha b’ urrainn dhuinn na beachdan ath-nuadhachadh aig an àm seo</string>
+ <string name="error_refresh_stats">Cha b’ urrainn dhuinn na stats ath-nuadhachadh aig an àm seo</string>
+ <string name="error_generic">Thachair mearachd</string>
+ <string name="error_moderate_comment">Thachair mearachd fhad \'s a bha sinn ri modarataireachd</string>
+ <string name="error_edit_comment">Thachair mearachd rè deasachadh a’ bheachd</string>
+ <string name="error_upload">Thachair mearachd fhad ’s a bha sinn a’ luchdadh suas %s</string>
+ <string name="notifications_empty_list">Gun bhrathan</string>
+ <string name="error_delete_post">Thachair mearachd fhad ’s a bhathar a’ sguabadh às %s</string>
+ <string name="error_refresh_posts">Cha b’ urrainn dhuinn na puist ath-nuadhachadh aig an àm seo</string>
+ <string name="error_refresh_pages">Cha b’ urrainn dhuinn na duilleagan ath-nuadhachadh aig an àm seo</string>
+ <string name="error_refresh_notifications">Cha b’ urrainn dhuinn na brathan ath-nuadhachadh aig an àm seo</string>
+ <string name="reply_failed">Dh’fhàillig an fhreagairt</string>
+ <string name="stats_empty_comments">Chan eil beachd ann fhathast</string>
+ <string name="stats_bar_graph_empty">Chan eil stats ri fhaighinn</string>
+ <string name="sdcard_message">Feumaidh tu cairt SD a chaidh a mhunntachadh mus urrainn dhut meadhanan a luchdadh suas</string>
+ <string name="category_automatically_renamed">Chan e ainm dligheach airson roinn-seòrsa a tha ann an %1$s. Chaidh %2$s a chur air an àite sin.</string>
+ <string name="no_account">Cha deach cunntas WordPress a lorg, cuir cunntas ris is feuch ris a-rithist</string>
+ <string name="cat_name_required">Tha feum air raon ainm na roinn-seòrsa</string>
+ <string name="adding_cat_success">Chaidh an roinn-seòrsa a chur ris</string>
+ <string name="adding_cat_failed">Dh’fhàillig cur ris na roinn-seòrsa</string>
+ <string name="no_site_error">Cha b’ urrainn dhuinn ceangal ri làrach WordPress</string>
+ <string name="theme_set_failed">Cha b’ urrainn dhuinn an t-ùrlar a shuidheachadh</string>
+ <string name="theme_auth_error_message">Dèan cinnteach gu bheil cead agad ùrlaran a shuidheachadh</string>
+ <string name="comments_empty_list">Gun bheachdan</string>
+ <string name="mnu_comment_unspam">Beachd còir</string>
+ <string name="gallery_error">Cha b’ urrainn dhuinn am meadhan fhaighinn</string>
+ <string name="blog_not_found">Thachair mearachd fhad ’s a bha sinn ag inntrigeadh a’ bhloga seo</string>
+ <string name="wait_until_upload_completes">Fuirich ort gus an deach a luchdadh suas</string>
+ <string name="theme_fetch_failed">Cha b’ urrainn dhuinn na h-ùrlaran fhaighinn dhut</string>
+ <string name="out_of_memory">Chan eil cuimhne air fhàgail air an uidheam</string>
+ <string name="no_network_message">Chan eil lìonra ri làimh</string>
+ <string name="could_not_remove_account">Cha b’ urrainn dhuinn an làrach a thoirt air falbh</string>
+ <string name="invalid_url_message">Dèan cinnteach gu bheil an URL seo dligheach</string>
+ <string name="blog_title_invalid">Tiotal làraich mì-dhligheach</string>
+ <string name="blog_name_must_include_letters">Feumaidh co-dhiù aon litir (a-z) a bhith ann an seòladh làraich</string>
+ <string name="blog_name_invalid">Seòladh làraich mì-dhligheach</string>
+ <string name="email_reserved">Chaidh am post-d sin a chleachdadh mu thràth. Thoir sùil aig a’ bhogsa phuist agad ach a bheil post-d ann gus a chur an gnìomh. Cha bhi e ri fhaighinn tuilleadh ann am beagan làithean mura dèan thu dad.</string>
+ <string name="email_cant_be_used_to_signup">Chan urrainn dhut am post-d sin a chleachdadh gus clàradh. Tha fhios againn gu bheil duilgheadas ann ’s iad a cur casg air cuid dhen phost-d againn. Saoil an cleachd thu solaraiche puist-d eile?</string>
+ <string name="email_hint">Seòladh puist-d</string>
+ <string name="site_address">Seòladh na fèin-òstaireachd agad (URL)</string>
+ <string name="required_field">Raon air a bheil feum</string>
+ <string name="reader_toast_err_add_tag">Cha ghabh an taga seo a chur ris</string>
+ <string name="reader_toast_err_remove_tag">Cha ghabh an taga seo a thoirt air falbh</string>
+ <string name="reader_share_link">Co-roinn an ceangal</string>
+ <string name="jetpack_message_not_admin">Tha feum air plugan Jetpack mu choinneamh nan stats. Cuir fios gu rianaire na làraich.</string>
+ <string name="reader_title_applog">Loga na h-aplacaid</string>
+ <string name="fatal_db_error">Thachair mearachd fhad ’s a bha sinn a’ cruthachadh stòr-dàta nan aplacaidean. Feuch is stàlaich an aplacaid às ùr.</string>
+ <string name="error_blog_hidden">’S e bloga falaichte a tha ann is cha b’ urrainn dhuinn a luchdadh. Cuir an comas e sna roghainnean is feuch ris a-rithist.</string>
+ <string name="wordpress_blog">Bloga WordPress</string>
+ <string name="add_account_blog_url">Seòladh a’ bhloga</string>
+ <string name="local_changes">Atharraichean ionadail</string>
+ <string name="image_settings">Roghainnean an deilbh</string>
+ <string name="privacy_policy">Am poileasaidh prìobhaideachd</string>
+ <string name="view_site">Seall an làrach agam</string>
+ <string name="new_media">Meadhan ùr</string>
+ <string name="new_post">Post ùr</string>
+ <string name="post_format">Fòrmat a’ phuist</string>
+ <string name="http_authorization_required">Tha feum air ùghdarrasadh</string>
+ <string name="pending_review">Ri lèirmheasadh</string>
+ <string name="http_credentials">Ainm ’s facal-faire HTTP (roghainneil)</string>
+ <string name="open_source_licenses">Ceadachasan Open Source</string>
+ <string name="location_not_found">Seòladh neo-aithnichte</string>
+ <string name="file_error_create">Cha b’ urrainn dhuinn am faidhle sealach a chruthachadh gus meadhan a luchdadh suas. Dèan cinnteach gu bheil rum gu leòr air an uidheam agad.</string>
+ <string name="share_action_post">Post ùr</string>
+ <string name="share_action_media">Leabhar-lann nam meadhanan</string>
+ <string name="category_parent">Pàrant na roinn-seòrsa (roghainneil):</string>
+ <string name="category_desc">Tuairisgeul na roinn-seòrsa (roghainneil)</string>
+ <string name="category_slug">Sluga na roinn-seòrsa (roghainneil)</string>
+ <string name="category_name">Ainm na roinne-seòrsa</string>
+ <string name="add_new_category">Cuir roinn-seòrsa ùr ris</string>
+ <string name="view_in_browser">Seall sa bhrabhsair</string>
+ <string name="page_not_published">Cha deach staid na duilleige fhoillseachadh</string>
+ <string name="post_not_published">Cha deach staid a’ phuist fhoillseachadh</string>
+ <string name="comment_added">Chaidh am beachd a chur ris</string>
+ <string name="preview_post">Ro-sheall am post</string>
+ <string name="preview_page">Ro-sheall an duilleag</string>
+ <string name="delete_draft">Sguab às an dreachd</string>
+ <string name="blog_removed_successfully">Chaidh an làrach a thoirt air falbh</string>
+ <string name="remove_account">Thoir air falbh an làrach</string>
+ <string name="author_email">Post-d an ùghdair</string>
+ <string name="author_url">URl an ùghdair</string>
+ <string name="hint_comment_content">Beachd</string>
+ <string name="saving_changes">A’ sàbhaladh nan atharrachadh</string>
+ <string name="sure_to_cancel_edit_comment">A bheil thu airson sgur de dheasachadh a’ bheachd seo?</string>
+ <string name="content_required">Tha feum air beachd</string>
+ <string name="toast_comment_unedited">Cha deach am beachd atharrachadh</string>
+ <string name="author_name">Ainm an ùghdair</string>
+ <string name="trash">An sgudal</string>
+ <string name="trash_no">Na cuir</string>
+ <string name="trash_yes">Cuir</string>
+ <string name="dlg_confirm_trash_comments">Cuir dhan sgudal?</string>
+ <string name="dlg_trashing_comments">’Ga chur dhan sgudal</string>
+ <string name="dlg_approving_comments">’Ga aontachadh</string>
+ <string name="dlg_unapproving_comments">A’ toirt air falbh an t-aonta</string>
+ <string name="dlg_spamming_comments">A’ cur comharra gur e spama a tha ann</string>
+ <string name="mnu_comment_spam">Spama</string>
+ <string name="mnu_comment_trash">Sgudal</string>
+ <string name="mnu_comment_approve">Aontaich ris</string>
+ <string name="mnu_comment_unapprove">Thoir air falbh an t-aonta</string>
+ <string name="comment_status_trash">San sgudal</string>
+ <string name="edit_comment">Deasaich am beachd</string>
+ <string name="comment_status_unapproved">Ri dhèanamh</string>
+ <string name="comment_status_spam">Spama</string>
+ <string name="comment_status_approved">Air aontachadh</string>
+ <string name="delete_page">Sguab às an duilleag</string>
+ <string name="delete_post">Sguab às am post</string>
+ <string name="horizontal_alignment">Co-thaobhadh còmhnard</string>
+ <string name="file_not_found">Cha b’ urrainn dhuinn greim fhaighinn air faidhle a’ mheadhain gus a luchdadh suas. An deach a sguabadh às no a ghluasad?</string>
+ <string name="post_settings">Roghainnean a’ phuist</string>
+ <string name="local_draft">Dreachd ionadail</string>
+ <string name="upload_failed">Dh’fhàillig a luchdadh suas</string>
+ <string name="page_settings">Roghainnean na duilleige</string>
+ <string name="create_a_link">Cruthaich ceangal</string>
+ <string name="theme_premium_theme">Ùrlar Premium</string>
+ <string name="link_enter_url_text">Teacsa a’ cheangail (roghainneil)</string>
+ <string name="themes_live_preview">Ro-shealladh beò</string>
+ <string name="theme_current_theme">An t-ùrlar làithreach</string>
+ <string name="cannot_delete_multi_media_items">Tha meadhanan ann ach gabh an sguabadh às an-dràsta fhèin. Feuch ris a-rithist an ceann greis.</string>
+ <string name="media_error_no_permission">Chan eil cead agad coimhead air leabhar-lann nam meadhanan</string>
+ <string name="media_gallery_type_thumbnail_grid">Griod nan dealbhagan</string>
+ <string name="media_gallery_edit">Deasaich an gailearaidh</string>
+ <string name="media_gallery_settings_title">Roghainnean a’ ghailearaidh</string>
+ <string name="media_gallery_image_order">Òrdugh nan dealbhan</string>
+ <string name="media_gallery_num_columns">Àireamh de cholbhan</string>
+ <string name="post_not_found">Thachair mearachd rè luchdadh a’ phuist. Ath-nuadhaich na puist is feuch ris a-rithist.</string>
+ <string name="learn_more">Barrachd fiosrachaidh</string>
+ <string name="scaled_image_error">Cuir a-steach luach sgèileadh leud dligheach</string>
+ <string name="connection_error">Mearachd leis a’ cheangal</string>
+ <string name="cancel_edit">Sguir dhen deasachadh</string>
+ <string name="select_categories">Tagh roinnean-seòrsa</string>
+ <string name="account_details">Fiosrachadh a’ chunntais</string>
+ <string name="edit_post">Deasaich am post</string>
+ <string name="add_comment">Cuir beachd ris</string>
+ <string name="xmlrpc_error">Cha b’ urrainn dhuinn ceangal a dhèanamh. Cuir a-steach an t-slighe shlàn gun xmlrpc.php air an làrach agad is feuch ris a-rithist.</string>
+ <string name="invalid_site_url_message">Dèan cinnteach gu bheil an URL a chuir thu a-steach dligheach</string>
+ <string name="notifications_empty_all">Chan eil brath ann… fhathast.</string>
+ <string name="share_link">Co-roinn an ceangal</string>
+ <string name="deleting_page">A’ sguabadh às na duilleige</string>
+ <string name="deleting_post">A’ sguabadh às a’ phuist</string>
+ <string name="share_url_post">Co-roinn am post</string>
+ <string name="share_url_page">Co-roinn an duilleag</string>
+ <string name="creating_your_account">A\' cruthachadh a\' chunntais agad</string>
+ <string name="creating_your_site">A\' cruthachadh na làraich agad</string>
+ <string name="reader_empty_posts_in_tag_updating">A\' faighinn nam post...</string>
+ <string name="error_refresh_media">Chaidh rudeigin cearr fhad \'s a bha sinn ag ath-nuadhachadh leabhar-lann nam meadhanan. Feuch ris a-rithist an ceann greis.</string>
+ <string name="reader_likes_you_and_multi">Thuirt thusa agus %,d eile gur toigh leibh seo</string>
+ <string name="reader_likes_multi">\'S toigh le %,d daoine seo</string>
+ <string name="reader_toast_err_get_comment">Chan urrainn dhuinn am beachd seo fhaighinn dhut</string>
+ <string name="reader_label_reply">Freagair</string>
+ <string name="video">Video</string>
+ <string name="download">A\' luchdadh a-nuas a\' mheadhain</string>
+ <string name="comment_spammed">Chaidh comharra spama a chur ris a\' bheachd</string>
+ <string name="cant_share_no_visible_blog">Chan urrainn dhut co-roinneadh gu WordPress as aonais bloga a tha ri fhaicinn</string>
+ <string name="select_time">Tagh àm</string>
+ <string name="reader_likes_you_and_one">Thuirt thusa agus aonan eile gur toigh leibh seo</string>
+ <string name="select_date">Tagh ceann-là</string>
+ <string name="pick_photo">Tagh dealbh</string>
+ <string name="account_two_step_auth_enabled">Tha dearbhadh dà-cheumnach an sàs sa chunntas seo. Tadhail air na roghainnean tèarainteachd agad air WordPress.com agus gin facal-faire gu sònraichte dhan aplacaid.</string>
+ <string name="pick_video">Tagh video</string>
+ <string name="reader_toast_err_get_post">Chan urrainn dhuinn am post seo fhaighinn dhut</string>
+ <string name="validating_user_data">A\' dearbhadh dàta a\' chleachdaiche</string>
+ <string name="validating_site_data">A\' dearbhadh dàta na làraich</string>
+ <string name="reader_empty_followed_blogs_description">Ach na gabh dragh, cha leig thu leas ach gnogag a thoirt air a ìomhaigheag gu h-àrd air an taobh deas gus an rùrachadh!</string>
+ <string name="password_invalid">Feumaidh tu facal-faire nas tèarainte. Dèan cinnteach gun cleachd thu co-dhiù 7 caractaran, measgachadh de litrichean mòra \'s beaga, àireamhan is caractaran sònraichte.</string>
+ <string name="nux_tap_continue">Air adhart</string>
+ <string name="nux_welcome_create_account">Cruthaich cunntas</string>
+ <string name="nux_add_selfhosted_blog">Cuir làrach air fèin-òstaireachd ris</string>
+ <string name="nux_oops_not_selfhosted_blog">\'Gad chlàradh a-steach gu WordPress.com</string>
+ <string name="signing_in">’Gad chlàradh a-steach…</string>
+ <string name="media_add_popup_title">Cuir ri leabharlann nam meadhanan</string>
+ <string name="media_add_new_media_gallery">Cruthaich gailearaidh</string>
+ <string name="select_from_media_library">Tagh o leabhar-lann nam meadhanan</string>
+ <string name="jetpack_message">Tha feum air plugan Jetpack mus fhaigh thu na stats. A bheil thu airson Jetpack a stàladh?</string>
+ <string name="jetpack_not_found">Cha deach am plugan Jetpack a lorg</string>
+ <string name="reader_untitled_post">(Gun tiotal)</string>
+ <string name="reader_share_subject">Air a cho-roinneadh o %s</string>
+ <string name="reader_btn_share">Co-roinn</string>
+ <string name="reader_btn_follow">Lean</string>
+ <string name="reader_btn_unfollow">A\' leanntainn</string>
+ <string name="reader_label_added_tag">Chaidh %s a chur ris</string>
+ <string name="reader_label_removed_tag">Chaidh %s a thoirt air falbh</string>
+ <string name="reader_likes_one">Tha aon duine ag ràdh gur toigh leotha seo</string>
+ <string name="reader_likes_only_you">\'S toigh leat seo</string>
+ <string name="reader_toast_err_comment_failed">Cha b\' urrainn dhuinn do bheachd a phostadh</string>
+ <string name="reader_toast_err_tag_exists">Tha thu a\' leantainn an taga seo mu thràth</string>
+ <string name="reader_toast_err_tag_invalid">Cha e taga dligheach a tha seo</string>
+ <string name="reader_toast_err_share_intent">Cha ghabh a cho-roinneadh</string>
+ <string name="reader_toast_err_view_image">Cha ghabh an dealbh seo a shealltainn</string>
+ <string name="reader_toast_err_url_intent">Cha ghabh %s fhosgladh</string>
+ <string name="connecting_wpcom">A\' ceangal ri WordPress.com</string>
+ <string name="username_invalid">Ainm-cleachdaiche mì-dhligheach</string>
+ <string name="limit_reached">Chleachd thu na tha ceadaichte dhut. Feuch ris a-rithist ann am mionaid. Ma dh\'fheuchas tu ris roimhe sin, fàsaidh an ùine mus dèid an toirmeasg a thoirt dhìot. Ma tha thu dhen bheachd gur e mearachd a tha seo, cuir fios gun sgioba taice.</string>
+ <string name="nux_tutorial_get_started_title">Toiseach tòiseachaidh!</string>
+ <string name="create_account_wpcom">Cruthaich cunntas air WordPress.com</string>
+ <string name="reader_empty_followed_tags">Chan eil thu a’ leantainn taga sam bith</string>
+ <string name="empty_list_default">Tha an liosta seo falamh</string>
+ <string name="reader_hint_comment_on_comment">Freagair am beachd…</string>
+ <string name="button_next">Air adhart</string>
+ <string name="themes">Ùrlaran</string>
+ <string name="all">Na h-uile</string>
+ <string name="images">Dealbhan</string>
+ <string name="unattached">Gun cheangal ris</string>
+ <string name="custom_date">Ceann-là gnàthaichte</string>
+ <string name="media_add_popup_capture_photo">Tog dealbh</string>
+ <string name="media_add_popup_capture_video">Tog video</string>
+ <string name="media_gallery_image_order_random">Air thuaiream</string>
+ <string name="media_gallery_image_order_reverse">Òrdugh contrarra</string>
+ <string name="media_gallery_type">Seòrsa</string>
+ <string name="media_gallery_type_squares">Ceàrnagan</string>
+ <string name="media_gallery_type_tiled">Mar leacan</string>
+ <string name="media_gallery_type_circles">Cearcallan</string>
+ <string name="media_gallery_type_slideshow">Taisbeanadh-shleamhnagan</string>
+ <string name="media_edit_title_text">Tiotal</string>
+ <string name="media_edit_caption_text">Caipsean</string>
+ <string name="media_edit_description_text">Tuairisgeul</string>
+ <string name="media_edit_title_hint">Cuir tiotal an-seo</string>
+ <string name="media_edit_caption_hint">Cuir caipsean an-seo</string>
+ <string name="media_edit_description_hint">Cuir tuairisgeul an-seo</string>
+ <string name="media_edit_success">Air ùrachadh</string>
+ <string name="media_edit_failure">Dh\'fhàillig an t-ùrachadh</string>
+ <string name="themes_details_label">Mion-fhiosrachadh</string>
+ <string name="themes_features_label">Feartan</string>
+ <string name="theme_activate_button">Cuir an gnìomh</string>
+ <string name="theme_activating_button">\'Ga ghnìomhachadh</string>
+ <string name="theme_set_success">Chaidh an t-ùrlar a shuidheachadh!</string>
+ <string name="theme_auth_error_title">Cha b\' urrainn dhuinn na h-ùrlaran fhaighinn dhut</string>
+ <string name="post_excerpt">Às-earrann</string>
+ <string name="share_action_title">Cuir ri ...</string>
+ <string name="share_action">Co-roinn</string>
+ <string name="stats">Stats</string>
+ <string name="stats_view_clicks">Briogaidhean</string>
+ <string name="stats_view_tags_and_categories">Tagaichean ⁊ roinnean-seòrsa</string>
+ <string name="stats_view_referrers">Ath-threòraichean</string>
+ <string name="stats_timeframe_today">An-diugh</string>
+ <string name="stats_timeframe_yesterday">An-dè</string>
+ <string name="stats_timeframe_days">Làithean</string>
+ <string name="stats_timeframe_weeks">Seachdainean</string>
+ <string name="stats_timeframe_months">Mìosan</string>
+ <string name="stats_entry_country">Dùthaich</string>
+ <string name="stats_entry_posts_and_pages">Tiotal</string>
+ <string name="stats_entry_tags_and_categories">Cuspair</string>
+ <string name="stats_entry_authors">Ùghdar</string>
+ <string name="stats_entry_referrers">Ath-threòraiche</string>
+ <string name="stats_view_visitors_and_views">Aoighean is na choimheadadh</string>
+ <string name="stats_totals_views">Seallaidhean</string>
+ <string name="stats_totals_clicks">Briogaidhean</string>
+ <string name="stats_totals_plays">Air a chluich</string>
+ <string name="passcode_enter_passcode">Cuir a-steach am PIN agad</string>
+ <string name="passcode_re_enter_passcode">Cuir a-steach am PIN agad a-rithist</string>
+ <string name="passcode_change_passcode">Atharraich am PIN</string>
+ <string name="passcode_manage">Stiùirich glas a\' PIN</string>
+ <string name="passcode_enter_old_passcode">Cuir a-steach an seann PIN agad</string>
+ <string name="passcode_set">Chaidh am PIN a shuidheachadh</string>
+ <string name="passcode_preference_title">Glas a\' PIN</string>
+ <string name="passcode_turn_off">Cuir glas a\' PIN dheth</string>
+ <string name="passcode_turn_on">Cuir glas a\' PIN air</string>
+ <string name="upload">Luchdadh suas</string>
+ <string name="discard">Tilg air falbh</string>
+ <string name="sign_in">Clàraich a-steach</string>
+ <string name="notifications">Brathan</string>
+ <string name="note_reply_successful">Chaidh an fhreagairt fhoillseachadh</string>
+ <string name="new_notifications">%d brath(an) ùra</string>
+ <string name="more_notifications">agus %d a bharrachd.</string>
+ <string name="follows">Daoine a lean</string>
+ <string name="loading">\'Ga luchdadh…</string>
+ <string name="httpuser">Ainm-cleachdaiche HTTP</string>
+ <string name="httppassword">Facal-faire HTTP</string>
+ <string name="error_media_upload">Thachair mearachd fhad \'s a bha sinn a\' luchdadh suas meadhan</string>
+ <string name="post_content">Susbaint (thoir gnogag gus teacsa \'s meadhanan a chur ris)</string>
+ <string name="publish_date">Foillsich</string>
+ <string name="content_description_add_media">Cuir meadhan ris</string>
+ <string name="incorrect_credentials">Ainm-cleachdaiche no facal-faire cearr.</string>
+ <string name="password">Facal-faire</string>
+ <string name="username">Ainm-cleachdaiche</string>
+ <string name="reader">Leughadair</string>
+ <string name="featured">Cleachd mar dhealbh brosnaichte</string>
+ <string name="featured_in_post">Gabh a-steach an dealbh ann an susbaint a\' phuist</string>
+ <string name="no_network_title">Chan eil lìonra ri fhaighinn</string>
+ <string name="pages">Duilleagan</string>
+ <string name="caption">Caipsean (roghainneil)</string>
+ <string name="width">Leud</string>
+ <string name="posts">Puist</string>
+ <string name="anonymous">Gun urra</string>
+ <string name="page">Duilleag</string>
+ <string name="post">Post</string>
+ <string name="blogusername">ainm cleachdaiche a\' bhloga</string>
+ <string name="ok">Ceart ma-thà</string>
+ <string name="upload_scaled_image">Luchdaich suas is dèan ceangal ris an dealbh sgèilichte</string>
+ <string name="scaled_image">Leud an deilbh sgèilichte</string>
+ <string name="scheduled">Air an sgeideal</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">’Ga luchdadh suas…</string>
+ <string name="version">Tionndadh</string>
+ <string name="tos">Teirmichean na seirbheise</string>
+ <string name="app_title">WordPress airson Android</string>
+ <string name="max_thumbnail_px_width">Leud bunaiteach an deilbh</string>
+ <string name="image_alignment">Co-thaobhadh</string>
+ <string name="refresh">Ath-nuadhaich</string>
+ <string name="untitled">Gun tiotal</string>
+ <string name="edit">Deasaich</string>
+ <string name="post_id">Post</string>
+ <string name="page_id">Duilleag</string>
+ <string name="post_password">Facal-faire (roghainneil)</string>
+ <string name="immediately">Sa bhad</string>
+ <string name="quickpress_add_alert_title">Suidhich ainm na h-ath-ghoirid</string>
+ <string name="today">An-diugh</string>
+ <string name="settings">Roghainnean</string>
+ <string name="share_url">Co-roinn an URL</string>
+ <string name="quickpress_window_title">Tagh bloga airson ath-ghoirid QuickPress</string>
+ <string name="quickpress_add_error">Chan fhaod ainm na h-ath-ghoirid a bhith bàn</string>
+ <string name="publish_post">Foillsich</string>
+ <string name="draft">Dreachd</string>
+ <string name="post_private">Prìobhaideach</string>
+ <string name="upload_full_size_image">Luchdaich suas is dèan ceangal ris an dealbh slàn</string>
+ <string name="title">Tiotal</string>
+ <string name="tags_separate_with_commas">Tagaichean (sgar na tagaichean le cromagan)</string>
+ <string name="categories">Roinnean-seòrsa</string>
+ <string name="dlg_deleting_comments">A’ sguabadh às nam beachdan</string>
+ <string name="notification_blink">Boillsg solas</string>
+ <string name="notification_vibrate">Crith</string>
+ <string name="notification_sound">Fuaim bhrathan</string>
+ <string name="status">Staid</string>
+ <string name="location">Ionad</string>
+ <string name="sdcard_title">Tha feum air cairt SD</string>
+ <string name="select_video">Tagh video on ghailearaidh</string>
+ <string name="media">Meadhan</string>
+ <string name="delete">Sguab às</string>
+ <string name="none">Chan eil gin</string>
+ <string name="blogs">Blogaichean</string>
+ <string name="select_photo">Tagh dealbh on ghailearaidh</string>
+ <string name="error">Mearachd</string>
+ <string name="cancel">Sguir dheth</string>
+ <string name="save">Sàbhail</string>
+ <string name="add">Cuir ris</string>
+ <string name="preview">Ro-shealladh</string>
+ <string name="on"> </string>
+ <string name="reply">Freagair</string>
+ <string name="yes">Tha</string>
+ <string name="no">Chan eil</string>
+ <string name="category_refresh_error">Mearachd le ath-nuadhachadh na roinn-seòrsa</string>
+ <string name="notification_settings">Roghainnean nam brathan</string>
+</resources>
diff --git a/WordPress/src/main/res/values-gl/strings.xml b/WordPress/src/main/res/values-gl/strings.xml
new file mode 100644
index 000000000..47712af16
--- /dev/null
+++ b/WordPress/src/main/res/values-gl/strings.xml
@@ -0,0 +1,1149 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="reader_label_image_count_one">1 imaxe</string>
+ <string name="timespan_now">agora</string>
+ <string name="role_admin">Administrador</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Colaborador</string>
+ <string name="role_follower">Seguidor</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_viewer">Lector</string>
+ <string name="error_post_my_profile_no_connection">Sen conexión, non se pode gardar o perfil.</string>
+ <string name="alignment_none">Ningún</string>
+ <string name="alignment_left">Esquerda</string>
+ <string name="alignment_right">Dereita</string>
+ <string name="site_settings_list_editor_action_mode_title">Seleccionado %1$d</string>
+ <string name="error_fetch_users_list">Non foi posible obter os usuarios do sitio</string>
+ <string name="plans_manage">Xestiona o teu plan en\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Aínda non tes lectores.</string>
+ <string name="title_follower">Seguidor</string>
+ <string name="people_fetching">Á procura dos usuarios...</string>
+ <string name="title_email_follower">Seguidor por correo-e</string>
+ <string name="people_empty_list_filtered_email_followers">Aínda non tes seguidor por correo electrónico ningún</string>
+ <string name="people_empty_list_filtered_followers">Aínda non tes seguidor ningún</string>
+ <string name="people_empty_list_filtered_users">Aínda non tes usuario ningún</string>
+ <string name="people_dropdown_item_viewers">Lectores</string>
+ <string name="people_dropdown_item_email_followers">Seguidores por correo-e</string>
+ <string name="people_dropdown_item_followers">Seguidores</string>
+ <string name="people_dropdown_item_team">Equipo</string>
+ <string name="invite_message_usernames_limit">Invita ate 10 enderezos de correo electrónico e/ou nomes de usuario de WordPress.com. A\naqueles que necesiten un nome de usuario se lles enviarán instrucións de como crear un.</string>
+ <string name="viewer_remove_confirmation_message">Se eliminas a este lector, el ou ela non poderán visitar o sitio.\n\nAínda así, queres eliminar a este lector?</string>
+ <string name="follower_remove_confirmation_message">Se o eliminas, este seguidor deixará de recibir notificacións sobre este sitio, agás que volva a seguirche.\n \n\nAínda así queres eliminar a este seguidor?</string>
+ <string name="follower_subscribed_since">Desde %1$s</string>
+ <string name="reader_label_view_gallery">Ver a Galería</string>
+ <string name="reader_label_image_count_multi">%d imaxes</string>
+ <string name="error_remove_viewer">Non foi posible eliminar o lector</string>
+ <string name="error_remove_follower">Non foi posible eliminar o seguidor</string>
+ <string name="error_fetch_email_followers_list">Non foi posible obter os seguidores do sitio por correo electrónico</string>
+ <string name="error_fetch_followers_list">Non foi posible obter os seguidores do sitio</string>
+ <string name="editor_failed_uploads_switch_html">Fallou a carga dalgúns ficheiros. Nesta situación non podes cambiar a modo HTML\nQueres borrar todas as cargas que fallaron e continuar?</string>
+ <string name="visual_editor">Editor visual</string>
+ <string name="image_thumbnail">Miniatura</string>
+ <string name="format_bar_description_html">Modo HTML</string>
+ <string name="format_bar_description_ul">Lista sen orde</string>
+ <string name="format_bar_description_ol">Lista ordenada</string>
+ <string name="format_bar_description_more">Inserir máis</string>
+ <string name="format_bar_description_media">Inserir ficheiro</string>
+ <string name="format_bar_description_strike">Riscado</string>
+ <string name="format_bar_description_quote">Cita</string>
+ <string name="format_bar_description_link">Inserir enlace</string>
+ <string name="format_bar_description_italic">Cursiva</string>
+ <string name="format_bar_description_underline">Subliñado</string>
+ <string name="image_settings_save_toast">Cambios gardados</string>
+ <string name="image_caption">Lenda</string>
+ <string name="image_alt_text">Texto alternativo</string>
+ <string name="image_link_to">Ligar a</string>
+ <string name="image_width">Largo</string>
+ <string name="format_bar_description_bold">Grosa</string>
+ <string name="image_settings_dismiss_dialog_title">Descartar os cambios sen gardar?</string>
+ <string name="stop_upload_button">Deter a carga</string>
+ <string name="stop_upload_dialog_title">Suspender a carga?</string>
+ <string name="alert_error_adding_media">Houbo un erro na inserción do ficheiro</string>
+ <string name="alert_action_while_uploading">Estanse a cargar os ficheiros. Por favor, espera a que o proceso termine.</string>
+ <string name="alert_insert_image_html_mode">Non se poden inserir ficheiros directamente no modo HTML. Hai que volver ao modo visual.</string>
+ <string name="uploading_gallery_placeholder">Cargando a galería...</string>
+ <string name="invite_error_some_failed">Enviouse a invitación, pero houbo algún erro.</string>
+ <string name="invite_sent">Invitación enviada con éxito</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="tap_to_try_again">Pulsa para tentalo outra vez!</string>
+ <string name="invite_error_sending">Houbo un erro mentres se enviaba a invitación.</string>
+ <string name="invite_error_invalid_usernames_multiple">Non se pode enviar: Hai nomes de usuario ou enderezos de correo non válidos.</string>
+ <string name="invite_error_invalid_usernames_one">Fallou o envío: un nome de usuario ou un enderezo non é válido.</string>
+ <string name="invite_error_no_usernames">Introduce como mínimo un nome de usuario</string>
+ <string name="invite_message_info">(Opcional) Podes escribir unha mensaxe persoal de ata 500 caracteres para incluíla na invitación ao/s usuario/s.</string>
+ <string name="invite_message_remaining_other">quedan %d caracteres</string>
+ <string name="invite_message_remaining_one">queda 1 carácter</string>
+ <string name="invite_message_remaining_zero">quedan 0 caracteres</string>
+ <string name="invite_invalid_email">O enderezo de correo \'%s\' non é válido</string>
+ <string name="invite_message_title">Mensaxe persoal</string>
+ <string name="invite_already_a_member">Xa hai un membro co nome de usuario \'%s\'</string>
+ <string name="invite_username_not_found">Non se atopou usuario co nome \'%s\'</string>
+ <string name="invite">Invitar</string>
+ <string name="invite_names_title">Nomes de usuario ou enderezos de correo</string>
+ <string name="send_link">Enviar o enlace</string>
+ <string name="my_site_header_external">Externo</string>
+ <string name="invite_people">Invitar xente</string>
+ <string name="signup_succeed_signin_failed">A túa conta está creada, pero houbo u erro mentres cha dabamos acceso. Proba a entrar cos teus nome de usuario e contrasinal novos.</string>
+ <string name="label_clear_search_history">Limpar o historial de busca</string>
+ <string name="dlg_confirm_clear_search_history">Limpar o historial de busca?</string>
+ <string name="reader_empty_posts_in_search_description">Non se atoparon artigos para %s na túa lingua</string>
+ <string name="reader_label_post_search_running">Buscando...</string>
+ <string name="reader_empty_posts_in_search_title">Non se atoparon artigos</string>
+ <string name="reader_label_related_posts">Lecturas relacionadas</string>
+ <string name="reader_label_post_search_explainer">Buscar en todos os blogues públicos de WordPress.com</string>
+ <string name="reader_hint_post_search">Buscar en WordPress.com</string>
+ <string name="reader_title_related_post_detail">Artigos relacionados</string>
+ <string name="reader_title_search_results">Busca de %s</string>
+ <string name="preview_screen_links_disabled">Os enlaces están inhabilitados na vista previa</string>
+ <string name="draft_explainer">Este artigo é un borrador sen publicar</string>
+ <string name="send">Enviar</string>
+ <string name="user_remove_confirmation_message">Se eliminas a %1$s, o usuario non poderá acceder máis a este sitio, aínda que todos os contidos creados por %1$s permanecerán nel.\n\nQueres aínda así eliminar a este usuario?</string>
+ <string name="person_removed">%1$s foi eliminado</string>
+ <string name="person_remove_confirmation_title">Eliminar a %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Os sitios desta lista non publicaron nada ultimamente</string>
+ <string name="people">Xente</string>
+ <string name="role">Rol</string>
+ <string name="edit_user">Editar usuario</string>
+ <string name="error_remove_user">Non foi posible eliminar o usuario</string>
+ <string name="error_fetch_viewers_list">Non foi posible obter os lectores do sitio</string>
+ <string name="error_update_role">Non foi posible actualizar o rol do usuario</string>
+ <string name="gravatar_camera_and_media_permission_required">Necesítase permiso para seleccionar ou facer unha foto</string>
+ <string name="error_updating_gravatar">Houbo un erro ao actualizar o teu Gravatar</string>
+ <string name="error_locating_image">Erro na localización da imaxe recortada</string>
+ <string name="error_refreshing_gravatar">Erro ao recargar o teu Gravatar</string>
+ <string name="gravatar_tip">Novo! Pulsa no teu Gravatar para cambialo!</string>
+ <string name="error_cropping_image">Erro no recorte da imaxe</string>
+ <string name="launch_your_email_app">Abre a túa aplicación de correo electrónico</string>
+ <string name="checking_email">Verificando o enderezo de correo electrónico</string>
+ <string name="not_on_wordpress_com">Non tes conta en WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Non dispoñible neste momento. Introduce o teu contrasinal.. </string>
+ <string name="check_your_email">Verifica o teu correo electrónico</string>
+ <string name="logging_in">Conectando</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Enviar un enlace ao teu correo para iniciar sesión de inmediato</string>
+ <string name="enter_your_password_instead">Introduce o teu contrasinal se non</string>
+ <string name="web_address_dialog_hint">Móstrase publicamente no teus comentarios.</string>
+ <string name="jetpack_not_connected_message">Jetpack está instalado, pero sen conexión a WordPress.com. Queres conectalo?</string>
+ <string name="username_email">Correo electrónico ou nome de usuario</string>
+ <string name="jetpack_not_connected">Jetpack non está conectado</string>
+ <string name="new_editor_reflection_error">O editor visual non é compatible co teu dispositivo. Foi\ndesactivado automaticamente.</string>
+ <string name="stats_insights_latest_post_no_title">(sen título)</string>
+ <string name="capture_or_pick_photo">Fai ou escolle unha foto</string>
+ <string name="plans_post_purchase_button_themes">Ver os temas</string>
+ <string name="plans_post_purchase_text_themes">Agora tes acceso ilimitado aos temas Premium. Proba unha vista previa de calquera deles no teu sitio para comezar.</string>
+ <string name="plans_post_purchase_title_themes">Atopa un tema Premium perfecto</string>
+ <string name="plans_post_purchase_button_video">Comezar un artigo novo</string>
+ <string name="plans_post_purchase_text_video">Con VideoPress podes cargar e aloxar vídeos no teu sitio, dispoñendo ademais dunha capacidade de almacenamento ampliada.</string>
+ <string name="plans_post_purchase_title_video">Dálle vida aos artigos incluíndo vídeos</string>
+ <string name="plans_post_purchase_button_customize">Personalizar o sitio</string>
+ <string name="plans_post_purchase_text_customize">Non tes acceso a caracteres ou colores personalizados, nin á posibilidade de edición de CSS.</string>
+ <string name="plans_post_purchase_title_customize">Personalizar caracteres e colores</string>
+ <string name="plans_post_purchase_text_intro">O teu sitio está a dar chimpos de entusiasmo! Explora as novas funcionalidades do teu sitio e escolle por onde che gustaría comezar.</string>
+ <string name="plans_post_purchase_title_intro">Todo teu, adiante!</string>
+ <string name="export_your_content_message">Os teus artigos, páxinas e configuración enviaránseche a %s.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Non foi posible cargar os plans</string>
+ <string name="export_your_content">Exportar os contidos</string>
+ <string name="exporting_content_progress">Exportando os contidos...</string>
+ <string name="export_email_sent">Mensaxe de exportación enviada</string>
+ <string name="premium_upgrades_message">Tes melloras Premium no teu sitio. Cancela as melloras antes de eliminalo.</string>
+ <string name="show_purchases">Mostrar compras</string>
+ <string name="checking_purchases">Comprobando as compras</string>
+ <string name="premium_upgrades_title">Melloras Premium</string>
+ <string name="purchases_request_error">Algo foi mal. Non foi posible solicitar as compras.</string>
+ <string name="delete_site_progress">Eliminando o sitio...</string>
+ <string name="delete_site_summary">Esta acción non se pode desfacer. Eliminar o sitio implica borrar todos os contidos, colaboradores y dominios do sitio.</string>
+ <string name="delete_site_hint">Eliminar o sitio</string>
+ <string name="export_site_hint">Exportar o sitio a un ficheiro XML</string>
+ <string name="are_you_sure">Estás seguro?</string>
+ <string name="export_site_summary">Se estás seguro, aproveita a ocasión para exportar os teus contidos agora. Máis adiante vai ser imposible recuperalos.</string>
+ <string name="keep_your_content">Garda os teus contidos</string>
+ <string name="domain_removal_hint">Dominios que deixarán de funcionar cando elimines o teu sitio</string>
+ <string name="domain_removal_summary">Mírao ben! A eliminación do sitio borrará tamén o/s dominio/s enumerados a continuación.</string>
+ <string name="primary_domain">Dominio principal</string>
+ <string name="domain_removal">Eliminación do dominio</string>
+ <string name="error_deleting_site_summary">Houbo un erro ao eliminar o sitio. Por favor, contacta co soporte para obter asistencia.</string>
+ <string name="error_deleting_site">Erro na eliminación do sitio</string>
+ <string name="confirm_delete_site_prompt">Escribe %1$s na caixa de embaixo para confirmar. Nese momento o teu sitio desaparecerá para sempre.</string>
+ <string name="site_settings_export_content_title">Exportar os contidos</string>
+ <string name="confirm_delete_site">Confirmar a eliminación do sitio</string>
+ <string name="contact_support">Contactar co soporte</string>
+ <string name="start_over_text">Se queres un sitio pero non queres preservar os contidos que tes agora, o noso soporte pode eliminar artigos, páxinas, ficheiros e comentarios por ti.\n \nIsto manterá activo o teu sitio e a súa URL, e darache a oportunidade de comezar a crear contidos desde cero. Contacta connosco para solicitar unha limpeza total do teu sitio.</string>
+ <string name="site_settings_start_over_hint">Borrar todo e comezar desde cero</string>
+ <string name="let_us_help">Imos che axudar</string>
+ <string name="me_btn_app_settings">Configuración da aplicación</string>
+ <string name="start_over">Comezar desde cero</string>
+ <string name="editor_remove_failed_uploads">Eliminar os contidos que non foi posible cargar</string>
+ <string name="editor_toast_failed_uploads">Fallou a carga dalgúns ficheiros. Nesta situación non se pode \n gardar nin publicar o artigo. Queres borrar todas as cargas que fallaron?</string>
+ <string name="comments_empty_list_filtered_trashed">Non hai comentarios no lixo</string>
+ <string name="site_settings_advanced_header">Avanzado</string>
+ <string name="comments_empty_list_filtered_pending">Non hai comentarios pendentes</string>
+ <string name="comments_empty_list_filtered_approved">Non hai comentarios aprobados</string>
+ <string name="button_done">Feito</string>
+ <string name="button_skip">Omitir</string>
+ <string name="site_timeout_error">Fallou a conexión a WordPress. Tempo de espera esgotado.</string>
+ <string name="xmlrpc_malformed_response_error">Non foi posible conectar. A instalación de WordPress respondeu cun documento XML-RPC non válido.</string>
+ <string name="xmlrpc_missing_method_error">Non foi posible conectar. Os métodos XML-RPC requiridos non están no servidor.</string>
+ <string name="post_format_status">Estado</string>
+ <string name="post_format_video">Vídeo</string>
+ <string name="alignment_center">Centro</string>
+ <string name="theme_free">Gratis</string>
+ <string name="theme_all">Todos</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_gallery">Galería</string>
+ <string name="post_format_image">Imaxe</string>
+ <string name="post_format_link">Enlace</string>
+ <string name="post_format_quote">Cita</string>
+ <string name="post_format_standard">Estándar</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_aside">Aparte</string>
+ <string name="notif_events">Información sobre os cursos e eventos de WordPress.com (en liña ou presenciais)</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Oportunidades para participar nas investigacións e enquisas de WordPress.com.</string>
+ <string name="notif_tips">Trucos para sacar o mellor partido de WordPress.com</string>
+ <string name="notif_community">Comunidade</string>
+ <string name="replies_to_my_comments">Respostas aos meus comentarios</string>
+ <string name="notif_suggestions">Propostas</string>
+ <string name="notif_research">Investigación</string>
+ <string name="site_achievements">Logros do sitio</string>
+ <string name="username_mentions">Mencións do usuario</string>
+ <string name="site_follows">Seguimentos do sitio</string>
+ <string name="likes_on_my_posts">"gústame" nos meus artigos</string>
+ <string name="likes_on_my_comments">"gústame" nos meus comentarios</string>
+ <string name="comments_on_my_site">Comentarios no meu sitio</string>
+ <string name="site_settings_list_editor_summary_other">%d elementos</string>
+ <string name="site_settings_list_editor_summary_one">1 elemento</string>
+ <string name="approve_auto_if_previously_approved">Comentarios de usuarios coñecidos</string>
+ <string name="approve_auto">Todos os usuarios</string>
+ <string name="approve_manual">Ningún comentario</string>
+ <string name="site_settings_paging_summary_other">%d comentarios por páxina</string>
+ <string name="site_settings_paging_summary_one">1 comentario por páxina</string>
+ <string name="site_settings_multiple_links_summary_other">Precisa aprobación se ten máis de %d enlaces</string>
+ <string name="site_settings_multiple_links_summary_one">Precisa aprobación se ten máis de 1 enlace</string>
+ <string name="site_settings_multiple_links_summary_zero">Precisa aprobación se ten máis de 0 enlaces</string>
+ <string name="detail_approve_auto">Aprobar automaticamente os comentarios de quen sexa.</string>
+ <string name="detail_approve_auto_if_previously_approved">Aprobar automaticamente se o usuario ten un comentario aprobado previamente</string>
+ <string name="detail_approve_manual">Solicitar aprobación manual para calquera comentario.</string>
+ <string name="filter_trashed_posts">No lixo</string>
+ <string name="days_quantity_one">1 día</string>
+ <string name="days_quantity_other">%d días</string>
+ <string name="filter_published_posts">Publicados</string>
+ <string name="filter_draft_posts">Borradores</string>
+ <string name="filter_scheduled_posts">Programados</string>
+ <string name="primary_site">Sitio principal</string>
+ <string name="pending_email_change_snackbar">Preme no enlace de verificación na mensaxe enviada a %1$s para confirmar o teu novo enderezo</string>
+ <string name="web_address">Enderezo web</string>
+ <string name="editor_toast_uploading_please_wait">Estanse a cargar os ficheiros. Por favor, espera a que o proceso termine.</string>
+ <string name="error_refresh_comments_showing_older">Non foi posible actualizar os comentarios neste momento - mostrando comentarios antigos</string>
+ <string name="editor_post_settings_set_featured_image">Poñer como imaxe destacada</string>
+ <string name="editor_post_settings_featured_image">Imaxe destacada</string>
+ <string name="new_editor_promo_desc">A aplicación de WordPress para Android inclúe agora un novo editor\n visual. Próbao creando unha entrada nova.</string>
+ <string name="new_editor_promo_title">Editor novo</string>
+ <string name="new_editor_promo_button_label">Estupendo, grazas!</string>
+ <string name="visual_editor_enabled">Editor visual habilitado</string>
+ <string name="editor_content_placeholder">Comparte aquí as túas anécdotas...</string>
+ <string name="editor_page_title_placeholder">Título da páxina</string>
+ <string name="editor_post_title_placeholder">Título do artigo</string>
+ <string name="email_address">Correo electrónico</string>
+ <string name="preference_show_visual_editor">Mostrar o editor visual</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comments">Eliminar definitivamente estes comentarios?</string>
+ <string name="dlg_sure_to_delete_comment">Eliminar definitivamente este comentario?</string>
+ <string name="mnu_comment_delete_permanently">Eliminar</string>
+ <string name="comment_deleted_permanently">Comentario eliminado</string>
+ <string name="mnu_comment_untrash">Recuperar</string>
+ <string name="comments_empty_list_filtered_spam">Non hai comentarios spam</string>
+ <string name="could_not_load_page">Non foi posible cargar a páxina</string>
+ <string name="comment_status_all">Todo</string>
+ <string name="interface_language">Idioma da interface</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">Acerca da aplicación</string>
+ <string name="error_post_account_settings">Non foi posible gardar a configuración da conta</string>
+ <string name="error_post_my_profile">Non foi posible gardar o perfil</string>
+ <string name="error_fetch_account_settings">Non foi posible acceder á configuración da conta</string>
+ <string name="error_fetch_my_profile">Non foi posible acceder ao teu perfil</string>
+ <string name="stats_widget_promo_ok_btn_label">Vale, entendido.</string>
+ <string name="stats_widget_promo_desc">Engade o widget á pantalla de inicio para acceder ás estatísticas cun toque</string>
+ <string name="stats_widget_promo_title">Widget de estatísticas para a pantalla de inicio</string>
+ <string name="site_settings_unknown_language_code_error">Código de idioma no aceptado</string>
+ <string name="site_settings_threading_dialog_description">Permitir a xerarquización dos comentarios en fíos</string>
+ <string name="site_settings_threading_dialog_header">Fíos até</string>
+ <string name="remove">Borrar</string>
+ <string name="search">Busca</string>
+ <string name="add_category">Engadir categoría</string>
+ <string name="disabled">Desactivado</string>
+ <string name="site_settings_image_original_size">Tamaño orixinal</string>
+ <string name="privacy_private">O sitio só é visible para ti e para os usuarios autorizados</string>
+ <string name="privacy_public_not_indexed">O sitio é visible para calquera, pero pídese aos motores de busca que non o indexen.</string>
+ <string name="privacy_public">O sitio é visible para calquera e os motores de busca poden indexalo</string>
+ <string name="about_me_hint">Unhas palabras acerca de ti...</string>
+ <string name="about_me">Acerca de mi</string>
+ <string name="public_display_name_hint">Se non se define, o nome mostrado por defecto será o nome de usuario,</string>
+ <string name="public_display_name">Nome público</string>
+ <string name="my_profile">O meu perfil</string>
+ <string name="first_name">Nome</string>
+ <string name="last_name">Apelido</string>
+ <string name="site_privacy_public_desc">Permitir a indexación do sitio aos motores de busca</string>
+ <string name="site_privacy_hidden_desc">Rexeitar a indexación do sitio polos motores de busca</string>
+ <string name="site_privacy_private_desc">Gustaríame que o sitio fose privado, visible só para os usuarios que eu escolla. </string>
+ <string name="cd_related_post_preview_image">Imaxe de vista previa do artigo relacionado</string>
+ <string name="error_post_remote_site_settings">Non foi posible gardar a información do sitio</string>
+ <string name="error_fetch_remote_site_settings">Non foi posible acceder á información do sitio</string>
+ <string name="error_media_upload_connection">Houbo un fallo na conexión mentres se cargaba o ficheiro</string>
+ <string name="site_settings_disconnected_toast">Sen conexión, edición inhabilitada.</string>
+ <string name="site_settings_unsupported_version_error">Versión de WordPress sen soporte</string>
+ <string name="site_settings_multiple_links_dialog_description">Solicitar aprobación dos comentarios que inclúan un número de enlaces superior a este</string>
+ <string name="site_settings_close_after_dialog_switch_text">Pechar automaticamente</string>
+ <string name="site_settings_close_after_dialog_description">Pechar automaticamente os comentarios nos artigos</string>
+ <string name="site_settings_paging_dialog_description">Separar os fíos de comentarios en distintas páxinas</string>
+ <string name="site_settings_paging_dialog_header">Comentarios por páxina</string>
+ <string name="site_settings_close_after_dialog_title">Pechar os comentarios</string>
+ <string name="site_settings_blacklist_description">Cando un comentario conteña algunha destas palabras no seu contido, nome, URL, correo electrónico ou IP, será sinalado como spam. Pódense incluír fragmentos de palabra, de xeito que "press" servirá para "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">Cando un comentario conteña algunha destas palabras no seu contido, nome, URL, correo electrónico ou IP, será retido na cola de moderación. Pódense incluír fragmentos de palabra, de xeito que "press" servirá para "WordPress". </string>
+ <string name="site_settings_list_editor_input_hint">Introduce unha palabra ou frase</string>
+ <string name="site_settings_list_editor_no_items_text">Sen elementos</string>
+ <string name="site_settings_learn_more_caption">Pódese modificar esta configuración individualmente en cada artigo</string>
+ <string name="site_settings_rp_preview3_site">en "Melloras"</string>
+ <string name="site_settings_rp_preview3_title">Novas melloras: VideoPress para vodas.</string>
+ <string name="site_settings_rp_preview2_site">en "Aplicacións"</string>
+ <string name="site_settings_rp_preview2_title">A aplicación WordPress para Android mellora o seu aspecto</string>
+ <string name="site_settings_rp_preview1_site">en "Móbil"</string>
+ <string name="site_settings_rp_preview1_title">Dispoñible actualización para iPhone/iPad</string>
+ <string name="site_settings_rp_show_images_title">Mostrar imaxes</string>
+ <string name="site_settings_rp_show_header_title">Mostrar a cabeceira</string>
+ <string name="site_settings_rp_switch_summary">"Artigos relacionados" mostra contido pertinente do sitio despois dos artigos.</string>
+ <string name="site_settings_rp_switch_title">Mostrar artigos relacionados</string>
+ <string name="site_settings_delete_site_hint">Borra os datos do sitio da aplicación</string>
+ <string name="site_settings_blacklist_hint">Os comentarios que coincidan cun filtro serán sinalados como spam</string>
+ <string name="site_settings_moderation_hold_hint">Os comentarios que coincidan cun filtro poñeranse na cola de moderación</string>
+ <string name="site_settings_multiple_links_hint">Ignora o límite de enlaces para os usuarios coñecidos</string>
+ <string name="site_settings_whitelist_hint"> O autor do comentario debe ter un comentario previamente aprobado</string>
+ <string name="site_settings_user_account_required_hint">Os usuarios deben estar rexistrados e conectados para comentar</string>
+ <string name="site_settings_identity_required_hint">O autor do comentario debe encher o nome e o correo electrónico</string>
+ <string name="site_settings_manual_approval_hint">Os comentarios deben ser aprobados manualmente</string>
+ <string name="site_settings_paging_hint">Mostrar os comentarios en grupos do tamaño especificado</string>
+ <string name="site_settings_threading_hint">Permitir a xerarquización dos comentarios até un nivel determinado</string>
+ <string name="site_settings_sort_by_hint">Establece a orde en que se mostran os comentarios</string>
+ <string name="site_settings_close_after_hint">No permitir comentarios despois do tempo especificado</string>
+ <string name="site_settings_receive_pingbacks_hint">Permitir notificacións de enlace desde outros blogues</string>
+ <string name="site_settings_send_pingbacks_hint">Tentar a notificación aos blogues enlazados desde o artigo</string>
+ <string name="site_settings_allow_comments_hint">Permitir enviar comentarios aos lectores</string>
+ <string name="site_settings_discussion_hint">Ver e cambiar a configuración de conversa do sitio</string>
+ <string name="site_settings_more_hint">Ver todas as opcións de conversa dispoñibles</string>
+ <string name="site_settings_related_posts_hint">Mostrar ou ocultar artigos relacionados na Guía de Lectura</string>
+ <string name="site_settings_upload_and_link_image_hint">Habilitar a carga de imaxes sempre en tamaño completo</string>
+ <string name="site_settings_image_width_hint">Escala as imaxes nos artigos a este largo</string>
+ <string name="site_settings_format_hint">Establece o formato do novo artigo</string>
+ <string name="site_settings_category_hint">Establece a categoría do novo artigo</string>
+ <string name="site_settings_location_hint">Engadir automaticamente a localización aos artigos</string>
+ <string name="site_settings_password_hint">Cambiar o contrasinal</string>
+ <string name="site_settings_username_hint">Conta de usuario actual</string>
+ <string name="site_settings_language_hint">Lingua na está escrito principalmente este blogue.</string>
+ <string name="site_settings_privacy_hint">Controla quen pode ver o sitio</string>
+ <string name="site_settings_address_hint">O cambio de enderezo non é admitido neste momento</string>
+ <string name="site_settings_tagline_hint">Unha descrición curta ou unha frase atractiva para describir o blogue</string>
+ <string name="site_settings_title_hint">Explica en poucas palabras de que trata este sitio</string>
+ <string name="site_settings_whitelist_known_summary">Comentarios de usuarios coñecidos</string>
+ <string name="site_settings_whitelist_all_summary">Comentarios de todos os usuarios</string>
+ <string name="site_settings_threading_summary">%d niveis</string>
+ <string name="site_settings_privacy_private_summary">Privado</string>
+ <string name="site_settings_privacy_hidden_summary">Oculto</string>
+ <string name="site_settings_delete_site_title">Eliminar o sitio</string>
+ <string name="site_settings_privacy_public_summary">Público</string>
+ <string name="site_settings_blacklist_title">Lista negra</string>
+ <string name="site_settings_moderation_hold_title">Pendentes de moderación</string>
+ <string name="site_settings_multiple_links_title">Enlaces nos comentarios</string>
+ <string name="site_settings_whitelist_title">Aprobar automaticamente</string>
+ <string name="site_settings_paging_title">Paxinación</string>
+ <string name="site_settings_threading_title">Xerarquización</string>
+ <string name="site_settings_sort_by_title">Ordenar por</string>
+ <string name="site_settings_account_required_title">O usuario debe estar conectado</string>
+ <string name="site_settings_identity_required_title">Debe incluír nome e correo electrónico</string>
+ <string name="site_settings_receive_pingbacks_title">Recibir Pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Enviar Pingbacks</string>
+ <string name="site_settings_allow_comments_title">Permitir comentarios</string>
+ <string name="site_settings_default_format_title">Formato predeterminado</string>
+ <string name="site_settings_default_category_title">Categoría predeterminada</string>
+ <string name="site_settings_location_title">Habilitar localización</string>
+ <string name="site_settings_address_title">Enderezo</string>
+ <string name="site_settings_title_title">Título do sitio</string>
+ <string name="site_settings_tagline_title">Subtítulo</string>
+ <string name="site_settings_this_device_header">Este dispositivo</string>
+ <string name="site_settings_discussion_new_posts_header">Prederterminado para os artigos novos</string>
+ <string name="site_settings_account_header">Conta</string>
+ <string name="site_settings_writing_header">Escrita</string>
+ <string name="newest_first">Os máis recentes primeiro</string>
+ <string name="site_settings_general_header">Xeral</string>
+ <string name="discussion">Conversa</string>
+ <string name="privacy">Privacidade</string>
+ <string name="related_posts">Artigos relacionados</string>
+ <string name="comments">Comentarios</string>
+ <string name="oldest_first">Os máis antigos primeiro</string>
+ <string name="close_after">Pecar despois de</string>
+ <string name="media_error_no_permission_upload">Non tes permiso para cargar ficheiros neste sitio</string>
+ <string name="never">Nunca</string>
+ <string name="unknown">Descoñecido</string>
+ <string name="reader_err_get_post_not_found">O artigo xa non existe</string>
+ <string name="reader_err_get_post_not_authorized">Non tes permiso para ver este artigo</string>
+ <string name="reader_err_get_post_generic">Non foi posible acceder a este artigo</string>
+ <string name="blog_name_no_spaced_allowed">O enderezo do sitio non pode conter espazos</string>
+ <string name="invalid_username_no_spaces">O nome de usuario non pode conter espazos</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Os sitios que segues non publicaron nada ultimamente</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Sen artigos recentes</string>
+ <string name="media_details_copy_url_toast">URL copiado ao portapapeis</string>
+ <string name="edit_media">Editar o ficheiro</string>
+ <string name="media_details_copy_url">Copiar o URL</string>
+ <string name="media_details_label_date_uploaded">Cargado</string>
+ <string name="media_details_label_date_added">Engadido</string>
+ <string name="selected_theme">Tema escollido</string>
+ <string name="could_not_load_theme">Non foi posible cargar o tema</string>
+ <string name="theme_activation_error">Algo foi mal. Non foi posible activar o tema.</string>
+ <string name="theme_by_author_prompt_append"> por %1$s</string>
+ <string name="theme_prompt">Grazas por escoller %1$s</string>
+ <string name="theme_view">Ver</string>
+ <string name="theme_details">Detalles</string>
+ <string name="theme_support">Soporte</string>
+ <string name="theme_done">FEITO</string>
+ <string name="theme_manage_site">XESTIONAR O SITIO</string>
+ <string name="theme_try_and_customize">Probar e personalizar</string>
+ <string name="theme_activate">Activar</string>
+ <string name="title_activity_theme_support">Temas</string>
+ <string name="current_theme">Tema actual</string>
+ <string name="customize">Personalizar</string>
+ <string name="date_range_start_date">Data de inicio</string>
+ <string name="date_range_end_date">Data de finalización</string>
+ <string name="details">Detalles</string>
+ <string name="support">Soporte</string>
+ <string name="active">Activo</string>
+ <string name="stats_referrers_spam_generic_error">Algo foi mal durante o proceso. O estado de spam non foi cambiado.</string>
+ <string name="stats_referrers_marking_not_spam">Sinalando como non spam</string>
+ <string name="stats_referrers_unspam">Non é spam</string>
+ <string name="stats_referrers_marking_spam">Sinalando como spam</string>
+ <string name="post_published">Artigo publicado</string>
+ <string name="page_published">Páxina publicada</string>
+ <string name="post_updated">Artigo actualizado</string>
+ <string name="page_updated">Páxina actualizada</string>
+ <string name="theme_auth_error_authenticate">Erro na procura dos temas: fallou a identificación do usuario.</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Sentímolo, non se atoparon temas.</string>
+ <string name="media_file_name">Nome do ficheiro: %s</string>
+ <string name="media_uploaded_on">Cargado o: %s</string>
+ <string name="media_dimensions">Dimensións: %s</string>
+ <string name="media_file_type">Tipo de ficheiro: %s</string>
+ <string name="upload_queued">Agardando na cola</string>
+ <string name="reader_label_gap_marker">Cargar máis artigos</string>
+ <string name="notifications_no_search_results">Non hai sitios que coincidan con \'%s\' </string>
+ <string name="search_sites">Buscar sitios</string>
+ <string name="unread">Sen ler</string>
+ <string name="notifications_empty_view_reader">Ver a Guía de Lectura</string>
+ <string name="notifications_empty_action_followers_likes">Faie ver: comenta nos artigos que liches.</string>
+ <string name="notifications_empty_action_comments">Únete a unha conversa: comenta nos artigos dos blogues que segues.</string>
+ <string name="notifications_empty_action_unread">Reaviva a conversa: escribe un artigo novo.</string>
+ <string name="notifications_empty_action_all">Sé activo! comenta nos artigos dos blogues que segues.</string>
+ <string name="notifications_empty_likes">Non hai novos "gústame" que mostrar ... de momento.</string>
+ <string name="notifications_empty_followers">Non hai novos seguidores dos que informar ... de momento</string>
+ <string name="notifications_empty_comments">Non hai comentarios novos ... de momento</string>
+ <string name="notifications_empty_unread">Xa estás ao día!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Por favor, accede ás estatísticas na aplicación e tenta engadir o widget máis tarde.</string>
+ <string name="stats_widget_error_readd_widget">Elimina o widget e engádeo de novo</string>
+ <string name="stats_widget_error_no_visible_blog">Non se pode acceder ás estatísticas sen ter un blogue visible.</string>
+ <string name="stats_widget_error_no_permissions">A túa conta de WordPress.com non pode acceder ás estatísticas deste blogue</string>
+ <string name="stats_widget_error_no_account">Por favor, inicia sesión en WordPress.</string>
+ <string name="stats_widget_error_generic">Non foi posible cargar as estatísticas</string>
+ <string name="stats_widget_loading_data">Cargando datos...</string>
+ <string name="stats_widget_name_for_blog">Estatísticas de hoxe para %1$s</string>
+ <string name="stats_widget_name">Estatísticas de WordPress hoxe</string>
+ <string name="add_location_permission_required">Necesítase permiso para engadir a localización</string>
+ <string name="add_media_permission_required">Necesítase permiso para engadir contido multimedia</string>
+ <string name="access_media_permission_required">Necesítase permiso para acceder aos contidos multimedia</string>
+ <string name="stats_enable_rest_api_in_jetpack">Para ver as estatísticas debes activar o módulo JSON API en Jetpack</string>
+ <string name="error_open_list_from_notification">Este artigo ou páxina foi publicado en outro sitio</string>
+ <string name="reader_short_comment_count_multi">%s comentarios</string>
+ <string name="reader_short_comment_count_one">1 comentario</string>
+ <string name="reader_label_submit_comment">ENVIADO</string>
+ <string name="reader_hint_comment_on_post">Responder ao artigo...</string>
+ <string name="reader_discover_visit_blog">Ver %s</string>
+ <string name="reader_discover_attribution_blog">Publicado orixinalmente en %s</string>
+ <string name="reader_discover_attribution_author">Publicado orixinalmente por %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Publicado orixinalmente por %1$s en %2$s</string>
+ <string name="reader_short_like_count_multi">%s "gústame"</string>
+ <string name="reader_label_follow_count">%,d seguidores</string>
+ <string name="reader_short_like_count_one">1 "gústame"</string>
+ <string name="reader_short_like_count_none">"Gústame"</string>
+ <string name="reader_menu_tags">Editar etiquetas e blogues</string>
+ <string name="reader_title_post_detail">Artigo da Guía de Lectura</string>
+ <string name="local_draft_explainer">Este artigo é un borrador local aínda sen publicar</string>
+ <string name="local_changes_explainer">Este artigo ten cambios locais aínda sen publicar</string>
+ <string name="notifications_push_summary">Configuración de notificacións que aparece no teu dispositivo</string>
+ <string name="notifications_email_summary">Configuración de notificacións enviada ao correo electrónico asociado á túa conta.</string>
+ <string name="notifications_tab_summary">Configuración de notificacións que aparece na lapela de notificacións</string>
+ <string name="notifications_disabled">As notificacións da aplicación están desactivadas. Pulsa aquí para activalas na Configuración.</string>
+ <string name="notification_types">Tipos de notificacións</string>
+ <string name="error_loading_notifications">Non foi posible cargar a configuración das notificacións</string>
+ <string name="replies_to_your_comments">Respostas aos teus comentarios</string>
+ <string name="comment_likes">"Gústame" nos comentarios</string>
+ <string name="app_notifications">Notificacións da aplicación</string>
+ <string name="notifications_tab">Lapela de notificacións</string>
+ <string name="email">Correo electrónico</string>
+ <string name="notifications_comments_other_blogs">Comentarios en outros sitios</string>
+ <string name="notifications_wpcom_updates">Actualizacións de WordPress.com</string>
+ <string name="notifications_other">Outras</string>
+ <string name="notifications_account_emails">Correo-e de WordPress.com</string>
+ <string name="notifications_account_emails_summary">Enviaremos sempre mensaxes importantes relativos á túa conta, e terás tamén algúns extras de utilidade.</string>
+ <string name="your_sites">Os teus sitios</string>
+ <string name="notifications_sights_and_sounds">Avisos de notificación</string>
+ <string name="stats_insights_latest_post_trend">Hai %1$s que %2$s foi publicado. Aquí tes como lle foi ao artigo até este momento...</string>
+ <string name="stats_insights_latest_post_summary">Resumo do último artigo</string>
+ <string name="button_revert">Reverter</string>
+ <string name="days_ago">Hai %d días</string>
+ <string name="yesterday">Onte</string>
+ <string name="connectionbar_no_connection">Sen conexión</string>
+ <string name="page_trashed">Páxina botada ao lixo</string>
+ <string name="post_deleted">Artigo eliminado</string>
+ <string name="post_trashed">Artigo botado ao lixo</string>
+ <string name="stats_no_activity_this_period">Non hai actividade neste período</string>
+ <string name="trashed">No lixo</string>
+ <string name="page_deleted">Páxina eliminada</string>
+ <string name="button_stats">Estatísticas</string>
+ <string name="button_preview">Vista previa</string>
+ <string name="button_view">Ver</string>
+ <string name="button_edit">Editar</string>
+ <string name="button_publish">Publicar</string>
+ <string name="button_back">Atrás</string>
+ <string name="button_trash">Lixo</string>
+ <string name="my_site_no_sites_view_subtitle">Gustaríache engadir un?</string>
+ <string name="my_site_no_sites_view_title">Aínda non tes sitio ningún en WordPress</string>
+ <string name="my_site_no_sites_view_drake">Ilustración</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Non tes autorización para acceder a este blogue</string>
+ <string name="reader_toast_err_follow_blog_not_found">Non foi posible atopar este blogue</string>
+ <string name="undo">Desfacer</string>
+ <string name="tabbar_accessibility_label_my_site">O meu sitio</string>
+ <string name="tabbar_accessibility_label_me">Eu</string>
+ <string name="passcodelock_prompt_message">Introduce o PIN</string>
+ <string name="editor_toast_changes_saved">Cambios gardados</string>
+ <string name="push_auth_expired">A solicitude expirou. Inicia sesión en WordPress.com para tentalo de novo.</string>
+ <string name="ignore">Ignorar</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de visitas</string>
+ <string name="stats_insights_best_ever">A mellor de todas</string>
+ <string name="stats_insights_most_popular_hour">Hora máis popular</string>
+ <string name="stats_insights_most_popular_day">Día máis popular</string>
+ <string name="stats_insights_popular">Día e hora máis populares</string>
+ <string name="stats_insights_today">Estatísticas de hoxe</string>
+ <string name="stats_insights_all_time">Entradas, visitas e lectores desde o comezo.</string>
+ <string name="stats_insights">Información</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Para ver as estatísticas, inicia sesión na conta de WordPress.com que utilizaches para conectar a Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Estás a buscar outras estatísticas recentes? Trasladámolas á páxina "Información".</string>
+ <string name="me_disconnect_from_wordpress_com">Desconectarse de WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Conectarse a WordPress.com</string>
+ <string name="me_btn_login_logout">Entrar/Saír</string>
+ <string name="account_settings">Configuración da conta</string>
+ <string name="site_picker_cant_hide_current_site">"%s" non foi ocultado porque é o sitio actual</string>
+ <string name="me_btn_support">Axuda e soporte</string>
+ <string name="site_picker_create_dotcom">Crear un sitio en WordPress.com</string>
+ <string name="site_picker_add_site">Engadir sitio</string>
+ <string name="site_picker_edit_visibility">Mostrar/ocultar sitios</string>
+ <string name="site_picker_add_self_hosted">Engadir un sitio autoaloxado</string>
+ <string name="my_site_btn_view_site">Ver o sitio</string>
+ <string name="site_picker_title">Escoller sitio</string>
+ <string name="my_site_btn_switch_site">Cambiar de sitio</string>
+ <string name="my_site_btn_view_admin">Ir ao Panel</string>
+ <string name="my_site_btn_blog_posts">Artigos do blogue</string>
+ <string name="my_site_btn_site_settings">Configuración</string>
+ <string name="my_site_header_publish">Publicar</string>
+ <string name="my_site_header_look_and_feel">Aparencia</string>
+ <string name="reader_label_new_posts_subtitle">Pulsa para velos</string>
+ <string name="my_site_header_configuration">Configuración</string>
+ <string name="notifications_account_required">Inicia sesión en WordPress.com para ver as notificacións</string>
+ <string name="stats_unknown_author">Autor descoñecido</string>
+ <string name="image_added">Imaxe engadida</string>
+ <string name="signout">Desconectar</string>
+ <string name="show">Mostrar</string>
+ <string name="hide">Ocultar</string>
+ <string name="select_all">Seleccionar todo</string>
+ <string name="deselect_all">Desmarcar todo</string>
+ <string name="sign_out_wpcom_confirm">Ao desconectar da túa conta borraranse todos os datos de @%s’s WordPress.com neste dispositivo, incluídos borradores e cambios locais.</string>
+ <string name="select_from_new_picker">Selección múltiple co novo selector</string>
+ <string name="no_device_videos">Non hai vídeos</string>
+ <string name="no_blog_images">Non hai imaxes</string>
+ <string name="no_blog_videos">Non hai vídeos</string>
+ <string name="no_device_images">Non hai imaxes</string>
+ <string name="error_loading_images">Erro na carga de vídeos</string>
+ <string name="error_loading_videos">Erro na carga de imaxes</string>
+ <string name="loading_blog_images">Á procura das imaxes...</string>
+ <string name="loading_blog_videos">Á procura dos vídeos...</string>
+ <string name="stats_generic_error">Non foi posible cargar as estatísticas requiridas</string>
+ <string name="error_loading_blog_images">Fallou a procura das imaxes</string>
+ <string name="error_loading_blog_videos">Fallou a procura dos vídeos</string>
+ <string name="no_media_sources">Fallou a procura dos ficheiros</string>
+ <string name="loading_videos">Cargando vídeos</string>
+ <string name="loading_images">Cargando imaxes</string>
+ <string name="no_media">Non hai ficheiros</string>
+ <string name="device">Dispositivo</string>
+ <string name="language">Idioma</string>
+ <string name="add_to_post">Engadir ao artigo</string>
+ <string name="media_picker_title">Escoller ficheiro</string>
+ <string name="take_photo">Fai unha foto</string>
+ <string name="take_video">Grava un vídeo</string>
+ <string name="tab_title_device_images">Imaxes do dispositivo</string>
+ <string name="tab_title_device_videos">Vídeos do dispositivo</string>
+ <string name="tab_title_site_images">Imaxes do sitio</string>
+ <string name="tab_title_site_videos">Vídeos do sitio</string>
+ <string name="media_details_label_file_name">Nome do ficheiro</string>
+ <string name="media_details_label_file_type">Tipo de ficheiro</string>
+ <string name="error_publish_no_network">Non se pode publicar sen ter conexión. Gardado como borrador.</string>
+ <string name="editor_toast_invalid_path">Ruta do ficheiro non válida</string>
+ <string name="verification_code">Código de verificación</string>
+ <string name="invalid_verification_code">Código de verificación non válido</string>
+ <string name="verify">Verificar</string>
+ <string name="two_step_footer_label">Introduce o código da túa aplicación de autenticación</string>
+ <string name="two_step_sms_sent">Busca o código de verificación nos teus SMS</string>
+ <string name="two_step_footer_button">Enviar o código por SMS</string>
+ <string name="sign_in_jetpack">Para conectar a Jetpack entra na túa conta de WordPress.com</string>
+ <string name="auth_required">Inicia sesión de novo para continuar</string>
+ <string name="stats_search_terms_unknown_search_terms">Termos de busca descoñecidos</string>
+ <string name="stats_followers_total_wpcom_paged">Mostrando %1$d - %2$d de %3$s seguidores de WordPress.com</string>
+ <string name="stats_empty_search_terms">Non se rexistraron termos de busca</string>
+ <string name="stats_entry_search_terms">Termo de busca</string>
+ <string name="stats_view_authors">Autores</string>
+ <string name="stats_view_search_terms">Termos de busca</string>
+ <string name="comments_fetching">Á procura dos comentarios...</string>
+ <string name="pages_fetching">Á procura das páxinas...</string>
+ <string name="posts_fetching">Á procura dos artigos...</string>
+ <string name="media_fetching">Á procura dos ficheiros...</string>
+ <string name="publisher">Editor:</string>
+ <string name="error_notification_open">Non foi posible abrir a notificación</string>
+ <string name="stats_followers_total_email_paged">Mostrando %1$d - %2$d de %3$s seguidores por correo-e</string>
+ <string name="toast_err_post_uploading">Non se pode abrir o artigo mentres está en proceso de carga</string>
+ <string name="stats_empty_search_terms_desc">Aprende máis sobre o tráfico de buscas mirando os termos que levaron aos lectores ao teu sitio.</string>
+ <string name="reader_empty_posts_request_failed">Fallou a procura de artigos</string>
+ <string name="post_uploading">Cargando</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">En total</string>
+ <string name="stats_period">Período</string>
+ <string name="reader_label_new_posts">Artigos novos</string>
+ <string name="reader_empty_posts_in_blog">Este blogue está baleiro</string>
+ <string name="stats_average_per_day">Media diaria</string>
+ <string name="stats_recent_weeks">Últimas semanas</string>
+ <string name="error_copy_to_clipboard">Houbo un erro ao copiar o texto no portapapeis</string>
+ <string name="reader_page_recommended_blogs">Sitios que poden gustarche</string>
+ <string name="stats_months_and_years">Meses e anos</string>
+ <string name="logs_copied_to_clipboard">Os rexistros da aplicación foron copiados no portapapeis</string>
+ <string name="stats_for">Estatísticas para %s</string>
+ <string name="stats_other_recent_stats_label">Outras estatísticas recentes</string>
+ <string name="stats_view_all">Ver todo</string>
+ <string name="stats_view">Ver</string>
+ <string name="stats_followers_an_hour_ago">hai unha hora</string>
+ <string name="stats_followers_a_minute_ago">hai un minuto</string>
+ <string name="stats_followers_seconds_ago">hai uns segundos</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_empty_followers">Sen seguidores</string>
+ <string name="stats_empty_video">Sen reproducións de vídeos</string>
+ <string name="stats_empty_referrers_title">Sen referencias rexistradas</string>
+ <string name="stats_empty_top_posts_title">Sen visitas en artigos ou páxinas</string>
+ <string name="stats_totals_followers">Desde</string>
+ <string name="stats_empty_geoviews">Sen países rexistrados</string>
+ <string name="stats_entry_video_plays">Vídeo</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_entry_followers">Seguidor</string>
+ <string name="stats_totals_publicize">Seguidores</string>
+ <string name="stats_entry_clicks_link">Enlace</string>
+ <string name="stats_view_top_posts_and_pages">Artigos e páxinas</string>
+ <string name="stats_view_videos">Vídeos</string>
+ <string name="stats_view_followers">Seguidores</string>
+ <string name="stats_view_countries">Países</string>
+ <string name="stats_pagination_label">Páxina %1$s de %2$s</string>
+ <string name="stats_timeframe_years">Anos</string>
+ <string name="stats_views">Visitas</string>
+ <string name="stats_visitors">Lectores</string>
+ <string name="themes_fetching">Á procura dos temas...</string>
+ <string name="stats_likes">"Gústame"</string>
+ <string name="stats_followers_months">%1$d meses</string>
+ <string name="stats_followers_a_year">Un ano</string>
+ <string name="stats_followers_years">%1$d anos</string>
+ <string name="stats_followers_a_month">Un mes</string>
+ <string name="stats_followers_minutes">%1$d minutos</string>
+ <string name="stats_followers_hours">%1$d horas</string>
+ <string name="stats_followers_a_day">Un día</string>
+ <string name="stats_followers_days">%1$d días</string>
+ <string name="stats_followers_total_email">Total de seguidores por correo-e: %1$s</string>
+ <string name="stats_followers_email_selector">Correo-e</string>
+ <string name="stats_followers_total_wpcom">Total de seguidores en WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Total de artigos con seguidores dos comentarios: %1$s</string>
+ <string name="stats_comments_by_authors">Por autores</string>
+ <string name="stats_comments_by_posts_and_pages">Por artigos e páxinas</string>
+ <string name="stats_empty_followers_desc">Fai un seguimento do número total de seguidores e do tempo que cada un leva seguindo o teu sitio.</string>
+ <string name="stats_empty_publicize_desc">Fai un seguimento dos teus seguidores desde diversas redes sociais utilizando Publicize.</string>
+ <string name="stats_empty_publicize">Nos se rexistraron seguidores en Publicize</string>
+ <string name="stats_entry_publicize">Servizo</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_empty_clicks_desc">Se os teus contidos inclúen enlaces a outros sitios, poderás ver cales son os máis utilizados polos lectores.</string>
+ <string name="stats_empty_clicks_title">Non se rexistraron clics</string>
+ <string name="stats_empty_video_desc">Se tes cargado vídeos con VideoPress, descobre cantas veces foron vistos.</string>
+ <string name="stats_empty_comments_desc">Se admites comentarios no teu sitio, fai seguimento dos comentadores máis activos e descobre que contidos provocan as conversas máis animadas (baseado nos 1000 comentarios máis recentes).</string>
+ <string name="stats_empty_tags_and_categories_desc">Bota unha ollada aos asuntos máis populares no teu sitio, segundo os teus artigos máis lidos da semana pasada.</string>
+ <string name="stats_empty_top_authors_desc">Fai seguimento das visitas aos artigos de cada colaborador e descobre o contido máis popular de cada un.</string>
+ <string name="stats_empty_tags_and_categories">Sen visitas a artigos ou páxinas etiquetados</string>
+ <string name="stats_empty_referrers_desc">Aprende máis sobre a visibilidade do teu sitio mirando os sitios e buscadores desde os que chegan máis visitas.</string>
+ <string name="stats_empty_top_posts_desc">Descobre cal é o teu contido máis visitado e comproba como lle vai a cada artigo ou paxina ao longo do tempo.</string>
+ <string name="stats_empty_geoviews_desc">Nesta lista verás que países e rexións xeran máis tráfico no teu sitio</string>
+ <string name="ssl_certificate_details">Detalles</string>
+ <string name="delete_sure_post">Eliminar este artigo</string>
+ <string name="delete_sure">Eliminar este borrador</string>
+ <string name="delete_sure_page">Eliminar esta páxina</string>
+ <string name="media_gallery_date_range">Mostrando ficheiros do %1$s ao %2$s</string>
+ <string name="confirm_delete_multi_media">Borrar os elementos seleccionados?</string>
+ <string name="confirm_delete_media">Borrar o elemento seleccionado?</string>
+ <string name="sure_to_remove_account">Borrar este sitio?</string>
+ <string name="cab_selected">%d seleccionados</string>
+ <string name="reader_empty_followed_blogs_title">Non estas a seguir sitio ningún aínda</string>
+ <string name="faq_button">FAQ</string>
+ <string name="create_new_blog_wpcom">Crear un blogue en WordPress.com</string>
+ <string name="new_blog_wpcom_created">Blogue en WordPress.com creado!</string>
+ <string name="reader_empty_comments">Sen comentarios aínda</string>
+ <string name="reader_empty_posts_in_tag">Non hai artigos con esta etiqueta</string>
+ <string name="reader_label_comment_count_multi">%,d comentarios</string>
+ <string name="reader_label_view_original">Ver o artigo orixinal</string>
+ <string name="reader_label_comment_count_single">Un comentario</string>
+ <string name="reader_label_comments_closed">Os comentarios están pechados</string>
+ <string name="reader_label_comments_on">Comentarios en</string>
+ <string name="reader_title_photo_viewer">%1$d de %2$d</string>
+ <string name="error_publish_empty_post">Non se pode publicar un artigo baleiro</string>
+ <string name="error_refresh_unauthorized_pages">Non tes permiso para ver ou editar páxinas</string>
+ <string name="error_refresh_unauthorized_posts">Non tes permiso para ver ou editar artigos</string>
+ <string name="error_refresh_unauthorized_comments">Non tes permiso para ver ou editar comentarios</string>
+ <string name="older_month">Con máis dun mes de antigüidade</string>
+ <string name="more">Máis</string>
+ <string name="older_two_days">Con máis de dous días de antigüidade</string>
+ <string name="older_last_week">Con máis dunha semana de antigüidade</string>
+ <string name="select_a_blog">Escolle un sitio de WordPress</string>
+ <string name="comment">Comentario</string>
+ <string name="comment_trashed">Comentario no lixo</string>
+ <string name="posts_empty_list">Aínda sen artigos. Por que non crear un?</string>
+ <string name="comment_reply_to_user">Resposta a %s</string>
+ <string name="pages_empty_list">Aínda sen páxinas. Por que non crear unha?</string>
+ <string name="media_empty_list_custom_date">Non hai ficheiros neste intervalo de tempo</string>
+ <string name="posting_post">Enviando "%s"</string>
+ <string name="signing_out">Saíndo...</string>
+ <string name="stats_no_blog">Non foi posible cargar as estatísticas do blogue requirido</string>
+ <string name="reader_empty_posts_liked">Non deches "gústame" a artigo ningún</string>
+ <string name="reader_label_like">Gústame</string>
+ <string name="reader_label_liked_by">Gustoulle a</string>
+ <string name="mnu_comment_liked">Gustou</string>
+ <string name="nux_help_description">Vai ao centro de axuda para obter respostas a preguntas comúns ou visita os foros para tratar outras cuestións.</string>
+ <string name="agree_terms_of_service">Ao creares unha conta aceptas os fascinantes %1$sTerms of Service%2$s</string>
+ <string name="sending_content">Cargando o contido de %s</string>
+ <string name="uploading_total">Cargando %1$d de %2$d</string>
+ <string name="browse_our_faq_button">Ves as FAQ</string>
+ <string name="reader_menu_block_blog">Bloquear este blogue</string>
+ <string name="reader_toast_err_block_blog">Non foi posible bloquear este blogue</string>
+ <string name="reader_toast_blog_blocked">Os artigos deste blogue non se mostrarán máis</string>
+ <string name="reader_toast_err_generic">Non foi posible realizar esta acción</string>
+ <string name="hs__username_blank_error">Introduce un nome válido</string>
+ <string name="hs__invalid_email_error">Introduce un correo electrónico válido</string>
+ <string name="contact_us">Contacta connosco</string>
+ <string name="hs__conversation_detail_error">Describe o problema que estás tendo</string>
+ <string name="hs__new_conversation_header">Chat de soporte</string>
+ <string name="hs__conversation_header">Chat de soporte</string>
+ <string name="add_location">Engadir localización</string>
+ <string name="current_location">Localización actual</string>
+ <string name="search_location">Buscar</string>
+ <string name="edit_location">Modificar</string>
+ <string name="search_current_location">Localizar</string>
+ <string name="preference_send_usage_stats_summary">Enviar automaticamente as estatísticas de uso para axudarnos a mellorar WordPress para Android</string>
+ <string name="preference_send_usage_stats">Enviar estatísticas</string>
+ <string name="update_verb">Actualizar</string>
+ <string name="schedule_verb">Programar</string>
+ <string name="reader_page_followed_tags">Etiquetas que sigo</string>
+ <string name="reader_page_followed_blogs">Sitios que sigo</string>
+ <string name="reader_hint_add_tag_or_url">Introduce unha URL ou unha etiqueta para seguila</string>
+ <string name="reader_label_tag_preview">Artigos coa etiqueta %s</string>
+ <string name="reader_toast_err_get_blog_info">Non se pode mostrar este blogue</string>
+ <string name="reader_toast_err_already_follow_blog">Xa segues este blogue</string>
+ <string name="reader_toast_err_follow_blog">Non foi posible seguir este blogue</string>
+ <string name="reader_toast_err_unfollow_blog">Non foi posible deixar de seguir este blogue</string>
+ <string name="reader_empty_recommended_blogs">Sen blogues recomendados</string>
+ <string name="reader_title_blog_preview">Blogue da Guía de Lectura</string>
+ <string name="reader_title_tag_preview">Etiqueta da Guía de Lectura</string>
+ <string name="reader_title_subs">Etiquetas e blogues</string>
+ <string name="reader_label_followed_blog">Blogue en seguimento</string>
+ <string name="saving">Gardando...</string>
+ <string name="media_empty_list">Sen ficheiros</string>
+ <string name="ptr_tip_message">Truco: Arrastra cara abaixo para actualizar</string>
+ <string name="forgot_password">Perdiches o contrasinal?</string>
+ <string name="help">Axuda</string>
+ <string name="forums">Foros</string>
+ <string name="help_center">Centro de axuda</string>
+ <string name="ssl_certificate_error">Certificado SSL non válido</string>
+ <string name="ssl_certificate_ask_trust">Se habitualmente conectas a este sitio sen problema, este erro pode significar que alguén esta¡á tentando suplantarche no sitio, así que que non deberías continuar. Queres confiar no certificado aínda así?</string>
+ <string name="no_network_message">Non hai rede dispoñible</string>
+ <string name="blog_not_found">Houbo un erro mentres se accedía a este blogue</string>
+ <string name="comments_empty_list">Non hai comentarios</string>
+ <string name="mnu_comment_unspam">Non é spam</string>
+ <string name="no_site_error">Non foi posible conectar ao sitio de WordPress</string>
+ <string name="adding_cat_success">Categoría engadida con éxito</string>
+ <string name="cat_name_required">O campo de nome de categoría é obrigatorio</string>
+ <string name="category_automatically_renamed">O nome de categoría %1$s non é válido. Foi cambiado por %2$s.</string>
+ <string name="no_account">Non se atopou conta de WordPress, engade unha conta e proba outra vez.</string>
+ <string name="stats_empty_comments">Non hai comentarios aínda</string>
+ <string name="stats_bar_graph_empty">Non hai estatísticas dispoñibles</string>
+ <string name="invalid_url_message">Comproba se a URL introducida é válida</string>
+ <string name="notifications_empty_list">Sen notificacións</string>
+ <string name="error_delete_post">Houbo un erro mentres se eliminaba o %s</string>
+ <string name="error_generic">Houbo un erro</string>
+ <string name="passcode_wrong_passcode">PIN incorrecto</string>
+ <string name="invalid_password_message">O contrasinal debe conter como mínimo 4 caracteres</string>
+ <string name="invalid_username_too_short">O nome de usuario debe ter máis de 4 caracteres</string>
+ <string name="invalid_username_too_long">O nome de usuario debe ter menos de 61 caracteres</string>
+ <string name="username_only_lowercase_letters_and_numbers">O nome de usuario só pode conter letras minúsculas (a-z) e números</string>
+ <string name="username_required">Introduce un nome de usuario</string>
+ <string name="username_not_allowed">Nome de usuario non permitido</string>
+ <string name="username_must_be_at_least_four_characters">O nome de usuario debe ter como mínimo 4 caracteres</string>
+ <string name="username_contains_invalid_characters">O nome de usuario non pode conter o carácter “_”</string>
+ <string name="username_must_include_letters">O nome de usuario debe conter como mínimo unha letra (a-z)</string>
+ <string name="email_not_allowed">Ese enderezo de correo electrónico non está permitido</string>
+ <string name="email_invalid">Introduce un enderezo de correo electrónico válido</string>
+ <string name="username_exists">Ese nome de usuario xa existe</string>
+ <string name="email_exists">Ese enderezo de correo electrónico xa está sendo usado</string>
+ <string name="username_reserved_but_may_be_available">Ese nome de usuario está reservado neste momento, pero pode ser que estea dispoñible nun par de días.</string>
+ <string name="blog_name_not_allowed">Ese enderezo de sitio non está permitido</string>
+ <string name="blog_name_required">Introduce un enderezo para o sitio</string>
+ <string name="blog_name_must_be_at_least_four_characters">O enderezo do sitio debe ter como mínimo 4 caracteres</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">O enderezo do sitio debe ter menos de 64 caracteres</string>
+ <string name="blog_name_contains_invalid_characters">O enderezo do sitio non pode conter o carácter “_”</string>
+ <string name="blog_name_cant_be_used">Non podes usar ese enderezo de sitio</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">O enderezo do sitio só pode conter letras minúsculas (a-z) e números</string>
+ <string name="blog_name_exists">Ese sitio xa existe</string>
+ <string name="blog_name_reserved">Ese sitio está reservado</string>
+ <string name="blog_name_reserved_but_may_be_available">Ese sitio está reservado neste momento, pero pode que estea dispoñible nun par de días.</string>
+ <string name="username_or_password_incorrect">O nome de usuario ou o contrasinal introducido é incorrecto</string>
+ <string name="nux_cannot_log_in">Non foi posible conectar</string>
+ <string name="gallery_error">Non foi posible obter o elemento multimedia</string>
+ <string name="wait_until_upload_completes">Agarda até que se complete a carga</string>
+ <string name="could_not_remove_account">Non foi posible borrar o sitio</string>
+ <string name="error_refresh_posts">Non foi posible actualizar os artigos neste momento</string>
+ <string name="error_refresh_pages">Non foi posible actualizar as páxinas neste momento</string>
+ <string name="error_load_comment">Non foi posible cargar o comentario</string>
+ <string name="error_refresh_stats">Non foi posible actualizar as estatísticas neste momento</string>
+ <string name="error_refresh_comments">Non foi posible actualizar os comentarios neste momento</string>
+ <string name="error_refresh_notifications">Non foi posible actualizar as notificacións neste momento</string>
+ <string name="invalid_email_message">O enderezo de correo non é válido</string>
+ <string name="reply_failed">Fallou a resposta</string>
+ <string name="error_moderate_comment">Houbo un erro durante a moderación</string>
+ <string name="error_edit_comment">Houbo un erro durante a edición do comentario</string>
+ <string name="error_upload">Houbo un erro mentres se cargaba o %s</string>
+ <string name="error_downloading_image">Erro na descarga da imaxe</string>
+ <string name="theme_fetch_failed">Fallou a procura de temas</string>
+ <string name="theme_set_failed">Fallou a activación do tema</string>
+ <string name="theme_auth_error_message">Asegúrate de se tes privilexio para activar temas</string>
+ <string name="adding_cat_failed">Fallou a adición da categoría</string>
+ <string name="sdcard_message">Para cargar ficheiros fai é necesario ter unha tarxeta SD montada</string>
+ <string name="out_of_memory">Memoria do dispositivo esgotada</string>
+ <string name="required_field">Campo obrigatorio</string>
+ <string name="email_hint">Correo electrónico</string>
+ <string name="blog_name_must_include_letters">O enderezo do sitio debe ter como mínimo unha letra (a-z)</string>
+ <string name="blog_name_invalid">Enderezo do sitio non válido</string>
+ <string name="blog_title_invalid">Título do sitio non válido</string>
+ <string name="notifications_empty_all">Sen notificacións ... de momento.</string>
+ <string name="new_post">Artigo novo</string>
+ <string name="new_media">Ficheiro novo</string>
+ <string name="view_site">Ver o sitio</string>
+ <string name="privacy_policy">Política de privacidade</string>
+ <string name="local_changes">Cambios locais</string>
+ <string name="image_settings">Configuración da imaxe</string>
+ <string name="add_account_blog_url">Enderezo do blogue</string>
+ <string name="wordpress_blog">Blogue feito con WordPress</string>
+ <string name="reader_toast_err_add_tag">Non foi posible engadir esta etiqueta</string>
+ <string name="add_new_category">Engadir categoría nova</string>
+ <string name="category_name">Nome da categoría</string>
+ <string name="category_slug">Identificador da categoría (opcional)</string>
+ <string name="category_desc">Descrición da categoría (opcional)</string>
+ <string name="category_parent">Categoría superior (opcional)</string>
+ <string name="share_action_post">Artigo novo</string>
+ <string name="share_action_media">Biblioteca Multimedia</string>
+ <string name="file_error_create">Non foi posible crear o ficheiro temporal para a carga. Asegúrate de que haxa espazo libre suficiente no dispositivo.</string>
+ <string name="location_not_found">Localización descoñecida</string>
+ <string name="invalid_site_url_message">Comproba se a URL introducida é válida</string>
+ <string name="pending_review">Pendente de revisión</string>
+ <string name="http_authorization_required">Necesítase autorización</string>
+ <string name="post_format">Formato do artigo</string>
+ <string name="hint_comment_content">Comentario</string>
+ <string name="saving_changes">Gardando os cambios</string>
+ <string name="remove_account">Borrar o sitio</string>
+ <string name="blog_removed_successfully">Sitio borrado con éxito</string>
+ <string name="delete_draft">Eliminar o borrador</string>
+ <string name="preview_page">Vista previa da páxina</string>
+ <string name="preview_post">Vista previa do artigo</string>
+ <string name="comment_added">Comentario engadido con éxito</string>
+ <string name="view_in_browser">Ver no navegador</string>
+ <string name="xmlrpc_error">Non foi posible conectar. Introduce a ruta completa a xmlrpc.php no teu sitio e proba outra vez.</string>
+ <string name="select_categories">Seleccionar categorías</string>
+ <string name="account_details">Detalles da conta</string>
+ <string name="edit_post">Editar o artigo</string>
+ <string name="add_comment">Engadir un comentario</string>
+ <string name="connection_error">Erro de conexión</string>
+ <string name="cancel_edit">Cancelar a edición</string>
+ <string name="media_gallery_type_thumbnail_grid">Grella de miniaturas</string>
+ <string name="media_gallery_edit">Editar a galería</string>
+ <string name="media_error_no_permission">Non tes permiso para ver a Biblioteca Multimedia</string>
+ <string name="cannot_delete_multi_media_items">Non se poden eliminar algún ficheiros neste momento. Téntao de novo máis tarde.</string>
+ <string name="themes_live_preview">Vista previa </string>
+ <string name="theme_current_theme">Tema actual</string>
+ <string name="theme_premium_theme">Tema Premium</string>
+ <string name="link_enter_url_text">Texto do enlace (opcional)</string>
+ <string name="create_a_link">Crear un enlace</string>
+ <string name="page_settings">Configuración da páxina</string>
+ <string name="local_draft">Borrador local</string>
+ <string name="upload_failed">Fallou a carga</string>
+ <string name="horizontal_alignment">Aliñamento horizontal</string>
+ <string name="post_settings">Configuración do artigo</string>
+ <string name="delete_post">Eliminar o artigo</string>
+ <string name="delete_page">Eliminar a páxina</string>
+ <string name="comment_status_approved">Aprobado</string>
+ <string name="comment_status_unapproved">Pendente</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">No lixo</string>
+ <string name="edit_comment">Editar o comentario</string>
+ <string name="mnu_comment_approve">Aprobar</string>
+ <string name="mnu_comment_unapprove">Rexeitar</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Botar ao lixo</string>
+ <string name="dlg_approving_comments">Aprobando</string>
+ <string name="dlg_unapproving_comments">Rexeitando</string>
+ <string name="dlg_spamming_comments">Sinalando como spam</string>
+ <string name="dlg_trashing_comments">Enviando ao lixo</string>
+ <string name="dlg_confirm_trash_comments">Botar ao lixo?</string>
+ <string name="trash_yes">Botalo ao lixo</string>
+ <string name="trash_no">Non botalo ao lixo</string>
+ <string name="trash">Lixo</string>
+ <string name="author_name">Nome do autor</string>
+ <string name="author_email">Correo-e do autor</string>
+ <string name="author_url">URL do autor</string>
+ <string name="sure_to_cancel_edit_comment">Cancelar a edición deste comentario?</string>
+ <string name="file_not_found">Non foi posible atopar o ficheiro para cargar. Terá sido eliminado ou movido?</string>
+ <string name="reader_toast_err_remove_tag">Non foi posible borrar esta etiqueta</string>
+ <string name="email_cant_be_used_to_signup">Ese enderezo de correo non serve para rexistrarche. Temos problemas con ese provedor por bloqueo das nosas mensaxes. Utiliza outro provedor de correo electrónico.</string>
+ <string name="email_reserved">Ese enderezo de correo xa está en uso. Comproba se tes unha mensaxe de activación na túa bandexa de entrada. Se non o activas, podes tentalo de novo dentro dalgúns días.</string>
+ <string name="reader_title_applog">Rexistro da aplicación</string>
+ <string name="error_blog_hidden">Este blogue non se pode cargar porque está oculto. Habilítao de novo na configuración e proba outra vez.</string>
+ <string name="fatal_db_error">Houbo un erro mentres se creaba a base de datos da aplicación. Proba a reinstalar a aplicación.</string>
+ <string name="jetpack_message_not_admin">O plugin Jetppack é necesario para as estatísticas. Contacta co administrador do sitio.</string>
+ <string name="page_not_published">O estado da páxina non está publicado</string>
+ <string name="open_source_licenses">Licenzas de Código Aberto</string>
+ <string name="http_credentials">Credenciais HTTP (opcional)</string>
+ <string name="content_required">Fai falla un comentario</string>
+ <string name="toast_comment_unedited">O comentario non foi modificado</string>
+ <string name="post_not_published">O estado do artigo non está publicado</string>
+ <string name="scaled_image_error">Introduce un valor de largura a escala válido</string>
+ <string name="post_not_found">Houbo un erro mentres se cargaba o artigo. Actualiza os artigos e téntao de novo.</string>
+ <string name="learn_more">Saber máis</string>
+ <string name="media_gallery_settings_title">Configuración da Galería</string>
+ <string name="media_gallery_image_order">Orde das imaxes</string>
+ <string name="media_gallery_num_columns">Número de columnas</string>
+ <string name="reader_share_link">Enlace para compartir</string>
+ <string name="site_address">Enderezo (URL) do teu sitio autoaloxado</string>
+ <string name="deleting_page">Eliminando a páxina</string>
+ <string name="deleting_post">Eliminando o artigo</string>
+ <string name="share_url_post">Compartir o artigo</string>
+ <string name="share_url_page">Compartir a páxina</string>
+ <string name="share_link">Enlace para compartir</string>
+ <string name="creating_your_site">Creando o teu sitio</string>
+ <string name="creating_your_account">Creando a conta</string>
+ <string name="error_refresh_media">Algo foi mal mentres se actualizaba a Biblioteca Multimedia. Téntao máis tarde.</string>
+ <string name="reader_empty_posts_in_tag_updating">Á procura de artigos...</string>
+ <string name="reader_label_reply">Responder</string>
+ <string name="video">Vídeo</string>
+ <string name="download">Descargando o ficheiro</string>
+ <string name="comment_spammed">Comentario sinalado como spam</string>
+ <string name="cant_share_no_visible_blog">Non podes compartir en WordPress se non tes algún blogue visible</string>
+ <string name="reader_likes_you_and_multi">A ti e a outros %,d gustoulles isto</string>
+ <string name="reader_likes_multi">A %,d persoas gustoulles isto</string>
+ <string name="reader_toast_err_get_comment">Non foi posible acceder a este comentario</string>
+ <string name="pick_photo">Escoller foto</string>
+ <string name="pick_video">Escoller vídeo</string>
+ <string name="select_time">Escoller a hora</string>
+ <string name="select_date">Escoller a data</string>
+ <string name="reader_empty_followed_blogs_description">Non hai problema, pulsa na icona enriba á dereita para comezar a exploración!</string>
+ <string name="reader_likes_you_and_one">A ti e a un máis gustoulles isto</string>
+ <string name="reader_toast_err_get_post">Non foi posible acceder a este artigo</string>
+ <string name="account_two_step_auth_enabled">Esta conta ten activada a autenticación de dou pasos. Vai á configuración de seguridade de WordPress.com e crea un contrasinal específico para a aplicación.</string>
+ <string name="validating_user_data">Validando os datos de usuario</string>
+ <string name="validating_site_data">Validando os datos do sitio</string>
+ <string name="nux_tap_continue">Continuar</string>
+ <string name="nux_welcome_create_account">Crear unha conta</string>
+ <string name="signing_in">Conectando...</string>
+ <string name="nux_oops_not_selfhosted_blog">Iniciar sesión en WordPress.com</string>
+ <string name="password_invalid">Necesitas un contrasinal máis seguro. Asegúrate de usar sete ou máis caracteres e mestura letras maiúsculas e minúsculas, números e caracteres especiais.</string>
+ <string name="nux_add_selfhosted_blog">Engadir un sitio autoaloxado</string>
+ <string name="reader_toast_err_tag_exists">Xa segues esta etiqueta</string>
+ <string name="reader_toast_err_tag_invalid">Non é unha etiqueta válida</string>
+ <string name="reader_empty_followed_tags">Non segues etiqueta ningunha</string>
+ <string name="create_account_wpcom">Crear unha conta en WordPress.com</string>
+ <string name="button_next">Seguinte</string>
+ <string name="connecting_wpcom">Conectando con WordPress.com</string>
+ <string name="username_invalid">Nome de usuario non válido</string>
+ <string name="nux_tutorial_get_started_title">Comezar!</string>
+ <string name="select_from_media_library">Escoller na Biblioteca Multimedia</string>
+ <string name="jetpack_message">Para as estatísticas é necesario o plugin JetPack. Queres instalar Jetpack?</string>
+ <string name="jetpack_not_found">Non se atopou Jetpack</string>
+ <string name="reader_untitled_post">(Sen título)</string>
+ <string name="reader_btn_share">Compartir</string>
+ <string name="reader_btn_follow">Seguir</string>
+ <string name="reader_btn_unfollow">Seguindo</string>
+ <string name="reader_hint_comment_on_comment">Responder ao comentario</string>
+ <string name="reader_label_added_tag">%s engadido</string>
+ <string name="reader_label_removed_tag">%s eliminado</string>
+ <string name="reader_toast_err_comment_failed">Non foi posible enviar o comentario</string>
+ <string name="media_add_popup_title">Engadir á Biblioteca Multimedia</string>
+ <string name="media_add_new_media_gallery">Crear unha Galería</string>
+ <string name="empty_list_default">Esta lista está baleira</string>
+ <string name="reader_likes_one">A unha persoa gustoulle isto</string>
+ <string name="reader_likes_only_you">Gústache isto</string>
+ <string name="reader_toast_err_share_intent">Non foi posible compartir</string>
+ <string name="reader_toast_err_view_image">Non foi posible mostrar a imaxe</string>
+ <string name="reader_toast_err_url_intent">No foi posible abrir %s</string>
+ <string name="reader_share_subject">Compartido desde %s</string>
+ <string name="limit_reached">Límite acadado. Podes intentalo de novo dentro dun minuto. Tentalo antes só servirá para incrementar a espera até que a prohibición caduque. Se consideras que se trata dun erro, contacta co soporte.</string>
+ <string name="stats_totals_plays">Reproducións</string>
+ <string name="passcode_manage">Administrar o bloqueo con PIN</string>
+ <string name="passcode_enter_passcode">Introduce o PIN</string>
+ <string name="passcode_enter_old_passcode">Introduce o PIN anterior</string>
+ <string name="passcode_re_enter_passcode">Volve introducir o PIN</string>
+ <string name="passcode_change_passcode">Cambiar o PIN</string>
+ <string name="passcode_set">PIN establecido</string>
+ <string name="passcode_preference_title">Bloqueo con PIN</string>
+ <string name="passcode_turn_off">Desactivar o bloqueo con PIN</string>
+ <string name="passcode_turn_on">Activar o bloqueo con PIN</string>
+ <string name="stats_view_tags_and_categories">Etiquetas e categorías</string>
+ <string name="stats_view_referrers">Referencias</string>
+ <string name="stats_timeframe_today">Hoxe</string>
+ <string name="stats_timeframe_yesterday">Onte</string>
+ <string name="stats_timeframe_days">Días</string>
+ <string name="stats_timeframe_weeks">Semanas</string>
+ <string name="stats_timeframe_months">Meses</string>
+ <string name="stats_entry_country">País</string>
+ <string name="stats_entry_posts_and_pages">Título</string>
+ <string name="stats_entry_tags_and_categories">Asunto</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Referencia</string>
+ <string name="stats_totals_views">Visitas</string>
+ <string name="media_edit_caption_hint">Introduce aquí unha lenda</string>
+ <string name="media_edit_description_hint">Introduce aquí unha descrición</string>
+ <string name="media_edit_success">Actualizado</string>
+ <string name="themes_details_label">Detalles</string>
+ <string name="themes_features_label">Características</string>
+ <string name="theme_activate_button">Activar</string>
+ <string name="post_excerpt">Extracto</string>
+ <string name="share_action">Compartir</string>
+ <string name="stats">Estatísticas</string>
+ <string name="stats_view_visitors_and_views">Lectores e visitas</string>
+ <string name="unattached">Sen anexar</string>
+ <string name="custom_date">Data personalizada</string>
+ <string name="media_gallery_image_order_random">Aleatorio</string>
+ <string name="media_gallery_image_order_reverse">Inverso</string>
+ <string name="media_gallery_type">Tipo</string>
+ <string name="media_gallery_type_squares">Cadrados</string>
+ <string name="media_gallery_type_tiled">Mosaico</string>
+ <string name="media_gallery_type_circles">Círculos</string>
+ <string name="media_gallery_type_slideshow">Pase de diapositivas</string>
+ <string name="media_edit_title_text">Título</string>
+ <string name="media_edit_caption_text">Lenda</string>
+ <string name="media_edit_description_text">Descrición</string>
+ <string name="media_edit_title_hint">Introduce aquí o título</string>
+ <string name="media_add_popup_capture_photo">Facer unha foto</string>
+ <string name="media_add_popup_capture_video">Gravar un vídeo</string>
+ <string name="themes">Temas</string>
+ <string name="all">Todo</string>
+ <string name="images">Imaxes</string>
+ <string name="stats_view_clicks">Clics</string>
+ <string name="stats_totals_clicks">Clics</string>
+ <string name="media_edit_failure">Fallou a actualización</string>
+ <string name="theme_activating_button">Activando</string>
+ <string name="theme_auth_error_title">Fallou a procura de temas</string>
+ <string name="share_action_title">Engadir a ...</string>
+ <string name="theme_set_success">Tema activado con éxito!</string>
+ <string name="upload">Cargar</string>
+ <string name="discard">Descartar</string>
+ <string name="new_notifications">%d notificacións novas</string>
+ <string name="more_notifications">e %d máis.</string>
+ <string name="sign_in">Entrar</string>
+ <string name="notifications">Notificacións</string>
+ <string name="note_reply_successful">Resposta publicada</string>
+ <string name="follows">Segue</string>
+ <string name="loading">Cargando...</string>
+ <string name="httpuser">usuario HTTP</string>
+ <string name="httppassword">contrasinal HTTP</string>
+ <string name="error_media_upload">Houbo un erro mentres se cargaba o ficheiro</string>
+ <string name="post_content">Contido (pulsa para engadir texto e ficheiros)</string>
+ <string name="publish_date">Publicar</string>
+ <string name="content_description_add_media">Engadir ficheiro</string>
+ <string name="incorrect_credentials">Nome de usuario ou contrasinal incorrecto</string>
+ <string name="password">Contrasinal</string>
+ <string name="username">Nome de usuario</string>
+ <string name="reader">Guía de Lectura</string>
+ <string name="pages">Páxinas</string>
+ <string name="caption">Lenda (opcional)</string>
+ <string name="width">Largo</string>
+ <string name="posts">Artigos</string>
+ <string name="anonymous">Anónimo</string>
+ <string name="page">Páxina</string>
+ <string name="post">Artigo</string>
+ <string name="featured">Usar como imaxe destacada</string>
+ <string name="featured_in_post">Incluír una imaxe no contido do artigo</string>
+ <string name="no_network_title">Non hai rede dispoñible</string>
+ <string name="blogusername">usuario</string>
+ <string name="ok">Vale</string>
+ <string name="scaled_image">Largo da imaxe escalada</string>
+ <string name="upload_scaled_image">Cargar e ligar á imaxe escalada</string>
+ <string name="scheduled">Programado</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Cargando...</string>
+ <string name="version">Versión</string>
+ <string name="tos">Termos de servizo</string>
+ <string name="app_title">WordPress para Android</string>
+ <string name="max_thumbnail_px_width">Largo predeterminado da imaxe</string>
+ <string name="image_alignment">Aliñación</string>
+ <string name="refresh">Actualizar</string>
+ <string name="untitled">Sen título</string>
+ <string name="edit">Editar</string>
+ <string name="post_id">Artigo</string>
+ <string name="page_id">Páxina</string>
+ <string name="post_password">Contrasinal (opcional)</string>
+ <string name="immediately">Inmediatamente</string>
+ <string name="quickpress_add_alert_title">Establecer o nome do atallo</string>
+ <string name="settings">Configuración</string>
+ <string name="today">Hoxe</string>
+ <string name="share_url">Compartir o URL</string>
+ <string name="quickpress_window_title">Seleccionar un blogue para o atallo de QuickPress</string>
+ <string name="quickpress_add_error">O nome do atallo non pode estar baleiro</string>
+ <string name="publish_post">Publicar</string>
+ <string name="draft">Borrador</string>
+ <string name="post_private">Privado</string>
+ <string name="upload_full_size_image">Cargar e ligar á imaxe completa</string>
+ <string name="title">Título</string>
+ <string name="tags_separate_with_commas">Etiquetas (separadas por comas)</string>
+ <string name="categories">Categorías</string>
+ <string name="dlg_deleting_comments">Borrando os comentarios</string>
+ <string name="notification_vibrate">Vibración</string>
+ <string name="notification_sound">Son de notificación</string>
+ <string name="notification_blink">Aviso luminoso</string>
+ <string name="status">Estado</string>
+ <string name="location">Localización</string>
+ <string name="sdcard_title">Tarxeta SD requirida</string>
+ <string name="select_video">Selecciona un vídeo da galería</string>
+ <string name="media">Multimedia</string>
+ <string name="delete">Borrar</string>
+ <string name="none">Ningún</string>
+ <string name="blogs">Blogues</string>
+ <string name="select_photo">Escolle unha foto da Galería</string>
+ <string name="yes">Si</string>
+ <string name="no">Non</string>
+ <string name="notification_settings">Opcións de notificación</string>
+ <string name="reply">Responder</string>
+ <string name="preview">Vista previa</string>
+ <string name="error">Erro</string>
+ <string name="cancel">Cancelar</string>
+ <string name="save">Gardar</string>
+ <string name="add">Engadir</string>
+ <string name="on">en</string>
+ <string name="category_refresh_error">Erro na actualización da categoría</string>
+</resources>
diff --git a/WordPress/src/main/res/values-he/strings.xml b/WordPress/src/main/res/values-he/strings.xml
new file mode 100644
index 000000000..da9ed339b
--- /dev/null
+++ b/WordPress/src/main/res/values-he/strings.xml
@@ -0,0 +1,1139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">מנהל המערכת</string>
+ <string name="role_editor">עורך</string>
+ <string name="role_author">מאת</string>
+ <string name="role_contributor">תורם</string>
+ <string name="role_follower">עוקב</string>
+ <string name="role_viewer">צופה</string>
+ <string name="error_post_my_profile_no_connection">אין חיבור, הפרופיל לא נשמר</string>
+ <string name="site_settings_list_editor_action_mode_title">%1$d נבחרו</string>
+ <string name="plans_manage">ניהול התוכנית שלך בכתובת\nWordPress.com/plans</string>
+ <string name="title_follower">עוקב</string>
+ <string name="people_empty_list_filtered_viewers">אין לך צופים עדיין.</string>
+ <string name="title_email_follower">עוקב אימייל</string>
+ <string name="people_empty_list_filtered_email_followers">אין לך עוקבים באימייל עדיין.</string>
+ <string name="people_empty_list_filtered_followers">אין לך עוקבים עדיין.</string>
+ <string name="people_empty_list_filtered_users">אין לך משתמשים עדיין.</string>
+ <string name="people_dropdown_item_email_followers">עוקבי אימייל</string>
+ <string name="people_dropdown_item_viewers">צופים</string>
+ <string name="people_dropdown_item_followers">עוקבים</string>
+ <string name="people_dropdown_item_team">צוות</string>
+ <string name="invite_message_usernames_limit">אפשר לשלוח עד 10 הזמנות לכתובות אימייל ו/או שמות משתמשים ב-WordPress.com. הוראות ליצירת שם משתמש יישלחו לכל מי שזקוק לשם משתמש.</string>
+ <string name="viewer_remove_confirmation_message">לאחר הסרת צופה זה, הוא או היא לא יוכלו לבקר יותר באתר זה.\n\nבחרת להסיר צופה זה - האם ההחלטה סופית?</string>
+ <string name="follower_remove_confirmation_message">לאחר הסרת עוקב זה, הוא יפסיק לקבל הודעות לגבי אתר זה, אלא אם יגדיר מעקב מחדש.\n\nבחרת להסיר עוקב זה - האם ההחלטה סופית?</string>
+ <string name="follower_subscribed_since">מאז %1$s</string>
+ <string name="reader_label_view_gallery">הצגת גלריה</string>
+ <string name="error_remove_follower">לא ניתן היה להסיר עוקב</string>
+ <string name="error_remove_viewer">לא ניתן היה להסיר צופה</string>
+ <string name="error_fetch_email_followers_list">לא הצלחנו לאחזר עוקבי אימייל של האתר</string>
+ <string name="error_fetch_followers_list">לא הצלחנו לאחזר עוקבים של האתר</string>
+ <string name="editor_failed_uploads_switch_html">מספר העלאות מדיה נכשלו. לא ניתן לעבור למצב HTML\n במצב זה. להסיר את כל ההעלאות שנכשלו ולהמשיך?</string>
+ <string name="format_bar_description_html">מצב HTML</string>
+ <string name="visual_editor">עורך ויזואלי</string>
+ <string name="image_thumbnail">תמונה ממוזערת של תמונה</string>
+ <string name="format_bar_description_ul">רשימה לא מסודרת</string>
+ <string name="format_bar_description_ol">רשימה מסודרת</string>
+ <string name="format_bar_description_more">הוספה</string>
+ <string name="format_bar_description_media">הוספת מדיה</string>
+ <string name="format_bar_description_quote">ציטוט</string>
+ <string name="format_bar_description_strike">קו חוצה</string>
+ <string name="format_bar_description_link">הוספת קישור</string>
+ <string name="format_bar_description_italic">נטוי</string>
+ <string name="format_bar_description_underline">קו תחתון</string>
+ <string name="image_width">רוחב</string>
+ <string name="format_bar_description_bold">מודגש</string>
+ <string name="image_settings_save_toast">שינוי נשמרו</string>
+ <string name="image_caption">כיתוב</string>
+ <string name="image_alt_text">טקסט חלופי</string>
+ <string name="image_link_to">קישור אל</string>
+ <string name="image_settings_dismiss_dialog_title">לבטל שינויים שלא נשמרו?</string>
+ <string name="stop_upload_dialog_title">לעצור העלאה?</string>
+ <string name="stop_upload_button">עצור העלאה</string>
+ <string name="alert_error_adding_media">אירעה שגיאה בעת הוספת מדיה</string>
+ <string name="alert_action_while_uploading">מתבצעת כרגע העלאת מדיה על-ידך. יש להמתין להשלמת הפעולה.</string>
+ <string name="alert_insert_image_html_mode">לא ניתן להוסיף מדיה ישירות במצב HTML. יש לחזור למצב ויזואלי.</string>
+ <string name="uploading_gallery_placeholder">מעלה גלריה…</string>
+ <string name="invite_error_some_failed">ההזמנה נשלחה אך אירעו שגיאות!</string>
+ <string name="invite_sent">ההזמנה נשלחה בהצלחה</string>
+ <string name="tap_to_try_again">יש להקיש כדי לנסות שוב!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">אירעה שגיאה בעת הניסיון לשלוח את ההזמנה!</string>
+ <string name="invite_error_invalid_usernames_multiple">לא ניתן לשלוח: יש שמות משתמש או כתובות אימייל לא תקפים</string>
+ <string name="invite_error_invalid_usernames_one">לא ניתן לשלוח: שם משתמש או אימייל לא תקפים</string>
+ <string name="invite_error_no_usernames">יש להוסיף שם משתמש אחד לפחות</string>
+ <string name="invite_message_info">(אופציונלי) אפשר להזין הודעה מותאמת אישית של עד 500 תווים שתיכלל בהזמנה למשתמשים.</string>
+ <string name="invite_message_remaining_other">נותרו %d תווים</string>
+ <string name="invite_message_remaining_one">תו אחד נותר</string>
+ <string name="invite_message_remaining_zero">לא נותרו תווים</string>
+ <string name="invite_invalid_email">כתובת האימייל \'%s\' לא תקפה</string>
+ <string name="invite_message_title">הודעה מותאמת אישית</string>
+ <string name="invite_already_a_member">כבר יש חבר בעל שם המשתמש \'%s\'</string>
+ <string name="invite_username_not_found">לא נמצא משתמש לשם המשתמש \'%s\'</string>
+ <string name="invite">הזמנה</string>
+ <string name="invite_names_title">שמות משתמש או אימיילים</string>
+ <string name="my_site_header_external">חיצוני</string>
+ <string name="send_link">שלח קישור</string>
+ <string name="signup_succeed_signin_failed">החשבון שלך נוצר, אך אירעה שגיאה כשהכנסנו אותך\n . יש לנסות להיכנס עם שם המשתמש והסיסמה החדשים שיצרת.</string>
+ <string name="invite_people">הזמנת אנשים</string>
+ <string name="label_clear_search_history">ניקוי היסטוריית חיפוש</string>
+ <string name="dlg_confirm_clear_search_history">לנקות היסטוריית חיפוש?</string>
+ <string name="reader_empty_posts_in_search_description">לא נמצאו פוסטים עבור %s בשפה שבחרת</string>
+ <string name="reader_label_post_search_running">מחפש...</string>
+ <string name="reader_empty_posts_in_search_title">לא נמצאו תוצאות</string>
+ <string name="reader_label_related_posts">חומרי קריאה קשורים</string>
+ <string name="reader_label_post_search_explainer">חיפוש בכל הבלוגים הציבוריים של WordPress.com</string>
+ <string name="reader_hint_post_search">חיפוש ב-WordPress.com</string>
+ <string name="reader_title_related_post_detail">באותו נושא</string>
+ <string name="reader_title_search_results">חיפוש %s</string>
+ <string name="preview_screen_links_disabled">קישורים מושבתים במסך התצוגה המקדימה</string>
+ <string name="draft_explainer">פוסט זה הוא טיוטה שלא פורסמה</string>
+ <string name="send">שלח</string>
+ <string name="user_remove_confirmation_message">לאחר הסרת %1$s, משתמש זה לא יקבל יותר גישה לאתר זה אך כל התוכן שנוצר על ידי %1$s יישאר באתר.\n\nבחרת להסיר משתמש זה - האם ההחלטה סופית?</string>
+ <string name="person_removed">הסרת בהצלחה את @%1$s</string>
+ <string name="person_remove_confirmation_title">הסרת %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">האתרים ברשימה זו לא פרסמו דבר בזמן האחרון</string>
+ <string name="people">אנשים</string>
+ <string name="edit_user">עריכת פרטי משתמש</string>
+ <string name="role">תפקיד</string>
+ <string name="error_remove_user">לא ניתן היה להסיר משתמש</string>
+ <string name="error_fetch_viewers_list">לא הצלחנו לאחזר את משתמשי האתר</string>
+ <string name="error_update_role">לא הצלחנו לעדכן תפקיד משתמש</string>
+ <string name="gravatar_camera_and_media_permission_required">דרושות הרשאות לבחירה או לכידה של תמונה</string>
+ <string name="error_updating_gravatar">שגיאה בעדכון ה-gravatar שלך</string>
+ <string name="error_locating_image">שגיאה באיתור התמונה החתוכה</string>
+ <string name="error_refreshing_gravatar">שגיאה בטעינה מחדש של ה-gravatar שלך</string>
+ <string name="gravatar_tip">חדש! יש להקיש על ה-gravatar כדי לשנות אותו!</string>
+ <string name="error_cropping_image">שגיאה בחיתוך בתמונה</string>
+ <string name="launch_your_email_app">הפעלת אפליקציית האימייל</string>
+ <string name="checking_email">בודק אימייל</string>
+ <string name="not_on_wordpress_com">אינך ב-WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">לא זמין כרגע. יש להזין סיסמה</string>
+ <string name="check_your_email">יש לבדוק אימייל</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">קבלת קישור להתחברות מיידית באימייל</string>
+ <string name="logging_in">נכנס</string>
+ <string name="enter_your_password_instead">במקום זאת יש להזין סיסמה</string>
+ <string name="web_address_dialog_hint">תוצג באופן ציבורי בעת פרסום תגובות שלך.</string>
+ <string name="jetpack_not_connected_message">תוסף Jetpack מותקן אבל לא מחובר ל-WordPress.com. האם ברצונך לחבר את Jetpack?</string>
+ <string name="username_email">אימייל או שם משתמש</string>
+ <string name="jetpack_not_connected">תוסף Jetpack לא מחובר</string>
+ <string name="new_editor_reflection_error">עורך ויזואלי לא תואם למכשיר שלך. הוא\n הושבת אוטומטית.</string>
+ <string name="stats_insights_latest_post_no_title">(חסרה כותרת)</string>
+ <string name="capture_or_pick_photo">בחירה או לכידה של תמונה</string>
+ <string name="plans_post_purchase_text_themes">כעת יש לך גישה בלתי מוגלת לערכות פרימיום. בתור התחלה, אפשר להציג כל אחת מהן בתצוגה מקדימה.</string>
+ <string name="plans_post_purchase_button_themes">חיפוש ערכות עיצוב</string>
+ <string name="plans_post_purchase_title_themes">מציאת ערכת פרימיום מושלמת</string>
+ <string name="plans_post_purchase_button_video">יצירת פוסט חדש</string>
+ <string name="plans_post_purchase_text_video">אפשר להעלות ולאחסן סרטוני וידאו באתר שלך דרך VideoPress ואחסון המדיה המורחב שלך.</string>
+ <string name="plans_post_purchase_title_video">סרטונים שיפיחו חיים בפוסטים</string>
+ <string name="plans_post_purchase_button_customize">התאמה אישית של האתר</string>
+ <string name="plans_post_purchase_text_customize">כעת יש לך גישה לגופנים מותאמים אישית, צבעים מותאמים אישית יכולות עריכה של CSS מותאם אישית.</string>
+ <string name="plans_post_purchase_text_intro">האתר שלך עושה סלטות מרוב התרגשות! זה הזמן לגלות את התכונות החדשות של האתר שלך ולבחור במה להתחיל.</string>
+ <string name="plans_post_purchase_title_customize">התאמה אישית של גופנים וצבעים</string>
+ <string name="plans_post_purchase_title_intro">הכול שלך, כל הכבוד!</string>
+ <string name="export_your_content_message">הפוסטים, העמודים וההגדרות שלך יישלחו באימייל לכתובת %s.</string>
+ <string name="plan">תוכנית</string>
+ <string name="plans">תוכניות</string>
+ <string name="plans_loading_error">לא ניתן לטעון תוכניות</string>
+ <string name="export_your_content">יצוא התוכן שלך</string>
+ <string name="exporting_content_progress">מייצא תוכן…</string>
+ <string name="export_email_sent">אימייל יצוא נשלח!</string>
+ <string name="premium_upgrades_message">קיימים שדרוגים פעילים באתר שלך. יש לבטל את השדרוגים לפני מחיקת האתר.</string>
+ <string name="show_purchases">הצגת רכישות</string>
+ <string name="checking_purchases">בודק רכישות</string>
+ <string name="premium_upgrades_title">שדרוגים</string>
+ <string name="purchases_request_error">משהו השתבש. לא ניתן לבקש רכישות.</string>
+ <string name="delete_site_progress">מוחק אתר…</string>
+ <string name="delete_site_summary">לא ניתן לבטל פעולה זו. מחיקת האתר שלך תסיר את כל התוכן, המשתתפים והדומיינים מהאתר.</string>
+ <string name="delete_site_hint">מחיקת אתר</string>
+ <string name="export_site_hint">יצוא האתר שלך לקובץ XML</string>
+ <string name="are_you_sure">ההחלטה הסופית?</string>
+ <string name="export_site_summary">אם ההחלטה שלך סופית, זה הזמן לייצא את התוכן שלך. לא ניתן יהיה לשחזר אותו בעתיד.</string>
+ <string name="keep_your_content">שמירת התוכן שלך</string>
+ <string name="domain_removal_hint">הדומיינים לא יעבדו עוד לאחר הסרת האתר</string>
+ <string name="domain_removal_summary">זהירות! מחיקת האתר שלך תסיר גם את הדומיינים להלן.</string>
+ <string name="primary_domain">דומיין ראשי</string>
+ <string name="domain_removal">הסרת דומיין</string>
+ <string name="error_deleting_site_summary">אירעה שגיאה בעת מחיקת האתר שלך. כדאי לפנות לתמיכה לקבלת סיוע נוסף</string>
+ <string name="error_deleting_site">שגיאה בעת מחיקת האתר</string>
+ <string name="confirm_delete_site_prompt">יש להקליד את הכתובת %1$s בשדה להלן כדי לאשר. לאחר מכן, האתר שלך ייעלם לעד.</string>
+ <string name="site_settings_export_content_title">יצוא תוכן</string>
+ <string name="contact_support">כדאי לפנות לתמיכה</string>
+ <string name="confirm_delete_site">אישור מחיקת אתר</string>
+ <string name="start_over_text">אם ברצונך להשתמש באתר בלי הפוסטים והעמודים שהוא מכיל, צוות התמיכה שלנו יכול למחוק את הפוסטים, העמודים, המדיה והתגובות עבורך.\n\nכך האתר וכתובת ה-URL שלך יישארו פעילים אבל תהיה לך אפשרות להתחיל ליצור תוכן מחדש. נותר לך רק לפנות אלינו כדי למחוק את התוכן הקיים.</string>
+ <string name="site_settings_start_over_hint">הקמת האתר מחדש</string>
+ <string name="let_us_help">נשמח לעזור</string>
+ <string name="me_btn_app_settings">הגדרות יישום</string>
+ <string name="start_over">להתחיל מחדש</string>
+ <string name="editor_remove_failed_uploads">הסרת העלאות שנכשלו</string>
+ <string name="editor_toast_failed_uploads">מספר העלאות מדיה נכשלו. לא ניתן לשמור או לפרסם\n הפוסט שלך במצב זה. האם ברצונך להסיר את כל המדיה שלא הועלתה?</string>
+ <string name="comments_empty_list_filtered_trashed">אין תגובות שהועברו לפח</string>
+ <string name="site_settings_advanced_header">אפשרויות מתקדמות</string>
+ <string name="comments_empty_list_filtered_pending">אין תגובות שממתינות לאישור</string>
+ <string name="comments_empty_list_filtered_approved">אין תגובות מאושרות</string>
+ <string name="button_done">בוצע</string>
+ <string name="button_skip">דילוג</string>
+ <string name="site_timeout_error">לא ניתן להתחבר ל-WordPress עקב שגיאה - תם הזמן הקצוב.</string>
+ <string name="xmlrpc_malformed_response_error">לא ניתן להתחבר. התקנת WordPress הגיבה עם מסמך XML-RPC לא תקף.</string>
+ <string name="xmlrpc_missing_method_error">לא ניתן להתחבר. שיטות XML-RPC נדרשות חסרות בשרת.</string>
+ <string name="post_format_video">וידאו</string>
+ <string name="theme_free">חינם</string>
+ <string name="theme_premium">פרימיום</string>
+ <string name="post_format_status">מצב</string>
+ <string name="theme_all">הכול</string>
+ <string name="post_format_chat">צ\'אט</string>
+ <string name="post_format_image">תמונה</string>
+ <string name="post_format_link">קישור</string>
+ <string name="post_format_quote">ציטוט</string>
+ <string name="post_format_gallery">גלריה</string>
+ <string name="post_format_standard">פוסט רגיל</string>
+ <string name="post_format_audio">אודיו</string>
+ <string name="notif_events">מידע על קורסים ואירועים ב-WordPress.com (ברשת ופרונטלי).</string>
+ <string name="post_format_aside">קצרצר</string>
+ <string name="notif_surveys">הזדמנויות להשתתף במחקרים וסקרים של WordPress.com.</string>
+ <string name="notif_tips">טיפים כדי להפיק את המרב מ-WordPress.com.</string>
+ <string name="notif_community">קהילה</string>
+ <string name="notif_suggestions">הצעות</string>
+ <string name="replies_to_my_comments">מענה לתגובות שלי</string>
+ <string name="notif_research">מחקר</string>
+ <string name="site_achievements">הישגי אתר</string>
+ <string name="username_mentions">אזכורים של שם משתמש</string>
+ <string name="likes_on_my_posts">לייקים בפוסטים שלי</string>
+ <string name="site_follows">עוקבים אחרי האתר</string>
+ <string name="likes_on_my_comments">לייקים בתגובות שלי</string>
+ <string name="comments_on_my_site">תגובות באתר שלי</string>
+ <string name="site_settings_list_editor_summary_other">%d פריטים</string>
+ <string name="site_settings_list_editor_summary_one">פריט אחד</string>
+ <string name="approve_auto">כל המשתמשים</string>
+ <string name="approve_auto_if_previously_approved">תגובות של משתמשים מוכרים</string>
+ <string name="approve_manual">אין תגובות</string>
+ <string name="site_settings_paging_summary_other">%d תגובות לעמוד</string>
+ <string name="site_settings_paging_summary_one">תגובה אחת לעמוד</string>
+ <string name="site_settings_multiple_links_summary_other">נדרש אישור עבור יותר מ-%d קישורים</string>
+ <string name="site_settings_multiple_links_summary_one">נדרש אישור עבור יותר מקישור אחד</string>
+ <string name="site_settings_multiple_links_summary_zero">נדרש אישור עבור יותר מ-0 קישורים</string>
+ <string name="detail_approve_auto">אישור אוטומטי של תגובות מכולם.</string>
+ <string name="detail_approve_auto_if_previously_approved">אישור אוטומטי אם תגובה קודמת של המשתמש אושרה</string>
+ <string name="detail_approve_manual">אישור ידני דרוש לתגובות מכולם.</string>
+ <string name="days_quantity_one">יום אחד</string>
+ <string name="days_quantity_other">%d ימים</string>
+ <string name="filter_trashed_posts">הועבר לפח</string>
+ <string name="filter_published_posts">פורסם</string>
+ <string name="filter_draft_posts">טיוטות</string>
+ <string name="filter_scheduled_posts">מתוזמן לפרסום</string>
+ <string name="primary_site">אתר ראשי</string>
+ <string name="pending_email_change_snackbar">יש ללחוץ על הקישור לאימות באימייל שנשלח לכתובת %1$s כדי לאשר את כתובתך החדשה</string>
+ <string name="web_address">כתובת URL</string>
+ <string name="editor_toast_uploading_please_wait">מתבצעת כרגע העלאת מדיה על-ידך. יש להמתין להשלמת הפעולה.</string>
+ <string name="error_refresh_comments_showing_older">לא ניתן לרענן תגובות כרגע - מציג תגובות ישנות</string>
+ <string name="editor_post_settings_set_featured_image">קביעת תמונה מרכזית</string>
+ <string name="editor_post_settings_featured_image">תמונה מרכזית</string>
+ <string name="new_editor_promo_desc">אפליקציית WordPress עבור Android כוללת כעת עורך ויזואלי חדש\n ויפהפה. ניתן להתנסות בו על ידי יצירת פוסט חדש.</string>
+ <string name="new_editor_promo_title">עורך חדש ומחודש</string>
+ <string name="new_editor_promo_button_label">נהדר, תודה!</string>
+ <string name="visual_editor_enabled">עורך ויזואלי מופעל</string>
+ <string name="editor_content_placeholder">מומלץ לשתף את הסיפור שלך כאן…</string>
+ <string name="editor_page_title_placeholder">כותרת עמוד</string>
+ <string name="editor_post_title_placeholder">כותרת הפוסט</string>
+ <string name="email_address">כתובת אימייל</string>
+ <string name="preference_show_visual_editor">הצגת עורך ויזואלי</string>
+ <string name="preference_editor">עורך</string>
+ <string name="dlg_sure_to_delete_comments">האם למחוק תגובות אלה לצמיתות?</string>
+ <string name="dlg_sure_to_delete_comment">האם למחוק תגובה זו לצמיתות?</string>
+ <string name="mnu_comment_delete_permanently">מחיקה</string>
+ <string name="comment_deleted_permanently">התגובה נמחקה</string>
+ <string name="mnu_comment_untrash">שחזור</string>
+ <string name="comments_empty_list_filtered_spam">אין תגובות זבל</string>
+ <string name="could_not_load_page">לא ניתן היה לטעון את העמוד</string>
+ <string name="comment_status_all">הכול</string>
+ <string name="interface_language">שפת ממשק</string>
+ <string name="off">כיבוי</string>
+ <string name="about_the_app">מידע על האפליקציה</string>
+ <string name="error_post_account_settings">לא ניתן לשמור הגדרות החשבון</string>
+ <string name="error_post_my_profile">הפרופיל לא נשמר</string>
+ <string name="error_fetch_account_settings">לא ניתן לאחזר את הגדרות החשבון שלך</string>
+ <string name="error_fetch_my_profile">לא ניתן לאחזר את הפרופיל שלך</string>
+ <string name="stats_widget_promo_ok_btn_label">אוקי, הבנתי</string>
+ <string name="stats_widget_promo_desc">להוסיף את הווידג׳ט למסך הבית כדי להגיע לסטטיסטיקות בקליק אחד.</string>
+ <string name="stats_widget_promo_title">וידג׳ט סטטיסטיקות למסך הבית</string>
+ <string name="site_settings_unknown_language_code_error">קוד שפה לא מזוהה</string>
+ <string name="site_settings_threading_dialog_description">אפשר תגובות משורשרות</string>
+ <string name="site_settings_threading_dialog_header">שרשור עד עומק של</string>
+ <string name="search">לחפש</string>
+ <string name="add_category">הוספת קטגוריה</string>
+ <string name="remove">להסיר</string>
+ <string name="disabled">לא מופעל</string>
+ <string name="site_settings_image_original_size">גודל מקורי</string>
+ <string name="privacy_private">האתר גלוי רק לך ולמשתמשים שאישרת</string>
+ <string name="privacy_public_not_indexed">האתר שלך גלוי לכולם אבל שולח בקשה למנועי חיפוש לא לצרף אותו לאינדקס</string>
+ <string name="privacy_public">האתר שלך גלוי לכולם ומנועי חיפוש יכולים לצרף אותו לאינדקס</string>
+ <string name="about_me_hint">תיאור אישי בכמה מילים…</string>
+ <string name="public_display_name_hint">אם לא יוגדר אחרת, שם המשתמש שלך ישמש כשם תצוגה בברירת מחדל</string>
+ <string name="about_me">מי אני</string>
+ <string name="public_display_name">שם תצוגה ציבורי</string>
+ <string name="my_profile">הפרופיל שלי</string>
+ <string name="first_name">שם פרטי</string>
+ <string name="last_name">שם משפחה</string>
+ <string name="site_privacy_public_desc">לאפשר למנועי חיפוש לצרף את האתר לאינדקס</string>
+ <string name="site_privacy_hidden_desc">למנוע ממנועי חיפוש לצרף את האתר לאינדקס</string>
+ <string name="site_privacy_private_desc">אני רוצה שהאתר שלי יהיה פרטי וגלוי רק למשתמשים שאבחר</string>
+ <string name="cd_related_post_preview_image">תמונת תצוגה מקדימה של פוסט קשור</string>
+ <string name="error_post_remote_site_settings">לא הצלחנו לשמור את פרטי האתר</string>
+ <string name="error_fetch_remote_site_settings">לא הצלחנו לאחזר את פרטי האתר</string>
+ <string name="error_media_upload_connection">ארעה שגיאת חיבור במהלך העלאת המדיה</string>
+ <string name="site_settings_disconnected_toast">מנותק, עריכה מושבתת.</string>
+ <string name="site_settings_unsupported_version_error">גרסת WordPress לא נתמכת</string>
+ <string name="site_settings_multiple_links_dialog_description">דרוש אישור לתגובות שכוללות כמות קישורים גבוהה מזו.</string>
+ <string name="site_settings_close_after_dialog_switch_text">סגירה אוטומטית</string>
+ <string name="site_settings_close_after_dialog_description">סגירת תגובות אוטומטית במאמרים.</string>
+ <string name="site_settings_paging_dialog_description">חלוקת שרשורי תגובות למספר עמודים.</string>
+ <string name="site_settings_paging_dialog_header">תגובות לעמוד</string>
+ <string name="site_settings_close_after_dialog_title">סגור לתגובות</string>
+ <string name="site_settings_blacklist_description">תגובה שתכיל מילה מהמילים הללו בתוכן שלה, בשם, בכתובת, באימייל או ב-IP תסומן כתגובת זבל. אפשר להשתמש גם בחלקי מילים, כך ש-"press" תתאים למילה "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">תגובה שתכיל מילה מהמילים הללו בתוכן שלה, בשם, בכתובת האתר, באימייל או ב-IP תישמר בתור המנהל. אפשר להשתמש גם בחלקי מילים, כך ש-"press" תתאים למילה "WordPress".</string>
+ <string name="site_settings_list_editor_input_hint">הזנת מילה או ביטוי</string>
+ <string name="site_settings_list_editor_no_items_text">אין פריטים</string>
+ <string name="site_settings_learn_more_caption">אפשר לעקוף הגדרות אלה בפוסטים נפרדים.</string>
+ <string name="site_settings_rp_preview3_site">תחת \'שדרוג\'</string>
+ <string name="site_settings_rp_preview3_title">המלצת שדרוג: VideoPress לחתונות</string>
+ <string name="site_settings_rp_preview2_site">תחת \'אפליקציות\'</string>
+ <string name="site_settings_rp_preview2_title">אפליקציית WordPress ל-Android עברה \'מתיחת פנים\' רצינית</string>
+ <string name="site_settings_rp_preview1_site">תחת \'נייד\'</string>
+ <string name="site_settings_rp_preview1_title">עדכון גדול ל-iPhone/‏iPad זמין עכשיו</string>
+ <string name="site_settings_rp_show_images_title">הצגת תמונות</string>
+ <string name="site_settings_rp_show_header_title">הצגת כותרת</string>
+ <string name="site_settings_rp_switch_summary">פוסטים קשורים מציגים תוכן רלוונטי מהאתר שלך, מתחת לפוסטים שלך.</string>
+ <string name="site_settings_rp_switch_title">הצג תכנים באותו נושא</string>
+ <string name="site_settings_delete_site_hint">מסיר את נתוני האתר שלך מהאפליקציה</string>
+ <string name="site_settings_blacklist_hint">תגובות שתואמות למסנן מסומנות כתגובות זבל</string>
+ <string name="site_settings_moderation_hold_hint">תגובות שתואמות למסנן עוברות לתור לאישור</string>
+ <string name="site_settings_multiple_links_hint">מתעלם ממגבלת קישורים אצל משתמשים מוכרים</string>
+ <string name="site_settings_whitelist_hint">המגיב הוא מישהו שתגובתו אושרה בעבר</string>
+ <string name="site_settings_user_account_required_hint">כדי להגיב יש להירשם לאתר ולהתחבר למערכת</string>
+ <string name="site_settings_identity_required_hint">המגיבים חייבים לכתוב את שמם ואת כתובת האימייל שלהם</string>
+ <string name="site_settings_manual_approval_hint">תגובות חייבות לעבור אישור ידני</string>
+ <string name="site_settings_paging_hint">הצגת כמות מוגדרת של תגובות</string>
+ <string name="site_settings_threading_hint">מאפשר שרשור תגובות עד עומק מסוים</string>
+ <string name="site_settings_sort_by_hint">קובע את סדר הצגת התגובות</string>
+ <string name="site_settings_close_after_hint">לא מאפשר הוספת תגובות אחרי פרק זמן מוגדר</string>
+ <string name="site_settings_receive_pingbacks_hint">מאפשר קבלת הודעות עם קישורים מבלוגים אחרים</string>
+ <string name="site_settings_send_pingbacks_hint">מנסה לשלוח הודעה לכל בלוג שהמאמר מקשר אליו</string>
+ <string name="site_settings_allow_comments_hint">מאפשר לקוראים לפרסם תגובות</string>
+ <string name="site_settings_discussion_hint">הצגה ושינוי של הגדרות הדיון באתרים שלך</string>
+ <string name="site_settings_more_hint">הצגת כל הגדרות הדיון הזמינות</string>
+ <string name="site_settings_related_posts_hint">הצגה/הסתרה של פוסטים קשורים ב-Reader</string>
+ <string name="site_settings_upload_and_link_image_hint">מאפשר תמיד להעלות תמונה בגודל מלא</string>
+ <string name="site_settings_image_width_hint">שינוי גודל של תמונות בפוסטים לרוחב זה</string>
+ <string name="site_settings_format_hint">הגדרת סוג פוסט חדש</string>
+ <string name="site_settings_category_hint">הגדרת קטגוריית פוסט חדשה</string>
+ <string name="site_settings_location_hint">הוספה אוטומטית של נתוני מיקום לפוסטים שלך</string>
+ <string name="site_settings_password_hint">שינוי סיסמה</string>
+ <string name="site_settings_username_hint">חשבון משתמש נוכחי</string>
+ <string name="site_settings_language_hint">השפה עיקרית בה כתוב בלוג זה</string>
+ <string name="site_settings_privacy_hint">מגדיר מי יכול לראות את האתר שלך</string>
+ <string name="site_settings_address_hint">האפשרות לשינוי כתובת לא נתמכת כרגע</string>
+ <string name="site_settings_tagline_hint">תיאור קצר או ביטוי קולע שמתארים את הבלוג שלך</string>
+ <string name="site_settings_title_hint">במספר מילים, הסבר במה עוסק האתר</string>
+ <string name="site_settings_whitelist_known_summary">תגובות ממשתמשים מוכרים</string>
+ <string name="site_settings_whitelist_all_summary">תגובות מכל המשתמשים</string>
+ <string name="site_settings_threading_summary">%d רמות</string>
+ <string name="site_settings_privacy_private_summary">פרטי</string>
+ <string name="site_settings_privacy_hidden_summary">מוסתר</string>
+ <string name="site_settings_delete_site_title">מחיקת אתר</string>
+ <string name="site_settings_privacy_public_summary">ציבורי</string>
+ <string name="site_settings_blacklist_title">רשימה שחורה</string>
+ <string name="site_settings_moderation_hold_title">המתנה לניהול</string>
+ <string name="site_settings_multiple_links_title">קישורים בתגובות</string>
+ <string name="site_settings_whitelist_title">אישור אוטומטי</string>
+ <string name="site_settings_threading_title">שרשור</string>
+ <string name="site_settings_paging_title">עימוד</string>
+ <string name="site_settings_sort_by_title">מיון לפי</string>
+ <string name="site_settings_account_required_title">משתמשים חייבים להתחבר</string>
+ <string name="site_settings_identity_required_title">חובה לציין שם ואימייל</string>
+ <string name="site_settings_receive_pingbacks_title">קבלת פינגבאקים</string>
+ <string name="site_settings_send_pingbacks_title">שליחת פינגבאקים</string>
+ <string name="site_settings_allow_comments_title">אפשר תגובות</string>
+ <string name="site_settings_default_format_title">סוג ברירת מחדל</string>
+ <string name="site_settings_default_category_title">קטגוריית ברירת מחדל</string>
+ <string name="site_settings_location_title">אפשר מיקום</string>
+ <string name="site_settings_address_title">כתובת</string>
+ <string name="site_settings_title_title">שם האתר</string>
+ <string name="site_settings_tagline_title">תיאור האתר</string>
+ <string name="site_settings_this_device_header">מכשיר זה</string>
+ <string name="site_settings_discussion_new_posts_header">ברירות מחדל עבור פוסטים חדשים</string>
+ <string name="site_settings_account_header">חשבון</string>
+ <string name="site_settings_writing_header">כתיבה</string>
+ <string name="site_settings_general_header">כללי</string>
+ <string name="newest_first">חדשים תחילה</string>
+ <string name="discussion">דיון</string>
+ <string name="privacy">פרטיות</string>
+ <string name="comments">תגובות</string>
+ <string name="close_after">סגור אחרי</string>
+ <string name="related_posts">באותו נושא</string>
+ <string name="oldest_first">ישנים תחילה</string>
+ <string name="media_error_no_permission_upload">אין לך הרשאה להעלות מדיה לאתר</string>
+ <string name="never">לעולם לא</string>
+ <string name="unknown">לא ידוע</string>
+ <string name="reader_err_get_post_not_found">הפוסט כבר לא קיים</string>
+ <string name="reader_err_get_post_not_authorized">אינך רשאי לראות פוסט זה</string>
+ <string name="reader_err_get_post_generic">לא ניתן לאחזר פוסט זה</string>
+ <string name="blog_name_no_spaced_allowed">כתובת האתר לא יכולה להכיל רווחים</string>
+ <string name="invalid_username_no_spaces">שם המשתמש לא יכול להכיל רווחים</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">האתרים אחריהם אתה עוקב לא פרסמו דבר לאחרונה</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">אין פוסטים אחרונים</string>
+ <string name="edit_media">עריכת מדיה</string>
+ <string name="media_details_copy_url_toast">הכתובת הועתקה ללוח העריכה</string>
+ <string name="media_details_copy_url">העתק כתובת</string>
+ <string name="media_details_label_date_uploaded">הועלה</string>
+ <string name="media_details_label_date_added">התווסף</string>
+ <string name="selected_theme">תבנית נבחרת</string>
+ <string name="could_not_load_theme">לא ניתן לטעון תבנית</string>
+ <string name="theme_activation_error">משהו השתבש. לאניתן להפעיל תבנית</string>
+ <string name="theme_by_author_prompt_append">מאת %1$s</string>
+ <string name="theme_prompt">תודה שבחרת %1$s</string>
+ <string name="theme_view">הצג</string>
+ <string name="theme_details">פרטים</string>
+ <string name="theme_support">תמיכה</string>
+ <string name="theme_done">הושלם</string>
+ <string name="theme_manage_site">ניהול אתר</string>
+ <string name="theme_try_and_customize">נסה והתאם אישית</string>
+ <string name="title_activity_theme_support">תבניות</string>
+ <string name="theme_activate">הפעלה</string>
+ <string name="date_range_start_date">תאריך התחלה</string>
+ <string name="date_range_end_date">תאריך סיום</string>
+ <string name="current_theme">תבנית נוכחית</string>
+ <string name="details">פרטים</string>
+ <string name="support">תמיכה</string>
+ <string name="customize">התאמה אישית</string>
+ <string name="active">פעיל</string>
+ <string name="stats_referrers_spam_generic_error">משהו השתבש במהלך הפעולה. הסטטוס של הספאם לא השתנה.</string>
+ <string name="stats_referrers_marking_not_spam">מסמן כלא זבל</string>
+ <string name="stats_referrers_unspam">לא ספאם</string>
+ <string name="stats_referrers_marking_spam">מסמן כזבל</string>
+ <string name="stats_referrers_spam">ספאם</string>
+ <string name="post_published">פוסט פורסם</string>
+ <string name="page_published">עמוד פורסם</string>
+ <string name="post_updated">פוסט עודכן</string>
+ <string name="page_updated">עמוד עודכן</string>
+ <string name="theme_auth_error_authenticate">נכשל בניסיון להביא תבניות: נכשל אימות משתמש</string>
+ <string name="theme_no_search_result_found">לא נמצאו תבניות.</string>
+ <string name="media_file_name">שם קובץ: %s</string>
+ <string name="media_uploaded_on">תאריך העלאה: %s</string>
+ <string name="media_dimensions">מימדים: %s</string>
+ <string name="media_file_type">סוג קובץ: %s</string>
+ <string name="upload_queued">בתור</string>
+ <string name="reader_label_gap_marker">פוסטים נוספים</string>
+ <string name="notifications_no_search_results">אף אתר אינו תואם \'%s\'</string>
+ <string name="search_sites">חיפוש אתרים</string>
+ <string name="unread">לא נקרא</string>
+ <string name="notifications_empty_view_reader">הצד קורא</string>
+ <string name="notifications_empty_action_followers_likes">קבל תשומת לב: תגובה לפוסטים שכבר קראת.</string>
+ <string name="notifications_empty_action_comments">הצטרף לדיון: תגובות לפוסטים באתרים אחריהם אתה עוקב.</string>
+ <string name="notifications_empty_action_unread">הצת מחדש את הדיון: כתוב פוסט חדש.</string>
+ <string name="notifications_empty_action_all">תהיה פעיל! תגיב לפוסטים באתרים אחריהם אתה עוקב.</string>
+ <string name="notifications_empty_likes">אין לייקים חדשים... עדיין.</string>
+ <string name="notifications_empty_followers">אין עוקבים חדשים... עדיין.</string>
+ <string name="notifications_empty_comments">אין תגובות חדשות... עדיין.</string>
+ <string name="notifications_empty_unread">כולכם נתפסתם!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">נא לגשת לסטטיסטיקות באפליקציה ולהוסיף וידג\'ט</string>
+ <string name="stats_widget_error_readd_widget">יש להסיר את הוידג\'ט ולהוסיף אותו שוב</string>
+ <string name="stats_widget_error_no_visible_blog">לא ניתן לגשת לסטטיסטיקות ללא אתר גלוי</string>
+ <string name="stats_widget_error_no_permissions">לא ניתן לגשת לסטטיסטיקות באתר זה באמצעות חשבון וורדפרס.קום זה</string>
+ <string name="stats_widget_error_no_account">נא להתחברת לוורדפרס</string>
+ <string name="stats_widget_error_generic">לא ניתן לטעון סטטיסטיקות</string>
+ <string name="stats_widget_loading_data">טוען נתונים...</string>
+ <string name="stats_widget_name_for_blog">סטטיסטיקות יומיות עבור %1$s</string>
+ <string name="stats_widget_name">סטטיסטיקות וורדפרס יומיות</string>
+ <string name="add_location_permission_required">נדרשות הרשאות מתאימות כדי להוסיף מיקומים</string>
+ <string name="add_media_permission_required">ההרשאות הדרושות כדי להוסיף מדיה</string>
+ <string name="access_media_permission_required">ההרשאות הדרושות כדי לגשת למדיה</string>
+ <string name="stats_enable_rest_api_in_jetpack">להצגת סטטיסטיקות, הפעילו מודול JSON API בג\'טפאק.</string>
+ <string name="error_open_list_from_notification">פוסט או עמוד זה פורסם באתר אחר</string>
+ <string name="reader_short_comment_count_multi">%s תגובות</string>
+ <string name="reader_short_comment_count_one">תגובה אחת</string>
+ <string name="reader_label_submit_comment">שלח</string>
+ <string name="reader_hint_comment_on_post">מענה לפוסט...</string>
+ <string name="reader_discover_visit_blog">בקר ב%s</string>
+ <string name="reader_discover_attribution_blog">פורסם במקור ב-%s</string>
+ <string name="reader_discover_attribution_author">פורסם במקור על ידי %s</string>
+ <string name="reader_discover_attribution_author_and_blog">פורסם במקור על ידי %1$s ב-%2$s</string>
+ <string name="reader_short_like_count_multi">%s לייקים</string>
+ <string name="reader_short_like_count_one">לייק אחד</string>
+ <string name="reader_short_like_count_none">לייק</string>
+ <string name="reader_label_follow_count">%,d עוקבים</string>
+ <string name="reader_menu_tags">עריכת תגיות ובלוגים</string>
+ <string name="reader_title_post_detail">קורא פוטס</string>
+ <string name="local_draft_explainer">לפוסט זה יש טיוטה מקומית שלא פורסמה</string>
+ <string name="local_changes_explainer">לפוסט זה יש שינויים מקומיים שטרם פורסמו</string>
+ <string name="notifications_push_summary">הגדרת התראות יוצגו במכשיר שלך. </string>
+ <string name="notifications_email_summary">הגדרת התראות שישלחו לאימייל של בעל האתר.</string>
+ <string name="notifications_tab_summary">הגדרת התראות יוצגו בטאב ההתראות.</string>
+ <string name="notifications_disabled">התראות אפליקציה הוסרו. ליחצו כאן כדי להפעיל שוב בהגדרות.</string>
+ <string name="notification_types">סוגי התראות</string>
+ <string name="error_loading_notifications">לא ניתן לטעון הגדרות של התראות</string>
+ <string name="replies_to_your_comments">מענה לתגובות שלך</string>
+ <string name="comment_likes">לייקים לתגובות</string>
+ <string name="email">אימייל</string>
+ <string name="notifications_tab">טאב התראות</string>
+ <string name="app_notifications">התראות אפליקציה</string>
+ <string name="notifications_comments_other_blogs">תגובות באתרים אחרים</string>
+ <string name="notifications_other">אחר</string>
+ <string name="notifications_wpcom_updates">עדכוני וורדפרס.קום</string>
+ <string name="notifications_account_emails">עדכונים מוורדפרס.קום</string>
+ <string name="notifications_account_emails_summary">תמיד נשלח מיילים חשובים הקשורים לאתר, אבל לפעמים תקבלו גם תוספות מועילות.</string>
+ <string name="your_sites">אתרים שלך</string>
+ <string name="notifications_sights_and_sounds">מראות וצלילים</string>
+ <string name="stats_insights_latest_post_trend">עברו %1$s מאז פרסום %2$s. קצת מידע על תפקוד הפוסט עד כה...</string>
+ <string name="stats_insights_latest_post_summary">סיכום פוסטים אחרונים</string>
+ <string name="button_revert">שיחזור</string>
+ <string name="days_ago">לפני %d ימים</string>
+ <string name="yesterday">אתמול</string>
+ <string name="connectionbar_no_connection">אין חיבור</string>
+ <string name="page_trashed">העמוד הועבר לפח</string>
+ <string name="post_deleted">הפוסט נמחק</string>
+ <string name="post_trashed">הפוסט הועבר לפח</string>
+ <string name="stats_no_activity_this_period">אין פעילות בפרק זמן זה</string>
+ <string name="trashed">הועבר לפח</string>
+ <string name="button_back">חזרה</string>
+ <string name="page_deleted">העמוד נמחק</string>
+ <string name="button_stats">נתונים סטטיסטיים</string>
+ <string name="button_trash">פח</string>
+ <string name="button_preview">תצוגה מקדימה</string>
+ <string name="button_view">הצגה</string>
+ <string name="button_edit">עריכה</string>
+ <string name="button_publish">פרסום</string>
+ <string name="my_site_no_sites_view_subtitle">האם תרצה להוסיף אחד?</string>
+ <string name="my_site_no_sites_view_title">עדיין אין לך אתרי וורדפרס.</string>
+ <string name="my_site_no_sites_view_drake">אילוסטרציה</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">אינך מורשה להיכנס לבלוג</string>
+ <string name="reader_toast_err_follow_blog_not_found">הבלוג לא נמצא</string>
+ <string name="undo">ביטול פעולה אחרונה</string>
+ <string name="tabbar_accessibility_label_my_site">האתר שלי</string>
+ <string name="tabbar_accessibility_label_me">אני</string>
+ <string name="passcodelock_prompt_message">הזנת קוד PIN</string>
+ <string name="editor_toast_changes_saved">השינויים נשמרו</string>
+ <string name="push_auth_expired">פג תוקף הבקשה. יש להיכנס לוורדפרס.קום כדי לנסות שוב.</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% מהצפיות</string>
+ <string name="stats_insights_best_ever">כמות הצפיות הגבוהה בכל הזמנים</string>
+ <string name="ignore">להתעלם</string>
+ <string name="stats_insights_most_popular_hour">השעה הפופולרית ביותר</string>
+ <string name="stats_insights_most_popular_day">היום הפופולרי ביותר</string>
+ <string name="stats_insights_popular">היום והשעה הפופולרים ביותר</string>
+ <string name="stats_insights_today">הנתונים הסטטיסטיים של היום</string>
+ <string name="stats_insights_all_time">פוסטים, צפיות ומבקרים מההתחלה ועד היום</string>
+ <string name="stats_insights">תובנות</string>
+ <string name="stats_sign_in_jetpack_different_com_account">להצגת הנתונים הסטטיסטיים שלך, יש להיכנס לוורדפרס.קום שבו השתמשת כדי לחבר את Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">מנסה למצוא נתונים סטטיסטיים אחרים מהזמן האחרון? העברנו אותם אל העמוד \'תובנות\'.</string>
+ <string name="me_disconnect_from_wordpress_com">התנתקות מוורדפרס.קום</string>
+ <string name="me_btn_login_logout">כניסה/יציאה</string>
+ <string name="me_connect_to_wordpress_com">התחברת לוורדפרס.קום</string>
+ <string name="site_picker_cant_hide_current_site">"%s" לא הוסתר מכיוון שזהו האתר הנוכחי</string>
+ <string name="account_settings">הגדרות חשבון</string>
+ <string name="me_btn_support">עזרה ותמיכה</string>
+ <string name="site_picker_create_dotcom">יצירת אתר וורדפרס.קום</string>
+ <string name="site_picker_edit_visibility">הצגת/הסתרת אתרים</string>
+ <string name="site_picker_add_self_hosted">הוספת אתר באחסון עצמי</string>
+ <string name="site_picker_add_site">הוספת אתר</string>
+ <string name="site_picker_title">בחירת אתר</string>
+ <string name="my_site_btn_switch_site">החלפת אתר</string>
+ <string name="my_site_btn_view_admin">הצגת מנהל מערכת</string>
+ <string name="my_site_btn_view_site">הצגת אתר</string>
+ <string name="my_site_header_publish">פרסום</string>
+ <string name="my_site_header_look_and_feel">סגנון</string>
+ <string name="my_site_btn_site_settings">הגדרות</string>
+ <string name="my_site_btn_blog_posts">פוסטים בבלוג</string>
+ <string name="reader_label_new_posts_subtitle">יש ללחוץ כדי להציג</string>
+ <string name="my_site_header_configuration">הגדרות תצורה</string>
+ <string name="notifications_account_required">התחברות לוורדפרס.קום עבור התראות</string>
+ <string name="stats_unknown_author">מחבר לא ידוע</string>
+ <string name="image_added">תמונה נוספה</string>
+ <string name="signout">התנתקות</string>
+ <string name="deselect_all">ביטול בחירה בכל האפשרויות</string>
+ <string name="hide">הסתר</string>
+ <string name="select_all">בחר הכל</string>
+ <string name="show">הצג</string>
+ <string name="sign_out_wpcom_confirm">ניתוק החשבון יסיר את כל נתוני וורדפרס.קום ‏של %s ממכשיר זה, כולל טיוטות מקומיות ושינויים מקומיים.</string>
+ <string name="select_from_new_picker">בחירה מרובה בעזרת רכיב חדש</string>
+ <string name="no_media_sources">לא ניתן לטעון קבצי מדיה</string>
+ <string name="loading_blog_videos">טוען וידאו</string>
+ <string name="loading_blog_images">טוען תמונות</string>
+ <string name="error_loading_videos">שגיאה בטעינת וידאו</string>
+ <string name="error_loading_images">שגיאה בטעינת תמונות</string>
+ <string name="error_loading_blog_images">לא ניתן לטעון תמונות</string>
+ <string name="error_loading_blog_videos">לא ניתן לטעון וידאו</string>
+ <string name="no_device_images">אין תמונות</string>
+ <string name="stats_generic_error">לא ניתן לטעון סטטוס נדרש</string>
+ <string name="no_device_videos">אין וידאו</string>
+ <string name="no_blog_images">אין תמונות</string>
+ <string name="no_blog_videos">אין וידאו</string>
+ <string name="loading_videos">טוען וידאו</string>
+ <string name="no_media">אין קבצי מדיה</string>
+ <string name="loading_images">טוען תמונות</string>
+ <string name="editor_toast_invalid_path">נתיב קובץ שגוי</string>
+ <string name="verification_code">קוד אימות</string>
+ <string name="invalid_verification_code">קוד אימות שגוי</string>
+ <string name="verify">אימות</string>
+ <string name="two_step_footer_label">יש להזין את הקוד מאפליקציית האימות.</string>
+ <string name="two_step_footer_button">שליחת קוד באמצעות הודעת טקסט</string>
+ <string name="two_step_sms_sent">יש לבדוק הודעות טקסט לקבלת קוד אימות.</string>
+ <string name="error_publish_no_network">לא ניתן לפרסם כאשר אין חיבור אינטרנט. נשמר כטיוטה.</string>
+ <string name="auth_required">יש להתחבר שוב כדי המשיך.</string>
+ <string name="tab_title_site_videos">וידאו מהאתר</string>
+ <string name="tab_title_site_images">תמונות מהאתר</string>
+ <string name="tab_title_device_videos">וידאו מהמכשיר</string>
+ <string name="tab_title_device_images">תמונות מהמכשיר</string>
+ <string name="take_video">צילום וידאו</string>
+ <string name="take_photo">צילום תמונה</string>
+ <string name="media_picker_title">בחירה</string>
+ <string name="add_to_post">הוספה לפוסט</string>
+ <string name="language">שפה</string>
+ <string name="device">מכשיר</string>
+ <string name="sign_in_jetpack">יש להתחבר לחשבון וורדפרס.קום כדי להתחבר אל Jetpack.</string>
+ <string name="media_details_label_file_type">סוג קובץ</string>
+ <string name="media_details_label_file_name">שם קובץ</string>
+ <string name="media_fetching">מביא מדיה...</string>
+ <string name="posts_fetching">מביא פוסטים...</string>
+ <string name="pages_fetching">מביא עמודים...</string>
+ <string name="toast_err_post_uploading">אי אפשר לפתוח פוסט תוך כדי העלאה</string>
+ <string name="stats_empty_search_terms">לא נרשמו מונחי חיפוש</string>
+ <string name="stats_entry_search_terms">מונח חיפוש</string>
+ <string name="stats_view_authors">מחברים</string>
+ <string name="stats_view_search_terms">מונחי חיפוש</string>
+ <string name="comments_fetching">מביא תגובות...</string>
+ <string name="stats_empty_search_terms_desc">ניתן ללמוד עוד על תעבורת החיפוש שלך באמצעות בדיקת המונחים שאותם חיפשו המבקרים כדי למצוא את האתר שלך.</string>
+ <string name="stats_followers_total_email_paged">מציג %1$d - %2$d מתוך %3$s עוקבים באימייל</string>
+ <string name="stats_search_terms_unknown_search_terms">מונחי חיפוש לא ידועים</string>
+ <string name="reader_empty_posts_request_failed">לא הצלחנו לאחזר פוסטים</string>
+ <string name="publisher">Publisher:</string>
+ <string name="error_notification_open">לא הצלחנו לפתוח את ההתראה</string>
+ <string name="stats_followers_total_wpcom_paged">מציג %1$d - %2$d מתוך %3$s עוקבי וורדפרס.קום</string>
+ <string name="error_copy_to_clipboard">אירעה שגיאה בעת העתקת הטקסט אל ה-clipboard</string>
+ <string name="stats_recent_weeks">שבועות אחרונים</string>
+ <string name="logs_copied_to_clipboard">הלוגים של האפליקציה הועתקו אל ה-clipboard</string>
+ <string name="stats_overall">מבט כללי</string>
+ <string name="stats_period">תקופה</string>
+ <string name="reader_empty_posts_in_blog">אתר ריק</string>
+ <string name="stats_average_per_day">ממוצע יומי</string>
+ <string name="reader_page_recommended_blogs">אתרים שאתה עשוי לאהוב</string>
+ <string name="stats_months_and_years">חודשים ושנים</string>
+ <string name="reader_label_new_posts">פוסטים חדשים</string>
+ <string name="stats_total">סה"כ</string>
+ <string name="post_uploading">מעלה</string>
+ <string name="stats_comments_total_comments_followers">סה"כ פוסטים עם עוקבים אחר תגובות: %1$s</string>
+ <string name="stats_empty_video">לא הופעל וידאו</string>
+ <string name="stats_view_videos">וידאו</string>
+ <string name="stats_visitors">מבקרים</string>
+ <string name="stats_timeframe_years">שנים</string>
+ <string name="stats_views">צפיות</string>
+ <string name="stats_pagination_label">עמוד %1$s מתוך %2$s</string>
+ <string name="stats_likes"> לייקים</string>
+ <string name="stats_view_countries">מדינות</string>
+ <string name="stats_view_followers">עוקבים</string>
+ <string name="stats_view_publicize">שיתוף אוטומטי</string>
+ <string name="stats_entry_clicks_link">קישור</string>
+ <string name="stats_view_top_posts_and_pages">פוסטים ועמודים</string>
+ <string name="stats_totals_publicize">עוקבים</string>
+ <string name="stats_entry_followers">עוקב</string>
+ <string name="stats_entry_publicize">שירות</string>
+ <string name="stats_entry_top_commenter">מחבר</string>
+ <string name="stats_entry_video_plays">וידאו</string>
+ <string name="stats_empty_geoviews_desc">עיון ברשימה יראה לך אילו מדינות ואזורים מפנים הכי הרבה תעבורה אל האתר שלך.</string>
+ <string name="stats_empty_geoviews">לא נרשמו מדינות</string>
+ <string name="stats_totals_followers">מאז</string>
+ <string name="stats_empty_top_posts_desc">זוהי דרך מצוינת לגלות אילו פריטי תוכן קיבלו הכי הרבה צפיות ולבדוק את הביצועים של פוסטים ועמודים ספציפיים לאורך זמן.</string>
+ <string name="stats_empty_top_posts_title">לא נצפו פוסטים או עמודים</string>
+ <string name="stats_empty_referrers_title">לא נרשמו הפניות</string>
+ <string name="stats_empty_clicks_title">לא נרשמו לחיצות</string>
+ <string name="stats_empty_referrers_desc">אפשר לקבל מידע נוסף על הנראות של האתר על ידי בדיקת האתרים ומנועי החיפוש ששולחים אליו הכי הרבה תעבורה</string>
+ <string name="stats_empty_tags_and_categories">לא נצפו פוסטים או עמודים מתויגים</string>
+ <string name="stats_empty_clicks_desc">אם התוכן שלך כולל קישורים לאתרים אחרים, אפשר לראות על אילו קישורים מבקרי האתר לחצו הכי הרבה.</string>
+ <string name="stats_empty_tags_and_categories_desc">אפשר לקבל סקירה כללית על הנושאים הפופולריים ביותר באתר שלך, לפי הפוסטים המובילים מהשבוע שחלף.</string>
+ <string name="stats_empty_top_authors_desc">אפשר לעקוב אחר הצפיות בפוסטים של כל משתתף ולהגדיל תמונה כדי לראות את הפריטים הפופולריים ביותר של כל מחבר.</string>
+ <string name="stats_empty_comments_desc">אם אפשרת תגובות באתר, אפשר לעקוב אחר המגיבים המובילים ולגלות את פריטי התוכן שהציתו את השיחות התוססות ביותר, על פי 1,000 התגובות האחרונות.</string>
+ <string name="stats_empty_video_desc">לאחר העלאת סרטוני וידאו בעזרת VideoPress אפשר לקבל פרטים על מספר הצפיות.</string>
+ <string name="stats_empty_publicize">לא נרשמו עוקבים בשיתוף האוטומטי</string>
+ <string name="stats_empty_followers">אין עוקבים</string>
+ <string name="stats_empty_publicize_desc">בעזרת שיתוף אוטומטי אפשר לנהל מעקב אחר העוקבים שלך מרשתות חברתיות שונות.</string>
+ <string name="stats_empty_followers_desc">אפשר לעקוב אחר המספר הכולל של עוקבים ומשך הזמן שכל אחד מהם עוקב אחר האתר שלך.</string>
+ <string name="stats_comments_by_posts_and_pages">לפי פוסטים ועמודים</string>
+ <string name="stats_comments_by_authors">לפי מחברים</string>
+ <string name="stats_followers_email_selector">אימייל</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_seconds_ago">לפני כמה שניות</string>
+ <string name="stats_followers_total_email">סה"כ עוקבי אימייל: %1$s</string>
+ <string name="stats_followers_days">%1$d ימים</string>
+ <string name="stats_followers_a_minute_ago">לפני דקה</string>
+ <string name="stats_followers_hours">%1$d שעות</string>
+ <string name="stats_followers_a_day">יום</string>
+ <string name="stats_followers_minutes">%1$d דקות</string>
+ <string name="stats_followers_an_hour_ago">לפני שעה</string>
+ <string name="stats_followers_years">%1$d שנים</string>
+ <string name="stats_followers_a_month">חודש</string>
+ <string name="stats_followers_months">%1$d חודשים</string>
+ <string name="stats_followers_a_year">שנה</string>
+ <string name="stats_view_all">להציג הכל</string>
+ <string name="stats_view">הצגה</string>
+ <string name="themes_fetching">משיג ערכות עיצוב…</string>
+ <string name="stats_for">סטטיסטיקות עבור %s</string>
+ <string name="stats_other_recent_stats_label">סטטיסטיקות אחרות מהזמן האחרון</string>
+ <string name="stats_followers_total_wpcom">סה"כ עוקבי וורדפרס.קום: %1$s</string>
+ <string name="ssl_certificate_details">פרטים</string>
+ <string name="media_gallery_date_range">הצגת פריטי מדיה מ- %1$s עד %2$s</string>
+ <string name="confirm_delete_media">מחיקת פריט שנבחר?</string>
+ <string name="confirm_delete_multi_media">מחיקת פריטים שנבחרו?</string>
+ <string name="cab_selected">%d נבחרו</string>
+ <string name="sure_to_remove_account">הסרת אתר?</string>
+ <string name="delete_sure_page">מחיקת עמוד</string>
+ <string name="delete_sure_post">מחיקת פוסט</string>
+ <string name="delete_sure">מחיקת טיוטה</string>
+ <string name="stats_no_blog">לא ניתן לטעון את הנתונים הסטטיסטיים עבור האתר הדרוש</string>
+ <string name="faq_button">שאלות נפוצות</string>
+ <string name="reader_empty_comments">אין תגובות עדיין</string>
+ <string name="reader_empty_posts_in_tag">אין פוסטים עם התגית הזו</string>
+ <string name="reader_label_comment_count_multi">%,d תגובות</string>
+ <string name="reader_label_view_original">לצפות בפוסט המקורי</string>
+ <string name="reader_label_comment_count_single">תגובה אחת</string>
+ <string name="reader_label_comments_on">תגובות מופעלות</string>
+ <string name="reader_label_comments_closed">תגובות לא מופעלות</string>
+ <string name="error_publish_empty_post">לא ניתן לפרסם פוסט ריק</string>
+ <string name="more">עוד</string>
+ <string name="error_refresh_unauthorized_pages">אין לך הרשאה לערוך או לצפות בעמודים</string>
+ <string name="error_refresh_unauthorized_posts">אין לך הרשאה לערוך או לצפות בפוסטים</string>
+ <string name="reader_title_photo_viewer">%1$d מתוך %2$d</string>
+ <string name="comment">תגובה</string>
+ <string name="comment_trashed">התגובה הועברה לפח</string>
+ <string name="posts_empty_list">אין פוסטים עדיין. למה לא ליצור אחד?</string>
+ <string name="comment_reply_to_user">להגיב ל-%s</string>
+ <string name="pages_empty_list">אין עמודים עדיין. למה לא ליצור אחד?</string>
+ <string name="reader_empty_posts_liked">לא עשית לייק על אף פוסט</string>
+ <string name="browse_our_faq_button">לעיון בשאלות הנפוצות שלנו</string>
+ <string name="nux_help_description">אפשר לבקר במרכז העזרה כדי לקבל תשובות לשאלות נפוצות, או לבקר בפורומים כדי לשאול שאלות חדשות</string>
+ <string name="reader_label_like">לייק</string>
+ <string name="error_refresh_unauthorized_comments">איך לך הרשאה להציג או לערוך תגובות</string>
+ <string name="older_month">בן יותר מחודש</string>
+ <string name="older_two_days">בן יותר מיומיים</string>
+ <string name="older_last_week">בן יותר משבוע</string>
+ <string name="select_a_blog">בחר אתר WordPress</string>
+ <string name="uploading_total">העלאה %1$d של %2$d</string>
+ <string name="mnu_comment_liked">פרסם לייק</string>
+ <string name="media_empty_list_custom_date">אין מדיה במרווח זמן זה</string>
+ <string name="posting_post">מפרסם "%s"</string>
+ <string name="signing_out">יוצא מהמערכת...</string>
+ <string name="agree_terms_of_service">‏יצירת חשבון מהווה הסכמה ל%1$sתנאי השימוש המרתקים%2$s שלנו</string>
+ <string name="sending_content">מעלה תוכן של %s</string>
+ <string name="create_new_blog_wpcom">יצירת אתר וורדפרס.קום</string>
+ <string name="new_blog_wpcom_created">נוצר אתר בוורדפרס.קום</string>
+ <string name="reader_empty_followed_blogs_title">אינך עוקב אחרי שום אתר עדיין</string>
+ <string name="reader_menu_block_blog">לחסום את האתר הזה</string>
+ <string name="reader_toast_blog_blocked">הפוסטים מהאתר הזה לא יוצגו עוד</string>
+ <string name="reader_toast_err_block_blog">לא ניתן לחסום אתר זה</string>
+ <string name="reader_toast_err_generic">לא ניתן לבצע את הפעולה</string>
+ <string name="contact_us">ליצור קשר</string>
+ <string name="hs__conversation_detail_error">תאר/י את הבעיה בה נתקלת</string>
+ <string name="hs__new_conversation_header">צ׳אט תמיכה</string>
+ <string name="hs__conversation_header">צ׳אט תמיכה</string>
+ <string name="hs__username_blank_error">נא להזין שם</string>
+ <string name="hs__invalid_email_error">נא להזין כתובת אימייל</string>
+ <string name="add_location">הוספת מיקום</string>
+ <string name="current_location">מיקום נוכחי</string>
+ <string name="search_location">חיפוש</string>
+ <string name="edit_location">עריכה</string>
+ <string name="search_current_location">אתר</string>
+ <string name="preference_send_usage_stats">שליחת סטטיסטיקות</string>
+ <string name="preference_send_usage_stats_summary">שליחת נתוני שימוש סטטיסטיים שיאפשרו לנו לשפר את WordPress לאנדרויד</string>
+ <string name="update_verb">עדכן</string>
+ <string name="schedule_verb">תזמן</string>
+ <string name="reader_toast_err_unfollow_blog">לא ניתן לבטל את המעקב אחר אתר זה</string>
+ <string name="reader_empty_recommended_blogs">אין אתרים מומלצים</string>
+ <string name="reader_toast_err_follow_blog">לא ניתן לעקוב אחר אתר זה</string>
+ <string name="reader_toast_err_already_follow_blog">אתה כבר עוקב אחר אתר זה</string>
+ <string name="reader_toast_err_get_blog_info">לא ניתן להציג אתר זה</string>
+ <string name="reader_label_followed_blog">אתר נעקב</string>
+ <string name="reader_title_subs">תגיות ואתרים</string>
+ <string name="reader_page_followed_blogs">אתרים נעקבים</string>
+ <string name="reader_page_followed_tags">תגיות נעקבות</string>
+ <string name="reader_hint_add_tag_or_url">הזן תגית או כתובת אתר למעקב</string>
+ <string name="reader_label_tag_preview">פוסטים שתויגו %s</string>
+ <string name="reader_title_blog_preview">קורא בלוג</string>
+ <string name="reader_title_tag_preview">קורא תגית</string>
+ <string name="saving">שומר...</string>
+ <string name="media_empty_list">אין מדיה</string>
+ <string name="ptr_tip_message">עצה: משוך כלפי מטה כדי לרענן</string>
+ <string name="help">עזרה</string>
+ <string name="forgot_password">איבדת את הסיסמה שלך?</string>
+ <string name="forums">פורומים</string>
+ <string name="help_center">מרכז עזרה</string>
+ <string name="ssl_certificate_error">אישור SSL לא תקף</string>
+ <string name="ssl_certificate_ask_trust">אם בדרך כלל אתה מתחבר לאתר זה ללא בעיות, ייתכן ששגיאה זו נובעת מכך שמישהו מנסה להתחזות לאתר ולכן אסור לך להמשיך. האם ברצונך בכל זאת לתת אמון באישור?</string>
+ <string name="blog_not_found">אירעה שגיאה בזמן הגישה לאתר זה</string>
+ <string name="could_not_remove_account">לא ניתן היה להסיר את האתר</string>
+ <string name="out_of_memory">זיכרון המכשיר מלא</string>
+ <string name="no_network_message">אין רשת זמינה</string>
+ <string name="gallery_error">לא ניתן היה לאחזר את פריט המדיה</string>
+ <string name="wait_until_upload_completes">המתן לסיום ההעלאה</string>
+ <string name="theme_fetch_failed">הבאת ערכות העיצוב נכשלה</string>
+ <string name="theme_set_failed">הגדרת ערכת העיצוב נכשלה</string>
+ <string name="theme_auth_error_message">ודא שיש לך הרשאה להגדיר ערכות עיצוב</string>
+ <string name="comments_empty_list">אין תגובות</string>
+ <string name="mnu_comment_unspam">לא תגובת זבל</string>
+ <string name="no_site_error">לא ניתן היה להתחבר לאתר WordPress</string>
+ <string name="adding_cat_failed">הוספת קטגוריה נכשלה</string>
+ <string name="adding_cat_success">הקטגוריה הוספה בהצלחה</string>
+ <string name="cat_name_required">שם הקטגוריה הוא שדה חובה</string>
+ <string name="category_automatically_renamed">שם הקטגוריה %1$s אינו תקף. הוא שונה ל-%2$s.</string>
+ <string name="no_account">לא נמצא חשבון ב-WordPress, הוסף חשבון ונסה שנית</string>
+ <string name="sdcard_message">יש לטעון כרטיס SD כדי להעלות קבצים</string>
+ <string name="stats_empty_comments">עדיין אין תגובות</string>
+ <string name="stats_bar_graph_empty">הנתונים הסטטיסטיים לא זמינים</string>
+ <string name="reply_failed">התשובה נכשלה</string>
+ <string name="notifications_empty_list">אין הודעות</string>
+ <string name="error_delete_post">אירעה שגיאה בזמן המחיקה של %s</string>
+ <string name="error_refresh_posts">לא ניתן לרענן רשומות כעת</string>
+ <string name="error_refresh_pages">לא ניתן לרענן דפים כעת</string>
+ <string name="error_refresh_notifications">לא ניתן לרענן הודעות כעת</string>
+ <string name="error_refresh_comments">לא ניתן לרענן תגובות כעת</string>
+ <string name="error_refresh_stats">לא ניתן לרענן נתונים סטטיסטיים כעת</string>
+ <string name="error_generic">אירעה שגיאה</string>
+ <string name="error_moderate_comment">אירעה שגיאה בזמן בדיקת התגובות</string>
+ <string name="error_edit_comment">אירעה שגיאה בזמן עריכת התגובה</string>
+ <string name="error_upload">אירעה שגיאה בזמן העלאת ה-%s</string>
+ <string name="error_load_comment">לא ניתן היה להעלות את התגובה</string>
+ <string name="error_downloading_image">שגיאה בזמן הורדת תמונה</string>
+ <string name="passcode_wrong_passcode">PIN שגוי</string>
+ <string name="invalid_email_message">כתובת האימייל שלך לא תקפה</string>
+ <string name="invalid_password_message">הסיסמה חייבת להכיל לפחות 4 תווים</string>
+ <string name="invalid_username_too_short">שם המשתמש חייב להכיל מעל 4 תווים</string>
+ <string name="invalid_username_too_long">שם המשתמש חייב להכיל פחות מ-61 תווים</string>
+ <string name="username_only_lowercase_letters_and_numbers">שם משתמש יכול להכיל רק אותיות לא רישיות (a-z) וספרות</string>
+ <string name="username_required">הקלד שם משתמש</string>
+ <string name="username_not_allowed">שם המשתמש לא מותר</string>
+ <string name="username_must_be_at_least_four_characters">שם המשתמש חייב להכיל לפחות 4 תווים</string>
+ <string name="username_contains_invalid_characters">שם המשתמש לא יכול לכלול את התו "_"</string>
+ <string name="username_must_include_letters">שם המשתמש חייב לכלול לפחות אות אחת (a-z)</string>
+ <string name="email_invalid">הקלד כתובת אימייל חוקית</string>
+ <string name="email_not_allowed">כתובת אימייל זו אינה מותרת</string>
+ <string name="username_exists">שם משתמש זה כבר קיים</string>
+ <string name="email_exists">כתובת אימייל זו כבר בשימוש</string>
+ <string name="username_reserved_but_may_be_available">שם המשתמש הזה שמור כעת אך עשוי להיות זמין בעוד כמה ימים</string>
+ <string name="blog_name_required">הזן כתובת אתר</string>
+ <string name="blog_name_not_allowed">כתובת אתר זו אינה מותרת</string>
+ <string name="blog_name_must_be_at_least_four_characters">כתובת האתר חייבת להכיל לפחות 4 תווים</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">כתובת האתר חייבת לכלול פחות מ-64 תווים</string>
+ <string name="blog_name_contains_invalid_characters">כתובת האתר לא יכולה לכלול את התו "_"</string>
+ <string name="blog_name_cant_be_used">אינך רשאי להשתמש בכתובת אתר זו</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">כתובת האתר יכולה לכלול רק אותיות לא רישיות (a-z) וספרות</string>
+ <string name="blog_name_exists">אתר זה כבר קיים</string>
+ <string name="blog_name_reserved">אתר זה שמור</string>
+ <string name="blog_name_reserved_but_may_be_available">אתר זה שמור כעת אך עשוי להיות זמין בעוד כמה ימים</string>
+ <string name="username_or_password_incorrect">שם המשתמש או הסיסמה שהקלדת שגויים</string>
+ <string name="nux_cannot_log_in">איננו יכולים לחבר אותך</string>
+ <string name="invalid_url_message">ודא שכתובת האתר שהוזנה תקפה</string>
+ <string name="wordpress_blog">אתר ב-WordPress</string>
+ <string name="blog_removed_successfully">האתר הוסר בהצלחה</string>
+ <string name="add_account_blog_url">כתובת האתר</string>
+ <string name="remove_account">הסר אתר</string>
+ <string name="error_blog_hidden">זה אתר סמוי ולא ניתן לטעון אותו. אפשר אותו שוב בהגדרות ונסה שנית.</string>
+ <string name="xmlrpc_error">לא ניתן להתחבר. הקלד את הנתיב המלא אל xmlrpc.php באתר שלך ונסה שנית.</string>
+ <string name="select_categories">בחר קטגוריות</string>
+ <string name="account_details">פרטי חשבון</string>
+ <string name="edit_post">ערוך רשומה</string>
+ <string name="add_comment">הוסף תגובה</string>
+ <string name="connection_error">שגיאת התחברות</string>
+ <string name="cancel_edit">בטל עריכה</string>
+ <string name="scaled_image_error">הקלד ערך תקף של רוחב מדורג</string>
+ <string name="post_not_found">אירעה שגיאה בזמן העלאת הרשומה. רענן את הרשומות שלך ונסה שנית.</string>
+ <string name="learn_more">מידע נוסף</string>
+ <string name="media_gallery_settings_title">הגדרות גלריה</string>
+ <string name="media_gallery_image_order">סדר התמונות</string>
+ <string name="media_gallery_num_columns">מספר העמודות</string>
+ <string name="media_gallery_type_thumbnail_grid">טבלת תמונות ממוזערות</string>
+ <string name="media_gallery_edit">ערוך גלריה</string>
+ <string name="media_error_no_permission">אין לך הרשאה לצפות בספריית המדיה</string>
+ <string name="cannot_delete_multi_media_items">לא ניתן בשלב זה למחוק חלק מהמדיה. נסה שנית מאוחר יותר.</string>
+ <string name="themes_live_preview">תצוגה מקדימה חיה</string>
+ <string name="theme_current_theme">ערכת עיצוב נוכחית</string>
+ <string name="theme_premium_theme">ערכת עיצוב מסוג Premium Theme </string>
+ <string name="link_enter_url_text">טקסט קישור (אופציונאלי)</string>
+ <string name="create_a_link">צור קישור</string>
+ <string name="page_settings">הגדרות דף</string>
+ <string name="local_draft">טיוטה מקומית</string>
+ <string name="upload_failed">העלאה נכשלה</string>
+ <string name="horizontal_alignment">יישור אופקי</string>
+ <string name="file_not_found">מציאת הקובץ להעלאה נכשלה. האם הוא נמחק או הועבר?</string>
+ <string name="post_settings">הגדרות רשומה</string>
+ <string name="delete_post">מחק רשומה</string>
+ <string name="delete_page">מחק דף</string>
+ <string name="comment_status_approved">אושר</string>
+ <string name="comment_status_unapproved">בהמתנה</string>
+ <string name="comment_status_spam">תגובות זבל</string>
+ <string name="comment_status_trash">הושלך לאשפה</string>
+ <string name="edit_comment">ערוך תגובה</string>
+ <string name="mnu_comment_approve">אשר</string>
+ <string name="mnu_comment_unapprove">בטל אישור</string>
+ <string name="mnu_comment_spam">תגובות זבל</string>
+ <string name="mnu_comment_trash">אשפה</string>
+ <string name="dlg_approving_comments">מאשר</string>
+ <string name="dlg_unapproving_comments">מבטל אישור</string>
+ <string name="dlg_spamming_comments">מסמן כתגובת זבל</string>
+ <string name="dlg_trashing_comments">שולח לאשפה</string>
+ <string name="dlg_confirm_trash_comments">לשלוח לאשפה?</string>
+ <string name="trash_yes">אשפה</string>
+ <string name="trash_no">לא לשלוח לאשפה</string>
+ <string name="trash">אשפה</string>
+ <string name="author_name">שם המחבר</string>
+ <string name="author_email">אימייל של המחבר</string>
+ <string name="author_url">כתובת URL של המחבר </string>
+ <string name="hint_comment_content">תגובה</string>
+ <string name="saving_changes">שומר שינויים</string>
+ <string name="sure_to_cancel_edit_comment">לבטל עריכה של תגובה זו?</string>
+ <string name="content_required">נדרשת תגובה</string>
+ <string name="toast_comment_unedited">התגובה לא השתנתה</string>
+ <string name="delete_draft">מחק טיוטה</string>
+ <string name="preview_page">תצוגה מקדימה של הדף</string>
+ <string name="preview_post">תצוגה מקדימה של הרשומה</string>
+ <string name="comment_added">התגובה הוספה בהצלחה</string>
+ <string name="post_not_published">מצב העריכה לא פורסם</string>
+ <string name="page_not_published">מצב הדף לא פורסם</string>
+ <string name="view_in_browser">הצג בדפדפן</string>
+ <string name="add_new_category">הוסף קטגוריה חדשה</string>
+ <string name="category_name">שם קטגוריה</string>
+ <string name="category_slug">מזהה לכתובת קטגוריה (אופציונאלי)</string>
+ <string name="category_desc">תיאור קטגוריה (אופציונאלי)</string>
+ <string name="category_parent">קטגוריית-אב (אופציונאלי):</string>
+ <string name="share_action_post">רשומה חדשה</string>
+ <string name="share_action_media">ספריית מדיה</string>
+ <string name="file_error_create">יצירת קובץ temp להעלאת קבצים לא הצליחה. וודא שיש מספיק מקום פנוי במכשיר.</string>
+ <string name="location_not_found">מיקום לא ידוע</string>
+ <string name="open_source_licenses">רישיונות קוד פתוח</string>
+ <string name="pending_review">בהמתנה לבדיקה</string>
+ <string name="http_credentials">אישורי HTTP (אופציונאלי)</string>
+ <string name="http_authorization_required">דרושה הרשאה</string>
+ <string name="post_format">סוג רשומה</string>
+ <string name="new_post">רשומה חדשה</string>
+ <string name="new_media">מדיה חדשה</string>
+ <string name="view_site">צפה באתר</string>
+ <string name="privacy_policy">מדיניות פרטיות</string>
+ <string name="local_changes">שינויים מקומיים</string>
+ <string name="image_settings">הגדרות תמונה</string>
+ <string name="fatal_db_error">אירעה שגיאה בזמן יצירת מסד הנתונים של היישום. נסה להתקין מחדש את היישום.</string>
+ <string name="jetpack_message_not_admin">התוסף Jetpack נדרש לקבלת נתונים סטטיסטיים. פנה למנהל האתר.</string>
+ <string name="reader_title_applog">יומן יישום</string>
+ <string name="reader_share_link">שתף בקישור</string>
+ <string name="reader_toast_err_add_tag">לא ניתן להוסיף תגית זו</string>
+ <string name="reader_toast_err_remove_tag">לא ניתן להסיר תגית זו</string>
+ <string name="required_field">שדה חובה</string>
+ <string name="email_hint">כתובת אימייל</string>
+ <string name="site_address">כתובת האחסון העצמי שלך (URL)</string>
+ <string name="email_cant_be_used_to_signup">אינך רשאי להשתמש בכתובת אימייל זו בהרשמה. יש לנו בעיה מכיוון שהם חוסמים חלק מהודעות האימייל שלנו. השתמש בספק אימייל אחר.</string>
+ <string name="email_reserved">כתובת אימייל זו כבר הייתה בשימוש. חפש בתיבת הדואר הנכנס שלך אימייל להפעלה. אם לא תפעיל עכשיו תוכל לנסות שנית בעוד כמה ימים.</string>
+ <string name="blog_name_must_include_letters">כתובת האתר חייב לכלול לפחות אות אחת (a-z)</string>
+ <string name="blog_name_invalid">כתובת אתר לא תקפה</string>
+ <string name="blog_title_invalid">כותרת אתר לא תקפה</string>
+ <string name="notifications_empty_all">אין התראות... עדיין.</string>
+ <string name="invalid_site_url_message">יש לוודא שכתובת ה-URL של האתר שהוזנה תקפה</string>
+ <string name="share_url_post">שתף פוסט</string>
+ <string name="share_url_page">שתף עמוד</string>
+ <string name="share_link">קישור לשיתוף</string>
+ <string name="deleting_page">מוחק דף</string>
+ <string name="deleting_post">מוחק רשומה</string>
+ <string name="creating_your_account">יוצר את החשבון שלך</string>
+ <string name="creating_your_site">יוצר את האתר שלך</string>
+ <string name="reader_empty_posts_in_tag_updating">מביא רשומות...</string>
+ <string name="error_refresh_media">משהו השתבש בזמן הרענון של ספריית המדיה. נסה שנית מאוחר יותר.</string>
+ <string name="cant_share_no_visible_blog">אינך יכול לשתף ב-WordPress ללא אתר גלוי</string>
+ <string name="reader_likes_you_and_multi">אתה ו- %,d אחרים אוהבים את זה</string>
+ <string name="reader_likes_multi">%,d אנשים אוהבים את זה</string>
+ <string name="reader_toast_err_get_comment">לא ניתן לאחזר תגובה זו</string>
+ <string name="reader_label_reply">תשובה</string>
+ <string name="video">וידאו</string>
+ <string name="download">מוריד מדיה</string>
+ <string name="comment_spammed">התגובה סומנה כתגובת זבל</string>
+ <string name="select_time">בחר שעה</string>
+ <string name="reader_likes_you_and_one">אתה ועוד אחד אוהבים את זה</string>
+ <string name="select_date">בחר תאריך</string>
+ <string name="pick_photo">בחר תמונה</string>
+ <string name="pick_video">בחר וידאו</string>
+ <string name="reader_toast_err_get_post">לא ניתן לאחזר רשומה זו</string>
+ <string name="validating_user_data">מאמת נתוני משתמש</string>
+ <string name="validating_site_data">מאמת נתוני אתר</string>
+ <string name="account_two_step_auth_enabled">בחשבון זה מופעל אימות דו-שלבי. עיין בהגדרות האבטחה בוורדפרס.קום וצור סיסמה ספציפית ליישום.</string>
+ <string name="reader_empty_followed_blogs_description">אך אין צורך לדאוג, יש להקיש מימין כדי להתחיל לשוטט!</string>
+ <string name="password_invalid">נחוצה לך סיסמה בטוחה יותר. הקפד להשתמש ב-7 תווים או יותר, שלב אותיות רישיות ורגילות, מספרים או תווים מיוחדים.</string>
+ <string name="nux_tap_continue">המשך</string>
+ <string name="nux_welcome_create_account">צור חשבון</string>
+ <string name="nux_add_selfhosted_blog">הוסף אתר באחסון עצמי</string>
+ <string name="signing_in">מתחבר...</string>
+ <string name="nux_oops_not_selfhosted_blog">התחבר לוורדפרס.קום</string>
+ <string name="media_add_popup_title">הוסף לספריית המדיה</string>
+ <string name="media_add_new_media_gallery">צור גלרייה</string>
+ <string name="empty_list_default">רשימה זו ריקה</string>
+ <string name="select_from_media_library">בחר מתוך ספריית מדיה</string>
+ <string name="jetpack_message">התוסף Jetpack נדרש לקבלת נתונים סטטיסטיים. אתה מעוניין להתקין את Jetpack?</string>
+ <string name="jetpack_not_found">התוסף Jetpack לא נמצא</string>
+ <string name="reader_untitled_post">(ללא כותרת)</string>
+ <string name="reader_share_subject">שיתוף מ-%s</string>
+ <string name="reader_btn_share">שתף</string>
+ <string name="reader_btn_follow">עקוב</string>
+ <string name="reader_btn_unfollow">עוקב</string>
+ <string name="reader_hint_comment_on_comment">השב לתגובה</string>
+ <string name="reader_label_added_tag">%s התווסף</string>
+ <string name="reader_label_removed_tag">%s הוסר</string>
+ <string name="reader_likes_one">אדם אחד אוהב את זה</string>
+ <string name="reader_likes_only_you">אתה אוהב את זה</string>
+ <string name="reader_toast_err_comment_failed">לא ניתן לפרסם את תגובתך</string>
+ <string name="reader_toast_err_tag_exists">אתה כבר עוקב אחר תגית זו</string>
+ <string name="reader_toast_err_tag_invalid">זו לא תגית תקפה</string>
+ <string name="reader_toast_err_share_intent">לא ניתן לשתף</string>
+ <string name="reader_toast_err_view_image">לא ניתן להציג תמונה</string>
+ <string name="reader_toast_err_url_intent">לא ניתן לפתוח את %s</string>
+ <string name="username_invalid">שם משתמש לא חוקי</string>
+ <string name="limit_reached">הגעת למגבלה. תוכל לנסות שנית בעוד דקה 1. אם תנסה שנית קודם לכן, זה רק יאריך את זמן ההמתנה שלך עד להסרת המגבלה. אם אתה חושב שזו שגיאה, פנה לתמיכה.</string>
+ <string name="nux_tutorial_get_started_title">צא לדרך!</string>
+ <string name="reader_empty_followed_tags">אין מעקב אחר שום תגית</string>
+ <string name="create_account_wpcom">יצירת חשבון וורדפרס.קום</string>
+ <string name="connecting_wpcom">מתחבר לוורדפרס.קום</string>
+ <string name="button_next">לשלב הבא</string>
+ <string name="themes">ערכות עיצוב</string>
+ <string name="all">הכל</string>
+ <string name="images">תמונות</string>
+ <string name="media_gallery_image_order_random">אקראי</string>
+ <string name="stats_view_tags_and_categories">תגים וקטגוריות</string>
+ <string name="unattached">מנותק</string>
+ <string name="custom_date">תאריך מותאם אישית</string>
+ <string name="media_add_popup_capture_photo">צלם תמונה</string>
+ <string name="media_add_popup_capture_video">צלם וידאו</string>
+ <string name="media_gallery_image_order_reverse">אחורה</string>
+ <string name="media_gallery_type">סוג</string>
+ <string name="media_gallery_type_squares">ריבועים</string>
+ <string name="media_gallery_type_tiled">אריחים</string>
+ <string name="media_gallery_type_circles">עיגולים</string>
+ <string name="media_gallery_type_slideshow">מצגת</string>
+ <string name="media_edit_title_text">כותרת</string>
+ <string name="media_edit_caption_text">כיתוב</string>
+ <string name="media_edit_description_text">תיאור</string>
+ <string name="media_edit_title_hint">הקלד כותרת כאן</string>
+ <string name="media_edit_caption_hint">הקלד כיתוב כאן</string>
+ <string name="media_edit_description_hint">הקלד תיאור כאן</string>
+ <string name="media_edit_success">עודכן</string>
+ <string name="media_edit_failure">עדכון נכשל</string>
+ <string name="themes_details_label">פרטים</string>
+ <string name="themes_features_label">תכונות</string>
+ <string name="theme_activate_button">הפעל</string>
+ <string name="theme_activating_button">מפעיל</string>
+ <string name="theme_set_success">ערכת העיצוב הוגדרה בהצלחה!</string>
+ <string name="theme_auth_error_title">הבאת ערכות העיצוב נכשלה</string>
+ <string name="post_excerpt">תקציר</string>
+ <string name="share_action_title">הוסף ל...</string>
+ <string name="share_action">שתף</string>
+ <string name="stats">נתונים סטטיסטיים</string>
+ <string name="stats_view_visitors_and_views">מבקרים וצפיות</string>
+ <string name="stats_view_clicks">קליקים</string>
+ <string name="stats_view_referrers">הפניות</string>
+ <string name="stats_timeframe_today">היום</string>
+ <string name="stats_timeframe_yesterday">אתמול</string>
+ <string name="stats_timeframe_days">ימים</string>
+ <string name="stats_timeframe_weeks">שבועות</string>
+ <string name="stats_timeframe_months">חודשים</string>
+ <string name="stats_entry_country">ארץ</string>
+ <string name="stats_entry_posts_and_pages">כותרת</string>
+ <string name="stats_entry_tags_and_categories">נושא</string>
+ <string name="stats_entry_authors">מחבר</string>
+ <string name="stats_entry_referrers">הפניה</string>
+ <string name="stats_totals_views">צפיות</string>
+ <string name="stats_totals_clicks">קליקים</string>
+ <string name="stats_totals_plays">ניגונים</string>
+ <string name="passcode_manage">ניהול חסימה בקוד אישי</string>
+ <string name="passcode_enter_passcode">הקלד את הקוד האישי שלך</string>
+ <string name="passcode_enter_old_passcode">הקלד את הקוד האישי הישן שלך</string>
+ <string name="passcode_re_enter_passcode">הקלד שנית את הקוד האישי שלך</string>
+ <string name="passcode_change_passcode">החלף את הקוד האישי שלך</string>
+ <string name="passcode_set">קוד אישי הוגדר</string>
+ <string name="passcode_preference_title">נעילת קוד אישי</string>
+ <string name="passcode_turn_off">הפסק נעילת קוד אישי</string>
+ <string name="passcode_turn_on">הפעל נעילת קוד אישי</string>
+ <string name="discard">הסתר</string>
+ <string name="upload">העלאה</string>
+ <string name="sign_in">התחבר</string>
+ <string name="notifications">התראות</string>
+ <string name="note_reply_successful">תגובה פורסמה</string>
+ <string name="new_notifications">%d התראות חדשות</string>
+ <string name="more_notifications">הוסף %d נוספים.</string>
+ <string name="follows">עוקבים</string>
+ <string name="loading">טוען...</string>
+ <string name="httpuser">שם משתמש</string>
+ <string name="httppassword">סיסמה</string>
+ <string name="error_media_upload">ארעה שגיאה במהלך העלאת הקובץ.</string>
+ <string name="publish_date">פרסם</string>
+ <string name="content_description_add_media">הוסף מדיה</string>
+ <string name="post_content">תוכן (הקליקו להוספת טקסט או קבצי מדיה)</string>
+ <string name="incorrect_credentials">שם משתמש או סיסמה לא נכונים.</string>
+ <string name="password">סיסמה</string>
+ <string name="username">שם משתמש</string>
+ <string name="reader">קורא</string>
+ <string name="width">רוחב</string>
+ <string name="posts">פוסטים</string>
+ <string name="pages">עמודים</string>
+ <string name="post">פוסט</string>
+ <string name="page">עמוד</string>
+ <string name="anonymous">אנונימי</string>
+ <string name="featured">תמונה מובחרת</string>
+ <string name="no_network_title">הרשת אינה זמינה</string>
+ <string name="caption">כיתוב תמונה (אופציונלי)</string>
+ <string name="featured_in_post">הוספת תמונה לתוכן</string>
+ <string name="ok">אוקיי</string>
+ <string name="blogusername">שם משתמש</string>
+ <string name="scaled_image">רוחב תמונה</string>
+ <string name="upload_scaled_image">העלאה וקישור לתמונה</string>
+ <string name="scheduled">תוזמן</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">מעלה...</string>
+ <string name="version">גרסה</string>
+ <string name="tos">תנאי שימוש</string>
+ <string name="app_title">וורדפרס עבור אנדרואיד</string>
+ <string name="max_thumbnail_px_width">ברירת מחדל לרוחב תמונה</string>
+ <string name="image_alignment">יישור</string>
+ <string name="refresh">רענון</string>
+ <string name="untitled">ללא כותרת</string>
+ <string name="edit">עריכה</string>
+ <string name="post_id">פוסט</string>
+ <string name="page_id">עמוד</string>
+ <string name="immediately">מיד</string>
+ <string name="post_password">ססמה (לא חובה)</string>
+ <string name="quickpress_add_alert_title">קבע שם מקוצר</string>
+ <string name="today">היום</string>
+ <string name="settings">הגדרות</string>
+ <string name="share_url">שתף כתובת קבועה</string>
+ <string name="quickpress_window_title">בחר אתר לקיצור דרך קוויקפרס</string>
+ <string name="quickpress_add_error">שם קיצור הדרך אינו יכול להיות ריק</string>
+ <string name="publish_post">פרסם</string>
+ <string name="draft">טיוטה</string>
+ <string name="post_private">פרטי</string>
+ <string name="upload_full_size_image">העלה וקשה לתמונה בגודל מלא</string>
+ <string name="categories">קטגוריות</string>
+ <string name="tags_separate_with_commas">תגים (יש להפריד בפסיקים)</string>
+ <string name="title">כותרת</string>
+ <string name="dlg_deleting_comments">מוחק תגובות</string>
+ <string name="notification_vibrate">רטט</string>
+ <string name="notification_blink">האר נורית הודעה</string>
+ <string name="notification_sound">צליל התראה</string>
+ <string name="status">סטטוס</string>
+ <string name="location">מיקום</string>
+ <string name="sdcard_title">דרוש כרטיס SD</string>
+ <string name="select_video">בחר וידאו מהגלריה</string>
+ <string name="media">מדיה</string>
+ <string name="delete">מחק</string>
+ <string name="none">ללא</string>
+ <string name="blogs">אתרים</string>
+ <string name="select_photo">בחר תמונה מהגלריה</string>
+ <string name="yes">כן</string>
+ <string name="no">לא</string>
+ <string name="add">הוסף</string>
+ <string name="save">שמור</string>
+ <string name="cancel">בטל</string>
+ <string name="error">שגיאה</string>
+ <string name="on">ב</string>
+ <string name="category_refresh_error">שגיאה ברענון קטגוריות</string>
+ <string name="reply">תגובה</string>
+ <string name="preview">תצוגה מקדימה</string>
+ <string name="notification_settings">הגדרות התראות</string>
+</resources>
diff --git a/WordPress/src/main/res/values-hi/strings.xml b/WordPress/src/main/res/values-hi/strings.xml
new file mode 100644
index 000000000..9d1cf7284
--- /dev/null
+++ b/WordPress/src/main/res/values-hi/strings.xml
@@ -0,0 +1,598 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="post_format_video">विडियो </string>
+ <string name="post_format_status">स्थिति</string>
+ <string name="theme_free">नि: शुल्क</string>
+ <string name="theme_all">सभी</string>
+ <string name="theme_premium">प्रीमियम</string>
+ <string name="post_format_standard">मानक</string>
+ <string name="post_format_chat">चैट</string>
+ <string name="post_format_gallery">गैलरी</string>
+ <string name="post_format_image">छवि</string>
+ <string name="post_format_link">कड़ी</string>
+ <string name="post_format_quote">उद्धरण</string>
+ <string name="post_format_aside">एक तरफ</string>
+ <string name="post_format_audio">ऑडियो</string>
+ <string name="notif_community">कम्युनिटी</string>
+ <string name="notif_suggestions">सुझाव</string>
+ <string name="site_settings_list_editor_summary_one">1 आइटम </string>
+ <string name="approve_auto">सभी उपयोगकर्ता</string>
+ <string name="days_quantity_one">1 दिन </string>
+ <string name="days_quantity_other">%d दिन</string>
+ <string name="web_address">वेब पता</string>
+ <string name="editor_page_title_placeholder">पृष्ठ शीर्षक</string>
+ <string name="editor_post_title_placeholder">पोस्ट शीर्षक</string>
+ <string name="email_address">ईमेल पता</string>
+ <string name="comment_status_all">सभी</string>
+ <string name="could_not_load_page">पृष्ठ लोड नहीं हो सका</string>
+ <string name="about_the_app">ऐप्प के बारे में</string>
+ <string name="search">खोजे</string>
+ <string name="remove">हटाये</string>
+ <string name="about_me">मेरे बारे में</string>
+ <string name="first_name">पहला नाम</string>
+ <string name="site_settings_privacy_private_summary">प्राइवेट</string>
+ <string name="site_settings_default_category_title">डिफ़ॉल्ट श्रेणी</string>
+ <string name="site_settings_tagline_title">टैगलाइन</string>
+ <string name="site_settings_title_title">साइट शीर्षक</string>
+ <string name="site_settings_this_device_header">यह डिवाइस</string>
+ <string name="comments">टिप्पणियां</string>
+ <string name="privacy">गोपनीयता</string>
+ <string name="never">कभी नहीं </string>
+ <string name="unknown">अज्ञात</string>
+ <string name="edit_media">मीडिया को संपादित करे</string>
+ <string name="media_details_copy_url">यूआरएल को कॉपी करे</string>
+ <string name="media_details_label_date_uploaded">अपलोड की गई</string>
+ <string name="could_not_load_theme">थीम लोड नहीं हो सका</string>
+ <string name="theme_by_author_prompt_append">द्वारा %1$s</string>
+ <string name="theme_support">सहायता</string>
+ <string name="theme_view">देखे</string>
+ <string name="theme_details">विवरण</string>
+ <string name="theme_activate">सक्रिय करे</string>
+ <string name="title_activity_theme_support">थीम</string>
+ <string name="active">सक्रिय</string>
+ <string name="support">सहायता</string>
+ <string name="date_range_start_date">प्रारंभ दिनांक</string>
+ <string name="details">विवरण</string>
+ <string name="current_theme">वर्तमान थीम</string>
+ <string name="stats_referrers_spam">स्पैम</string>
+ <string name="post_published">पोस्ट प्रकाशित किया गया</string>
+ <string name="post_updated">पोस्ट अपडेट किया गया</string>
+ <string name="media_uploaded_on">अपलोड की गई: %s</string>
+ <string name="reader_label_gap_marker">अधिक लेख लोड करे </string>
+ <string name="notifications_no_search_results">\'%s\' से किसी साइट का मेल नहीं हुआ </string>
+ <string name="search_sites">साइट्स खोजें</string>
+ <string name="unread">अपठित</string>
+ <string name="notifications_empty_view_reader">पाठक देखें</string>
+ <string name="notifications_empty_action_all">सक्रिय हों! जिन ब्लॉग का अनुगमन करते है उनके लेखों पर टिप्पणी करें </string>
+ <string name="notifications_empty_comments">कोई नई टिप्पणी नहीं है... अबतक।</string>
+ <string name="stats_widget_error_readd_widget">कृपया विजेट को निकाल कर दोबारा जोड़ें</string>
+ <string name="stats_widget_error_no_visible_blog">आंकड़े देखने के लिए एक ब्लॉग प्रत्यक्ष होना चाहिए</string>
+ <string name="add_media_permission_required">मीडिया डालने के लिए अनुमति जरुरी है|</string>
+ <string name="access_media_permission_required">मीडिया खोजने के लिए अनुमति जरुरी है|</string>
+ <string name="stats_enable_rest_api_in_jetpack">अपने आँकड़ों को देखने के लिए, जेटपैक में JSON API को सक्षम करें। </string>
+ <string name="error_open_list_from_notification">यह लेख किसी और साइट पर पोस्ट हो चुका है|</string>
+ <string name="reader_short_comment_count_multi">%s टिप्पणियाँ</string>
+ <string name="reader_short_comment_count_one">1 टिप्पणी</string>
+ <string name="reader_label_submit_comment">भेजें</string>
+ <string name="reader_hint_comment_on_post">लेख का उत्तर दें...</string>
+ <string name="reader_discover_visit_blog">यात्रा %s</string>
+ <string name="reader_short_like_count_multi">%s पसंद</string>
+ <string name="reader_short_like_count_one">1 पसंद</string>
+ <string name="reader_short_like_count_none">पसंद</string>
+ <string name="reader_label_follow_count">%,d अनुयायी</string>
+ <string name="reader_title_post_detail">पाठक लेख</string>
+ <string name="local_changes_explainer">इस लेख में स्थानीय परिवर्तन हुआ है, जो प्रकाशित नहीं हुआ है।</string>
+ <string name="notifications_push_summary">अपने उपकरण में सूचना हेतु व्यवस्था।</string>
+ <string name="notification_types">अधिसूचना के प्रकार</string>
+ <string name="replies_to_your_comments">मेरी टिप्पणियों के उत्तर</string>
+ <string name="notifications_tab">अधिसूचना सूचि</string>
+ <string name="email">ईमेल</string>
+ <string name="app_notifications">एप्लिकेशन अधिसूची</string>
+ <string name="notifications_other">अन्य</string>
+ <string name="notifications_account_emails">Wordpress.com की ओर से ईमेल</string>
+ <string name="your_sites">आपकी साइटे</string>
+ <string name="button_revert">उलट दें</string>
+ <string name="days_ago">%d दिन पहले</string>
+ <string name="yesterday">कल</string>
+ <string name="button_back">पीछे</string>
+ <string name="page_deleted">पृष्ठ हट गया</string>
+ <string name="button_preview">पूर्वावलोकन</string>
+ <string name="button_view">देखें</string>
+ <string name="button_publish">प्रकाशित करें</string>
+ <string name="button_edit">संपादित करें</string>
+ <string name="post_trashed">पोस्ट ट्रैश में भेज दिया गया</string>
+ <string name="page_trashed">पृष्ठ ट्रैश में भेज दिया गया</string>
+ <string name="button_trash">ट्रैश</string>
+ <string name="button_stats">आकड़े</string>
+ <string name="post_deleted">पोस्ट नष्ट कर दिया गया</string>
+ <string name="my_site_no_sites_view_subtitle">क्या आप एक जोड़ना चाहेंगे?</string>
+ <string name="reader_toast_err_follow_blog_not_found">इस ब्लॉग को पाया नहीं जा सका</string>
+ <string name="tabbar_accessibility_label_my_site">मेरा साईट</string>
+ <string name="tabbar_accessibility_label_me">मैं </string>
+ <string name="editor_toast_changes_saved">परिवर्तन सहेजा गया</string>
+ <string name="ignore">ध्यान न दें</string>
+ <string name="stats_insights_most_popular_hour">सबसे लोकप्रिय घण्टा</string>
+ <string name="stats_insights_most_popular_day">सबसे लोकप्रिय दिन</string>
+ <string name="stats_insights_popular">सबसे लोकप्रिय दिन और घण्टा</string>
+ <string name="stats_insights_today">आज के आँकड़े</string>
+ <string name="me_btn_support">मदद और सहायता</string>
+ <string name="site_picker_add_self_hosted">सेल्फ-होस्टेड साइट डाले </string>
+ <string name="site_picker_edit_visibility">साइट्स दिखाए या छिपाये</string>
+ <string name="site_picker_add_site">साईट जोड़े</string>
+ <string name="my_site_btn_view_site">साईट पर जाये</string>
+ <string name="my_site_btn_switch_site">साइट स्विच करे</string>
+ <string name="my_site_header_publish">प्रकाशित</string>
+ <string name="my_site_btn_blog_posts">ब्लॉग पोस्ट्स</string>
+ <string name="my_site_btn_site_settings">सेटिंग्स</string>
+ <string name="stats_unknown_author">अज्ञात लेखक</string>
+ <string name="show">दिखाएँ</string>
+ <string name="hide">छुपाएँ</string>
+ <string name="select_all">सभी चुनें</string>
+ <string name="loading_blog_images">छवियाँ लाई जा रही है</string>
+ <string name="stats_generic_error">आवश्यक सांख्यिकी लोड करने में असफल</string>
+ <string name="error_loading_videos">वीडियो लाने में असमर्थ</string>
+ <string name="no_media_sources">मीडिया लाने में असफल</string>
+ <string name="error_loading_blog_videos">वीडियो लाने में असमर्थ</string>
+ <string name="error_loading_images">छवियाँ लाने में असमर्थ</string>
+ <string name="error_loading_blog_images">छवियाँ लाने में असमर्थ</string>
+ <string name="no_blog_videos">कोई वीडियो नहीं</string>
+ <string name="no_device_images">कोई चित्र नहीं</string>
+ <string name="no_blog_images">कोई चित्र नहीं</string>
+ <string name="loading_blog_videos">वीडियोलाये जा रहे है</string>
+ <string name="no_device_videos">कोई वीडियो नहीं</string>
+ <string name="loading_videos">वीडियो लोड हो रहे है</string>
+ <string name="loading_images">छवियाँ लोड हो रही है</string>
+ <string name="no_media">मीडिया नहीं</string>
+ <string name="device">यंत्र</string>
+ <string name="language">भाषा</string>
+ <string name="add_to_post">पोस्ट में जोड़ें</string>
+ <string name="verify">सत्यापित करें</string>
+ <string name="verification_code">सत्यापन कोड</string>
+ <string name="take_video">वीडियो बनाए</string>
+ <string name="take_photo">फोटो खींचे</string>
+ <string name="tab_title_site_videos">साइट वीडियो</string>
+ <string name="tab_title_site_images">साइट की छवियां</string>
+ <string name="pages_fetching">पृष्ठ ला रहे है...</string>
+ <string name="publisher">प्रकाशक:</string>
+ <string name="stats_view_authors">लेखक</string>
+ <string name="stats_search_terms_unknown_search_terms">अज्ञात खोज शब्द</string>
+ <string name="stats_total">टोटल</string>
+ <string name="reader_empty_posts_in_blog">यह ब्लॉग रिक्त है</string>
+ <string name="stats_view_videos">वीडियो</string>
+ <string name="stats_empty_geoviews">कोई देश दर्ज नहीं की गई</string>
+ <string name="stats_followers_days">%1$d दिन</string>
+ <string name="stats_followers_hours">%1$d घंटा</string>
+ <string name="stats_followers_minutes">%1$d मिनट</string>
+ <string name="stats_followers_months">%1$d महीने</string>
+ <string name="stats_followers_years">%1$d वर्ष</string>
+ <string name="stats_followers_a_day">एक दिन</string>
+ <string name="stats_followers_a_minute_ago">एक दिन पहले</string>
+ <string name="stats_followers_a_month">एक महीना</string>
+ <string name="stats_followers_a_year">एक साल</string>
+ <string name="stats_followers_email_selector">ई-मेल</string>
+ <string name="stats_followers_an_hour_ago">एक घंटा पहले</string>
+ <string name="stats_entry_top_commenter">लेखक</string>
+ <string name="stats_entry_followers">प्रसंशक</string>
+ <string name="stats_totals_publicize">प्रसंशक</string>
+ <string name="stats_view_followers">प्रसंशक</string>
+ <string name="stats_timeframe_years">वर्ष </string>
+ <string name="stats_entry_video_plays">वीडियो </string>
+ <string name="stats_view">देखे</string>
+ <string name="stats_view_countries">देश</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="ssl_certificate_details">विवरण</string>
+ <string name="delete_sure_post">इस लेख को हटाओ</string>
+ <string name="cab_selected">%d चयनित</string>
+ <string name="delete_sure_page">इस पृष्ठ को हटाएं</string>
+ <string name="delete_sure">इस प्रारूप को हटाएं</string>
+ <string name="reader_label_comment_count_single">एक टिप्पणी</string>
+ <string name="reader_label_comments_closed">टिप्पणियाँ बंद हैं</string>
+ <string name="signing_out">भाग रद्द करना</string>
+ <string name="reader_label_comment_count_multi">%,d टिप्पणियां</string>
+ <string name="reader_title_photo_viewer">%1$d का %2$d</string>
+ <string name="comment">टिप्पणी</string>
+ <string name="comment_trashed">टिप्पणी ट्रैश किया गया</string>
+ <string name="reader_label_comments_on">पर टिप्पणियाँ</string>
+ <string name="reader_menu_block_blog">इस ब्लॉग को ब्लॉक करे</string>
+ <string name="reader_toast_err_block_blog">इस ब्लॉग को ब्लॉक करने में असमर्थ</string>
+ <string name="reader_toast_err_generic">इस कार्य को निष्पादित करने में असमर्थ</string>
+ <string name="reader_toast_blog_blocked">इस ब्लॉग से पोस्ट अब नहीं दिखाया जाएगा</string>
+ <string name="hs__new_conversation_header">सहायता चैट</string>
+ <string name="hs__conversation_header">सहायता चैट</string>
+ <string name="hs__username_blank_error">एक मान्य नाम दर्ज करें</string>
+ <string name="hs__invalid_email_error">मान्य ईमेल पता दर्ज करें</string>
+ <string name="hs__conversation_detail_error">आप जो समस्या देख रहे है उसका वर्णन करें</string>
+ <string name="contact_us">हमसे संपर्क करें</string>
+ <string name="add_location">स्थान जोड़ें</string>
+ <string name="current_location">वर्तमान स्थान</string>
+ <string name="search_location">खोज</string>
+ <string name="edit_location">संपादित करें</string>
+ <string name="search_current_location">स्थान निर्धारण करे</string>
+ <string name="preference_send_usage_stats">आंकड़े भेजें</string>
+ <string name="preference_send_usage_stats_summary">स्वचालित रूप से उपयोग के आंकड़े भेज कर हमें एंड्रॉयड के लिए वर्डप्रेस को बेहतर बनाने में मदद करे\n\n</string>
+ <string name="update_verb">अपडेट करे</string>
+ <string name="schedule_verb">निर्धारित करे</string>
+ <string name="reader_label_followed_blog">अनुसरण किये गए ब्लॉग</string>
+ <string name="reader_toast_err_already_follow_blog">आप पहले से ही इस ब्लॉग का अनुसरण कर रहे है</string>
+ <string name="reader_title_blog_preview">ब्लॉग पूर्वावलोकन</string>
+ <string name="reader_title_tag_preview">टैग पूर्वावलोकन</string>
+ <string name="reader_title_subs">टैग और ब्लॉग</string>
+ <string name="reader_toast_err_follow_blog">इस ब्लॉग का अनुसरण करने में असमर्थ</string>
+ <string name="reader_toast_err_get_blog_info">इस ब्लॉग को दिखाने में असमर्थ</string>
+ <string name="reader_toast_err_unfollow_blog">इस ब्लॉग अनुसरण रद्द करने में असमर्थ</string>
+ <string name="reader_empty_recommended_blogs">कोई अनुशंसित ब्लॉग नहीं</string>
+ <string name="reader_hint_add_tag_or_url">अनुसरण के लिए एक टैग या यूआरएल दर्ज करें</string>
+ <string name="reader_page_followed_blogs">अनुसरण किये गए ब्लॉग </string>
+ <string name="reader_page_followed_tags">अनुसरण किये गए टैग्स </string>
+ <string name="reader_label_tag_preview">%s पोस्ट को टैग किया गया</string>
+ <string name="media_empty_list">कोई मीडिया नहीं</string>
+ <string name="saving">सहेजा जा रहा…</string>
+ <string name="ptr_tip_message">सुझाव: रिफ्रेश करने के लिए नीचे खींचे</string>
+ <string name="help_center">सहायता केंद्र</string>
+ <string name="help">सहायता</string>
+ <string name="ssl_certificate_error">अमान्य एसएसएल प्रमाणपत्र</string>
+ <string name="forums">फोरम</string>
+ <string name="forgot_password">आपका पासवर्ड खो गया?</string>
+ <string name="ssl_certificate_ask_trust">आप आमतौर पर समस्याओं के बिना इस साइट से कनेक्ट करते हैं, तो यह त्रुटि किसी साइट को प्रतिरूपित करने की कोशिश कर रहा है कि अर्थ हो सकता है, और यदि आप जारी नहीं किया जाना चाहिए. क्या आप फिर भी प्रमाण पत्र पर विश्वास करना चाहेंगे?</string>
+ <string name="out_of_memory">डिवाइस की मेमोरी समाप्त</string>
+ <string name="blog_name_required">साइट का पता दर्ज करें</string>
+ <string name="username_required">उपयोगकर्ता नाम दर्ज करें</string>
+ <string name="email_invalid">मान्य ईमेल पता दर्ज करें</string>
+ <string name="error_generic">एक त्रुटि हुई</string>
+ <string name="adding_cat_success">श्रेणी सफलतापूर्वक जोड़ा गया</string>
+ <string name="could_not_remove_account">ब्लॉग हटाया नहीं जा सका</string>
+ <string name="error_downloading_image">छवि डाउनलोड करते समय त्रुटि </string>
+ <string name="comments_empty_list">कोई टिप्पणी नहीं</string>
+ <string name="stats_empty_comments">अभी तक कोई टिप्पणी नहीं</string>
+ <string name="notifications_empty_list">कोई सूचनाएं नहीं</string>
+ <string name="mnu_comment_unspam">स्पैम नहीं</string>
+ <string name="invalid_password_message">कूटशब्द कम से कम 4 अक्षर का चाहिए</string>
+ <string name="no_network_message">नेटवर्क उपलब्ध नहीं है </string>
+ <string name="stats_bar_graph_empty">कोई आंकड़े उपलब्ध नहीं</string>
+ <string name="invalid_email_message">आपका ईमेल पता मान्य नहीं है</string>
+ <string name="adding_cat_failed">श्रेणी जोड़ना असफल हुआ</string>
+ <string name="reply_failed">जवाब असफल हुआ</string>
+ <string name="nux_cannot_log_in">हम लॉग इन नहीं कर सकते</string>
+ <string name="error_load_comment">टिप्पणी लोड नहीं हो सका</string>
+ <string name="theme_set_failed">थीम को सेट करने में असफल</string>
+ <string name="passcode_wrong_passcode">गलत पिन</string>
+ <string name="wait_until_upload_completes">अपलोड पूर्ण होने तक प्रतीक्षा करे</string>
+ <string name="username_not_allowed">उपयोगकर्ता नाम की अनुमति नहीं</string>
+ <string name="username_or_password_incorrect">आपके द्वारा दर्ज उपयोगकर्ता नाम या कूटशब्द गलत है</string>
+ <string name="gallery_error">मीडिया आइटम पुनः प्राप्त नहीं किया जा सका</string>
+ <string name="error_delete_post">%s को हटाने के दौरान एक त्रुटि हुई</string>
+ <string name="error_edit_comment">टिप्पणी के संपादन के दौरान एक त्रुटि हुई</string>
+ <string name="error_upload">%s को अपलोड करने के दौरान एक त्रुटि हुई</string>
+ <string name="blog_name_cant_be_used">आप उस साइट पते का उपयोग नहीं कर सकते</string>
+ <string name="invalid_url_message">दर्ज किया गये ब्लॉग यूआरएल की वैधता की जाँच करे</string>
+ <string name="error_refresh_comments">टिप्पणिया इस समय रिफ्रेश नहीं हो सकती</string>
+ <string name="error_refresh_pages">पृष्ठे इस समय रिफ्रेश नहीं हो सकती</string>
+ <string name="theme_fetch_failed">थीम को प्राप्त करने में असफल</string>
+ <string name="cat_name_required">श्रेणी नाम फ़ील्ड आवश्यक है</string>
+ <string name="username_exists">उपयोगकर्ता नाम पहले से मौजूद है</string>
+ <string name="blog_name_reserved">वह साइट आरक्षित है</string>
+ <string name="blog_name_exists">वह साइट पहले से मौजूद है</string>
+ <string name="blog_name_not_allowed">उस साइट पते की अनुमति नहीं है</string>
+ <string name="email_not_allowed">उस ईमेल पते की अनुमति नहीं है</string>
+ <string name="email_exists">वह ईमेल पता पहले ही से प्रयोग में है</string>
+ <string name="error_refresh_stats">आँकड़े को इस समय रिफ्रेश नहीं किया जा सकता</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">साइट का पता केवल छोटे अक्षरों (a-z) और अंक हो सकते हैं</string>
+ <string name="blog_name_contains_invalid_characters">साइट के पते में "_" अक्षर नहीं हो सकता है</string>
+ <string name="blog_name_must_be_at_least_four_characters">साइट का पता कम से कम 4 अक्षरों का होना चाहिए</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">साइट का पता 64 अक्षर से कम होना चाहिए</string>
+ <string name="username_only_lowercase_letters_and_numbers">उपयोगकर्ता नाम केवल छोटे अक्षरों (a-z) और अंक हो सकते हैं</string>
+ <string name="username_contains_invalid_characters">उपयोगकर्ता नाम में "_" अक्षर नहीं हो सकता है</string>
+ <string name="username_must_be_at_least_four_characters">उपयोगकर्ता नाम कम से कम 4 अक्षरों का होना चाहिए</string>
+ <string name="invalid_username_too_short">उपयोगकर्ता नाम 4 अक्षरों से अधिक समय होना चाहिए</string>
+ <string name="invalid_username_too_long">उपयोगकर्ता नाम 61 अक्षरो से कम होना चाहिए</string>
+ <string name="username_must_include_letters">उपयोगकर्ता नाम एक से कम 1 अक्षर (a-z) का होना आवश्यक है</string>
+ <string name="blog_name_reserved_but_may_be_available">वह साइट वर्तमान में आरक्षित है, लेकिन एक दो दिन में उपलब्ध हो सकता है</string>
+ <string name="username_reserved_but_may_be_available">वह उपयोगकर्ता नाम वर्तमान में आरक्षित है, लेकिन एक दो दिन में उपलब्ध हो सकता है</string>
+ <string name="blog_not_found">इस ब्लॉग से पहुँच स्थापित करते समय ​​एक त्रुटि हुई</string>
+ <string name="no_site_error">वर्डप्रेस साइट से संपर्क स्थापित नहीं सका</string>
+ <string name="error_refresh_notifications">सूचनाएं को इस समय रिफ्रेश नहीं किया जा सकता</string>
+ <string name="error_refresh_posts">पोस्ट को इस समय रिफ्रेश नहीं किया जा सकता</string>
+ <string name="theme_auth_error_message">यह सुनिश्चित करे की आपको थीमस निर्धारित करने का विशेषाधिकार है</string>
+ <string name="category_automatically_renamed">श्रेणी नाम %1$s मान्य नहीं है.यह %2$s में बदल दिया गया है.</string>
+ <string name="error_moderate_comment">मध्यस्थता करते हुए त्रुटि हुई</string>
+ <string name="no_account">कोई वर्डप्रेस खाता नहीं है,एक खाता जोड़े और फिर से प्रयास करे</string>
+ <string name="sdcard_message">मीडिया अपलोड करने के लिए एक माउंटेड एसडी कार्ड की आवश्यकता है</string>
+ <string name="add_comment">टिप्पणी जोड़ें</string>
+ <string name="add_new_category">नई श्रेणी जोड़ें</string>
+ <string name="hint_comment_content">टिप्पणी</string>
+ <string name="comment_added">टिप्पणी सफलतापूर्वक जोड़ा गया</string>
+ <string name="content_required">टिप्पणी आवश्यक है</string>
+ <string name="view_site">साइट देखें</string>
+ <string name="author_email">लेखक ईमेल</string>
+ <string name="author_name">लेखक का नाम</string>
+ <string name="author_url">लेखक यूआरएल</string>
+ <string name="http_authorization_required">प्रमाणीकरण आवश्यक</string>
+ <string name="add_account_blog_url">ब्लॉग का पता</string>
+ <string name="delete_post">पोस्ट हटाये</string>
+ <string name="edit_comment">टिप्पणी संपादित करें</string>
+ <string name="media_gallery_edit">गैलरी संपादित करें</string>
+ <string name="edit_post">पोस्ट संपादित करें</string>
+ <string name="cancel_edit">संपादन रद्द करें</string>
+ <string name="category_name">श्रेणी नाम</string>
+ <string name="delete_page">पृष्ठ हटाये</string>
+ <string name="account_details">खाता विवरण</string>
+ <string name="theme_current_theme">वर्तमान थीम</string>
+ <string name="email_hint">ईमेल पता</string>
+ <string name="media_gallery_settings_title">गैलरी सेटिंग्स</string>
+ <string name="category_desc">श्रेणी विवरण (वैकल्पिक)</string>
+ <string name="create_a_link">एक कड़ी बनाएं</string>
+ <string name="media_gallery_image_order">छवि के क्रम</string>
+ <string name="image_settings">छवि सेटिंग्स</string>
+ <string name="blog_name_invalid">अमान्य साइट का पता</string>
+ <string name="blog_title_invalid">अमान्य साइट शीर्षक</string>
+ <string name="learn_more">और जानें</string>
+ <string name="link_enter_url_text">कड़ी शब्द (वैकल्पिक)</string>
+ <string name="dlg_spamming_comments">स्पैम के रूप में चिन्हित</string>
+ <string name="new_media">नया मीडिया</string>
+ <string name="share_action_post">नया पोस्ट</string>
+ <string name="new_post">नया पोस्ट</string>
+ <string name="open_source_licenses">मुक्त स्रोत लाइसेंस</string>
+ <string name="page_settings">पृष्ठ सेटिंग्स</string>
+ <string name="comment_status_unapproved">विचाराधीन</string>
+ <string name="pending_review">समीक्षा विचाराधीन है</string>
+ <string name="post_format">पोस्ट प्रारूप</string>
+ <string name="post_settings">पोस्ट सेटिंग्स</string>
+ <string name="post_not_published">पोस्ट की स्थिति प्रकाशित नहीं है</string>
+ <string name="theme_premium_theme">प्रीमियम थीम</string>
+ <string name="preview_page">पूर्वावलोकन पृष्ठ</string>
+ <string name="preview_post">पूर्वावलोकन पोस्ट</string>
+ <string name="privacy_policy">गोपनीयता नीति</string>
+ <string name="reader_share_link">कड़ी साझा करे</string>
+ <string name="comment_status_spam">स्पैम</string>
+ <string name="mnu_comment_spam">स्पैम</string>
+ <string name="wordpress_blog">वर्डप्रेस ब्लॉग</string>
+ <string name="view_in_browser">ब्राउज़र में देखें</string>
+ <string name="delete_draft">प्रारूप हटाये</string>
+ <string name="mnu_comment_trash">कचरा पेटी</string>
+ <string name="trash_yes">कचरा पेटी</string>
+ <string name="trash">कचरा पेटी</string>
+ <string name="comment_status_trash">कचरा पेटी में डाला गया</string>
+ <string name="trash_no">कचरा पेटी में नहीं डाले</string>
+ <string name="upload_failed">अपलोड असफल हो गया</string>
+ <string name="location_not_found">अज्ञात स्थान</string>
+ <string name="reader_toast_err_add_tag">इस टैग को जोड़ने में असमर्थ</string>
+ <string name="reader_toast_err_remove_tag">इस टैग को हटाने में असमर्थ</string>
+ <string name="mnu_comment_unapprove">अस्वीकृत</string>
+ <string name="dlg_unapproving_comments">अस्वीकृत किया जा रहा</string>
+ <string name="mnu_comment_approve">स्वीकृत करे</string>
+ <string name="comment_status_approved">स्वीकृत किया गया</string>
+ <string name="dlg_approving_comments">स्वीकृत किया जा रहा</string>
+ <string name="connection_error">कनेक्शन त्रुटि</string>
+ <string name="media_error_no_permission">आपको मीडिया समूह देखने की अनुमति नहीं है</string>
+ <string name="select_categories">श्रेणियों को चुने</string>
+ <string name="share_action_media">मीडिया संग्रह</string>
+ <string name="required_field">आवश्यक फ़ील्ड</string>
+ <string name="saving_changes">परिवर्तन सहेजे जा रहा</string>
+ <string name="site_address">आपका अपने से होस्ट किया हुआ पता(यूआरएल)</string>
+ <string name="toast_comment_unedited">टिप्पणी नहीं बदला है</string>
+ <string name="media_gallery_num_columns">कॉलम की संख्या</string>
+ <string name="page_not_published">पृष्ठ स्थिति प्रकाशित नहीं है</string>
+ <string name="dlg_confirm_trash_comments">कचरा पेटी में भेजें?</string>
+ <string name="dlg_trashing_comments">कचरा पेटी में भेजा जा रहा</string>
+ <string name="sure_to_cancel_edit_comment">इस टिप्पणी का संपादन रद्द करें?</string>
+ <string name="blog_name_must_include_letters">साइट के पते में कम से कम 1 अक्षर (a-z) होना आवश्यक है</string>
+ <string name="cannot_delete_multi_media_items">कुछ मीडिया को इस समय हटाया नहीं जा सकता है. बाद में पुन: प्रयास करें.</string>
+ <string name="media_gallery_type_thumbnail_grid">लघुछवि ग्रिड</string>
+ <string name="post_not_found">पोस्ट लोड करते समय एक त्रुटि हुई.पोस्ट को रिफ्रेश करे और फिर से प्रयास करें</string>
+ <string name="horizontal_alignment">क्षैतिज संरेखण</string>
+ <string name="local_changes">स्थानीय परिवर्तन</string>
+ <string name="local_draft">स्थानीय प्रारूप</string>
+ <string name="category_parent">श्रेणी जनक (वैकल्पिक):</string>
+ <string name="category_slug">श्रेणी स्लग (वैकल्पिक)</string>
+ <string name="scaled_image_error">एक वैध बढ़ाया चौड़ाई का मान दर्ज करें</string>
+ <string name="error_blog_hidden">यह ब्लॉग छिपा हुआ है और लोड नहीं किया जा सकता है. सेटिंग्स में इसे फिर से सक्रिय करे और फिर कोशिश करें.</string>
+ <string name="file_error_create">मीडिया अपलोड करने के लिए अस्थायी फ़ाइल नहीं बनाया जा सका. आपके डिवाइस पर पर्याप्त खाली स्थान है सुनिश्चित करें.</string>
+ <string name="file_not_found">अपलोड करने के लिए मीडिया फाइल नहीं मिल सका. उसे हटाया या स्थानांतरित किया गया?</string>
+ <string name="fatal_db_error">एप्लिकेशन बनाते हुए त्रुटि हुई. एप्लिकेशन को फिर से इनस्टॉल करने का प्रयास करें.</string>
+ <string name="themes_live_preview">लाइव पूर्वावलोकन</string>
+ <string name="reader_title_applog">एप्लीकेशन लॉग</string>
+ <string name="xmlrpc_error">कनेक्ट नहीं हो सका.अपने साईट पर xmlrpc.php का पूरा पथ दर्ज करें और पुन: प्रयास करे</string>
+ <string name="jetpack_message_not_admin">Jetpack प्लगइन आँकड़ों के लिए आवश्यक है. साइट व्यवस्थापक से संपर्क करें.</string>
+ <string name="http_credentials">HTTP क्रेडेंशियल्स (वैकल्पिक)</string>
+ <string name="email_cant_be_used_to_signup">आप पंजीकरण करने के लिए कि उस ईमेल पते का उपयोग नहीं कर सकते हैं. हमें उनके साथ समस्या हो रही है क्योंकि वे हमारे ईमेल को ब्लॉक कर रहे है. एक अन्य ईमेल प्रदाता का प्रयोग करें.</string>
+ <string name="email_reserved">यह ईमेल पता पहले से ही इस्तेमाल में है. सक्रियण ईमेल के लिए अपने इनबॉक्स की जाँच करें. यदि आप सक्रिय नहीं करेंगे, तो आप कुछ दिनों में फिर से कोशिश कर सकते हैं.</string>
+ <string name="remove_account">ब्लॉग हटाये</string>
+ <string name="blog_removed_successfully">ब्लॉग सफलतापूर्वक हटा दिया गया</string>
+ <string name="deleting_page">पृष्ठ हटाया जा रहा है</string>
+ <string name="deleting_post">पोस्ट हटाया जा रहा है</string>
+ <string name="share_link">कड़ी साझा करे</string>
+ <string name="share_url_page">पृष्ठ साझा करे</string>
+ <string name="share_url_post">पोस्ट साझा करे</string>
+ <string name="creating_your_account">आपका खाता बनाया जा रहा</string>
+ <string name="creating_your_site">आपकी साइट बनाया जा रहा</string>
+ <string name="reader_empty_posts_in_tag_updating">पोस्ट्स को प्राप्त किया जा रहा...</string>
+ <string name="error_refresh_media">मीडिया लाइब्रेरी को रिफ्रेश करते समय कुछ गलत हो गया. बाद में पुन: प्रयास करें.</string>
+ <string name="video">वीडियो</string>
+ <string name="download">मीडिया डाउनलोड हो रहा</string>
+ <string name="reader_label_reply">जवाब दें</string>
+ <string name="reader_toast_err_get_comment">इस टिप्पणी को पुनः प्राप्त में असमर्थ</string>
+ <string name="reader_likes_multi">%,d लोगों ने पसंद किया</string>
+ <string name="reader_likes_you_and_multi">आपने और %,d लोगो ने इसे पसंद किया है</string>
+ <string name="cant_share_no_visible_blog"> आप एक प्रत्यक्ष ब्लॉग के बिना वर्डप्रेस में साझा नहीं कर सकते</string>
+ <string name="comment_spammed">टिप्पणी स्पैम के रूप में चिह्नित </string>
+ <string name="reader_toast_err_get_post">इस संदेश को पुनः प्राप्त में असमर्थ</string>
+ <string name="reader_likes_you_and_one">आपने और एक और ने इसे पसंद किया है</string>
+ <string name="validating_user_data">उपयोगकर्ता डेटा को सत्यापित किया जा रहा</string>
+ <string name="validating_site_data">साईट डेटा को सत्यापित किया जा रहा</string>
+ <string name="select_date">तिथि को चुने</string>
+ <string name="pick_photo">तस्वीर को चुने</string>
+ <string name="select_time">समय चुने</string>
+ <string name="pick_video">विडियो को चुने</string>
+ <string name="account_two_step_auth_enabled">इस खाते में दो स्टेप प्रमाणीकरण सक्षम किया गया है. WordPress.com पर अपनी सुरक्षा सेटिंग में जाएं और एक ऐप्लिकेशन विशिष्ट पासवर्ड उत्पन्न करे.</string>
+ <string name="nux_tap_continue">जारी रखे</string>
+ <string name="nux_welcome_create_account">खाता बनाएं</string>
+ <string name="nux_oops_not_selfhosted_blog">वर्डप्रेस डॉट कॉम पर साइन इन करे</string>
+ <string name="nux_add_selfhosted_blog">अपने से होस्ट किया हुआ साईट जोड़े</string>
+ <string name="password_invalid">आपको एक अधिक सुरक्षित पासवर्ड की जरूरत है. सुनिश्चित करें की आप 7 या अधिक वर्णों ,बड़े और छोटे अक्षरों, संख्या या विशेष वर्ण मिश्रण का उपयोग कर रहे है.</string>
+ <string name="reader_untitled_post">(शीर्षक रहित)</string>
+ <string name="reader_label_added_tag">जोड़ा गया %s</string>
+ <string name="reader_toast_err_comment_failed">आपकी टिप्पणी पोस्ट नहीं कर सके</string>
+ <string name="media_add_new_media_gallery">गैलरी बनाएं</string>
+ <string name="username_invalid">अमान्य उपयोगकर्ता नाम</string>
+ <string name="jetpack_not_found">जेटपैक प्लगइन नहीं मिला</string>
+ <string name="reader_likes_one">एक व्यक्ति ने पसंद किया</string>
+ <string name="reader_btn_share">साझा करे</string>
+ <string name="reader_likes_only_you">आपने इसे पसंद किया</string>
+ <string name="reader_btn_follow">अनुसरण करे</string>
+ <string name="reader_btn_unfollow">अनुसरण कर रहे</string>
+ <string name="reader_label_removed_tag">हटाये गए %s</string>
+ <string name="reader_toast_err_url_intent">खोलने में असमर्थ %s</string>
+ <string name="reader_toast_err_share_intent">साझा करने में असमर्थ</string>
+ <string name="reader_toast_err_view_image">छवि को देखने में असमर्थ</string>
+ <string name="media_add_popup_title">मीडिया संग्रह में जोड़े</string>
+ <string name="reader_toast_err_tag_exists">आप पहले से ही इस टैग का अनुसरण कर रहे है</string>
+ <string name="select_from_media_library">मीडिया लाइब्रेरी से चुने </string>
+ <string name="reader_share_subject">%s द्वारा साझा किया गया</string>
+ <string name="empty_list_default">इस सूची खाली है</string>
+ <string name="nux_tutorial_get_started_title">शुरू हो जाये!</string>
+ <string name="reader_toast_err_tag_invalid">वह एक मान्य टैग नहीं है</string>
+ <string name="connecting_wpcom">WordPress.com से कनेक्ट हो रहा</string>
+ <string name="jetpack_message">Jetpack प्लगइन आँकड़ों के लिए आवश्यक है. आप Jetpack इनस्टॉल करना चाहते हैं?</string>
+ <string name="limit_reached">सीमा पहुँच गयी. आप 1 मिनट में फिर से कोशिश कर सकते हैं. उसके पहले कोशिश करने पर आपके प्रतिबंध हटाने के इन्तजार समय में वृद्धि होगी. आपको लगता है यह त्रुटी है तो, सहायता से संपर्क करे.</string>
+ <string name="reader_hint_comment_on_comment">टिप्पणी का जवाब दे...</string>
+ <string name="create_account_wpcom">Wordpress.com पे अकाउंट बनाये|</string>
+ <string name="all">सभी</string>
+ <string name="stats_entry_country">देश</string>
+ <string name="stats_entry_authors">लेखक</string>
+ <string name="media_edit_title_text">शीर्षक</string>
+ <string name="stats_entry_posts_and_pages">शीर्षक</string>
+ <string name="stats_timeframe_today">आज</string>
+ <string name="stats_timeframe_days">दिन</string>
+ <string name="media_edit_description_text">विवरण</string>
+ <string name="themes_details_label">विवरण</string>
+ <string name="theme_activate_button">सक्रिय करें</string>
+ <string name="theme_activating_button">सक्रिय किया जा रहा</string>
+ <string name="media_edit_description_hint">यहा विवरण दर्ज करें</string>
+ <string name="media_edit_title_hint">यहा शीर्षक दर्ज करें</string>
+ <string name="media_gallery_type_circles">वृत्त</string>
+ <string name="stats_view_clicks">क्लिक</string>
+ <string name="themes_features_label">विशेषताएँ</string>
+ <string name="images">छविया</string>
+ <string name="stats_timeframe_months">महीने</string>
+ <string name="share_action">साझा करे</string>
+ <string name="themes">थीम्स</string>
+ <string name="stats_totals_clicks">क्लिक</string>
+ <string name="media_gallery_image_order_random">क्रमरहित</string>
+ <string name="stats_timeframe_weeks">सप्ताह</string>
+ <string name="stats_timeframe_yesterday">कल</string>
+ <string name="media_edit_success">अपडेट किया गया</string>
+ <string name="share_action_title">में जोड़े …</string>
+ <string name="media_edit_failure">अपडेट करने में असफल</string>
+ <string name="media_gallery_type">प्रकार</string>
+ <string name="stats_entry_tags_and_categories">विषय</string>
+ <string name="media_gallery_image_order_reverse">उल्टा</string>
+ <string name="stats_totals_views">देखें गये</string>
+ <string name="passcode_change_passcode">पिन बदलें</string>
+ <string name="theme_auth_error_title">थीम को प्राप्त करने में असफल</string>
+ <string name="stats">आँकड़े</string>
+ <string name="theme_set_success">थीम सफलतापूर्वक सेट किया गया!</string>
+ <string name="media_gallery_type_squares">वर्गाकार</string>
+ <string name="media_gallery_type_slideshow">स्लाइड शो</string>
+ <string name="media_edit_caption_text">शीर्षक</string>
+ <string name="media_edit_caption_hint">यहा शीर्षक दर्ज करें</string>
+ <string name="passcode_enter_old_passcode">अपना पुराना पिन दर्ज करें</string>
+ <string name="passcode_enter_passcode">अपना पिन दर्ज करें</string>
+ <string name="unattached">असंबद्ध</string>
+ <string name="stats_view_visitors_and_views">दर्शक और देखें गये</string>
+ <string name="stats_entry_referrers">रेफ़रर</string>
+ <string name="stats_view_referrers">रेफ़ररस</string>
+ <string name="passcode_set">पिन निर्धारित करें</string>
+ <string name="stats_totals_plays">चलाया गया</string>
+ <string name="custom_date">कस्टम तिथि</string>
+ <string name="post_excerpt">अंश</string>
+ <string name="media_add_popup_capture_photo">छवि कैप्चर करे</string>
+ <string name="media_add_popup_capture_video">विडियो कैप्चर करे</string>
+ <string name="passcode_turn_off">पिन को ऑफ करे</string>
+ <string name="passcode_turn_on">पिन को ऑन करे</string>
+ <string name="passcode_manage">पिन का प्रबंधन करे</string>
+ <string name="passcode_preference_title">पिन लॉक</string>
+ <string name="media_gallery_type_tiled">टाइल किया हुआ</string>
+ <string name="passcode_re_enter_passcode">अपना पिन फिर से दर्ज करें</string>
+ <string name="stats_view_tags_and_categories">टैग और श्रेणियाँ</string>
+ <string name="upload">अपलोड</string>
+ <string name="discard">नज़रअंदाज़ करे</string>
+ <string name="notifications">सूचनाएं</string>
+ <string name="note_reply_successful">जवाब प्रकाशित हो गया</string>
+ <string name="new_notifications">%d नयी सूचनाएं</string>
+ <string name="more_notifications">और %d और भी.</string>
+ <string name="sign_in">साइन इन</string>
+ <string name="loading">लोड किया जा रहा...</string>
+ <string name="httppassword">एचटीटीपी कूटशब्द</string>
+ <string name="httpuser">एचटीटीपी उपयोगकर्ता नाम</string>
+ <string name="error_media_upload">मीडिया अपलोड करने के दौरान एक त्रुटि हुई</string>
+ <string name="content_description_add_media">मीडिया जोड़ें</string>
+ <string name="publish_date">प्रकाशित करे</string>
+ <string name="post_content">सामग्री(शब्द और मीडिया को जोड़ने के लिए टैप करे)</string>
+ <string name="incorrect_credentials">गलत उपयोगकर्ता नाम या कूटशब्द.</string>
+ <string name="username">उपयोगकर्ता नाम</string>
+ <string name="password">कूटशब्द</string>
+ <string name="reader">पाठक</string>
+ <string name="anonymous">अनाम</string>
+ <string name="no_network_title">नेटवर्क उपलब्ध नहीं </string>
+ <string name="page">पृष्ठ</string>
+ <string name="pages">पृष्ठे</string>
+ <string name="post">पोस्ट</string>
+ <string name="posts">पोस्ट</string>
+ <string name="width">चौडाई</string>
+ <string name="caption">शीर्षक (वैकल्पिक)</string>
+ <string name="featured">विशेष छवि के रुप में उपयोग करें</string>
+ <string name="featured_in_post">पोस्ट सामग्री में छवि को शामिल करे</string>
+ <string name="blogusername">ब्लॉग उपयोगकर्ता नाम</string>
+ <string name="ok">ओके</string>
+ <string name="upload_scaled_image">अपलोड करें और बढाये गए छवि से जोड़े</string>
+ <string name="scaled_image">बढाये गए छवि की चौड़ाई</string>
+ <string name="scheduled"> नियत किया गया</string>
+ <string name="link_enter_url">यूआरएल</string>
+ <string name="version">संस्करण</string>
+ <string name="tos">सेवा की शर्तें</string>
+ <string name="app_title">एंड्राइड के लिए वर्डप्रेस</string>
+ <string name="max_thumbnail_px_width">मूलभूत छवि की चौड़ाई</string>
+ <string name="image_alignment">संरेखण</string>
+ <string name="refresh">रिफ्रेश</string>
+ <string name="untitled">बिना शीर्षक का</string>
+ <string name="edit">संपादित करें</string>
+ <string name="page_id">पृष्ठ</string>
+ <string name="post_id">पोस्ट</string>
+ <string name="post_password">कूटशब्द (वैकल्पिक)</string>
+ <string name="immediately">तुरंत</string>
+ <string name="quickpress_add_alert_title">शॉर्टकट नाम निर्धारित करें </string>
+ <string name="settings">सेटिंग्स</string>
+ <string name="today">आज</string>
+ <string name="share_url">यूआरएल साझा करे</string>
+ <string name="quickpress_add_error">शॉर्टकट का नाम खाली नहीं हो सकता</string>
+ <string name="quickpress_window_title">QuickPress शॉर्टकट के लिए एक ब्लॉग चुने</string>
+ <string name="publish_post">प्रकाशित करे</string>
+ <string name="post_private">प्राइवेट</string>
+ <string name="draft">प्रारूप</string>
+ <string name="upload_full_size_image">अपलोड करें और पूर्ण छवि से जोड़े</string>
+ <string name="categories">श्रेणिया</string>
+ <string name="title">शीर्षक</string>
+ <string name="tags_separate_with_commas">टैग (अल्पविराम से अलग किये जाये टैग)</string>
+ <string name="dlg_deleting_comments">टिप्पणियों को हटाना</string>
+ <string name="notification_vibrate">वाइब्रेट</string>
+ <string name="notification_blink">ब्लिंक सुचना का प्रकाश </string>
+ <string name="status">स्थति</string>
+ <string name="location">स्थान</string>
+ <string name="select_video">गैलरी से एक वीडियो को चुने</string>
+ <string name="sdcard_title">एसडी कार्ड आवश्यक है</string>
+ <string name="media">मीडिया</string>
+ <string name="delete">हटाये</string>
+ <string name="none">कुछ भी नहीं</string>
+ <string name="blogs">ब्लॉग</string>
+ <string name="select_photo">गैलरी से एक तस्वीर को चुने </string>
+ <string name="add"> जोड़े</string>
+ <string name="cancel">रद्द करें</string>
+ <string name="error">त्रुटि</string>
+ <string name="no">नहीं</string>
+ <string name="on">ऑन</string>
+ <string name="preview">पूर्वावलोकन</string>
+ <string name="reply">जवाब दें</string>
+ <string name="save">सहेजें</string>
+ <string name="yes">हां</string>
+ <string name="category_refresh_error">श्रेणी रिफ्रेश त्रुटि</string>
+</resources>
diff --git a/WordPress/src/main/res/values-hr/strings.xml b/WordPress/src/main/res/values-hr/strings.xml
new file mode 100644
index 000000000..19a2dc49b
--- /dev/null
+++ b/WordPress/src/main/res/values-hr/strings.xml
@@ -0,0 +1,892 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="show_purchases">Prikaži kupovine</string>
+ <string name="delete_site_hint">Obriši web stranicu</string>
+ <string name="are_you_sure">Da li ste sigurni?</string>
+ <string name="primary_domain">Primarna domena</string>
+ <string name="contact_support">Kontaktirajte podršku</string>
+ <string name="error_post_account_settings">Neuspjelo snimanje postavki vašeg računa</string>
+ <string name="error_post_my_profile">Neuspjelo snimanje vašeg profila</string>
+ <string name="error_fetch_account_settings">Neuspjelo dohvaćanje postavki vašeg računa</string>
+ <string name="error_fetch_my_profile">Neuspjelo dohvaćanje vašeg profila</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, uspjelo</string>
+ <string name="stats_widget_promo_desc">Dodajte widget na početni zaslon za pristup Statistici u jednom kliku.</string>
+ <string name="stats_widget_promo_title">Widget statistike početnog zaslona</string>
+ <string name="site_settings_unknown_language_code_error">Jezični kod nije prepoznat</string>
+ <string name="site_settings_threading_dialog_description">Dopusti grananje komentara.</string>
+ <string name="site_settings_threading_dialog_header">Grananje do </string>
+ <string name="remove">Ukloni</string>
+ <string name="search">Pretraga</string>
+ <string name="add_category">Dodaj kategoriju</string>
+ <string name="disabled">Onemogući</string>
+ <string name="site_settings_image_original_size">Originalna veličina</string>
+ <string name="privacy_private">Vaša web stranica je vidljiva samo vama i korisnicima koje odobrite</string>
+ <string name="privacy_public_not_indexed">Vaša web stranica je vidljiva svima, ali traži od pretraživača da ju ne indeksiraju</string>
+ <string name="privacy_public">Vaša web stranica je vidljiva svima, i pretraživači će ju indeksirati</string>
+ <string name="about_me_hint">Nekoliko riječi o vama...</string>
+ <string name="about_me">O meni</string>
+ <string name="public_display_name_hint">Prikazano ime biti će vaše korisničko ime, ako drugo nije postavljeno</string>
+ <string name="public_display_name">Javno prikazano ime</string>
+ <string name="my_profile">Moj profil</string>
+ <string name="first_name">Ime</string>
+ <string name="last_name">Prezime</string>
+ <string name="site_privacy_public_desc">Dopusti pretraživačima da indeksiraju web stranicu</string>
+ <string name="site_privacy_hidden_desc">Obeshrabri pretraživače da indeksiraju web stranicu</string>
+ <string name="site_privacy_private_desc">Želim da je moja stranica privatna, vidljiva samo korisnicima koje odaberem</string>
+ <string name="cd_related_post_preview_image">Predpregled slika srodnih objava</string>
+ <string name="error_post_remote_site_settings">Nije moguće snimiti info o web stranici</string>
+ <string name="error_fetch_remote_site_settings">Nije moguće dohvatiti info o web stranici</string>
+ <string name="error_media_upload_connection">Tijekom prijenosa medijskoga zapisa dogodila se greška u spajanju</string>
+ <string name="site_settings_disconnected_toast">Odspojeno, uređivanje onemogućeno</string>
+ <string name="site_settings_unsupported_version_error">WordPress inačica koja nije podržana</string>
+ <string name="site_settings_multiple_links_dialog_description">Potrebno odobrenje za komentare koji sadrže više od ovog broja komentara.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatski zatvori</string>
+ <string name="site_settings_close_after_dialog_description">Automatski zatvori komentare na člancima</string>
+ <string name="site_settings_paging_dialog_description">Prijelom niza komentara u više stranica.</string>
+ <string name="site_settings_paging_dialog_header">Komentara po stranici</string>
+ <string name="site_settings_close_after_dialog_title">Zatvori komentiranje</string>
+ <string name="site_settings_blacklist_description">Kada komentar sadrži bilo koju od ovih riječi u svom sadržaju, imenu, URL-u, e-pošti, ili IP-u, biti će označen kao spam. Možete unijeti dijelove riječi, tako da će &amp;#8220;press&amp;#8221; odgovarati i riječi &amp;#8220;WordPress&amp;#8221;.</string>
+ <string name="site_settings_hold_for_moderation_description">Kada komentar sadrži bilo koju od ovih riječi u svom sadržaju, imenu, URL-u, e-pošti, ili IP-u, biti će označen kao spam. Možete unijeti dijelove riječi, tako da će &amp;#8220;press&amp;#8221; odgovarati i riječi &amp;#8220;WordPress&amp;#8221;.</string>
+ <string name="site_settings_list_editor_input_hint">Unesite riječi ili frazu</string>
+ <string name="site_settings_list_editor_no_items_text">Nema stavki</string>
+ <string name="site_settings_learn_more_caption">Možete zaobići ove postavke na individualnim objavama.</string>
+ <string name="site_settings_rp_preview3_site">u "Nadogradnji"</string>
+ <string name="site_settings_rp_preview3_title">Fokus nadogradnje: VideoPress za vjenčanja</string>
+ <string name="site_settings_rp_preview2_site">u "Aplikacijama"</string>
+ <string name="site_settings_rp_preview2_title">WordPress for Android aplikacija dobila redizajn</string>
+ <string name="site_settings_rp_preview1_site">u "Mobilnom"</string>
+ <string name="site_settings_rp_preview1_title">Veliko iPhone/iPad ažuriranje sada dostupno</string>
+ <string name="site_settings_rp_show_images_title">Prikaži slike</string>
+ <string name="site_settings_rp_show_header_title">Prikaži zaglavlje</string>
+ <string name="site_settings_rp_switch_summary">Srodne objave prikazuju srodni sadržaj sa vaše stranice ispod vaših objava.</string>
+ <string name="site_settings_rp_switch_title">Prikaži srodne objave</string>
+ <string name="site_settings_delete_site_hint">Uklonite svoju web stranicu iz aplikacije</string>
+ <string name="site_settings_blacklist_hint">Komentari koji odgovaraju filteru označeni su kao spam</string>
+ <string name="site_settings_moderation_hold_hint">Komentari koji odgovaraju filteru stavljeni su na čekanje</string>
+ <string name="site_settings_multiple_links_hint">Ignoriranje limita poveznica od poznatih korisnika</string>
+ <string name="site_settings_whitelist_hint">Autor komentara mora imati prethodno odobreni komentar</string>
+ <string name="site_settings_user_account_required_hint">Korisnici moraju biti registrirani i prijavljeni kako bi objavili komentare</string>
+ <string name="site_settings_identity_required_hint">Autor komentara mora popuniti ime i adresu e-pošte</string>
+ <string name="site_settings_manual_approval_hint">Komentari moraju biti manualno odobreni</string>
+ <string name="site_settings_paging_hint">Prikaži komentare u grupama određene veličine</string>
+ <string name="site_settings_threading_hint">Dopusti ugniježđene komentare do određenog nivoa</string>
+ <string name="site_settings_sort_by_hint">Određuje kojim poretkom su prikazani komentari</string>
+ <string name="site_settings_close_after_hint">Onemogući komentare nakon određenog vremena</string>
+ <string name="site_settings_receive_pingbacks_hint">Dopusti obavijesti o poveznicama sa drugih blogova</string>
+ <string name="site_settings_send_pingbacks_hint">Pokušaj obavijestiti sve blogove prema kojim postoji poveznica u objavi</string>
+ <string name="site_settings_allow_comments_hint">Dopusti čitateljima da objave komentare</string>
+ <string name="site_settings_discussion_hint">Pregledajte i promijenite postavke rasprave na web stranici</string>
+ <string name="site_settings_more_hint">Pregledajte sve dostupne postavke resprave</string>
+ <string name="site_settings_related_posts_hint">Prikažite ili sakrijte srodne objave u čitaču</string>
+ <string name="site_settings_upload_and_link_image_hint">Omogućite da se uvijek prenose slike u punoj veličini</string>
+ <string name="site_settings_image_width_hint">Redimenzionirajte slike u objavama na ovu širinu</string>
+ <string name="site_settings_format_hint">Postavlja novi format objave</string>
+ <string name="site_settings_category_hint">Postavlja novu kategoriju objave</string>
+ <string name="site_settings_location_hint">Automatski dodaj podatke o lokaciji mojim objavama</string>
+ <string name="site_settings_password_hint">Promijenite svoju lozinku</string>
+ <string name="site_settings_username_hint">Trenutni korisnički račun</string>
+ <string name="site_settings_language_hint">Jezik na kojem je primarno pisan ovaj blog</string>
+ <string name="site_settings_privacy_hint">Kontrolira tko može vidjeti vašu web stranicu</string>
+ <string name="site_settings_address_hint">Promjena vaše adrese trenutno nije podržana</string>
+ <string name="site_settings_tagline_hint">Kratki opis ili pamtljiva fraza koja opisuje vaš blog</string>
+ <string name="site_settings_title_hint">U nekoliko riječi, objasnite o čemu govori ova web stranice.</string>
+ <string name="site_settings_whitelist_known_summary">Komentari od nepoznatih korisnika</string>
+ <string name="site_settings_whitelist_all_summary">Komentari svih korisnika</string>
+ <string name="site_settings_threading_summary">%d nivoa</string>
+ <string name="site_settings_privacy_private_summary">Privatno</string>
+ <string name="site_settings_privacy_hidden_summary">Skriveno</string>
+ <string name="site_settings_delete_site_title">Obriši web stranicu</string>
+ <string name="site_settings_privacy_public_summary">Javno</string>
+ <string name="site_settings_blacklist_title">Crna lista</string>
+ <string name="site_settings_moderation_hold_title">Zadržati za moderaciju</string>
+ <string name="site_settings_multiple_links_title">Poveznice u komentarima</string>
+ <string name="site_settings_whitelist_title">Automatsko odobravanje</string>
+ <string name="site_settings_threading_title">Grananje</string>
+ <string name="site_settings_paging_title">Paging</string>
+ <string name="site_settings_sort_by_title">Sortiraj po</string>
+ <string name="site_settings_account_required_title">Korisnici moraju biti prijavljeni</string>
+ <string name="site_settings_identity_required_title">Morate uključiti i ima i adresu e-pošte</string>
+ <string name="site_settings_receive_pingbacks_title">Primanje povratnih pingova</string>
+ <string name="site_settings_send_pingbacks_title">Slanje povratnih pingova</string>
+ <string name="site_settings_allow_comments_title">Dopusti komentare</string>
+ <string name="site_settings_default_format_title">Zadani format</string>
+ <string name="site_settings_default_category_title">Zadana kategorija</string>
+ <string name="site_settings_location_title">Omogući lokaciju</string>
+ <string name="site_settings_address_title">Adresa</string>
+ <string name="site_settings_title_title">Naslov web strancie</string>
+ <string name="site_settings_tagline_title">Slogan</string>
+ <string name="site_settings_this_device_header">Ovaj uređaj</string>
+ <string name="site_settings_discussion_new_posts_header">Zadane postavke za nove objave</string>
+ <string name="site_settings_account_header">Račun</string>
+ <string name="site_settings_writing_header">Pisanje</string>
+ <string name="newest_first">Prvo najnoviji</string>
+ <string name="site_settings_general_header">Općenito</string>
+ <string name="discussion">Rasprava</string>
+ <string name="privacy">Privatnost</string>
+ <string name="related_posts">Srodne objave</string>
+ <string name="comments">Komentari</string>
+ <string name="close_after">Zatvori nakon</string>
+ <string name="oldest_first">Prvo najstariji</string>
+ <string name="media_error_no_permission_upload">Nemate dopuštenje za prijenos medijskih zapisa na web stranicu</string>
+ <string name="never">Nikad</string>
+ <string name="unknown">Nepoznato</string>
+ <string name="reader_err_get_post_not_found">Objava više ne postoji</string>
+ <string name="reader_err_get_post_not_authorized">Nemate pravo pristupa pregledavanja ove objave</string>
+ <string name="reader_err_get_post_generic">Nije moguće učitati objavu</string>
+ <string name="blog_name_no_spaced_allowed">Adresa web stranice ne može sadržavati razmake</string>
+ <string name="invalid_username_no_spaces">Korisničko ime ne može sadržavati razmake</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Web stranice koje pratite nisu ništa objavile u posljednje vrijeme</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Nema novijih objava</string>
+ <string name="media_details_copy_url_toast">URL kopiran u međuspremnik</string>
+ <string name="edit_media">Uredi medijski zapis</string>
+ <string name="media_details_copy_url">Kopiraj URL</string>
+ <string name="media_details_label_date_uploaded">Preneseno</string>
+ <string name="media_details_label_date_added">Dodano</string>
+ <string name="selected_theme">Odabrana tema</string>
+ <string name="could_not_load_theme">Nije moguće učitati temu</string>
+ <string name="theme_activation_error">Nešto je pošlo krivo. Nije moguće aktivirati temu.</string>
+ <string name="theme_by_author_prompt_append">od %1$s</string>
+ <string name="theme_prompt">Hvala što ste izabrali %1$s</string>
+ <string name="theme_try_and_customize">Isprobaj i prilagodi</string>
+ <string name="theme_view">Pregled</string>
+ <string name="theme_details">Detalji</string>
+ <string name="theme_support">Podrška</string>
+ <string name="theme_done">GOTOVO</string>
+ <string name="theme_manage_site">UPRAVLJANJE WEB STRANICOM</string>
+ <string name="title_activity_theme_support">Teme</string>
+ <string name="theme_activate">Aktiviraj</string>
+ <string name="date_range_start_date">Početni datum</string>
+ <string name="date_range_end_date">Krajnji datum</string>
+ <string name="current_theme">Trenutna tema</string>
+ <string name="customize">Prilagodba</string>
+ <string name="details">Detalji</string>
+ <string name="support">Podrška</string>
+ <string name="active">Aktivno</string>
+ <string name="stats_referrers_spam_generic_error">Nešto je pošlo krivo tijekom operacije. Spam stanje nije promijenjeno.</string>
+ <string name="stats_referrers_marking_not_spam">Označavanje da nije spam</string>
+ <string name="stats_referrers_unspam">Nije spam</string>
+ <string name="stats_referrers_marking_spam">Označavanje kao spam</string>
+ <string name="theme_auth_error_authenticate">Neuspjelo dohvaćanje tema: neuspjela identifikacija korisnika</string>
+ <string name="post_published">Objava publicirana</string>
+ <string name="page_published">Stranica objavljena</string>
+ <string name="post_updated">Objava ažurirana</string>
+ <string name="page_updated">Stranica ažurirana</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Nažalost, teme nisu pronađene.</string>
+ <string name="media_uploaded_on">Preneseno: %s</string>
+ <string name="media_dimensions">Dimenzije: %s</string>
+ <string name="media_file_name">Ime datoteke: %s</string>
+ <string name="media_file_type">Vrsta datoteke: %s</string>
+ <string name="upload_queued">Na čekanju</string>
+ <string name="reader_label_gap_marker">Učitaj više objava</string>
+ <string name="notifications_no_search_results">Nijedna web stranica se ne podudara sa \'%s\'</string>
+ <string name="search_sites">Pretraživanje web stranica</string>
+ <string name="notifications_empty_view_reader">Pregledaj čitač</string>
+ <string name="unread">Označi kao ne pročitano</string>
+ <string name="notifications_empty_action_followers_likes">Budite primijećeni: komentirajte pročitane objave.</string>
+ <string name="notifications_empty_action_comments">Pridružite se raspravi: komentirajte objave blogova koje pratite.</string>
+ <string name="notifications_empty_action_unread">Ponovno pokrenite raspravu: napišite novu objavu.</string>
+ <string name="notifications_empty_action_all">Postanite aktivni! Komentirajte objave blogova koje pratite.</string>
+ <string name="notifications_empty_likes">Nema novih lajkova... još.</string>
+ <string name="notifications_empty_followers">Nemate novih pratitelja... još.</string>
+ <string name="notifications_empty_comments">Nema novih komentara... još.</string>
+ <string name="notifications_empty_unread">Sve ste pročitali!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Pregledajte Statistiku u aplikaciji, i kasnije pokušajte dodati novi widget</string>
+ <string name="stats_widget_error_readd_widget">Uklonite widget i zatim ga ponovno dodajte</string>
+ <string name="stats_widget_error_no_visible_blog">Nije moguće pristupiti Statistici bez vidljivog bloga</string>
+ <string name="stats_widget_error_no_permissions">Vaš WordPress.com račun ne može pristupiti Statistici na ovom blogu</string>
+ <string name="stats_widget_error_no_account">Prijavite se u WordPress</string>
+ <string name="stats_widget_error_generic">Statistiku nije moguće učitati</string>
+ <string name="stats_widget_loading_data">Učitavanje podataka...</string>
+ <string name="stats_widget_name_for_blog">Današnja statistika za %1$s</string>
+ <string name="stats_widget_name">Današnja WordPress statistika</string>
+ <string name="add_location_permission_required">Potrebno je dopuštenje kako bi se dodala lokacija</string>
+ <string name="add_media_permission_required">Potrebno je dopuštenje kako bi se dodao media zapis</string>
+ <string name="access_media_permission_required">Potrebno je dopuštenje kako bi se pristupilo media zapisu</string>
+ <string name="stats_enable_rest_api_in_jetpack">Za pregled statistike uključite JSON API modul u Jetpacku.</string>
+ <string name="error_open_list_from_notification">Ova objava ili stranica je objavljena na drugoj web stranici.</string>
+ <string name="reader_short_comment_count_multi">%s komentara</string>
+ <string name="reader_short_comment_count_one">1 komentar</string>
+ <string name="reader_label_submit_comment">POŠALJI</string>
+ <string name="reader_hint_comment_on_post">Odgovori na objavu...</string>
+ <string name="reader_discover_visit_blog">Pošalji %s</string>
+ <string name="reader_discover_attribution_blog">Izvorno objavljeno na %s</string>
+ <string name="reader_discover_attribution_author">Originalni autor %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Inicijalno objavio %1$s na %2$s</string>
+ <string name="reader_short_like_count_multi">%s Lajkova</string>
+ <string name="reader_short_like_count_none">Lajkovi</string>
+ <string name="reader_label_follow_count">%,d sljedbenika</string>
+ <string name="reader_short_like_count_one">1 lajk</string>
+ <string name="reader_menu_tags">Uredi oznake i blogove</string>
+ <string name="reader_title_post_detail">Objava u Čitaču</string>
+ <string name="local_draft_explainer">Ova objava je skica koja još nije objavljena</string>
+ <string name="local_changes_explainer">Ova objava sadrži promjene koje još nisu objavljene</string>
+ <string name="notifications_push_summary">Postavke za obavijesti na vašem uređaju</string>
+ <string name="notifications_email_summary">Postavke za obavijesti koje će biti poslane na adresu e-pošte povezane s tvojim korisničkim računom</string>
+ <string name="notifications_tab_summary">Postavke za obavijesti koje se pojavljuju u kartici Obavijesti</string>
+ <string name="notifications_disabled">Obavijesti aplikacije su onemogućene. Dodirnite ovdje kako biste ih omogućili u Postavkama.</string>
+ <string name="notification_types">Tip obavjesti</string>
+ <string name="error_loading_notifications">Nije moguće učitati postavke obavijesti</string>
+ <string name="replies_to_your_comments">Odgovori na Vaše komentare</string>
+ <string name="comment_likes">Lajkovi komentara</string>
+ <string name="app_notifications">Obavijesti aplikacije</string>
+ <string name="notifications_tab">Kartica obavijesti</string>
+ <string name="email">E-pošta</string>
+ <string name="notifications_comments_other_blogs">Komentari na drugim web stranicama</string>
+ <string name="notifications_wpcom_updates">WordPress.com ažuriranja</string>
+ <string name="notifications_other">Drugo</string>
+ <string name="notifications_account_emails">Poruke od WordPress.com</string>
+ <string name="notifications_account_emails_summary">Uvijek ćemo slati važne poruke e-pošte o vašem računu, ali također možete dobiti i dodatnu pomoć.</string>
+ <string name="notifications_sights_and_sounds">Prizori i zvukovi</string>
+ <string name="your_sites">Vaše stranice</string>
+ <string name="stats_insights_latest_post_trend">Prošlo je %1$s od kad je %2$s objavljen. Ovo su informacije kako je primljen kod čitatelja...</string>
+ <string name="stats_insights_latest_post_summary">Najnoviji sažetak objava</string>
+ <string name="button_revert">Vrati</string>
+ <string name="yesterday">Jučer</string>
+ <string name="days_ago">prije %d dana</string>
+ <string name="connectionbar_no_connection">Nema konekcije</string>
+ <string name="button_view">Pogledaj</string>
+ <string name="button_edit">Uredi</string>
+ <string name="button_publish">Objavi</string>
+ <string name="page_deleted">Stranica obrisana</string>
+ <string name="page_trashed">Stanica poslana u smeće</string>
+ <string name="post_trashed">Objava poslana u smeće</string>
+ <string name="button_trash">Smeće</string>
+ <string name="button_back">Nazad</string>
+ <string name="button_stats">Statistika</string>
+ <string name="button_preview">Predpregled</string>
+ <string name="post_deleted">Objava obrisana</string>
+ <string name="stats_no_activity_this_period">U ovom periodu nema aktivnosti</string>
+ <string name="trashed">Stavljeno u smeće</string>
+ <string name="my_site_no_sites_view_subtitle">Želite li dodati web stranicu?</string>
+ <string name="my_site_no_sites_view_title">Nemate WordPress web stranica.</string>
+ <string name="my_site_no_sites_view_drake">Ilustracija</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Nemate pravo pristupa ovom blogu</string>
+ <string name="reader_toast_err_follow_blog_not_found">Blog nije moguće pronaći</string>
+ <string name="undo">Poništi</string>
+ <string name="tabbar_accessibility_label_my_site">Moja stranica</string>
+ <string name="tabbar_accessibility_label_me">Ja</string>
+ <string name="passcodelock_prompt_message">Unesi svoj PIN</string>
+ <string name="editor_toast_changes_saved">Promjene spremljene</string>
+ <string name="push_auth_expired">Zahtjev je istekao. Za ponovni pokušaj prijavite u WordPress.com.</string>
+ <string name="ignore">Ignoriraj</string>
+ <string name="stats_insights_best_ever">Najviše pregleda</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% pregleda</string>
+ <string name="stats_insights_most_popular_hour">Najpopularniji sat</string>
+ <string name="stats_insights_most_popular_day">Najpopularniji dan</string>
+ <string name="stats_insights_today">Današnja statistika</string>
+ <string name="stats_insights_popular">Najpopularniji dan i sat</string>
+ <string name="stats_insights_all_time">Sve objave, pregledi i posjetitelji</string>
+ <string name="stats_insights">Uvid</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Kako biste vidjeli statistiku, prijavite se sa istim onim WordPress.com računom koji ste upotrijebili za spajanje Jetpacka.</string>
+ <string name="stats_other_recent_stats_moved_label">Tražite Druge nedavne statistike? Premjestili smo ih na Insights stranicu</string>
+ <string name="me_disconnect_from_wordpress_com">Odspoji sa WordPress.com</string>
+ <string name="me_btn_login_logout">Prijava/Odjava</string>
+ <string name="me_connect_to_wordpress_com">Spoji sa WordPress.com</string>
+ <string name="me_btn_support">Pomoć i podrška</string>
+ <string name="site_picker_cant_hide_current_site">"%s" nije skriven jer je to trenutna web stranica.</string>
+ <string name="account_settings">Postavke računa</string>
+ <string name="site_picker_create_dotcom">Kreiraj WordPress.com web stranicu</string>
+ <string name="site_picker_add_site">Dodaj stranicu</string>
+ <string name="site_picker_add_self_hosted">Dodaj samostalno hostanu web stranicu</string>
+ <string name="site_picker_edit_visibility">Prikaži/sakrij web stranice</string>
+ <string name="my_site_btn_view_site">Pogledaj stranicu</string>
+ <string name="site_picker_title">Odaberi stranicu</string>
+ <string name="my_site_btn_view_admin">Prikaži Admin</string>
+ <string name="my_site_btn_switch_site">Promjeni web stranicu</string>
+ <string name="my_site_btn_site_settings">Postavke</string>
+ <string name="my_site_header_publish">Objavi</string>
+ <string name="my_site_btn_blog_posts">Blog objave</string>
+ <string name="my_site_header_look_and_feel">Izgled i dojam</string>
+ <string name="my_site_header_configuration">Konfiguracija</string>
+ <string name="reader_label_new_posts_subtitle">Pritisni za prikaz</string>
+ <string name="notifications_account_required">Za obavijesti, prijavite se u WordPress.com</string>
+ <string name="stats_unknown_author">Nepoznati autor</string>
+ <string name="image_added">Slika dodana</string>
+ <string name="signout">Prekinuta veza</string>
+ <string name="show">Prikaži</string>
+ <string name="hide">Sakrij</string>
+ <string name="select_all">Odaberi sve</string>
+ <string name="deselect_all">Odznači sve</string>
+ <string name="sign_out_wpcom_confirm">Odspajanjem računa ukloniti će se svi @%s WordPress.com podaci sa ovog uređaja, uključujući lokalne skice i lokalne promjene.</string>
+ <string name="select_from_new_picker">Višestruki odabir s novim odabirom</string>
+ <string name="no_device_videos">Bez videa</string>
+ <string name="no_blog_images">Bez slika</string>
+ <string name="no_blog_videos">Bez videa</string>
+ <string name="no_device_images">Bez slika</string>
+ <string name="stats_generic_error">Traženu statistiku nije moguće učitati</string>
+ <string name="error_loading_blog_images">Nije moguće dohvatiti slike</string>
+ <string name="error_loading_blog_videos">Nije moguće dohvatiti video</string>
+ <string name="error_loading_images">Greška pri učitavanju slika</string>
+ <string name="error_loading_videos">Greška pri učitavanju videa</string>
+ <string name="loading_blog_images">Dohvaćanje slika</string>
+ <string name="loading_blog_videos">Dohvaćanje videa</string>
+ <string name="no_media_sources">Nije moguće dohvatiti medijske zapise</string>
+ <string name="loading_videos">Učitavanje videa</string>
+ <string name="loading_images">Učitavanje slika</string>
+ <string name="no_media">Nema medijskih zapisa</string>
+ <string name="device">Uređaj</string>
+ <string name="language">Jezik</string>
+ <string name="add_to_post">Dodaj u objavu</string>
+ <string name="take_photo">Slikaj</string>
+ <string name="take_video">Snimi video</string>
+ <string name="tab_title_site_images">Slike stranice</string>
+ <string name="verification_code">Kod za provjeru</string>
+ <string name="verify">Provjeri</string>
+ <string name="media_picker_title">Odabir medijskoga zapisa</string>
+ <string name="tab_title_device_images">Slike sa uređaja</string>
+ <string name="tab_title_device_videos">Video sa uređaja</string>
+ <string name="tab_title_site_videos">Video sa web stranice</string>
+ <string name="error_publish_no_network">Nije moguće objaviti dok nema veze. Snimljeno kao skica.</string>
+ <string name="editor_toast_invalid_path">Neispravna putanja datoteke</string>
+ <string name="invalid_verification_code">Neispravan verifikacijski kod</string>
+ <string name="two_step_footer_label">Unesite kod iz autentifikacijske aplikacije.</string>
+ <string name="two_step_footer_button">Slanje koda putem poruke</string>
+ <string name="two_step_sms_sent">Provjerite poruke za verifikacijski kod.</string>
+ <string name="sign_in_jetpack">Prijavite se u WordPress.com račun kako bi spojili Jetpack.</string>
+ <string name="auth_required">Ponovno se prijavite za nastavak.</string>
+ <string name="media_details_label_file_name">Ime datoteke</string>
+ <string name="media_details_label_file_type">Vrsta datoteke</string>
+ <string name="publisher">Izdavač:</string>
+ <string name="error_notification_open">Ne mogu otvoriti obavjesti</string>
+ <string name="comments_fetching">Dohvaćanje komentara</string>
+ <string name="pages_fetching">Dohvaćanje stranica...</string>
+ <string name="toast_err_post_uploading">Ne mogu otvoriti objavu dok se prenosi na stranicu</string>
+ <string name="posts_fetching">Dohvaćanje objava...</string>
+ <string name="media_fetching">Dohvaćanje medija</string>
+ <string name="stats_view_authors">Autori</string>
+ <string name="stats_entry_search_terms">Pojam pretrage</string>
+ <string name="reader_empty_posts_request_failed">Nije moguće dohvatiti objave</string>
+ <string name="stats_view_search_terms">Pojmovi pretraživanja</string>
+ <string name="stats_followers_total_email_paged">Prikazuje se %1$d - %2$d od %3$s e-pošta pratitelja</string>
+ <string name="stats_search_terms_unknown_search_terms">Nepoznati pojam pretrage</string>
+ <string name="stats_followers_total_wpcom_paged">Prikazuje se %1$d - %2$d od %3$s WordPress.com pratitelja</string>
+ <string name="stats_empty_search_terms">Nema zabilježenih pojmova pretraga</string>
+ <string name="stats_empty_search_terms_desc">Saznajte više o prometu tražilica prateći pojmove koje su vaši posjetitelji tražili da pronađu vašu web stranicu.</string>
+ <string name="stats_total">Ukupno</string>
+ <string name="stats_overall">Sveukupno</string>
+ <string name="reader_empty_posts_in_blog">Ovaj blog je prazan</string>
+ <string name="stats_average_per_day">Prosjek po danu</string>
+ <string name="stats_recent_weeks">Posljednjih tjedana</string>
+ <string name="error_copy_to_clipboard">Došlo je do pogreške prilikom kopiranja teksta u međuspremnik</string>
+ <string name="stats_months_and_years">Mjeseci i godine</string>
+ <string name="reader_label_new_posts">Nove objave</string>
+ <string name="stats_period">Razdoblje</string>
+ <string name="reader_page_recommended_blogs">Web stranice koje bi vam se mogle sviđati</string>
+ <string name="post_uploading">Prijenos</string>
+ <string name="logs_copied_to_clipboard">Zapisi aplikacije su kopirani međuspremnik</string>
+ <string name="stats_followers_an_hour_ago">Prije sat vremena</string>
+ <string name="stats_followers_hours">%1$d sati</string>
+ <string name="stats_followers_a_day">Jučer</string>
+ <string name="stats_followers_days">%1$d dana</string>
+ <string name="stats_followers_a_minute_ago">prije minutu</string>
+ <string name="stats_followers_seconds_ago">prije sekundu</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_total_wpcom">Ukupan broj WordPress.com pratitelja: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Broj objava s komentarima pratitelja: %1$s</string>
+ <string name="stats_comments_by_authors">Od autora</string>
+ <string name="stats_comments_by_posts_and_pages">Od objava i stranica</string>
+ <string name="stats_empty_followers_desc">Pratite broj sljedbenika, i koliko dugo oni prate Vašu stranicu.</string>
+ <string name="stats_empty_followers">Nema sljedbenika</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_entry_publicize">Servis</string>
+ <string name="stats_entry_followers">Sljedbenik</string>
+ <string name="stats_totals_publicize">Sljedbenici</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_view_top_posts_and_pages">Objave i stranice</string>
+ <string name="stats_view_videos">Videi</string>
+ <string name="stats_view_followers">Sljedbenici</string>
+ <string name="stats_view_countries">Države</string>
+ <string name="stats_likes">Lajkovi</string>
+ <string name="stats_view_all">Pogledaj sve</string>
+ <string name="stats_view">Pogledaj</string>
+ <string name="stats_totals_followers">Od</string>
+ <string name="stats_visitors">Posjetitelji</string>
+ <string name="stats_timeframe_years">Godine</string>
+ <string name="stats_views">Pregledi</string>
+ <string name="themes_fetching">Dohvaćanje tema...</string>
+ <string name="stats_for">Statistika za %s</string>
+ <string name="stats_followers_months">%1$d mjeseci</string>
+ <string name="stats_followers_a_year">Godina</string>
+ <string name="stats_followers_years">%1$d godina</string>
+ <string name="stats_followers_a_month">Mjesec</string>
+ <string name="stats_followers_minutes">%1$d minuta</string>
+ <string name="stats_pagination_label">Stranica %1$s od %2$s</string>
+ <string name="stats_followers_total_email">Ukupan broj e-pošta pratitelja: %1$s</string>
+ <string name="stats_followers_email_selector">E-pošta</string>
+ <string name="stats_empty_clicks_title">Nema zabilježenih klikova</string>
+ <string name="stats_empty_referrers_title">Nema zabilježenih referiranja</string>
+ <string name="stats_empty_top_posts_title">Nema pregledanih objava ili stranica</string>
+ <string name="stats_empty_tags_and_categories">Nema pregleda označenih objava ili stranica</string>
+ <string name="stats_empty_video">Nema pregleda videa</string>
+ <string name="stats_empty_geoviews">Nema zabilježenih zemalja</string>
+ <string name="stats_other_recent_stats_label">Druge nedavne statistike</string>
+ <string name="stats_view_publicize">Publicirajte</string>
+ <string name="stats_empty_publicize_desc">Imajte u uvid sve vaše pratitelje sa različitih društvenih mreža koristeći publiciranje.</string>
+ <string name="stats_empty_publicize">Nema zabilježenih publiciranih pratitelja</string>
+ <string name="stats_empty_video_desc">Ako ste prenijeli video koristeći VideoPress, saznajte koliko su puta pregledani.</string>
+ <string name="stats_empty_comments_desc">Ako omogućite komentare na vašoj web stranici, pratite top komentatore i otkrijte koji sadržaj potiče najživlje rasprave, na temelju najnovijih 1.000 komentara.</string>
+ <string name="stats_empty_tags_and_categories_desc">Proučite osvrt najpopularnijih tema na vašoj web stranici, koje se refleksija vaših najpopularnijih objava u prošlom tjednu.</string>
+ <string name="stats_empty_top_authors_desc">Pratite preglede na objavama suradnika, i saznajte najpopularniji sadržaj svakog autora.</string>
+ <string name="stats_empty_clicks_desc">Kada vaša objava sadrži poveznica na druge web stranice, vidjeti ćete na koje poveznice posjetitelji najviše klikaju.</string>
+ <string name="stats_empty_referrers_desc">Naučite više o vidljivosti vaše web stranice prateći koje web stranice i tražilice šalju najviše prometa.</string>
+ <string name="stats_empty_top_posts_desc">Saznajte koji je vaš najgledaniji sadržaj, i provjerite kako se individualne objave i stranice ponašaju tijekom vremena.</string>
+ <string name="stats_empty_geoviews_desc">Istražite listu kako bi saznali koje zemlje i regije generiraju najviše prometa prema vašoj web stranici.</string>
+ <string name="ssl_certificate_details">Detalji</string>
+ <string name="sure_to_remove_account">Ukloni ovu stranicu</string>
+ <string name="delete_sure_post">Obriši ovu obavijest</string>
+ <string name="delete_sure">Obriši ovu skicu</string>
+ <string name="delete_sure_page">Obriši ovu stranicu</string>
+ <string name="confirm_delete_multi_media">Obriši odabrane stavke?</string>
+ <string name="confirm_delete_media">Obriši odabranu stavku?</string>
+ <string name="cab_selected">%d odabrano</string>
+ <string name="media_gallery_date_range">Prikazivanje medijskih zapisa od %1$s do %2$s</string>
+ <string name="reader_empty_posts_liked">Niste lajkali ni jednu objavu</string>
+ <string name="faq_button">FAQ</string>
+ <string name="reader_label_like">Lajk</string>
+ <string name="reader_label_comment_count_single">Komentar</string>
+ <string name="reader_empty_comments">Još nema komentara</string>
+ <string name="reader_label_comment_count_multi">%,d komentara</string>
+ <string name="reader_label_view_original">Pogledaj izvorni članak</string>
+ <string name="mnu_comment_liked">Lajkao</string>
+ <string name="comment">Komentar</string>
+ <string name="signing_out">Odjava ...</string>
+ <string name="reader_empty_followed_blogs_title">Još ne pratite ni jednu web stranicu</string>
+ <string name="create_new_blog_wpcom">Kreiraj WordPress.com blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog je kreiran!</string>
+ <string name="reader_label_comments_closed">Komentari su zatvoreni</string>
+ <string name="reader_label_comments_on">Komentari o</string>
+ <string name="reader_title_photo_viewer">%1$d od %2$d</string>
+ <string name="error_publish_empty_post">Nije moguće objaviti praznu objavu</string>
+ <string name="error_refresh_unauthorized_posts">Nemate dopuštenje za pregled ili uređivanje objava</string>
+ <string name="error_refresh_unauthorized_pages">Nemate dopuštenje za pregled ili uređivanje stranica</string>
+ <string name="error_refresh_unauthorized_comments">Nemate dopuštenje za pregled ili uređivanje komentara</string>
+ <string name="older_month">Starije od mjesec dana</string>
+ <string name="older_two_days">Starije od 2 dana</string>
+ <string name="older_last_week">Starije od tjedan dana</string>
+ <string name="select_a_blog">Odaberite WordPress web stranicu</string>
+ <string name="sending_content">Prijenos %s sadržaja</string>
+ <string name="uploading_total">Prijenos %1$d od %2$d</string>
+ <string name="comment_trashed">Komentar premješten u smeće</string>
+ <string name="posts_empty_list">Još nemate objava. Zašto ne napišete jednu?</string>
+ <string name="comment_reply_to_user">Odgovor na %s</string>
+ <string name="pages_empty_list">Još nemate stranica. Zašto ne kreirate jednu?</string>
+ <string name="media_empty_list_custom_date">Nema medijski zapisa u ovom vremenskom intervalu</string>
+ <string name="posting_post">Objavljivanje "%s"</string>
+ <string name="nux_help_description">Posjetite centar za pomoć kako bi dobili odgovore na učestala pitanja, ili posjetite forume kako bi postavili nova pitanja</string>
+ <string name="browse_our_faq_button">Pregledajte naš ČPP (FAQ)</string>
+ <string name="agree_terms_of_service">Kreiranjem računa pristajete na fascinantne %1$sUvjeti pružanja usluge%2$s</string>
+ <string name="stats_no_blog">Nije moguće učitati statistiku za navedeni blog</string>
+ <string name="more">Više</string>
+ <string name="reader_empty_posts_in_tag">Nema objava s ovom oznakom</string>
+ <string name="reader_toast_err_generic">Nije moguće izvesti tu akciju</string>
+ <string name="reader_toast_err_block_blog">Nije moguće blokirati ovaj blog</string>
+ <string name="reader_toast_blog_blocked">Objave sa ovog bloga više neće biti prikazane</string>
+ <string name="reader_menu_block_blog">Blokiraj ovaj blog</string>
+ <string name="hs__invalid_email_error">Unesite valjanu adresu e-pošte</string>
+ <string name="contact_us">Kontaktirajte nas</string>
+ <string name="hs__username_blank_error">Unesite valjano ime</string>
+ <string name="hs__conversation_detail_error">Opišite problem koji se pojavljuje</string>
+ <string name="hs__new_conversation_header">Razgovor s podrškom</string>
+ <string name="hs__conversation_header">Razgovor s podrškom</string>
+ <string name="add_location">Dodaj lokaciju</string>
+ <string name="current_location">Trenutna lokacija</string>
+ <string name="search_location">Pretraga</string>
+ <string name="edit_location">Uredi</string>
+ <string name="search_current_location">Pronaći</string>
+ <string name="preference_send_usage_stats">Šalji statistiku</string>
+ <string name="preference_send_usage_stats_summary">Automatski šalji statistiku o korištenju kako bi nam pomogli poboljšati WordPress za Andorid</string>
+ <string name="update_verb">Ažuriraj</string>
+ <string name="schedule_verb">Zakazati</string>
+ <string name="reader_page_followed_tags">Praćene oznake</string>
+ <string name="reader_page_followed_blogs">Praćene web stranice</string>
+ <string name="reader_title_subs">Oznake i blogovi</string>
+ <string name="reader_hint_add_tag_or_url">Unesite URL ili oznaku za praćenje</string>
+ <string name="reader_label_tag_preview">Objave označene %s</string>
+ <string name="reader_toast_err_get_blog_info">Nije moguće prikazati blog</string>
+ <string name="reader_toast_err_already_follow_blog">Već pratite ovaj blog</string>
+ <string name="reader_toast_err_follow_blog">Nije moguće pratiti ovaj blog</string>
+ <string name="reader_toast_err_unfollow_blog">Prestanak praćenja bloga nije moguć</string>
+ <string name="reader_empty_recommended_blogs">Nema preporučenih blogova</string>
+ <string name="reader_label_followed_blog">Pratite blog</string>
+ <string name="reader_title_blog_preview">Blog u Čitaču</string>
+ <string name="reader_title_tag_preview">Oznaka u Čitaču</string>
+ <string name="saving">Spremam...</string>
+ <string name="media_empty_list">Nema medijskih zapisa</string>
+ <string name="ptr_tip_message">Savjet: Povucite dolje za osvježavanje</string>
+ <string name="help">Pomoć</string>
+ <string name="forgot_password">Izgubili ste lozinku?</string>
+ <string name="forums">Forumi</string>
+ <string name="help_center">Centar za pomoć</string>
+ <string name="ssl_certificate_error">Nevažeći SSL certifikat</string>
+ <string name="ssl_certificate_ask_trust">Ako se inače uspijete spojiti sa ovom web stranicom bez problema, ova greška mogla bi značiti da netko pokušava oponašati tu web stranicu, i ne bi trebali nastaviti. Vjerujete certifikat unatoč tome? </string>
+ <string name="username_exists">Ovo korisničko ime već postoji</string>
+ <string name="blog_name_exists">Ova internet stranica već postoji</string>
+ <string name="username_required">Unesite korisničko ime</string>
+ <string name="passcode_wrong_passcode">Pogrešan PIN</string>
+ <string name="invalid_password_message">Lozinka mora sadržavati barem 4 znaka</string>
+ <string name="invalid_username_too_short">Korisničko ime mora biti duže od 4 znaka</string>
+ <string name="invalid_username_too_long">Korisničko ime mora biti kraće od 61 znak</string>
+ <string name="username_only_lowercase_letters_and_numbers">Korisničko ime može sadžavati samo mala slova (a-z) i brojke</string>
+ <string name="username_not_allowed">Korisničko ime nije dopušteno</string>
+ <string name="username_must_be_at_least_four_characters">Korisničko ime mora biti duže od 4 znaka</string>
+ <string name="username_contains_invalid_characters">Korisničko ime ne smije sadržavati karaktere “_”</string>
+ <string name="username_must_include_letters">Korisničko ime mora koristiti barem jedno slovo (a-z)</string>
+ <string name="notifications_empty_list">Nema obavijesti</string>
+ <string name="out_of_memory">Uređaju je iskoristio dostupnu memoriju</string>
+ <string name="no_network_message">Mreža nedostupna</string>
+ <string name="could_not_remove_account">Nije moguće ukloniti web stranicu</string>
+ <string name="gallery_error">Nije moguće dohvatiti medijski zapis.</string>
+ <string name="blog_not_found">Dogodila se greška prilikom pristupa ovom blogu</string>
+ <string name="wait_until_upload_completes">Pričekajte dok se prijenos završi</string>
+ <string name="theme_fetch_failed">Neuspjelo dohvaćanje tema</string>
+ <string name="theme_set_failed">Neuspjelo postavljanje teme</string>
+ <string name="theme_auth_error_message">Provjerite je li imate dopuštenje za postavljanje tema</string>
+ <string name="comments_empty_list">Nema komentara</string>
+ <string name="mnu_comment_unspam">Nije spam</string>
+ <string name="no_site_error">Nije se moguće spojiti na WordPress web stranicu</string>
+ <string name="adding_cat_failed">Neuspjelo dodavanje kategorije</string>
+ <string name="adding_cat_success">Kategorija uspješno dodana</string>
+ <string name="cat_name_required">Polje imena kategorije je obavezno</string>
+ <string name="category_automatically_renamed">Ime kategorije %1$s nije valjano. Preimenovana je u %2$s.</string>
+ <string name="no_account">Nije pronađen WordPress.com račun. Dodajte račun i pokušajte ponovno</string>
+ <string name="sdcard_message">Za prijenos medijskih zapisa potrebna je montirana SD kartica</string>
+ <string name="stats_empty_comments">Još nema komentara</string>
+ <string name="stats_bar_graph_empty">Nema dostupne statistike</string>
+ <string name="error_delete_post">Dogodila se greška prilikom brisanja %s</string>
+ <string name="error_refresh_posts">Trenutno nije moguće osvježiti objave</string>
+ <string name="error_refresh_notifications">Trenutno nije moguće osvježiti obavijesti</string>
+ <string name="error_refresh_pages">Trenutno nije moguće osvježiti stranice</string>
+ <string name="error_refresh_comments">Trenutno nije moguće osvježiti komentare</string>
+ <string name="error_refresh_stats">Trenutno nije moguće osvježiti statistiku</string>
+ <string name="error_generic">Dogodila se greška</string>
+ <string name="error_moderate_comment">Dogodila se greška tijekom moderiranja</string>
+ <string name="error_edit_comment">Dogodila se greška tijekom uređivanja komentara</string>
+ <string name="error_upload">Dogodila se greška tijekom prijenosa %s</string>
+ <string name="error_load_comment">Nije moguće učitati komentar</string>
+ <string name="error_downloading_image">Greška pri preuzimanju slike</string>
+ <string name="reply_failed">Neuspjeli odgovor</string>
+ <string name="username_reserved_but_may_be_available">Navedeno korisničko ime je trenutno rezervirano, ali možda se oslobodi kroz par dana</string>
+ <string name="blog_name_required">Unesite adresu web stranice</string>
+ <string name="blog_name_not_allowed">Adresa web stranice nije dozvoljena</string>
+ <string name="blog_name_must_be_at_least_four_characters">Adresa web stranice mora sadržavati najmanje 4 znaka</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Adresa web stranice ne može sadržavati više od 64 znaka</string>
+ <string name="blog_name_contains_invalid_characters">Adresa web stranice ne može sadržavati znak “_”</string>
+ <string name="blog_name_cant_be_used">Ne možete koristiti navedenu adresu web stranice</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Adresa web stranice može sadržavati samo mala slova (a-z) i brojeve</string>
+ <string name="blog_name_reserved">Navedena web stranica je rezervirana</string>
+ <string name="blog_name_reserved_but_may_be_available">Naveden web stranica je trenutno rezervirana, ali možda se oslobodi kroz par dana</string>
+ <string name="username_or_password_incorrect">Korisničko ime ili lozinka koje ste unijeli nisu točni</string>
+ <string name="nux_cannot_log_in">Prijava nije moguća</string>
+ <string name="invalid_email_message">Vaša adresa e-pošte nije valjana</string>
+ <string name="email_invalid">Unesite ispravnu adresu e-pošte</string>
+ <string name="email_not_allowed">Ta adresa e-pošte nije dopuštena</string>
+ <string name="email_exists">Ova adresa e-pošte se već koristi</string>
+ <string name="invalid_url_message">Provjerite da je uneseni URL ispravan</string>
+ <string name="view_site">Pogledaj stranicu</string>
+ <string name="add_new_category">Dodaj novu kategoriju</string>
+ <string name="category_name">Ime kategorije</string>
+ <string name="trash">Smeće</string>
+ <string name="author_name">Ime autora</string>
+ <string name="author_url">URL autora</string>
+ <string name="hint_comment_content">Komentar</string>
+ <string name="trash_yes">Smeće</string>
+ <string name="preview_page">Predpregled stranice</string>
+ <string name="delete_draft">Obriši skicu</string>
+ <string name="mnu_comment_trash">Smeće</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="create_a_link">Kreiraj poveznicu</string>
+ <string name="reader_title_applog">Aplikacijski zapisnik</string>
+ <string name="site_address">Adresa vaše samostalno hostane web stranice (URL)</string>
+ <string name="add_account_blog_url">Blog adresa</string>
+ <string name="notifications_empty_all">Nema obavijesti... još.</string>
+ <string name="privacy_policy">Pravila privatnosti</string>
+ <string name="open_source_licenses">Licence otvorenog softvera</string>
+ <string name="reader_share_link">Podjeli poveznicu</string>
+ <string name="post_settings">Postavke objave</string>
+ <string name="location_not_found">Nepoznata lokacija</string>
+ <string name="xmlrpc_error">Spajanje nije moguće. Unesite punu putanju do xmlrpc.php na vašoj web stranici.</string>
+ <string name="select_categories">Odaberite kateogrije</string>
+ <string name="account_details">Detalji o računu</string>
+ <string name="edit_post">Uredi objavu</string>
+ <string name="add_comment">Dodaj komentar</string>
+ <string name="connection_error">Greška spajanja</string>
+ <string name="cancel_edit">Otkaži uređivanje</string>
+ <string name="scaled_image_error">Unesite valjanu vrijednost skalirane širine</string>
+ <string name="learn_more">Saznajte više</string>
+ <string name="media_gallery_settings_title">Postavke galerije</string>
+ <string name="media_gallery_image_order">Redoslijed slika</string>
+ <string name="media_gallery_num_columns">Broj stupaca</string>
+ <string name="media_gallery_type_thumbnail_grid">Rešetka sličica</string>
+ <string name="media_gallery_edit">Uredi galeriju</string>
+ <string name="media_error_no_permission">Nemate dopuštenje za pregled galerije medijske zbirke</string>
+ <string name="cannot_delete_multi_media_items">Trenutno se neki medijski zapisi ne mogu izbrisati. Pokušajte ponovno kasnije.</string>
+ <string name="themes_live_preview">Pregled uživo</string>
+ <string name="theme_current_theme">Trenutna tema</string>
+ <string name="theme_premium_theme">Premium tema</string>
+ <string name="link_enter_url_text">Tekst poveznice (opcionalno)</string>
+ <string name="page_settings">Postavke stranice</string>
+ <string name="local_draft">Lokalna skica</string>
+ <string name="upload_failed">Neuspjeli prijenos</string>
+ <string name="horizontal_alignment">Horizontalno poravnanje</string>
+ <string name="delete_post">Obriši objavu</string>
+ <string name="delete_page">Obriši stranicu</string>
+ <string name="comment_status_approved">Odobreno</string>
+ <string name="comment_status_unapproved">Na čekanju</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Smeće</string>
+ <string name="edit_comment">Uredi komentar</string>
+ <string name="mnu_comment_approve">Odobri</string>
+ <string name="mnu_comment_unapprove">Ne odobri</string>
+ <string name="dlg_approving_comments">Odobravanje</string>
+ <string name="dlg_unapproving_comments">Neodobravanje</string>
+ <string name="dlg_spamming_comments">Označavanje kao spam</string>
+ <string name="dlg_trashing_comments">Slanje u smeće</string>
+ <string name="dlg_confirm_trash_comments">Poslati u smeće?</string>
+ <string name="trash_no">Ne poslati u smeće</string>
+ <string name="saving_changes">Snimanje promjena</string>
+ <string name="sure_to_cancel_edit_comment">Otkazati uređivanje komentara?</string>
+ <string name="content_required">Komentar je obavezan</string>
+ <string name="toast_comment_unedited">Komentar nije promjenjen</string>
+ <string name="remove_account">Ukloni web stranicu</string>
+ <string name="blog_removed_successfully">Web stranica uspješno uklonjena</string>
+ <string name="preview_post">Predpregled objave</string>
+ <string name="comment_added">Uspješno dodan komentar</string>
+ <string name="post_not_published">Status objave nije objavljen</string>
+ <string name="page_not_published">Status stranice nije objavljen</string>
+ <string name="view_in_browser">Pregled u pregledniku</string>
+ <string name="category_slug">Slug kategorije (opcionalno)</string>
+ <string name="category_desc">Opis kategorije (opcionalno)</string>
+ <string name="category_parent">Matična kategorija (opcionalno)</string>
+ <string name="share_action_post">Nova objava</string>
+ <string name="share_action_media">Medijski zapisi</string>
+ <string name="pending_review">Čeka moderaciju</string>
+ <string name="post_format">Format objave</string>
+ <string name="new_post">Nova objava</string>
+ <string name="new_media">Novi medijski zapis</string>
+ <string name="local_changes">Lokalne promjene</string>
+ <string name="image_settings">Postavke slike</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="reader_toast_err_add_tag">Nije moguće dodati ovu oznaku</string>
+ <string name="reader_toast_err_remove_tag">Nije moguće ukloniti ovu oznaku</string>
+ <string name="required_field">Obavezno polje</string>
+ <string name="email_hint">Adresa e-pošte</string>
+ <string name="blog_name_invalid">Neispravna adresa web stranice</string>
+ <string name="blog_title_invalid">Neispravno ime web stranice</string>
+ <string name="blog_name_must_include_letters">Adresa web stranice mora sadržavati najmanje 1 slovo (a-z)</string>
+ <string name="author_email">E-pošta autora</string>
+ <string name="post_not_found">Dogodila se greška tijekom učitavanja objave. Osvježite objave i pokušajte ponovno.</string>
+ <string name="file_not_found">Nije moguće pronaći medijski zapis za prijenos. Jeli obrisan ili premješten?</string>
+ <string name="file_error_create">Nije moguće kreirati privremeni zapis za prijenos medijskog zapisa. Provjerite da ima dovoljno slobodnog mjesta na vašem uređaju.</string>
+ <string name="http_credentials">HTTP akreditiv (opcionalno)</string>
+ <string name="http_authorization_required">Potrebna autorizacija</string>
+ <string name="error_blog_hidden">Blog je skriven i nije ga moguće učitati. Ponovno ga omogućite u postavkama i pokušajte ponovno.</string>
+ <string name="fatal_db_error">Pojavila se greška prilikom kreiranja app baze podataka. Pokušajte ponovno instalirati app.</string>
+ <string name="jetpack_message_not_admin">Za statistiku je potreban Jetpack dodatak. Kontaktirajte administratora web stranice.</string>
+ <string name="email_cant_be_used_to_signup">Ne možete koristiti tu adresu e-pošte za registraciju. Imamo problema sa njima jer blokiraju neke naše poruke. Koristite drugog pružatelja adrese e-pošte.</string>
+ <string name="email_reserved">Navedena adresa e-pošte se već koristi. Provjerite vaš dolaznu poštu za aktivacijsku poruku. Ako ne aktivirate, možete ponovno pokušati za par dana.</string>
+ <string name="share_url_post">Podjeli objavu</string>
+ <string name="share_url_page">Podjeli članak</string>
+ <string name="deleting_page">Brišem stranicu</string>
+ <string name="deleting_post">Brišem objavu</string>
+ <string name="share_link">Podjeli poveznicu</string>
+ <string name="creating_your_account">Napravi svoj račun</string>
+ <string name="creating_your_site">Napravi svoju stranicu</string>
+ <string name="reader_empty_posts_in_tag_updating">Dohvaćanje objava...</string>
+ <string name="error_refresh_media">Nešto je pošlo krivo tijekom osvježavanja medijskih zapisa. Pokušajte ponovno kasnije.</string>
+ <string name="reader_likes_you_and_multi">Vi i %,d drugih se ovo sviđa</string>
+ <string name="reader_likes_multi">%,d ljudi se ovo sviđa</string>
+ <string name="reader_toast_err_get_comment">Nije moguće dohvatiti komentare</string>
+ <string name="reader_label_reply">Odgovor</string>
+ <string name="video">Video</string>
+ <string name="download">Preuzimanje medijskih zapisa</string>
+ <string name="comment_spammed">Komentar označen kao spam</string>
+ <string name="cant_share_no_visible_blog">Ne možete dijeliti na WordPressu bez vidljivog bloga</string>
+ <string name="select_date">Odaberi datum</string>
+ <string name="pick_photo">Odaberi sliku</string>
+ <string name="select_time">Odaberi vrijeme</string>
+ <string name="pick_video">Odaberi video</string>
+ <string name="reader_toast_err_get_post">Nije moguće dohvatiti ovu objavu</string>
+ <string name="validating_user_data">Provjeravanje korisničkih podataka</string>
+ <string name="validating_site_data">Provjeravanje podataka web stranice</string>
+ <string name="reader_likes_you_and_one">Vama i još jednoj osobi ovo se sviđa</string>
+ <string name="reader_empty_followed_blogs_description">Ne brinite, samo pritisnite ikonu u gore desno za početak istraživanja!</string>
+ <string name="account_two_step_auth_enabled">Ovaj račun ima omogućenu provjeru u dva koraka. Posjetite vaše sigurnosne postavke na WordPress.com i generirajte lozinku za ovu aplikaciju.</string>
+ <string name="nux_oops_not_selfhosted_blog">Prijavite se u WordPress.com</string>
+ <string name="nux_add_selfhosted_blog">Dodaj samostalno hostanu web stranicu</string>
+ <string name="nux_welcome_create_account">Kreiraj račun</string>
+ <string name="nux_tap_continue">Nastavi</string>
+ <string name="signing_in">Prijavljivanje...</string>
+ <string name="password_invalid">Trebate sigurniju lozinku. Koristite 7 ili više znakova, miješana mala i velika slova, brojeve i specijalne znakove.</string>
+ <string name="media_add_new_media_gallery">Kreiraj galeriju</string>
+ <string name="create_account_wpcom">Kreiraj račun na WordPress.com</string>
+ <string name="reader_btn_follow">Prati</string>
+ <string name="reader_empty_followed_tags">Ne pratite ni jednu oznaku</string>
+ <string name="media_add_popup_title">Dodaj u medijske zapise</string>
+ <string name="empty_list_default">Popis je prazan</string>
+ <string name="select_from_media_library">Odaberi iz medijskih zapisa</string>
+ <string name="jetpack_not_found">Dodatak Jetpack nije pronađen</string>
+ <string name="reader_untitled_post">(Bez naslova)</string>
+ <string name="reader_btn_share">Podjeli</string>
+ <string name="reader_btn_unfollow">Prati se</string>
+ <string name="reader_hint_comment_on_comment">Odgovor na komentar...</string>
+ <string name="reader_label_added_tag">Dodano %s</string>
+ <string name="reader_label_removed_tag">Uklonjeno %s</string>
+ <string name="reader_likes_one">Jednoj osobi se ovo sviđa</string>
+ <string name="reader_likes_only_you">Vama se ovo sviđa</string>
+ <string name="reader_toast_err_comment_failed">Vaš komentar nije moguće objavaiti</string>
+ <string name="reader_toast_err_tag_exists">Već pratite ovu oznaku</string>
+ <string name="reader_toast_err_tag_invalid">To nije ispravna oznaka</string>
+ <string name="reader_toast_err_share_intent">Nije moguće podijeliti</string>
+ <string name="reader_toast_err_view_image">Nije moguće prikazati sliku</string>
+ <string name="reader_toast_err_url_intent">Nije moguće otvoriti %s</string>
+ <string name="connecting_wpcom">Spajanje sa WordPress.com</string>
+ <string name="username_invalid">Neispravno korisničko ime</string>
+ <string name="nux_tutorial_get_started_title">Započnimo!</string>
+ <string name="jetpack_message">Za statistiku je potreban dodatak Jetpack. Želite li instalirati Jetpack?</string>
+ <string name="reader_share_subject">Podijeljeno sa %s</string>
+ <string name="limit_reached">Dosegnuti je limit. Možete ponovno pokušati za 1 minutu. Ako pokušate prije toga produžiti ćete vrijeme koje morate čekati prije nego što se digne ban. Ako mislite da je ovo greška, kontaktirajte podršku.</string>
+ <string name="stats_timeframe_days">Dani</string>
+ <string name="stats_timeframe_weeks">Tjedni</string>
+ <string name="stats_timeframe_months">Mjeseci</string>
+ <string name="stats_entry_country">Država</string>
+ <string name="stats_entry_posts_and_pages">Naslov</string>
+ <string name="stats_entry_tags_and_categories">Tema</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Preporučitelj</string>
+ <string name="stats_totals_views">Pregleda</string>
+ <string name="stats_totals_clicks">Klikova</string>
+ <string name="stats_view_referrers">Preporučitelji</string>
+ <string name="stats_timeframe_today">Danas</string>
+ <string name="stats_timeframe_yesterday">Jučer</string>
+ <string name="theme_activate_button">Aktiviraj</string>
+ <string name="media_edit_success">Ažuriran</string>
+ <string name="all">Sve</string>
+ <string name="stats_view_tags_and_categories">Oznake i kategorije</string>
+ <string name="passcode_preference_title">Zaključavanje PIN-om</string>
+ <string name="passcode_change_passcode">Promjeni PIN</string>
+ <string name="themes">Teme</string>
+ <string name="post_excerpt">Izvadak</string>
+ <string name="stats_totals_plays">Reprodukcije</string>
+ <string name="share_action_title">Dodaj u ...</string>
+ <string name="share_action">Podjeli</string>
+ <string name="stats">Statistika</string>
+ <string name="stats_view_visitors_and_views">Posjetitelji i pregledi</string>
+ <string name="stats_view_clicks">Klikovi</string>
+ <string name="images">Slike</string>
+ <string name="custom_date">Prilagođeni datum</string>
+ <string name="media_add_popup_capture_photo">Uslikajte fotografiju</string>
+ <string name="media_add_popup_capture_video">Snimite video</string>
+ <string name="media_gallery_image_order_random">Nasumično</string>
+ <string name="media_gallery_image_order_reverse">Obrnuti</string>
+ <string name="media_gallery_type">Tip</string>
+ <string name="media_gallery_type_squares">Kvadrati</string>
+ <string name="media_gallery_type_tiled">Pločice</string>
+ <string name="media_gallery_type_circles">Kružnice</string>
+ <string name="media_gallery_type_slideshow">Dijaprojekcija</string>
+ <string name="media_edit_title_text">Naslov</string>
+ <string name="media_edit_description_text">Opis</string>
+ <string name="media_edit_caption_text">Podnatpis</string>
+ <string name="media_edit_title_hint">Ovdje unesite naslov</string>
+ <string name="media_edit_caption_hint">Ovdje unesite podnaslov</string>
+ <string name="media_edit_description_hint">Ovdje unesite opis</string>
+ <string name="media_edit_failure">Neuspjelo ažuriranje</string>
+ <string name="themes_details_label">Detalji</string>
+ <string name="themes_features_label">Značajke</string>
+ <string name="theme_activating_button">Aktiviranje</string>
+ <string name="theme_set_success">Uspješno postavljena tema!</string>
+ <string name="theme_auth_error_title">Nije moguće dohvatiti teme</string>
+ <string name="passcode_manage">Upravljanje PIN zaključavanjem</string>
+ <string name="passcode_enter_passcode">Unesite PIN</string>
+ <string name="passcode_enter_old_passcode">Unesite stari PIN</string>
+ <string name="passcode_re_enter_passcode">Ponovno unesite PIN</string>
+ <string name="passcode_set">PIN set</string>
+ <string name="passcode_turn_off">Isključi zaključavanje PIN-om</string>
+ <string name="passcode_turn_on">Uključi zaključavanje PIN-om</string>
+ <string name="unattached">Nepriloženo</string>
+ <string name="upload">Prijenos</string>
+ <string name="sign_in">Prijavi se</string>
+ <string name="notifications">Obavijesti</string>
+ <string name="new_notifications">%d novih obavijesti</string>
+ <string name="more_notifications">i %d više.</string>
+ <string name="note_reply_successful">Odgovor objavljen</string>
+ <string name="follows">Praćenje</string>
+ <string name="loading">Učitavanje...</string>
+ <string name="httppassword">HTTP lozinka</string>
+ <string name="httpuser">HTTP korisničko ime</string>
+ <string name="error_media_upload">Dogodila se greška tijekom prijenosa medijskog zapisa</string>
+ <string name="publish_date">Objavi</string>
+ <string name="content_description_add_media">Dodaj medijski zapis</string>
+ <string name="post_content">Sadržaj (dodirnite za dodavanje teksta i medijskih zapisa)</string>
+ <string name="incorrect_credentials">Neispravno korisničko ime ili lozinka.</string>
+ <string name="password">Lozinka</string>
+ <string name="username">Korisničko ime</string>
+ <string name="reader">Čitač</string>
+ <string name="no_network_title">Nema dostupne mreže</string>
+ <string name="width">Širina</string>
+ <string name="anonymous">Anonimno</string>
+ <string name="page">Stranica</string>
+ <string name="pages">Stranice</string>
+ <string name="caption">Opis (opcionalan)</string>
+ <string name="featured">Upotrijebi kao istaknutu sliku</string>
+ <string name="posts">Objave</string>
+ <string name="featured_in_post">Uključi sliku u sadržaj objave</string>
+ <string name="post">Objava</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">blogusername</string>
+ <string name="upload_scaled_image">Prenesi i poveži do dimenzionirane slike</string>
+ <string name="scaled_image">Širina dimenzionirane slike</string>
+ <string name="scheduled">Zakazano</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Prijenos...</string>
+ <string name="version">Inačica</string>
+ <string name="tos">Uvjeti Korištenja</string>
+ <string name="app_title">WordPress za Android</string>
+ <string name="max_thumbnail_px_width">Zadana širina slike</string>
+ <string name="image_alignment">Poravnanje</string>
+ <string name="refresh">Osvježi</string>
+ <string name="untitled">Nenaslovljeno</string>
+ <string name="edit">Uredi</string>
+ <string name="page_id">Stranica</string>
+ <string name="post_id">Objava</string>
+ <string name="post_password">Lozinka (opcionalno)</string>
+ <string name="immediately">Odmah</string>
+ <string name="quickpress_add_alert_title">Postavi naziv prečaca</string>
+ <string name="today">Danas</string>
+ <string name="settings">Postavke</string>
+ <string name="share_url">Podijeli URL</string>
+ <string name="quickpress_window_title">Odaberi blog za QuickPress prečac</string>
+ <string name="quickpress_add_error">Naziv prečaca ne može biti prazan</string>
+ <string name="publish_post">Objavi</string>
+ <string name="draft">Skica</string>
+ <string name="post_private">Privatno</string>
+ <string name="upload_full_size_image">Prenesi i poveži do original slike</string>
+ <string name="title">Naslov</string>
+ <string name="tags_separate_with_commas">Oznake (razdvoji oznake zarezima)</string>
+ <string name="categories">Kategorije</string>
+ <string name="dlg_deleting_comments">Brisanje komentara</string>
+ <string name="notification_blink">Trepereće svjetlo za obavijesti</string>
+ <string name="notification_vibrate">Vibracija</string>
+ <string name="notification_sound">Zvučna obavijest </string>
+ <string name="status">Status</string>
+ <string name="location">Lokacija</string>
+ <string name="sdcard_title">Neophodna SD kartica</string>
+ <string name="select_video">Odaberi video iz galerije</string>
+ <string name="media">Medijski zapisi</string>
+ <string name="delete">Izbriši</string>
+ <string name="none">Ništa</string>
+ <string name="blogs">Blogovi</string>
+ <string name="select_photo">Odaberi sliku iz galerije</string>
+ <string name="error">Pogreška</string>
+ <string name="add">Dodaj</string>
+ <string name="reply">Odgovori</string>
+ <string name="yes">Da</string>
+ <string name="no">Ne</string>
+ <string name="cancel">Odustani</string>
+ <string name="on">u</string>
+ <string name="save">Snimi</string>
+ <string name="category_refresh_error">Pogreška u osvježavanju kategorija</string>
+ <string name="preview">Pretpregled</string>
+ <string name="notification_settings">Postavke obavijesti</string>
+</resources>
diff --git a/WordPress/src/main/res/values-hu/strings.xml b/WordPress/src/main/res/values-hu/strings.xml
new file mode 100644
index 000000000..ace869d3a
--- /dev/null
+++ b/WordPress/src/main/res/values-hu/strings.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="delete_sure_post">Bejegyzés törlése</string>
+ <string name="delete_sure">Vázlat törlése</string>
+ <string name="delete_sure_page">Oldal törlése</string>
+ <string name="confirm_delete_multi_media">Kiválasztott sablon törlése?</string>
+ <string name="confirm_delete_media">Kiválasztott tétel törlése?</string>
+ <string name="cab_selected">%d kiválasztva</string>
+ <string name="reader_empty_posts_liked">Még egy bejegyzés sincs tetszettként jelölve</string>
+ <string name="faq_button">GYIK</string>
+ <string name="browse_our_faq_button">GYIK tallózása</string>
+ <string name="create_new_blog_wpcom">WordPress.com honlap létrehozása</string>
+ <string name="new_blog_wpcom_created">WordPress.com honlap létrehozva!</string>
+ <string name="reader_empty_comments">Vélemény?</string>
+ <string name="reader_empty_posts_in_tag">Ezzel a címkével nincs bejegyzés jelölve</string>
+ <string name="reader_label_comment_count_multi">%,d hozzászólás</string>
+ <string name="error_refresh_unauthorized_posts">A bejegyzés megtekintéséhez vagy szerkesztéséhez nincs megfelelő jogosultság</string>
+ <string name="error_refresh_unauthorized_pages">Az oldalak megtekintéséhez vagy szerkesztéséhez nincs megfelelő jogosultság</string>
+ <string name="error_refresh_unauthorized_comments">A hozzászólások megtekintéséhez vagy szerkesztéséhez nincs megfelelő jogosultság</string>
+ <string name="older_month">Egy hónapnál régebbi</string>
+ <string name="more">Bövebben</string>
+ <string name="older_two_days">2 napnál régebbi</string>
+ <string name="older_last_week">Egy hétnél régebbi</string>
+ <string name="pages_empty_list">Még nincs egy oldal sem. Létrehozzunk egyet?</string>
+ <string name="media_empty_list_custom_date">Ebben az időintervallumban nincs média</string>
+ <string name="reader_menu_block_blog">Blog tiltása</string>
+ <string name="reader_toast_err_generic">A művelet végrehajtása nem sikerült</string>
+ <string name="reader_toast_err_block_blog">Nem sikerült a blog letiltása</string>
+ <string name="reader_toast_blog_blocked">Ennek a blognak a bejegyzései a továbbiakban nem jelennek meg</string>
+ <string name="contact_us">Kapcsolat</string>
+ <string name="hs__invalid_email_error">Adjunk meg valós e-mail címet</string>
+ <string name="hs__username_blank_error">Adjunk meg valós nevet</string>
+ <string name="hs__conversation_detail_error">Írjuk le a problémát, amivel találkoztunk</string>
+ <string name="hs__new_conversation_header">Támogató chat</string>
+ <string name="hs__conversation_header">Támogató chat</string>
+ <string name="add_location">Hely hozzáadás</string>
+ <string name="current_location">Jelenlegi hely</string>
+ <string name="edit_location">Szerkeszt</string>
+ <string name="search_location">Keresés</string>
+ <string name="search_current_location">Hely meghatározás</string>
+ <string name="preference_send_usage_stats">Statisztika küldés</string>
+ <string name="update_verb">Frissítést</string>
+ <string name="schedule_verb">Időzítés</string>
+ <string name="reader_title_subs">Címkék és blogok</string>
+ <string name="reader_label_followed_blog">Követett blogok</string>
+ <string name="reader_page_followed_tags">Követett címkék</string>
+ <string name="reader_toast_err_follow_blog">Nem lehetséges ezt a blogot követni</string>
+ <string name="reader_empty_recommended_blogs">Nincsenek ajánlott blogok</string>
+ <string name="reader_toast_err_already_follow_blog">Ezt a blogot már követjük</string>
+ <string name="reader_toast_err_unfollow_blog">Nem sikerült ezt a blogot követni</string>
+ <string name="saving">Mentés...</string>
+ <string name="media_empty_list">Nincs média</string>
+ <string name="ptr_tip_message">Tipp: A frissítéshez húzzuk le</string>
+ <string name="help">Segítség</string>
+ <string name="forgot_password">Elfelejtett jelszó?</string>
+ <string name="forums">Fórumok</string>
+ <string name="help_center">Támogatói központ</string>
+ <string name="ssl_certificate_error">Érvénytelen SSL hitelesítés</string>
+ <string name="out_of_memory">Az eszköz memóriája megtelt</string>
+ <string name="comments_empty_list">Nincsenek hozzászólások</string>
+ <string name="mnu_comment_unspam">Nem spam</string>
+ <string name="no_network_message">Nincs elérehtő hálózat</string>
+ <string name="stats_empty_comments">Nincsenek még hozzászólások</string>
+ <string name="sdcard_message">SD kártya csatlakoztatása szükséges a média feltötéshez</string>
+ <string name="error_downloading_image">Kép letöltési hiba</string>
+ <string name="invalid_email_message">Az email cím hibás</string>
+ <string name="error_refresh_comments">A hozzászólásokat most nem lehet frissíténi</string>
+ <string name="username_must_be_at_least_four_characters">Felhasználó névnek legalább 4 karakter hosszúnak kell lenni</string>
+ <string name="error_refresh_notifications">Értesitéseket most nem lehet frissíteni</string>
+ <string name="gallery_error">Nem sikerült a média elemet letölteni</string>
+ <string name="theme_fetch_failed">A sablonok lekérdezése nem sikerült</string>
+ <string name="notifications_empty_list">Nincsenek értesítések</string>
+ <string name="stats_bar_graph_empty">Nincsenek elérhető statisztikák</string>
+ <string name="blog_name_not_allowed">Ez a weboldal cím nem engedélyezett</string>
+ <string name="email_invalid">Adjunk meg egy valós e-mail címet</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="author_name">Szerző neve</string>
+ <string name="delete_draft">Vázlat törlése</string>
+ <string name="category_parent">Kategória szülő (opcionális)</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="media_gallery_settings_title">Galléria beálitások</string>
+ <string name="preview_post">Bejegyzés előnézet</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="account_details">Felhasználói fiók részletei</string>
+ <string name="preview_page">Oldal előnézet</string>
+ <string name="email_hint">E-mail cím</string>
+ <string name="theme_premium_theme">Prémium sablon</string>
+ <string name="hint_comment_content">Hozzászólás</string>
+ <string name="comment_added">Sikeres hozzászólás</string>
+ <string name="author_email">Szerző e-mail</string>
+ <string name="category_desc">Kategória leírás (nem kötelező)</string>
+ <string name="mnu_comment_trash">Lomtár</string>
+ <string name="media_error_no_permission">Nincs elég jogosultság a médiatár megtekitéséhez</string>
+ <string name="share_action_post">Új bejegyzés</string>
+ <string name="post_settings">Bejegyzés beálitások</string>
+ <string name="upload_failed">Sikertelen feltöltés</string>
+ <string name="edit_comment">Hozzászólás szerkesztése</string>
+ <string name="dlg_trashing_comments">Lomtárba küldés</string>
+ <string name="pending_review">Áttekintésre várakozik</string>
+ <string name="download">Média letöltés folyamatban</string>
+ <string name="media_add_new_media_gallery">Galéria létrehozása</string>
+ <string name="reader_btn_follow">Követés</string>
+ <string name="empty_list_default">Ez a lista üres</string>
+ <string name="themes">Sablonok</string>
+ <string name="all">Összes</string>
+ <string name="custom_date">Egyedi dátum</string>
+ <string name="media_add_popup_capture_photo">Fénykép készítése</string>
+ <string name="media_add_popup_capture_video">Videó készítése</string>
+ <string name="media_gallery_image_order_random">Véletlenszerű</string>
+ <string name="media_gallery_type">Típus</string>
+ <string name="media_gallery_image_order_reverse">Megfordítás</string>
+ <string name="media_edit_caption_text">Felirat</string>
+ <string name="media_edit_description_text">Leírás</string>
+ <string name="media_edit_title_hint">Címsor megadása</string>
+ <string name="media_edit_success">Frissítve</string>
+ <string name="theme_activating_button">Aktiválás</string>
+ <string name="post_excerpt">Kivonat</string>
+ <string name="stats_view_referrers">Hivatkozások</string>
+ <string name="stats_timeframe_today">Ma</string>
+ <string name="stats_timeframe_yesterday">Tegnap</string>
+ <string name="stats_view_clicks">Kattintások</string>
+ <string name="stats_view_tags_and_categories">Címkék és kategóriák</string>
+ <string name="stats_entry_tags_and_categories">Téma</string>
+ <string name="stats_entry_authors">Szerző</string>
+ <string name="themes_details_label">Részletek</string>
+ <string name="themes_features_label">Funkciók</string>
+ <string name="media_gallery_type_squares">Négyzetek</string>
+ <string name="theme_activate_button">Bekapcsolás</string>
+ <string name="media_gallery_type_circles">Körök</string>
+ <string name="media_edit_caption_hint">A feliratot itt lehet megadni</string>
+ <string name="media_edit_description_hint">A leírást itt lehet megadni</string>
+ <string name="media_edit_failure">A feltöltés nem sikerült</string>
+ <string name="theme_set_success">Sikerült beállítani a sablont!</string>
+ <string name="share_action_title">Hozzáadás...</string>
+ <string name="share_action">Megosztás</string>
+ <string name="stats_view_visitors_and_views">Látogatók és megtekintések</string>
+ <string name="stats_timeframe_days">nap</string>
+ <string name="stats_timeframe_weeks">hét</string>
+ <string name="stats_timeframe_months">hónap</string>
+ <string name="stats_entry_country">ország</string>
+ <string name="stats_entry_referrers">Hivatkozók</string>
+ <string name="stats_totals_views">Megtekintések</string>
+ <string name="stats_totals_clicks">Kattintások</string>
+ <string name="stats_totals_plays">Lejátszások</string>
+ <string name="passcode_manage">Azonosító kód kezelése</string>
+ <string name="passcode_enter_passcode">Azonosító kód megadása</string>
+ <string name="passcode_enter_old_passcode">Régi azonosító kód megadása</string>
+ <string name="passcode_re_enter_passcode">Azonosító kód újbóli megadása</string>
+ <string name="passcode_change_passcode">Azonosító kód változtatása</string>
+ <string name="passcode_set">Azonosító kód megadva</string>
+ <string name="passcode_turn_off">Azonosító kódos védelem kikapcsolása</string>
+ <string name="passcode_turn_on">Azonosító kódos védelem bekapcsolása</string>
+ <string name="stats_entry_posts_and_pages">Fejléc</string>
+ <string name="media_gallery_type_slideshow">Diavetítés</string>
+ <string name="media_gallery_type_tiled">Mozaik</string>
+ <string name="unattached">Független</string>
+ <string name="media_edit_title_text">Cím</string>
+ <string name="images">Kép</string>
+ <string name="theme_auth_error_title">Nem sikerült a sablonok betöltése</string>
+ <string name="stats">Statisztika</string>
+ <string name="passcode_preference_title">Azonosítókód lezárás</string>
+ <string name="upload">Feltöltés</string>
+ <string name="discard">Elvetés</string>
+ <string name="sign_in">Bejelentkezés</string>
+ <string name="notifications">Értesítések</string>
+ <string name="note_reply_successful">Válasz elküldve</string>
+ <string name="new_notifications">%d új értesítés</string>
+ <string name="more_notifications">és %d további.</string>
+ <string name="loading">Betöltés</string>
+ <string name="httpuser">HTTP felhasználónév</string>
+ <string name="httppassword">HTTP jelszó</string>
+ <string name="error_media_upload">Hiba történt a média feltöltése közben</string>
+ <string name="publish_date">Közzététel</string>
+ <string name="content_description_add_media">Média hozzáadása</string>
+ <string name="post_content">Tartalom (éritsük meg szöveg és média hozzáadásához)</string>
+ <string name="incorrect_credentials">Hibás felhasználónév vagy jelszó.</string>
+ <string name="password">Jelszó</string>
+ <string name="username">Felhasználó név</string>
+ <string name="reader">Olvasó</string>
+ <string name="featured">Használat kiemelt képként</string>
+ <string name="pages">Oldalak</string>
+ <string name="no_network_title">Nincs elérhető hálózat</string>
+ <string name="width">Szélesség</string>
+ <string name="anonymous">Névtelen</string>
+ <string name="caption">Felirat (nem kötelező)</string>
+ <string name="post">Bejegyzés</string>
+ <string name="page">Oldal</string>
+ <string name="featured_in_post">Kép beillesztése a bejegyzés tartalmába</string>
+ <string name="posts">Bejegyzés</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">honlap-felhasználó</string>
+ <string name="upload_scaled_image">Feltöltés és hivatkozás az átméretezett képre</string>
+ <string name="scaled_image">Átméretezett kép szélessége</string>
+ <string name="scheduled">Időzítve</string>
+ <string name="link_enter_url">URL</string>
+ <string name="tos">Szolgáltatási feltételek</string>
+ <string name="version">verzió</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="image_alignment">Kép igazítás</string>
+ <string name="refresh">Frissítés</string>
+ <string name="untitled">Cím nélküli</string>
+ <string name="edit">Szerkesztés</string>
+ <string name="post_id">Bejegyzés</string>
+ <string name="page_id">Oldal</string>
+ <string name="post_password">Jelszó (opcionális)</string>
+ <string name="immediately">Azonnal</string>
+ <string name="quickpress_add_alert_title">Hivatkozás neve</string>
+ <string name="today">Ma</string>
+ <string name="settings">Beállítások</string>
+ <string name="share_url">URL megosztása</string>
+ <string name="quickpress_add_error">A hivatkozás neve nem lehet üres</string>
+ <string name="quickpress_window_title">Válasszunk egy honlapot a QuickPress rövidcímhez</string>
+ <string name="draft">Vázlat</string>
+ <string name="post_private">Magánjellegű</string>
+ <string name="publish_post">Közzétéve</string>
+ <string name="upload_full_size_image">Feltöltés és hivatkozás a teljes képhez</string>
+ <string name="tags_separate_with_commas">Címkék (vesszővel elválasztva)</string>
+ <string name="categories">Kategóriák</string>
+ <string name="title">Címsor</string>
+ <string name="notification_blink">Figyelmeztető fényjelzés</string>
+ <string name="notification_vibrate">Rezgés</string>
+ <string name="status">Állapot</string>
+ <string name="sdcard_title">SD kártya szükséges</string>
+ <string name="select_video">Videó választása a galériából</string>
+ <string name="location">Földrajzi hely</string>
+ <string name="media">Média</string>
+ <string name="delete">Törlés</string>
+ <string name="none">Egyik sem</string>
+ <string name="blogs">Honlapok</string>
+ <string name="select_photo">Válasszunk egy képet a galériából</string>
+ <string name="error">Hiba</string>
+ <string name="cancel">Megszakítás</string>
+ <string name="save">Mentés</string>
+ <string name="add">Hozzáad</string>
+ <string name="category_refresh_error">Kategóriák frissítése sikertelen</string>
+ <string name="on"> -</string>
+ <string name="reply">Válasz</string>
+ <string name="yes">Igen</string>
+ <string name="no">Nem</string>
+ <string name="preview">Előnézet</string>
+</resources>
diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml
new file mode 100644
index 000000000..1010ca7ac
--- /dev/null
+++ b/WordPress/src/main/res/values-id/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Penulis</string>
+ <string name="role_contributor">Kontributor</string>
+ <string name="role_follower">Pengikut</string>
+ <string name="role_viewer">Penonton</string>
+ <string name="error_post_my_profile_no_connection">Tak ada koneksi, gagal menyimpan profil Anda</string>
+ <string name="alignment_none">Tak ada</string>
+ <string name="alignment_left">Kiri</string>
+ <string name="alignment_right">Kanan</string>
+ <string name="site_settings_list_editor_action_mode_title">Memilih %1$d</string>
+ <string name="error_fetch_users_list">Gagal menampilkan pengguna situs</string>
+ <string name="plans_manage">Kelola paket Anda di \nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Anda belum memiliki satupun pengunjung.</string>
+ <string name="title_follower">Pengikut</string>
+ <string name="title_email_follower">Pengikut Email</string>
+ <string name="people_fetching">Mengambil pengguna...</string>
+ <string name="people_empty_list_filtered_email_followers">Anda belum memiliki satupun pengikut email.</string>
+ <string name="people_empty_list_filtered_followers">Anda belum memiliki satupun pengikut.</string>
+ <string name="people_empty_list_filtered_users">Anda belum memiliki satupun pengguna.</string>
+ <string name="people_dropdown_item_email_followers">Pengikut Email</string>
+ <string name="people_dropdown_item_viewers">Pengunjung</string>
+ <string name="people_dropdown_item_followers">Pengikut</string>
+ <string name="people_dropdown_item_team">Tim</string>
+ <string name="invite_message_usernames_limit">Undang hingga 10 alamat email dan/atau nama pengguna WordPress.com. Mereka yang membutuhkan nama pengguna akan menerima instruksi untuk mendapatkannya.</string>
+ <string name="viewer_remove_confirmation_message">Jika Anda menghapus pengunjung ini, ia tak akan bisa mengunjungi situs ini lagi.\n\nApakah Anda masih ingin menghapus pengunjung ini?</string>
+ <string name="follower_remove_confirmation_message">Jika dihapus, pengunjung ini akan berhenti menerima notifikasi mengenai situs ini, kecuali mereka mengikuti lagi.\n\nApakah Anda masih ingin menghapus pengunjung ini?</string>
+ <string name="follower_subscribed_since">Sejak %1$s</string>
+ <string name="reader_label_view_gallery">Lihat Galeri</string>
+ <string name="error_remove_follower">Gagal menghapus pengikut</string>
+ <string name="error_remove_viewer">Gagal menghapus pengunjung</string>
+ <string name="error_fetch_email_followers_list">Gagal menampilkan pengikut email situs</string>
+ <string name="error_fetch_followers_list">Gagal menampilkan pengikut situs</string>
+ <string name="editor_failed_uploads_switch_html">Beberapa media gagal diunggah. Anda bisa pindah ke mode HTML\n dalam situasi begini. Hapus semua unggahan yang gagal dan lanjutkan?</string>
+ <string name="format_bar_description_html">Mode HTML</string>
+ <string name="visual_editor">Editor Visual</string>
+ <string name="image_thumbnail">Miniatur gambar</string>
+ <string name="format_bar_description_ul">Daftar tak bernomor</string>
+ <string name="format_bar_description_ol">Daftar bernomor</string>
+ <string name="format_bar_description_more">Sisipkan lagi</string>
+ <string name="format_bar_description_media">Sisipkan media</string>
+ <string name="format_bar_description_strike">Coretan</string>
+ <string name="format_bar_description_quote">Blok kutipan</string>
+ <string name="format_bar_description_link">Sisipkan tautan</string>
+ <string name="format_bar_description_underline">Garis bawah</string>
+ <string name="format_bar_description_italic">Miring</string>
+ <string name="image_settings_save_toast">Perubahan tersimpan</string>
+ <string name="image_caption">Keterangan gambar</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Tautkan ke</string>
+ <string name="image_width">Lebar</string>
+ <string name="format_bar_description_bold">Tebal</string>
+ <string name="image_settings_dismiss_dialog_title">Buang perubahan yang belum disimpan?</string>
+ <string name="stop_upload_dialog_title">Stop mengunggah?</string>
+ <string name="stop_upload_button">Stop Pengunggahan</string>
+ <string name="alert_error_adding_media">Terjadi galat saat menyisipkan media</string>
+ <string name="alert_action_while_uploading">Anda sedang mengunggah media. Mohon tunggu hingga selesai.</string>
+ <string name="alert_insert_image_html_mode">Gagal menyisipkan media dalam mode HTML. Silakan pindah ke mode visual.</string>
+ <string name="uploading_gallery_placeholder">Mengunggah galeri...</string>
+ <string name="invite_sent">Undangan berhasil terkirim</string>
+ <string name="tap_to_try_again">Ketuk untuk mencoba lagi!</string>
+ <string name="invite_error_some_failed">Undangan terkirim namun terjadi galat!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">Terjadi sebuah galat saat mencoba mengirimkan undangan!</string>
+ <string name="invite_error_invalid_usernames_multiple">Gagal mengirim: Ada nama pengguna atau email yang tak valid</string>
+ <string name="invite_error_invalid_usernames_one">Gagal mengirim: Ada sebuah nama pengguna atau email yang tak valid</string>
+ <string name="invite_error_no_usernames">Mohon tambahkan sedikitnya satu nama pengguna</string>
+ <string name="invite_message_info">(Opsional) Anda bisa memasukkan sebuah pesan sepanjang 500 karakter yang akan disertakan dalam undangan bagi para pengguna.</string>
+ <string name="invite_message_remaining_other">%d karakter tersisa</string>
+ <string name="invite_message_remaining_one">1 karakter tersisa</string>
+ <string name="invite_message_remaining_zero">0 karakter tersisa</string>
+ <string name="invite_invalid_email">Alamat email \'%s\' invalid</string>
+ <string name="invite_message_title">Pesan Kustom</string>
+ <string name="invite_already_a_member">Sudah ada anggota dengan nama pengguna \'%s\'</string>
+ <string name="invite_username_not_found">Tak ditemukan pengguna dengan nama \'%s\'</string>
+ <string name="invite">Undang</string>
+ <string name="invite_names_title">Nama pengguna atau Email</string>
+ <string name="signup_succeed_signin_failed">Akun Anda telah dibuat namun terjadi galat saat mencoba masuk.\n Coba untuk masuk dengan nama pengguna dan password baru Anda.</string>
+ <string name="send_link">Kirim tautan</string>
+ <string name="my_site_header_external">Eksternal</string>
+ <string name="invite_people">Undang Orang-orang</string>
+ <string name="label_clear_search_history">Bersihkan riwayat pencarian</string>
+ <string name="dlg_confirm_clear_search_history">Bersihkan riwayat pencarian?</string>
+ <string name="reader_empty_posts_in_search_description">Tak ditemukan pos untuk %s dalam bahasa Anda</string>
+ <string name="reader_label_post_search_running">Mencari...</string>
+ <string name="reader_label_related_posts">Bacaan Terkait</string>
+ <string name="reader_empty_posts_in_search_title">Tak ditemukan pos</string>
+ <string name="reader_label_post_search_explainer">Cari seluruh blog publik WordPress.com</string>
+ <string name="reader_hint_post_search">Cari WordPress.com</string>
+ <string name="reader_title_related_post_detail">Pos Terkait</string>
+ <string name="reader_title_search_results">Cari %s</string>
+ <string name="preview_screen_links_disabled">Tautan dinonaktifkan pada laman pratinjau</string>
+ <string name="draft_explainer">Pos ini adalah sebuah draf yang belum diterbitkan</string>
+ <string name="send">Kirim</string>
+ <string name="user_remove_confirmation_message">Jika Anda menghapus %1$s, pengguna tsb tak bisa mengakses situs ini lagi, tetapi semua konten yang telah dibuat oleh %1$s akan tetap ada di situs ini.\n\nApakah Anda masih ingin menghapus pengguna ini?</string>
+ <string name="person_removed">Berhasil menghapus %1$s</string>
+ <string name="person_remove_confirmation_title">Hapus %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Situs dalam daftar ini belum memposting apa pun baru-baru ini</string>
+ <string name="people">Orang</string>
+ <string name="edit_user">Edit Pengguna</string>
+ <string name="role">Peran</string>
+ <string name="error_remove_user">Tidak dapat menghapus pengguna</string>
+ <string name="error_update_role">Tidak dapat memperbarui peran pengguna</string>
+ <string name="error_fetch_viewers_list">Gagal menampilkan pengunjung situs</string>
+ <string name="gravatar_camera_and_media_permission_required">Izin diperlukan untuk memilih atau mengambil foto</string>
+ <string name="error_updating_gravatar">Terjadi galat saat memperbarui Gravatar</string>
+ <string name="error_locating_image">Terjadi galat saat mencari gambar yang dipangkas</string>
+ <string name="error_refreshing_gravatar">Terjadi galat saat memuat ulang Gravatar</string>
+ <string name="gravatar_tip">Baru! Ketuk Gravatar untuk mengubahnya!</string>
+ <string name="error_cropping_image">Terjadi galat saat memangkas gambar</string>
+ <string name="launch_your_email_app">Luncurkan aplikasi surat elektronik</string>
+ <string name="checking_email">Memeriksa surat elektronik</string>
+ <string name="not_on_wordpress_com">Tidak di WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Saat ini tidak tersedia. Masukkan kata sandi Anda</string>
+ <string name="check_your_email">Periksa surat elektronik Anda</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Dapatkan tautan yang dikirimkan ke surat elektronik Anda untuk langsung masuk</string>
+ <string name="logging_in">Proses masuk</string>
+ <string name="enter_your_password_instead">Sebaiknya masukkan kata sandi Anda</string>
+ <string name="web_address_dialog_hint">Ditampilkan secara publik saat Anda berkomentar.</string>
+ <string name="jetpack_not_connected_message">Plugin Jetpack terpasang, tetapi tidak terhubung ke WordPress.com. Ingin menghubungkan Jetpack?</string>
+ <string name="username_email">Surat elektronik atau nama pengguna</string>
+ <string name="jetpack_not_connected">Plugin Jetpack tidak terhubung</string>
+ <string name="new_editor_reflection_error">Visual editor tidak kompatibel dengan perangkat Anda. Ini\n dinonaktifkan secara otomatis.</string>
+ <string name="stats_insights_latest_post_no_title">(tanpa judul)</string>
+ <string name="capture_or_pick_photo">Ambil atau pilih foto</string>
+ <string name="plans_post_purchase_text_themes">Anda sekarang memiliki akses tak terbatas ke tema Premium. Pratinjau tema apa pun di situs Anda untuk memulai.</string>
+ <string name="plans_post_purchase_button_themes">Telusuri Tema</string>
+ <string name="plans_post_purchase_title_themes">Temukan tema Premium yang sempurna</string>
+ <string name="plans_post_purchase_button_video">Mulai pos baru</string>
+ <string name="plans_post_purchase_text_video">Anda dapat mengunggah dan meng-hosting video di situs Anda dengan VideoPress dan penyimpanan media yang diperluas.</string>
+ <string name="plans_post_purchase_title_video">Hidupkan pos dengan video</string>
+ <string name="plans_post_purchase_button_customize">Sesuaikan Situs saya</string>
+ <string name="plans_post_purchase_text_customize">Anda sekarang memiliki akses ke fon khusus, warna khusus, dan kemampuan pengeditan CSS khusus.</string>
+ <string name="plans_post_purchase_text_intro">Situs Anda melakukan perubahan dalam keseruan! Sekarang jelajahi fitur baru Anda dan pilih dari mana Anda ingin memulainya.</string>
+ <string name="plans_post_purchase_title_customize">Menyesuaikan Fon &amp; Warna</string>
+ <string name="plans_post_purchase_title_intro">Semua milik Anda, saatnya melangkah!</string>
+ <string name="export_your_content_message">Pos, halaman, dan pengaturan Anda akan dikirimkan melalui email kepada Anda pada %s.</string>
+ <string name="plan">Paket</string>
+ <string name="plans">Paket</string>
+ <string name="plans_loading_error">Tidak dapat memuat paket</string>
+ <string name="export_your_content">Ekspor konten Anda</string>
+ <string name="exporting_content_progress">Mengekspor konten…</string>
+ <string name="export_email_sent">Email ekspor sudah dikirimkan!</string>
+ <string name="premium_upgrades_message">Anda memiliki peningkatan premium aktif dalam situs Anda. Batalkan peningkatan sebelum menghapus situs Anda.</string>
+ <string name="show_purchases">Tampilkan pembelian</string>
+ <string name="checking_purchases">Memeriksa pembelian</string>
+ <string name="premium_upgrades_title">Peningkatan Premium</string>
+ <string name="purchases_request_error">Terjadi masalah. Tidak dapat meminta pembelian.</string>
+ <string name="delete_site_progress">Menghapus situs…</string>
+ <string name="delete_site_summary">Tindakan ini tidak dapat dibatalkan. Menghapus situs Anda akan membuang semua konten, kontributor, dan domain dari situs.</string>
+ <string name="delete_site_hint">Hapus situs</string>
+ <string name="export_site_hint">Ekspor situs Anda ke file XML</string>
+ <string name="are_you_sure">Apakah Anda Yakin?</string>
+ <string name="export_site_summary">Jika Anda yakin, segera luangkan waktu dan ekspor konten Anda sekarang. Itu tidak dapat dikembalikan di masa mendatang.</string>
+ <string name="keep_your_content">Jaga Konten Anda</string>
+ <string name="domain_removal_hint">Domain yang tidak berfungsi setelah Anda menghapus situs Anda</string>
+ <string name="domain_removal_summary">Hati-hati! Menghapus situs Anda juga akan menghapus domain Anda di bawah ini.</string>
+ <string name="primary_domain">Domain Primer</string>
+ <string name="domain_removal">Penghapusan Domain</string>
+ <string name="error_deleting_site_summary">Ada kesalahan saat menghapus situs Anda. Hubungi dukungan untuk bantuan selengkapnya</string>
+ <string name="error_deleting_site">Kesalahan saat menghapus situs</string>
+ <string name="confirm_delete_site_prompt">Harap ketikkan %1$s di bidang di bawah untuk mengonfirmasi. Situs Anda akan dihapus selamanya.</string>
+ <string name="site_settings_export_content_title">Ekspor konten</string>
+ <string name="contact_support">Hubungi dukungan</string>
+ <string name="confirm_delete_site">Konfirmasi Penghapusan Situs</string>
+ <string name="start_over_text">Jika Anda menginginkan sebuah situs tetapi tidak menginginkan beberapa pos dan halaman yang Anda miliki saat ini, tim dukungan kami dapat menghapus pos, halaman, media, dan komentar untuk Anda.\n\nTindakan ini akan membuat situs dan URL Anda tetap aktif, tetapi memberi Anda titik awal yang segar di pembuatan konten. Cukup hubungi kami untuk membersihkan konten Anda saat ini.</string>
+ <string name="site_settings_start_over_hint">Mulai dari awal situs Anda</string>
+ <string name="let_us_help">Mari Kami Bantu</string>
+ <string name="me_btn_app_settings">Pengaturan Aplikasi</string>
+ <string name="start_over">Mulai</string>
+ <string name="editor_remove_failed_uploads">Buang unggahan yang gagal</string>
+ <string name="editor_toast_failed_uploads">Gagal mengunggah beberapa media. Tidak dapat menyimpan atau menerbitkan\n pos Anda dalam status ini. Anda ingin membuang semua media yang gagal?</string>
+ <string name="comments_empty_list_filtered_trashed">Tidak ada komentar Sampah</string>
+ <string name="site_settings_advanced_header">Lanjutan</string>
+ <string name="comments_empty_list_filtered_pending">Tidak ada komentar Tertunda</string>
+ <string name="comments_empty_list_filtered_approved">Tidak ada komentar Disetujui</string>
+ <string name="button_done">Selesai</string>
+ <string name="button_skip">Lewati</string>
+ <string name="site_timeout_error">Tidak dapat terhubung ke situs WordPress karena kesalahan Waktu Habis.</string>
+ <string name="xmlrpc_malformed_response_error">Tidak dapat terhubung. Pemasangan WordPress merespons dengan dokumen XML-RPC yang tidak valid.</string>
+ <string name="xmlrpc_missing_method_error">Tidak dapat terhubung. Metode XML-RPC yang diperlukan tidak ditemukan di server.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Tengah</string>
+ <string name="theme_free">Gratis</string>
+ <string name="theme_all">Semua</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Obrolan</string>
+ <string name="post_format_gallery">Galeri</string>
+ <string name="post_format_image">Gambar</string>
+ <string name="post_format_link">Tautan</string>
+ <string name="post_format_quote">Kutipan</string>
+ <string name="post_format_standard">Standar</string>
+ <string name="notif_events">Informasi tentang kursus dan acara WordPress.com (online &amp; tatap muka).</string>
+ <string name="post_format_aside">Ke samping</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Kesempatan untuk berpartisipasi dalam riset &amp; survei WordPress.com.</string>
+ <string name="notif_tips">Kiat untuk mendapat maksimal dari WordPress.com.</string>
+ <string name="notif_community">Komunitas</string>
+ <string name="replies_to_my_comments">Balas komentar saya</string>
+ <string name="notif_suggestions">Saran</string>
+ <string name="notif_research">Riset</string>
+ <string name="site_achievements">Prestasi situs</string>
+ <string name="username_mentions">Penyebutan nama pengguna</string>
+ <string name="likes_on_my_posts">Suka pada pos saya</string>
+ <string name="site_follows">Pengikut Situs</string>
+ <string name="likes_on_my_comments">Suka pada komentar saya</string>
+ <string name="comments_on_my_site">Komentar di situs saya</string>
+ <string name="site_settings_list_editor_summary_other">%d items</string>
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="approve_auto_if_previously_approved">Komentar pengguna yang diketahui</string>
+ <string name="approve_auto">Semua pengguna</string>
+ <string name="approve_manual">Tak ada komentar</string>
+ <string name="site_settings_paging_summary_other">%d komentar per halaman</string>
+ <string name="site_settings_paging_summary_one">1 komentar per halaman</string>
+ <string name="site_settings_multiple_links_summary_other">Minta persetujuan untuk lebih dari %d tautan</string>
+ <string name="site_settings_multiple_links_summary_one">Minta persetujuan untuk lebih dari 1 tautan</string>
+ <string name="site_settings_multiple_links_summary_zero">Minta persetujuan untuk lebih dari 0 tautan</string>
+ <string name="detail_approve_auto">Otomatis setujui komentar semua orang.</string>
+ <string name="detail_approve_auto_if_previously_approved">Otomatis setujui jika pengguna memiliki komentar yang disetujui sebelumnya</string>
+ <string name="detail_approve_manual">Wajibkan persetujuan manual untuk komentar semua orang.</string>
+ <string name="filter_trashed_posts">Dibuang</string>
+ <string name="days_quantity_one">1 hari</string>
+ <string name="days_quantity_other">%d hari</string>
+ <string name="filter_published_posts">Diterbitkan</string>
+ <string name="filter_draft_posts">Draf</string>
+ <string name="filter_scheduled_posts">Terjadwal</string>
+ <string name="pending_email_change_snackbar">Klik tautan verifikasi di email yang dikirimkan ke %1$s untuk mengonfirmasi alamat baru Anda</string>
+ <string name="primary_site">Situs Utama</string>
+ <string name="web_address">Alamat Web</string>
+ <string name="editor_toast_uploading_please_wait">Saat ini Anda sedang mengunggah media. Harap tunggu hingga selesai.</string>
+ <string name="error_refresh_comments_showing_older">Komentar tidak dapat disegarkan pada saat ini - menunjukkan komentar lama</string>
+ <string name="editor_post_settings_set_featured_image">Atur Gambar Unggulan</string>
+ <string name="editor_post_settings_featured_image">Gambar Unggulan</string>
+ <string name="new_editor_promo_desc">Aplikasi WordPress untuk Android sekarang berisi visual baru yang cantik\n penyunting Cobalah dengan membuat pos baru.</string>
+ <string name="new_editor_promo_title">Penyunting baru</string>
+ <string name="new_editor_promo_button_label">Bagus, terima kasih!</string>
+ <string name="visual_editor_enabled">Editor Visual diaktifkan</string>
+ <string name="editor_content_placeholder">Editor Visual diaktifkan</string>
+ <string name="editor_page_title_placeholder">Judul Halaman</string>
+ <string name="editor_post_title_placeholder">Judul Kiriman</string>
+ <string name="email_address">Alamat email</string>
+ <string name="preference_show_visual_editor">Tampilkan editor visual</string>
+ <string name="dlg_sure_to_delete_comments">Hapus komentar ini secara permanen?</string>
+ <string name="preference_editor">Penyunting</string>
+ <string name="dlg_sure_to_delete_comment">Hapus komentar ini secara permanen?</string>
+ <string name="mnu_comment_delete_permanently">Hapus</string>
+ <string name="comment_deleted_permanently">Komentar dihapus</string>
+ <string name="mnu_comment_untrash">Pulihkan</string>
+ <string name="comments_empty_list_filtered_spam">Tidak ada komentar Spam</string>
+ <string name="could_not_load_page">Tidak dapat memuat halaman</string>
+ <string name="comment_status_all">Semua</string>
+ <string name="interface_language">Bahasa Antarmuka</string>
+ <string name="off">Nonaktif</string>
+ <string name="about_the_app">Tentang aplikasi</string>
+ <string name="error_post_account_settings">Tidak dapat menyimpan pengaturan akun Anda</string>
+ <string name="error_post_my_profile">Tidak dapat menyimpan profil Anda</string>
+ <string name="error_fetch_account_settings">Tidak dapat mengambil pengaturan akun Anda</string>
+ <string name="error_fetch_my_profile">Tidak dapat mengambil profil Anda</string>
+ <string name="stats_widget_promo_ok_btn_label">Baik, mengerti</string>
+ <string name="stats_widget_promo_desc">Tambahkan widget ke layar awal Anda untuk mengakses Statistik Anda dalam sekali klik.</string>
+ <string name="stats_widget_promo_title">Widget Statistik Layar Awal</string>
+ <string name="site_settings_unknown_language_code_error">Kode bahasa tidak dikenali</string>
+ <string name="site_settings_threading_dialog_description">Izinkan komentar ditumpuk dalam utas.</string>
+ <string name="site_settings_threading_dialog_header">Utas hingga</string>
+ <string name="remove">Buang</string>
+ <string name="search">Cari</string>
+ <string name="add_category">Tambahkan kategori</string>
+ <string name="disabled">Dinonaktifkan</string>
+ <string name="site_settings_image_original_size">Ukuran Asli</string>
+ <string name="privacy_private">Situs Anda hanya dapat dilihat oleh Anda dan pengguna yang Anda setujui</string>
+ <string name="privacy_public_not_indexed">Situs Anda dapat dilihat oleh semua orang, tetapi meminta mesin pencari untuk tidak mengindeks.</string>
+ <string name="privacy_public">Situs Anda dapat dilihat oleh semua orang dan mungkin diindeks berdasarkan mesin pencarian.</string>
+ <string name="about_me_hint">Beberapa kata tentang Anda...</string>
+ <string name="about_me">Tentang saya</string>
+ <string name="public_display_name_hint">Nama tampilan secara default akan menjadi nama pengguna Anda bila tidak diatur</string>
+ <string name="public_display_name">Nama tampilan publik</string>
+ <string name="my_profile">Profil Saya</string>
+ <string name="first_name">Nama depan</string>
+ <string name="last_name">Nama belakang</string>
+ <string name="site_privacy_public_desc">Izinkan mesin pencari untuk mengindeks situs ini</string>
+ <string name="site_privacy_hidden_desc">Larang mesin pencari untuk mengindeks situs ini</string>
+ <string name="site_privacy_private_desc">Saya ingin situs saya menjadi privat, hanya terlihat oleh pengguna yang saya pilih</string>
+ <string name="cd_related_post_preview_image">Gambar pratinjau pos terkait</string>
+ <string name="error_post_remote_site_settings">Tidak dapat menyimpan info situs</string>
+ <string name="error_fetch_remote_site_settings">Tidak dapat mengambil info situs</string>
+ <string name="error_media_upload_connection">Terjadi kesalahan koneksi saat mengunggah media</string>
+ <string name="site_settings_disconnected_toast">Terputus, penyuntingan dinonaktifkan.</string>
+ <string name="site_settings_unsupported_version_error">Versi WordPress tidak didukung</string>
+ <string name="site_settings_multiple_links_dialog_description">Minta persetujuan untuk komentar yang memiliki lebih dari jumlah tautan ini.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Tutup secara otomatis</string>
+ <string name="site_settings_close_after_dialog_description">Tutup komentar secara otomatis di artikel.</string>
+ <string name="site_settings_paging_dialog_description">Bagi utas komentar ke dalam beberapa halaman.</string>
+ <string name="site_settings_paging_dialog_header">Komentar per halaman</string>
+ <string name="site_settings_close_after_dialog_title">Tutup komentar</string>
+ <string name="site_settings_blacklist_description">Saat komentar memuat kata-kata berikut dalam isi, nama, tautan, email, atau IP, maka akan ditandai sebagai spam. Anda bisa menuliskan bagian kata, jadi "press" akan cocok dengan "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">Saat komentar memuat kata-kata berikut dalam isi, nama, tautan, email, atau IP, maka akan tertunda dalam antrean moderasi. Anda bisa menuliskan bagian kata, jadi "press" akan cocok dengan "WordPress."</string>
+ <string name="site_settings_list_editor_input_hint">Masukkan kata atau kalimat</string>
+ <string name="site_settings_list_editor_no_items_text">Tidak ada item</string>
+ <string name="site_settings_learn_more_caption">Anda dapat mengubah pengaturan ini untuk pos individual.</string>
+ <string name="site_settings_rp_preview3_site">dalam "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Fokus Upgrade: VideoPress For Weddings</string>
+ <string name="site_settings_rp_preview2_site">dalam "Applikasi"</string>
+ <string name="site_settings_rp_preview2_title">Applikasi WordPress untuk Android Melakukan Perubahan Besar</string>
+ <string name="site_settings_rp_preview1_site">dalam "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Pembaruan Besar untuk iPhone/iPad Kini Tersedia</string>
+ <string name="site_settings_rp_show_images_title">Tampilkan Gambar</string>
+ <string name="site_settings_rp_show_header_title">Tampilkan Header</string>
+ <string name="site_settings_rp_switch_summary">Pos terkait menampilkan konten yang relevan dari situs Anda di bawah pos Anda.</string>
+ <string name="site_settings_rp_switch_title">Tampilkan Pos Terkait</string>
+ <string name="site_settings_delete_site_hint">Hapus data situs Anda dari aplikasi</string>
+ <string name="site_settings_blacklist_hint">Komentar yang cocok dengan filter ditandai sebagai spam</string>
+ <string name="site_settings_moderation_hold_hint">Komentar yang cocok dengan filter akan menunggu antrean moderasi</string>
+ <string name="site_settings_multiple_links_hint">Abaikan batasan tautan untuk pengguna yang dikenal</string>
+ <string name="site_settings_whitelist_hint">Penulis komentar harus mempunyai komentar yang sudah disetujui sebelumnya</string>
+ <string name="site_settings_user_account_required_hint">Pengguna harus sudah terdaftar dan masuk untuk memberi komentar</string>
+ <string name="site_settings_identity_required_hint">Penulis komentar harus memasukkan nama dan email</string>
+ <string name="site_settings_manual_approval_hint">Komentar harus disetujui secara manual</string>
+ <string name="site_settings_paging_hint">Tampilkan komentar dalam beberapa bagian dengan ukuran spesifik</string>
+ <string name="site_settings_threading_hint">Izinkan komentar cabang sampai kedalaman tertentu</string>
+ <string name="site_settings_sort_by_hint">Tentukan urutan komentar yang ditampilkan</string>
+ <string name="site_settings_close_after_hint">Larang komentar setelah waktu yang ditentukan</string>
+ <string name="site_settings_receive_pingbacks_hint">Perbolehkan pemberitahuan tautan dari blog lain</string>
+ <string name="site_settings_send_pingbacks_hint">Upayakan memberi notifikasi ke blog lain yang tertaut dari artikel</string>
+ <string name="site_settings_allow_comments_hint">Perbolehkan pembaca untuk menulis komentar</string>
+ <string name="site_settings_discussion_hint">Tampilkan dan ubah pengaturan diskusi situs</string>
+ <string name="site_settings_more_hint">Tampilkan semua pengaturan Diskusi yang ada</string>
+ <string name="site_settings_related_posts_hint">Tampilkan atau sembunyikan pos terkait di pembaca</string>
+ <string name="site_settings_upload_and_link_image_hint">Perbolehkan untuk selalu mengirim gambar dalam ukuran asli</string>
+ <string name="site_settings_image_width_hint">Ubah ukuran gambar dalam pos ke lebar berikut</string>
+ <string name="site_settings_format_hint">Tentukan format pos baru</string>
+ <string name="site_settings_category_hint">Tentukan kategori pos baru</string>
+ <string name="site_settings_location_hint">Tambahkan secara otomatis data lokasi ke pos Anda</string>
+ <string name="site_settings_password_hint">Ubah kata sandi Anda</string>
+ <string name="site_settings_username_hint">Akun pengguna saat ini</string>
+ <string name="site_settings_language_hint">Bahasa yang dipakai dalam blog mayoritas adalah</string>
+ <string name="site_settings_privacy_hint">Kontrol siapa saja yang dapat melihat situs Anda</string>
+ <string name="site_settings_address_hint">Mengubah alamat Anda saat ini tidak dimungkinkan</string>
+ <string name="site_settings_tagline_hint">Deskripsi pendek atau kalimat unik untuk menggambarkan blog Anda</string>
+ <string name="site_settings_title_hint">Dalam beberapa kata, jelaskan mengenai situs ini</string>
+ <string name="site_settings_whitelist_known_summary">Komentar dari user yang dikenal</string>
+ <string name="site_settings_whitelist_all_summary">Komentar dari semua pengguna</string>
+ <string name="site_settings_threading_summary">%d level</string>
+ <string name="site_settings_privacy_private_summary">Privat</string>
+ <string name="site_settings_privacy_hidden_summary">Tersembunyi</string>
+ <string name="site_settings_delete_site_title">Hapus Situs</string>
+ <string name="site_settings_privacy_public_summary">Publik</string>
+ <string name="site_settings_blacklist_title">Daftar hitam</string>
+ <string name="site_settings_moderation_hold_title">Menunggu untuk Moderasi</string>
+ <string name="site_settings_multiple_links_title">Tautan dalam komentar</string>
+ <string name="site_settings_whitelist_title">Otomatis disetujui</string>
+ <string name="site_settings_threading_title">Rangkaian</string>
+ <string name="site_settings_paging_title">Penomoran halaman</string>
+ <string name="site_settings_sort_by_title">Urut berdasar</string>
+ <string name="site_settings_account_required_title">Pengguna harus sudah masuk</string>
+ <string name="site_settings_identity_required_title">Harus menyertakan nama dan email</string>
+ <string name="site_settings_receive_pingbacks_title">Terima Ping Balik</string>
+ <string name="site_settings_send_pingbacks_title">Kirim Ping Balik</string>
+ <string name="site_settings_allow_comments_title">Izinkan Komentar</string>
+ <string name="site_settings_default_format_title">Format Baku</string>
+ <string name="site_settings_default_category_title">Kategori Baku</string>
+ <string name="site_settings_location_title">Aktifkan Lokasi</string>
+ <string name="site_settings_address_title">Alamat</string>
+ <string name="site_settings_title_title">Judul Situs</string>
+ <string name="site_settings_tagline_title">Slogan</string>
+ <string name="site_settings_this_device_header">Perangkat ini</string>
+ <string name="site_settings_discussion_new_posts_header">Baku untuk pos baru</string>
+ <string name="site_settings_account_header">Akun</string>
+ <string name="site_settings_writing_header">Tulisan</string>
+ <string name="site_settings_general_header">Umum</string>
+ <string name="newest_first">Terbaru terlebih dahulu</string>
+ <string name="discussion">Diskusi</string>
+ <string name="privacy">Privasi</string>
+ <string name="related_posts">Pos Terkait</string>
+ <string name="comments">Komentar</string>
+ <string name="close_after">Tutup setelah</string>
+ <string name="oldest_first">Paling lama terlebih dahulu</string>
+ <string name="media_error_no_permission_upload">Anda tidak memiliki izin untuk menambahkan media ke dalam situs</string>
+ <string name="never">Tidak pernah</string>
+ <string name="unknown">Tidak diketahui</string>
+ <string name="reader_err_get_post_not_found">Pos ini tidak ada lagi</string>
+ <string name="reader_err_get_post_not_authorized">Anda tidak diizinkan melihat pos ini</string>
+ <string name="reader_err_get_post_generic">Gagal mengambil pos ini</string>
+ <string name="blog_name_no_spaced_allowed">Alamat situs tak boleh memuat spasi</string>
+ <string name="invalid_username_no_spaces">Nama pengguna tak boleh memuat spasi</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Situs-situs yang Anda ikuti belum menerbitkan pos apapun belakangan ini</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Tak ada pos terbaru</string>
+ <string name="media_details_copy_url_toast">URL tersalin dalam papan klip</string>
+ <string name="edit_media">Edit media</string>
+ <string name="media_details_copy_url">Salin URL</string>
+ <string name="media_details_label_date_uploaded">Terunggah</string>
+ <string name="media_details_label_date_added">Ditambahkan</string>
+ <string name="selected_theme">Tema Terpilih</string>
+ <string name="could_not_load_theme">Tidak dapat memuat tema</string>
+ <string name="theme_activation_error">Terjadi masalah. Tidak dapat mengaktifkan tema</string>
+ <string name="theme_by_author_prompt_append"> oleh %1$s</string>
+ <string name="theme_prompt">Terima kasih sudah memilih %1$s</string>
+ <string name="theme_try_and_customize">Coba &amp; Sesuaikan</string>
+ <string name="theme_view">Lihat</string>
+ <string name="theme_details">Rincian</string>
+ <string name="theme_support">Dukungan</string>
+ <string name="theme_done">SELESAI</string>
+ <string name="theme_manage_site">KELOLA SITUS</string>
+ <string name="title_activity_theme_support">Tema</string>
+ <string name="theme_activate">Aktifkan</string>
+ <string name="date_range_start_date">Tanggal Mulai</string>
+ <string name="date_range_end_date">Tanggal Selesai</string>
+ <string name="current_theme">Tema Saat Ini</string>
+ <string name="customize">Sesuaikan</string>
+ <string name="details">Rincian</string>
+ <string name="support">Dukungan</string>
+ <string name="active">Aktif</string>
+ <string name="stats_referrers_spam_generic_error">Ada masalah selama pengoperasian. Status spam tidak diubah.</string>
+ <string name="stats_referrers_marking_not_spam">Menandai sebagai bukan spam</string>
+ <string name="stats_referrers_unspam">Bukan spam</string>
+ <string name="stats_referrers_marking_spam">Menandai sebagai spam</string>
+ <string name="theme_auth_error_authenticate">Gagal mengambil tema: gagal mengautentikasi pengguna</string>
+ <string name="post_published">Pos diterbitkan</string>
+ <string name="page_published">Halaman diterbitkan</string>
+ <string name="post_updated">Pos diperbarui</string>
+ <string name="page_updated">Halaman diperbarui</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Maaf, tema tidak ditemukan.</string>
+ <string name="media_file_name">Nama file: %s</string>
+ <string name="media_uploaded_on">Diunggah pada: %s</string>
+ <string name="media_dimensions">Dimensi: %s</string>
+ <string name="upload_queued">Diantrekan</string>
+ <string name="media_file_type">Jenis file: %s</string>
+ <string name="reader_label_gap_marker">Muat pos lainnya</string>
+ <string name="notifications_no_search_results">Tidak ada situs yang cocok dengan \'%s\'</string>
+ <string name="search_sites">Cari situs</string>
+ <string name="notifications_empty_view_reader">Lihat Pembaca</string>
+ <string name="unread">Belum dibaca</string>
+ <string name="notifications_empty_action_followers_likes">Dapatkan perhatian: komentari pos yang sudah Anda baca.</string>
+ <string name="notifications_empty_action_comments">Bergabung dengan percakapan: komentari pos dari blog yang Anda ikuti.</string>
+ <string name="notifications_empty_action_unread">Aktifkan kembali percakapan: tulis pos baru.</string>
+ <string name="notifications_empty_action_all">Aktiflah! Komentari pos dari blog yang Anda ikuti.</string>
+ <string name="notifications_empty_likes">Belum ada suka baru untuk ditampilkan.</string>
+ <string name="notifications_empty_followers">Tidak ada pengikut untuk dilaporkan…sampai sekarang.</string>
+ <string name="notifications_empty_comments">Tidak ada komentar baru…lagi.</string>
+ <string name="notifications_empty_unread">Anda semua terperangkap!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Harap akses Statistik di aplikasi, dan coba tambahkan widget nantinya</string>
+ <string name="stats_widget_error_readd_widget">Hapus widget dan tambahkan lagi</string>
+ <string name="stats_widget_error_no_visible_blog">Statistik tidak dapat diakses tanpa blog terlihat</string>
+ <string name="stats_widget_error_no_permissions">Akun WordPress.com Anda tidak dapat mengakses Statistik di blog ini</string>
+ <string name="stats_widget_error_no_account">Harap masuk ke WordPress</string>
+ <string name="stats_widget_error_generic">Statistik tidak dapat dimuat</string>
+ <string name="stats_widget_loading_data">Memuat data…</string>
+ <string name="stats_widget_name_for_blog">Statistik Hari Ini untuk %1$s</string>
+ <string name="stats_widget_name">Statistik Hari Ini WordPress</string>
+ <string name="add_location_permission_required">Izin diperlukan untuk menambahkan lokasi</string>
+ <string name="add_media_permission_required">Izin diperlukan untuk menambahkan media</string>
+ <string name="access_media_permission_required">Izin diperlukan untuk mengakses media</string>
+ <string name="stats_enable_rest_api_in_jetpack">Untuk menampilkan statistik Anda, aktifkan module JSON API di Jetpack.</string>
+ <string name="error_open_list_from_notification">Pos atau laman ini diterbitkan di situs lain</string>
+ <string name="reader_short_comment_count_multi">%s Komentar</string>
+ <string name="reader_short_comment_count_one">1 Komentar</string>
+ <string name="reader_label_submit_comment">KIRIM</string>
+ <string name="reader_hint_comment_on_post">Tanggapan untuk pos...</string>
+ <string name="reader_discover_visit_blog">Kunjungi %s</string>
+ <string name="reader_discover_attribution_blog">Semula diposkan pada %s</string>
+ <string name="reader_discover_attribution_author">Semula diposkan oleh %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Semula diposkan oleh %1$s pada %2$s</string>
+ <string name="reader_short_like_count_multi">%s Suka</string>
+ <string name="reader_short_like_count_one">1 Suka</string>
+ <string name="reader_label_follow_count">%,d pengikut</string>
+ <string name="reader_short_like_count_none">Suka</string>
+ <string name="reader_menu_tags">Sunting tag dan blog</string>
+ <string name="reader_title_post_detail">Pos Pembaca</string>
+ <string name="local_draft_explainer">Pos ini adalah konsep lokal yang belum pernah diterbitkan</string>
+ <string name="local_changes_explainer">Pos ini memiliki perubahan lokal yang belum pernah diterbitkan</string>
+ <string name="notifications_push_summary">Pengaturan pemberitahuan yang muncul di perangkat Anda.</string>
+ <string name="notifications_email_summary">Pengaturan pemberitahuan yang dikirim melalui email yang terhubung dengan akun Anda.</string>
+ <string name="notifications_tab_summary">Pengaturan pemberitahuan yang muncul di tab Pemberitahuan.</string>
+ <string name="notifications_disabled">Pemberitahuan aplikasi telah dinonaktifkan. Ketuk di sini untuk mengaktifkannya di Pengaturan.</string>
+ <string name="notification_types">Jenis Pemberitahuan</string>
+ <string name="error_loading_notifications">Tidak dapat memuat pengaturan pemberitahuan</string>
+ <string name="replies_to_your_comments">Membalas komentar Anda</string>
+ <string name="comment_likes">Menyukai komentar</string>
+ <string name="app_notifications">Pemberitahuan aplikasi</string>
+ <string name="notifications_tab">Tab pemberitahuan</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Komentar di situs lain</string>
+ <string name="notifications_other">Lainnya</string>
+ <string name="notifications_wpcom_updates">Berita Terbaru WordPress.com</string>
+ <string name="notifications_account_emails">Email dari WordPress.com</string>
+ <string name="notifications_account_emails_summary">Kami akan selalu mengirim email penting terkait akun Anda, namun Anda juga dapat memperoleh beberapa tambahan yang bermanfaat.</string>
+ <string name="notifications_sights_and_sounds">Penglihatan dan Suara</string>
+ <string name="your_sites">Situs Anda</string>
+ <string name="stats_insights_latest_post_trend">Telah %1$s sejak %2$s diterbitkan. Berikut ini gambaran kinerja pos sejauh ini…</string>
+ <string name="stats_insights_latest_post_summary">Ringkasan Pos Terbaru</string>
+ <string name="button_revert">Kembalikan</string>
+ <string name="days_ago">%d hari yang lalu</string>
+ <string name="yesterday">Kemarin</string>
+ <string name="connectionbar_no_connection">Tak ada koneksi</string>
+ <string name="page_trashed">Halaman dibuang ke tong sampah</string>
+ <string name="stats_no_activity_this_period">Tidak ada aktivitas dalam periode ini</string>
+ <string name="trashed">Dibuang</string>
+ <string name="button_back">Kembali</string>
+ <string name="page_deleted">Halaman dihapus</string>
+ <string name="button_stats">Statistik</string>
+ <string name="button_trash">Buang</string>
+ <string name="button_preview">Pratinjau</string>
+ <string name="button_view">Lihat</string>
+ <string name="button_edit">Sunting</string>
+ <string name="button_publish">Terbitkan</string>
+ <string name="post_deleted">Pos dihapus</string>
+ <string name="post_trashed">Pos dibuang ke tong sampah</string>
+ <string name="my_site_no_sites_view_subtitle">Mau menambahkan satu?</string>
+ <string name="my_site_no_sites_view_title">Anda belum punya situs WordPress sama sekali.</string>
+ <string name="my_site_no_sites_view_drake">Ilustrasi</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Anda tidak punya otoritas untuk mengakses blog ini.</string>
+ <string name="reader_toast_err_follow_blog_not_found">Blog ini tak bisa ditemukan</string>
+ <string name="undo">Urungkan</string>
+ <string name="tabbar_accessibility_label_my_site">Situs Saya</string>
+ <string name="tabbar_accessibility_label_me">Saya</string>
+ <string name="editor_toast_changes_saved">Perubahan disimpan</string>
+ <string name="passcodelock_prompt_message">Masukkan PIN</string>
+ <string name="push_auth_expired">Permintaan sudah kedaluwarsa. Masuk ke WordPress.com untuk mencoba lagi.</string>
+ <string name="ignore">Abaikan</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% dilihat</string>
+ <string name="stats_insights_best_ever">Tampilan Terbaik</string>
+ <string name="stats_insights_most_popular_hour">Jam paling populer</string>
+ <string name="stats_insights_most_popular_day">Hari paling populer</string>
+ <string name="stats_insights_popular">Hari dan jam paling populer</string>
+ <string name="stats_insights_today">Statistik Hari Ini</string>
+ <string name="stats_insights_all_time">Pos, tampilan, dan pengunjung sepanjang waktu</string>
+ <string name="stats_insights">Wawasan</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Untuk melihat statistik Anda, masuk ke akun WordPress.com yang Anda gunakan untuk terhubung dengan Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Mencari Statistik Terbaru Lainnya? Kami telah memindahkannya ke halaman Wawasan.</string>
+ <string name="me_disconnect_from_wordpress_com">Putuskan koneksi WordPress.com</string>
+ <string name="me_btn_login_logout">Log masuk/keluar</string>
+ <string name="me_connect_to_wordpress_com">Koneksikan ke WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">"%s" tidak tersembunyi karena itu adalah situs saat ini</string>
+ <string name="me_btn_support">Bantuan &amp; Dukungan</string>
+ <string name="account_settings">Pengaturan Akun</string>
+ <string name="site_picker_create_dotcom">Buat situs WordPress.com</string>
+ <string name="site_picker_add_self_hosted">Tambahkan situs mandiri (self-hosted)</string>
+ <string name="site_picker_edit_visibility">Tampilkan/sembunyikan situs</string>
+ <string name="site_picker_add_site">Tambahkan situs</string>
+ <string name="site_picker_title">Pilih situs</string>
+ <string name="my_site_btn_switch_site">Ganti Situs</string>
+ <string name="my_site_btn_view_admin">Tampilkan Admin</string>
+ <string name="my_site_btn_view_site">Tampilkan Situs</string>
+ <string name="my_site_header_look_and_feel">Tampilan dan Nuansa</string>
+ <string name="my_site_header_publish">Terbitkan</string>
+ <string name="my_site_btn_site_settings">Pengaturan</string>
+ <string name="my_site_btn_blog_posts">Pos Blog</string>
+ <string name="reader_label_new_posts_subtitle">Ketuk untuk menampilkannya</string>
+ <string name="my_site_header_configuration">Konfigurasi</string>
+ <string name="notifications_account_required">Masuk ke WordPress.com untuk notifikasi</string>
+ <string name="stats_unknown_author">Penulis Tak Dikenal</string>
+ <string name="image_added">Gambar ditambahkan</string>
+ <string name="signout">Putuskan koneksi</string>
+ <string name="sign_out_wpcom_confirm">Memutuskan koneksi akun Anda akan menghapus seluruh data WordPress.com untuk @%s dari perangkat ini, termasuk draf lokal dan perubahan lokal.</string>
+ <string name="select_all">Pilih semua</string>
+ <string name="deselect_all">Batalkan semua pilihan</string>
+ <string name="show">Tampilkan</string>
+ <string name="hide">Sembunyikan</string>
+ <string name="select_from_new_picker">Beberapa-pilihan dengan pemilih baru</string>
+ <string name="loading_blog_images">Memuat gambar</string>
+ <string name="loading_blog_videos">Memuat video</string>
+ <string name="no_media_sources">Gagal memuat media</string>
+ <string name="error_loading_videos">Galat memuat video</string>
+ <string name="stats_generic_error">Statistik yang diminta gagal dimuat</string>
+ <string name="error_loading_blog_images">Gagal memuat gambar</string>
+ <string name="error_loading_blog_videos">Gagal memuat video</string>
+ <string name="error_loading_images">Galat memuat gambar</string>
+ <string name="no_device_videos">Tak ada video</string>
+ <string name="no_blog_images">Tak ada gambar</string>
+ <string name="no_blog_videos">Tak ada video</string>
+ <string name="no_device_images">Tak ada gambar</string>
+ <string name="loading_videos">Memuat video</string>
+ <string name="loading_images">Memuat gambar</string>
+ <string name="no_media">Tak ada media</string>
+ <string name="auth_required">Masuk lagi untuk melanjutkan.</string>
+ <string name="sign_in_jetpack">Masuk ke akun WordPress.com Anda untuk menyambungkan dengan Jetpack.</string>
+ <string name="two_step_sms_sent">Periksa pesan teks Anda untuk melihat kode verifikasi.</string>
+ <string name="two_step_footer_button">Kirimkan kode via pesan teks</string>
+ <string name="verify">Verifikasikan</string>
+ <string name="two_step_footer_label">Masukkan kode dari authenticator app.</string>
+ <string name="invalid_verification_code">Kode verifikasi tidak valid</string>
+ <string name="verification_code">Kode verifikasi</string>
+ <string name="editor_toast_invalid_path">Lokasi file tidak valid</string>
+ <string name="error_publish_no_network">Tak bisa menerbitkan tanpa koneksi internet. Tersimpan sebagai draf.</string>
+ <string name="tab_title_device_videos">Video Perangkat</string>
+ <string name="tab_title_device_images">Gambar Perangkat</string>
+ <string name="tab_title_site_images">Gambar Situs</string>
+ <string name="tab_title_site_videos">Video Situs</string>
+ <string name="take_video">Ambil video</string>
+ <string name="take_photo">Ambil foto</string>
+ <string name="media_picker_title">Pilih media</string>
+ <string name="add_to_post">Tambahkan ke Pos</string>
+ <string name="language">Bahasa</string>
+ <string name="device">Perangkat</string>
+ <string name="media_details_label_file_name">Nama file</string>
+ <string name="media_details_label_file_type">Tipe file</string>
+ <string name="posts_fetching">Memuat pos...</string>
+ <string name="media_fetching">Memuat media...</string>
+ <string name="toast_err_post_uploading">Gagal membuka pos saat diunggah</string>
+ <string name="comments_fetching">Memuat komentar...</string>
+ <string name="pages_fetching">Memuat laman...</string>
+ <string name="stats_view_search_terms">Terma-terma Pencarian</string>
+ <string name="stats_view_authors">Para Penulis</string>
+ <string name="stats_entry_search_terms">Terma Pencarian</string>
+ <string name="stats_empty_search_terms_desc">Pelajari lebih lanjut mengenai trafik pencarian Anda dengan melihat terma-terma pencarian yang dipergunakan oleh pengunjung saat menemukan situs Anda.</string>
+ <string name="stats_empty_search_terms">Tak ada terma pencarian yang tercatat</string>
+ <string name="stats_search_terms_unknown_search_terms">Terma Pencarian Tak Dikenal</string>
+ <string name="stats_followers_total_wpcom_paged">Menampilkan %1$d - %2$d dari %3$s Pengikut WordPress.com</string>
+ <string name="stats_followers_total_email_paged">Menampilkan %1$d - %2$d dari %3$s Pengikut via Email</string>
+ <string name="reader_empty_posts_request_failed">Gagal memuat pos-pos</string>
+ <string name="publisher">Penerbit:</string>
+ <string name="error_notification_open">Gagal membuka notifikasi</string>
+ <string name="post_uploading">Mengunggah</string>
+ <string name="stats_overall">Keseluruhan</string>
+ <string name="reader_empty_posts_in_blog">Blog ini kosong</string>
+ <string name="stats_recent_weeks">Beberapa Minggu Terakhir</string>
+ <string name="stats_total">Total</string>
+ <string name="reader_label_new_posts">Pos baru</string>
+ <string name="error_copy_to_clipboard">Terjadi kesalahan saat menyalin teks ke clipboard</string>
+ <string name="stats_period">Periode</string>
+ <string name="logs_copied_to_clipboard">Log aplikasi sudah disalin ke clipboard</string>
+ <string name="stats_average_per_day">Rata-Rata per Hari</string>
+ <string name="stats_months_and_years">Bulan dan Tahun</string>
+ <string name="reader_page_recommended_blogs">Situs yang mungkin Anda sukai</string>
+ <string name="stats_comments_total_comments_followers">Total pos dengan pengikut komentar: %1$s</string>
+ <string name="stats_view_publicize">Publikasikan</string>
+ <string name="stats_view_followers">Pengikut</string>
+ <string name="stats_view_countries">Negara</string>
+ <string name="stats_likes">Suka</string>
+ <string name="stats_view_top_posts_and_pages">Pos &amp; Halaman</string>
+ <string name="stats_view_videos">Video</string>
+ <string name="stats_entry_followers">Pengikut</string>
+ <string name="stats_totals_publicize">Pengikut</string>
+ <string name="stats_empty_geoviews_desc">Telusuri daftar untuk melihat negara dan wilayah mana yang menghasilkan lalu-lintas terbanyak ke situs Anda.</string>
+ <string name="stats_empty_geoviews">Tak ada negara yang direkam</string>
+ <string name="stats_empty_top_posts_desc">Temukan konten Anda yang paling sering dilihat, dan periksa kinerja pos dan halaman individual.</string>
+ <string name="stats_empty_referrers_title">Tidak ada perujuk yang direkam</string>
+ <string name="stats_empty_top_posts_title">Tidak ada pos atau halaman yang dilihat</string>
+ <string name="stats_empty_clicks_title">Tidak ada klik yang terekam</string>
+ <string name="stats_empty_referrers_desc">Pelajari selengkapnya tentang visibilitas situs Anda dengan memeriksa situs web dan mesin pencari yang mengirim paling banyak lalu lintas ke situs Anda</string>
+ <string name="stats_empty_tags_and_categories">Tidak ada pos atau halaman ditandai yang dilihat</string>
+ <string name="stats_empty_clicks_desc">Jika konten Anda menyertakan tautan ke situs lainnya, Anda akan melihat tautan mana yang paling sering diklik pengunjung Anda.</string>
+ <string name="stats_empty_top_authors_desc">Lacak tayangan di pos setiap kontributor, dan lihat lebih dekat untuk menemukan konten paling populer dari setiap penulis.</string>
+ <string name="stats_empty_tags_and_categories_desc">Dapatkan ikhtisar topik paling populer di situs Anda, seperti yang tampak di pos teratas Anda dari minggu lalu.</string>
+ <string name="stats_empty_video">Tidak ada video yang diputar</string>
+ <string name="stats_empty_video_desc">Jika Anda sudah mengunggah video menggunakan VideoPress, cari tahu berapa kali video tersebut sudah ditonton.</string>
+ <string name="stats_empty_comments_desc">Jika Anda mengizinkan komentar di situs Anda, lacak komentator teratas Anda dan temukan konten apa yang memicu percakapan paling ramai, berdasarkan 1000 komentar terbaru.</string>
+ <string name="stats_empty_followers">Tak ada pengikut</string>
+ <string name="stats_empty_publicize_desc">Tetap pantau pengikut Anda dari berbagai layanan jejaring sosial menggunakan publikasikan.</string>
+ <string name="stats_empty_publicize">Tidak ada pengikut publikasikan yang terekam</string>
+ <string name="stats_comments_by_posts_and_pages">Berdasarkan Pos &amp; Halaman</string>
+ <string name="stats_empty_followers_desc">Tetap lacak seluruh pengikut Anda, dan berapa lama masing-masing pengikut Anda telah mengikuti situs Anda.</string>
+ <string name="stats_view">Lihat</string>
+ <string name="stats_followers_seconds_ago">detik yang lalu</string>
+ <string name="stats_followers_total_email">Total Pengikut Email: %1$s</string>
+ <string name="stats_other_recent_stats_label">Statistik Terbaru Lainnya</string>
+ <string name="stats_view_all">Lihat semua</string>
+ <string name="stats_for">Statistik untuk %s</string>
+ <string name="themes_fetching">Mengambil tema…</string>
+ <string name="stats_visitors">Pengunjung</string>
+ <string name="stats_timeframe_years">Tahun</string>
+ <string name="stats_views">Tampilan</string>
+ <string name="stats_pagination_label">Halaman %1$s dari %2$s</string>
+ <string name="stats_entry_clicks_link">Tautan</string>
+ <string name="stats_entry_top_commenter">Penulis</string>
+ <string name="stats_entry_publicize">Layanan</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_totals_followers">Sejak</string>
+ <string name="stats_comments_by_authors">Berdasarkan Penulis</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Total Pengikut WordPress.com: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_days">%1$d hari</string>
+ <string name="stats_followers_a_minute_ago">satu menit yang lalu</string>
+ <string name="stats_followers_hours">%1$d jam</string>
+ <string name="stats_followers_a_day">Satu hari</string>
+ <string name="stats_followers_minutes">%1$d menit</string>
+ <string name="stats_followers_an_hour_ago">satu jam yang lalu</string>
+ <string name="stats_followers_years">%1$d tahun</string>
+ <string name="stats_followers_a_month">Satu bulan</string>
+ <string name="stats_followers_a_year">Satu tahun</string>
+ <string name="stats_followers_months">%1$d bulan</string>
+ <string name="ssl_certificate_details">Rincian</string>
+ <string name="sure_to_remove_account">Hapus situs ini?</string>
+ <string name="delete_sure_post">Hapus pos ini</string>
+ <string name="delete_sure">Hapus draf ini</string>
+ <string name="delete_sure_page">Hapus laman ini</string>
+ <string name="confirm_delete_media">Hapus item terpilih?</string>
+ <string name="confirm_delete_multi_media">Hapus semua item terpilih?</string>
+ <string name="cab_selected">%d terpilih</string>
+ <string name="media_gallery_date_range">Menampilkan media dari %1$s hingga %2$s</string>
+ <string name="reader_empty_posts_liked">Anda belum menyukai pos sama sekali</string>
+ <string name="faq_button">Tanya Jawab Umum</string>
+ <string name="browse_our_faq_button">Telusuri Tanya Jawab Umum kami</string>
+ <string name="nux_help_description">Kunjungi pusat bantuan untuk mendapatkan jawaban atas pertanyaan umum, atau kunjungi forum untuk mengajukan pertanyaan baru</string>
+ <string name="agree_terms_of_service">Dengan membuat akun, berarti Anda menyetujui untuk mematuhi %1$sKetentuan Layanan%2$s</string>
+ <string name="create_new_blog_wpcom">Buat blog WordPress.com</string>
+ <string name="new_blog_wpcom_created">Blog WordPress.com sudah dibuat!</string>
+ <string name="reader_empty_comments">Belum ada komentar</string>
+ <string name="reader_empty_posts_in_tag">Belum ada pos dengan tag ini</string>
+ <string name="reader_label_comment_count_multi">%,d komentar</string>
+ <string name="reader_label_view_original">Lihat artikel asli</string>
+ <string name="reader_label_like">Suka</string>
+ <string name="reader_label_liked_by">Disukai oleh</string>
+ <string name="reader_label_comment_count_single">Satu komentar</string>
+ <string name="reader_label_comments_closed">Komentar ditutup</string>
+ <string name="reader_label_comments_on">Komentar pada</string>
+ <string name="reader_title_photo_viewer">%1$d dari %2$d</string>
+ <string name="error_publish_empty_post">Tidak dapat menerbitkan pos kosong</string>
+ <string name="error_refresh_unauthorized_posts">Anda tidak memiliki izin untuk melihat atau menyunting pos</string>
+ <string name="error_refresh_unauthorized_pages">Anda tidak memiliki izin untuk melihat atau menyunting halaman</string>
+ <string name="error_refresh_unauthorized_comments">Anda tidak memiliki izin untuk melihat atau menyunting komentar</string>
+ <string name="older_month">Lebih dari satu bulan</string>
+ <string name="more">Lainnya</string>
+ <string name="older_two_days">Lebih dari 2 hari</string>
+ <string name="older_last_week">Lebih dari satu minggu</string>
+ <string name="stats_no_blog">Statistik tidak dapat dimuat untuk blog yang diminta</string>
+ <string name="select_a_blog">Pilih situs WordPress</string>
+ <string name="sending_content">Mengunggah konten %s</string>
+ <string name="uploading_total">Mengunggah %1$d dari %2$d</string>
+ <string name="mnu_comment_liked">Disukai</string>
+ <string name="comment">Komentar</string>
+ <string name="comment_trashed">Komentar dibuang</string>
+ <string name="posts_empty_list">Belum ada pos. Mengapa tidak membuatnya?</string>
+ <string name="comment_reply_to_user">Balas ke %s</string>
+ <string name="pages_empty_list">Belum ada halaman. Mengapa tidak membuatnya?</string>
+ <string name="media_empty_list_custom_date">Tidak ada media dalam interval waktu ini</string>
+ <string name="posting_post">Mengeposkan "%s"</string>
+ <string name="signing_out">Keluar…</string>
+ <string name="reader_empty_followed_blogs_title">Anda belum mengikuti situs apapun</string>
+ <string name="reader_toast_err_generic">Tidak bisa menjalankan proses ini</string>
+ <string name="reader_toast_err_block_blog">Tidak bisa memblokir blog ini</string>
+ <string name="reader_toast_blog_blocked">Pos-pos dari blog ini tak akan ditampilkan lagi</string>
+ <string name="reader_menu_block_blog">Blokir blog ini</string>
+ <string name="contact_us">Hubungi kami</string>
+ <string name="hs__conversation_detail_error">Uraikan permasalahan yang Anda hadapi</string>
+ <string name="hs__new_conversation_header">Chat dukungan</string>
+ <string name="hs__conversation_header">Chat dukungan</string>
+ <string name="hs__username_blank_error">Masukkan sebuah nama yang sah</string>
+ <string name="hs__invalid_email_error">Masukkan sebuah alamat email yang sah</string>
+ <string name="current_location">Lokasi saat ini</string>
+ <string name="add_location">Tambahkan lokasi</string>
+ <string name="search_location">Cari</string>
+ <string name="edit_location">Sunting</string>
+ <string name="search_current_location">Temukan</string>
+ <string name="preference_send_usage_stats">Kirim statistik</string>
+ <string name="preference_send_usage_stats_summary">Secara otomatis mengirim statistik penggunaan untuk membantu kami meningkatkan WordPress untuk Android</string>
+ <string name="update_verb">Mutakhirkan</string>
+ <string name="schedule_verb">Jadwalkan</string>
+ <string name="reader_title_subs">Tag &amp; Blog</string>
+ <string name="reader_page_followed_tags">Tag yang diikuti</string>
+ <string name="reader_label_followed_blog">Blog diikuti</string>
+ <string name="reader_label_tag_preview">Pos dengan tag %s</string>
+ <string name="reader_toast_err_get_blog_info">Tak bisa menampilkan blog ini</string>
+ <string name="reader_toast_err_already_follow_blog">Anda sudah mengikuti blog ini</string>
+ <string name="reader_toast_err_follow_blog">Tak bisa mengikuti blog ini</string>
+ <string name="reader_toast_err_unfollow_blog">Tak bisa berhenti mengikuti blog ini</string>
+ <string name="reader_empty_recommended_blogs">Tak ada blog yang direkomendasikan</string>
+ <string name="reader_title_blog_preview">Blog Pembaca</string>
+ <string name="reader_title_tag_preview">Tag Pembaca</string>
+ <string name="reader_page_followed_blogs">Situs yang diikuti</string>
+ <string name="reader_hint_add_tag_or_url">Masukkan sebuah URL atau tag untuk mengikuti</string>
+ <string name="saving">Menyimpan...</string>
+ <string name="media_empty_list">Tak ada media</string>
+ <string name="ptr_tip_message">Tip: Tarik ke bawah untuk menyegarkan</string>
+ <string name="help">Bantuan</string>
+ <string name="forgot_password">Lupa password Anda?</string>
+ <string name="forums">Forum</string>
+ <string name="help_center">Pusat bantuan</string>
+ <string name="ssl_certificate_error">Sertifikat SSL tidak sah</string>
+ <string name="ssl_certificate_ask_trust">Jika biasanya Anda bisa terhubung dengan situs ini tanpa masalah, galat ini bisa berarti seseorang sedang mencoba meniru situs, dan Anda sebaiknya tidak melanjutkannya. Apakah Anda ingin mempercayai sertifikat ini?</string>
+ <string name="no_network_message">Tidak ada jaringan yang tersedia</string>
+ <string name="blog_not_found">Terjadi kekeliruan ketika mengakses blog ini</string>
+ <string name="wait_until_upload_completes">Tunggu sampai pengunggahan lengkap</string>
+ <string name="out_of_memory">Perangkat kehabisan memori.</string>
+ <string name="gallery_error">Item media gagal diambil</string>
+ <string name="theme_fetch_failed">Gagal mengunduh tema</string>
+ <string name="theme_set_failed">Gagal memasang tema</string>
+ <string name="comments_empty_list">Tidak ada tanggapan</string>
+ <string name="mnu_comment_unspam">Bukan spam</string>
+ <string name="no_site_error">Gagal terhubung dengan situs WordPress</string>
+ <string name="adding_cat_failed">Gagal menambahkan kategori</string>
+ <string name="theme_auth_error_message">Pastikan kamu punya hak untuk mengatur tema</string>
+ <string name="adding_cat_success">Berhasil menambahkan kategori</string>
+ <string name="cat_name_required">Nama kategori wajib diisi</string>
+ <string name="category_automatically_renamed">Nama kategori %1$s tidak sah. Sudah diubah menjadi %2$s.</string>
+ <string name="no_account">Akun WordPress tidak ditemukan, masukkan akun dan coba lagi</string>
+ <string name="sdcard_message">Kartu SD tambahan diperlukan untuk mengunggah media</string>
+ <string name="stats_empty_comments">Belum ada tanggapan</string>
+ <string name="stats_bar_graph_empty">Belum ada statistik</string>
+ <string name="reply_failed">Gagal mengirim balasan</string>
+ <string name="notifications_empty_list">Tak ada pemberitahuan</string>
+ <string name="error_delete_post">Terjadi galat saat menghapus %s</string>
+ <string name="error_refresh_posts">Pos tidak bisa disegarkan saat ini</string>
+ <string name="error_refresh_pages">Laman tidak bisa disegarkan saat ini</string>
+ <string name="error_refresh_notifications">Pemberitahuan tidak bisa disegarkan saat ini</string>
+ <string name="error_refresh_comments">Tanggapan tidak bisa disegarkan saat ini</string>
+ <string name="error_refresh_stats">Statistik tidak bisa disegarkan saat ini</string>
+ <string name="error_generic">Terjadi galat</string>
+ <string name="error_moderate_comment">Terjadi galat saat sedang memoderasi</string>
+ <string name="error_edit_comment">Terjadi galat saat sedang mengedit tanggapan</string>
+ <string name="error_upload">Terjadi galat saat sedang mengunggah %s</string>
+ <string name="error_load_comment">Gagal memuat tanggapan</string>
+ <string name="error_downloading_image">Galat saat mengunduh gambar</string>
+ <string name="passcode_wrong_passcode">PIN keliru</string>
+ <string name="invalid_email_message">Alamat emailmu tidak sah</string>
+ <string name="invalid_password_message">Password harus berisi minimal 4 karakter</string>
+ <string name="invalid_username_too_short">Nama pengguna harus lebih dari 4 karakter</string>
+ <string name="invalid_username_too_long">Nama pengguna harus kurang dari 61 karakter</string>
+ <string name="username_only_lowercase_letters_and_numbers">Nama pengguna hanya boleh mengandung huruf kecil (a-z) dan angka</string>
+ <string name="username_required">Masukkan nama pengguna</string>
+ <string name="username_not_allowed">Nama pengguna tidak diizinkan</string>
+ <string name="username_must_be_at_least_four_characters">Nama pengguna harus lebih dari 4 karakter</string>
+ <string name="username_contains_invalid_characters">Nama pengguna tidak boleh mengandung karakter “_”</string>
+ <string name="username_must_include_letters">Nama pengguna harus mengandung minimal 1 huruf kecil (a-z)</string>
+ <string name="email_invalid">Masukkan alamat email yang sah</string>
+ <string name="email_not_allowed">Alamat email itu tidak diizinkan</string>
+ <string name="username_exists">Nama pengguna itu sudah terpakai</string>
+ <string name="email_exists">Alamat email itu sudah terpakai</string>
+ <string name="username_reserved_but_may_be_available">Nama pengguna itu sedang dicadangkan, namun bisa menjadi tersedia dalam beberapa hari</string>
+ <string name="blog_name_required">Masukkan alamat situs</string>
+ <string name="blog_name_not_allowed">Alamat situs itu tidak diperbolehkan</string>
+ <string name="blog_name_must_be_at_least_four_characters">Alamat situs harus memiliki setidaknya 4 karakter</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Alamat situs harus kurang dari 64 karakter</string>
+ <string name="blog_name_contains_invalid_characters">Alamat situs tidak boleh mengandung karakter “_”</string>
+ <string name="blog_name_cant_be_used">Anda tidak diperbolehkan menggunakan alamat situs tersebut</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Alamat situs hanya boleh mengandung huruf kecil (a-z) dan angka</string>
+ <string name="blog_name_exists">Situs itu sudah ada</string>
+ <string name="blog_name_reserved">Situs itu dicadangkan</string>
+ <string name="blog_name_reserved_but_may_be_available">Situs itu sedang dicadangkan, namun bisa menjadi tersedia dalam beberapa hari</string>
+ <string name="username_or_password_incorrect">Nama pengguna atau password yang Anda masukkan tidak tepat</string>
+ <string name="nux_cannot_log_in">Kami tak bisa me-log masukkan Anda</string>
+ <string name="could_not_remove_account">Gagal menghapus situs</string>
+ <string name="invalid_url_message">Pastikan URL yang dimasukkan sudah valid</string>
+ <string name="edit_post">Edit tulisan</string>
+ <string name="add_comment">Tambahkan komentar</string>
+ <string name="connection_error">Galat sambungan</string>
+ <string name="cancel_edit">Batalkan edit</string>
+ <string name="media_gallery_image_order">Urutan gambar</string>
+ <string name="learn_more">Pelajari lebih lanjut</string>
+ <string name="xmlrpc_error">Tak bisa tersambung. Masukkan path lengkap ke xmlrpc.php pada situs Anda dan coba lagi.</string>
+ <string name="select_categories">Pilih kategori</string>
+ <string name="account_details">Rincian Akun</string>
+ <string name="post_not_found">Terjadi galat saat memuat pos. Segarkan pos Anda dan coba lagi.</string>
+ <string name="media_gallery_settings_title">Pengaturan Galeri</string>
+ <string name="media_gallery_num_columns">Jumlah kolom</string>
+ <string name="scaled_image_error">Masukkan nilai lebar terskala yang sah</string>
+ <string name="media_gallery_edit">Edit galeri</string>
+ <string name="media_error_no_permission">Anda tidak memiliki izin untuk melihat pustaka media</string>
+ <string name="cannot_delete_multi_media_items">Beberapa media tak bisa dihapus sekarang. Coba lagi nanti.</string>
+ <string name="themes_live_preview">Pratinjau langsung</string>
+ <string name="theme_current_theme">Tema sekarang</string>
+ <string name="theme_premium_theme">Tema premium</string>
+ <string name="link_enter_url_text">Teks tautan (opsional)</string>
+ <string name="create_a_link">Buat tautan</string>
+ <string name="page_settings">Pengaturan laman</string>
+ <string name="local_draft">Draf lokal</string>
+ <string name="upload_failed">Pengunggahan gagal</string>
+ <string name="horizontal_alignment">Perataan horisontal</string>
+ <string name="file_not_found">Tidak bisa menemukan media yang hendak diunggah. Apakah sudah dihapus atau dipindahkan?</string>
+ <string name="post_settings">Pengaturan pos</string>
+ <string name="delete_post">Hapus pos</string>
+ <string name="delete_page">Hapus laman</string>
+ <string name="comment_status_approved">Diizinkan</string>
+ <string name="comment_status_unapproved">Tertunda</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Dibuang</string>
+ <string name="edit_comment">Edit komentar</string>
+ <string name="mnu_comment_approve">Izinkan</string>
+ <string name="mnu_comment_unapprove">Tolak</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Tong Sampah</string>
+ <string name="dlg_approving_comments">Mengizinkan</string>
+ <string name="dlg_unapproving_comments">Menolak</string>
+ <string name="dlg_spamming_comments">Menandai sebagai spam</string>
+ <string name="dlg_trashing_comments">Membuang ke Tong Sampah</string>
+ <string name="dlg_confirm_trash_comments">Buang ke Tong Sampah?</string>
+ <string name="trash_yes">Buang</string>
+ <string name="trash_no">Jangan buang</string>
+ <string name="trash">Tong Sampah</string>
+ <string name="author_name">Nama penulis</string>
+ <string name="author_email">Email penulis</string>
+ <string name="author_url">URL penulis</string>
+ <string name="hint_comment_content">Komentar</string>
+ <string name="saving_changes">Menyimpan perubahan</string>
+ <string name="sure_to_cancel_edit_comment">Batal mengedit komentar ini?</string>
+ <string name="content_required">Komentar diwajibkan</string>
+ <string name="toast_comment_unedited">Komentar belum berubah</string>
+ <string name="delete_draft">Hapus draf</string>
+ <string name="preview_page">Pratinjau laman</string>
+ <string name="preview_post">Pratinjau pos</string>
+ <string name="comment_added">Komentar berhasil ditambahkan</string>
+ <string name="page_not_published">Laman belum diterbitkan</string>
+ <string name="post_not_published">Pos belum diterbitkan</string>
+ <string name="view_in_browser">Tampilkan di peramban</string>
+ <string name="add_new_category">Tambahkan kategori baru</string>
+ <string name="category_name">Nama kategori</string>
+ <string name="category_slug">Slug kategori (opsional)</string>
+ <string name="category_desc">Deskripsi kategori (opsional)</string>
+ <string name="category_parent">Induk kategori (opsional)</string>
+ <string name="share_action_post">Pos baru</string>
+ <string name="share_action_media">Pustaka media</string>
+ <string name="file_error_create">Tidak bisa membuat berkas temporer untuk pengunggahan media. Pastikan masih cukup ruang di perangkat Anda.</string>
+ <string name="open_source_licenses">Lisensi sumber terbuka</string>
+ <string name="location_not_found">Lokasi tidak diketahui</string>
+ <string name="pending_review">Tinjauan tertunda</string>
+ <string name="http_credentials">HTTP credentials (opsional)</string>
+ <string name="http_authorization_required">Otorisasi diwajibkan</string>
+ <string name="post_format">Format pos</string>
+ <string name="new_post">Pos baru</string>
+ <string name="new_media">Media baru</string>
+ <string name="view_site">Tampilkan situs</string>
+ <string name="privacy_policy">Kebijakan privasi</string>
+ <string name="local_changes">Perubahan lokal</string>
+ <string name="image_settings">Pengaturan gambar</string>
+ <string name="add_account_blog_url">Alamat blog</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="error_blog_hidden">Blog ini tersembunyi dan tak bisa dimuat. Aktifkan lagi melalui pengaturan dan coba lagi.</string>
+ <string name="fatal_db_error">Terjadi gagal ketika membuat basis data app. Coba instal ulang app.</string>
+ <string name="jetpack_message_not_admin">Plugin Jetpack dibutuhkan untuk statistik. Hubungi administrator situs.</string>
+ <string name="reader_title_applog">Log aplikasi</string>
+ <string name="reader_share_link">Bagikan tautan</string>
+ <string name="reader_toast_err_add_tag">Tidak bisa menambahkan tag ini</string>
+ <string name="reader_toast_err_remove_tag">Tidak bisa menghapus tag ini</string>
+ <string name="required_field">Isian wajib</string>
+ <string name="email_hint">Alamat email</string>
+ <string name="site_address">Alamat (URL) host mandiri Anda</string>
+ <string name="email_cant_be_used_to_signup">Anda tak bisa menggunakan alamat email tersebut untuk mendaftar. Kami bermasalah dengan blokir mereka atas sejumlah email kami. Silakan gunakan layanan email lainnya.</string>
+ <string name="email_reserved">Alamat email tersebut telah digunakan. Periksa kotak masuk Anda untuk email aktivasi. Jika Anda tidak mengaktifkannya, Anda masih bisa mencoba lagi dalam beberapa hari.</string>
+ <string name="blog_name_must_include_letters">Alamat situs harus memiliki setidaknya 1 huruf (a-z)</string>
+ <string name="blog_name_invalid">Alamat situs tidak sah</string>
+ <string name="blog_title_invalid">Judul situs tidak sah</string>
+ <string name="media_gallery_type_thumbnail_grid">Thumbnail grid</string>
+ <string name="blog_removed_successfully">Situs berhasil dihapus</string>
+ <string name="remove_account">Hapus situs</string>
+ <string name="notifications_empty_all">Belum ada pemberitahuan...sampai sekarang.</string>
+ <string name="invalid_site_url_message">Pastikan URL situs yang dimasukkan sudah valid</string>
+ <string name="deleting_page">Menghapus laman</string>
+ <string name="deleting_post">Menghapus pos</string>
+ <string name="share_url_post">Bagikan pos</string>
+ <string name="share_url_page">Bagikan laman</string>
+ <string name="share_link">Bagikan tautan</string>
+ <string name="creating_your_account">Membuat akun Anda</string>
+ <string name="creating_your_site">Membuat situs Anda</string>
+ <string name="reader_empty_posts_in_tag_updating">Mengambil pos...</string>
+ <string name="error_refresh_media">Ada sesuatu yang salah saat menyegarkan pustaka media. Coba lagi nanti.</string>
+ <string name="reader_likes_you_and_multi">Anda dan %,d lainnya menyukai ini</string>
+ <string name="reader_likes_multi">%,d orang menyukai ini</string>
+ <string name="reader_toast_err_get_comment">Tak bisa menampilkan komentar ini</string>
+ <string name="reader_label_reply">Balas</string>
+ <string name="video">Video</string>
+ <string name="download">Mengunduh media</string>
+ <string name="cant_share_no_visible_blog">Anda tidak bisa berbagi ke WordPress tanpa satupun blog yang terlihat oleh publik</string>
+ <string name="comment_spammed">Komentar ditandai sebagai spam</string>
+ <string name="select_time">Pilih waktu</string>
+ <string name="reader_likes_you_and_one">Anda dan satu orang lainnya menyukai ini</string>
+ <string name="select_date">Pilih tanggal</string>
+ <string name="pick_photo">Pilih foto</string>
+ <string name="account_two_step_auth_enabled">Akun ini memiliki sistem autentifikasi dua langkah yang aktif. Kunjungi pengaturan keamanan Anda di WordPress.com dan buat password khusus untuk aplikasi.</string>
+ <string name="pick_video">Pilih video</string>
+ <string name="reader_toast_err_get_post">Gagal membuka pos ini</string>
+ <string name="validating_user_data">Memeriksa keabsahan data pengguna</string>
+ <string name="validating_site_data">Memeriksa keabsahan data situs</string>
+ <string name="reader_empty_followed_blogs_description">Tapi jangan khawatir, cukup ketuk ikon di kanan atas untuk mulai menjelajah!</string>
+ <string name="password_invalid">Anda butuh password yang lebih aman. Pastikan menggunakan 7 atau lebih karakter, kombinasikan huruf kapital dan kecil, angka atau tanda baca.</string>
+ <string name="nux_tap_continue">Lanjutkan</string>
+ <string name="nux_welcome_create_account">Buat akun</string>
+ <string name="nux_add_selfhosted_blog">Tambahkan situs dengan hostiing mandiri</string>
+ <string name="nux_oops_not_selfhosted_blog">Masuk ke WordPress.com</string>
+ <string name="signing_in">Masuk…</string>
+ <string name="media_add_popup_title">Tambah ke pustaka media</string>
+ <string name="media_add_new_media_gallery">Buat album</string>
+ <string name="empty_list_default">List ini masih kosong</string>
+ <string name="select_from_media_library">Pilih dari pustaka media</string>
+ <string name="jetpack_message">Plugin Jetpack diperlukan untuk Statistik. Apakah anda akan menginstall Jetpack ?</string>
+ <string name="jetpack_not_found">Plugin Jetpack tidak ditemukan</string>
+ <string name="reader_untitled_post">(Tanpa Judul)</string>
+ <string name="reader_share_subject">di sebarkan dari %s</string>
+ <string name="reader_btn_share">Bagikan</string>
+ <string name="reader_btn_follow">Ikuti</string>
+ <string name="reader_btn_unfollow">Pengikut</string>
+ <string name="reader_label_added_tag">%s ditambahkan</string>
+ <string name="reader_label_removed_tag">%s dihapus</string>
+ <string name="reader_likes_one">Satu orang menyukai ini</string>
+ <string name="reader_likes_only_you">Anda menyukai ini</string>
+ <string name="reader_toast_err_comment_failed">Tidak bisa menambahkan komentar anda</string>
+ <string name="reader_toast_err_tag_exists">Anda telah mengikuti tag ini</string>
+ <string name="reader_toast_err_tag_invalid">Tag ini tidak valid</string>
+ <string name="reader_toast_err_share_intent">Tidak dapat membagikan</string>
+ <string name="reader_toast_err_view_image">Tidak dapat melihat gambar</string>
+ <string name="reader_toast_err_url_intent">Tidak bisa membuka %s</string>
+ <string name="reader_empty_followed_tags">Anda belum mengikuti satupun tag</string>
+ <string name="create_account_wpcom">Buat akun dengan Wordpress.com</string>
+ <string name="connecting_wpcom">MEnghubungkan ke Wordpress.com</string>
+ <string name="username_invalid">Username tidak valid</string>
+ <string name="limit_reached">Mencapai batasan. Anda dapat mencoba kembali setelah 1 menit. Cobalah kembali, ini akan menambahkan kesempatan kepada anda sebelum benar-benar masuk list banned. Jika menurut anda ini adalah suatu kesalahan, hubungi Support.</string>
+ <string name="nux_tutorial_get_started_title">Memulai!</string>
+ <string name="reader_hint_comment_on_comment">Balas komentar…</string>
+ <string name="button_next">Selanjutnya</string>
+ <string name="all">Semua</string>
+ <string name="images">Gambar</string>
+ <string name="themes">Tema</string>
+ <string name="custom_date">Tanggal Tersuai</string>
+ <string name="unattached">Tidak terlampir</string>
+ <string name="media_add_popup_capture_photo">Ambil foto</string>
+ <string name="media_add_popup_capture_video">Rekam video</string>
+ <string name="media_edit_title_text">Judul</string>
+ <string name="media_edit_caption_text">Subjudul</string>
+ <string name="media_edit_description_text">Deskripsi</string>
+ <string name="media_edit_title_hint">Masukkan judul di sini</string>
+ <string name="media_edit_caption_hint">Masukkan subjudul di sini</string>
+ <string name="media_edit_description_hint">Masukkan deskripsi di sini</string>
+ <string name="media_edit_success">Telah diperbarui</string>
+ <string name="media_gallery_image_order_random">Acak</string>
+ <string name="media_gallery_image_order_reverse">Terbalik</string>
+ <string name="media_gallery_type">Tipe</string>
+ <string name="media_gallery_type_squares">Kotak-kotak</string>
+ <string name="media_gallery_type_tiled">Tersusun</string>
+ <string name="media_gallery_type_circles">Lingkaran</string>
+ <string name="media_gallery_type_slideshow">Slide</string>
+ <string name="media_edit_failure">Gagal memperbarui</string>
+ <string name="themes_details_label">Detil</string>
+ <string name="themes_features_label">Fitur</string>
+ <string name="theme_activate_button">Aktivasi</string>
+ <string name="theme_activating_button">Mengaktivasi</string>
+ <string name="theme_set_success">Berhasil memasang tema!</string>
+ <string name="theme_auth_error_title">Gagal mengambil tema</string>
+ <string name="stats_view_clicks">Klik</string>
+ <string name="stats_view_tags_and_categories">Tag &amp; Kategori</string>
+ <string name="stats_view_referrers">Pengacu</string>
+ <string name="stats_timeframe_today">Hari Ini</string>
+ <string name="stats_timeframe_yesterday">Kemarin</string>
+ <string name="post_excerpt">Rangkuman</string>
+ <string name="share_action_title">Tambah ke ...</string>
+ <string name="share_action">Bagi</string>
+ <string name="stats">Stat</string>
+ <string name="stats_view_visitors_and_views">Pengunjung dan Tampilan</string>
+ <string name="stats_totals_views">Tampilan</string>
+ <string name="stats_totals_clicks">Klik</string>
+ <string name="stats_totals_plays">Pemutaran</string>
+ <string name="stats_timeframe_days">Hari</string>
+ <string name="stats_timeframe_weeks">Minggu</string>
+ <string name="stats_timeframe_months">Bulan</string>
+ <string name="stats_entry_country">Negara</string>
+ <string name="stats_entry_posts_and_pages">Judul</string>
+ <string name="stats_entry_tags_and_categories">Topik</string>
+ <string name="stats_entry_authors">Penulis</string>
+ <string name="stats_entry_referrers">Pengacu</string>
+ <string name="passcode_set">Pasang PIN</string>
+ <string name="passcode_preference_title">Kunci PIN</string>
+ <string name="passcode_turn_off">Matikan kunci PIN</string>
+ <string name="passcode_turn_on">Nyalakan kunci PIN</string>
+ <string name="passcode_manage">Atur kunci PIN</string>
+ <string name="passcode_enter_passcode">Masukkan PIN Anda</string>
+ <string name="passcode_enter_old_passcode">Masukkan PIN lama Anda</string>
+ <string name="passcode_re_enter_passcode">Masukkan kembali PIN Anda</string>
+ <string name="passcode_change_passcode">Ubah PIN</string>
+ <string name="upload">Unggah</string>
+ <string name="discard">Batal</string>
+ <string name="notifications">Notifikasi</string>
+ <string name="note_reply_successful">Balasan telah terbit</string>
+ <string name="new_notifications">%d notifikasi baru</string>
+ <string name="more_notifications">dan %d lagi.</string>
+ <string name="sign_in">Log masuk</string>
+ <string name="follows">Mengikuti</string>
+ <string name="loading">Memuat...</string>
+ <string name="httpuser">Nama pengguna HTTP</string>
+ <string name="httppassword">Kata sandi HTTP</string>
+ <string name="error_media_upload">Ada masalah ketika mengunggah media</string>
+ <string name="content_description_add_media">Tambah media</string>
+ <string name="publish_date">Terbitkan</string>
+ <string name="post_content">Konten (tekan untuk menambah teks dan media)</string>
+ <string name="incorrect_credentials">Nama pengguna atau kata sandi salah.</string>
+ <string name="password">Kata sandi</string>
+ <string name="username">Nama pengguna</string>
+ <string name="reader">Pembaca</string>
+ <string name="post">Terbitkan</string>
+ <string name="no_network_title">Tidak ada jaringan</string>
+ <string name="page">Halaman</string>
+ <string name="pages">Halaman</string>
+ <string name="caption">Subjudul (opsional)</string>
+ <string name="width">Lebar</string>
+ <string name="posts">Tulisan</string>
+ <string name="anonymous">Anonim</string>
+ <string name="featured">Gunakan sebagai gambar terfitur</string>
+ <string name="featured_in_post">Sertakan gambar dalam konten tulisan</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Unggah dan tautkan ke gambar terskala</string>
+ <string name="scaled_image">Lebar gambar terskala</string>
+ <string name="scheduled">Dijadwalkan</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Mengunggah...</string>
+ <string name="version">Versi</string>
+ <string name="app_title">WordPress untuk Android</string>
+ <string name="tos">Syarat Layanan</string>
+ <string name="max_thumbnail_px_width">Lebar Gambar Baku</string>
+ <string name="image_alignment">Penjajaran</string>
+ <string name="refresh">Segarkan</string>
+ <string name="untitled">Tanpa judul</string>
+ <string name="edit">Sunting</string>
+ <string name="post_id">Tulisan</string>
+ <string name="page_id">Halaman</string>
+ <string name="post_password">Kata sandi (opsional)</string>
+ <string name="immediately">Segera</string>
+ <string name="quickpress_add_alert_title">Tentukan nama pintasan</string>
+ <string name="today">Hari ini</string>
+ <string name="settings">Pengaturan</string>
+ <string name="share_url">Berbagi URL</string>
+ <string name="quickpress_window_title">Pilih blog untuk pintasan QuickPress</string>
+ <string name="quickpress_add_error">Nama pintasan tidak boleh kosong</string>
+ <string name="publish_post">Publikasi</string>
+ <string name="draft">Naskah</string>
+ <string name="post_private">Privat</string>
+ <string name="upload_full_size_image">Unggah dan tautkan ke gambar ukuran penuh</string>
+ <string name="categories">Kategori</string>
+ <string name="title">Judul</string>
+ <string name="tags_separate_with_commas">Tag (pisahkan tag dengan tanda koma)</string>
+ <string name="dlg_deleting_comments">Menghapus komentar</string>
+ <string name="notification_vibrate">Getar</string>
+ <string name="notification_blink">Lampu notifikasi berkelip</string>
+ <string name="notification_sound">Bunyi notifikasi</string>
+ <string name="status">Status</string>
+ <string name="select_video">Pilih video dari galeri</string>
+ <string name="sdcard_title">Dibutuhkan SD Card</string>
+ <string name="location">Lokasi</string>
+ <string name="media">Media</string>
+ <string name="delete">Hapus</string>
+ <string name="none">Tiada</string>
+ <string name="blogs">Blog</string>
+ <string name="select_photo">Pilih sebuah foto dari galeri</string>
+ <string name="reply">Balasan</string>
+ <string name="preview">Pratinjau</string>
+ <string name="on">pada</string>
+ <string name="cancel">Batal</string>
+ <string name="save">Simpan</string>
+ <string name="add">Tambah</string>
+ <string name="yes">Ya</string>
+ <string name="no">Tidak</string>
+ <string name="error">Galat</string>
+ <string name="category_refresh_error">Gagal memuat ulang Kategori</string>
+ <string name="notification_settings">Pengaturan Pemberitahuan</string>
+</resources>
diff --git a/WordPress/src/main/res/values-in b/WordPress/src/main/res/values-in
new file mode 120000
index 000000000..f7118b95e
--- /dev/null
+++ b/WordPress/src/main/res/values-in
@@ -0,0 +1 @@
+values-id \ No newline at end of file
diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..a10f26ff7
--- /dev/null
+++ b/WordPress/src/main/res/values-it/strings.xml
@@ -0,0 +1,1132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="error_fetch_users_list">Non è possibile recuperare gli utenti del sito</string>
+ <string name="plans_manage">Gestisci il tuo piano su\nWordPress.com/plans</string>
+ <string name="title_follower">Follower</string>
+ <string name="title_email_follower">Email al follower</string>
+ <string name="people_empty_list_filtered_viewers">Non hai ancora visitatori.</string>
+ <string name="people_empty_list_filtered_email_followers">Non hai ancora follower via email.</string>
+ <string name="people_empty_list_filtered_followers">Non hai ancora follower.</string>
+ <string name="people_empty_list_filtered_users">Non hai ancora utenti.</string>
+ <string name="people_dropdown_item_email_followers">Follower via email</string>
+ <string name="people_dropdown_item_viewers">Visitatori</string>
+ <string name="people_dropdown_item_followers">Follower</string>
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="invite_message_usernames_limit">Invita fino a 10 indirizzi email e/o nome utente di WordPress.com. A coloro che che non hanno un nome utente, saranno spedite le istruzioni su come crearne uno.</string>
+ <string name="viewer_remove_confirmation_message">Se rimuovi questo visitatore, lui o lei non avrà più la possibilità di visitare questo sito.\n\nVuoi ancora rimuovere questo visitatore?</string>
+ <string name="follower_remove_confirmation_message">Se rimosso, questo follower smetterà di ricevere notifiche da questo sito, a meno che non si iscriva di nuovo.\n\nVuoi ancora di rimuovere questo follower?</string>
+ <string name="follower_subscribed_since">Dal %1$s</string>
+ <string name="reader_label_view_gallery">Visualizza galleria</string>
+ <string name="error_remove_follower">Non è possibile rimuovere il follower</string>
+ <string name="error_remove_viewer">Non è possibile rimuovere il visitatore</string>
+ <string name="error_fetch_email_followers_list">Non è possibile recuperare l\'email dei follower del sito</string>
+ <string name="error_fetch_followers_list">Non è possibile recuperare i follower del sito</string>
+ <string name="editor_failed_uploads_switch_html">Il caricamento di alcuni elementi multimediali non è andato a buon fine. Non puoi passare alla modalità HTML\n in questo momento. Vuoi rimuovere i caricamenti falliti e proseguire?</string>
+ <string name="format_bar_description_html">Modalità HTML</string>
+ <string name="visual_editor">Editor visuale</string>
+ <string name="image_thumbnail">Immagine in minaitura</string>
+ <string name="format_bar_description_ol">Lista numerata</string>
+ <string name="format_bar_description_ul">Lista puntata</string>
+ <string name="format_bar_description_more">Inserire di più</string>
+ <string name="format_bar_description_media">Inserire l\'elemento multimediale</string>
+ <string name="format_bar_description_link">Inserisci link</string>
+ <string name="format_bar_description_strike">Barrato</string>
+ <string name="format_bar_description_quote">Citazione</string>
+ <string name="format_bar_description_italic">Corsivo</string>
+ <string name="format_bar_description_underline">Sottolineato</string>
+ <string name="image_link_to">Link a</string>
+ <string name="image_alt_text">Testo alternativo</string>
+ <string name="image_settings_save_toast">Modifiche salvate</string>
+ <string name="image_caption">Didascalia</string>
+ <string name="image_width">Larghezza</string>
+ <string name="format_bar_description_bold">Grassetto</string>
+ <string name="image_settings_dismiss_dialog_title">Elimina le modifiche non salvate?</string>
+ <string name="stop_upload_button">Ferma il caricamento</string>
+ <string name="stop_upload_dialog_title">Ferma il caricamento?</string>
+ <string name="alert_error_adding_media">Durante l\'inserimento di un elemento multimediale si è verificato un errore.</string>
+ <string name="alert_action_while_uploading">Stai attualmente caricando un elemento multimediale. Attendi il suo completamento.</string>
+ <string name="alert_insert_image_html_mode">Impossibile inserire direttamente elementi multimediali in modalità HTML.Passa alla modalità visuale.</string>
+ <string name="uploading_gallery_placeholder">Caricamento della galleria…</string>
+ <string name="tap_to_try_again">Tocca per provare di nuovo!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Invito inviato con successo</string>
+ <string name="invite_error_some_failed">L\'invito è stato inviato ma c\'è stato qualche problema!</string>
+ <string name="invite_error_sending">Si è verificato un errore nel tentativo di inviare l\'invito!</string>
+ <string name="invite_error_invalid_usernames_multiple">Non è possibile inviare l\'email: ci sono dei nomi utente o degli indirizzi non validi.</string>
+ <string name="invite_error_invalid_usernames_one">Non è possibile inviare l\'email: c\'è un nomi utente o un indirizzo non valido. </string>
+ <string name="invite_error_no_usernames">Aggiungi almeno un nome utente</string>
+ <string name="invite_message_info">(Opzionale) Puoi inserire un messaggio personalizzato sino a 500 caratteri che verrà inserito nell\'invito a/agli utente/i.</string>
+ <string name="invite_message_remaining_other">%d caratteri rimanenti</string>
+ <string name="invite_message_remaining_one">1 carattere rimanente</string>
+ <string name="invite_message_remaining_zero">0 caratteri rimanenti</string>
+ <string name="invite_invalid_email">L\'indirizzo email \'%s\' non è valido</string>
+ <string name="invite_message_title">Messaggio personalizzato</string>
+ <string name="invite_already_a_member">Vi è già un membro con il nome utente \'%s\'</string>
+ <string name="invite_username_not_found">Nessun utente trovato per il nome utente \'%s\'</string>
+ <string name="invite">Invita</string>
+ <string name="invite_names_title">Nomi utente o email</string>
+ <string name="send_link">Invia link</string>
+ <string name="my_site_header_external">Esterno</string>
+ <string name="invite_people">Invita delle persone</string>
+ <string name="signup_succeed_signin_failed">Il tuo account è stato creato ma si è verificato un errore quando abbiamo provato ad autenticarti.\n Provata ad entrare col i tuo nome utente e la password appena creati.</string>
+ <string name="label_clear_search_history">Pulisci lo storico della ricerca</string>
+ <string name="dlg_confirm_clear_search_history">Pulire lo storico della ricerca?</string>
+ <string name="reader_empty_posts_in_search_description">Nessun articolo trovato riguardo %s per la tua lingua</string>
+ <string name="reader_label_post_search_running">Ricerca in corso...</string>
+ <string name="reader_label_related_posts">Letture correlate</string>
+ <string name="reader_empty_posts_in_search_title">Nessun articolo trovato</string>
+ <string name="reader_label_post_search_explainer">Cerca in tutti i blog pubblici di WordPress.com</string>
+ <string name="reader_hint_post_search">Cerca in WordPress.com</string>
+ <string name="reader_title_related_post_detail">Articoli correlati</string>
+ <string name="reader_title_search_results">Cerca %s</string>
+ <string name="preview_screen_links_disabled">I link sono disabilitati nella schermata di anteprima</string>
+ <string name="draft_explainer">Questo articolo è un bozza che non è stata ancora pubblicata</string>
+ <string name="send">Invia</string>
+ <string name="user_remove_confirmation_message">Se rimuovi %1$s, questo utente non sarà più in grado di accedere a questo sito ma qualsiasi contenuto creato da %1$s rimarrà disponibile sul sito.\n\nVuoi davvero rimuovere questo utente?</string>
+ <string name="person_removed">\@%1$s è stato rimosso correttamente</string>
+ <string name="person_remove_confirmation_title">Rimuovere %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">I siti in questo elenco non hanno pubblicato nullla di recente</string>
+ <string name="people">Persone</string>
+ <string name="edit_user">Modifica utente</string>
+ <string name="role">Ruolo</string>
+ <string name="error_remove_user">Impossibile rimuovere l\'utente</string>
+ <string name="error_update_role">Impossibile aggiornare il ruolo utente</string>
+ <string name="error_fetch_viewers_list">Non è possibile recuperare i visitatori del sito</string>
+ <string name="gravatar_camera_and_media_permission_required">Autorizzazioni necessarie al fine di selezionare o catturare una foto</string>
+ <string name="error_updating_gravatar">Errore nell\'aggiornamento del tuo Gravatar</string>
+ <string name="error_refreshing_gravatar">Errore nel ricaricamento del tuo Gravatar</string>
+ <string name="error_locating_image">Errore nella localizzazione dell\'immagine ritagliata</string>
+ <string name="gravatar_tip">Novità! Fai un tap sul tuo Gravatar per modificarlo!</string>
+ <string name="error_cropping_image">Errore nel ritaglio dell\'immagine</string>
+ <string name="launch_your_email_app">Esegui la tua app email</string>
+ <string name="checking_email">Verifica della email</string>
+ <string name="not_on_wordpress_com">Non sei su WordPress.com?</string>
+ <string name="check_your_email">Controlla la tua email</string>
+ <string name="magic_link_unavailable_error_message">Attualmente non disponibile. Inserisci la tua password</string>
+ <string name="logging_in">Sto entrando</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Ottieni un link inviato alla tua email per autenticarti istantaneamente</string>
+ <string name="enter_your_password_instead">Oppure inserisci la tua password</string>
+ <string name="web_address_dialog_hint">Mostrato pubblicamente quando si commenta.</string>
+ <string name="jetpack_not_connected_message">Il plugin Jetpack è installato ma non è connesso a WordPress.com. Desideri connettere Jetpack?</string>
+ <string name="username_email">Email o nome utente</string>
+ <string name="jetpack_not_connected">Il plugin Jetpack non è connesso</string>
+ <string name="new_editor_reflection_error">L\'editor visuale non è compatibile con il tuo device. &amp;Egrave; stato automaticamente disabilitato.</string>
+ <string name="stats_insights_latest_post_no_title">(nessun titolo)</string>
+ <string name="capture_or_pick_photo">Catturare o selezionare una foto</string>
+ <string name="plans_post_purchase_button_themes">Sfoglia i temi</string>
+ <string name="plans_post_purchase_text_themes">Ora hai accesso illimitato ai temi premium. Per cominciare, guarda un\'anteprima di ogni tema sul tuo sito.</string>
+ <string name="plans_post_purchase_title_themes">Trova il tema premium perfetto</string>
+ <string name="plans_post_purchase_button_video">Comincia un nuovo articolo</string>
+ <string name="plans_post_purchase_text_video">Potrai effettuare il caricamento dei video nel tuo sito con VideoPress e nel tuo spazio di archiviazione esteso.</string>
+ <string name="plans_post_purchase_title_video">Anima gli articoli con i video</string>
+ <string name="plans_post_purchase_button_customize">Personalizza il mio sito</string>
+ <string name="plans_post_purchase_text_customize">Ora hai accesso a font e colori personalizzati e le possibilità di editare un CSS personalizzato.</string>
+ <string name="plans_post_purchase_title_customize">Personalizza font e colori</string>
+ <string name="plans_post_purchase_text_intro">Il tuo sito sta facendo salti di gioia! Ora esplora le nuove funzionalità del tuo sito e scegli da dove vuoi cominciare.</string>
+ <string name="plans_post_purchase_title_intro">È tutto tuo, ben fatto!</string>
+ <string name="plan">Piano</string>
+ <string name="plans">Piani</string>
+ <string name="export_your_content_message">I tuoi articoli, pagine e impostazioni ti saranno inviate via e-mail all\'indirizzo %s.</string>
+ <string name="plans_loading_error">Non è stato possibile caricare i piani</string>
+ <string name="export_your_content">Esporta i contenuti</string>
+ <string name="exporting_content_progress">Esportazione dei contenuti in corso ...</string>
+ <string name="export_email_sent">Email di esportazione inviata!</string>
+ <string name="show_purchases">Mostra gli acquisti</string>
+ <string name="premium_upgrades_message">Sul tuo sito sono attivi alcuni aggiornamenti premium. Cancella gli aggiornamenti prima di cancellare il sito.</string>
+ <string name="checking_purchases">Controllo degli acquisti</string>
+ <string name="premium_upgrades_title">Aggiornamenti alla versione Premium</string>
+ <string name="purchases_request_error">Qualcosa è andato storto. Non è stato possibile richiedere gli acquisti.</string>
+ <string name="delete_site_progress">Cancellazione del sito in corso ...</string>
+ <string name="delete_site_summary">Questa azione è irreversibile. Cancellando il tuo sito rimuoverai tutti i contenuti, collaboratori, e domini.</string>
+ <string name="delete_site_hint">Elimina il sito</string>
+ <string name="export_site_hint">Esporta il sito in un file XML</string>
+ <string name="are_you_sure">Sei sicuro?</string>
+ <string name="export_site_summary">Se sei sicuro, esporta prima i contenuti perchè non potranno più essere recuperati.</string>
+ <string name="keep_your_content">Mantieni il tuo contenuto</string>
+ <string name="domain_removal_hint">I domini non funzioneranno più una volta che avrai rimosso il sito</string>
+ <string name="domain_removal_summary">Attenzione! Cancellando il sito rimuoverai anche i domini sottoelencati.</string>
+ <string name="primary_domain">Dominio principale</string>
+ <string name="domain_removal">Rimozione del dominio</string>
+ <string name="error_deleting_site_summary">C\'è stato un errore durante la cancellazione del sito. Contatta il forum di supporto per ulteriore assistenza</string>
+ <string name="error_deleting_site">Errore durante la cancellazione del sito</string>
+ <string name="confirm_delete_site_prompt">Digita %1$s nel campo sotto alla conferma. Dopo il sito sarà cancellato per sempre.</string>
+ <string name="site_settings_export_content_title">Esporta il contenuto</string>
+ <string name="contact_support">Contatta il supporto</string>
+ <string name="confirm_delete_site">Conferma la cancellazione del sito</string>
+ <string name="start_over_text">Se vuoi mantenere il sito ma non vuoi più tenere nessuno degli articoli e delle pagine che ci sono ora, la nostra squadra di supporto può cancellare per te articoli, pagine, media e commenti.\n\n Questo terrà attivi sito e URL, e ti permetterà di creare contenuti nuovi. Contattaci per chiederci di cancellare i contenuti attuali.</string>
+ <string name="let_us_help">Lascia che ti aiutiamo</string>
+ <string name="site_settings_start_over_hint">Ricomincia da capo il tuo sito</string>
+ <string name="me_btn_app_settings">Impostazioni dell\'applicazione</string>
+ <string name="start_over">Ricomincia</string>
+ <string name="editor_remove_failed_uploads">Rimuovi i caricamenti falliti</string>
+ <string name="editor_toast_failed_uploads">Il caricamento di qualche media è fallito. Tu puoi non salvare oppure pubblicare \n il tuo articolo così. Vuoi rimuovere i file dei media il cui caricamento è fallito?</string>
+ <string name="comments_empty_list_filtered_trashed">Nessun commento nel cestino</string>
+ <string name="site_settings_advanced_header">Avanzato</string>
+ <string name="comments_empty_list_filtered_pending">Nessun commento in sospeso</string>
+ <string name="comments_empty_list_filtered_approved">Nessun commento approvato</string>
+ <string name="button_done">Fatto</string>
+ <string name="button_skip">Salta</string>
+ <string name="site_timeout_error">Non è stato possibile connettersi al sito WordPress a causa di un errore di timeout.</string>
+ <string name="xmlrpc_malformed_response_error">Non è possibile connettersi. L\'installazione WordPress risponde con un documento XML-RPC non valido.</string>
+ <string name="xmlrpc_missing_method_error">Non è possibile connettersi. Sul server mancano i necessari metodi XML-RPC</string>
+ <string name="post_format_status">Stato</string>
+ <string name="post_format_video">Video</string>
+ <string name="theme_free">Gratuito</string>
+ <string name="theme_all">Tutti</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Galleria</string>
+ <string name="post_format_image">Immagine</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Citazione</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Informazioni sui corsi e sugli eventi di WordPress.com (online e di persona).</string>
+ <string name="post_format_aside">Digressione</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Opportunità di partecipare ai sondaggi e alle ricerche di WordPress.com.</string>
+ <string name="notif_tips">Suggerimenti per utilizzare al meglio WordPress.com.</string>
+ <string name="notif_community">Comunità</string>
+ <string name="replies_to_my_comments">Risposte ai miei commenti</string>
+ <string name="notif_suggestions">Suggerimenti</string>
+ <string name="notif_research">Ricerca</string>
+ <string name="site_achievements">Obiettivi del sito</string>
+ <string name="username_mentions">Riferimenti al nome utente</string>
+ <string name="likes_on_my_posts">Mi piace per i miei articoli</string>
+ <string name="site_follows">Segui del sito</string>
+ <string name="likes_on_my_comments">Mi piace per i miei commenti</string>
+ <string name="comments_on_my_site">Commenti sul mio sito</string>
+ <string name="site_settings_list_editor_summary_other">%d elementi</string>
+ <string name="site_settings_list_editor_summary_one">1 elemento</string>
+ <string name="approve_auto">Tutti gli utenti</string>
+ <string name="approve_auto_if_previously_approved">Commenti di utenti conosciuti</string>
+ <string name="approve_manual">Nessun commento</string>
+ <string name="site_settings_paging_summary_other">%d commenti per pagina</string>
+ <string name="site_settings_paging_summary_one">1 commento per pagina</string>
+ <string name="site_settings_multiple_links_summary_other">Richiedi l\'approvazione per più di %d link</string>
+ <string name="site_settings_multiple_links_summary_one">Richiedi l\'approvazione per più di 1 link</string>
+ <string name="site_settings_multiple_links_summary_zero">Richiedi l\'approvazione per più di 0 link</string>
+ <string name="detail_approve_auto">Approva automaticamente i commenti di tutti gli utenti.</string>
+ <string name="detail_approve_auto_if_previously_approved">Approva automaticamente se l\'utente ha approvato un commento in precedenza</string>
+ <string name="detail_approve_manual">Richiedi l\'approvazione manuale per i commenti di tutti gli utenti.</string>
+ <string name="days_quantity_other">%d giorni</string>
+ <string name="days_quantity_one">1 giorno</string>
+ <string name="filter_trashed_posts">Eliminati</string>
+ <string name="filter_published_posts">Pubblicati</string>
+ <string name="filter_draft_posts">Bozze</string>
+ <string name="filter_scheduled_posts">Programmati</string>
+ <string name="pending_email_change_snackbar">Fai clic sul link di verifica nell\'e-mail inviata a %1$s per confermare il tuo nuovo indirizzo</string>
+ <string name="primary_site">Sito principale</string>
+ <string name="web_address">Indirizzo web</string>
+ <string name="editor_toast_uploading_please_wait">Al momento stai caricando file multimediali. Attendi il termine dell\'operazione.</string>
+ <string name="error_refresh_comments_showing_older">Impossibile aggiornare i commenti in questo momento, vengono visualizzati i commenti precedenti</string>
+ <string name="editor_post_settings_set_featured_image">Imposta immagine in primo piano</string>
+ <string name="editor_post_settings_featured_image">Immagine in evidenza</string>
+ <string name="new_editor_promo_desc">L\'app WordPress per Android ora include un nuovo bellissimo editor\n visuale. Provalo creando un nuovo articolo.</string>
+ <string name="new_editor_promo_title">Un editor completamente nuovo</string>
+ <string name="new_editor_promo_button_label">Fantastico, grazie!</string>
+ <string name="visual_editor_enabled">Editor visuale abilitato</string>
+ <string name="editor_content_placeholder">Condividi la tua storia qui...</string>
+ <string name="editor_page_title_placeholder">Titolo della pagina</string>
+ <string name="editor_post_title_placeholder">Titolo dell\'articolo</string>
+ <string name="email_address">Indirizzo e-mail</string>
+ <string name="preference_show_visual_editor">Mostra editor visuale</string>
+ <string name="dlg_sure_to_delete_comments">Eliminare in modo permanente questi commenti?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Eliminare in modo permanente questo commento?</string>
+ <string name="mnu_comment_delete_permanently">Elimina</string>
+ <string name="mnu_comment_untrash">Ripristina</string>
+ <string name="comment_deleted_permanently">Commento eliminato</string>
+ <string name="comments_empty_list_filtered_spam">Nessun commento spam</string>
+ <string name="could_not_load_page">Impossibile caricare la pagina</string>
+ <string name="comment_status_all">Tutti</string>
+ <string name="interface_language">Lingua dell\'interfaccia</string>
+ <string name="off">Disattivata</string>
+ <string name="about_the_app">Info sull\'app</string>
+ <string name="error_post_account_settings">Impossibile salvare le impostazioni del tuo account</string>
+ <string name="error_post_my_profile">Impossibile salvare il profilo</string>
+ <string name="error_fetch_account_settings">Impossibile recuperare le impostazioni dell\'account</string>
+ <string name="error_fetch_my_profile">Impossibile recuperare il profilo</string>
+ <string name="stats_widget_promo_ok_btn_label">OK</string>
+ <string name="stats_widget_promo_desc">Aggiungi il widget alla schermata Home per accedere alle tue statistiche con un clic.</string>
+ <string name="stats_widget_promo_title">Widget delle statistiche della schermata Home</string>
+ <string name="site_settings_unknown_language_code_error">Codice lingua non riconosciuto</string>
+ <string name="site_settings_threading_dialog_description">Consenti di nidificare i commenti in thread.</string>
+ <string name="site_settings_threading_dialog_header">Thread fino a</string>
+ <string name="remove">Rimuovi</string>
+ <string name="search">Cerca</string>
+ <string name="add_category">Aggiungi categoria</string>
+ <string name="disabled">Disattivata</string>
+ <string name="site_settings_image_original_size">Dimensioni originali</string>
+ <string name="privacy_private">Il tuo sito è visibile solo a te e agli utenti che approvi</string>
+ <string name="privacy_public_not_indexed">Il tuo sito è visibile a chiunque, ma chiede ai motori di ricerca di non indicizzarlo</string>
+ <string name="privacy_public">Il tuo sito è visibile a chiunque e può essere indicizzato dai motori di ricerca</string>
+ <string name="about_me_hint">Alcune informazioni su di te…</string>
+ <string name="public_display_name_hint">Se non viene impostato, il nome visualizzato coinciderà con il nome utente per impostazione predefinita</string>
+ <string name="about_me">Informazioni su di me</string>
+ <string name="public_display_name">Nome visualizzato (pubblico)</string>
+ <string name="my_profile">Il mio profilo</string>
+ <string name="first_name">Nome</string>
+ <string name="last_name">Cognome</string>
+ <string name="site_privacy_public_desc">Permetti ai motori di ricerca di indicizzare questo sito</string>
+ <string name="site_privacy_hidden_desc">Scoraggia i motori di ricerca ad effettuare l\'indicizzazione di questo sito</string>
+ <string name="site_privacy_private_desc">Vorrei che il mio sito fosse privato, visibile solo agli utenti scelti da me</string>
+ <string name="cd_related_post_preview_image">Immagine di anteprima dell\'articolo correlato</string>
+ <string name="error_post_remote_site_settings">Impossibile salvare le informazioni del sito</string>
+ <string name="error_fetch_remote_site_settings">Impossibile recuperare le informazioni del sito</string>
+ <string name="error_media_upload_connection">Si è verificato un errore di connessione durante il caricamento dei contenuti multimediali</string>
+ <string name="site_settings_disconnected_toast">Disconnesso. Modifica disabilitata.</string>
+ <string name="site_settings_unsupported_version_error">Versione di WordPress non supportata</string>
+ <string name="site_settings_multiple_links_dialog_description">Richiedi l\'approvazione per i commenti che includono un numero di link superiore a quello specificato.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Chiudi automaticamente</string>
+ <string name="site_settings_close_after_dialog_description">Chiudi automaticamente i commenti sugli articoli.</string>
+ <string name="site_settings_paging_dialog_description">Suddividi il flusso dei commenti in più pagine.</string>
+ <string name="site_settings_paging_dialog_header">Commenti per pagina</string>
+ <string name="site_settings_close_after_dialog_title">Chiudi commenti</string>
+ <string name="site_settings_blacklist_description">Quando un commento contiene una di queste parole nel testo, nel nome, nell\'URL, nell\'e-mail o nell\'IP, verrà contrassegnato come spam. Puoi inserire parzialmente le parole, per cui "press" corrisponderà a "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">Quando un commento contiene una di queste parole nel testo, nel nome, nell\'URL, nell\'e-mail o nell\'IP, verrà inserito nella coda di moderazione. Puoi inserire parzialmente le parole, per cui "press" corrisponderà a "WordPress".</string>
+ <string name="site_settings_list_editor_input_hint">Inserisci una parola o una frase</string>
+ <string name="site_settings_list_editor_no_items_text">Nessun elemento</string>
+ <string name="site_settings_learn_more_caption">Puoi sovrascrivere queste impostazioni per i singoli articoli.</string>
+ <string name="site_settings_rp_preview3_site">in "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Punti principali dell\'aggiornamento: VideoPress per matrimoni</string>
+ <string name="site_settings_rp_preview2_site">in "App"</string>
+ <string name="site_settings_rp_preview2_title">Grande rinnovamento di WordPress per l\'app Android</string>
+ <string name="site_settings_rp_preview1_site">in "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Grande aggiornamento per iPhone/iPad ora disponibile</string>
+ <string name="site_settings_rp_show_images_title">Mostra le immagini</string>
+ <string name="site_settings_rp_show_header_title">Mostra l\'intestazione</string>
+ <string name="site_settings_rp_switch_summary">Articoli correlati visualizza contenuti pertinenti del tuo sito sotto i tuoi articoli.</string>
+ <string name="site_settings_rp_switch_title">Mostra gli articoli correlati</string>
+ <string name="site_settings_delete_site_hint">Rimuovi i dati del sito dall\'app</string>
+ <string name="site_settings_blacklist_hint">I commenti che corrispondono a un filtro vengono contrassegnati come spam</string>
+ <string name="site_settings_moderation_hold_hint">I commenti che corrispondono a un filtro vengono inseriti nella coda di moderazione</string>
+ <string name="site_settings_multiple_links_hint">Ignora limite di link da utenti conosciuti</string>
+ <string name="site_settings_whitelist_hint">L\'autore del commento deve avere un commento già approvato in precedenza</string>
+ <string name="site_settings_user_account_required_hint">Gli utenti devono essere registrati ed aver effettuato l\'accesso per poter inviare commenti</string>
+ <string name="site_settings_identity_required_hint">L\'autore del commento deve inserire nome e indirizzo e-mail</string>
+ <string name="site_settings_manual_approval_hint">I commenti devono essere approvati manualmente</string>
+ <string name="site_settings_paging_hint">Visualizza commenti in parti di dimensioni specifiche</string>
+ <string name="site_settings_threading_hint">Consenti commenti nidificati per una data profondità</string>
+ <string name="site_settings_sort_by_hint">Determina l\'ordine in cui i commenti vengono visualizzati</string>
+ <string name="site_settings_close_after_hint">Rifiuta i commenti dopo l\'orario specificato</string>
+ <string name="site_settings_receive_pingbacks_hint">Consenti notifiche link da altri blog</string>
+ <string name="site_settings_send_pingbacks_hint">Tenta di notificare tutti i blog che hanno un link nell\'articolo</string>
+ <string name="site_settings_allow_comments_hint">Consenti ai lettori di pubblicare commenti</string>
+ <string name="site_settings_discussion_hint">Visualizza e modifica le impostazioni di discussione del sito</string>
+ <string name="site_settings_more_hint">Visualizza tutte le impostazioni di discussione disponibili</string>
+ <string name="site_settings_related_posts_hint">Mostra o nascondi gli articoli correlati nel lettore</string>
+ <string name="site_settings_upload_and_link_image_hint">Consenti sempre di caricare l\'immagine completa</string>
+ <string name="site_settings_image_width_hint">Ridimensiona le immagini negli articoli secondo questa larghezza</string>
+ <string name="site_settings_format_hint">Imposta nuovo formato articolo</string>
+ <string name="site_settings_category_hint">Imposta nuova categoria articolo</string>
+ <string name="site_settings_location_hint">Aggiungi automaticamente i dati sulla posizione agli articoli</string>
+ <string name="site_settings_password_hint">Modifica la password</string>
+ <string name="site_settings_username_hint">Account utente attuale</string>
+ <string name="site_settings_language_hint">Lingua in cui questo blog è scritto prevalentemente</string>
+ <string name="site_settings_privacy_hint">Controlla chi può vedere questo sito</string>
+ <string name="site_settings_address_hint">La modifica dell\'indirizzo non è attualmente supportata</string>
+ <string name="site_settings_tagline_hint">Una breve descrizione o una frase accattivante per descrivere il tuo blog</string>
+ <string name="site_settings_title_hint">Spiega in poche parole l\'argomento del sito</string>
+ <string name="site_settings_whitelist_known_summary">Commenti da utenti conosciuti</string>
+ <string name="site_settings_whitelist_all_summary">Commenti da tutti gli utenti</string>
+ <string name="site_settings_threading_summary">%d livelli</string>
+ <string name="site_settings_privacy_private_summary">Privato</string>
+ <string name="site_settings_privacy_hidden_summary">Nascosto</string>
+ <string name="site_settings_delete_site_title">Elimina sito</string>
+ <string name="site_settings_privacy_public_summary">Pubblico</string>
+ <string name="site_settings_blacklist_title">Blacklist</string>
+ <string name="site_settings_moderation_hold_title">Sottoponi a moderazione</string>
+ <string name="site_settings_multiple_links_title">Link nei commenti</string>
+ <string name="site_settings_whitelist_title">Approva automaticamente</string>
+ <string name="site_settings_paging_title">Paginazione</string>
+ <string name="site_settings_threading_title">Flusso</string>
+ <string name="site_settings_sort_by_title">Ordina per</string>
+ <string name="site_settings_account_required_title">Gli utenti devono essere registrati</string>
+ <string name="site_settings_identity_required_title">Devono includere nome ed e-mail</string>
+ <string name="site_settings_receive_pingbacks_title">Ricevi pingback</string>
+ <string name="site_settings_send_pingbacks_title">Invia pingback</string>
+ <string name="site_settings_allow_comments_title">Consenti commenti</string>
+ <string name="site_settings_default_format_title">Formato predefinito</string>
+ <string name="site_settings_default_category_title">Categoria predefinita</string>
+ <string name="site_settings_location_title">Abilita posizione</string>
+ <string name="site_settings_address_title">Indirizzo</string>
+ <string name="site_settings_title_title">Titolo sito</string>
+ <string name="site_settings_tagline_title">Tagline</string>
+ <string name="site_settings_this_device_header">Questo dispositivo</string>
+ <string name="site_settings_discussion_new_posts_header">Impostazioni predefinite per nuovi articoli</string>
+ <string name="site_settings_account_header">Account</string>
+ <string name="site_settings_writing_header">Scrittura</string>
+ <string name="newest_first">Prima i più recenti</string>
+ <string name="site_settings_general_header">Generali</string>
+ <string name="discussion">Discussione</string>
+ <string name="privacy">Privacy</string>
+ <string name="related_posts">Articoli correlati</string>
+ <string name="comments">Commenti</string>
+ <string name="close_after">Chiudi dopo</string>
+ <string name="oldest_first">Prima i meno recenti</string>
+ <string name="media_error_no_permission_upload">Non disponi delle autorizzazioni per caricare contenuti multimediali sul sito</string>
+ <string name="never">Mai</string>
+ <string name="unknown">Sconosciuto</string>
+ <string name="reader_err_get_post_not_found">Questo articolo non esiste più</string>
+ <string name="reader_err_get_post_not_authorized">Non disponi del permesso per visualizzare questo articolo</string>
+ <string name="reader_err_get_post_generic">Impossibile recuperare questo articolo</string>
+ <string name="blog_name_no_spaced_allowed">L\'indirizzo del sito non può contenere spazi</string>
+ <string name="invalid_username_no_spaces">Il nome utente non può contenere spazi</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">I siti che segui non hanno pubblicato nulla recentemente</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Nessun articolo recente</string>
+ <string name="media_details_copy_url_toast">URL copiata negli appunti</string>
+ <string name="edit_media">Modifica media</string>
+ <string name="media_details_copy_url">Copia URL</string>
+ <string name="media_details_label_date_uploaded">Caricato</string>
+ <string name="media_details_label_date_added">Aggiunto</string>
+ <string name="selected_theme">Tema selezionato</string>
+ <string name="could_not_load_theme">Impossibile caricare il tema</string>
+ <string name="theme_activation_error">Qualche cosa è andato storto. Impossibile attivare il tema.</string>
+ <string name="theme_by_author_prompt_append">di %1$s</string>
+ <string name="theme_prompt">Grazie per aver scelto %1$s</string>
+ <string name="theme_try_and_customize">Prova e personalizza</string>
+ <string name="theme_view">Visualizza</string>
+ <string name="theme_details">Dettagli</string>
+ <string name="theme_support">Supporto</string>
+ <string name="theme_done">FATTO</string>
+ <string name="theme_manage_site">GESTISCI SITO</string>
+ <string name="title_activity_theme_support">Temi</string>
+ <string name="theme_activate">Attiva</string>
+ <string name="date_range_start_date">Data di inizio</string>
+ <string name="date_range_end_date">Data di fine</string>
+ <string name="current_theme">Tema corrente</string>
+ <string name="customize">Personalizza</string>
+ <string name="details">Dettagli</string>
+ <string name="support">Supporto</string>
+ <string name="active">Attivo</string>
+ <string name="stats_referrers_spam_generic_error">Qualche cosa è andato storto durante l\'operazione. Lo stato dello spam non è cambiato.</string>
+ <string name="stats_referrers_marking_not_spam">Contrassegna come non spam</string>
+ <string name="stats_referrers_unspam">Non spam</string>
+ <string name="stats_referrers_marking_spam">Contrassegna come spam</string>
+ <string name="theme_auth_error_authenticate">Impossibile recuperare il tema: autenticazione utente fallita</string>
+ <string name="post_published">Articolo pubblicato</string>
+ <string name="page_published">Pagina pubblicata</string>
+ <string name="post_updated">Articolo aggiornato</string>
+ <string name="page_updated">Pagina aggiornata</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Nessun tema trovato.</string>
+ <string name="media_file_name">Nome del file: %s</string>
+ <string name="media_uploaded_on">Aggiornato il: %s</string>
+ <string name="media_dimensions">Dimensioni: %s</string>
+ <string name="upload_queued">Accodato</string>
+ <string name="media_file_type">Tipo di file: %s</string>
+ <string name="reader_label_gap_marker">Carica altri articoli</string>
+ <string name="notifications_no_search_results">Nessun sito corrisponde a \'%s\'</string>
+ <string name="search_sites">Ricerca siti</string>
+ <string name="notifications_empty_view_reader">Visualizza nel Reader</string>
+ <string name="unread">Non letto</string>
+ <string name="notifications_empty_action_followers_likes">Ottieni notifiche: commenti ad articoli che hai letto.</string>
+ <string name="notifications_empty_action_comments">Unisciti ad una conversazione: commenti su articoli di blog che segui.</string>
+ <string name="notifications_empty_action_unread">Ravviva la conversazione: scrivi un nuovo articolo.</string>
+ <string name="notifications_empty_action_all">Sii attivo! Commenta articoli dei blog che segui.</string>
+ <string name="notifications_empty_likes">Nessun nuovo mi piace da mostrare... per ora.</string>
+ <string name="notifications_empty_followers">Nessun nuovo utente ti segue... per ora.</string>
+ <string name="notifications_empty_comments">Nessun nuovo commento... per ora.</string>
+ <string name="notifications_empty_unread">Nessun articolo non letto!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Accedi alla Statistiche nella app e quindi successivamente prova ad aggiungere il widget</string>
+ <string name="stats_widget_error_readd_widget">Rimuovi il widget e aggiungilo nuovamente</string>
+ <string name="stats_widget_error_no_visible_blog">Le statistiche non sono accessibili senza un blog visibile</string>
+ <string name="stats_widget_error_no_permissions">Il tuo account WordPress.com non può accedere alle Statistiche di questo blog</string>
+ <string name="stats_widget_error_no_account">Effettua il login su WordPress</string>
+ <string name="stats_widget_error_generic">Le Statistiche non possno essere caricate</string>
+ <string name="stats_widget_loading_data">Caricamento dati...</string>
+ <string name="stats_widget_name_for_blog">Statistiche di oggi per %1$s</string>
+ <string name="stats_widget_name">Statistiche di oggi di WordPress</string>
+ <string name="add_location_permission_required">Per aggiungere una località occorre disporre dei permessi appositi</string>
+ <string name="add_media_permission_required">Autorizzazioni richieste per aggiungere file multimediali</string>
+ <string name="access_media_permission_required">Autorizzazioni richieste per accedere a file multimediali</string>
+ <string name="stats_enable_rest_api_in_jetpack">Per visualizzare le tue statistiche, abilita il modulo JSON API di Jetpack.</string>
+ <string name="error_open_list_from_notification">Questo articolo o questa pagina sono state pubblicate su un altro sito</string>
+ <string name="reader_short_comment_count_multi">%s commenti</string>
+ <string name="reader_short_comment_count_one">1 Commento</string>
+ <string name="reader_label_submit_comment">SPEDISCI</string>
+ <string name="reader_hint_comment_on_post">Rispondi all’articolo…</string>
+ <string name="reader_discover_visit_blog">Visite %s</string>
+ <string name="reader_discover_attribution_blog">Pubblicato originariamente su %s</string>
+ <string name="reader_discover_attribution_author">Pubblicato originariamente da %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Pubblicato oginariamente da %1$s su %2$s</string>
+ <string name="reader_short_like_count_multi">%s Mi piace </string>
+ <string name="reader_short_like_count_one">1 Mi piace</string>
+ <string name="reader_label_follow_count">%,d ti seguono</string>
+ <string name="reader_short_like_count_none">Mi piace</string>
+ <string name="reader_menu_tags">Modifica tag e blog</string>
+ <string name="reader_title_post_detail">Reader articoli</string>
+ <string name="local_draft_explainer">Questo articolo è una bozza locale che non è stata pubblicata</string>
+ <string name="local_changes_explainer">Questo articolo contiene modifiche locali che non sono state pubblicate</string>
+ <string name="notifications_push_summary">Impostazioni per le notifiche visualizzate sul dispositivo.</string>
+ <string name="notifications_email_summary">Impostazioni per le notifiche inviate all\'e-mail associata al tuo account.</string>
+ <string name="notifications_tab_summary">Impostazioni per le notifiche visualizzate nella scheda Notifiche.</string>
+ <string name="notifications_disabled">Le notifiche della app sono state disattivate. Tocca qui per attivarle in Impostazioni.</string>
+ <string name="notification_types">Tipi di notifiche</string>
+ <string name="error_loading_notifications">Impossibile caricare le impostazioni delle notifiche</string>
+ <string name="replies_to_your_comments">Risposte ai tuoi commenti</string>
+ <string name="comment_likes">Mi piace dei commenti</string>
+ <string name="notifications_tab">Scheda Notifiche</string>
+ <string name="email">E-mail</string>
+ <string name="app_notifications">Notifiche della app</string>
+ <string name="notifications_comments_other_blogs">Commenti su altri siti</string>
+ <string name="notifications_wpcom_updates">Aggiornamenti di WordPress.com</string>
+ <string name="notifications_other">Altro</string>
+ <string name="notifications_account_emails">Email da WordPress.com</string>
+ <string name="notifications_account_emails_summary">Invieremo sempre e-mail importanti riguardanti il tuo account, ma potrai ricevere anche informazioni extra utili.</string>
+ <string name="notifications_sights_and_sounds">Immagini e suoni</string>
+ <string name="your_sites">I tuoi siti</string>
+ <string name="stats_insights_latest_post_trend">Sono passati %1$s dalla pubblicazione di %2$s. Ecco com\'è andato finora l\'articolo...</string>
+ <string name="stats_insights_latest_post_summary">Riassunto ultimo articolo</string>
+ <string name="button_revert">Riconverti</string>
+ <string name="days_ago">%d giorni fa</string>
+ <string name="yesterday">Ieri</string>
+ <string name="connectionbar_no_connection">Nessuna connessione</string>
+ <string name="page_trashed">Pagina inviata al cestino</string>
+ <string name="post_deleted">Articolo cancellato</string>
+ <string name="post_trashed">Articolo inviato al cestino</string>
+ <string name="stats_no_activity_this_period">Nessuna attivata in questo periodo</string>
+ <string name="trashed">Cestinato</string>
+ <string name="button_back">Indietro</string>
+ <string name="page_deleted">Pagina cancellata</string>
+ <string name="button_stats">Statistiche</string>
+ <string name="button_trash">Cestino</string>
+ <string name="button_preview">Anteprima</string>
+ <string name="button_view">Visualizza</string>
+ <string name="button_edit">Modifica</string>
+ <string name="button_publish">Pubblica</string>
+ <string name="my_site_no_sites_view_subtitle">Vuoi aggiungerne uno?</string>
+ <string name="my_site_no_sites_view_title">No hai ancora nessun sito WordPress</string>
+ <string name="my_site_no_sites_view_drake">Illustrazione</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Non sei autorizzato ad accedere a questo blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">impossibile trovare questo blog</string>
+ <string name="undo">Annulla</string>
+ <string name="tabbar_accessibility_label_my_site">Il mio sito</string>
+ <string name="tabbar_accessibility_label_me">Io</string>
+ <string name="editor_toast_changes_saved">Cambiamenti salvati</string>
+ <string name="passcodelock_prompt_message">Inserisci il tuo PIN</string>
+ <string name="push_auth_expired">La richiesta è scaduta. Accedi a WordPress.com per riprovare.</string>
+ <string name="stats_insights_best_ever">Le migliore visualizzazioni di sempre</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% di visite</string>
+ <string name="ignore">Ignora</string>
+ <string name="stats_insights_most_popular_hour">Ora più popolare.</string>
+ <string name="stats_insights_most_popular_day">Giorno più popolare.</string>
+ <string name="stats_insights_popular">Giorno e ora più popolari.</string>
+ <string name="stats_insights_today">Statistiche odierne.</string>
+ <string name="stats_insights_all_time">Articoli, visualizzazioni e visitatori dall\'inizio</string>
+ <string name="stats_insights">Approfondimenti</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Per vedere le tue statistiche, accedi con l\'account WordPress.com che hai usato per connettere Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Stai cercando le tue altre statististiche recenti? Le abbiamo spostaste nella pagina insights.</string>
+ <string name="me_disconnect_from_wordpress_com">Disconnettiti da WordPress.com</string>
+ <string name="me_btn_login_logout">Login/Logout</string>
+ <string name="me_connect_to_wordpress_com">Connettiti a WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">"%s" non è stato nascosto perchè è il sito corrente</string>
+ <string name="me_btn_support">Aiuto e supporto</string>
+ <string name="account_settings">Impostazioni account</string>
+ <string name="site_picker_create_dotcom">Crea un sito su WordPress.com</string>
+ <string name="site_picker_edit_visibility">Mostra/nascondi siti</string>
+ <string name="site_picker_add_self_hosted">Aggiungi un sito self-hosted</string>
+ <string name="site_picker_add_site">Aggiungi sito</string>
+ <string name="my_site_btn_view_site">Visualizza il sito</string>
+ <string name="my_site_btn_switch_site">Cambia sito</string>
+ <string name="site_picker_title">Seleziona un sito</string>
+ <string name="my_site_btn_view_admin">Visualizza l\'amministrazione</string>
+ <string name="my_site_header_look_and_feel">Aspetto</string>
+ <string name="my_site_header_publish">Pubblica</string>
+ <string name="my_site_btn_site_settings">Impostazioni</string>
+ <string name="my_site_btn_blog_posts">Articoli blog</string>
+ <string name="reader_label_new_posts_subtitle">Tocca per visualizzarli</string>
+ <string name="my_site_header_configuration">Configurazione</string>
+ <string name="notifications_account_required">Collegati a WordPress.com per le notifiche</string>
+ <string name="stats_unknown_author">Autore sconosciuto</string>
+ <string name="signout">Disconnettiti</string>
+ <string name="image_added">Immagine aggiunta</string>
+ <string name="sign_out_wpcom_confirm">Disconnettendo il tuo account tutti i dati @%s WordPress.com da questa periferica, comprese le bozze locali e le modifiche locali.</string>
+ <string name="hide">Nascondi</string>
+ <string name="select_all">Seleziona tutto</string>
+ <string name="deselect_all">Deseleziona tutto</string>
+ <string name="show">Mostra</string>
+ <string name="select_from_new_picker">Scegli più voci con il nuovo selezionatore</string>
+ <string name="loading_blog_images">Recupero immagini</string>
+ <string name="loading_blog_videos">Recupero video</string>
+ <string name="no_media_sources">Impossibile recuperare elementi media</string>
+ <string name="error_loading_blog_videos">Impossibile recuperare i video</string>
+ <string name="error_loading_images">Errore nel caricamento delle immagini</string>
+ <string name="error_loading_videos">Errore nel caricamento dei video</string>
+ <string name="no_device_videos">Nessun video</string>
+ <string name="no_blog_images">Nessuna immagine</string>
+ <string name="no_blog_videos">Nessun video</string>
+ <string name="no_device_images">Nessuna immagine</string>
+ <string name="error_loading_blog_images">Impossibile recuperare le immagini</string>
+ <string name="stats_generic_error">Le statistiche richieste non possono venir caricate</string>
+ <string name="no_media">Nessun elemento media</string>
+ <string name="loading_videos">Caricamento video</string>
+ <string name="loading_images">Caricamento immagini</string>
+ <string name="tab_title_device_videos">Video dispositivo</string>
+ <string name="device">Dispositivo</string>
+ <string name="tab_title_device_images">Immagini dispositivo</string>
+ <string name="language">Lingua</string>
+ <string name="add_to_post">Aggiungi ad articolo</string>
+ <string name="media_picker_title">Seleziona media</string>
+ <string name="take_photo">Fai una foto</string>
+ <string name="take_video">Fai un video</string>
+ <string name="tab_title_site_images">Immagini sito</string>
+ <string name="tab_title_site_videos">Video sito</string>
+ <string name="error_publish_no_network">Impossibile pubblicare perchè non c\'è una connessione. Salva come bozza.</string>
+ <string name="editor_toast_invalid_path">Percorso file non valido</string>
+ <string name="verification_code">Codice di verifica</string>
+ <string name="invalid_verification_code">Codice di verifica non valido</string>
+ <string name="verify">Verifica</string>
+ <string name="two_step_footer_label">Inserisci il codice dalla tua app di autenticazione.</string>
+ <string name="two_step_footer_button">Invia codice come messaggio di testo</string>
+ <string name="two_step_sms_sent">Verifica i messaggi di testo per il codice di verifica.</string>
+ <string name="sign_in_jetpack">Autentificati al tuo account WordPress.com per connetterti a Jetpack.</string>
+ <string name="auth_required">Autentificati nuovamente per continuare.</string>
+ <string name="media_details_label_file_name">Nome file</string>
+ <string name="media_details_label_file_type">Tipo file</string>
+ <string name="media_fetching">Recupero file multimediali in corso…</string>
+ <string name="posts_fetching">Recupero degli articoli in corso...</string>
+ <string name="toast_err_post_uploading">Impossibile aprire l\'articolo durante il suo caricamento</string>
+ <string name="pages_fetching">Recupero pagine in corso…</string>
+ <string name="comments_fetching">Recupero commenti in corso...</string>
+ <string name="stats_empty_search_terms_desc">Ottieni maggiori informazioni sul tuo traffico di ricerca scoprendo i termini utilizzati dai visitatori per trovare il tuo sito.</string>
+ <string name="stats_followers_total_wpcom_paged">Mostra %1$d - %2$d di %3$s follower WordPress.com</string>
+ <string name="stats_followers_total_email_paged">Mostra %1$d - %2$d di %3$s follower e-mail</string>
+ <string name="error_notification_open">Impossibile aprire la notifica</string>
+ <string name="publisher">Editore:</string>
+ <string name="stats_view_authors">Autori</string>
+ <string name="stats_view_search_terms">Termini di ricerca</string>
+ <string name="stats_entry_search_terms">Termine di ricerca</string>
+ <string name="stats_empty_search_terms">Nessun termine di ricerca registrato</string>
+ <string name="stats_search_terms_unknown_search_terms">Termini di ricerca sconosciuti</string>
+ <string name="reader_empty_posts_request_failed">Impossibile recuperare gli articoli</string>
+ <string name="stats_months_and_years">Mesi ed anni</string>
+ <string name="stats_recent_weeks">Settimane recenti</string>
+ <string name="error_copy_to_clipboard">Si è verificato un errore durante la copia del testo negli appunti</string>
+ <string name="stats_total">Totale</string>
+ <string name="stats_overall">Tutto</string>
+ <string name="stats_period">Periodo</string>
+ <string name="logs_copied_to_clipboard">I log dell\'applicazione sono stati copiati negli appunti</string>
+ <string name="reader_label_new_posts">Nuovi articoli</string>
+ <string name="reader_empty_posts_in_blog">Il blog è vuoto</string>
+ <string name="stats_average_per_day">Media giornaliera</string>
+ <string name="post_uploading">Caricamento</string>
+ <string name="reader_page_recommended_blogs">Siti che potrebbero piacerti</string>
+ <string name="stats_comments_total_comments_followers">Totale articoli con commento dei follower: %1$s</string>
+ <string name="stats_likes">Mi piace</string>
+ <string name="stats_views">Visite</string>
+ <string name="stats_view_publicize">Pubblicizza</string>
+ <string name="stats_view_followers">Follower</string>
+ <string name="stats_view_top_posts_and_pages">Articoli e pagine</string>
+ <string name="stats_entry_followers">Follower</string>
+ <string name="stats_totals_publicize">Follower</string>
+ <string name="stats_empty_geoviews_desc">Esplora l\'elenco per scoprire quali Paesi e quali aree geografiche generano maggiore traffico verso il tuo sito.</string>
+ <string name="stats_empty_geoviews">Nessun Paese registrato</string>
+ <string name="stats_empty_top_posts_desc">Scopri quali sono i tuoi contenuti più visualizzati e controlla l\'andamento nel tempo di ogni articolo o pagina.</string>
+ <string name="stats_empty_top_posts_title">Nessuna visita su articoli o pagine</string>
+ <string name="stats_empty_referrers_desc">Scopri di più sulla visibilità del tuo sito individuando i siti web e i motori di ricerca che indirizzano la maggior parte del traffico sul tuo sito</string>
+ <string name="stats_empty_clicks_desc">Se i tuoi contenuti includono link ad altri siti, scoprirai su quali link i visitatori fanno clic più spesso.</string>
+ <string name="stats_empty_tags_and_categories">Articoli senza tag o pagine non visualizzate</string>
+ <string name="stats_empty_tags_and_categories_desc">Ottieni una panoramica degli argomenti più popolari sul tuo sito, in base ai tuoi articoli migliori della settimana precedente.</string>
+ <string name="stats_empty_top_authors_desc">Traccia le visualizzazioni degli articoli di ogni collaboratore e scopri i contenuti più popolari per ogni autore.</string>
+ <string name="stats_empty_video">Nessun video riprodotto</string>
+ <string name="stats_empty_comments_desc">Se sul tuo sito è possibile lasciare dei commenti, traccia i tuoi commentatori principali e scopri quali contenuti hanno generato le conversazioni più attive in base ai 1.000 commenti più recenti.</string>
+ <string name="stats_empty_publicize">Nessun follower di Pubblicizza registrato</string>
+ <string name="stats_empty_publicize_desc">Tieni traccia dei tuoi follower dai vari servizi di social networking utilizzando Pubblicizza.</string>
+ <string name="stats_empty_followers">Nessun follower</string>
+ <string name="stats_comments_by_authors">Per autori</string>
+ <string name="stats_empty_followers_desc">Tieni traccia del numero totale di follower e da quanto tempo ognuno di essi segue il tuo sito web.</string>
+ <string name="stats_followers_email_selector">E-mail</string>
+ <string name="stats_followers_total_wpcom">Follower WordPress.com totali: %1$s</string>
+ <string name="stats_followers_total_email">Follower e-mail totali: %1$s</string>
+ <string name="stats_followers_wpcom_selector">wordpress.com</string>
+ <string name="stats_view">Vista</string>
+ <string name="stats_followers_seconds_ago">pochi secondi fa</string>
+ <string name="stats_visitors">Visitatori</string>
+ <string name="stats_pagination_label">Pagina %1$s di %2$s</string>
+ <string name="stats_timeframe_years">Anni</string>
+ <string name="stats_view_countries">Paesi</string>
+ <string name="stats_view_videos">Video</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_entry_publicize">Servizio</string>
+ <string name="stats_entry_top_commenter">Autore</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_totals_followers">Da</string>
+ <string name="stats_empty_referrers_title">Nessun referrer registrato</string>
+ <string name="stats_empty_clicks_title">Nessun clic registrato</string>
+ <string name="stats_empty_video_desc">Se hai caricato video tramite VideoPress, scopri quante volte vengono visualizzati.</string>
+ <string name="stats_comments_by_posts_and_pages">Per articoli e pagine</string>
+ <string name="stats_followers_days">%1$d giorni</string>
+ <string name="stats_followers_a_minute_ago">un minuto fa</string>
+ <string name="stats_followers_a_day">Un giorno</string>
+ <string name="stats_followers_hours">%1$d ore</string>
+ <string name="stats_followers_minutes">%1$d minuti</string>
+ <string name="stats_followers_an_hour_ago">un\'ora fa</string>
+ <string name="stats_followers_a_month">Un mese</string>
+ <string name="stats_followers_years">%1$d anni</string>
+ <string name="stats_followers_a_year">Un anno</string>
+ <string name="stats_followers_months">%1$d mesi</string>
+ <string name="stats_view_all">Visualizza tutte</string>
+ <string name="stats_other_recent_stats_label">Altre statistiche recenti</string>
+ <string name="stats_for">Statistiche per %s</string>
+ <string name="themes_fetching">Recupero temi…</string>
+ <string name="ssl_certificate_details">Dettagli</string>
+ <string name="sure_to_remove_account">Rimuovere questo sito?</string>
+ <string name="delete_sure_post">Cancella questo articolo</string>
+ <string name="delete_sure">Cancella questa bozza</string>
+ <string name="delete_sure_page">Cancella questa pagina</string>
+ <string name="confirm_delete_multi_media">Cancellare gli elementi selezionati?</string>
+ <string name="confirm_delete_media">Cancellare l\'elemento selezionato?</string>
+ <string name="cab_selected">%d selezionati</string>
+ <string name="media_gallery_date_range">Visualizzazione elementi media da %1$s a %2$s</string>
+ <string name="faq_button">FAQ</string>
+ <string name="reader_empty_comments">Ancora nessun commento</string>
+ <string name="reader_label_comment_count_multi">%,d commenti</string>
+ <string name="reader_label_like">Like</string>
+ <string name="reader_empty_posts_liked">Non hai ancora fatto un like ad un articolo</string>
+ <string name="browse_our_faq_button">Scorri le nostre FAQ</string>
+ <string name="nux_help_description">Visita il nostro centro assistenza per ottenere le risposte alle domande più frequenti o visita il forum per porne di nuove</string>
+ <string name="agree_terms_of_service">Creando un account accetti le affascinanti %1$sConizioni di Servizio%2$s</string>
+ <string name="create_new_blog_wpcom">Crea un blog su WordPress.com</string>
+ <string name="new_blog_wpcom_created">Il blog su WordPress.com è stato creato!</string>
+ <string name="reader_empty_posts_in_tag">Nessun articolo con questo tag</string>
+ <string name="reader_label_view_original">Visualizza l\'articolo originale</string>
+ <string name="reader_label_comment_count_single">Un commento</string>
+ <string name="reader_label_comments_closed">I commenti sono chiusi</string>
+ <string name="reader_label_comments_on">Commenti su</string>
+ <string name="reader_title_photo_viewer">%1$d di %2$d</string>
+ <string name="error_publish_empty_post">Non è possibile pubblicare un articolo vuoto</string>
+ <string name="error_refresh_unauthorized_posts">Non disponi del permesso di visualizzare o modificare gli articoli</string>
+ <string name="error_refresh_unauthorized_pages">Non disponi del permesso di visualizzare o modificare le pagine</string>
+ <string name="error_refresh_unauthorized_comments">Non disponi del permesso di visualizzare o modificare i commenti</string>
+ <string name="older_month">Più vecchio di un mese</string>
+ <string name="more">Altro</string>
+ <string name="older_two_days">Più vecchio di 2 giorni</string>
+ <string name="older_last_week">Più vecchio di una settimana</string>
+ <string name="stats_no_blog">Non è possibile caricare le statistiche per il blog indicato</string>
+ <string name="select_a_blog">Seleziona un sito WordPress</string>
+ <string name="sending_content">Caricamento del contenuto %s</string>
+ <string name="uploading_total">Caricamento %1$d di %2$d</string>
+ <string name="mnu_comment_liked">Like</string>
+ <string name="comment">Commento</string>
+ <string name="comment_trashed">Commento cestinato</string>
+ <string name="posts_empty_list">Ancora nessun articolo. Perché non crearne uno?</string>
+ <string name="comment_reply_to_user">Replica a %s</string>
+ <string name="pages_empty_list">Ancora nessuna pagina. Perché non crearne una?</string>
+ <string name="media_empty_list_custom_date">Nessun elemento media in questo intervallo di tempo</string>
+ <string name="posting_post">Pubblicazione di "%s"</string>
+ <string name="signing_out">Scollegamento...</string>
+ <string name="reader_empty_followed_blogs_title">Non stai ancora seguendo alcun sito</string>
+ <string name="reader_toast_err_generic">Impossibile eseguire questa azione</string>
+ <string name="reader_toast_err_block_blog">Impossibile bloccare questo blog</string>
+ <string name="reader_toast_blog_blocked">Gli articoli di questo blog non appariranno più</string>
+ <string name="reader_menu_block_blog">Blocca questo blog</string>
+ <string name="contact_us">Contattaci</string>
+ <string name="hs__conversation_detail_error">Descrivi il problema che si è presentato</string>
+ <string name="hs__new_conversation_header">Chat di supporto</string>
+ <string name="hs__conversation_header">Chat di supporto</string>
+ <string name="hs__username_blank_error">Inserisce un nome valido</string>
+ <string name="hs__invalid_email_error">Inserisci un email valida</string>
+ <string name="add_location">Aggiungi una località</string>
+ <string name="current_location">Località corrente</string>
+ <string name="search_location">Cerca</string>
+ <string name="edit_location">Modifica</string>
+ <string name="search_current_location">Rintraccia</string>
+ <string name="preference_send_usage_stats">Invia le statistiche</string>
+ <string name="preference_send_usage_stats_summary">Invia automaticamente le statistiche di utilizzo per aiutarci a migliorare WordPress per Android</string>
+ <string name="update_verb">Aggiorna</string>
+ <string name="schedule_verb">Pianifica</string>
+ <string name="reader_title_subs">Tag e blog</string>
+ <string name="reader_page_followed_tags">Tag seguiti</string>
+ <string name="reader_label_followed_blog">Blog seguito</string>
+ <string name="reader_toast_err_get_blog_info">Impossibile mostrare questo blog</string>
+ <string name="reader_toast_err_already_follow_blog">Segui già questo blog</string>
+ <string name="reader_toast_err_follow_blog">Impossibile seguire questo blog</string>
+ <string name="reader_toast_err_unfollow_blog">Impossibile smettere di seguire questo blog</string>
+ <string name="reader_empty_recommended_blogs">Nessun blog consigliato</string>
+ <string name="reader_label_tag_preview">Articolo con tag %s</string>
+ <string name="reader_title_blog_preview">Reader blog</string>
+ <string name="reader_title_tag_preview">Reader tag</string>
+ <string name="reader_page_followed_blogs">Siti che segui</string>
+ <string name="reader_hint_add_tag_or_url">Inserisci una URL o untag da seguire</string>
+ <string name="saving">Salvataggio in corso...</string>
+ <string name="media_empty_list">Nessun media</string>
+ <string name="ptr_tip_message">Suggerimento: scorri il dito verso il basso per aggiornare</string>
+ <string name="help">Aiuto</string>
+ <string name="forgot_password">Password dimenticata?</string>
+ <string name="forums">Forum</string>
+ <string name="help_center">Centro assistenza</string>
+ <string name="ssl_certificate_error">Certificato SSL non valido</string>
+ <string name="ssl_certificate_ask_trust">Se in genere ti connetti al sito senza problemi, questo errore potrebbe significare che qualcuno sta tentando di impersonare il sito, pertanto non dovresti continuare. Desideri comunque confermare l\'affidabilità del certificato?</string>
+ <string name="could_not_remove_account">Impossibile rimuovere il sito</string>
+ <string name="out_of_memory">Memoria del dispositivo esaurita</string>
+ <string name="no_network_message">Nessuna rete disponibile</string>
+ <string name="gallery_error">Impossibile recuperare il file</string>
+ <string name="blog_not_found">Si è verificato un errore durante l\'accesso a questo blog</string>
+ <string name="wait_until_upload_completes">Attendi la fine del caricamento</string>
+ <string name="theme_fetch_failed">Impossibile recuperare i temi</string>
+ <string name="theme_set_failed">Impossibile impostare il tema</string>
+ <string name="theme_auth_error_message">Assicurati di disporre dei privilegi per impostare i temi</string>
+ <string name="comments_empty_list">Nessun commento</string>
+ <string name="mnu_comment_unspam">Non spam</string>
+ <string name="no_site_error">Impossibile connettersi al sito WordPress</string>
+ <string name="adding_cat_failed">Impossibile aggiungere la categoria</string>
+ <string name="adding_cat_success">Categoria aggiunta correttamente</string>
+ <string name="cat_name_required">Il campo Nome categoria è obbligatorio</string>
+ <string name="category_automatically_renamed">Il valore del campo Nome categoria %1$s non è valido. È stato rinominato in %2$s.</string>
+ <string name="no_account">Impossibile trovare un account WordPress. Aggiungi un account e riprova</string>
+ <string name="sdcard_message">Occorre una scheda SD per caricare i file</string>
+ <string name="stats_empty_comments">Ancora nessun commento</string>
+ <string name="stats_bar_graph_empty">Nessuna statistica disponibile</string>
+ <string name="reply_failed">Impossibile rispondere</string>
+ <string name="notifications_empty_list">Nessuna notifica</string>
+ <string name="error_delete_post">Si è verificato un errore durante l\'eliminazione di %s</string>
+ <string name="error_refresh_posts">Impossibile aggiornare gli articoli al momento</string>
+ <string name="error_refresh_pages">Impossibile aggiornare le pagine al momento</string>
+ <string name="error_refresh_notifications">Impossibile aggiornare le notifiche al momento</string>
+ <string name="error_refresh_comments">Impossibile aggiornare i commenti al momento</string>
+ <string name="error_refresh_stats">Impossibile aggiornare le statistiche al momento</string>
+ <string name="error_generic">Si è verificato un errore</string>
+ <string name="error_moderate_comment">Si è verificato un errore durante la moderazione</string>
+ <string name="error_edit_comment">Si è verificato un errore durante la modifica del commento</string>
+ <string name="error_upload">Si è verificato un errore durante il caricamento di %s</string>
+ <string name="error_load_comment">Impossibile caricare il commento</string>
+ <string name="error_downloading_image">Errore durante il download dell\'immagine</string>
+ <string name="passcode_wrong_passcode">PIN errato</string>
+ <string name="invalid_email_message">Il tuo indirizzo e-mail non è valido</string>
+ <string name="invalid_password_message">La password deve contenere almeno 4 caratteri</string>
+ <string name="invalid_username_too_short">Il nome utente deve contenere più di 4 caratteri</string>
+ <string name="invalid_username_too_long">Il nome utente può contenere fino a 61 caratteri</string>
+ <string name="username_only_lowercase_letters_and_numbers">Il nome utente può contenere solo lettere minuscole (a-z) e numeri</string>
+ <string name="username_required">Inserisci un nome utente</string>
+ <string name="username_not_allowed">Nome utente non consentito</string>
+ <string name="username_must_be_at_least_four_characters">Il nome utente deve contenere almeno 4 caratteri</string>
+ <string name="username_contains_invalid_characters">Il nome utente non può includere il carattere "_"</string>
+ <string name="username_must_include_letters">Il nome utente deve includere almeno 1 lettera (a-z)</string>
+ <string name="email_invalid">Inserisci un indirizzo e-mail valido</string>
+ <string name="email_not_allowed">Indirizzo e-mail non consentito</string>
+ <string name="username_exists">Il nome utente esiste già</string>
+ <string name="email_exists">L\'indirizzo e-mail è già in uso</string>
+ <string name="username_reserved_but_may_be_available">Il nome utente è attualmente riservato, ma potrebbe essere disponibile tra un paio di giorni</string>
+ <string name="blog_name_required">Inserisci un indirizzo del sito</string>
+ <string name="blog_name_not_allowed">Indirizzo del sito non consentito</string>
+ <string name="blog_name_must_be_at_least_four_characters">Il sito deve contenere almeno 4 caratteri</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Il sito deve contenere non più di 64 caratteri</string>
+ <string name="blog_name_contains_invalid_characters">L\'indirizzo del sito non può includere il carattere "_"</string>
+ <string name="blog_name_cant_be_used">Non puoi utilizzare questo indirizzo del sito</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">L\'indirizzo del sito può contenere solo lettere minuscole (a-z) e numeri</string>
+ <string name="blog_name_exists">Il sito esiste già</string>
+ <string name="blog_name_reserved">Il sito è riservato</string>
+ <string name="blog_name_reserved_but_may_be_available">Il sito è attualmente riservato, ma potrebbe essere disponibile tra un paio di giorni</string>
+ <string name="username_or_password_incorrect">Il nome utente o la password immessi non sono corretti</string>
+ <string name="nux_cannot_log_in">Impossibile effettuare l\'accesso</string>
+ <string name="invalid_url_message">Verifica la validità dell\'URL inserita</string>
+ <string name="blog_removed_successfully">Sito rimosso correttamente</string>
+ <string name="remove_account">Rimuovi sito</string>
+ <string name="xmlrpc_error">Impossibile connettersi. Inserisci il percorso completo a xmlrpc.php del tuo sito e riprova.</string>
+ <string name="select_categories">Seleziona le categorie</string>
+ <string name="account_details">Dati dell\'account</string>
+ <string name="edit_post">Modifica articolo</string>
+ <string name="add_comment">Aggiungi commento</string>
+ <string name="connection_error">Errore di connessione</string>
+ <string name="cancel_edit">Annulla la modifica</string>
+ <string name="scaled_image_error">Inserisci un valore della larghezza in scala valido</string>
+ <string name="post_not_found">Si è verificato un errore durante il caricamento dell\'articolo. Aggiorna gli articoli e riprova.</string>
+ <string name="learn_more">Ulteriori informazioni</string>
+ <string name="media_gallery_settings_title">Impostazioni della galleria</string>
+ <string name="media_gallery_image_order">Ordine immagini</string>
+ <string name="media_gallery_num_columns">Numero di colonne</string>
+ <string name="media_gallery_type_thumbnail_grid">Griglia miniatura</string>
+ <string name="media_gallery_edit">Modifica galleria</string>
+ <string name="media_error_no_permission">Non disponi dell\'autorizzazione a visualizzare la libreria multimediale</string>
+ <string name="cannot_delete_multi_media_items">Impossibile eliminare il file al momento. Riprova in seguito.</string>
+ <string name="themes_live_preview">Anteprima live</string>
+ <string name="theme_current_theme">Tema corrente</string>
+ <string name="theme_premium_theme">Tema Premium</string>
+ <string name="link_enter_url_text">Testo link (opzionale)</string>
+ <string name="create_a_link">Crea un link</string>
+ <string name="page_settings">Impostazioni pagina</string>
+ <string name="local_draft">Bozza locale</string>
+ <string name="upload_failed">Caricamento non riuscito</string>
+ <string name="horizontal_alignment">Allineamento orizzontale</string>
+ <string name="file_not_found">Impossibile trovare il file da caricare. È stato eliminato o spostato?</string>
+ <string name="post_settings">Impostazioni articolo</string>
+ <string name="delete_post">Elimina articolo</string>
+ <string name="delete_page">Elimina pagina</string>
+ <string name="comment_status_approved">Approvato</string>
+ <string name="comment_status_unapproved">In attesa</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Eliminato nel cestino</string>
+ <string name="edit_comment">Modifica commento</string>
+ <string name="mnu_comment_approve">Approva</string>
+ <string name="mnu_comment_unapprove">Rimuovi approvazione</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Cestina</string>
+ <string name="dlg_approving_comments">Approvazione in corso</string>
+ <string name="dlg_unapproving_comments">Rimozione dell\'approvazione in corso</string>
+ <string name="dlg_spamming_comments">Contrassegno come spam in corso</string>
+ <string name="dlg_trashing_comments">Invio al cestino in corso</string>
+ <string name="dlg_confirm_trash_comments">Inviare al cestino?</string>
+ <string name="trash_yes">Cestina</string>
+ <string name="trash_no">Non cestinare</string>
+ <string name="trash">Cestina</string>
+ <string name="author_name">Nome autore</string>
+ <string name="author_email">E-mail autore</string>
+ <string name="author_url">URL autore</string>
+ <string name="hint_comment_content">Commento</string>
+ <string name="saving_changes">Salvataggio delle modifiche in corso</string>
+ <string name="sure_to_cancel_edit_comment">Annullare la modifica a questo commento?</string>
+ <string name="content_required">Il commento è richiesto</string>
+ <string name="toast_comment_unedited">Il commento non è stato modificato</string>
+ <string name="delete_draft">Elimina bozza</string>
+ <string name="preview_page">Anteprima pagina</string>
+ <string name="preview_post">Anteprima articolo</string>
+ <string name="comment_added">Commento aggiunto correttamente</string>
+ <string name="post_not_published">Articolo non pubblicato</string>
+ <string name="page_not_published">Pagina non pubblicata</string>
+ <string name="view_in_browser">Visualizza nel browser</string>
+ <string name="add_new_category">Aggiungi nuova categoria</string>
+ <string name="category_name">Nome categoria</string>
+ <string name="category_slug">Abbreviazione categoria (opzionale)</string>
+ <string name="category_desc">Descrizione categoria (opzionale)</string>
+ <string name="category_parent">Categoria padre (opzionale)</string>
+ <string name="share_action_post">Nuovo articolo</string>
+ <string name="share_action_media">Libreria multimediale</string>
+ <string name="file_error_create">Impossibile creare un file temporaneo per il caricamento. Assicurati che vi sia spazio libero sufficiente sul dispositivo.</string>
+ <string name="location_not_found">Posizione non nota</string>
+ <string name="open_source_licenses">Licenze Open Source</string>
+ <string name="pending_review">In attesa di revisione</string>
+ <string name="http_credentials">Credenziali HTTP (opzionale)</string>
+ <string name="http_authorization_required">Autorizzazione richiesta</string>
+ <string name="post_format">Formato articolo</string>
+ <string name="new_post">Nuovo articolo</string>
+ <string name="new_media">Nuovo supporto</string>
+ <string name="view_site">Visualizza sito</string>
+ <string name="privacy_policy">Informativa sulla privacy</string>
+ <string name="local_changes">Modifiche locali</string>
+ <string name="image_settings">Impostazioni immagini</string>
+ <string name="add_account_blog_url">Indirizzo blog</string>
+ <string name="wordpress_blog">Blog di WordPress</string>
+ <string name="error_blog_hidden">Questo blog è nascosto e non può essere caricato. Abilitalo nuovamente nelle impostazioni e riprova.</string>
+ <string name="fatal_db_error">Si è verificato un errore durante la creazione del database delle app. Prova a reinstallare l\'app.</string>
+ <string name="jetpack_message_not_admin">Il plugin Jetpack è richiesto per le statistiche. Contatta l\'amministratore del sito.</string>
+ <string name="reader_title_applog">Registro applicazione</string>
+ <string name="reader_share_link">Condividi il link</string>
+ <string name="reader_toast_err_add_tag">Impossibile aggiungere questo tag</string>
+ <string name="reader_toast_err_remove_tag">Impossibile rimuovere questo tag</string>
+ <string name="required_field">Campo obbligatorio</string>
+ <string name="email_hint">Indirizzo e-mail</string>
+ <string name="site_address">Indirizzo del tuo sito (URL)</string>
+ <string name="email_cant_be_used_to_signup">Non puoi utilizzare questo indirizzo e-mail per effettuare l\'accesso. Si sono verificati dei problemi con questi indirizzi, poiché bloccano i nostri messaggi. Usa un altro provider del servizio di posta elettronica.</string>
+ <string name="email_reserved">Questo indirizzo e-mail è già stato utilizzato. Controlla l\'e-mail di attivazione che ti abbiamo inviato. Se non esegui l\'attivazione, puoi riprovare tra alcuni giorni.</string>
+ <string name="blog_name_must_include_letters">L\'indirizzo del sito deve includere almeno 1 lettera (a-z)</string>
+ <string name="blog_name_invalid">Indirizzo del sito non valido</string>
+ <string name="blog_title_invalid">Titolo del sito non valido</string>
+ <string name="notifications_empty_all">Nessuna notifica... per ora.</string>
+ <string name="invalid_site_url_message">Controlla che l\'URL del sito sia valido</string>
+ <string name="deleting_page">Eliminando la pagina</string>
+ <string name="deleting_post">Eliminando l\'articolo</string>
+ <string name="share_url_post">Condividi l\'articolo</string>
+ <string name="share_url_page">Condividi la pagina</string>
+ <string name="share_link">Condividi il link</string>
+ <string name="creating_your_account">Creazione dell\'account in corso</string>
+ <string name="creating_your_site">Creazione del sito in corso</string>
+ <string name="reader_empty_posts_in_tag_updating">Recupero post in corso...</string>
+ <string name="error_refresh_media">Si è verificato un errore durante l\'aggiornamento della libreria multimediale. Ritenta in un secondo momento.</string>
+ <string name="reader_likes_you_and_multi">Piace a te e ad altri %,d</string>
+ <string name="reader_likes_multi">Piace a %,d persone</string>
+ <string name="reader_toast_err_get_comment">Impossibile recuperare questo commento</string>
+ <string name="reader_label_reply">Rispondi</string>
+ <string name="video">Video</string>
+ <string name="download">Download file multimediale in corso</string>
+ <string name="comment_spammed">Commento contrassegnato come spam</string>
+ <string name="cant_share_no_visible_blog">Non è possibile condividere su WordPress senza un blog visibile</string>
+ <string name="select_time">Seleziona orario</string>
+ <string name="reader_likes_you_and_one">Piace a te e a un\'altra persona</string>
+ <string name="select_date">Seleziona data</string>
+ <string name="pick_photo">Seleziona foto</string>
+ <string name="account_two_step_auth_enabled">Per questo account è attiva l\'autenticazione in due fasi. Visita le tue impostazioni di sicurezza in WordPress.com e genera una password specifica per l\'applicazione.</string>
+ <string name="pick_video">Seleziona video</string>
+ <string name="reader_toast_err_get_post">Impossibile recuperare questo post</string>
+ <string name="validating_user_data">Convalida dati utente in corso</string>
+ <string name="validating_site_data">Convalida dati sito in corso</string>
+ <string name="reader_empty_followed_blogs_description">Ma non preoccuparti, fai un tap sull\'icona in alto a destra per iniziare ad esplorare!</string>
+ <string name="password_invalid">Devi impostare una password più sicura. Usa 7 o più caratteri sia minuscoli che maiuscoli, numeri o caratteri speciali.</string>
+ <string name="nux_tap_continue">Continua</string>
+ <string name="nux_welcome_create_account">Crea account</string>
+ <string name="nux_add_selfhosted_blog">Aggiungi sito self-hosted</string>
+ <string name="nux_oops_not_selfhosted_blog">Accedi a WordPress.com</string>
+ <string name="signing_in">Collegamento...</string>
+ <string name="media_add_popup_title">Aggiungi alla libreria multimediale</string>
+ <string name="media_add_new_media_gallery">Crea una galleria</string>
+ <string name="empty_list_default">Questo elenco è vuoto</string>
+ <string name="select_from_media_library">Seleziona dalla libreria multimediale</string>
+ <string name="jetpack_message">Il plugin Jetpack è necessario per accedere alle statistiche. Vuoi installare Jetpack?</string>
+ <string name="jetpack_not_found">Il plugin Jetpack non è stato trovato</string>
+ <string name="reader_label_added_tag">Aggiunto %s</string>
+ <string name="reader_label_removed_tag">Rimosso %s</string>
+ <string name="reader_likes_one">Ad una persona piace questo</string>
+ <string name="reader_likes_only_you">Ti piace questo</string>
+ <string name="reader_untitled_post">(Senza titolo)</string>
+ <string name="reader_share_subject">Condividi da %s</string>
+ <string name="reader_btn_share">Condividi</string>
+ <string name="reader_btn_follow">Segui</string>
+ <string name="reader_btn_unfollow">Seguendo</string>
+ <string name="reader_toast_err_comment_failed">Non riesco ad inviare il tuo commento</string>
+ <string name="reader_toast_err_tag_exists">Stai già seguendo questo tag</string>
+ <string name="reader_toast_err_tag_invalid">Questo non è un tag valido</string>
+ <string name="reader_toast_err_share_intent">Impossibile condividere</string>
+ <string name="reader_toast_err_view_image">Impossibile visualizzare l\'immagine</string>
+ <string name="reader_toast_err_url_intent">Impossibile aprire %s</string>
+ <string name="reader_empty_followed_tags">Non stai seguendo alcun tag</string>
+ <string name="create_account_wpcom">Apri un account su WordPress.com</string>
+ <string name="connecting_wpcom">Collegamento con WordPress.com in corso</string>
+ <string name="limit_reached">Limite raggiunto. Si consiglia di attendere almeno un minuto prima di riprovare. Se l\'errore persiste, o se pensi che sia un errore del sistema, si prega di contattare il supporto.</string>
+ <string name="username_invalid">Nome utente non valido</string>
+ <string name="nux_tutorial_get_started_title">Inizia!</string>
+ <string name="reader_hint_comment_on_comment">Rispondi al commento…</string>
+ <string name="button_next">Successivo</string>
+ <string name="themes">Temi</string>
+ <string name="all">Tutto</string>
+ <string name="images">Immagini</string>
+ <string name="custom_date">Data personalizzata</string>
+ <string name="media_add_popup_capture_photo">Scatta una foto</string>
+ <string name="media_add_popup_capture_video">Registra un video</string>
+ <string name="media_edit_title_text">Titolo</string>
+ <string name="media_edit_description_text">Descrizione</string>
+ <string name="media_edit_title_hint">Inserisci un titolo qui</string>
+ <string name="media_edit_description_hint">Inserisci una descrizione qui</string>
+ <string name="theme_set_success">Tema impostato con successo!</string>
+ <string name="share_action_title">Aggiungi a...</string>
+ <string name="stats_view_tags_and_categories">Tag &amp; Categorie</string>
+ <string name="stats_timeframe_today">Oggi</string>
+ <string name="stats_timeframe_yesterday">Ieri</string>
+ <string name="stats_timeframe_days">Giorni</string>
+ <string name="stats_timeframe_weeks">Settimane</string>
+ <string name="stats_totals_views">Visualizzazioni</string>
+ <string name="stats_totals_clicks">Click</string>
+ <string name="media_gallery_image_order_random">Casuale</string>
+ <string name="media_gallery_image_order_reverse">Invertito</string>
+ <string name="media_gallery_type">Tipo</string>
+ <string name="stats_entry_country">Paese</string>
+ <string name="stats_entry_posts_and_pages">Titolo</string>
+ <string name="media_edit_failure">Aggiornamento fallito</string>
+ <string name="passcode_preference_title">PIN bloccato</string>
+ <string name="passcode_turn_off">Disattiva il blocco PIN</string>
+ <string name="passcode_turn_on">Attiva il blocco PIN</string>
+ <string name="passcode_change_passcode">Cambia PIN</string>
+ <string name="passcode_enter_passcode">Inserisci il tuo PIN</string>
+ <string name="passcode_enter_old_passcode">Inserisci il vecchio PIN</string>
+ <string name="passcode_re_enter_passcode">Reimmetti il tuo PIN</string>
+ <string name="media_gallery_type_tiled">A riquadri.</string>
+ <string name="passcode_set">Imposta PIN</string>
+ <string name="themes_details_label">Dettagli</string>
+ <string name="themes_features_label">Funzionalità</string>
+ <string name="theme_activate_button">Attiva</string>
+ <string name="media_edit_caption_text">Didascalia</string>
+ <string name="media_edit_caption_hint">Inserire qui la didascalia</string>
+ <string name="stats">Statistiche</string>
+ <string name="stats_timeframe_months">Mesi</string>
+ <string name="stats_view_referrers">Referrer</string>
+ <string name="post_excerpt">Riassunto</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_view_clicks">Clic</string>
+ <string name="media_gallery_type_circles">Cerchi</string>
+ <string name="stats_entry_authors">Autore</string>
+ <string name="media_gallery_type_squares">Riquadri</string>
+ <string name="stats_entry_tags_and_categories">Argomenti</string>
+ <string name="theme_activating_button">Attivazione</string>
+ <string name="passcode_manage">Gestione blocco PIN</string>
+ <string name="media_gallery_type_slideshow">Visualizza come slideshow</string>
+ <string name="share_action">Condividi</string>
+ <string name="stats_totals_plays">Numero di riproduzioni</string>
+ <string name="theme_auth_error_title">Impossibile recuperare i temi</string>
+ <string name="media_edit_success">Aggiornato</string>
+ <string name="unattached">Non allegato</string>
+ <string name="stats_view_visitors_and_views">Visitatori e visite</string>
+ <string name="upload">Caricamento</string>
+ <string name="discard">Rimuovi</string>
+ <string name="sign_in">Accedi</string>
+ <string name="notifications">Notifiche</string>
+ <string name="note_reply_successful">Risposta pubblicata</string>
+ <string name="follows">Segui</string>
+ <string name="new_notifications">%d nuove notifiche</string>
+ <string name="more_notifications">e altre %d.</string>
+ <string name="loading">Caricamento in corso…</string>
+ <string name="httpuser">HTTP nome utente</string>
+ <string name="httppassword">Password HTTP</string>
+ <string name="error_media_upload">Si è verificato un errore durante il caricamento</string>
+ <string name="post_content">Contenuto (tocca per aggiungere testo e file multimediali)</string>
+ <string name="publish_date">Pubblica</string>
+ <string name="content_description_add_media">Aggiungi file multimediale</string>
+ <string name="incorrect_credentials">Nome utente o password errati</string>
+ <string name="password">Password</string>
+ <string name="username">Nome utente</string>
+ <string name="reader">Lettore</string>
+ <string name="width">Larghezza</string>
+ <string name="featured">Usa come immagine in evidenza</string>
+ <string name="featured_in_post">Includi immagine nel contenuto dell\'articolo</string>
+ <string name="no_network_title">Nessuna rete disponibile</string>
+ <string name="pages">Pagine</string>
+ <string name="caption">Didascalia (facoltativa)</string>
+ <string name="posts">Articoli</string>
+ <string name="anonymous">Anonimo</string>
+ <string name="page">Pagina</string>
+ <string name="post">Articolo</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">nomeutenteblog</string>
+ <string name="upload_scaled_image">Carica e inserisci link all\'immagine ridimensionata</string>
+ <string name="scaled_image">Larghezza immagine ridimensionata</string>
+ <string name="scheduled">Programmato</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Caricamento...</string>
+ <string name="version">Versione</string>
+ <string name="tos">Condizioni del servizio</string>
+ <string name="app_title">WordPress per Android</string>
+ <string name="max_thumbnail_px_width">Larghezza predefinita immagine</string>
+ <string name="image_alignment">Allineamento</string>
+ <string name="refresh">Aggiorna</string>
+ <string name="untitled">Senza titolo</string>
+ <string name="edit">Modifica</string>
+ <string name="post_id">Articolo</string>
+ <string name="page_id">Pagina</string>
+ <string name="immediately">Subito</string>
+ <string name="post_password">Password (facoltativa)</string>
+ <string name="quickpress_add_alert_title">Imposta nome breve</string>
+ <string name="today">Oggi</string>
+ <string name="settings">Impostazioni</string>
+ <string name="share_url">Condividi URL</string>
+ <string name="quickpress_window_title">Selezionare il blog per la scelta rapida QuickPress</string>
+ <string name="quickpress_add_error">Il nome breve non può essere vuoto</string>
+ <string name="post_private">Privato</string>
+ <string name="publish_post">Pubblica</string>
+ <string name="draft">Bozza</string>
+ <string name="upload_full_size_image">Carica e collega all\'immagine a pieno formato</string>
+ <string name="title">Titolo</string>
+ <string name="categories">Categorie</string>
+ <string name="tags_separate_with_commas">Etichette (separate da una virgola)</string>
+ <string name="dlg_deleting_comments">Eliminazione commenti</string>
+ <string name="notification_vibrate">Vibrazione</string>
+ <string name="notification_blink">Esegui notifica visiva</string>
+ <string name="notification_sound">Suono di notifica</string>
+ <string name="status">Stato</string>
+ <string name="location">Posizione</string>
+ <string name="sdcard_title">Scheda SD necessaria</string>
+ <string name="select_video">Seleziona un video dalla Galleria</string>
+ <string name="media">Media</string>
+ <string name="delete">Elimina</string>
+ <string name="none">Nessuna</string>
+ <string name="blogs">Blog</string>
+ <string name="select_photo">Seleziona una foto dalla galleria</string>
+ <string name="cancel">Annulla</string>
+ <string name="save">Salva</string>
+ <string name="add">Aggiungi</string>
+ <string name="no">No</string>
+ <string name="error">Errore</string>
+ <string name="category_refresh_error">Errore durante l\'aggiornamento delle categorie</string>
+ <string name="preview">Anteprima</string>
+ <string name="on">on</string>
+ <string name="yes">Sì</string>
+ <string name="reply">Rispondi</string>
+ <string name="notification_settings">Impostazioni notifiche</string>
+</resources>
diff --git a/WordPress/src/main/res/values-iw b/WordPress/src/main/res/values-iw
new file mode 120000
index 000000000..57bf91954
--- /dev/null
+++ b/WordPress/src/main/res/values-iw
@@ -0,0 +1 @@
+values-he \ No newline at end of file
diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml
new file mode 100644
index 000000000..624704dd6
--- /dev/null
+++ b/WordPress/src/main/res/values-ja/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">管理者</string>
+ <string name="role_follower">フォロワー</string>
+ <string name="role_author">投稿者</string>
+ <string name="role_contributor">寄稿者</string>
+ <string name="role_editor">編集者</string>
+ <string name="role_viewer">閲覧者</string>
+ <string name="error_post_my_profile_no_connection">接続されていないためプロフィールを保存できませんでした</string>
+ <string name="alignment_none">なし</string>
+ <string name="alignment_left">左</string>
+ <string name="alignment_right">右</string>
+ <string name="site_settings_list_editor_action_mode_title">%1$d が選択されています</string>
+ <string name="error_fetch_users_list">サイトのユーザーを取得できませんでした</string>
+ <string name="plans_manage">プランの管理は次で行います\nWordPress.com/plans</string>
+ <string name="title_follower">フォロワー</string>
+ <string name="title_email_follower">メールフォロワー</string>
+ <string name="people_empty_list_filtered_viewers">まだ読者がいません。</string>
+ <string name="people_fetching">ユーザーを取得中…</string>
+ <string name="people_empty_list_filtered_email_followers">まだメールフォロワーがいません。</string>
+ <string name="people_empty_list_filtered_followers">まだフォロワーがいません。</string>
+ <string name="people_empty_list_filtered_users">まだユーザーがいません。</string>
+ <string name="people_dropdown_item_viewers">閲覧者</string>
+ <string name="people_dropdown_item_email_followers">メールフォロワー</string>
+ <string name="people_dropdown_item_followers">フォロワー</string>
+ <string name="people_dropdown_item_team">チーム</string>
+ <string name="invite_message_usernames_limit">メールアドレスまたは WordPress.com ユーザー名を使って10名まで招待できます。ユーザー名をお持ちでない方には作成方法が送信されます。</string>
+ <string name="viewer_remove_confirmation_message">この読者を削除すると、この読者はこのサイトにアクセスできなくなります。\n\n本当にこの読者を削除しますか ?</string>
+ <string name="follower_remove_confirmation_message">削除すると、このフォロワーが再度フォローしない限り、このサイトの通知を受け取れなくなります。\n\n本当にこのフォロワーを削除しますか ?</string>
+ <string name="follower_subscribed_since">購読日: %1$s</string>
+ <string name="reader_label_view_gallery">ギャラリーを表示</string>
+ <string name="error_remove_follower">フォロワーを削除できませんでした。</string>
+ <string name="error_remove_viewer">閲覧者を削除できませんでした。</string>
+ <string name="error_fetch_email_followers_list">サイトのメールフォロワーを取得できませんでした。</string>
+ <string name="error_fetch_followers_list">サイトのフォロワーを取得できませんでした。</string>
+ <string name="editor_failed_uploads_switch_html">一部のメディアのアップロードに失敗しました。この状態では、HTML モードに切り替えることは\nできません。失敗したアップロードをすべて削除して続行しますか ?</string>
+ <string name="format_bar_description_html">HTML モード</string>
+ <string name="visual_editor">ビジュアルエディター</string>
+ <string name="image_thumbnail">画像サムネイル</string>
+ <string name="format_bar_description_ol">順序付きリスト</string>
+ <string name="format_bar_description_ul">番号なしリスト</string>
+ <string name="format_bar_description_media">メディアを挿入</string>
+ <string name="format_bar_description_more">more タグを挿入</string>
+ <string name="format_bar_description_strike">取り消し線</string>
+ <string name="format_bar_description_link">リンクを挿入</string>
+ <string name="format_bar_description_quote">引用をブロック</string>
+ <string name="format_bar_description_underline">下線</string>
+ <string name="format_bar_description_italic">斜体</string>
+ <string name="image_caption">見出し</string>
+ <string name="image_alt_text">Alt テキスト</string>
+ <string name="image_width">幅</string>
+ <string name="image_settings_save_toast">変更を保存しました</string>
+ <string name="image_link_to">リンク先</string>
+ <string name="format_bar_description_bold">太字</string>
+ <string name="image_settings_dismiss_dialog_title">保存されていない変更を破棄しますか ?</string>
+ <string name="stop_upload_dialog_title">アップロードを中止しますか ?</string>
+ <string name="stop_upload_button">アップロードを中止</string>
+ <string name="alert_error_adding_media">メディアの挿入中にエラーが発生しました</string>
+ <string name="alert_action_while_uploading">メディアをアップロードしています。完了するまで、しばらくお待ちください。</string>
+ <string name="alert_insert_image_html_mode">HTML モードでは直接メディアを挿入できません。ビジュアルモードに戻してください。</string>
+ <string name="uploading_gallery_placeholder">ギャラリーをアップロードしています…</string>
+ <string name="invite_error_some_failed">招待メールを送信しましたが、エラーが発生しました。</string>
+ <string name="invite_sent">招待メールを送信しました</string>
+ <string name="tap_to_try_again">タップしてもう一度お試しください。</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">招待メールの送信中にエラーが発生しました。</string>
+ <string name="invite_error_invalid_usernames_multiple">送信できません。無効なユーザー名またはメールアドレスがあります</string>
+ <string name="invite_error_invalid_usernames_one">送信できません。ユーザー名またはメールアドレスが無効です</string>
+ <string name="invite_error_no_usernames">最低1つのユーザー名を追加してください</string>
+ <string name="invite_message_info">(オプション) ユーザーへの招待メールには500文字以下のカスタムメッセージを含めることができます。</string>
+ <string name="invite_message_remaining_other">残りの文字数: %d</string>
+ <string name="invite_message_remaining_one">残り1文字です</string>
+ <string name="invite_message_remaining_zero">残り0文字です</string>
+ <string name="invite_invalid_email">メールアドレス \'%s\' が無効です</string>
+ <string name="invite_message_title">カスタムメッセージ</string>
+ <string name="invite_already_a_member">ユーザー名が \'%s\' のメンバーはすでに存在します</string>
+ <string name="invite_username_not_found">ユーザー名が \'%s\' のユーザーは見つかりませんでした</string>
+ <string name="invite">招待</string>
+ <string name="invite_names_title">ユーザー名またはメールアドレス</string>
+ <string name="send_link">リンクを送信</string>
+ <string name="signup_succeed_signin_failed">アカウントが作成されましたが、サインイン中にエラーが発生\nしました。新しく作成したユーザー名とパスワードを使用してサインインしてください。</string>
+ <string name="my_site_header_external">外部</string>
+ <string name="invite_people">他の人を招待</string>
+ <string name="label_clear_search_history">検索履歴の削除</string>
+ <string name="dlg_confirm_clear_search_history">検索履歴を削除しますか ?</string>
+ <string name="reader_empty_posts_in_search_description">お使いの言語では %s に関する投稿は見つかりませんでした。</string>
+ <string name="reader_empty_posts_in_search_title">投稿が見つかりません</string>
+ <string name="reader_label_post_search_running">検索中…</string>
+ <string name="reader_label_related_posts">こちらもどうぞ</string>
+ <string name="reader_label_post_search_explainer">WordPress.com の公開ブログをすべて検索</string>
+ <string name="reader_hint_post_search">WordPress.com を検索</string>
+ <string name="reader_title_search_results">「%s」を検索</string>
+ <string name="reader_title_related_post_detail">関連記事</string>
+ <string name="preview_screen_links_disabled">プレビュー画面ではリンクが無効になっています</string>
+ <string name="draft_explainer">この投稿は公開されていない下書きです</string>
+ <string name="send">送信</string>
+ <string name="user_remove_confirmation_message">%1$s を削除すると、そのユーザーはこのサイトにアクセスできなくなりますが、%1$s によって作成されたコンテンツはサイト上に残ります。\n\n本当にこのユーザーを削除しますか ?</string>
+ <string name="person_removed">%1$s を削除しました</string>
+ <string name="person_remove_confirmation_title">%1$s を削除</string>
+ <string name="edit_user">ユーザーの編集</string>
+ <string name="reader_empty_posts_in_custom_list">このリスト内のサイトには最近の投稿がありません。</string>
+ <string name="people">人</string>
+ <string name="role">権限グループ</string>
+ <string name="error_remove_user">ユーザーを削除できませんでした</string>
+ <string name="error_update_role">ユーザー権限グループを更新できませんでした</string>
+ <string name="error_fetch_viewers_list">サイトの閲覧者を取得できませんでした。</string>
+ <string name="gravatar_camera_and_media_permission_required">写真の選択または撮影に必要な権限</string>
+ <string name="error_updating_gravatar">Gravatar 更新エラー</string>
+ <string name="error_locating_image">切り抜き済み画像位置不明エラー</string>
+ <string name="error_refreshing_gravatar">Gravatar 再読み込みエラー</string>
+ <string name="gravatar_tip">[New] Gravatar をタップして変更できます。</string>
+ <string name="error_cropping_image">画像切り抜きエラー</string>
+ <string name="launch_your_email_app">メールアプリを開く</string>
+ <string name="checking_email">メールをチェック</string>
+ <string name="not_on_wordpress_com">WordPress.com をご利用ではありませんか ?</string>
+ <string name="check_your_email">メールをご確認ください</string>
+ <string name="magic_link_unavailable_error_message">現在ご利用いただけません。パスワードを入力してください</string>
+ <string name="logging_in">ログインしています</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">メールで送信されたリンクですぐにログイン</string>
+ <string name="enter_your_password_instead">代わりにパスワードを入力</string>
+ <string name="web_address_dialog_hint">コメントする際に公開表示されます。</string>
+ <string name="username_email">メールアドレスまたはユーザー名</string>
+ <string name="jetpack_not_connected_message">Jetpack プラグインがインストールされていますが、WordPress.com と連携されていません。Jetpack との連携を行いますか ?</string>
+ <string name="jetpack_not_connected">Jetpack プラグインが連携されていません</string>
+ <string name="new_editor_reflection_error">ビジュアルエディターはお使いのデバイスとの互換性がありません。自動で無効化されました。</string>
+ <string name="stats_insights_latest_post_no_title">(タイトルなし)</string>
+ <string name="capture_or_pick_photo">写真を撮影または選択</string>
+ <string name="plans_post_purchase_button_themes">テーマを表示</string>
+ <string name="plans_post_purchase_text_themes">さまざまなプレミアムテーマを無制限に利用できます。まずはサイト上で1つのテーマをプレビューしてください。</string>
+ <string name="plans_post_purchase_title_themes">サイトにぴったりのプレミアムテーマを見つけましょう</string>
+ <string name="plans_post_purchase_button_video">新規投稿をスタート</string>
+ <string name="plans_post_purchase_text_video">VideoPress と拡張メディアストレージを使用して、サイトに動画をアップロードし、ホスティングできます。</string>
+ <string name="plans_post_purchase_title_video">動画で投稿に彩りを。</string>
+ <string name="plans_post_purchase_button_customize">サイトをカスタマイズ</string>
+ <string name="plans_post_purchase_text_customize">カスタムフォント、カスタムカラー、カスタム CSS 編集機能を利用できます。</string>
+ <string name="plans_post_purchase_title_customize">フォントや色をカスタマイズ</string>
+ <string name="plans_post_purchase_text_intro">サイトをアップグレード中です。サイトの新機能をご確認いただき、どこから開始するかを選択してください。</string>
+ <string name="plans_post_purchase_title_intro">あなたのものになりました。おめでとうございます !</string>
+ <string name="plan">プラン</string>
+ <string name="plans">プラン</string>
+ <string name="plans_loading_error">プランを読み込めません</string>
+ <string name="export_your_content_message">投稿、ページ、設定については、%s 宛てにメールで送信されます。</string>
+ <string name="export_your_content">コンテンツをエクスポート</string>
+ <string name="exporting_content_progress">コンテンツをエクスポート中…</string>
+ <string name="export_email_sent">エクスポートメールを送信しました。</string>
+ <string name="premium_upgrades_message">サイトには有効なプレミアムアップグレードがあります。サイトを削除する前に、アップグレードをキャンセルしてください。</string>
+ <string name="show_purchases">購入を表示</string>
+ <string name="checking_purchases">購入を確認中</string>
+ <string name="premium_upgrades_title">プレミアムアップグレード</string>
+ <string name="purchases_request_error">エラーが発生しました。購入をリクエストできませんでした。</string>
+ <string name="delete_site_progress">サイトを削除中…</string>
+ <string name="delete_site_summary">この操作は元に戻すことはできません。サイトを削除すると、すべてのコンテンツ、寄稿者、ドメインがサイトから削除されます。</string>
+ <string name="delete_site_hint">サイトを削除</string>
+ <string name="export_site_hint">サイトを XML ファイルとしてエクスポート</string>
+ <string name="are_you_sure">本当によろしいですか ?</string>
+ <string name="export_site_summary">本当に削除する場合、今すぐコンテンツをエクスポートしてください。後でこのコンテンツを復元することはできません。</string>
+ <string name="keep_your_content">コンテンツを保存</string>
+ <string name="domain_removal_hint">サイトを削除すると、ドメインは機能しなくなります</string>
+ <string name="domain_removal_summary">ご注意ください。サイトを削除すると、以下のドメインも削除されます。</string>
+ <string name="primary_domain">主要ドメイン</string>
+ <string name="domain_removal">ドメインの削除</string>
+ <string name="error_deleting_site_summary">サイトの削除中にエラーが発生しました。ヘルプが必要な場合はサポートにご連絡ください</string>
+ <string name="error_deleting_site">サイト削除エラー</string>
+ <string name="site_settings_export_content_title">コンテンツをエクスポート</string>
+ <string name="confirm_delete_site_prompt">以下のフィールドに %1$s と入力して確認してください。サイトは永久に消去されます。</string>
+ <string name="contact_support">サポートに問い合わせる</string>
+ <string name="confirm_delete_site">サイト削除を確認</string>
+ <string name="start_over_text">サイトは維持したままで現在の投稿やページを削除したい場合は、サポートスタッフが投稿、ページ、メディア、コメントをすべて削除するお手伝いをさせていただきます。\n\nこうすることで、サイトと URL を保ったまま新しいコンテンツを一から作り始めることができます。サポートに連絡し、現在のコンテンツをすべて削除したい旨をお知らせください。</string>
+ <string name="site_settings_start_over_hint">サイトを最初からやり直す</string>
+ <string name="let_us_help">お手伝いさせてください</string>
+ <string name="me_btn_app_settings">アプリ設定</string>
+ <string name="start_over">最初からやり直す</string>
+ <string name="editor_remove_failed_uploads">アップロードに失敗したメディアを削除</string>
+ <string name="editor_toast_failed_uploads">一部のメディアのアップロードに失敗しました。この状態の投稿は\n保存や公開ができません。失敗したメディアをすべて削除しますか ?</string>
+ <string name="comments_empty_list_filtered_trashed">ゴミ箱内のコメントはありません</string>
+ <string name="site_settings_advanced_header">高度な設定</string>
+ <string name="comments_empty_list_filtered_pending">保留中のコメントはありません</string>
+ <string name="comments_empty_list_filtered_approved">承認済みのコメントはありません</string>
+ <string name="button_done">終了</string>
+ <string name="button_skip">スキップ</string>
+ <string name="site_timeout_error">タイムアウトエラーのため WordPress サイトに接続できませんでした。</string>
+ <string name="xmlrpc_malformed_response_error">接続できませんでした。WordPress インストールが、無効な XML-RPC ドキュメントで応答しました。</string>
+ <string name="xmlrpc_missing_method_error">接続できませんでした。必要な XML-RPC メソッドがサーバーにありません。</string>
+ <string name="post_format_status">ステータス</string>
+ <string name="post_format_video">動画</string>
+ <string name="theme_free">無料</string>
+ <string name="theme_all">すべて</string>
+ <string name="theme_premium">プレミアム</string>
+ <string name="alignment_center">中央</string>
+ <string name="post_format_chat">チャット</string>
+ <string name="post_format_gallery">ギャラリー</string>
+ <string name="post_format_image">画像</string>
+ <string name="post_format_link">リンク</string>
+ <string name="post_format_quote">引用</string>
+ <string name="post_format_standard">標準</string>
+ <string name="notif_events">WordPress.com のコースやイベント (オンライン・オフライン) の情報。</string>
+ <string name="post_format_aside">アサイド</string>
+ <string name="post_format_audio">音声ファイル</string>
+ <string name="notif_surveys">WordPress.com の研究・調査に参加する機会。</string>
+ <string name="notif_tips">WordPress.com を最大限に活用するためのヒント。</string>
+ <string name="notif_community">コミュニティ</string>
+ <string name="replies_to_my_comments">自分のコメントへの返信</string>
+ <string name="notif_suggestions">提案</string>
+ <string name="notif_research">リサーチ</string>
+ <string name="site_achievements">サイトの達成記録</string>
+ <string name="username_mentions">ユーザー名メンション</string>
+ <string name="likes_on_my_posts">自分の投稿への「いいね」</string>
+ <string name="site_follows">サイトでフォロー</string>
+ <string name="likes_on_my_comments">自分のコメントへの「いいね」</string>
+ <string name="comments_on_my_site">サイト上のコメント</string>
+ <string name="site_settings_list_editor_summary_other">%d項目</string>
+ <string name="site_settings_list_editor_summary_one">1項目</string>
+ <string name="approve_auto_if_previously_approved">既知のユーザーのコメント</string>
+ <string name="approve_auto">すべてのユーザー</string>
+ <string name="approve_manual">コメントなし</string>
+ <string name="site_settings_paging_summary_other">%d件のコメント (1ページあたり)</string>
+ <string name="site_settings_paging_summary_one">1件のコメント (1ページあたり)</string>
+ <string name="site_settings_multiple_links_summary_other">%dリンク以上の承認が必要です</string>
+ <string name="site_settings_multiple_links_summary_one">2リンク以上の承認が必要です</string>
+ <string name="site_settings_multiple_links_summary_zero">1リンク以上の承認が必要です</string>
+ <string name="detail_approve_auto">すべてのコメントを自動的に承認します。</string>
+ <string name="detail_approve_auto_if_previously_approved">ユーザーのコメントが以前に承認されている場合、自動的に承認します</string>
+ <string name="detail_approve_manual">すべてのコメントで手動の承認が必要です。</string>
+ <string name="filter_trashed_posts">ゴミ箱に移動</string>
+ <string name="days_quantity_one">1日</string>
+ <string name="days_quantity_other">%d日</string>
+ <string name="filter_published_posts">公開済み</string>
+ <string name="filter_draft_posts">下書き</string>
+ <string name="filter_scheduled_posts">予約済み</string>
+ <string name="pending_email_change_snackbar">%1$s へ送信されたメール内の認証リンクをクリックして、新しいアドレスを承認してください。</string>
+ <string name="primary_site">基本のサイト</string>
+ <string name="web_address">ウェブアドレス</string>
+ <string name="editor_toast_uploading_please_wait">メディアをアップロードしています。完了するまで、しばらくお待ちください。</string>
+ <string name="error_refresh_comments_showing_older">現在、コメントを更新することができないため、古いコメントが表示されます。</string>
+ <string name="editor_post_settings_set_featured_image">アイキャッチ画像を設定</string>
+ <string name="editor_post_settings_featured_image">アイキャッチ画像</string>
+ <string name="new_editor_promo_desc">Android 用の WordPress アプリに、美しいビジュアルエディターが\n新たに追加されました。新しい投稿を作って、その機能をお試しください。</string>
+ <string name="new_editor_promo_title">刷新されたエディター</string>
+ <string name="new_editor_promo_button_label">ありがとうございます。</string>
+ <string name="visual_editor_enabled">ビジュアルエディターを有効化しました</string>
+ <string name="editor_content_placeholder">こちらでストーリーを共有してください...</string>
+ <string name="editor_page_title_placeholder">ページタイトル</string>
+ <string name="editor_post_title_placeholder">投稿タイトル</string>
+ <string name="email_address">メールアドレス</string>
+ <string name="preference_show_visual_editor">ビジュアルエディターを表示</string>
+ <string name="dlg_sure_to_delete_comments">これらのコメントを完全に削除しますか ?</string>
+ <string name="preference_editor">編集者</string>
+ <string name="dlg_sure_to_delete_comment">このコメントを完全に削除しますか ?</string>
+ <string name="mnu_comment_delete_permanently">削除</string>
+ <string name="comment_deleted_permanently">コメントを削除しました</string>
+ <string name="mnu_comment_untrash">復元</string>
+ <string name="comments_empty_list_filtered_spam">スパムコメントはありません</string>
+ <string name="could_not_load_page">ページを読み込めません</string>
+ <string name="comment_status_all">すべて</string>
+ <string name="interface_language">管理画面の言語</string>
+ <string name="off">オフ</string>
+ <string name="about_the_app">このアプリについて</string>
+ <string name="error_post_account_settings">アカウント設定を保存できませんでした</string>
+ <string name="error_post_my_profile">プロフィールを保存できませんでした</string>
+ <string name="error_fetch_account_settings">アカウント設定を読み込めませんでした</string>
+ <string name="error_fetch_my_profile">プロフィールを読み込めませんでした</string>
+ <string name="stats_widget_promo_ok_btn_label">了解</string>
+ <string name="stats_widget_promo_desc">ワンクリックで統計情報にアクセスするには、ホーム画面にウィジェットを追加してください。</string>
+ <string name="stats_widget_promo_title">ホーム画面統計情報ウィジェット</string>
+ <string name="site_settings_unknown_language_code_error">言語コードを認識できません</string>
+ <string name="site_settings_threading_dialog_description">コメントスレッドでの入れ子化を許可する。</string>
+ <string name="site_settings_threading_dialog_header">入れ子数の上限</string>
+ <string name="remove">削除</string>
+ <string name="search">検索</string>
+ <string name="add_category">カテゴリーを追加</string>
+ <string name="disabled">無効</string>
+ <string name="site_settings_image_original_size">元のサイズ</string>
+ <string name="privacy_private">サイトはあなたと、承認したユーザーのみが表示できます。</string>
+ <string name="privacy_public_not_indexed">誰でもサイトを閲覧することができますが、検索エンジンにはインデックスしないよう要求します。</string>
+ <string name="privacy_public">誰でもサイトを閲覧することができ、検索エンジンにインデックスされる可能性があります。</string>
+ <string name="about_me_hint">簡単な自己紹介</string>
+ <string name="about_me">自己紹介</string>
+ <string name="public_display_name_hint">表示名が設定されていない場合、デフォルトでユーザー名が指定されます</string>
+ <string name="public_display_name">公開表示名</string>
+ <string name="my_profile">プロフィール</string>
+ <string name="first_name">名</string>
+ <string name="last_name">姓</string>
+ <string name="site_privacy_public_desc">検索エンジンによるサイトのインデックスを許可する</string>
+ <string name="site_privacy_hidden_desc">検索エンジンがサイトをインデックスしないようにする</string>
+ <string name="site_privacy_private_desc">サイトを非公開にし、選択したユーザーにのみ表示する</string>
+ <string name="cd_related_post_preview_image">関連記事のプレビュー画像</string>
+ <string name="error_post_remote_site_settings">サイト情報を保存できませんでした</string>
+ <string name="error_fetch_remote_site_settings">サイト情報を取得できませんでした</string>
+ <string name="error_media_upload_connection">メディアのアップロード中に接続エラーが発生しました</string>
+ <string name="site_settings_disconnected_toast">接続が中断されたため編集を無効化しました。</string>
+ <string name="site_settings_unsupported_version_error">サポートされていない WordPress バージョン</string>
+ <string name="site_settings_multiple_links_dialog_description">この数より多くのリンクがコメントに含まれる場合、承認が必要です。</string>
+ <string name="site_settings_close_after_dialog_switch_text">自動的に閉じる</string>
+ <string name="site_settings_close_after_dialog_description">記事のコメントを自動的に閉じます。</string>
+ <string name="site_settings_paging_dialog_description">コメントスレッドを複数のページに分けます。</string>
+ <string name="site_settings_paging_dialog_header">1ページあたりのコメント数</string>
+ <string name="site_settings_close_after_dialog_title">コメントを閉じます</string>
+ <string name="site_settings_blacklist_description">コメントの本文、名前、URL、メール、IP にこれらの語のいずれかが含まれている場合、スパムとしてマークされます。語句の一部を入力できます。例えば、「press」は「WordPress」に一致します。</string>
+ <string name="site_settings_hold_for_moderation_description">コメントの本文、名前、URL、メール、IP にこれらの語のいずれかが含まれている場合、承認待ちになります。語句の一部を入力できます。例えば、「press」は「WordPress」に一致します。</string>
+ <string name="site_settings_list_editor_input_hint">単語やフレーズを入力してください</string>
+ <string name="site_settings_list_editor_no_items_text">項目なし</string>
+ <string name="site_settings_learn_more_caption">これらの設定は投稿ごとに上書きできます。</string>
+ <string name="site_settings_rp_preview3_site">「アップグレード」カテゴリー</string>
+ <string name="site_settings_rp_preview3_title">アップグレードフォーカス: ウェディング向け VideoPress</string>
+ <string name="site_settings_rp_preview2_site">「アプリ」カテゴリー</string>
+ <string name="site_settings_rp_preview2_title">WordPress for Android アプリが大幅リニューアル</string>
+ <string name="site_settings_rp_preview1_site">「モバイル」カテゴリー</string>
+ <string name="site_settings_rp_preview1_title">iPhone/iPad の大幅なアップデートが利用できるようになりました</string>
+ <string name="site_settings_rp_show_images_title">画像を表示</string>
+ <string name="site_settings_rp_show_header_title">ヘッダーを表示</string>
+ <string name="site_settings_rp_switch_summary">「関連記事」は投稿の下にサイト内の関連コンテンツを表示します。</string>
+ <string name="site_settings_rp_switch_title">関連記事を表示</string>
+ <string name="site_settings_delete_site_hint">サイトのデータをアプリから削除します</string>
+ <string name="site_settings_blacklist_hint">フィルターに一致するコメントはスパムとしてマークされます</string>
+ <string name="site_settings_moderation_hold_hint">フィルターに一致するコメントは承認待ちになります</string>
+ <string name="site_settings_multiple_links_hint">既知のユーザーからのリンク制限を無視します</string>
+ <string name="site_settings_whitelist_hint">すでに承認されたコメントの投稿者のコメントを許可し、それ以外のコメントを承認待ちにする</string>
+ <string name="site_settings_user_account_required_hint">ユーザー登録してログインしたユーザーのみコメントをつけられるようにする</string>
+ <string name="site_settings_identity_required_hint">コメントの際、名前とメールアドレスの入力を必須にする</string>
+ <string name="site_settings_manual_approval_hint">コメントを手動で承認する必要があります</string>
+ <string name="site_settings_paging_hint">コメントを指定したサイズに分けて表示します</string>
+ <string name="site_settings_threading_hint">指定の深さまで入れ子コメントを許可します</string>
+ <string name="site_settings_sort_by_hint">コメントを表示する順番を決定します</string>
+ <string name="site_settings_close_after_hint">指定した時間後のコメントを許可しません</string>
+ <string name="site_settings_receive_pingbacks_hint">他のブログからのリンク通知を許可します</string>
+ <string name="site_settings_send_pingbacks_hint">この投稿に含まれるすべてのリンクへの通知を試みる</string>
+ <string name="site_settings_allow_comments_hint">読者からのコメントの投稿を許可します</string>
+ <string name="site_settings_discussion_hint">サイトのディスカッション設定を表示して変更します</string>
+ <string name="site_settings_more_hint">使用可能なディスカッション設定をすべて表示します</string>
+ <string name="site_settings_related_posts_hint">Reader で関連記事を表示または非表示にします</string>
+ <string name="site_settings_upload_and_link_image_hint">常時フルサイズ画像でのアップロードを有効にします</string>
+ <string name="site_settings_image_width_hint">投稿の画像のサイズを幅に合わせて変更します</string>
+ <string name="site_settings_format_hint">新しい投稿のフォーマットを設定します</string>
+ <string name="site_settings_category_hint">新しい投稿のカテゴリーを設定します</string>
+ <string name="site_settings_location_hint">位置データを投稿に自動的に追加します</string>
+ <string name="site_settings_password_hint">パスワードを変更します</string>
+ <string name="site_settings_username_hint">現在のユーザーアカウント</string>
+ <string name="site_settings_language_hint">ブログを書くときに使う主な言語</string>
+ <string name="site_settings_privacy_hint">サイトを閲覧できるユーザーを管理します</string>
+ <string name="site_settings_address_hint">アドレスの変更は現在サポートされていません</string>
+ <string name="site_settings_tagline_hint">ブログの簡単な説明やブログを表す魅力的なフレーズ</string>
+ <string name="site_settings_title_hint">このサイトを短い文章で説明してください</string>
+ <string name="site_settings_whitelist_known_summary">既知のユーザーからのコメント</string>
+ <string name="site_settings_whitelist_all_summary">すべてのユーザーからのコメント</string>
+ <string name="site_settings_threading_summary">%d階層</string>
+ <string name="site_settings_privacy_private_summary">プライベート</string>
+ <string name="site_settings_privacy_hidden_summary">非表示</string>
+ <string name="site_settings_delete_site_title">サイトを削除</string>
+ <string name="site_settings_privacy_public_summary">一般公開</string>
+ <string name="site_settings_blacklist_title">ブラックリスト</string>
+ <string name="site_settings_moderation_hold_title">承認待ち</string>
+ <string name="site_settings_multiple_links_title">コメント内のリンク</string>
+ <string name="site_settings_whitelist_title">自動で承認</string>
+ <string name="site_settings_paging_title">ページ送り</string>
+ <string name="site_settings_threading_title">スレッド化</string>
+ <string name="site_settings_sort_by_title">並び替え</string>
+ <string name="site_settings_account_required_title">ユーザーのログインを必須にする</string>
+ <string name="site_settings_identity_required_title">名前とメールを必須にする</string>
+ <string name="site_settings_receive_pingbacks_title">ピンバックを受け取る</string>
+ <string name="site_settings_send_pingbacks_title">ピンバックを送信</string>
+ <string name="site_settings_allow_comments_title">コメントを許可</string>
+ <string name="site_settings_default_format_title">デフォルトのフォーマット</string>
+ <string name="site_settings_default_category_title">デフォルトカテゴリー</string>
+ <string name="site_settings_location_title">位置情報を有効化</string>
+ <string name="site_settings_address_title">住所</string>
+ <string name="site_settings_title_title">サイトのタイトル</string>
+ <string name="site_settings_tagline_title">キャッチフレーズ</string>
+ <string name="site_settings_this_device_header">このデバイス</string>
+ <string name="site_settings_discussion_new_posts_header">新しい投稿のデフォルト</string>
+ <string name="site_settings_account_header">アカウント</string>
+ <string name="site_settings_writing_header">投稿設定</string>
+ <string name="newest_first">最新のものを先頭表示</string>
+ <string name="site_settings_general_header">一般</string>
+ <string name="discussion">ディスカッション</string>
+ <string name="privacy">プライバシー</string>
+ <string name="related_posts">関連記事</string>
+ <string name="comments">コメント</string>
+ <string name="close_after">特定の日付に終了:</string>
+ <string name="oldest_first">最も古いものを先頭表示</string>
+ <string name="media_error_no_permission_upload">このサイトにメディアをアップロードする権限がありません</string>
+ <string name="never">なし</string>
+ <string name="unknown">不明</string>
+ <string name="reader_err_get_post_not_found">この投稿は存在しません</string>
+ <string name="reader_err_get_post_not_authorized">この投稿を表示する権限がありません</string>
+ <string name="reader_err_get_post_generic">この投稿を取得できません</string>
+ <string name="blog_name_no_spaced_allowed">サイトのアドレスにスペースを含めることはできません</string>
+ <string name="invalid_username_no_spaces">ユーザー名にスペースを含めることはできません</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">フォローしているサイトには、最近の投稿はありません</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">最近の投稿はありません</string>
+ <string name="media_details_copy_url_toast">URL がクリップボードにコピーされました</string>
+ <string name="edit_media">メディアを編集</string>
+ <string name="media_details_copy_url">URL をコピー</string>
+ <string name="media_details_label_date_uploaded">アップロード済み</string>
+ <string name="media_details_label_date_added">追加済み</string>
+ <string name="selected_theme">選択済みテーマ</string>
+ <string name="could_not_load_theme">テーマを読み込めませんでした</string>
+ <string name="theme_activation_error">エラーが発生しました。テーマを有効化できませんでした。</string>
+ <string name="theme_by_author_prompt_append"> by %1$s</string>
+ <string name="theme_prompt">%1$s を選んでいただきありがとうございます。</string>
+ <string name="theme_try_and_customize">お試し &amp; カスタマイズ</string>
+ <string name="theme_view">表示</string>
+ <string name="theme_details">詳細</string>
+ <string name="theme_support">サポート</string>
+ <string name="theme_done">完了</string>
+ <string name="theme_manage_site">サイト管理</string>
+ <string name="title_activity_theme_support">テーマ</string>
+ <string name="theme_activate">有効化</string>
+ <string name="date_range_start_date">開始日</string>
+ <string name="date_range_end_date">終了日</string>
+ <string name="current_theme">現在のテーマ</string>
+ <string name="customize">カスタマイズ</string>
+ <string name="details">詳細</string>
+ <string name="support">サポート</string>
+ <string name="active">有効</string>
+ <string name="stats_referrers_spam_generic_error">操作中に問題が発生しました。スパムの状態は変更されていません。</string>
+ <string name="stats_referrers_marking_not_spam">非スパムとしてマーク中</string>
+ <string name="stats_referrers_unspam">非スパム</string>
+ <string name="stats_referrers_marking_spam">スパムとしてマーク中</string>
+ <string name="theme_auth_error_authenticate">テーマの取得に失敗しました。ユーザーを認証できません。</string>
+ <string name="post_published">投稿を公開しました</string>
+ <string name="page_published">ページを公開しました</string>
+ <string name="post_updated">投稿を更新しました</string>
+ <string name="page_updated">ページを更新しました</string>
+ <string name="stats_referrers_spam">スパム</string>
+ <string name="theme_no_search_result_found">テーマが見つかりませんでした。</string>
+ <string name="media_file_name">ファイル名: %s</string>
+ <string name="media_uploaded_on">アップロード日: %s</string>
+ <string name="media_dimensions">サイズ: %s</string>
+ <string name="upload_queued">待機中</string>
+ <string name="media_file_type">ファイル形式: %s</string>
+ <string name="reader_label_gap_marker">さらに投稿を読み込む</string>
+ <string name="notifications_no_search_results">「%s」に一致するサイトはありません</string>
+ <string name="search_sites">サイトを検索</string>
+ <string name="notifications_empty_view_reader">Reader を表示</string>
+ <string name="unread">未読</string>
+ <string name="notifications_empty_action_followers_likes">存在に気づいてもらうため、読んだ投稿にコメントしてみましょう。</string>
+ <string name="notifications_empty_action_comments">会話に参加しましょう。フォローしているブログの投稿にコメントしてみませんか。</string>
+ <string name="notifications_empty_action_unread">会話をまた活発にするため、新しい記事を書いてみましょう。</string>
+ <string name="notifications_empty_action_all">フォローしているブログの投稿にコメントしてみましょう。</string>
+ <string name="notifications_empty_likes">新しい「いいね」はまだついていません。</string>
+ <string name="notifications_empty_followers">新しいフォロワーはまだいません。</string>
+ <string name="notifications_empty_comments">コメントはまだありません。</string>
+ <string name="notifications_empty_unread">すべて既読になっています !</string>
+ <string name="stats_widget_error_jetpack_no_blogid">アプリ内で統計情報にアクセスし、後でウィジェットを追加してみてください</string>
+ <string name="stats_widget_error_readd_widget">ウィジェットを削除し、もう一度追加してみてください</string>
+ <string name="stats_widget_error_no_visible_blog">表示可能なブログを持っていない場合、統計情報にはアクセスできません</string>
+ <string name="stats_widget_error_no_permissions">あなたの WordPress.com のアカウントではこのブログの統計情報にアクセスできません。</string>
+ <string name="stats_widget_error_no_account">WordPress にログインしてください</string>
+ <string name="stats_widget_error_generic">統計情報を読み込めませんでした</string>
+ <string name="stats_widget_loading_data">データを読み込み中…</string>
+ <string name="stats_widget_name_for_blog">WordPress 統計情報 (%1$s)</string>
+ <string name="stats_widget_name">WordPress 今日の統計情報</string>
+ <string name="add_location_permission_required">位置情報を追加するために必要なアクセス権限</string>
+ <string name="add_media_permission_required">メディアを追加するには権限が必要です</string>
+ <string name="access_media_permission_required">メディアにアクセスするには権限が必要です</string>
+ <string name="stats_enable_rest_api_in_jetpack">統計情報を表示するには、Jetpack の JSON API モジュールを有効化してください。</string>
+ <string name="error_open_list_from_notification">この投稿またはページは他のサイトで公開されています</string>
+ <string name="reader_short_comment_count_multi">%s件のコメント</string>
+ <string name="reader_short_comment_count_one">1件のコメント</string>
+ <string name="reader_label_submit_comment">送信</string>
+ <string name="reader_hint_comment_on_post">投稿に返信…</string>
+ <string name="reader_discover_visit_blog">%s を表示</string>
+ <string name="reader_discover_attribution_blog">%s から引用</string>
+ <string name="reader_discover_attribution_author">元の投稿者: %s</string>
+ <string name="reader_discover_attribution_author_and_blog">元の投稿者: %1$s サイト: %2$s</string>
+ <string name="reader_short_like_count_multi">%s個の「いいね」</string>
+ <string name="reader_short_like_count_one">1個の「いいね」</string>
+ <string name="reader_label_follow_count">%,d人のフォロワー</string>
+ <string name="reader_short_like_count_none">いいね</string>
+ <string name="reader_menu_tags">タグとブログの編集</string>
+ <string name="reader_title_post_detail">Reader 投稿</string>
+ <string name="local_draft_explainer">この投稿は公開されていないローカルの下書きです</string>
+ <string name="local_changes_explainer">この投稿には公開されていないローカルの変更があります</string>
+ <string name="notifications_push_summary">デバイスに表示される通知の設定。</string>
+ <string name="notifications_email_summary">アカウントに結び付けられているメールに送信される通知の設定。</string>
+ <string name="notifications_tab_summary">「通知」タブに表示される通知の設定。</string>
+ <string name="notifications_disabled">アプリ通知が無効化されています。ここをタップして「設定」で有効化します。</string>
+ <string name="notification_types">通知の種類</string>
+ <string name="error_loading_notifications">通知設定を読み込めませんでした</string>
+ <string name="replies_to_your_comments">コメントへの返信</string>
+ <string name="comment_likes">コメントへの「いいね」</string>
+ <string name="app_notifications">アプリ通知</string>
+ <string name="notifications_tab">「通知」タブ</string>
+ <string name="email">メール</string>
+ <string name="notifications_comments_other_blogs">他のサイトのコメント</string>
+ <string name="notifications_wpcom_updates">WordPress.com のニュース</string>
+ <string name="notifications_other">その他</string>
+ <string name="notifications_account_emails">WordPress.com からのメール</string>
+ <string name="notifications_account_emails_summary">アカウントに関する重要なお知らせは常にメールでお送りしますが、役に立つ追加情報を受け取ることもできます。</string>
+ <string name="your_sites">あなたのサイト</string>
+ <string name="notifications_sights_and_sounds">視覚効果と音声</string>
+ <string name="stats_insights_latest_post_trend">「%2$s」が公開されてから%1$sが経過しました。過去の投稿のパフォーマンスを次に示します…</string>
+ <string name="stats_insights_latest_post_summary">最新の投稿概要</string>
+ <string name="button_revert">戻す</string>
+ <string name="days_ago">%d日前</string>
+ <string name="yesterday">昨日</string>
+ <string name="connectionbar_no_connection">接続できません</string>
+ <string name="page_trashed">ページをゴミ箱へ移動しました</string>
+ <string name="post_deleted">投稿を削除しました</string>
+ <string name="post_trashed">投稿をゴミ箱へ移動しました</string>
+ <string name="stats_no_activity_this_period">この期間中のアクティビティがありません</string>
+ <string name="trashed">ゴミ箱移動済み</string>
+ <string name="button_back">戻る</string>
+ <string name="page_deleted">ページを削除しました</string>
+ <string name="button_stats">統計情報</string>
+ <string name="button_trash">ゴミ箱</string>
+ <string name="button_preview">プレビュー</string>
+ <string name="button_view">表示</string>
+ <string name="button_edit">編集</string>
+ <string name="button_publish">公開</string>
+ <string name="my_site_no_sites_view_subtitle">追加しますか ?</string>
+ <string name="my_site_no_sites_view_title">まだ WordPress サイトがありません。</string>
+ <string name="my_site_no_sites_view_drake">イラスト</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">このブログにアクセスする権限がありません</string>
+ <string name="reader_toast_err_follow_blog_not_found">ブログが見つかりませんでした</string>
+ <string name="undo">元に戻す</string>
+ <string name="tabbar_accessibility_label_my_site">参加サイト</string>
+ <string name="tabbar_accessibility_label_me">自分</string>
+ <string name="editor_toast_changes_saved">変更を保存しました</string>
+ <string name="passcodelock_prompt_message">PIN コードを入力</string>
+ <string name="push_auth_expired">リクエストの有効期限が切れています。もう一度試すには WordPress.com にログインしてください。</string>
+ <string name="ignore">無視</string>
+ <string name="stats_insights_most_popular_percent_views">表示数の%1$d%%</string>
+ <string name="stats_insights_best_ever">過去最高の表示数</string>
+ <string name="stats_insights_most_popular_hour">最も人気の時間</string>
+ <string name="stats_insights_most_popular_day">最も人気の日</string>
+ <string name="stats_insights_popular">最も人気の日と時間</string>
+ <string name="stats_insights_today">今日の統計情報</string>
+ <string name="stats_insights_all_time">全期間の投稿、表示数、訪問者</string>
+ <string name="stats_insights">まとめ</string>
+ <string name="stats_sign_in_jetpack_different_com_account">統計情報を表示するには、Jetpack 連携の際に使用した WordPress.com アカウントにログインしてください。</string>
+ <string name="stats_other_recent_stats_moved_label">他の最近の統計をお探しですか?「まとめ」ページへ移動しました。</string>
+ <string name="me_disconnect_from_wordpress_com">WordPress.com 連携を解除</string>
+ <string name="me_connect_to_wordpress_com">WordPress.com アカウントに連携</string>
+ <string name="me_btn_login_logout">ログイン/ログアウト</string>
+ <string name="account_settings">アカウント設定</string>
+ <string name="me_btn_support">ヘルプ &amp; サポート</string>
+ <string name="site_picker_cant_hide_current_site">現在のサイトのため “%s” は非表示になりませんでした</string>
+ <string name="site_picker_create_dotcom">WordPress.com サイトを作成</string>
+ <string name="site_picker_edit_visibility">サイトを表示/非表示</string>
+ <string name="site_picker_add_site">サイトを追加</string>
+ <string name="site_picker_add_self_hosted">インストール型サイトを追加</string>
+ <string name="my_site_btn_switch_site">サイト切り替え</string>
+ <string name="my_site_btn_view_admin">管理画面表示</string>
+ <string name="my_site_btn_view_site">サイトを表示</string>
+ <string name="site_picker_title">サイトを選択</string>
+ <string name="my_site_btn_blog_posts">ブログ投稿</string>
+ <string name="my_site_btn_site_settings">設定</string>
+ <string name="my_site_header_look_and_feel">ルックアンドフィール</string>
+ <string name="my_site_header_publish">公開</string>
+ <string name="my_site_header_configuration">設定</string>
+ <string name="reader_label_new_posts_subtitle">タップして表示</string>
+ <string name="notifications_account_required">通知を受け取るには WordPress.com にログインしてください</string>
+ <string name="stats_unknown_author">不明な投稿者</string>
+ <string name="image_added">画像を追加しました</string>
+ <string name="signout">連携を解除</string>
+ <string name="sign_out_wpcom_confirm">アカウントの連携を解除すると、ローカルの下書きおよび変更を含む @%s のすべての WordPress.com データがこのデバイスから削除されます。</string>
+ <string name="deselect_all">すべて選択解除</string>
+ <string name="show">表示</string>
+ <string name="hide">隠す</string>
+ <string name="select_all">すべて選択</string>
+ <string name="select_from_new_picker">新しいピッカーでの複数選択</string>
+ <string name="error_loading_videos">動画読み込みエラー</string>
+ <string name="stats_generic_error">必須の統計情報が読み込めませんでした</string>
+ <string name="no_blog_images">画像なし</string>
+ <string name="no_device_videos">動画なし</string>
+ <string name="no_blog_videos">動画なし</string>
+ <string name="no_device_images">画像なし</string>
+ <string name="error_loading_images">画像読み込みエラー</string>
+ <string name="loading_blog_videos">動画を取得中</string>
+ <string name="no_media_sources">メディアを取得できませんでした</string>
+ <string name="error_loading_blog_videos">動画を取得できません</string>
+ <string name="loading_blog_images">画像を取得中</string>
+ <string name="error_loading_blog_images">画像を取得できません</string>
+ <string name="no_media">メディアがありません</string>
+ <string name="loading_videos">動画を読み込み中</string>
+ <string name="loading_images">画像を読み込み中</string>
+ <string name="auth_required">続行するにはもう一度サインインしてください。</string>
+ <string name="sign_in_jetpack">Jetpack に接続するため WordPress.com アカウントにサインインしてください。</string>
+ <string name="two_step_sms_sent">テキストメッセージの認証コードを確認してください。</string>
+ <string name="two_step_footer_button">テキストメッセージでコードを送信</string>
+ <string name="two_step_footer_label">お使いの認証アプリからのコードを入力してください。</string>
+ <string name="verify">認証</string>
+ <string name="invalid_verification_code">無効な認証コード</string>
+ <string name="verification_code">認証コード</string>
+ <string name="editor_toast_invalid_path">無効なファイルパス</string>
+ <string name="error_publish_no_network">接続がないため公開できません。下書きとして保存します。</string>
+ <string name="device">端末</string>
+ <string name="tab_title_site_images">サイトの画像</string>
+ <string name="tab_title_site_videos">サイトの動画</string>
+ <string name="tab_title_device_images">端末の画像</string>
+ <string name="tab_title_device_videos">端末の動画</string>
+ <string name="take_video">動画を撮る</string>
+ <string name="take_photo">写真を撮る</string>
+ <string name="media_picker_title">メディアを選択</string>
+ <string name="add_to_post">投稿に追加</string>
+ <string name="language">言語</string>
+ <string name="media_details_label_file_name">ファイル名</string>
+ <string name="media_details_label_file_type">ファイル形式</string>
+ <string name="stats_empty_search_terms_desc">訪問者があなたのサイトを見つけるために検索したキーワードを見て、検索トラフィックについてより詳しく知りましょう。</string>
+ <string name="toast_err_post_uploading">アップロード中は投稿を開けません</string>
+ <string name="stats_view_authors">投稿者</string>
+ <string name="stats_entry_search_terms">検索キーワード</string>
+ <string name="stats_view_search_terms">検索キーワード</string>
+ <string name="comments_fetching">コメントを取得中…</string>
+ <string name="stats_empty_search_terms">検索キーワードが記録されていません</string>
+ <string name="pages_fetching">ページを取得中…</string>
+ <string name="media_fetching">メディアを取得中…</string>
+ <string name="posts_fetching">投稿を取得中…</string>
+ <string name="stats_followers_total_wpcom_paged">%1$d - %2$d人目を表示中 (%3$s人の WordPress.com フォロワー中)</string>
+ <string name="stats_followers_total_email_paged">%1$d - %2$d人目を表示中 (%3$s人のメールフォロワー中)</string>
+ <string name="reader_empty_posts_request_failed">投稿を取得できません</string>
+ <string name="error_notification_open">通知を開けませんでした</string>
+ <string name="stats_search_terms_unknown_search_terms">不明な検索語</string>
+ <string name="publisher">投稿者:</string>
+ <string name="stats_months_and_years">月と年</string>
+ <string name="stats_recent_weeks">最近の週</string>
+ <string name="error_copy_to_clipboard">テキストをクリップボードにコピーする際にエラーが発生しました</string>
+ <string name="stats_average_per_day">一日の平均</string>
+ <string name="reader_label_new_posts">新規投稿</string>
+ <string name="reader_empty_posts_in_blog">このブログは空です</string>
+ <string name="stats_period">期間</string>
+ <string name="logs_copied_to_clipboard">アプリケーションログをクリップボードにコピーしました</string>
+ <string name="stats_total">合計</string>
+ <string name="stats_overall">全体</string>
+ <string name="post_uploading">アップロード中</string>
+ <string name="reader_page_recommended_blogs">おすすめのサイト</string>
+ <string name="stats_comments_total_comments_followers">コメントをフォロー中の人がいる投稿の合計数: %1$s</string>
+ <string name="stats_empty_geoviews_desc">リストから、どの国や地域からのトラフィックが多いのかを見ることができます。</string>
+ <string name="stats_empty_top_posts_desc">最も頻繁に表示されているコンテンツを発見し、各投稿やページの長期間に渡ったパフォーマンスをチェックできます。</string>
+ <string name="stats_empty_referrers_desc">あなたのサイトにトラフィックを送ってくれているサイトや検索エンジンを見ることで、サイトの認知度をさらに理解しましょう。</string>
+ <string name="stats_empty_clicks_desc">コンテンツに外部サイトへのリンクが含まれている場合、どのリンクが訪問者に最もよくクリックされているかを知ることができます。</string>
+ <string name="stats_empty_top_authors_desc">各寄稿者の投稿表示数をチェックし、さらに投稿者ごとに人気のコンテンツを発見できます。</string>
+ <string name="stats_empty_tags_and_categories_desc">過去1週間の人気投稿に基づいた、サイトの人気トピックの概要を取得できます。</string>
+ <string name="stats_empty_video_desc">VideoPress を使って動画をアップロードした場合は、何回視聴されたかを知ることができます。</string>
+ <string name="stats_empty_comments_desc">サイトでコメントを許可している場合、最も頻繁にコメントを投稿している人やコメントがたくさんついているコンテンツを最近1000件のコメントに基づいて確認できます。</string>
+ <string name="stats_empty_followers_desc">フォロワーの総数や、各フォロワーがあなたのサイトをどのくらいの期間フォローしているかを確認できます。</string>
+ <string name="stats_empty_publicize_desc">パブリサイズ機能を使っている各種 SNS でのフォロワーの数を確認できます。</string>
+ <string name="stats_views">表示数</string>
+ <string name="stats_visitors">サイト訪問者</string>
+ <string name="stats_timeframe_years">年</string>
+ <string name="stats_pagination_label">%1$sページ目 (%2$sページ中)</string>
+ <string name="stats_view_videos">動画</string>
+ <string name="stats_view_publicize">パブリサイズ共有 </string>
+ <string name="stats_view_followers">フォロワー</string>
+ <string name="stats_view_countries">国</string>
+ <string name="stats_likes">いいね</string>
+ <string name="stats_entry_clicks_link">リンク</string>
+ <string name="stats_view_top_posts_and_pages">投稿とページ</string>
+ <string name="stats_totals_publicize">フォロワー</string>
+ <string name="stats_entry_publicize">サービス</string>
+ <string name="stats_entry_followers">フォロワー</string>
+ <string name="stats_entry_video_plays">動画</string>
+ <string name="stats_entry_top_commenter">投稿者</string>
+ <string name="stats_empty_geoviews">国名が記録されていません</string>
+ <string name="stats_totals_followers">以降</string>
+ <string name="stats_empty_top_posts_title">投稿またはページが表示されていません</string>
+ <string name="stats_empty_clicks_title">リンクがクリックされていません</string>
+ <string name="stats_empty_referrers_title">リファラが記録されていません</string>
+ <string name="stats_empty_publicize">パブリサイズフォロワーがいません</string>
+ <string name="stats_empty_video">動画が再生されていません</string>
+ <string name="stats_empty_tags_and_categories">タグ付きの投稿またはページが表示されていません</string>
+ <string name="stats_empty_followers">フォロワーなし</string>
+ <string name="stats_comments_by_authors">投稿者</string>
+ <string name="stats_comments_by_posts_and_pages">投稿とページ</string>
+ <string name="stats_followers_total_wpcom">WordPress.com フォロワーの合計数: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">メール</string>
+ <string name="stats_followers_total_email">メールフォロワーの合計数: %1$s</string>
+ <string name="stats_followers_a_minute_ago">1分前</string>
+ <string name="stats_followers_seconds_ago">数秒前</string>
+ <string name="stats_followers_days">%1$d日</string>
+ <string name="stats_followers_a_day">1日</string>
+ <string name="stats_followers_hours">%1$d時間</string>
+ <string name="stats_followers_an_hour_ago">1時間前</string>
+ <string name="stats_followers_minutes">%1$d分</string>
+ <string name="stats_followers_years">%1$d年</string>
+ <string name="stats_followers_a_month">1ヶ月</string>
+ <string name="stats_followers_months">%1$dヶ月</string>
+ <string name="stats_followers_a_year">1年</string>
+ <string name="stats_view">表示</string>
+ <string name="stats_view_all">すべて表示</string>
+ <string name="stats_other_recent_stats_label">最近の他の統計情報</string>
+ <string name="stats_for">%s の統計情報</string>
+ <string name="themes_fetching">テーマを取得中…</string>
+ <string name="ssl_certificate_details">詳細</string>
+ <string name="sure_to_remove_account">このサイトを削除しますか ?</string>
+ <string name="delete_sure_post">この投稿を削除</string>
+ <string name="delete_sure">この下書きを削除</string>
+ <string name="delete_sure_page">このページを削除</string>
+ <string name="confirm_delete_multi_media">選択した項目を削除しますか ?</string>
+ <string name="confirm_delete_media">選択した項目を削除しますか ?</string>
+ <string name="cab_selected">%d件選択</string>
+ <string name="media_gallery_date_range">%1$s から %2$s のメディアを表示中</string>
+ <string name="reader_empty_posts_liked">「いいね」を付けた投稿はありません。</string>
+ <string name="faq_button">よく聞かれる質問</string>
+ <string name="browse_our_faq_button">よく聞かれる質問を表示</string>
+ <string name="nux_help_description">ヘルプセンターにアクセスして一般的な質問の答えを探すか、フォーラムにアクセスして新しい質問を尋ねてください。</string>
+ <string name="agree_terms_of_service">アカウントを作成すると%1$s利用規約%2$sに合意したものとみなされます。</string>
+ <string name="create_new_blog_wpcom">WordPress.com ブログを作成</string>
+ <string name="new_blog_wpcom_created">WordPress.com ブログを作成しました。</string>
+ <string name="reader_empty_comments">コメントはまだありません</string>
+ <string name="reader_empty_posts_in_tag">このタグを使った投稿はありません</string>
+ <string name="reader_label_view_original">元の記事を表示</string>
+ <string name="reader_label_like">いいね</string>
+ <string name="reader_label_liked_by">いいね:</string>
+ <string name="reader_label_comments_closed">コメントは停止中です。</string>
+ <string name="reader_label_comments_on">次の投稿へのコメント:</string>
+ <string name="reader_title_photo_viewer">%1$d/%2$d</string>
+ <string name="error_publish_empty_post">空の投稿はできません。</string>
+ <string name="error_refresh_unauthorized_posts">投稿の表示/編集の権限がありません。</string>
+ <string name="error_refresh_unauthorized_pages">ページの表示/編集の権限がありません。</string>
+ <string name="error_refresh_unauthorized_comments">コメントの表示/編集の権限がありません。</string>
+ <string name="more">続き</string>
+ <string name="stats_no_blog">要求されたブログの統計を読み込めませんでした。</string>
+ <string name="select_a_blog">WordPress のサイトを選択してください。</string>
+ <string name="sending_content">%s のコンテンツをアップロードしています</string>
+ <string name="uploading_total">%2$d件中%1$d件目をアップロード中</string>
+ <string name="mnu_comment_liked">「いいね」済み</string>
+ <string name="comment">コメント</string>
+ <string name="comment_trashed">ゴミ箱に入れたコメント</string>
+ <string name="posts_empty_list">投稿はまだありません。新規作成してください。</string>
+ <string name="comment_reply_to_user">%s への返信</string>
+ <string name="pages_empty_list">ページはまだありません。新規作成してください。</string>
+ <string name="media_empty_list_custom_date">この期間のメディアはありません。</string>
+ <string name="posting_post">"%s" を投稿</string>
+ <string name="signing_out">ログアウト中…</string>
+ <string name="reader_label_comment_count_multi">コメント%,d件</string>
+ <string name="reader_label_comment_count_single">1件のコメント</string>
+ <string name="older_month">1ヶ月以上前</string>
+ <string name="older_two_days">2日以上前</string>
+ <string name="older_last_week">1週間以上前</string>
+ <string name="reader_empty_followed_blogs_title">フォローしているサイトがまだありません</string>
+ <string name="reader_toast_blog_blocked">このブログからの投稿は今後表示されません</string>
+ <string name="reader_menu_block_blog">このブログをブロック</string>
+ <string name="reader_toast_err_block_blog">ブログをブロックできませんでした</string>
+ <string name="reader_toast_err_generic">操作を実行できませんでした</string>
+ <string name="contact_us">お問い合わせ</string>
+ <string name="hs__conversation_detail_error">発生している問題を説明してください</string>
+ <string name="hs__new_conversation_header">サポートチャット</string>
+ <string name="hs__conversation_header">サポートチャット</string>
+ <string name="hs__username_blank_error">正しいお名前を入力してください</string>
+ <string name="hs__invalid_email_error">正しいメールアドレスを入力してください</string>
+ <string name="add_location">位置情報を追加</string>
+ <string name="current_location">現在の位置</string>
+ <string name="search_location">検索</string>
+ <string name="edit_location">編集</string>
+ <string name="search_current_location">位置情報取得</string>
+ <string name="preference_send_usage_stats">統計情報を送信</string>
+ <string name="preference_send_usage_stats_summary">WordPress for Android を改善するため、利用統計情報を自動的に送信する</string>
+ <string name="update_verb">更新</string>
+ <string name="schedule_verb">予約</string>
+ <string name="reader_label_tag_preview">%s タグの付いた投稿</string>
+ <string name="reader_label_followed_blog">ブログをフォローしました</string>
+ <string name="reader_toast_err_already_follow_blog">このブログをすでにフォローしています</string>
+ <string name="reader_toast_err_unfollow_blog">このブログをフォローできませんでした</string>
+ <string name="reader_toast_err_get_blog_info">このブログを表示できません</string>
+ <string name="reader_title_subs">タグ・ブログ</string>
+ <string name="reader_toast_err_follow_blog">このブログをフォローできません</string>
+ <string name="reader_page_followed_tags">フォロー中のタグ</string>
+ <string name="reader_empty_recommended_blogs">おすすめブログはありません</string>
+ <string name="reader_title_blog_preview">Reader ブログ</string>
+ <string name="reader_title_tag_preview">Reader タグ</string>
+ <string name="reader_page_followed_blogs">フォロー中のサイト</string>
+ <string name="reader_hint_add_tag_or_url">フォローしたい URL またはタグを入力</string>
+ <string name="saving">保存中…</string>
+ <string name="media_empty_list">メディアなし</string>
+ <string name="ptr_tip_message">ヒント: 再読み込みするにはプルダウン</string>
+ <string name="forgot_password">パスワードを忘れた場合</string>
+ <string name="forums">フォーラム</string>
+ <string name="help">ヘルプ</string>
+ <string name="help_center">ヘルプセンター</string>
+ <string name="ssl_certificate_error">無効な SSL 証明書</string>
+ <string name="ssl_certificate_ask_trust">通常問題なくサイトに接続できる場合、このエラーは誰かがあなたのサイトになりすましていることを意味しているかもしれないので、そのまま進むべきではありません。それでもこの証明書を信頼してもよいですか ?</string>
+ <string name="error_upload">%sをアップロードする際にエラーが発生しました</string>
+ <string name="error_downloading_image">画像ダウンロードエラー</string>
+ <string name="blog_not_found">このブログにアクセスする際にエラーが発生しました</string>
+ <string name="comments_empty_list">コメントはありません</string>
+ <string name="gallery_error">メディア項目を読み込めませんでした。</string>
+ <string name="mnu_comment_unspam">スパムはありません</string>
+ <string name="no_network_message">利用可能なネットワークがありません</string>
+ <string name="out_of_memory">デバイスのメモリが足りません</string>
+ <string name="theme_fetch_failed">テーマの取得に失敗しました</string>
+ <string name="theme_set_failed">テーマの設定に失敗しました</string>
+ <string name="adding_cat_failed">カテゴリーの追加に失敗しました</string>
+ <string name="adding_cat_success">カテゴリーを追加しました</string>
+ <string name="blog_name_cant_be_used">そのサイトアドレスは使えません</string>
+ <string name="blog_name_contains_invalid_characters">サイトアドレスに \'_\' (アンダースコア記号) を含めることはできません</string>
+ <string name="blog_name_must_be_at_least_four_characters">サイトアドレスは4文字以上にしてください。</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">サイトアドレスは半角で64文字以下にしてください</string>
+ <string name="blog_name_not_allowed">許可されていないサイトアドレスです</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">サイトアドレスには英小文字 (a-z) と数字以外は含められません</string>
+ <string name="blog_name_required">サイトのアドレスを入力してください</string>
+ <string name="cat_name_required">カテゴリー名は必須項目です</string>
+ <string name="category_automatically_renamed">カテゴリー名「%1$s」は無効です。「%2$s」に変更しました。</string>
+ <string name="email_exists">そのメールアドレスはすでに使われています</string>
+ <string name="email_invalid">有効なメールアドレスを入力してください</string>
+ <string name="email_not_allowed">そのメールアドレスでは登録できません</string>
+ <string name="error_delete_post">%sを削除する際にエラーが発生しました</string>
+ <string name="error_edit_comment">コメント編集の際にエラーが発生しました</string>
+ <string name="error_generic">エラーが発生しました</string>
+ <string name="error_load_comment">コメントを読み込めませんでした</string>
+ <string name="error_moderate_comment">承認の際にエラーが発生しました</string>
+ <string name="error_refresh_notifications">通知を再読み込みできませんでした</string>
+ <string name="invalid_email_message">無効なメールアドレスです</string>
+ <string name="invalid_password_message">パスワードは4文字以上にしてください</string>
+ <string name="invalid_username_too_long">ユーザー名は半角で61文字以下にしてください。</string>
+ <string name="invalid_username_too_short">ユーザー名は4文字以上にしてください</string>
+ <string name="no_account">WordPress アカウントが見つかりません。アカウントを追加して、もう一度お試しください。</string>
+ <string name="no_site_error">WordPress サイトに接続できませんでした</string>
+ <string name="notifications_empty_list">通知がありません</string>
+ <string name="nux_cannot_log_in">ログインできませんでした。</string>
+ <string name="passcode_wrong_passcode">不正な PIN 番号</string>
+ <string name="reply_failed">返信できませんでした</string>
+ <string name="sdcard_message">メディアをアップロードするには SD カードが必要です</string>
+ <string name="stats_bar_graph_empty">統計情報がありません</string>
+ <string name="stats_empty_comments">まだコメントがありません</string>
+ <string name="theme_auth_error_message">お使いのアカウントにテーマ設定を行う権限があるか確認してください</string>
+ <string name="username_contains_invalid_characters">ユーザー名に \'_\' (アンダースコア記号) を含めることはできません</string>
+ <string name="username_exists">このユーザー名はすでに使われています</string>
+ <string name="username_must_be_at_least_four_characters">ユーザー名は4文字以上にしてください</string>
+ <string name="username_not_allowed">使用できないユーザー名です</string>
+ <string name="username_required">ユーザー名を入力してください</string>
+ <string name="blog_name_exists">そのサイトはすでに存在しています</string>
+ <string name="blog_name_reserved">そのサイトは予約済みです</string>
+ <string name="username_or_password_incorrect">入力されたユーザー名またはパスワードが間違っています</string>
+ <string name="wait_until_upload_completes">アップロードが完了するまでお待ちください</string>
+ <string name="username_must_include_letters">ユーザー名には必ず英小文字 (a-z) を1つ以上含めてください</string>
+ <string name="error_refresh_posts">投稿を再読み込みできませんでした</string>
+ <string name="error_refresh_pages">ページを再読み込みできませんでした</string>
+ <string name="error_refresh_comments">コメントを再読み込みできませんでした</string>
+ <string name="error_refresh_stats">統計情報を再読み込みできませんでした</string>
+ <string name="username_reserved_but_may_be_available">そのユーザー名は現在予約済みですが、数日中に利用可能になるかもしれません</string>
+ <string name="blog_name_reserved_but_may_be_available">このサイト名は予約済みですが、数日中に利用可能になるかもしれません</string>
+ <string name="username_only_lowercase_letters_and_numbers">ユーザー名には英小文字 (a-z) と数字以外は含められません</string>
+ <string name="could_not_remove_account">サイトを削除できませんでした</string>
+ <string name="invalid_url_message">入力したブログ URL が正しいか確認してください</string>
+ <string name="image_settings">画像設定</string>
+ <string name="local_changes">ローカルの変更</string>
+ <string name="new_media">新規メディア</string>
+ <string name="view_site">サイトを表示</string>
+ <string name="account_details">アカウント詳細</string>
+ <string name="add_account_blog_url">ブログアドレス</string>
+ <string name="add_comment">コメントを追加</string>
+ <string name="add_new_category">新規カテゴリーを追加</string>
+ <string name="author_email">投稿者メールアドレス</string>
+ <string name="author_name">投稿者名</string>
+ <string name="author_url">投稿者 URL</string>
+ <string name="blog_name_invalid">無効なサイトアドレス</string>
+ <string name="blog_title_invalid">無効なサイトタイトル</string>
+ <string name="cancel_edit">編集をキャンセル</string>
+ <string name="category_desc">カテゴリーの説明 (オプション):</string>
+ <string name="category_name">カテゴリー名</string>
+ <string name="category_parent">親カテゴリー (オプション):</string>
+ <string name="category_slug">カテゴリースラッグ (オプション):</string>
+ <string name="comment_added">コメントを追加しました</string>
+ <string name="comment_status_approved">承認済み</string>
+ <string name="comment_status_spam">スパム</string>
+ <string name="comment_status_trash">ゴミ箱移動済み</string>
+ <string name="comment_status_unapproved">承認待ち</string>
+ <string name="connection_error">接続エラー</string>
+ <string name="content_required">コメント欄は必須項目です</string>
+ <string name="create_a_link">リンクを作成</string>
+ <string name="delete_draft">下書きを削除</string>
+ <string name="delete_page">固定ページを削除</string>
+ <string name="delete_post">投稿を削除</string>
+ <string name="dlg_approving_comments">承認中</string>
+ <string name="dlg_confirm_trash_comments">ゴミ箱に移動しますか ?</string>
+ <string name="dlg_spamming_comments">スパムとしてマーク中</string>
+ <string name="dlg_trashing_comments">ゴミ箱に移動中</string>
+ <string name="dlg_unapproving_comments">承認解除中</string>
+ <string name="edit_comment">コメント編集</string>
+ <string name="edit_post">投稿を編集</string>
+ <string name="email_hint">メールアドレス</string>
+ <string name="email_reserved">そのメールアドレスはすでに使われています。有効化メールが届いていないかチェックしてみてください。有効化しなかった場合、数日後に同じアドレスでもう一度登録できるようになります。</string>
+ <string name="file_error_create">メディアアップロードの際に一時ファイルを作成できませんでした。デバイス上に空き容量があるか確認してください。</string>
+ <string name="file_not_found">アップロードするメディアファイルが見つかりませんでした。削除または移動されたようです。</string>
+ <string name="hint_comment_content">コメント</string>
+ <string name="horizontal_alignment">横配置</string>
+ <string name="http_authorization_required">認証が必要です</string>
+ <string name="http_credentials">HTTP ログイン情報 (オプション)</string>
+ <string name="learn_more">さらに詳しく</string>
+ <string name="link_enter_url_text">リンク​​テキスト (オプション)</string>
+ <string name="local_draft">ローカルの下書き</string>
+ <string name="location_not_found">位置情報が見つかりません</string>
+ <string name="media_gallery_edit">ギャラリーを編集</string>
+ <string name="media_gallery_image_order">画像の順序</string>
+ <string name="media_gallery_num_columns">カラム数</string>
+ <string name="media_gallery_settings_title">ギャラリー設定</string>
+ <string name="media_gallery_type_thumbnail_grid">サムネイルグリッド</string>
+ <string name="mnu_comment_approve">承認</string>
+ <string name="mnu_comment_spam">スパム</string>
+ <string name="mnu_comment_trash">ゴミ箱</string>
+ <string name="mnu_comment_unapprove">承認を解除</string>
+ <string name="new_post">新規投稿</string>
+ <string name="open_source_licenses">オープンソースライセンス</string>
+ <string name="page_not_published">このページはまだ公開されていません</string>
+ <string name="page_settings">ページ設定</string>
+ <string name="pending_review">レビュー待ち</string>
+ <string name="post_format">投稿フォーマット</string>
+ <string name="post_not_published">この投稿はまだ公開されていません</string>
+ <string name="post_settings">投稿設定</string>
+ <string name="preview_page">ページをプレビュー</string>
+ <string name="preview_post">投稿をプレビュー</string>
+ <string name="privacy_policy">個人情報保護方針</string>
+ <string name="reader_share_link">リンクを共有</string>
+ <string name="reader_title_applog">アプリケーションログ</string>
+ <string name="reader_toast_err_add_tag">このブログを追加できません</string>
+ <string name="reader_toast_err_remove_tag">このタグを削除できません</string>
+ <string name="required_field">記入必須欄</string>
+ <string name="saving_changes">変更を保存中</string>
+ <string name="scaled_image_error">正しい画像幅を入力してください</string>
+ <string name="select_categories">カテゴリーを選択</string>
+ <string name="share_action_media">メディアギャラリー</string>
+ <string name="share_action_post">新規投稿</string>
+ <string name="site_address">インストール型 WordPress のアドレス (URL)</string>
+ <string name="sure_to_cancel_edit_comment">このコメントの修正を中止しますか ?</string>
+ <string name="theme_current_theme">現在のテーマ</string>
+ <string name="theme_premium_theme">プレミアムテーマ</string>
+ <string name="themes_live_preview">ライブプレビュー</string>
+ <string name="toast_comment_unedited">コメントが変更されていません</string>
+ <string name="trash">ゴミ箱</string>
+ <string name="trash_no">ゴミ箱に移動しない</string>
+ <string name="trash_yes">ゴミ箱に移動</string>
+ <string name="upload_failed">アップロード失敗</string>
+ <string name="view_in_browser">ブラウザで表示</string>
+ <string name="wordpress_blog">WordPress ブログ</string>
+ <string name="xmlrpc_error">接続できませんでした。サイト上の xmlrpc.php へのフルパスを入力し、もう一度お試しください。</string>
+ <string name="fatal_db_error">アプリのデータベースを作成する際にエラーが発生しました。アプリをもう一度インストールしてみてください。</string>
+ <string name="jetpack_message_not_admin">統計情報を表示するには Jetpack プラグインが必要です。サイト管理者にご連絡ください。</string>
+ <string name="post_not_found">投稿を読み込む際にエラーが発生しました。再読み込みしてもう一度お試しください。</string>
+ <string name="media_error_no_permission">メディアライブラリを表示する権限がありません</string>
+ <string name="cannot_delete_multi_media_items">一部のメディアは現在削除できません。後ほどもう一度お試しください。</string>
+ <string name="error_blog_hidden">このブログは非表示になっており読み込めません。設定画面から再有効化してもう一度お試しください。</string>
+ <string name="blog_name_must_include_letters">サイトアドレスには必ず英小文字 (a-z) を1つ以上含めてください</string>
+ <string name="email_cant_be_used_to_signup">こちらから送信するメールがブロックされる問題が発生しているため、そのメールアドレスは登録の際にお使いいただけません。他のメールプロバイダをご利用ください。</string>
+ <string name="blog_removed_successfully">サイトを削除しました</string>
+ <string name="remove_account">サイトを削除</string>
+ <string name="notifications_empty_all">通知はまだありません。</string>
+ <string name="invalid_site_url_message">入力したサイト URL が正しいか確認してください</string>
+ <string name="deleting_page">ページを削除中</string>
+ <string name="deleting_post">投稿を削除中</string>
+ <string name="share_link">リンクを共有</string>
+ <string name="share_url_post">投稿を共有</string>
+ <string name="share_url_page">ページを共有</string>
+ <string name="creating_your_site">サイトを作成しています</string>
+ <string name="creating_your_account">アカウントを作成しています</string>
+ <string name="reader_empty_posts_in_tag_updating">投稿を取得中…</string>
+ <string name="error_refresh_media">メディアライブラリを再読込する際にエラーが発生しました。後ほどもう一度お試しください。</string>
+ <string name="comment_spammed">コメントをスパムとしてマークしました</string>
+ <string name="download">メディアをダウンロード中</string>
+ <string name="reader_label_reply">返信</string>
+ <string name="video">動画</string>
+ <string name="reader_toast_err_get_comment">このコメントを読み込めません</string>
+ <string name="cant_share_no_visible_blog">公開ブログがないため WordPress で共有できません</string>
+ <string name="reader_likes_you_and_multi">あなたと%,d人が「いいね」をしています。</string>
+ <string name="reader_likes_multi">%,d 人が「いいね」しています。</string>
+ <string name="pick_photo">画像を選択</string>
+ <string name="pick_video">動画を選択</string>
+ <string name="select_date">日付を選択</string>
+ <string name="select_time">時間を選択</string>
+ <string name="validating_site_data">サイトのデータをチェックしています</string>
+ <string name="validating_user_data">ユーザーのデータをチェックしています</string>
+ <string name="reader_toast_err_get_post">この投稿を読み込みできません</string>
+ <string name="reader_likes_you_and_one">あなたと他1人が「いいね」をつけました</string>
+ <string name="account_two_step_auth_enabled">このアカウントでは2段階認証が有効化されています。WordPress.com のセキュリティ設定ページでアプリケーション用パスワードを生成してください。</string>
+ <string name="reader_empty_followed_blogs_description">右上のタグアイコンをタップし、お気に入りを発見してみてください。</string>
+ <string name="nux_add_selfhosted_blog">インストール型 WordPress ブログを追加</string>
+ <string name="nux_oops_not_selfhosted_blog">WordPress.com にログイン</string>
+ <string name="nux_tap_continue">次へ</string>
+ <string name="nux_welcome_create_account">アカウントを作成</string>
+ <string name="password_invalid">セキュリティ強度の高いパスワードが必要です。7文字以上の大文字・小文字・数字・記号を含むものにすることをおすすめします。</string>
+ <string name="signing_in">ログイン中…</string>
+ <string name="nux_tutorial_get_started_title">始めてみましょう</string>
+ <string name="media_add_popup_title">メディアライブラリに追加</string>
+ <string name="media_add_new_media_gallery">ギャラリーを作成</string>
+ <string name="empty_list_default">リストが空です</string>
+ <string name="select_from_media_library">メディアライブラリから選択</string>
+ <string name="reader_untitled_post">(無題)</string>
+ <string name="reader_btn_share">共有</string>
+ <string name="reader_btn_follow">フォロー</string>
+ <string name="reader_btn_unfollow">フォロー中</string>
+ <string name="reader_label_added_tag">%s を追加</string>
+ <string name="reader_label_removed_tag">%s を削除</string>
+ <string name="reader_likes_only_you">「いいね」をつけました</string>
+ <string name="username_invalid">無効なユーザー名</string>
+ <string name="reader_share_subject">%s から共有</string>
+ <string name="jetpack_not_found">Jetpack プラグインが見つかりません</string>
+ <string name="reader_toast_err_view_image">画像を表示できません</string>
+ <string name="reader_toast_err_tag_exists">すでにこのタグをフォロー中です</string>
+ <string name="reader_toast_err_tag_invalid">有効なタグではありません</string>
+ <string name="reader_toast_err_share_intent">共有に失敗しました</string>
+ <string name="reader_toast_err_url_intent">%sを開けませんでした</string>
+ <string name="reader_empty_followed_tags">現在何もタグをフォローしていません</string>
+ <string name="create_account_wpcom">WordPress.com でアカウントを作成</string>
+ <string name="jetpack_message">統計情報を表示するには Jetpack プラグインが必要です。Jetpack をインストールしますか ?</string>
+ <string name="reader_likes_one">1人がいいねをつけました</string>
+ <string name="reader_toast_err_comment_failed">コメントを投稿できませんでした</string>
+ <string name="connecting_wpcom">WordPress.com に接続中</string>
+ <string name="limit_reached">上限に達しました。1分後にもう一度お試しいただけます。それより前にログインしようとすると、制限解除までの時間がさらに長くなります。エラーだと思われる場合はサポートにご連絡ください。</string>
+ <string name="reader_hint_comment_on_comment">コメントに返信…</string>
+ <string name="button_next">次へ</string>
+ <string name="themes">テーマ</string>
+ <string name="images">画像</string>
+ <string name="media_gallery_image_order_random">ランダム</string>
+ <string name="media_gallery_type_squares">正方形タイル</string>
+ <string name="media_gallery_type_tiled">タイルモザイク</string>
+ <string name="media_gallery_type_circles">丸型</string>
+ <string name="media_gallery_image_order_reverse">順序を逆にする</string>
+ <string name="media_gallery_type">表示形式</string>
+ <string name="media_gallery_type_slideshow">スライドショー</string>
+ <string name="media_edit_title_text">タイトル</string>
+ <string name="media_edit_caption_text">キャプション</string>
+ <string name="media_edit_description_text">説明</string>
+ <string name="theme_activate_button">有効化</string>
+ <string name="share_action">共有</string>
+ <string name="stats">統計</string>
+ <string name="stats_view_clicks">クリック数</string>
+ <string name="all">すべて</string>
+ <string name="unattached">未使用</string>
+ <string name="media_edit_title_hint">タイトルを入力</string>
+ <string name="stats_timeframe_today">今日</string>
+ <string name="stats_timeframe_yesterday">昨日</string>
+ <string name="stats_timeframe_days">日</string>
+ <string name="stats_timeframe_weeks">週</string>
+ <string name="stats_timeframe_months">月</string>
+ <string name="stats_entry_country">国名</string>
+ <string name="stats_entry_posts_and_pages">タイトル</string>
+ <string name="stats_entry_tags_and_categories">トピック</string>
+ <string name="stats_entry_authors">投稿者</string>
+ <string name="stats_entry_referrers">リファラ</string>
+ <string name="stats_totals_views">表示</string>
+ <string name="stats_totals_clicks">クリック数</string>
+ <string name="stats_totals_plays">再生回数</string>
+ <string name="media_add_popup_capture_photo">写真を撮影</string>
+ <string name="media_add_popup_capture_video">動画を撮影</string>
+ <string name="media_edit_success">更新しました</string>
+ <string name="themes_details_label">詳細</string>
+ <string name="themes_features_label">機能</string>
+ <string name="stats_view_referrers">リファラ</string>
+ <string name="media_edit_failure">更新に失敗しました</string>
+ <string name="media_edit_description_hint">説明を入力</string>
+ <string name="media_edit_caption_hint">キャプションを入力</string>
+ <string name="passcode_preference_title">PIN 番号ロック</string>
+ <string name="passcode_turn_on">PIN 番号ロックを有効化</string>
+ <string name="passcode_turn_off">PIN 番号ロックを無効化</string>
+ <string name="passcode_set">PIN 番号変更</string>
+ <string name="passcode_re_enter_passcode">PIN 番号再入力</string>
+ <string name="passcode_change_passcode">PIN 番号変更</string>
+ <string name="passcode_enter_old_passcode">旧 PIN 番号を入力</string>
+ <string name="passcode_enter_passcode">PIN 番号を入力</string>
+ <string name="passcode_manage">PIN 番号ロック管理</string>
+ <string name="stats_view_tags_and_categories">タグとカテゴリー</string>
+ <string name="stats_view_visitors_and_views">訪問者数・表示回数</string>
+ <string name="post_excerpt">抜粋</string>
+ <string name="theme_activating_button">有効化中</string>
+ <string name="theme_set_success">テーマの設定を完了しました。</string>
+ <string name="share_action_title">追加先</string>
+ <string name="theme_auth_error_title">テーマ情報の取得に失敗しました</string>
+ <string name="custom_date">カスタム日付</string>
+ <string name="upload">アップロード</string>
+ <string name="discard">削除</string>
+ <string name="sign_in">ログイン</string>
+ <string name="notifications">通知</string>
+ <string name="note_reply_successful">返信を公開しました</string>
+ <string name="follows">フォロー</string>
+ <string name="more_notifications">あと%d件。</string>
+ <string name="new_notifications">%d件の新しい通知</string>
+ <string name="loading">読込中…</string>
+ <string name="httpuser">HTTP ユーザー名</string>
+ <string name="httppassword">HTTP パスワード</string>
+ <string name="error_media_upload">メディアをアップロードする際にエラーが発生しました</string>
+ <string name="post_content">コンテンツ (タップしてテキスト・メディアを追加)</string>
+ <string name="publish_date">公開</string>
+ <string name="content_description_add_media">メディアを追加</string>
+ <string name="incorrect_credentials">ユーザー名またはパスワードが正しくありません。</string>
+ <string name="password">パスワード</string>
+ <string name="username">ユーザー名</string>
+ <string name="reader">購読ブログ</string>
+ <string name="featured">アイキャッチ画像として使用</string>
+ <string name="featured_in_post">投稿本文中に画像を含める</string>
+ <string name="no_network_title">利用可能なネットワークがありません</string>
+ <string name="pages">ページ</string>
+ <string name="caption">キャプション (オプション)</string>
+ <string name="width">横幅</string>
+ <string name="page">ページ</string>
+ <string name="posts">投稿</string>
+ <string name="anonymous">匿名</string>
+ <string name="post">投稿</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="scaled_image">変更後の画像幅</string>
+ <string name="upload_scaled_image">アップロードしてサイズを変更した画像にリンク</string>
+ <string name="scheduled">予約済み</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">アップロード中…</string>
+ <string name="version">バージョン</string>
+ <string name="tos">利用規約</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">標準の画像幅</string>
+ <string name="image_alignment">配置</string>
+ <string name="refresh">再読み込み</string>
+ <string name="untitled">タイトル未設定</string>
+ <string name="edit">編集</string>
+ <string name="post_id">投稿</string>
+ <string name="page_id">固定ページ</string>
+ <string name="post_password">パスワード (オプション)</string>
+ <string name="immediately">すぐに</string>
+ <string name="quickpress_add_alert_title">ショートカット名を設定する</string>
+ <string name="settings">設定</string>
+ <string name="today">今日</string>
+ <string name="share_url">URLを共有</string>
+ <string name="quickpress_window_title">QuickPress ショートカット用のブログを選択</string>
+ <string name="quickpress_add_error">ショートカット名を記入してください</string>
+ <string name="publish_post">公開</string>
+ <string name="draft">下書き</string>
+ <string name="post_private">非公開</string>
+ <string name="upload_full_size_image">アップロードしてフルサイズの画像にリンクする</string>
+ <string name="title">タイトル</string>
+ <string name="tags_separate_with_commas">タグ (コンマ区切り)</string>
+ <string name="categories">カテゴリー</string>
+ <string name="dlg_deleting_comments">コメントを削除中</string>
+ <string name="notification_blink">通知ライトを点滅</string>
+ <string name="notification_vibrate">バイブ</string>
+ <string name="notification_sound">通知音</string>
+ <string name="status">状態</string>
+ <string name="location">場所</string>
+ <string name="sdcard_title">SD カードが必要です</string>
+ <string name="select_video">ギャラリーからビデオを選択する</string>
+ <string name="media">メディア</string>
+ <string name="delete">削除</string>
+ <string name="none">なし</string>
+ <string name="blogs">ブログ</string>
+ <string name="select_photo">ギャラリーから写真を選択</string>
+ <string name="reply">返信する</string>
+ <string name="preview">プレビュー</string>
+ <string name="on">オン</string>
+ <string name="cancel">キャンセル</string>
+ <string name="save">保存</string>
+ <string name="add">追加</string>
+ <string name="yes">はい</string>
+ <string name="no">いいえ</string>
+ <string name="error">エラー</string>
+ <string name="category_refresh_error">カテゴリーの再読み込みエラー</string>
+ <string name="notification_settings">通知設定</string>
+</resources>
diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml
new file mode 100644
index 000000000..237176ceb
--- /dev/null
+++ b/WordPress/src/main/res/values-ko/strings.xml
@@ -0,0 +1,1136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">관리자</string>
+ <string name="role_editor">편집자</string>
+ <string name="role_author">글쓴이</string>
+ <string name="role_contributor">공동 작업자</string>
+ <string name="role_follower">팔로워</string>
+ <string name="role_viewer">독자</string>
+ <string name="error_post_my_profile_no_connection">인터넷에 연결되어 있지 않아 프로필을 저장할 수 없습니다.</string>
+ <string name="alignment_none">없음</string>
+ <string name="alignment_left">왼쪽</string>
+ <string name="alignment_right">오른쪽</string>
+ <string name="site_settings_list_editor_action_mode_title">%1$d 선택</string>
+ <string name="error_fetch_users_list">사이트 사용자를 가져올 수 없습니다.</string>
+ <string name="plans_manage">요금제 관리:\n워드프레스닷컴/요금제</string>
+ <string name="people_empty_list_filtered_viewers">아직 방문자가 없습니다.</string>
+ <string name="people_fetching">사용자를 가져오는 중…</string>
+ <string name="title_follower">팔로워</string>
+ <string name="title_email_follower">이메일 팔로워</string>
+ <string name="people_empty_list_filtered_email_followers">아직 이메일 팔로워가 없습니다.</string>
+ <string name="people_empty_list_filtered_followers">아직 팔로워가 없습니다.</string>
+ <string name="people_empty_list_filtered_users">아직 사용자가 없습니다.</string>
+ <string name="people_dropdown_item_email_followers">이메일 팔로워</string>
+ <string name="people_dropdown_item_viewers">방문자</string>
+ <string name="people_dropdown_item_followers">팔로워</string>
+ <string name="people_dropdown_item_team">팀</string>
+ <string name="invite_message_usernames_limit">최대 10개의 이메일 주소 및/또는 워드프레스닷컴 사용자명을 초대하세요. 사용자명이 필요한 사람들은 사용자명 생성 방법에 관한 지침을 받게 됩니다.</string>
+ <string name="viewer_remove_confirmation_message">이 방문자를 제거하면 해당 방문자가 이 사이트에 방문할 수 없게 됩니다.\n\n이 방문자를 제거하시겠습니까?</string>
+ <string name="follower_remove_confirmation_message">이 팔로워가 제거된 경우 해당 팔로워는 다시 팔로우하지 않는 한 이 사이트에 대한 알림을 수신할 수 없게 됩니다.\n\n이 팔로워를 제거하시겠습니까?</string>
+ <string name="follower_subscribed_since">%1$s 이후</string>
+ <string name="reader_label_view_gallery">갤러리 보기</string>
+ <string name="error_remove_follower">팔로워를 제거할 수 없습니다.</string>
+ <string name="error_remove_viewer">방문자를 제거할 수 없습니다.</string>
+ <string name="error_fetch_email_followers_list">사이트 이메일 팔로워를 가져올 수 없습니다.</string>
+ <string name="error_fetch_followers_list">사이트 팔로워를 가져올 수 없습니다.</string>
+ <string name="editor_failed_uploads_switch_html">일부 미디어 업로드에 실패했습니다. 이 상태에서는 HTML 모드로 전환할 수\n 없습니다. 실패한 업로드를 모두 제거하고 계속할까요?</string>
+ <string name="format_bar_description_html">HTML 모드</string>
+ <string name="visual_editor">비주얼 편집기</string>
+ <string name="image_thumbnail">이미지 썸네일</string>
+ <string name="format_bar_description_ul">순서 없는 목록</string>
+ <string name="format_bar_description_ol">순서 있는 목록</string>
+ <string name="format_bar_description_more">추가 삽입</string>
+ <string name="format_bar_description_media">미디어 삽입</string>
+ <string name="format_bar_description_link">링크 삽입</string>
+ <string name="format_bar_description_strike">취소선</string>
+ <string name="format_bar_description_quote">인용 차단</string>
+ <string name="format_bar_description_italic">이탤릭</string>
+ <string name="format_bar_description_underline">밑줄</string>
+ <string name="image_settings_save_toast">변경사항 저장됨</string>
+ <string name="image_caption">캡션</string>
+ <string name="image_alt_text">대체 텍스트</string>
+ <string name="image_link_to">링크 연결 대상:</string>
+ <string name="image_width">가로</string>
+ <string name="format_bar_description_bold">굵게</string>
+ <string name="image_settings_dismiss_dialog_title">저장되지 않은 변경 사항을 취소할까요?</string>
+ <string name="stop_upload_dialog_title">업로드를 중지할까요?</string>
+ <string name="stop_upload_button">업로드 중지</string>
+ <string name="alert_error_adding_media">미디어를 삽입하는 동안 오류가 발생했습니다.</string>
+ <string name="alert_action_while_uploading">미디어를 업로드 중입니다. 완료될 때까지 기다려 주세요.</string>
+ <string name="alert_insert_image_html_mode">HTML 모드에서 직접 미디어를 삽입할 수 없습니다. 비주얼 모드로 전환하세요.</string>
+ <string name="uploading_gallery_placeholder">갤러리 업데이트 중...</string>
+ <string name="invite_sent">초대를 전송했습니다.</string>
+ <string name="tap_to_try_again">눌러서 다시 시도하세요!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_message_info">(선택사항) 사용자 초대에 포함될 사용자 정의 메시지를 500자까지 입력할 수 있습니다.</string>
+ <string name="invite_message_remaining_other">%d자 남음</string>
+ <string name="invite_message_remaining_one">1자 남음</string>
+ <string name="invite_message_remaining_zero">0자 남음</string>
+ <string name="invite_invalid_email">이메일 주소 \'%s\'이(가) 유효하지 않음</string>
+ <string name="invite_message_title">사용자 정의 메시지</string>
+ <string name="invite_already_a_member">사용자명이 \'%s\'인 회원이 이미 있습니다.</string>
+ <string name="invite_username_not_found">사용자명이 \'%s\'인 사용자를 찾을 수 없습니다.</string>
+ <string name="invite">초대</string>
+ <string name="invite_names_title">사용자명 또는 이메일</string>
+ <string name="signup_succeed_signin_failed">회원님의 계정이 생성되었으나 로그인하는 동안 오류가\n 발생했습니다. 새로 만든 사용자명 및 비밀번호로 로그인해 보세요.</string>
+ <string name="send_link">링크 전송</string>
+ <string name="my_site_header_external">외부</string>
+ <string name="invite_people">사용자 초대</string>
+ <string name="label_clear_search_history">검색 내역 지우기</string>
+ <string name="dlg_confirm_clear_search_history">검색 내역을 지울까요?</string>
+ <string name="reader_empty_posts_in_search_description">사용자 언어로 된 %s에 대한 글을 찾을 수 없습니다.</string>
+ <string name="reader_label_post_search_running">검색 중...</string>
+ <string name="reader_label_related_posts">관련 읽을거리</string>
+ <string name="reader_empty_posts_in_search_title">글을 찾을 수 없습니다.</string>
+ <string name="reader_label_post_search_explainer">모든 공개 워드프레스닷컴 블로그 검색</string>
+ <string name="reader_hint_post_search">워드프레스닷컴 검색</string>
+ <string name="reader_title_related_post_detail">관련 글</string>
+ <string name="reader_title_search_results">%s 검색</string>
+ <string name="preview_screen_links_disabled">미리보기 화면에서 링크가 비활성화되어 있습니다.</string>
+ <string name="draft_explainer">이 글은 아직 발행되지 않은 임시글입니다.</string>
+ <string name="send">보내기</string>
+ <string name="person_remove_confirmation_title">%1$s 제거</string>
+ <string name="reader_empty_posts_in_custom_list">이 목록에 있는 사이트에서 최근 게시된 글이 없습니다.</string>
+ <string name="people">사람</string>
+ <string name="edit_user">사용자 편집</string>
+ <string name="role">역할</string>
+ <string name="error_remove_user">사용자를 제거할 수 없습니다.</string>
+ <string name="error_update_role">사용자 역할을 업데이트할 수 없습니다.</string>
+ <string name="gravatar_camera_and_media_permission_required">사진을 선택하거나 캡처하려면 권한이 필요합니다.</string>
+ <string name="error_updating_gravatar">Gravatar를 업데이트하는 중 오류가 발생했습니다.</string>
+ <string name="error_locating_image">잘린 이미지를 찾는 중 오류가 발생했습니다.</string>
+ <string name="error_refreshing_gravatar">Gravatar를 다시 로드하는 중 오류가 발생했습니다.</string>
+ <string name="gravatar_tip">새로운 기능 Gravatar를 변경하려면 탭하세요.</string>
+ <string name="error_cropping_image">이미지를 자르는 중 오류가 발생했습니다.</string>
+ <string name="launch_your_email_app">이메일 앱 시작</string>
+ <string name="checking_email">이메일 확인 중</string>
+ <string name="not_on_wordpress_com">워드프레스닷컴을 사용하고 있지 않습니까?</string>
+ <string name="magic_link_unavailable_error_message">현재 사용할 수 없습니다. 비밀번호를 입력하세요.</string>
+ <string name="check_your_email">이메일을 확인하세요.</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">바로 로그인하려면 이메일로 링크를 받으세요.</string>
+ <string name="logging_in">로그인 중</string>
+ <string name="enter_your_password_instead">비밀번호를 대신 입력하세요.</string>
+ <string name="web_address_dialog_hint">댓글을 달 때 공개적으로 표시합니다.</string>
+ <string name="jetpack_not_connected_message">젯팩 플러그인이 설치되었지만 워드프레스닷컴에 연결되지 않았습니다. 젯팩을 연결하시겠습니까?</string>
+ <string name="username_email">이메일 또는 사용자명</string>
+ <string name="jetpack_not_connected">젯팩 플러그인이 연결되지 않았습니다.</string>
+ <string name="new_editor_reflection_error">비주얼 편집기가 장치와 호환되지 않습니다. 자동으로\n 사용 중지되었습니다.</string>
+ <string name="stats_insights_latest_post_no_title">(제목 없음)</string>
+ <string name="capture_or_pick_photo">사진 캡처 또는 선택</string>
+ <string name="plans_post_purchase_text_themes">이제 프리미엄 테마에 무제한으로 접근할 수 있습니다. 시작하려면 아무 테마나 미리보기하시면 됩니다.</string>
+ <string name="plans_post_purchase_button_themes">테마 찾아보기</string>
+ <string name="plans_post_purchase_title_themes">완벽한 프리미엄 테마 찾기</string>
+ <string name="plans_post_purchase_button_video">새 글 시작</string>
+ <string name="plans_post_purchase_text_video">확장된 미디어 저장 공간과 함께 비디오프레스를 통해 사이트에 비디오를 업로드하고 호스팅할 수 있습니다.</string>
+ <string name="plans_post_purchase_title_video">비디오를 통해 글을 생동감 있게 구현</string>
+ <string name="plans_post_purchase_button_customize">사이트 수정하기</string>
+ <string name="plans_post_purchase_text_customize">이제 사용자 정의 글꼴, 사용자 정의 색상 및 사용자 정의 CSS 편집 기능에 액세스할 수 있습니다.</string>
+ <string name="plans_post_purchase_title_customize">글꼴과 색상 수정하기</string>
+ <string name="plans_post_purchase_text_intro">사이트를 마음껏 이용하세요! 이제 사이트의 새로운 기능을 살펴보고 시작하려는 위치를 선택하세요.</string>
+ <string name="plans_post_purchase_title_intro">구매하셨습니다. 지금 사용해보세요.</string>
+ <string name="export_your_content_message">글, 페이지, 설정을 %s로 발송할 것입니다.</string>
+ <string name="plan">계획</string>
+ <string name="plans">계획</string>
+ <string name="plans_loading_error">요금제를 로드할 수 없음</string>
+ <string name="export_your_content">콘텐츠 내보내기</string>
+ <string name="exporting_content_progress">콘텐츠를 내보내는 중…</string>
+ <string name="export_email_sent">전송된 이메일을 내보내세요!</string>
+ <string name="premium_upgrades_message">회원님의 사이트에서 프리미엄 업그레이드가 활성화되었습니다. 사이트를 삭제하기 전에 업그레이드를 취소하세요.</string>
+ <string name="show_purchases">구매 항목 표시</string>
+ <string name="checking_purchases">구매 항목 확인 중</string>
+ <string name="premium_upgrades_title">프리미엄 업그레이드</string>
+ <string name="purchases_request_error">문제가 생겼습니다. 구매를 요청할 수 없습니다.</string>
+ <string name="delete_site_progress">사이트를 삭제하는 중…</string>
+ <string name="delete_site_summary">이 작업은 되돌릴 수 없습니다. 사이트를 삭제하면 사이트의 모든 콘텐츠, 공동 작업자 및 도메인도 제거됩니다.</string>
+ <string name="delete_site_hint">사이트 삭제</string>
+ <string name="export_site_hint">사이트를 XML 파일로 내보내기</string>
+ <string name="are_you_sure">확실합니까?</string>
+ <string name="export_site_summary">확실한 경우 시간을 내서 지금 콘텐츠를 내보내세요. 나중에 복구할 수 없습니다.</string>
+ <string name="keep_your_content">콘텐츠 유지</string>
+ <string name="domain_removal_hint">사이트를 제거하면 도메인이 작동하지 않게 됩니다.</string>
+ <string name="domain_removal_summary">조심하세요! 사이트를 삭제하면 아래 나열된 도메인도 제거됩니다.</string>
+ <string name="primary_domain">주 도메인</string>
+ <string name="domain_removal">도메인 삭제</string>
+ <string name="error_deleting_site_summary">사이트를 삭제하는 동안 오류가 발생했습니다. 도움이 필요하면 사용자지원에 요청하세요.</string>
+ <string name="error_deleting_site">사이트 삭제 오류</string>
+ <string name="confirm_delete_site_prompt">확인을 위해 아래 칸에 %1$s를 입력하세요. 사이트가 영원히 사라질 것입니다.</string>
+ <string name="site_settings_export_content_title">내용 내보내기</string>
+ <string name="confirm_delete_site">사이트 삭제 확인</string>
+ <string name="contact_support">지원팀에 문의</string>
+ <string name="start_over_text">사이트를 유지하되 현재 글과 페이지를 원하지 않는 경우 지원팀에서 글, 페이지, 미디어, 댓글을 삭제해드립니다.\n\n사이트와 URL은 활성 상태를 유지하지만 콘텐츠 생성을 처음부터 시작할 수 있게 됩니다. 현재 콘텐츠를 삭제하려면 지원팀에 문의해주세요.</string>
+ <string name="site_settings_start_over_hint">사이트 시작하기</string>
+ <string name="let_us_help">Let Us Help</string>
+ <string name="me_btn_app_settings">앱 설정</string>
+ <string name="start_over">시작하기</string>
+ <string name="editor_remove_failed_uploads">실패한 업로드 제거</string>
+ <string name="editor_toast_failed_uploads">일부 미디어 업로드에 실패했습니다. 이 상태의 글은 저장하거나 게시할 수\n 없습니다. 실패한 미디어를 모두 제거하시겠습니까?</string>
+ <string name="comments_empty_list_filtered_trashed">삭제된 댓글 없음</string>
+ <string name="site_settings_advanced_header">고급</string>
+ <string name="comments_empty_list_filtered_pending">대기중인 댓글 없음</string>
+ <string name="comments_empty_list_filtered_approved">승인한 댓글 없음</string>
+ <string name="button_done">완료</string>
+ <string name="button_skip">넘어가기</string>
+ <string name="site_timeout_error">시간이 초과되어 워드프레스 사이트에 접속할 수 없습니다.</string>
+ <string name="xmlrpc_malformed_response_error">연결할 수 없습니다. 워드프레스 설치 프로그램이 잘못된 XML-RPC 문서를 보냈습니다.</string>
+ <string name="xmlrpc_missing_method_error">연결할 수 없습니다. 필수 XML-RPC 함수가 서버에 없습니다.</string>
+ <string name="post_format_status">상태</string>
+ <string name="post_format_video">비디오</string>
+ <string name="theme_free">무료</string>
+ <string name="theme_all">모두</string>
+ <string name="theme_premium">프리미엄</string>
+ <string name="post_format_chat">채팅</string>
+ <string name="post_format_gallery">갤러리</string>
+ <string name="post_format_image">이미지</string>
+ <string name="post_format_link">링크</string>
+ <string name="post_format_quote">인용</string>
+ <string name="post_format_standard">기본</string>
+ <string name="notif_events">워드프레스닷컴 강의 및 이벤트에 대한 정보(온라인 및 직접 만남).</string>
+ <string name="post_format_aside">추가 정보</string>
+ <string name="post_format_audio">오디오</string>
+ <string name="notif_surveys">워드프레스닷컴 리서치 및 설문 조사에 참여할 기회.</string>
+ <string name="notif_tips">워드프레스닷컴을 최대한 활용하기 위한 팁.</string>
+ <string name="notif_community">커뮤니티</string>
+ <string name="replies_to_my_comments">내 댓글에 대한 답글</string>
+ <string name="notif_suggestions">제안</string>
+ <string name="notif_research">리서치</string>
+ <string name="site_achievements">사이트 작성 글</string>
+ <string name="username_mentions">사용자명 멘션</string>
+ <string name="likes_on_my_posts">내 글의 좋아요</string>
+ <string name="site_follows">사이트 팔로우</string>
+ <string name="likes_on_my_comments">내 댓글의 좋아요</string>
+ <string name="comments_on_my_site">내 사이트의 댓글</string>
+ <string name="site_settings_list_editor_summary_other">%d개 항목</string>
+ <string name="site_settings_list_editor_summary_one">1개의 항목</string>
+ <string name="approve_auto_if_previously_approved">알려진 사용자의 댓글</string>
+ <string name="approve_auto">모든 사용자</string>
+ <string name="approve_manual">댓글 없음</string>
+ <string name="site_settings_paging_summary_other">페이지당 댓글 %d개</string>
+ <string name="site_settings_paging_summary_one">페이지당 댓글 1개</string>
+ <string name="site_settings_multiple_links_summary_other">링크가 %d개를 넘는 경우 승인 필요</string>
+ <string name="site_settings_multiple_links_summary_one">링크가 1개를 넘는 경우 승인 필요</string>
+ <string name="site_settings_multiple_links_summary_zero">링크가 0개를 넘는 경우 승인 필요</string>
+ <string name="detail_approve_auto">모든 사람의 댓글을 자동으로 승인합니다.</string>
+ <string name="detail_approve_auto_if_previously_approved">사용자가 이전에 댓글을 승인한 경우 자동으로 승인합니다.</string>
+ <string name="detail_approve_manual">모든 사람의 댓글에 수동 승인이 필요합니다.</string>
+ <string name="filter_trashed_posts">삭제됨</string>
+ <string name="days_quantity_one">1일</string>
+ <string name="days_quantity_other">%d일</string>
+ <string name="filter_published_posts">발행됨</string>
+ <string name="filter_draft_posts">임시글</string>
+ <string name="filter_scheduled_posts">예약됨</string>
+ <string name="pending_email_change_snackbar">새 주소를 확인하려면 %1$s(으)로 전송된 이메일에 있는 확인 링크를 클릭하세요.</string>
+ <string name="primary_site">기본 사이트</string>
+ <string name="web_address">웹 주소</string>
+ <string name="editor_toast_uploading_please_wait">미디어를 업로드 중입니다. 완료될 때까지 기다려 주세요.</string>
+ <string name="error_refresh_comments_showing_older">지금은 댓글을 새로 고칠 수 없습니다. 이전 댓글을 표시 중입니다.</string>
+ <string name="editor_post_settings_set_featured_image">특성 이미지 설정</string>
+ <string name="editor_post_settings_featured_image">특성 이미지</string>
+ <string name="new_editor_promo_desc">Android용 워드프레스 앱에 멋진\n 편집기. 새 글을 작성하면서 사용해 보세요.</string>
+ <string name="new_editor_promo_title">완전히 새로운 편집기</string>
+ <string name="new_editor_promo_button_label">네, 확인했습니다.</string>
+ <string name="visual_editor_enabled">비주얼 편집기 사용 설정됨</string>
+ <string name="editor_content_placeholder">여기서 스토리 공유…</string>
+ <string name="editor_page_title_placeholder">페이지 제목</string>
+ <string name="editor_post_title_placeholder">글 제목</string>
+ <string name="email_address">이메일 주소</string>
+ <string name="preference_show_visual_editor">비주얼 편집기 표시</string>
+ <string name="dlg_sure_to_delete_comments">이 댓글을 영구 삭제하시겠습니까?</string>
+ <string name="preference_editor">편집자</string>
+ <string name="dlg_sure_to_delete_comment">이 댓글을 영구 삭제하시겠습니까?</string>
+ <string name="mnu_comment_delete_permanently">삭제</string>
+ <string name="comment_deleted_permanently">댓글 삭제됨</string>
+ <string name="mnu_comment_untrash">복원</string>
+ <string name="comments_empty_list_filtered_spam">스팸 댓글 없음</string>
+ <string name="could_not_load_page">페이지를 로드할 수 없음</string>
+ <string name="comment_status_all">모두</string>
+ <string name="interface_language">인터페이스 언어</string>
+ <string name="off">끔</string>
+ <string name="about_the_app">앱 정보</string>
+ <string name="error_post_account_settings">계정 설정을 저장할 수 없습니다.</string>
+ <string name="error_post_my_profile">프로필을 저장할 수 없습니다.</string>
+ <string name="error_fetch_account_settings">계정 설정을 검색할 수 없습니다.</string>
+ <string name="error_fetch_my_profile">프로필을 검색할 수 없습니다.</string>
+ <string name="stats_widget_promo_ok_btn_label">네, 이해했습니다.</string>
+ <string name="stats_widget_promo_desc">홈 화면에 위젯을 추가하여 클릭 한 번으로 통계에 액세스하세요.</string>
+ <string name="stats_widget_promo_title">홈 화면 통계 위젯</string>
+ <string name="site_settings_unknown_language_code_error">언어 코드가 인식되지 않음</string>
+ <string name="site_settings_threading_dialog_description">스레드에서 댓글이 계층화되도록 허용합니다.</string>
+ <string name="site_settings_threading_dialog_header">최대 스레드</string>
+ <string name="remove">제거</string>
+ <string name="search">검색</string>
+ <string name="add_category">카테고리 추가</string>
+ <string name="disabled">해제</string>
+ <string name="site_settings_image_original_size">원래 크기</string>
+ <string name="privacy_private">사이트가 본인과 본인이 승인한 사용자에게만 공개됩니다.</string>
+ <string name="privacy_public_not_indexed">사이트가 모든 사람에게 공개되지만 검색 엔진에 사이트가 검색되지 않도록 요청합니다.</string>
+ <string name="privacy_public">사이트가 모든 사람에게 공개되며 검색 엔진이 검색할 수 있습니다.</string>
+ <string name="about_me_hint">사용자에 대한 몇 가지 정보...</string>
+ <string name="public_display_name_hint">대화명을 설정하지 않을 경우 기본적으로 사용자명으로 설정됩니다.</string>
+ <string name="about_me">내 소개</string>
+ <string name="public_display_name">공개 이름</string>
+ <string name="my_profile">내 프로필</string>
+ <string name="first_name">이름</string>
+ <string name="last_name">성</string>
+ <string name="site_privacy_public_desc">검색 엔진이 이 사이트 검색을 허용하기</string>
+ <string name="site_privacy_hidden_desc">검색 엔진이 이 사이트 검색 차단하기</string>
+ <string name="site_privacy_private_desc">사이트를 비공개로 설정하고 제가 선택하는 사용자만 볼 수 있도록 하고 싶습니다.</string>
+ <string name="cd_related_post_preview_image">관련 글 미리보기 이미지</string>
+ <string name="error_post_remote_site_settings">사이트 정보를 저장할 수 없습니다.</string>
+ <string name="error_fetch_remote_site_settings">사이트 정보를 가져올 수 없습니다.</string>
+ <string name="error_media_upload_connection">미디어를 업로드하는 동안 연결 오류가 발생했습니다.</string>
+ <string name="site_settings_disconnected_toast">연결 해제되어 편집이 비활성화되었습니다.</string>
+ <string name="site_settings_unsupported_version_error">지원되지 않는 워드프레스 버전</string>
+ <string name="site_settings_multiple_links_dialog_description">이 링크 수보다 링크가 많이 포함된 댓글에 대한 승인이 필요합니다.</string>
+ <string name="site_settings_close_after_dialog_switch_text">자동으로 닫기</string>
+ <string name="site_settings_close_after_dialog_description">글에 대한 댓글을 자동으로 닫습니다.</string>
+ <string name="site_settings_paging_dialog_description">댓글 스레드를 여러 페이지로 나눕니다.</string>
+ <string name="site_settings_paging_dialog_header">페이지당 댓글 수</string>
+ <string name="site_settings_close_after_dialog_title">댓글 닫기</string>
+ <string name="site_settings_blacklist_description">댓글의 내용이나 이름, URL, 이메일, IP에 아래 단어가 들어있으면 스팸으로 처리됩니다. 단어의 일부만 입력해도 됩니다. "프레스"라고 입력하면 "워드프레스"로 인식될 수 있습니다.</string>
+ <string name="site_settings_hold_for_moderation_description">댓글의 내용, 이름, URL, 이메일 또는 IP에 아래 단어가 들어 있으면 중재 대기열에 보관됩니다. 단어의 일부만 입력해도 됩니다. "프레스"라고 입력하면 "워드프레스"로 인식될 수 있습니다.</string>
+ <string name="site_settings_list_editor_input_hint">단어 또는 구절을 입력하세요.</string>
+ <string name="site_settings_list_editor_no_items_text">아이템 없음</string>
+ <string name="site_settings_learn_more_caption">개별 글에 대해 이러한 설정을 우선 적용할 수 있습니다.</string>
+ <string name="site_settings_rp_preview3_site">"업그레이드"에서</string>
+ <string name="site_settings_rp_preview3_title">업그레이드 핵심: 웨딩용 VideoPress</string>
+ <string name="site_settings_rp_preview2_site">"앱"에서</string>
+ <string name="site_settings_rp_preview2_title">대대적으로 업데이트된 Android 앱용 WordPress</string>
+ <string name="site_settings_rp_preview1_site">"모바일"에서</string>
+ <string name="site_settings_rp_preview1_title">iPhone/iPad 업데이트 지금 이용 가능</string>
+ <string name="site_settings_rp_show_images_title">이미지 보이기</string>
+ <string name="site_settings_rp_show_header_title">헤더 표시</string>
+ <string name="site_settings_rp_switch_summary">관련 글에는 글 아래에 있는 사이트의 관련 콘텐츠가 표시됩니다.</string>
+ <string name="site_settings_rp_switch_title">관련 글 표시</string>
+ <string name="site_settings_delete_site_hint">앱에서 사이트 데이터를 제거합니다.</string>
+ <string name="site_settings_blacklist_hint">필터와 일치하는 댓글은 스팸으로 표시됩니다.</string>
+ <string name="site_settings_moderation_hold_hint">필터와 일치하는 댓글은 중재 대기열에 배치됩니다.</string>
+ <string name="site_settings_multiple_links_hint">알려진 사용자의 링크 제한을 무시합니다.</string>
+ <string name="site_settings_whitelist_hint">댓글을 쓴 사람이 예전에 댓글이 승인된 적이 있어야 합니다.</string>
+ <string name="site_settings_user_account_required_hint">가입하여 로그인한 사용자만 댓글을 남길 수 있습니다.</string>
+ <string name="site_settings_identity_required_hint">댓글을 쓴 사람의 이름과 이메일을 꼭 남겨야 합니다.</string>
+ <string name="site_settings_manual_approval_hint">댓글은 수동으로 승인되어야 합니다.</string>
+ <string name="site_settings_paging_hint">지정된 크기 단위로 댓글 표시</string>
+ <string name="site_settings_threading_hint">특정 깊이로 계층형 댓글 허용</string>
+ <string name="site_settings_sort_by_hint">댓글이 표시되는 순서를 결정합니다.</string>
+ <string name="site_settings_close_after_hint">지정된 시간 이후 댓글을 허용하지 않음</string>
+ <string name="site_settings_receive_pingbacks_hint">다른 블로그의 링크 알림 허용</string>
+ <string name="site_settings_send_pingbacks_hint">글에서 링크한 블로그에 링크 사실을 알림</string>
+ <string name="site_settings_allow_comments_hint">독자가 댓글을 게시할 수 있도록 허용</string>
+ <string name="site_settings_discussion_hint">사이트 토론 설정 보기 및 변경</string>
+ <string name="site_settings_more_hint">사용 가능한 모든 토론 설정 보기</string>
+ <string name="site_settings_related_posts_hint">리더에서 관련 글 표시 또는 숨기기</string>
+ <string name="site_settings_upload_and_link_image_hint">항상 최대 크기 이미지를 업로드할 수 있도록 허용</string>
+ <string name="site_settings_image_width_hint">글의 이미지를 이 폭으로 크기 조정합니다.</string>
+ <string name="site_settings_format_hint">새 글 형식을 설정합니다.</string>
+ <string name="site_settings_category_hint">새 글 카테고리를 설정합니다.</string>
+ <string name="site_settings_location_hint">글에 자동으로 위치 데이터 추가</string>
+ <string name="site_settings_password_hint">비밀번호 변경</string>
+ <string name="site_settings_username_hint">현재 사용자 계정</string>
+ <string name="site_settings_language_hint">이 블로그의 기본 언어</string>
+ <string name="site_settings_privacy_hint">사이트를 볼 수 있는 사용자를 제어합니다.</string>
+ <string name="site_settings_address_hint">현재 주소 변경은 지원되지 않습니다.</string>
+ <string name="site_settings_tagline_hint">블로그를 설명하는 간단한 설명이나 기억하기 쉬운 구절</string>
+ <string name="site_settings_title_hint">이 사이트가 추구하는 것에 관해 짧게 쓰세요.</string>
+ <string name="site_settings_whitelist_known_summary">알려진 사용자의 댓글</string>
+ <string name="site_settings_whitelist_all_summary">모든 사용자의 댓글</string>
+ <string name="site_settings_threading_summary">%d개 레벨</string>
+ <string name="site_settings_privacy_private_summary">비공개</string>
+ <string name="site_settings_privacy_hidden_summary">숨김</string>
+ <string name="site_settings_delete_site_title">사이트 삭제</string>
+ <string name="site_settings_privacy_public_summary">공개</string>
+ <string name="site_settings_blacklist_title">블랙리스트</string>
+ <string name="site_settings_moderation_hold_title">중재를 위해 보관</string>
+ <string name="site_settings_multiple_links_title">댓글의 링크</string>
+ <string name="site_settings_whitelist_title">자동으로 승인</string>
+ <string name="site_settings_threading_title">스레딩</string>
+ <string name="site_settings_paging_title">페이징</string>
+ <string name="site_settings_sort_by_title">정렬 기준</string>
+ <string name="site_settings_account_required_title">사용자가 로그인해야 합니다.</string>
+ <string name="site_settings_identity_required_title">이름과 이메일을 포함해야 합니다.</string>
+ <string name="site_settings_receive_pingbacks_title">핑백 받기</string>
+ <string name="site_settings_send_pingbacks_title">핑백 보내기</string>
+ <string name="site_settings_allow_comments_title">댓글 허용</string>
+ <string name="site_settings_default_format_title">기본 형식</string>
+ <string name="site_settings_default_category_title">기본 카테고리</string>
+ <string name="site_settings_location_title">위치 활성화</string>
+ <string name="site_settings_address_title">주소</string>
+ <string name="site_settings_title_title">사이트 제목</string>
+ <string name="site_settings_tagline_title">태그라인</string>
+ <string name="site_settings_this_device_header">이 기기</string>
+ <string name="site_settings_discussion_new_posts_header">새 글에 대한 기본값</string>
+ <string name="site_settings_account_header">계정</string>
+ <string name="site_settings_writing_header">쓰기</string>
+ <string name="newest_first">최근 댓글 먼저</string>
+ <string name="site_settings_general_header">일반</string>
+ <string name="discussion">토론</string>
+ <string name="privacy">개인 정보</string>
+ <string name="related_posts">관련 글</string>
+ <string name="comments">댓글</string>
+ <string name="close_after">다음 시간 후에 닫기:</string>
+ <string name="oldest_first">오래된 항목 먼저</string>
+ <string name="media_error_no_permission_upload">미디어를 사이트에 업로드하는 권한이 없습니다.</string>
+ <string name="never">절대</string>
+ <string name="unknown">알 수 없음</string>
+ <string name="reader_err_get_post_not_found">이 글은 더 이상 존재하지 않습니다.</string>
+ <string name="reader_err_get_post_not_authorized">이 글을 볼 권한이 없습니다.</string>
+ <string name="reader_err_get_post_generic">이 글을 가져올 수 없습니다.</string>
+ <string name="blog_name_no_spaced_allowed">사이트 주소에 공백을 포함할 수 없습니다.</string>
+ <string name="invalid_username_no_spaces">사용자명에 공백을 포함할 수 없습니다.</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">팔로우하는 사이트에 최근 게시된 글이 없습니다.</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">최근 글 없음</string>
+ <string name="media_details_copy_url_toast">URL이 클립보드에 복사됨</string>
+ <string name="edit_media">미디어 편집</string>
+ <string name="media_details_copy_url">URL 복사</string>
+ <string name="media_details_label_date_uploaded">업로드됨</string>
+ <string name="media_details_label_date_added">추가됨</string>
+ <string name="selected_theme">선택된 테마</string>
+ <string name="could_not_load_theme">테마를 로드할 수 없음</string>
+ <string name="theme_activation_error">문제가 생겼습니다. 테마를 활성화할 수 없음</string>
+ <string name="theme_by_author_prompt_append"> 작성자: %1$s</string>
+ <string name="theme_prompt">%1$s을(를) 선택해 주셔서 감사합니다.</string>
+ <string name="theme_manage_site">사이트 관리</string>
+ <string name="theme_done">완료</string>
+ <string name="theme_support">지원</string>
+ <string name="theme_details">세부사항</string>
+ <string name="theme_view">보기</string>
+ <string name="theme_try_and_customize">사용 및 사용자 정의하기</string>
+ <string name="theme_activate">활성화</string>
+ <string name="title_activity_theme_support">테마</string>
+ <string name="active">활성</string>
+ <string name="support">지원</string>
+ <string name="details">세부사항</string>
+ <string name="customize">사용자 정의</string>
+ <string name="current_theme">현재 테마</string>
+ <string name="date_range_end_date">종료 일자</string>
+ <string name="date_range_start_date">시작 일자</string>
+ <string name="stats_referrers_spam_generic_error">작업 중 문제가 발생했습니다. 스팸 상태가 변경되지 않았습니다.</string>
+ <string name="stats_referrers_marking_not_spam">스팸이 아닌 것으로 표시</string>
+ <string name="stats_referrers_marking_spam">스팸으로 표시</string>
+ <string name="stats_referrers_unspam">스팸 아님</string>
+ <string name="stats_referrers_spam">스팸</string>
+ <string name="post_updated">글이 업데이트됨</string>
+ <string name="page_updated">페이지가 업데이트됨</string>
+ <string name="page_published">페이지가 발행됨</string>
+ <string name="post_published">글이 발행됨</string>
+ <string name="theme_auth_error_authenticate">테마를 가져오지 못함: 사용자 인증 실패</string>
+ <string name="theme_no_search_result_found">죄송합니다. 테마를 찾을 수 없습니다.</string>
+ <string name="media_dimensions">크기: %s</string>
+ <string name="media_uploaded_on">업로드 위치: %s</string>
+ <string name="media_file_name">파일 이름: %s</string>
+ <string name="media_file_type">파일 형식: %s</string>
+ <string name="upload_queued">대기열에 추가됨</string>
+ <string name="reader_label_gap_marker">더 많은 글 로드</string>
+ <string name="notifications_no_search_results">\'%s\'에 일치하는 사이트 없음</string>
+ <string name="search_sites">사이트 검색</string>
+ <string name="unread">읽지 않음</string>
+ <string name="notifications_empty_view_reader">리더 보기</string>
+ <string name="notifications_empty_action_followers_likes">알림: 읽은 글에 댓글이 추가됐습니다.</string>
+ <string name="notifications_empty_action_comments">대화 참여: 팔로우하는 블로그의 글에 댓글이 추가됐습니다.</string>
+ <string name="notifications_empty_action_unread">대화 다시 계속하기: 새 글을 작성하세요.</string>
+ <string name="notifications_empty_action_all">활발한 소통을 위해 팔로우하는 블로그의 글에 댓글을 달아보세요.</string>
+ <string name="notifications_empty_likes">표시할 새 좋아요가 아직 없습니다.</string>
+ <string name="notifications_empty_followers">보고할 새 팔로워가 아직 없습니다.</string>
+ <string name="notifications_empty_comments">새로운 댓글이 아직 없습니다.</string>
+ <string name="notifications_empty_unread">모두 확인하셨습니다!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">앱에서 통계를 확인하고 나중에 위젯 추가를 시도하세요.</string>
+ <string name="stats_widget_error_readd_widget">위젯을 제거하고 다시 추가하세요.</string>
+ <string name="stats_widget_error_no_visible_blog">공개된 블로그가 없으면 통계에 액세스할 수 없습니다.</string>
+ <string name="stats_widget_error_no_permissions">회원님의 워드프레스닷컴 계정에서 이 블로그의 통계에 액세스할 수 없습니다.</string>
+ <string name="stats_widget_error_no_account">워드프레스에 로그인하세요.</string>
+ <string name="stats_widget_error_generic">통계를 로드할 수 없음</string>
+ <string name="stats_widget_loading_data">데이터 로드 중...</string>
+ <string name="stats_widget_name_for_blog">%1$s 오늘의 통계</string>
+ <string name="stats_widget_name">워드프레스 오늘의 통계</string>
+ <string name="add_location_permission_required">위치를 추가하려면 권한이 필요합니다.</string>
+ <string name="add_media_permission_required">미디어를 추가하려면 권한이 필요합니다.</string>
+ <string name="access_media_permission_required">미디어에 액세스하려면 권한이 필요합니다.</string>
+ <string name="stats_enable_rest_api_in_jetpack">통계를 보기 위해서는 Jetpack에서 JSON API 모듈을 활성화하세요.</string>
+ <string name="error_open_list_from_notification">이 글이나 페이지는 다른 사이트에서 게시되었습니다.</string>
+ <string name="reader_short_comment_count_multi">%s 댓글</string>
+ <string name="reader_short_comment_count_one">1 댓글</string>
+ <string name="reader_label_submit_comment">보내기</string>
+ <string name="reader_hint_comment_on_post">글에 답장하기...</string>
+ <string name="reader_discover_visit_blog">%s 방문</string>
+ <string name="reader_discover_attribution_blog">원래 %s에 게시됨</string>
+ <string name="reader_discover_attribution_author">원래 %s님이 게시함</string>
+ <string name="reader_discover_attribution_author_and_blog">원래 %2$s에 %1$s님이 게시함</string>
+ <string name="reader_short_like_count_multi">좋아요 %s개</string>
+ <string name="reader_short_like_count_one">좋아요 1개</string>
+ <string name="reader_label_follow_count">팔로워 %,d명</string>
+ <string name="reader_short_like_count_none">좋아요</string>
+ <string name="reader_menu_tags">태그 및 블로그 편집</string>
+ <string name="reader_title_post_detail">리더 글</string>
+ <string name="local_draft_explainer">이 글은 아직 발행되지 않은 로컬 임시글입니다.</string>
+ <string name="local_changes_explainer">이 글은 아직 발행되지 않은 로컬 변경입니다.</string>
+ <string name="notifications_push_summary">사용자 기기에 표시되는 알림에 대한 설정입니다.</string>
+ <string name="notifications_email_summary">사용자 계정에 연결된 이메일로 전송되는 알림에 대한 설정입니다.</string>
+ <string name="notifications_tab_summary">알림 탭에 표시되는 알림에 대한 설정입니다.</string>
+ <string name="notifications_disabled">앱 알림이 사용 중지되었습니다. 설정에서 사용 설정하려면 여기를 누르세요.</string>
+ <string name="notification_types">알림 유형</string>
+ <string name="error_loading_notifications">알림 설정을 로드할 수 없습니다.</string>
+ <string name="replies_to_your_comments">댓글에 대한 답글</string>
+ <string name="comment_likes">댓글 좋아요</string>
+ <string name="app_notifications">앱 알림</string>
+ <string name="notifications_tab">알림 탭</string>
+ <string name="email">이메일</string>
+ <string name="notifications_comments_other_blogs">다른 사이트의 댓글</string>
+ <string name="notifications_wpcom_updates">워드프레스닷컴 소식</string>
+ <string name="notifications_other">기타</string>
+ <string name="notifications_account_emails">워드프레스닷컴 소식</string>
+ <string name="notifications_account_emails_summary">사용자 계정에 관한 중요 이메일을 항상 보내 드리며, 유용한 정보도 받으실 수 있습니다.</string>
+ <string name="notifications_sights_and_sounds">보기 및 사운드</string>
+ <string name="your_sites">내 사이트</string>
+ <string name="stats_insights_latest_post_trend">%2$s이(가) 발행된 후 %1$s일이 지났습니다. 지금까지 글의 성과입니다.</string>
+ <string name="stats_insights_latest_post_summary">최신 글 요약</string>
+ <string name="button_revert">되돌리기</string>
+ <string name="days_ago">%d일 전</string>
+ <string name="yesterday">어제</string>
+ <string name="connectionbar_no_connection">연결 없음</string>
+ <string name="page_trashed">휴지통으로 이동된 페이지</string>
+ <string name="post_deleted">글 삭제됨</string>
+ <string name="post_trashed">휴지통으로 이동된 글</string>
+ <string name="stats_no_activity_this_period">이 기간에 활동이 없음</string>
+ <string name="trashed">삭제됨</string>
+ <string name="button_back">돌아가기</string>
+ <string name="page_deleted">페이지 삭제됨</string>
+ <string name="button_stats">통계</string>
+ <string name="button_trash">휴지통</string>
+ <string name="button_preview">미리 보기</string>
+ <string name="button_view">보기</string>
+ <string name="button_edit">편집</string>
+ <string name="button_publish">발행</string>
+ <string name="my_site_no_sites_view_subtitle">더 추가하시겠습니까?</string>
+ <string name="my_site_no_sites_view_title">아직 WordPress사이트를 등록하지 않았습니다.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">이 블로그에 접근할 권한이 없습니다.</string>
+ <string name="reader_toast_err_follow_blog_not_found">블로그를 찾을 수 없습니다.</string>
+ <string name="undo">실행 취소</string>
+ <string name="tabbar_accessibility_label_my_site">내 사이트</string>
+ <string name="tabbar_accessibility_label_me">내 계정</string>
+ <string name="passcodelock_prompt_message">PIN 입력</string>
+ <string name="editor_toast_changes_saved">변경사항 저장됨</string>
+ <string name="push_auth_expired">요청이 만료되었습니다. 워드프레스닷컴에 로그인하고 다시 시도하세요.</string>
+ <string name="ignore">무시</string>
+ <string name="stats_insights_most_popular_percent_views">조회수 %1$d%%</string>
+ <string name="stats_insights_best_ever">최고 조회수</string>
+ <string name="stats_insights_most_popular_hour">가장 인기 있는 시간대</string>
+ <string name="stats_insights_most_popular_day">가장 인기 있는 요일</string>
+ <string name="stats_insights_today">오늘의 통계</string>
+ <string name="stats_insights_popular">가장 인기 있는 요일 및 시간대</string>
+ <string name="stats_insights_all_time">최고 게시글, 조회수 및 방문자</string>
+ <string name="stats_insights">인사이트</string>
+ <string name="stats_sign_in_jetpack_different_com_account">통계를 보려면 젯팩 연결에 사용한 워드프레스닷컴 계정에 로그인하세요.</string>
+ <string name="stats_other_recent_stats_moved_label">기타 최근 통계를 찾으시나요? 인사이트 페이지로 이동되었습니다.</string>
+ <string name="me_disconnect_from_wordpress_com">워드프레스닷컴 연결 해제</string>
+ <string name="me_btn_login_logout">로그인/로그아웃</string>
+ <string name="me_connect_to_wordpress_com">워드프레스닷컴 연결하기</string>
+ <string name="site_picker_cant_hide_current_site">"%s"은(는) 현재 사이트이기 때문에 숨길 수 없습니다.</string>
+ <string name="me_btn_support">도움 및 지원</string>
+ <string name="account_settings">계정 설정</string>
+ <string name="site_picker_create_dotcom">워드프레스닷컴 사이트 생성</string>
+ <string name="site_picker_edit_visibility">사이트 표시/숨기기</string>
+ <string name="site_picker_add_site">사이트 추가</string>
+ <string name="site_picker_add_self_hosted">독립 호스트 사이트 추가</string>
+ <string name="site_picker_title">사이트 선택</string>
+ <string name="my_site_btn_switch_site">사이트 전환</string>
+ <string name="my_site_btn_view_site">사이트 보기</string>
+ <string name="my_site_btn_view_admin">관리자 보기</string>
+ <string name="my_site_header_publish">게시</string>
+ <string name="my_site_header_look_and_feel">모양과 느낌</string>
+ <string name="my_site_btn_site_settings">설정</string>
+ <string name="my_site_btn_blog_posts">블로그 글</string>
+ <string name="reader_label_new_posts_subtitle">눌러서 표시</string>
+ <string name="my_site_header_configuration">설정</string>
+ <string name="notifications_account_required">알림을 보려면 워드프레스닷컴에 로그인</string>
+ <string name="stats_unknown_author">알 수 없는 글쓴이</string>
+ <string name="signout">연결 해제</string>
+ <string name="image_added">이미지 추가됨</string>
+ <string name="sign_out_wpcom_confirm">계정의 연결을 해제하면 이 장치에서 로컬 임시글 및 로컬 변경 사항을 포함한 @%s의 모든 워드프레스닷컴 데이터가 제거됩니다.</string>
+ <string name="select_all">모두 선택</string>
+ <string name="show">표시</string>
+ <string name="hide">숨기기</string>
+ <string name="deselect_all">모두 선택 해제</string>
+ <string name="select_from_new_picker">새로운 선택 도구로 복수 선택</string>
+ <string name="loading_blog_images">이미지 가져오는 중</string>
+ <string name="loading_blog_videos">비디오 가져오는 중</string>
+ <string name="no_media_sources">미디어를 가져올 수 없음</string>
+ <string name="error_loading_blog_videos">비디오를 가져올 수 없음</string>
+ <string name="error_loading_images">이미지 로드 오류</string>
+ <string name="error_loading_videos">비디오 로드 오류</string>
+ <string name="error_loading_blog_images">이미지를 가져올 수 없음</string>
+ <string name="no_blog_images">이미지 없음</string>
+ <string name="no_blog_videos">비디오 없음</string>
+ <string name="no_device_images">이미지 없음</string>
+ <string name="stats_generic_error">필요한 통계 자료를 로드할 수 없음</string>
+ <string name="no_device_videos">비디오 없음</string>
+ <string name="no_media">미디어 없음</string>
+ <string name="loading_videos">비디오 로드 중</string>
+ <string name="loading_images">이미지 로드 중</string>
+ <string name="device">장치</string>
+ <string name="language">언어</string>
+ <string name="add_to_post">글에 추가</string>
+ <string name="media_picker_title">미디어 선택</string>
+ <string name="take_photo">사진 촬영</string>
+ <string name="take_video">비디오 촬영</string>
+ <string name="tab_title_device_images">장치 이미지</string>
+ <string name="tab_title_device_videos">장치 비디오</string>
+ <string name="tab_title_site_images">사이트 이미지</string>
+ <string name="tab_title_site_videos">사이트 비디오</string>
+ <string name="error_publish_no_network">네트워크에 연결되지 않아 게시할 수 없습니다. 임시글로 저장되었습니다.</string>
+ <string name="editor_toast_invalid_path">잘못된 파일 경로</string>
+ <string name="verification_code">확인 코드</string>
+ <string name="invalid_verification_code">잘못된 확인 코드</string>
+ <string name="verify">확인</string>
+ <string name="two_step_footer_label">인증 앱의 코드를 입력하세요.</string>
+ <string name="two_step_footer_button">문자 메시지를 통한 코드 보내기</string>
+ <string name="two_step_sms_sent">확인 코드 문자 메시지가 왔는지 확인하세요.</string>
+ <string name="sign_in_jetpack">워드프레스닷컴 계정에 로그인하여 젯팩에 연결하세요.</string>
+ <string name="auth_required">계속하려면 다시 로그인하세요.</string>
+ <string name="media_details_label_file_name">파일명</string>
+ <string name="media_details_label_file_type">파일형식</string>
+ <string name="stats_empty_search_terms_desc">방문자들이 내 사이트를 찾기 위해 검색한 용어를 확인하여 검색 트래픽에 대해 자세히 알아보세요.</string>
+ <string name="stats_empty_search_terms">기록된 검색어 없음</string>
+ <string name="error_notification_open">알림을 열 수 없음</string>
+ <string name="stats_followers_total_email_paged">%1$d 표시 - %2$d/%3$s 이메일 팔로워</string>
+ <string name="stats_followers_total_wpcom_paged">%1$d 표시 - %2$d/%3$s WordPress.com 팔로워</string>
+ <string name="stats_view_search_terms">검색어</string>
+ <string name="stats_entry_search_terms">검색어</string>
+ <string name="stats_view_authors">글쓴이</string>
+ <string name="stats_search_terms_unknown_search_terms">알 수 없는 검색어</string>
+ <string name="pages_fetching">페이지를 가져오는 중...</string>
+ <string name="toast_err_post_uploading">업로드하는 동안 글을 여는데 실패함.</string>
+ <string name="posts_fetching">글을 가져오는 중...</string>
+ <string name="reader_empty_posts_request_failed">글을 가져올 수 없음</string>
+ <string name="publisher">게시자:</string>
+ <string name="comments_fetching">답글을 가져오는 중...</string>
+ <string name="media_fetching">미디어를 가져오는 중...</string>
+ <string name="reader_page_recommended_blogs">회원님이 좋아할 만한 블로그</string>
+ <string name="stats_months_and_years">월 및 연도</string>
+ <string name="stats_recent_weeks">최근 몇 주</string>
+ <string name="error_copy_to_clipboard">텍스트를 클립보드에 복사하는 동안 오류가 발생했습니다.</string>
+ <string name="reader_empty_posts_in_blog">이 블로그는 비었습니다.</string>
+ <string name="stats_average_per_day">하루 평균</string>
+ <string name="stats_total">총</string>
+ <string name="stats_overall">전체</string>
+ <string name="stats_period">기간</string>
+ <string name="logs_copied_to_clipboard">애플리케이션 로그가 클립보드에 복사되었습니다.</string>
+ <string name="post_uploading">업로드</string>
+ <string name="reader_label_new_posts">새 게시물</string>
+ <string name="stats_comments_total_comments_followers">댓글 팔로워 수를 포함한 총 게시글 수: %1$s</string>
+ <string name="stats_timeframe_years">년</string>
+ <string name="stats_views">조회수</string>
+ <string name="stats_visitors">방문자</string>
+ <string name="stats_pagination_label">%1$s/%2$s 페이지</string>
+ <string name="stats_view_videos">비디오</string>
+ <string name="stats_view_publicize">배포 기능</string>
+ <string name="stats_view_followers">팔로워</string>
+ <string name="stats_view_countries">국가</string>
+ <string name="stats_likes">좋아요</string>
+ <string name="stats_entry_clicks_link">링크</string>
+ <string name="stats_view_top_posts_and_pages">게시물 또는 페이지</string>
+ <string name="stats_entry_followers">팔로워</string>
+ <string name="stats_totals_publicize">팔로워</string>
+ <string name="stats_entry_top_commenter">글쓴이</string>
+ <string name="stats_entry_publicize">서비스</string>
+ <string name="stats_empty_geoviews_desc">목록을 탐색하여 회원님 사이트로 트래픽을 생성하는 국가와 지역을 확인하세요.</string>
+ <string name="stats_entry_video_plays">비디오</string>
+ <string name="stats_totals_followers">이후</string>
+ <string name="stats_empty_geoviews">기록된 국가가 없음</string>
+ <string name="stats_empty_top_posts_desc">최근에 본 콘텐츠가 무엇인지 살펴보고 개별 게시물과 페이지가 시간이 지나면서 어떻게 작동하는지 확인하세요.</string>
+ <string name="stats_empty_referrers_title">참조자가 기록되지 않음</string>
+ <string name="stats_empty_top_posts_title">조회한 게시물이나 페이지가 없음</string>
+ <string name="stats_empty_clicks_title">기록된 클릭 수가 없음</string>
+ <string name="stats_empty_referrers_desc">회원님의 사이트로 가장 많은 트래픽을 보내는 웹사이트 및 검색 엔진을 살펴보고 사이트의 가시성에 대해 알아보세요.</string>
+ <string name="stats_empty_clicks_desc"> 콘텐츠에 다른 사이트에 대한 링크가 포함된 경우 방문자가 가장 많이 클릭한 링크를 확인하세요.</string>
+ <string name="stats_empty_tags_and_categories">조회한 태그 지정된 게시물이나 페이지가 없음</string>
+ <string name="stats_empty_top_authors_desc">각 공동 작업자의 게시물에 대한 조회 수를 추적하고 각 작성자별로 가장 인기 있는 콘텐츠를 자세히 살펴보세요.</string>
+ <string name="stats_empty_tags_and_categories_desc">지난 주 회원님의 인기 게시물에 반영된 대로 회원님 사이트에서 가장 인기 있는 주제에 대한 개요를 살펴보세요.</string>
+ <string name="stats_empty_comments_desc">사이트에 댓글을 허용할 경우 가장 최근의 댓글 1,000개를 기준으로 하여 가장 댓글을 많이 단 사용자를 추적하고 가장 생생한 대화를 유도하는 콘텐츠를 찾아보세요.</string>
+ <string name="stats_empty_video_desc">VideoPress를 사용하여 비디오를 업로드한 경우에는 비디오를 시청한 횟수를 확인하세요.</string>
+ <string name="stats_empty_video">재생된 비디오가 없음</string>
+ <string name="stats_empty_publicize">Publicize 팔로워가 기록되지 않음</string>
+ <string name="stats_empty_publicize_desc">Publicize를 사용하여 여러 소셜 네트워킹 서비스에서 팔로워를 추적하세요.</string>
+ <string name="stats_empty_followers">팔로워 없음</string>
+ <string name="stats_empty_followers_desc">전반적인 팔로워 수와 각 팔로워가 사이트를 팔로우한 기간을 추적합니다.</string>
+ <string name="stats_comments_by_posts_and_pages">게시물 및 페이지별</string>
+ <string name="stats_comments_by_authors">작성자별</string>
+ <string name="stats_followers_total_wpcom">총 WordPress.com 팔로워 수: %1$s</string>
+ <string name="stats_followers_email_selector">이메일</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_total_email">총 이메일 팔로워 수: %1$s</string>
+ <string name="stats_followers_a_minute_ago">1분 전</string>
+ <string name="stats_followers_seconds_ago">몇 초 전</string>
+ <string name="stats_followers_days">%1$d일</string>
+ <string name="stats_followers_a_day">1일</string>
+ <string name="stats_followers_hours">%1$d시간</string>
+ <string name="stats_followers_an_hour_ago">1시간 전</string>
+ <string name="stats_followers_minutes">%1$d분</string>
+ <string name="stats_followers_a_month">1개월</string>
+ <string name="stats_followers_years">%1$d년</string>
+ <string name="stats_followers_a_year">1년</string>
+ <string name="stats_followers_months">%1$d개월</string>
+ <string name="stats_view">보기</string>
+ <string name="stats_view_all">전체보기</string>
+ <string name="themes_fetching">테마를 가져오는 중…</string>
+ <string name="stats_for">%s 통계</string>
+ <string name="stats_other_recent_stats_label">기타 최근 통계</string>
+ <string name="ssl_certificate_details">상세</string>
+ <string name="sure_to_remove_account">이 사이트를 제거할까요?</string>
+ <string name="delete_sure_post">이 글을 삭제하기</string>
+ <string name="delete_sure">이 임시 글을 삭제하기</string>
+ <string name="delete_sure_page">이 페이지를 삭제하기</string>
+ <string name="media_gallery_date_range">미디어는 %1$s 부터 %2$s 까지 표시하고 있습니다</string>
+ <string name="confirm_delete_multi_media">선택된 아이템을 삭제될까요?</string>
+ <string name="confirm_delete_media">선택된 아이템을 삭제될까요?</string>
+ <string name="cab_selected">%d개 선택함</string>
+ <string name="reader_empty_followed_blogs_title">아직 팔로우하는 블로그가 없습니다.</string>
+ <string name="reader_empty_posts_liked">좋아하는 게시물이 없군요.</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">FAQ 찾아보기</string>
+ <string name="nux_help_description">일반적인 질문에 대한 답변을 얻으려면 도움말 센터를 방문하고 새로운 질문을 하려면 포럼을 방문하세요.</string>
+ <string name="agree_terms_of_service">계정을 만들면 %1$s서비스 약관%2$s에 동의하는 것입니다.</string>
+ <string name="create_new_blog_wpcom">WordPress.com 블로그 만들기</string>
+ <string name="new_blog_wpcom_created">WordPress.com 블로그 생성!</string>
+ <string name="reader_empty_comments">아직 댓글이 없음</string>
+ <string name="reader_empty_posts_in_tag">이 태그가 지정된 게시물이 없음</string>
+ <string name="reader_label_comment_count_multi">댓글 %,d개</string>
+ <string name="reader_label_view_original">원본 기사 보기</string>
+ <string name="reader_label_like">좋아요</string>
+ <string name="reader_label_comment_count_single">1개의 댓글</string>
+ <string name="reader_label_comments_closed">댓글을 남길 수 없습니다.</string>
+ <string name="reader_label_comments_on">댓글:</string>
+ <string name="reader_title_photo_viewer">%1$d/%2$d</string>
+ <string name="error_publish_empty_post">빈 게시물은 게시할 수 없습니다.</string>
+ <string name="error_refresh_unauthorized_posts">게시물을 확인 또는 편집할 권한이 없습니다.</string>
+ <string name="error_refresh_unauthorized_pages">페이지를 확인 또는 편집할 권한이 없습니다.</string>
+ <string name="error_refresh_unauthorized_comments">댓글을 확인 또는 편집할 권한이 없습니다.</string>
+ <string name="older_month">1개월 이전</string>
+ <string name="more">더</string>
+ <string name="older_two_days">2일 이전</string>
+ <string name="older_last_week">1주일 이전</string>
+ <string name="stats_no_blog">요청한 블로그에 대해 통계 자료를 로드할 수 없습니다.</string>
+ <string name="select_a_blog">WordPress 사이트 선택</string>
+ <string name="sending_content">%s 콘텐츠 업로딩</string>
+ <string name="uploading_total">%1$d/%2$d 업로딩</string>
+ <string name="mnu_comment_liked">좋아함</string>
+ <string name="comment">댓글</string>
+ <string name="comment_trashed">휴지통으로 버려진 댓글</string>
+ <string name="posts_empty_list">아직 글이 없습니다. 하나 만들어보세요!</string>
+ <string name="comment_reply_to_user">%s에 응답</string>
+ <string name="pages_empty_list">페이지가 없습니다. 하나 만들어보세요!</string>
+ <string name="media_empty_list_custom_date">이 시간대에 미디어 없음</string>
+ <string name="posting_post">"%s" 게시</string>
+ <string name="signing_out">로그아웃…</string>
+ <string name="reader_toast_err_generic">이 작업을 실행할 수 없습니다</string>
+ <string name="reader_toast_err_block_blog">이 블로그를 차단할 수 없습니다</string>
+ <string name="reader_toast_blog_blocked">이 블로그에서 글 올리기는 더 이상 보이지 않을 것입니다</string>
+ <string name="reader_menu_block_blog">이 블로그 차단</string>
+ <string name="contact_us">문의하기</string>
+ <string name="hs__conversation_detail_error">나타나는 문제점을 설명해주세요</string>
+ <string name="hs__new_conversation_header">지원 채팅</string>
+ <string name="hs__conversation_header">지원 채팅</string>
+ <string name="hs__username_blank_error">유효한 이름을 입력하세요</string>
+ <string name="hs__invalid_email_error">유효한 이메일 주소를 입력하세요</string>
+ <string name="add_location">위치 추가</string>
+ <string name="current_location">현재 위치</string>
+ <string name="search_location">검색</string>
+ <string name="edit_location">편집</string>
+ <string name="search_current_location">위치 지정</string>
+ <string name="preference_send_usage_stats">통계 보내기</string>
+ <string name="preference_send_usage_stats_summary">사용 통계를 자동으로 보내면 안드로이드용 워드프레스를 개선하는데 도움이 됩니다.</string>
+ <string name="update_verb">업데이트</string>
+ <string name="schedule_verb">예약</string>
+ <string name="reader_title_subs">태그 및 블로그</string>
+ <string name="reader_page_followed_tags">팔로우된 태그</string>
+ <string name="reader_page_followed_blogs">팔로우된 블로그</string>
+ <string name="reader_hint_add_tag_or_url">팔로우할 태그나 URL 입력</string>
+ <string name="reader_label_followed_blog">팔로우한 블로그</string>
+ <string name="reader_label_tag_preview">%s 태그가 지정된 글</string>
+ <string name="reader_toast_err_get_blog_info">이 블로그를 표시할 수 없습니다.</string>
+ <string name="reader_toast_err_already_follow_blog">이 블로그를 이미 팔로우하고 있습니다.</string>
+ <string name="reader_toast_err_follow_blog">이 블로그를 팔로우할 수 없습니다.</string>
+ <string name="reader_toast_err_unfollow_blog">이 블로그에 대한 팔로우를 취소할 수 없습니다.</string>
+ <string name="reader_empty_recommended_blogs">추천 블로그가 없습니다.</string>
+ <string name="reader_title_blog_preview">리더 블로그</string>
+ <string name="reader_title_tag_preview">리더 태그</string>
+ <string name="saving">저장중...</string>
+ <string name="media_empty_list">미디어가 없습니다.</string>
+ <string name="ptr_tip_message">팁: 당겨서 새로 고침</string>
+ <string name="help">도우미</string>
+ <string name="forgot_password">패스워드를 잊으셨나요?</string>
+ <string name="forums">포럼</string>
+ <string name="help_center">도우미 센터</string>
+ <string name="ssl_certificate_error">잘못된 SSL 인증서</string>
+ <string name="ssl_certificate_ask_trust">일반적으로 문제없이 이 사이트에 연결하는 경우, 이 오류는 누군가가 사이트를 사칭하는 것을 의미 할 수 있습니다, 그렇다면 연결하면 안됩니다. 그래도 인증서를 신뢰 하시겠습니까?</string>
+ <string name="blog_not_found">블로그에 접속하는 동안 에러가 발생하였습니다.</string>
+ <string name="mnu_comment_unspam">스팸이 아닙니다.</string>
+ <string name="error_refresh_posts">게시물을 다시로드 할 수 없습니다.</string>
+ <string name="error_refresh_comments">댓글을 다시로드 할 수 없습니다 .</string>
+ <string name="error_refresh_pages">고정 페이지를 다시로드 할 수 없습니다.</string>
+ <string name="error_refresh_notifications">알림을 다시로드 할 수 없습니다.</string>
+ <string name="username_or_password_incorrect">입력된 사용자 이름이나 암호가 잘못되었습니다.</string>
+ <string name="invalid_username_too_long">사용자 이름에는 최대 64 자 이하로 되어야 함.</string>
+ <string name="invalid_username_too_short">사용자 이름은 4자 이상입니다.</string>
+ <string name="wait_until_upload_completes">업로드가 완료 될 때까지 기다리십시오.</string>
+ <string name="blog_name_reserved_but_may_be_available">이 사이트 이름은 예약 중입니다만, 며칠 동안 사용 가능하게 될지도 모릅니다.</string>
+ <string name="comments_empty_list">댓글이 없습니다.</string>
+ <string name="notifications_empty_list">알림이 없습니다.</string>
+ <string name="email_invalid">유효한 이메일 주소를 입력 하십시오.</string>
+ <string name="nux_cannot_log_in">로그인 할 수 없습니다.</string>
+ <string name="email_not_allowed">허용되지 않는 메일 주소 입니다.</string>
+ <string name="invalid_email_message">잘못된 이메일 주소입니다.</string>
+ <string name="username_not_allowed">허용되지 않은 사용자 이름입니다.</string>
+ <string name="blog_name_not_allowed">허용되지 않는 사이트 주소입니다.</string>
+ <string name="blog_name_exists">그 사이트는 이미 작성되었습니다.</string>
+ <string name="blog_name_reserved">사이트 주소는 현재 예약되어 있습니다.</string>
+ <string name="category_automatically_renamed">카테고리 이름 %1$s은 유효하지 않습니다. %2$s로 변경했습니다.</string>
+ <string name="username_contains_invalid_characters">사용자 이름에는 밑줄 기호(_)은 포함되지 않습니다.</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">사이트 주소는 소문자(az)와 숫자 이외는 포함되지 않습니다.</string>
+ <string name="blog_name_cant_be_used">사이트 주소는 사용하실 수 없습니다.</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">사이트 주소는 64자를 초과합니다.</string>
+ <string name="invalid_password_message">암호는 4자 이상이어야 합니다.</string>
+ <string name="username_must_be_at_least_four_characters">사용자 이름은 4자 이상입니다.</string>
+ <string name="blog_name_must_be_at_least_four_characters">사이트 주소는 4자 이상이어야 합니다.</string>
+ <string name="blog_name_contains_invalid_characters">사이트 주소에 밑줄 기호(_)은 포함되지 않습니다.</string>
+ <string name="username_exists">사용자 이름이 이미 작성 되었습니다.</string>
+ <string name="email_exists">그 메일 주소는 이미 사용되고 있습니다.</string>
+ <string name="username_required">사용자 이름을 입력하십시오.</string>
+ <string name="username_only_lowercase_letters_and_numbers">사용자 이름에는 소문자(az)와 숫자 이외는 포함되지 않습니다.</string>
+ <string name="username_must_include_letters">사용자 이름에는 영문자 소문자(az)를 반드시 포함해야합니다.</string>
+ <string name="blog_name_required">사이트 이름을 입력 하십시오.</string>
+ <string name="username_reserved_but_may_be_available">이 사용자 이름은 현재 예약되었지만, 며칠 후에 사용할 수 있게 될지도 모릅니다.</string>
+ <string name="stats_empty_comments">코멘트가 없습니다.</string>
+ <string name="gallery_error">미디어 항목을 로드 할 수 없습니다.</string>
+ <string name="passcode_wrong_passcode">PIN 번호가 잘못되었습니다 . 다시 시도 하십시오.</string>
+ <string name="theme_set_failed">테마의 설정에 실패했습니다. 다시 시도하십시오.</string>
+ <string name="theme_auth_error_message">귀하의 계정에 테마 설정을 할 권한이 있는지 확인하십시오.</string>
+ <string name="error_downloading_image">이미지를 다운로드하는 중에 오류가 발생했습니다. 다시 시도하십시오.</string>
+ <string name="theme_fetch_failed">테마 정보를 가져오는데 실패했습니다. 다시 시도하십시오.</string>
+ <string name="stats_bar_graph_empty">통계 정보가 없습니다.</string>
+ <string name="error_load_comment">코멘트 를 로드 할 수 없습니다. 다시 시도 하십시오.</string>
+ <string name="reply_failed">회신 할 수 없습니다 .</string>
+ <string name="error_generic">오류가 발생 했습니다. 나중에 다시 시도 하십시오.</string>
+ <string name="error_edit_comment">코멘트 편집 할 때 오류가 발생 했습니다. 나중에 다시 시도 하십시오.</string>
+ <string name="error_upload">%s을 업로드 할 때 오류가 발생 했습니다.</string>
+ <string name="out_of_memory">장치 메모리가 부족합니다.</string>
+ <string name="no_site_error">WordPress 사이트에 연결할 수 없습니다 . 잠시 후 다시 시도해 주십시오.</string>
+ <string name="no_network_message">사용 가능한 네트워크가 없습니다. 네트워크에 연결하고 다시 시도하십시오 .</string>
+ <string name="sdcard_message">미디어를 업로드하려면 SD카드가 필요합니다.</string>
+ <string name="no_account">WordPress 계정을 찾을 수 없습니다. 계정을 추가하고 다시 시도하십시오.</string>
+ <string name="cat_name_required">카테고리 이름은 필수 항목입니다.</string>
+ <string name="adding_cat_success">카테고리를 추가했습니다.</string>
+ <string name="adding_cat_failed">카테고리 추가에 실패했습니다.</string>
+ <string name="error_delete_post">%s를 삭제하는 동안 에러가 발생했습니다.</string>
+ <string name="error_moderate_comment">진행중에 에러가 발생했습니다.</string>
+ <string name="could_not_remove_account">사이트를 제거할 수 없습니다.</string>
+ <string name="error_refresh_stats">통계를 다시 로드 할 수 없습니다.</string>
+ <string name="invalid_url_message">입력한 URL이 올바른지 확인합니다.</string>
+ <string name="media_error_no_permission">미디어 라이브러리를 볼 수 있는 권한이 없습니다</string>
+ <string name="share_action_media">미디어 라이브러리</string>
+ <string name="page_settings">페이지 설정</string>
+ <string name="post_settings">작성 설정</string>
+ <string name="error_blog_hidden">이 블로그는 비공개 설정이되어 로드 할 수 없습니다. 설정 화면에서 다시 활성하고 다시 시도하십시오.</string>
+ <string name="add_account_blog_url">블로그 주소</string>
+ <string name="learn_more">추가 정보</string>
+ <string name="site_address">인스톨 형 WordPress의 주소(URL)</string>
+ <string name="cannot_delete_multi_media_items">일부 미디어를 다시로드 할 수 없습니다. 나중에 다시 시도하십시오.</string>
+ <string name="http_authorization_required">인증이 필요합니다</string>
+ <string name="reader_title_applog">응용 프로그램 로그</string>
+ <string name="reader_share_link">링크를 공유</string>
+ <string name="email_hint">이메일</string>
+ <string name="blog_title_invalid">잘못된 사이트 제목</string>
+ <string name="blog_name_invalid">잘못된 사이트 주소</string>
+ <string name="blog_name_must_include_letters">사이트 주소는 반각 영문자의 소문자를 포함해야합니다.</string>
+ <string name="email_reserved">그 메일 주소는 이미 사용되고 있습니다. 활성화 메일이 도착해 있지 않은가 체크해 주세요. 활성화하지 않으면 며칠 후 같은 주소로 다시 등록 할 수 있습니다.</string>
+ <string name="email_cant_be_used_to_signup">지정된 이메일 주소를 등록 제공되지 않습니다. 이 메일 공급자는 여기로부터의 메일이 차단되는 현상이 발생하고 있기 때문에 다른 공급자의 이메일 주소를 입력하십시오.</string>
+ <string name="media_gallery_num_columns">열 수</string>
+ <string name="media_gallery_type_thumbnail_grid">썸네일 그리드</string>
+ <string name="media_gallery_settings_title">갤러리 설정</string>
+ <string name="media_gallery_edit">갤러리 를 편집</string>
+ <string name="themes_live_preview">실시간 미리보기</string>
+ <string name="theme_current_theme">현재 테마</string>
+ <string name="theme_premium_theme">프리미엄 테마</string>
+ <string name="share_action_post">새 게시물</string>
+ <string name="new_media">새 미디어</string>
+ <string name="media_gallery_image_order">이미지 순서</string>
+ <string name="comment_added">댓글 추가에 성공했습니다</string>
+ <string name="add_comment">댓글을 추가</string>
+ <string name="edit_comment">코멘트를 편집</string>
+ <string name="view_site">사이트보기</string>
+ <string name="open_source_licenses">오픈 소스 라이센스</string>
+ <string name="http_credentials">HTTP 로그인 정보(옵션)</string>
+ <string name="file_error_create">미디어 업로드시에 임시 파일을 만들 수 없습니다 . 장치에 여유 공간이 있는지 확인 하십시오.</string>
+ <string name="post_format">작성 형식</string>
+ <string name="category_name">카테고리 이름</string>
+ <string name="category_slug">카테고리 슬러그(옵션)</string>
+ <string name="category_desc">카테고리 설명(옵션)</string>
+ <string name="file_not_found">올라간 미디어 파일을 찾을 수 없습니다. 삭제 또는 이동 된 것 같습니다.</string>
+ <string name="horizontal_alignment">가로</string>
+ <string name="image_settings">이미지 설정</string>
+ <string name="wordpress_blog">WordPress 블로그</string>
+ <string name="sure_to_cancel_edit_comment">이 코멘트 수정을 취소하시겠습니까?</string>
+ <string name="xmlrpc_error">연결할 수 없습니다. 사이트에 xmlrpc.php의 전체 경로를 입력하고 다시 시도하십시오.</string>
+ <string name="local_changes">로컬 변경</string>
+ <string name="scaled_image_error">올바른 이미지의 너비를 입력 하십시오</string>
+ <string name="author_name">이름</string>
+ <string name="author_email">이메일</string>
+ <string name="author_url">URL</string>
+ <string name="content_required">댓글 본문은 필수 항목입니다.</string>
+ <string name="saving_changes">변경 사항을 저장 중</string>
+ <string name="location_not_found">위치 정보를 찾을 수 없습니다</string>
+ <string name="create_a_link">링크 만들기</string>
+ <string name="link_enter_url_text">링크 텍스트(옵션) :</string>
+ <string name="privacy_policy">개인 정보 보호 정책</string>
+ <string name="local_draft">로컬 초안</string>
+ <string name="upload_failed">업로드에 실패했습니다.</string>
+ <string name="view_in_browser">브라우저에서 표시</string>
+ <string name="post_not_published">이 게시물은 아직 공개되지 않았습니다</string>
+ <string name="page_not_published">이 페이지는 아직 공개되지 않았습니다</string>
+ <string name="pending_review">리뷰 진행중</string>
+ <string name="delete_post">게시물 삭제</string>
+ <string name="delete_page">페이지를 제거</string>
+ <string name="account_details">계정 정보</string>
+ <string name="required_field">필수 항목</string>
+ <string name="category_parent">부모 카테고리(옵션):</string>
+ <string name="add_new_category">새로운 카테고리를 추가</string>
+ <string name="select_categories">카테고리를 선택</string>
+ <string name="new_post">새 게시물</string>
+ <string name="preview_post">게시물 미리보기</string>
+ <string name="delete_draft">초안을 제거</string>
+ <string name="edit_post">게시물을 수정</string>
+ <string name="connection_error">연결 오류</string>
+ <string name="cancel_edit">편집을 취소</string>
+ <string name="preview_page">페이지 미리보기</string>
+ <string name="post_not_found">게시물을 로딩중 에러가 발생했습니다. 새로고침 후 다시 시도하세요.</string>
+ <string name="comment_status_approved">승인</string>
+ <string name="comment_status_unapproved">대기중</string>
+ <string name="comment_status_spam">스팸</string>
+ <string name="comment_status_trash">휴지통</string>
+ <string name="mnu_comment_approve">승인</string>
+ <string name="mnu_comment_unapprove">미승인</string>
+ <string name="fatal_db_error">앱 데이터베이스를 생성하는 동안 에러가 발생했습니다. 앱을 다시 설치하시기 바랍니다.</string>
+ <string name="jetpack_message_not_admin">제트 팩 플러그인은 통계가 필요합니다. 사이트 관리자에게 문의하세요.</string>
+ <string name="reader_toast_err_add_tag">태그를 추가할 수 없습니다.</string>
+ <string name="reader_toast_err_remove_tag">태그를 삭제할 수 없습니다.</string>
+ <string name="mnu_comment_spam">스팸</string>
+ <string name="mnu_comment_trash">휴지통</string>
+ <string name="dlg_approving_comments">승인</string>
+ <string name="dlg_spamming_comments">스팸으로 표시</string>
+ <string name="dlg_trashing_comments">휴지통으로 이동</string>
+ <string name="dlg_confirm_trash_comments">휴지통에 보내겠습니까?</string>
+ <string name="trash_yes">휴지통</string>
+ <string name="trash_no">휴지통에 버리지 마세요.</string>
+ <string name="trash">휴지통</string>
+ <string name="hint_comment_content">코멘트</string>
+ <string name="toast_comment_unedited">코멘트를 변경할 수 없습니다.</string>
+ <string name="dlg_unapproving_comments">미승인</string>
+ <string name="blog_removed_successfully">사이트가 제거되었습니다.</string>
+ <string name="remove_account">사이트 제거</string>
+ <string name="notifications_empty_all">알림이 없습니다.</string>
+ <string name="invalid_site_url_message">입력한 사이트 URL이 올바른지 확인합니다.</string>
+ <string name="deleting_page">페이지 삭제</string>
+ <string name="deleting_post">글 삭제</string>
+ <string name="share_url_post">글 공유</string>
+ <string name="share_url_page">페이지 공유</string>
+ <string name="share_link">링크 공유</string>
+ <string name="creating_your_site">사이트를 만들 수 있습니다</string>
+ <string name="creating_your_account">계정을 생성 합니다</string>
+ <string name="reader_empty_posts_in_tag_updating">리뷰 가져 오는 중 ...</string>
+ <string name="error_refresh_media">미디어 라이브러리를 새로고침 할때 오류가 발생했습니다. 나중에 다시 시도하십시오.</string>
+ <string name="comment_spammed">댓글이 스팸으로 처리됨</string>
+ <string name="download">미디어를 다운로드 중</string>
+ <string name="reader_label_reply">회신</string>
+ <string name="video">동영상</string>
+ <string name="reader_toast_err_get_comment">이 코멘트를 읽을 수 없습니다</string>
+ <string name="cant_share_no_visible_blog">공개 블로그가 없기 때문에 WordPress에 공유 할 수 없습니다</string>
+ <string name="reader_likes_you_and_multi">당신에게 %d명이 "좋아요"를 하고 있습니다.</string>
+ <string name="reader_likes_multi">%d명이 "좋아요"를 눌렀습니다.</string>
+ <string name="pick_photo">이미지를 선택</string>
+ <string name="pick_video">동영상을 선택</string>
+ <string name="select_date">날짜를 선택</string>
+ <string name="select_time">시간을 선택</string>
+ <string name="validating_site_data">사이트의 데이터를 체크 합니다</string>
+ <string name="validating_user_data">사용자의 데이터를 체크 합니다</string>
+ <string name="reader_toast_err_get_post">이 게시물 을 로드 할 수 없습니다</string>
+ <string name="reader_likes_you_and_one">당신과 다른 1명이 "좋아요"를 눌렀습니다.</string>
+ <string name="account_two_step_auth_enabled">이 계정에서 2단계 인증이 활성화되어 있습니다. WordPress.com의 보안 설정 페이지에서 응용 프로그램 암호를 생성하십시오.</string>
+ <string name="reader_empty_followed_blogs_description">걱정하지 마세요. 둘러보려면 태그 아이콘을 누르세요!</string>
+ <string name="nux_welcome_create_account">계정 만들기</string>
+ <string name="nux_add_selfhosted_blog">인스톨형 WordPress 블로그를 추가</string>
+ <string name="nux_oops_not_selfhosted_blog">WordPress.com에 로그인</string>
+ <string name="nux_tap_continue">다음</string>
+ <string name="password_invalid">보안 강도 높은 암호가 필요합니다. 7자 이상의 대문자/소문자와 숫자와 기호를 포함하는 것을 추천합니다.</string>
+ <string name="signing_in">로그인 중...</string>
+ <string name="media_add_new_media_gallery">갤러리 만들기</string>
+ <string name="select_from_media_library">미디어 라이브러리에서 선택</string>
+ <string name="reader_btn_share">공유</string>
+ <string name="reader_btn_follow">팔로우</string>
+ <string name="reader_toast_err_url_intent">%s를 열 수 없습니다</string>
+ <string name="empty_list_default">이 목록에는 아무것도 포함되어 있지 않습니다.</string>
+ <string name="nux_tutorial_get_started_title">시작 하자</string>
+ <string name="media_add_popup_title">미디어 라이브러리 에 추가</string>
+ <string name="reader_untitled_post">(제목 없음)</string>
+ <string name="reader_btn_unfollow">팔로잉</string>
+ <string name="reader_label_added_tag">%s를 추가</string>
+ <string name="reader_label_removed_tag">%s를 제거</string>
+ <string name="reader_likes_only_you">"좋아요"를 클릭 했습니다</string>
+ <string name="username_invalid">잘못된 사용자 이름</string>
+ <string name="reader_share_subject">%s에서 공유</string>
+ <string name="jetpack_not_found">Jetpack 플러그인 을 찾을 수 없습니다</string>
+ <string name="reader_toast_err_view_image">이미지를 표시 할 수 없습니다</string>
+ <string name="reader_toast_err_tag_exists">이미 이 태그를 팔로우 중입니다</string>
+ <string name="reader_toast_err_tag_invalid">유효한 태그가 없습니다</string>
+ <string name="reader_toast_err_share_intent">공유에 실패</string>
+ <string name="jetpack_message">통계를 보려면 Jetpack 플러그인이 필요합니다. Jetpack을 설치하시겠습니까 ?</string>
+ <string name="reader_likes_one">1명이 "좋아요"를 달았습니다</string>
+ <string name="reader_toast_err_comment_failed">의견을 게시 할 수 없습니다</string>
+ <string name="connecting_wpcom">WordPress.com에 연결 중</string>
+ <string name="limit_reached">한도에 도달했습니다. 1분 후에 다시 시도해 수 있습니다. 이전에 로그인을 시도하면 제한 해제까지의 시간이 더 길어집니다. 오류라고 생각되면 지원에 문의하십시오.</string>
+ <string name="reader_empty_followed_tags">태그를 팔로우하지 않습니다.</string>
+ <string name="create_account_wpcom">WordPress.com에서 계정 만들기</string>
+ <string name="reader_hint_comment_on_comment">답변 달기</string>
+ <string name="button_next">다음</string>
+ <string name="themes">테마</string>
+ <string name="images">이미지</string>
+ <string name="media_gallery_image_order_random">랜덤</string>
+ <string name="media_gallery_type_circles">원형</string>
+ <string name="media_edit_title_text">제목</string>
+ <string name="media_edit_caption_text">캡션</string>
+ <string name="media_edit_description_text">설명</string>
+ <string name="themes_features_label">기능</string>
+ <string name="theme_activate_button">활성화</string>
+ <string name="theme_activating_button">활성화 중</string>
+ <string name="stats_view_clicks">클릭 수</string>
+ <string name="stats_view_tags_and_categories">태그 &amp; 카테고리</string>
+ <string name="stats_timeframe_today">오늘</string>
+ <string name="stats_timeframe_yesterday">어제</string>
+ <string name="stats_timeframe_weeks">주</string>
+ <string name="stats_timeframe_months">월</string>
+ <string name="stats_timeframe_days">일</string>
+ <string name="stats_entry_country">국가</string>
+ <string name="share_action">공유</string>
+ <string name="stats">통계</string>
+ <string name="stats_entry_posts_and_pages">제목</string>
+ <string name="media_gallery_type_squares">사각 타일</string>
+ <string name="media_gallery_type_tiled">타일 모자이크</string>
+ <string name="media_gallery_image_order_reverse">순서를 반대로 하는</string>
+ <string name="media_gallery_type">표시 형식</string>
+ <string name="media_gallery_type_slideshow">슬라이드 쇼</string>
+ <string name="all">모든</string>
+ <string name="unattached">미사용</string>
+ <string name="media_edit_title_hint">제목을 입력</string>
+ <string name="stats_entry_tags_and_categories">항목</string>
+ <string name="stats_entry_authors">등록자</string>
+ <string name="stats_entry_referrers">리퍼러</string>
+ <string name="stats_totals_views">보기</string>
+ <string name="stats_totals_clicks">클릭 수</string>
+ <string name="stats_totals_plays">조회</string>
+ <string name="media_add_popup_capture_photo">사진을 촬영</string>
+ <string name="media_add_popup_capture_video">동영상을 촬영</string>
+ <string name="media_edit_success">업데이트되었습니다</string>
+ <string name="themes_details_label">정보</string>
+ <string name="stats_view_referrers">리퍼러</string>
+ <string name="media_edit_failure">업데이트에 실패</string>
+ <string name="media_edit_description_hint">설명을 입력</string>
+ <string name="media_edit_caption_hint">캡션을 입력</string>
+ <string name="passcode_preference_title">PIN 번호 잠금</string>
+ <string name="passcode_turn_on">PIN 번호 잠금을 활성화</string>
+ <string name="passcode_turn_off">PIN 번호 잠금을 해제</string>
+ <string name="passcode_set">PIN 번호 변경</string>
+ <string name="passcode_re_enter_passcode">PIN 번호 다시 입력</string>
+ <string name="passcode_change_passcode">PIN 번호 변경</string>
+ <string name="passcode_enter_old_passcode">이전 PIN 번호를 입력</string>
+ <string name="passcode_enter_passcode">PIN 번호를 입력</string>
+ <string name="passcode_manage">PIN 번호 잠금 관리</string>
+ <string name="stats_view_visitors_and_views">방문자수 · 페이지 뷰</string>
+ <string name="post_excerpt">발췌</string>
+ <string name="theme_set_success">테마 설정을 완료 했습니다.</string>
+ <string name="share_action_title">추가 정보</string>
+ <string name="theme_auth_error_title">테마 정보를 가져오는데 실패했습니다</string>
+ <string name="custom_date">사용자 지정 날짜</string>
+ <string name="upload">업로드</string>
+ <string name="discard">제거</string>
+ <string name="sign_in">로그인</string>
+ <string name="notifications">알림</string>
+ <string name="follows">팔로우</string>
+ <string name="note_reply_successful">답장을 공개했습니다</string>
+ <string name="more_notifications">%d개.</string>
+ <string name="new_notifications">%d개의 새로운 알림</string>
+ <string name="loading">로드 중입니다...</string>
+ <string name="httpuser">HTTP 사용자 이름</string>
+ <string name="httppassword">HTTP 암호</string>
+ <string name="error_media_upload">미디어를 업로드할때 오류가 발생했습니다</string>
+ <string name="post_content">콘텐츠 (눌러 텍스트 미디어를 추가)</string>
+ <string name="publish_date">공개</string>
+ <string name="content_description_add_media">미디어를 추가</string>
+ <string name="incorrect_credentials">사용자 이름이나 암호가 올바르지 않습니다 .</string>
+ <string name="password">암호</string>
+ <string name="username">사용자 이름</string>
+ <string name="reader">가입 블로그</string>
+ <string name="pages">페이지</string>
+ <string name="anonymous">익명</string>
+ <string name="page">페이지</string>
+ <string name="featured">같은 기능을 갖춘 이미지를 사용</string>
+ <string name="featured_in_post">게시물 본문에 이미지를 포함</string>
+ <string name="no_network_title">사용 가능한 네트워크가 없습니다</string>
+ <string name="caption">캡션(옵션)</string>
+ <string name="width">가로</string>
+ <string name="posts">작성</string>
+ <string name="post">작성</string>
+ <string name="ok">확인</string>
+ <string name="blogusername">blogusername</string>
+ <string name="scaled_image">변경 후의 이미지 폭</string>
+ <string name="upload_scaled_image">업로드 하여 크기를 조정 한 이미지 링크</string>
+ <string name="scheduled">예약</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">업로드 중...</string>
+ <string name="version">버전</string>
+ <string name="tos">이용 약관</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">기본 이미지 너비</string>
+ <string name="image_alignment">배치</string>
+ <string name="refresh">다시 로드</string>
+ <string name="untitled">제목 미설정</string>
+ <string name="edit">편집</string>
+ <string name="post_id">작성</string>
+ <string name="page_id">고정 페이지</string>
+ <string name="post_password">암호(옵션)</string>
+ <string name="immediately">즉시</string>
+ <string name="quickpress_add_alert_title">바로 가기 이름을 설정</string>
+ <string name="today">오늘</string>
+ <string name="settings">설정</string>
+ <string name="share_url">URL을 공유</string>
+ <string name="quickpress_window_title">QuickPress 바로가기에 대한 블로그를 선택</string>
+ <string name="quickpress_add_error">바로가기 이름을 입력하세요</string>
+ <string name="publish_post">공개</string>
+ <string name="draft">초안</string>
+ <string name="post_private">비공개</string>
+ <string name="upload_full_size_image">업로드하여 풀 사이즈의 이미지 링크</string>
+ <string name="title">제목</string>
+ <string name="categories">카테고리</string>
+ <string name="tags_separate_with_commas">태그(쉼표로 구분)</string>
+ <string name="dlg_deleting_comments">댓글 삭제 중</string>
+ <string name="notification_blink">알림빛을 깜빡임</string>
+ <string name="notification_vibrate">바이브</string>
+ <string name="notification_sound">알림 소리</string>
+ <string name="status">상태</string>
+ <string name="location">위치</string>
+ <string name="sdcard_title">SD카드가 필요합니다</string>
+ <string name="select_video">갤러리에서 비디오를 선택</string>
+ <string name="media">미디어</string>
+ <string name="delete">제거</string>
+ <string name="none">없음</string>
+ <string name="blogs">블로그</string>
+ <string name="select_photo">갤러리에서 사진을 선택</string>
+ <string name="no">아니오</string>
+ <string name="yes">예</string>
+ <string name="error">오류</string>
+ <string name="cancel">취소</string>
+ <string name="save">저장</string>
+ <string name="add">추가</string>
+ <string name="preview">미리보기</string>
+ <string name="reply">회신</string>
+ <string name="on">선택</string>
+ <string name="category_refresh_error">카테고리의 다시 읽기 오류</string>
+ <string name="notification_settings">알림 설정</string>
+</resources>
diff --git a/WordPress/src/main/res/values-land/integers.xml b/WordPress/src/main/res/values-land/integers.xml
new file mode 100644
index 000000000..56f51fcdf
--- /dev/null
+++ b/WordPress/src/main/res/values-land/integers.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="media_grid_num_columns">3</integer>
+</resources>
diff --git a/WordPress/src/main/res/values-land/styles.xml b/WordPress/src/main/res/values-land/styles.xml
new file mode 100644
index 000000000..1e0cf3a9e
--- /dev/null
+++ b/WordPress/src/main/res/values-land/styles.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="WordPressWelcomeStyle">
+ <item name="android:paddingTop">20dp</item>
+ <item name="android:paddingBottom">20dp</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values-large-hdpi/dimens.xml b/WordPress/src/main/res/values-large-hdpi/dimens.xml
new file mode 100644
index 000000000..fe4373e40
--- /dev/null
+++ b/WordPress/src/main/res/values-large-hdpi/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="format_bar_height">40dp</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-large-hdpi/styles.xml b/WordPress/src/main/res/values-large-hdpi/styles.xml
new file mode 100644
index 000000000..261eb9555
--- /dev/null
+++ b/WordPress/src/main/res/values-large-hdpi/styles.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="WordPressTitleAppearance" parent="@android:style/TextAppearance">
+ <item name="android:singleLine">true</item>
+ <item name="android:shadowColor">#BB000000</item>
+ <item name="android:shadowRadius">2.75</item>
+ <item name="android:textColor">#FFF6F6F6</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values-large-tvdpi/dimens.xml b/WordPress/src/main/res/values-large-tvdpi/dimens.xml
new file mode 100644
index 000000000..43ca58511
--- /dev/null
+++ b/WordPress/src/main/res/values-large-tvdpi/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="settings_padding">60dp</dimen>
+ <dimen name="format_bar_height">50dp</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-mk/strings.xml b/WordPress/src/main/res/values-mk/strings.xml
new file mode 100644
index 000000000..af97559b4
--- /dev/null
+++ b/WordPress/src/main/res/values-mk/strings.xml
@@ -0,0 +1,507 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="stats_months_and_years">Месеци и години</string>
+ <string name="error_copy_to_clipboard">Се случи грешка при копирање на текстот во клипбордот</string>
+ <string name="stats_recent_weeks">Последни недели</string>
+ <string name="stats_average_per_day">Средна вредност на ден</string>
+ <string name="reader_empty_posts_in_blog">Овој блог е празен</string>
+ <string name="reader_label_new_posts">Нови написи</string>
+ <string name="logs_copied_to_clipboard">Логовите на апликацијата се копирани во клипбордот</string>
+ <string name="stats_period">Период</string>
+ <string name="stats_overall">Севкупно</string>
+ <string name="stats_total">Вкупно</string>
+ <string name="post_uploading">Прикачување</string>
+ <string name="stats_empty_top_authors_desc">Пратете ги гледањата на написите на секој соработник и откријте ја најпопуларната содржина на секој автор.</string>
+ <string name="stats_empty_comments_desc">Доколку дозволите коментари на вашата страница, пратете ги вашите најдобри коментатори и откријте што ја распламтува конверзацијата врз база на последните 1000 коментари.</string>
+ <string name="stats_followers_total_email">Вкупно Е-Пошта Следбеници: %1$s</string>
+ <string name="stats_timeframe_years">Години</string>
+ <string name="stats_followers_days">%1$d денови</string>
+ <string name="stats_followers_a_day">Ден</string>
+ <string name="stats_views">Гледања</string>
+ <string name="stats_followers_a_month">Месец</string>
+ <string name="stats_visitors">Посетители</string>
+ <string name="stats_empty_followers">Нема следбеници</string>
+ <string name="stats_empty_tags_and_categories">Нема гледано означени написи или страници</string>
+ <string name="stats_empty_publicize_desc">Пратете ги вашите следбеници од повеќе социјални мрежи</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_entry_publicize">Сервис</string>
+ <string name="stats_empty_video_desc">Доколку прикачувате видеа со VideoPress, дознајте колку пати се гледани.</string>
+ <string name="stats_empty_publicize">Нема записи за следбеници на објавата</string>
+ <string name="stats_followers_months">%1$d месеци</string>
+ <string name="stats_view_top_posts_and_pages">Написи и страници</string>
+ <string name="stats_empty_top_posts_desc">Откријте која содржина е најгледана и како индивидуални написи и страници се извршуваат од време на време.</string>
+ <string name="stats_empty_tags_and_categories_desc">Земете преглед на најпопуларните теми на вашата веб страница врз база на најдобрите написи од минатата недела.</string>
+ <string name="stats_view_publicize">Објавување</string>
+ <string name="stats_likes">Допаѓања</string>
+ <string name="stats_empty_followers_desc">Пратете го вашиот вкупен број на следбеници и колку време секој од нив ја следи вашата веб страница.</string>
+ <string name="stats_empty_referrers_desc">Научете повеќе за видливоста на вашата страница со прегледување на веб страниците и пребарувачите што испраќаат најмногу сообраќај кон вас</string>
+ <string name="stats_empty_geoviews_desc">Прегледајте ја листата да видете кои земји генерираат најмнгу сообраќај до вашата страница.</string>
+ <string name="stats_pagination_label">Страница %1$s од %2$s</string>
+ <string name="stats_view_videos">Видеа</string>
+ <string name="stats_entry_followers">Следбеник</string>
+ <string name="stats_empty_clicks_desc">Кога вашата содржина вклучува врски до други страници, може да видете на кои корисникот клика најмногу.</string>
+ <string name="stats_empty_clicks_title">Нема зачувано кликови</string>
+ <string name="stats_followers_a_minute_ago">пред една минута</string>
+ <string name="stats_followers_hours">%1$d часови</string>
+ <string name="stats_followers_minutes">%1$d минути</string>
+ <string name="stats_empty_geoviews">Нема зачувано земји</string>
+ <string name="stats_view_all">Види ги сите</string>
+ <string name="stats_followers_an_hour_ago">пред еден час</string>
+ <string name="stats_followers_years">%1$d години</string>
+ <string name="stats_comments_by_authors">По автори</string>
+ <string name="stats_empty_top_posts_title">Нема гледано написи или страници</string>
+ <string name="stats_view_countries">Земји</string>
+ <string name="stats_view_followers">Следбеници</string>
+ <string name="stats_followers_email_selector">Е-пошта</string>
+ <string name="stats_followers_seconds_ago">пред секунди</string>
+ <string name="stats_totals_publicize">Следбеници</string>
+ <string name="stats_entry_video_plays">Видео</string>
+ <string name="stats_entry_clicks_link">Врска</string>
+ <string name="stats_empty_video">Не се пуштани видеа</string>
+ <string name="stats_comments_by_posts_and_pages">По написи и страници</string>
+ <string name="stats_empty_referrers_title">Нема пренасочувачи</string>
+ <string name="stats_totals_followers">Од</string>
+ <string name="stats_followers_a_year">Година</string>
+ <string name="stats_followers_total_wpcom">Вкупен број на WordPress.com следбеници: %1$s</string>
+ <string name="stats_entry_top_commenter">Автор</string>
+ <string name="stats_view">Прегледи</string>
+ <string name="stats_other_recent_stats_label">Други последни статистики</string>
+ <string name="stats_for">Статистики за %s</string>
+ <string name="themes_fetching">Примање теми...</string>
+ <string name="ssl_certificate_details">Детали</string>
+ <string name="delete_sure_post">Избриши напис</string>
+ <string name="delete_sure">Избриши овој драфт</string>
+ <string name="confirm_delete_multi_media">Избриши слектирни?</string>
+ <string name="confirm_delete_media">Избриши селектирано</string>
+ <string name="cab_selected">%d слекетирано</string>
+ <string name="media_gallery_date_range">Покажи медиа од %1$s до %2$s</string>
+ <string name="delete_sure_page">Избриши оваа страница</string>
+ <string name="reader_empty_posts_liked">Сеуште немате допаднити написи</string>
+ <string name="faq_button">ЧПП</string>
+ <string name="nux_help_description">Посети го центарот за помош за да добиеш одговори на веќе поставени прашања или посети го форумот за да поставиш нови</string>
+ <string name="browse_our_faq_button">Разгледи наши ЧПП</string>
+ <string name="create_new_blog_wpcom">Создади WordPress.com блог</string>
+ <string name="new_blog_wpcom_created">WordPress.com блог создаден!</string>
+ <string name="reader_empty_comments">Нема коментари сеуште</string>
+ <string name="agree_terms_of_service">Со креирање на сметка вие се согласувате на фасцинирачките %1$sУслови за користење%2$s</string>
+ <string name="reader_label_comment_count_multi">%,d коментари</string>
+ <string name="reader_label_view_original">Види ја оригиналната статија</string>
+ <string name="reader_empty_posts_in_tag">Нема постови со оваа ознака</string>
+ <string name="reader_label_like">Сака</string>
+ <string name="reader_label_comment_count_single">Еден коментар</string>
+ <string name="reader_label_comments_closed">Коментари затворени</string>
+ <string name="reader_label_comments_on">Коментари на</string>
+ <string name="error_refresh_unauthorized_pages">Немате дозвола за гледање или уредување на страници</string>
+ <string name="error_refresh_unauthorized_comments">Немате дозвола за гледање или уредување на коментари</string>
+ <string name="error_refresh_unauthorized_posts">Немате дозвола за гледање или уредување на написи</string>
+ <string name="error_publish_empty_post">Неможе да се објави празен напис</string>
+ <string name="reader_title_photo_viewer">%1$d of %2$d</string>
+ <string name="older_month">Постари од месец</string>
+ <string name="more">Повеќе</string>
+ <string name="older_two_days">Постари од 2 дена</string>
+ <string name="older_last_week">Постари од седмица</string>
+ <string name="stats_no_blog">Статиска неможе да се покаже за побараниот блог</string>
+ <string name="select_a_blog">Избери WordPress страница</string>
+ <string name="sending_content">Испраќа %s содржина</string>
+ <string name="uploading_total">Испраќа %1$d of %2$d</string>
+ <string name="mnu_comment_liked">Допаднато</string>
+ <string name="comment">Коментар</string>
+ <string name="posts_empty_list">Сеуште нема написи. Зошто не создадете еден?</string>
+ <string name="comment_reply_to_user">Одговори на %s</string>
+ <string name="pages_empty_list">Сеуште нема страници. Зошто не создадете една?</string>
+ <string name="media_empty_list_custom_date">Нема медиа во овој временски интервал</string>
+ <string name="posting_post">Објавување "%s"</string>
+ <string name="signing_out">Одјавување...</string>
+ <string name="comment_trashed">Комента исфрлен</string>
+ <string name="reader_toast_err_generic">Неможе да се изврши ова дејство</string>
+ <string name="reader_toast_err_block_blog">Неможе да се блокира овој блог</string>
+ <string name="reader_toast_blog_blocked">Написи од овој блог повеќе нема да се прикажуваат</string>
+ <string name="reader_menu_block_blog">Блокирај го овој блог</string>
+ <string name="hs__conversation_detail_error">Опиши го проблемот кој го гледаш</string>
+ <string name="hs__new_conversation_header">Поддршка разговор</string>
+ <string name="hs__conversation_header">Поддршка разговор</string>
+ <string name="hs__username_blank_error">Воведи валидно име</string>
+ <string name="hs__invalid_email_error">Воведи валидна е-адреса</string>
+ <string name="contact_us">Контактирај нè</string>
+ <string name="add_location">Додади локација</string>
+ <string name="current_location">Моментална локација</string>
+ <string name="search_location">Пребарувај</string>
+ <string name="edit_location">Уреди</string>
+ <string name="search_current_location">Лоцирај</string>
+ <string name="preference_send_usage_stats">Испрати статистика</string>
+ <string name="preference_send_usage_stats_summary">Автоматски испраќај статистика од употребата да ни помогнеш да го подобриме WordPress за Андроид</string>
+ <string name="update_verb">Ажурирај</string>
+ <string name="schedule_verb">Закажи</string>
+ <string name="reader_title_subs">Ознаки и Блогови</string>
+ <string name="reader_page_followed_tags">Проследени ознаки</string>
+ <string name="reader_label_followed_blog">Блогот се следи</string>
+ <string name="reader_label_tag_preview">Означени написи %s</string>
+ <string name="reader_toast_err_get_blog_info">Неможе да се прикаже овој блог</string>
+ <string name="reader_toast_err_already_follow_blog">Веќе го следите овој блог</string>
+ <string name="reader_toast_err_follow_blog">Неможе да се следи овој блог</string>
+ <string name="reader_toast_err_unfollow_blog">Неможе да се прекине следењето на овој блог</string>
+ <string name="reader_empty_recommended_blogs">Нема препорачани блогови</string>
+ <string name="saving">Зачувување...</string>
+ <string name="media_empty_list">Нема медиа</string>
+ <string name="ptr_tip_message">Совет: Повлечи надолу за освежување</string>
+ <string name="help">Помош</string>
+ <string name="forgot_password">Ја изгубивте Вашата лозинка?</string>
+ <string name="help_center">Центар за помош</string>
+ <string name="forums">Форуми</string>
+ <string name="ssl_certificate_error">Невалиден SSl сертификат</string>
+ <string name="ssl_certificate_ask_trust">Доколку вообичаено се поврзувате на страницата без проблеми, оваа грешка може да значи дека некој се обидува да ја имитира вашата страница и не треба да продолжите. Дали му верувате на сертификатот и покрај тоа?</string>
+ <string name="comments_empty_list">Нема коментари</string>
+ <string name="mnu_comment_unspam">Не е спам</string>
+ <string name="adding_cat_success">Категоријата е успешно додадена</string>
+ <string name="stats_empty_comments">Сеуште нема коментари</string>
+ <string name="notifications_empty_list">Нема известувања</string>
+ <string name="error_generic">Се појави грешка</string>
+ <string name="passcode_wrong_passcode">Погрешен PIN</string>
+ <string name="out_of_memory">Уредот е остана без меморија</string>
+ <string name="no_network_message">Нема досптана мрежа</string>
+ <string name="wait_until_upload_completes">Почекај додека заврши испраќањето</string>
+ <string name="theme_fetch_failed">Неможе да ги прикаже темите</string>
+ <string name="theme_set_failed">Неможе да промени тема</string>
+ <string name="theme_auth_error_message">Осигурете се дека имате привилегија за поставување на теми</string>
+ <string name="invalid_email_message">Вашата е-пошта е невалидна</string>
+ <string name="no_site_error">Неможе да се поврза со страницата на WordPress</string>
+ <string name="invalid_password_message">Лозинка мора да содржи барем 4 знаци</string>
+ <string name="invalid_username_too_short">Корисничкото име мора да содржи барем 4 знаци</string>
+ <string name="adding_cat_failed">Додавањето категорија е неуспешно</string>
+ <string name="cat_name_required">Полето со името на категоријата е задолжително</string>
+ <string name="invalid_username_too_long">Корисничкотое е мора да содржи помалку од 61 знак</string>
+ <string name="email_invalid">Воведи валидна е-поштта</string>
+ <string name="email_not_allowed">Оваа е-пошта не е дозволена</string>
+ <string name="username_exists">Корсиничкото име веќе постои</string>
+ <string name="email_exists">Оваа е-поштта веќе се користи</string>
+ <string name="blog_name_required">Воведи веб-адреса</string>
+ <string name="category_automatically_renamed">Името на категоријата %1$s не е валидно. Преименувано е во %2$s.</string>
+ <string name="blog_name_not_allowed">Веб-адресата не е дозволена</string>
+ <string name="blog_name_must_be_at_least_four_characters">Веб адресата мора да содржи барем 4 знака</string>
+ <string name="no_account">Не е пронајдена WordPress корисничка сметка, додади корисничка сметка и обиди се повторно</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Веб адреста треба да има помалку од 64 знака</string>
+ <string name="blog_name_contains_invalid_characters">Веб адресата не треба да содржи знак “_”</string>
+ <string name="blog_name_cant_be_used">Вие не може да ја користите оваа веб адреса</string>
+ <string name="blog_name_exists">Оваа веб адреса веќе постои</string>
+ <string name="blog_name_reserved">Оваа веб адреса е резервирана</string>
+ <string name="username_or_password_incorrect">Корисничкото име или лозинката се неточни</string>
+ <string name="error_downloading_image">Грешно превземање на слика</string>
+ <string name="username_required">Воведи корисничко име</string>
+ <string name="username_not_allowed">Корисничкото име е не дозоволено</string>
+ <string name="username_must_be_at_least_four_characters">Корисничкото име мора да има барем 4 знака</string>
+ <string name="username_contains_invalid_characters">Корсисничкото име не треба да содржи “_”</string>
+ <string name="error_upload">Се појави грешка додека се испраќаше %s</string>
+ <string name="username_only_lowercase_letters_and_numbers">Корисничкото име може да содржи само мали буви (a-z) или бројки</string>
+ <string name="reply_failed">Не успешен одговор</string>
+ <string name="stats_bar_graph_empty">Нема статистика</string>
+ <string name="error_delete_post">Се појави грешка додека се бришаше %s</string>
+ <string name="sdcard_message">Сврзана SD карта е потребна за качување на медиа</string>
+ <string name="error_load_comment">Неможе да се покаже коментар</string>
+ <string name="nux_cannot_log_in">Неможе да ве најавиме</string>
+ <string name="username_must_include_letters">Корисничкото има треба да има барем 1 буква (a-z)</string>
+ <string name="error_moderate_comment">Настана грешка при модерирање</string>
+ <string name="error_refresh_stats">Статистика неможе да биде освежена во ова време</string>
+ <string name="error_refresh_comments">Коментарите неможат да бидат освежени во овој момент</string>
+ <string name="error_edit_comment">Настана грешка додека се уредуваше коментар</string>
+ <string name="username_reserved_but_may_be_available">Корисничкото име во моментот е резевирано може да биде достпно после неколку дена</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Веб адресата може да содрижи само мали букви (a-z) или бројки</string>
+ <string name="blog_not_found">Настана грешка додека се достапувше блогот</string>
+ <string name="error_refresh_notifications">Известувањата неможат да се ажурират во моментот</string>
+ <string name="error_refresh_pages">Страниците неможат да се ажурират во моментот</string>
+ <string name="error_refresh_posts">Напсите неможат да се ажурират во моментот</string>
+ <string name="blog_name_reserved_but_may_be_available">Ова адреса во моментот е резервирана, но можеби може да биде досптана по неколку денови</string>
+ <string name="gallery_error">Неможе да биде превземена медиата.</string>
+ <string name="edit_post">Измени напис</string>
+ <string name="add_comment">Додади коментар</string>
+ <string name="connection_error">Грешка во поврзувањето</string>
+ <string name="cancel_edit">Откажи промени</string>
+ <string name="location_not_found">Непозната локација</string>
+ <string name="post_not_found">Настана грешка при вчитување на написот. Освежи ги написите и обиди се повторно.</string>
+ <string name="learn_more">Научи повеќе</string>
+ <string name="theme_current_theme">Сегашна тема</string>
+ <string name="theme_premium_theme">Премиум тема</string>
+ <string name="create_a_link">Креирај врска</string>
+ <string name="page_settings">Подесувања на страницата</string>
+ <string name="http_authorization_required">Ауторизација е потребна</string>
+ <string name="upload_failed">Качувањето е неуспешно</string>
+ <string name="mnu_comment_spam">Спам</string>
+ <string name="horizontal_alignment">Хоризонтално порамнување</string>
+ <string name="dlg_confirm_trash_comments">Испрати во корпа?</string>
+ <string name="file_not_found">Не е пронајдена медиа за качување. Дали е избришана или преместена?</string>
+ <string name="post_settings">Подесувања на напис</string>
+ <string name="delete_post">Избриши напис</string>
+ <string name="delete_page">Избриши страница</string>
+ <string name="comment_status_approved">Одобрено</string>
+ <string name="comment_status_unapproved">Чекање</string>
+ <string name="author_name">Име на автор</string>
+ <string name="author_email">Е-пошта на автор</string>
+ <string name="author_url">URL на автор</string>
+ <string name="hint_comment_content">Коментар</string>
+ <string name="saving_changes">Зачувување на подесувањата</string>
+ <string name="sure_to_cancel_edit_comment">Откажи измените на коментарот?</string>
+ <string name="content_required">Коментарот е задолжителен</string>
+ <string name="toast_comment_unedited">Коментарот не е променет</string>
+ <string name="delete_draft">Избриши нацрт</string>
+ <string name="preview_page">Прегледај страница</string>
+ <string name="preview_post">Прегледај напис</string>
+ <string name="image_settings">Подесувања на сликата</string>
+ <string name="add_account_blog_url">Адреса на блогот</string>
+ <string name="wordpress_blog">WordPress блог</string>
+ <string name="select_categories">Избери категории</string>
+ <string name="error_blog_hidden">Овој блог е скриен и неможе да се вчита. Овозможи го повторно во подесувањата и обиди се повторно.</string>
+ <string name="account_details">Детаили за сметка</string>
+ <string name="fatal_db_error">Настана грешка при креирање на базата на податоци за апликацијата. Направи преинсталирање на апликацијата.</string>
+ <string name="jetpack_message_not_admin">Jetpack додатокот е задолжителен за статистика. Контактирајте го администраторот на страницата.</string>
+ <string name="reader_title_applog">Дневник на апликацијата</string>
+ <string name="reader_share_link">Сподели врска</string>
+ <string name="reader_toast_err_add_tag">Неможе да се додаде оваа обележување</string>
+ <string name="reader_toast_err_remove_tag">Неможе да се отстрани оваа обележување</string>
+ <string name="required_field">Задолжително поле</string>
+ <string name="email_hint">Е-пошта</string>
+ <string name="edit_comment">Измени коментар</string>
+ <string name="mnu_comment_approve">Одобри</string>
+ <string name="mnu_comment_trash">Отпадок</string>
+ <string name="dlg_approving_comments">Одобрување</string>
+ <string name="dlg_spamming_comments">Обележување како спам</string>
+ <string name="dlg_trashing_comments">Испраќање во отпадок</string>
+ <string name="trash_yes">Отпадок</string>
+ <string name="trash_no">Не исфрлај</string>
+ <string name="trash">Отпадок</string>
+ <string name="comment_added">Коментарот е додаден успешно</string>
+ <string name="post_not_published">Статусот на написот не е објавен</string>
+ <string name="page_not_published">Статусот на страницата не е објавен</string>
+ <string name="view_in_browser">Види во прелистувач</string>
+ <string name="local_changes">Локални промени</string>
+ <string name="blog_name_invalid">Адресата на страницата не е валидна</string>
+ <string name="blog_title_invalid">Насловот на страницата не е валиден</string>
+ <string name="blog_name_must_include_letters">Адресата на страницата мора да содржи барем една буква (a-z)</string>
+ <string name="email_reserved">Оваа адреса за е-пошта е веќе искористена. Проверете го вашето влезно сандаче за порака за активирање. Доколку не активирате може да се обидете повторно за неколку дена.</string>
+ <string name="privacy_policy">Полиса за приватност</string>
+ <string name="media_gallery_image_order">Подредба на слики</string>
+ <string name="themes_live_preview">Преглед во живо</string>
+ <string name="media_gallery_settings_title">Галерија поднесувања</string>
+ <string name="media_gallery_num_columns">Број на колони</string>
+ <string name="media_gallery_edit">Уредување на галерија</string>
+ <string name="media_error_no_permission">Немате достап за да ја видете медиа библиотеката</string>
+ <string name="cannot_delete_multi_media_items">Некои медиа арихи неможат да бидат избришани во моментот. Обидете се покасно.</string>
+ <string name="media_gallery_type_thumbnail_grid">Мини-мрежа</string>
+ <string name="xmlrpc_error">Неможе да се поврза. Водедете ја целосната патека до xmlrpc.php од вашиот сајт и обидете повторно.</string>
+ <string name="scaled_image_error">Воведете валидни намелени ширини</string>
+ <string name="add_new_category">Додади нова категорија</string>
+ <string name="category_name">Име на категорија</string>
+ <string name="share_action_post">Нов напис</string>
+ <string name="new_media">Нова медиа</string>
+ <string name="view_site">Види страница</string>
+ <string name="category_slug">Голич на категорија (опционален)</string>
+ <string name="category_desc">Опис на категорија (опционален)</string>
+ <string name="category_parent">Родител на категорија (опционален)</string>
+ <string name="comment_status_trash">Исфрлени</string>
+ <string name="comment_status_spam">Спам</string>
+ <string name="local_draft">Локални нацрти</string>
+ <string name="link_enter_url_text">Сврзи текст (опционален)</string>
+ <string name="mnu_comment_unapprove">Не одобрувај</string>
+ <string name="dlg_unapproving_comments">Не се одобрува</string>
+ <string name="open_source_licenses">Отворен код лиценци</string>
+ <string name="pending_review">Чека за преглед</string>
+ <string name="share_action_media">Медиа библиотека</string>
+ <string name="file_error_create">Неможе да се созададе темп архива за медиа испраќање. Ве волиме да обезбедите доволоно простор на вашиот уред</string>
+ <string name="http_credentials">HTTP акредитиви (опционален)</string>
+ <string name="post_format">Напис формат</string>
+ <string name="new_post">Нов напис</string>
+ <string name="site_address">Ваша само хостирана адреса (URL)</string>
+ <string name="email_cant_be_used_to_signup">Неможете да ја користите оваа адреса за да се регистрирате. Овој провајдер за електронска пошта блокира некои од нашите пораки. Користете друг провајдер.</string>
+ <string name="share_link">Сподели врска</string>
+ <string name="deleting_page">Бришење на страница</string>
+ <string name="deleting_post">Бришење на напис</string>
+ <string name="share_url_post">Сподели напис</string>
+ <string name="share_url_page">Сподели страница</string>
+ <string name="creating_your_account">Креирање на вашата корисничка сметка</string>
+ <string name="creating_your_site">Креирање на вашата страница</string>
+ <string name="reader_empty_posts_in_tag_updating">Преземање написи...</string>
+ <string name="error_refresh_media">Настана проблем при освежувањето на медиа библиотеката. Обидете се подоцна.</string>
+ <string name="cant_share_no_visible_blog">Неможе да споделите WordPress блог без да е видлив</string>
+ <string name="reader_likes_you_and_multi">На тебе и на уште %,d други им се допаѓа ова</string>
+ <string name="reader_likes_multi">На %,d луѓе им се допаѓа ова</string>
+ <string name="reader_toast_err_get_comment">Неможе да се вчита овој коментар</string>
+ <string name="reader_label_reply">Одговори</string>
+ <string name="video">Видео</string>
+ <string name="download">Преземање на медиа</string>
+ <string name="comment_spammed">Коментарот е означен како спам</string>
+ <string name="pick_photo">Избери фотографија</string>
+ <string name="validating_site_data">Валидирање на податоци за сајт</string>
+ <string name="reader_toast_err_get_post">Неможе да се вчита овој напис</string>
+ <string name="pick_video">Избери видео</string>
+ <string name="validating_user_data">Валидација на кориснички податоци</string>
+ <string name="reader_likes_you_and_one">На вас и на уште еден му се допаѓа ова</string>
+ <string name="select_time">Избери време</string>
+ <string name="select_date">Избери дата</string>
+ <string name="account_two_step_auth_enabled">Оваа корисничка сметка има вклучено автентикација во два чекори. Посетете ги вашите безбедносни подесувања на WordPress.com за да генерирате лозинка за специфичната апликација.</string>
+ <string name="signing_in">Најавување...</string>
+ <string name="nux_oops_not_selfhosted_blog">Најави се на WordPress.com</string>
+ <string name="nux_tap_continue">Продолжи</string>
+ <string name="nux_welcome_create_account">Креирај корисничка сметка</string>
+ <string name="password_invalid">Потребна ви е посигурна лозинка. Употребете 7 или повеќе карактери помешани со големи и мали букви, бројки и специјални карактери.</string>
+ <string name="nux_add_selfhosted_blog">Додади автономна страница</string>
+ <string name="reader_label_removed_tag">Одстранети %s</string>
+ <string name="media_add_popup_title">Додади во медиа библиотеката</string>
+ <string name="media_add_new_media_gallery">Креирај галерија</string>
+ <string name="empty_list_default">Оваа листа е празна</string>
+ <string name="select_from_media_library">Избери медиа од библиотеката</string>
+ <string name="jetpack_message">Jetpack додатокот е задолжителен за статистиката. Дали сакате да инсталирате Jetpack?</string>
+ <string name="jetpack_not_found">Jetpack додатокот не е пронајден</string>
+ <string name="reader_untitled_post">(без наслов)</string>
+ <string name="reader_share_subject">Споделено од %s</string>
+ <string name="reader_btn_share">Сподели</string>
+ <string name="reader_btn_follow">Следи</string>
+ <string name="reader_toast_err_comment_failed">Неможе да се испрати вашиот коментар</string>
+ <string name="reader_toast_err_tag_exists">Веќе го следите ова обележување</string>
+ <string name="reader_toast_err_tag_invalid">Тоа не е валидно обележување</string>
+ <string name="reader_toast_err_share_intent">Неможе да се сподели</string>
+ <string name="reader_toast_err_view_image">Неможе да се види сликата</string>
+ <string name="reader_toast_err_url_intent">Неможе да се отвори %s</string>
+ <string name="reader_empty_followed_tags">Вие не следите ниту едно обележување</string>
+ <string name="create_account_wpcom">Креирајте корисничка сметка на WordPress.com</string>
+ <string name="connecting_wpcom">Поврзување со WordPress.com</string>
+ <string name="username_invalid">Невалидно корисничко име</string>
+ <string name="limit_reached">Ограничувањето е достигнато. Може да се обидете повторно за 1 минута. Доколку се обидете повторно пред тоа време, ќе го зголемите времето за чекање пред да биде отстранета забраната. Доколку мислите дека ова е грешка, контактирајте ја поддршката.</string>
+ <string name="nux_tutorial_get_started_title">Започнете!</string>
+ <string name="reader_btn_unfollow">Следење</string>
+ <string name="reader_label_added_tag">Додадено %s</string>
+ <string name="reader_likes_one">На едно лице му се допаѓа ова</string>
+ <string name="reader_likes_only_you">На тебе ти се допаѓа ова</string>
+ <string name="stats_timeframe_yesterday">Вчера</string>
+ <string name="stats_totals_plays">Пуштени</string>
+ <string name="stats_entry_posts_and_pages">Наслов</string>
+ <string name="themes">Теми</string>
+ <string name="stats_timeframe_days">Денови</string>
+ <string name="passcode_change_passcode">Промени ПИН</string>
+ <string name="media_gallery_image_order_reverse">Обратно</string>
+ <string name="media_gallery_type">Тип</string>
+ <string name="media_gallery_type_squares">Квадрати</string>
+ <string name="media_gallery_type_tiled">Плочки</string>
+ <string name="media_gallery_type_circles">Кругови</string>
+ <string name="media_gallery_type_slideshow">Слајдшоу</string>
+ <string name="media_edit_title_text">Наслов</string>
+ <string name="media_edit_caption_text">Наслов</string>
+ <string name="media_edit_description_text">Опис</string>
+ <string name="media_edit_title_hint">Внеси наслов овде</string>
+ <string name="media_edit_caption_hint">Внеси наслов овде</string>
+ <string name="media_edit_description_hint">Внеси опис овде</string>
+ <string name="media_edit_success">Ажурирано</string>
+ <string name="passcode_enter_old_passcode">Воведи го твојот стар ПИН</string>
+ <string name="stats_timeframe_weeks">Седмици</string>
+ <string name="passcode_manage">Управување со PIN заклучување</string>
+ <string name="passcode_enter_passcode">Внесете го вашиот PIN</string>
+ <string name="passcode_re_enter_passcode">Внесете го повторно вашиот PIN</string>
+ <string name="passcode_set">PIN-от е подесен</string>
+ <string name="passcode_preference_title">PIN заклучување</string>
+ <string name="passcode_turn_off">Исклучете го PIN заклучувањето</string>
+ <string name="passcode_turn_on">Вклучете го PIN заклучувањето</string>
+ <string name="stats_totals_views">Прегледи</string>
+ <string name="stats_totals_clicks">Кликнувања</string>
+ <string name="theme_activating_button">Активирање</string>
+ <string name="theme_set_success">Темата е поставена успешно!</string>
+ <string name="theme_auth_error_title">Неуспешно преземање на теми</string>
+ <string name="post_excerpt">Извадок</string>
+ <string name="share_action_title">Додади на ...</string>
+ <string name="share_action">Сподели</string>
+ <string name="stats">Статистики</string>
+ <string name="stats_view_visitors_and_views">Посетители и прегледи</string>
+ <string name="stats_view_clicks">Кликнувања</string>
+ <string name="all">Сите</string>
+ <string name="media_edit_failure">Неуспешно качување</string>
+ <string name="themes_details_label">Детали</string>
+ <string name="themes_features_label">Особини</string>
+ <string name="theme_activate_button">Активирај</string>
+ <string name="stats_timeframe_today">Денес</string>
+ <string name="stats_timeframe_months">Месеци</string>
+ <string name="stats_entry_country">Земја</string>
+ <string name="stats_entry_tags_and_categories">Тема</string>
+ <string name="stats_entry_authors">Автор</string>
+ <string name="images">Слики</string>
+ <string name="media_gallery_image_order_random">Случајни</string>
+ <string name="stats_view_referrers">Препорачувачи</string>
+ <string name="stats_entry_referrers">Препорачувач</string>
+ <string name="custom_date">Прилагодена дата</string>
+ <string name="media_add_popup_capture_photo">Сликање</string>
+ <string name="media_add_popup_capture_video">Снимање</string>
+ <string name="unattached">Неприкачени</string>
+ <string name="upload">Качување</string>
+ <string name="notifications">Известувања</string>
+ <string name="new_notifications">%d нови известувања</string>
+ <string name="more_notifications">и уште %d .</string>
+ <string name="sign_in">Најави се</string>
+ <string name="note_reply_successful">Одговори на објавено</string>
+ <string name="loading">Вчитување...</string>
+ <string name="httpuser">HTTP корисничко име</string>
+ <string name="httppassword">HTTP лозинка</string>
+ <string name="error_media_upload">Настана грешка при качување на медиа</string>
+ <string name="publish_date">Објави</string>
+ <string name="content_description_add_media">Додади медиа</string>
+ <string name="post_content">Содржина (допрете за додавање на текст и медиа)</string>
+ <string name="incorrect_credentials">Невалидно корисничко име или лозинка.</string>
+ <string name="password">Лозинка</string>
+ <string name="username">Корисничко име</string>
+ <string name="reader">Читач</string>
+ <string name="width">Широчина</string>
+ <string name="post">Напис</string>
+ <string name="page">Страница</string>
+ <string name="anonymous">Анонимно</string>
+ <string name="posts">Написи</string>
+ <string name="caption">Наслов (изборно)</string>
+ <string name="pages">Страници</string>
+ <string name="no_network_title">Не е достапна мрежа</string>
+ <string name="featured_in_post">Вклучете ги сликите во содржината на написот</string>
+ <string name="featured">Користи како избрана слика</string>
+ <string name="ok">Добро</string>
+ <string name="blogusername">блогкорисник</string>
+ <string name="upload_scaled_image">Испрати и сврзи со намелана слика</string>
+ <string name="scaled_image">Ширина на прилагодена слика</string>
+ <string name="scheduled">Распоредени</string>
+ <string name="link_enter_url">URL</string>
+ <string name="tos">Услови за користење</string>
+ <string name="version">Верзија</string>
+ <string name="app_title">WordPress за Андроид</string>
+ <string name="image_alignment">Усогласување</string>
+ <string name="refresh">Освежи</string>
+ <string name="untitled">Без наслов</string>
+ <string name="edit">Уреди</string>
+ <string name="post_id">Напис</string>
+ <string name="page_id">Страница</string>
+ <string name="post_password">Лозинка (опционално)</string>
+ <string name="immediately">Веднаш</string>
+ <string name="quickpress_add_alert_title">Постави име на кратенка</string>
+ <string name="today">Денес</string>
+ <string name="settings">Поставувања</string>
+ <string name="share_url">URL за споделување</string>
+ <string name="quickpress_add_error">Името на кратенката не смее да биде празно</string>
+ <string name="quickpress_window_title">Изберете блог за QuickPress кратенка</string>
+ <string name="publish_post">Објави</string>
+ <string name="draft">Нацрт</string>
+ <string name="post_private">Приватен</string>
+ <string name="upload_full_size_image">Качи на сервер и додај врска до оригиналната фотографија</string>
+ <string name="title">Наслов</string>
+ <string name="tags_separate_with_commas">Ознаки (одделете ги ознаките со запирки)</string>
+ <string name="categories">Категории</string>
+ <string name="notification_blink">Известување со трепкачко светло</string>
+ <string name="notification_vibrate">Вибрирај</string>
+ <string name="status">Статус</string>
+ <string name="location">Локација</string>
+ <string name="sdcard_title">Потребна е SD картичка</string>
+ <string name="select_video">Изберете видео од галеријата</string>
+ <string name="media">Мултимедија</string>
+ <string name="delete">Бриши</string>
+ <string name="none">Нема</string>
+ <string name="blogs">Блогови</string>
+ <string name="select_photo">Избери фотографија од галеријата</string>
+ <string name="error">Грешка</string>
+ <string name="cancel">Откажи</string>
+ <string name="save">Зачувај</string>
+ <string name="add">Додај</string>
+ <string name="category_refresh_error">Грешка при освежување на категориите</string>
+ <string name="on">на</string>
+ <string name="reply">Одговори</string>
+ <string name="yes">Да</string>
+ <string name="no">Не</string>
+ <string name="preview">Преглед</string>
+</resources>
diff --git a/WordPress/src/main/res/values-ms/strings.xml b/WordPress/src/main/res/values-ms/strings.xml
new file mode 100644
index 000000000..beda81908
--- /dev/null
+++ b/WordPress/src/main/res/values-ms/strings.xml
@@ -0,0 +1,1132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="error_fetch_users_list">Tidak boleh mendapatkan pengguna laman</string>
+ <string name="plans_manage">Urus pelan anda di\nWordPress.com/plans</string>
+ <string name="title_follower">Pengikut</string>
+ <string name="title_email_follower">Pengikut Emel</string>
+ <string name="people_empty_list_filtered_viewers">Anda belum mempunyai penonton.</string>
+ <string name="people_empty_list_filtered_email_followers">Anda belum mempunyai pengikut emel.</string>
+ <string name="people_empty_list_filtered_followers">Anda belum mempunyai pengikut.</string>
+ <string name="people_empty_list_filtered_users">Anda belum mempunyai pengguna.</string>
+ <string name="people_dropdown_item_viewers">Penonton</string>
+ <string name="people_dropdown_item_email_followers">Pengikut Emel</string>
+ <string name="people_dropdown_item_followers">Pengikut</string>
+ <string name="people_dropdown_item_team">Pasukan</string>
+ <string name="invite_message_usernames_limit">Jemput sehingga 10 alamat emel dan/atau nama pengguna WordPress.com. Mereka yang memerlukan nama pengguna akan dihantar arahan tentang bagaimana untuk mewujudkannya.</string>
+ <string name="viewer_remove_confirmation_message">Jika anda membuang penonton ini, beliau tidak akan dapat melawat laman ini.\n\nAnda masih ingin membuang penonton ini?</string>
+ <string name="follower_remove_confirmation_message">Jika dibuang, pengikut ini akan berhenti menerima pemberitahuan tentang laman ini, kecuali jika mereka mengikut kembali.\n\nAnda masih ingin membuang pengikut ini?</string>
+ <string name="follower_subscribed_since">Sejak %1$s</string>
+ <string name="reader_label_view_gallery">Lihat Galeri</string>
+ <string name="error_remove_follower">Tidak dapat membuang pengikut</string>
+ <string name="error_remove_viewer">Tidak dapat membuang penonton</string>
+ <string name="error_fetch_email_followers_list">Tidak boleh mendapatkan pengikut emel laman</string>
+ <string name="error_fetch_followers_list">Tidak boleh mendapatkan pengikut laman</string>
+ <string name="editor_failed_uploads_switch_html">Sebahagian muat naik media telah gagal. Anda tidak boleh menukar ke mod HTML\ndalam keadaan ini. in this state. Buang semua muat naik yang gagal dan bersambung?</string>
+ <string name="format_bar_description_html">Mod HTML</string>
+ <string name="visual_editor">Penyunting visual</string>
+ <string name="image_thumbnail">Imej kecil</string>
+ <string name="format_bar_description_ul">Senarai tidak tertib</string>
+ <string name="format_bar_description_ol">Senarai tertib</string>
+ <string name="format_bar_description_media">Sisip media</string>
+ <string name="format_bar_description_more">Sisip selanjutnya</string>
+ <string name="format_bar_description_strike">Garis batal</string>
+ <string name="format_bar_description_quote">Petikan blok</string>
+ <string name="format_bar_description_link">Sisip pautan</string>
+ <string name="format_bar_description_italic">Italik</string>
+ <string name="format_bar_description_underline">Garis bawah</string>
+ <string name="image_caption">Sari Kata</string>
+ <string name="image_alt_text">Teks alt</string>
+ <string name="image_link_to">Paut ke</string>
+ <string name="image_width">Lebar</string>
+ <string name="format_bar_description_bold">Tebal</string>
+ <string name="image_settings_save_toast">Penukaran disimpan</string>
+ <string name="image_settings_dismiss_dialog_title">Buang penukaran yang tidak disimpan?</string>
+ <string name="stop_upload_dialog_title">Berhenti memuat naik?</string>
+ <string name="stop_upload_button">Berhenti Muat Naik</string>
+ <string name="alert_error_adding_media">Ralat berlaku semasa menyisip media</string>
+ <string name="alert_action_while_uploading">Anda sedang memuat naik media. Sila tunggu sehingga ianya selesai.</string>
+ <string name="alert_insert_image_html_mode">Tidak dapat menyisip media terus dalam mod HTML. Sila tukar kembali ke mod visual.</string>
+ <string name="uploading_gallery_placeholder">Memuat naik galeri...</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_some_failed">Jemputan dihantar tetapi ralat berlaku!</string>
+ <string name="invite_sent">Jemputan berjaya dihantar</string>
+ <string name="tap_to_try_again">Ketik untuk cuba semula!</string>
+ <string name="invite_error_sending">Ralat berlaku semasa cuba menghantar jemputan!</string>
+ <string name="invite_error_invalid_usernames_multiple">Tidak dapat menghantar: Terdapat nama pengguna atau emel yang tidak sah</string>
+ <string name="invite_error_invalid_usernames_one">Tidak dapat menghantar: Nama pengguna atau emel adalah tidak sah</string>
+ <string name="invite_error_no_usernames">Sila tambah sekurang-kurangnya satu nama pengguna</string>
+ <string name="invite_message_info">(Pilihan) Anda boleh memasukkan mesej tersuai sehingga 500 aksara yang akan disertakan dalam jemputan kepada pengguna.</string>
+ <string name="invite_message_remaining_other">Tinggal %d aksara</string>
+ <string name="invite_message_remaining_one">Tinggal 1 aksara</string>
+ <string name="invite_message_remaining_zero">Tinggal 0 aksara</string>
+ <string name="invite_invalid_email">Alamat emel \'%s\' tidak sah</string>
+ <string name="invite_message_title">Mesej Tersuai</string>
+ <string name="invite_already_a_member">Sudah terdapat ahli dengan nama pengguna \'%s\'</string>
+ <string name="invite_username_not_found">Tiada pengguna dijumpai bagi nama pengguna \'%s\'</string>
+ <string name="invite">Jemput</string>
+ <string name="invite_names_title">Nama Pengguna atau Emel</string>
+ <string name="signup_succeed_signin_failed">Akaun anda telah direka tetapi ralat berlaku semasa kami melog anda\nmasuk. Sila log masuk dengan nama pengguna dan kata laluan baharu anda.</string>
+ <string name="send_link">Hantar pautan</string>
+ <string name="my_site_header_external">Luaran</string>
+ <string name="invite_people">Jemput Orang</string>
+ <string name="label_clear_search_history">Kosongkan sejarah carian</string>
+ <string name="dlg_confirm_clear_search_history">Kosongkan sejarah carian?</string>
+ <string name="reader_empty_posts_in_search_description">Tiada kiriman dijumpai untuk %s bagi bahasa anda</string>
+ <string name="reader_label_post_search_running">Mencari...</string>
+ <string name="reader_label_related_posts">Bacaan Berkaitan</string>
+ <string name="reader_empty_posts_in_search_title">Tiada kiriman ditemui</string>
+ <string name="reader_label_post_search_explainer">Cari semua blog umum WordPress.com</string>
+ <string name="reader_hint_post_search">Cari WordPress.com</string>
+ <string name="reader_title_search_results">Carian untuk %s</string>
+ <string name="reader_title_related_post_detail">Kiriman Berkaitan</string>
+ <string name="preview_screen_links_disabled">Pautan dinyah boleh pada skrin pratonton</string>
+ <string name="draft_explainer">Kiriman ini adalah draf yang belum diterbitkan</string>
+ <string name="send">Hantar</string>
+ <string name="person_removed">Berjaya membuang %1$s</string>
+ <string name="user_remove_confirmation_message">Jika anda membuang %1$s, pengguna berkenaan tidak lagi akan dapat mencapai laman ini, tetapi apa-apa kandungan yang diwujudkan oleh %1$s akan tetap berada pada laman berkenaan.\n\nAnda ingin membuang pengguna ini?</string>
+ <string name="person_remove_confirmation_title">Buang %1$s</string>
+ <string name="edit_user">Sunting Pengguna</string>
+ <string name="role">Peranan</string>
+ <string name="people">Orang</string>
+ <string name="reader_empty_posts_in_custom_list">Laman dalam senarai ini tiada kiriman baharu.</string>
+ <string name="error_remove_user">Tidak dapat membuang pengguna</string>
+ <string name="error_fetch_viewers_list">Tidak boleh mendapatkan penonton laman</string>
+ <string name="error_update_role">Tidak dapat mengemaskini peranan pengguna</string>
+ <string name="gravatar_camera_and_media_permission_required">Kebenaran diperlukan untuk memilih atau menangkap gambar</string>
+ <string name="error_updating_gravatar">Ralat mengemaskini Gravatar anda</string>
+ <string name="error_refreshing_gravatar">Ralat memuatkan Gravatar anda</string>
+ <string name="error_locating_image">Ralat mencari imej dipangkas</string>
+ <string name="gravatar_tip">Baharu! Ketik Gravatar anda untuk menukarnya!</string>
+ <string name="error_cropping_image">Ralat memangkas imej</string>
+ <string name="checking_email">Menyemak emel</string>
+ <string name="launch_your_email_app">Lancarkan aplikasi emel anda</string>
+ <string name="not_on_wordpress_com">Tidak dalam WordPress.com?</string>
+ <string name="check_your_email">Semak emel anda</string>
+ <string name="magic_link_unavailable_error_message">Tidak terdapat pada masa ini. Sila masukkan kata laluan anda</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Dapatkan pautan dihantar ke emel anda untuk daftar masuk segera</string>
+ <string name="logging_in">Melog masuk</string>
+ <string name="enter_your_password_instead">Sebaliknya masukkan kata laluan anda</string>
+ <string name="web_address_dialog_hint">Tunjukkan kepada umum apabila anda mengulas.</string>
+ <string name="username_email">Emel atau nama pengguna</string>
+ <string name="jetpack_not_connected_message">Pemalam Jetpack telah dipasang, tetapi tidak dihubungkan ke WordPress.com. Adakah anda ingin menghubungkan Jetpack?</string>
+ <string name="jetpack_not_connected">Pemalam Jetpack tidak dihubungkan</string>
+ <string name="new_editor_reflection_error">Penyunting visual tidak serasi dengan peranti anda. Ia dinyah boleh\nsecara automatik</string>
+ <string name="stats_insights_latest_post_no_title">(tiada tajuk)</string>
+ <string name="capture_or_pick_photo">Tangkap atau pilih gambar</string>
+ <string name="plans_post_purchase_text_themes">Anda kini mempunyai capaian tanpa had ke tema Premium. Pratonton mana-mana tema pada laman anda untuk bermula.</string>
+ <string name="plans_post_purchase_button_themes">Layari Tema</string>
+ <string name="plans_post_purchase_title_themes">Cari tema premium dan sempurna</string>
+ <string name="plans_post_purchase_button_video">Mulakan kiriman baharu</string>
+ <string name="plans_post_purchase_text_video">Anda boleh muatnaik dan hoskan video di laman anda dengan VideoPress dan storan media anda yang dibesarkan.</string>
+ <string name="plans_post_purchase_title_video">Hidupkan kiriman dengan video</string>
+ <string name="plans_post_purchase_button_customize">Suai Langgan Laman saya</string>
+ <string name="plans_post_purchase_text_customize">Anda kini mempunyai capaian ke fon tersuai, warna tersuai, dan ciri-ciri penyuntingan CSS tersuai.</string>
+ <string name="plans_post_purchase_text_intro">Laman anda sedang melompat kegembiraan! Sekarang lihat ciri-ciri baharu laman anda dan pilih dimana anda ingin mulakan.</string>
+ <string name="plans_post_purchase_title_customize">Suai Langgan Fon &amp; Warna</string>
+ <string name="plans_post_purchase_title_intro">Semuanya milik anda, sangat bagus!</string>
+ <string name="export_your_content_message">Kiriman, halaman, dan tetapan anda akan diemel kepada anda di %s.</string>
+ <string name="plan">Pelan</string>
+ <string name="plans">Pelan</string>
+ <string name="plans_loading_error">Tidak boleh memuatkan pelan</string>
+ <string name="export_your_content">Eksport kandungan anda</string>
+ <string name="export_email_sent">Emel eksport dihantar!</string>
+ <string name="exporting_content_progress">Mengeksport kandungan...</string>
+ <string name="premium_upgrades_message">Anda mempunyai naik taraf premium aktif di laman anda. Sila batalkan naik taraf anda sebelum menghapuskan laman anda.</string>
+ <string name="show_purchases">Tunjukkan pembelian</string>
+ <string name="checking_purchases">Menyemak pembelian</string>
+ <string name="premium_upgrades_title">Naiktaraf Premium</string>
+ <string name="purchases_request_error">Sesuatu tidak kena. Tidak boleh memohon pembelian</string>
+ <string name="delete_site_progress">Menghapus laman...</string>
+ <string name="delete_site_hint">Padamkan laman</string>
+ <string name="delete_site_summary">Tindakan ini tidak boleh diulang semula. Menghapuskan laman anda akan membuang semua kandungan, penyumbang, dan domain dari laman.</string>
+ <string name="are_you_sure">Anda Pasti?</string>
+ <string name="export_site_hint">Eksport laman anda ke fail XML</string>
+ <string name="export_site_summary">Jika anda pasti, pastikan anda mengambil masa dan eksport kandungan sekarang. Ia tidak boleh didapati semula.</string>
+ <string name="keep_your_content">Simpan Kandungan Anda</string>
+ <string name="domain_removal_hint">Domain yang tidak akan berfungsi jika anda membuang laman anda</string>
+ <string name="domain_removal_summary">Berhat-hati! Menghapuskan laman anda juga akan membuang domain anda yang disenaraikan di bawah.</string>
+ <string name="primary_domain">Domain Utama</string>
+ <string name="domain_removal">Pembuangan Domain</string>
+ <string name="error_deleting_site_summary">Terdapat ralat ketika menghapuskan laman anda. Sila hubungi sokongan untuk bantuan lanjut</string>
+ <string name="error_deleting_site">Ralat untuk memadamkan laman</string>
+ <string name="site_settings_export_content_title">Eksport isi kandungan</string>
+ <string name="confirm_delete_site_prompt">Sila taipkan %1$s dalam medan di bawah untuk mengesahkan. Laman anda akan hilang selamanya.</string>
+ <string name="contact_support">Hubungi sokongan</string>
+ <string name="confirm_delete_site">Sah Hapuskan Laman</string>
+ <string name="start_over_text">Jika anda mahukan laman tapi tidak mahu apa-apa kiriman atau laman yang anda ada sekarang, kumpulan sokongan kami boleh menghapuskan kiriman, halaman, media dan komen untuk anda.\n\nIni akan mengekalkan status aktif laman dan URL anda, tetapi memberi anda permulaan baharu untuk mewujudkan kandungan. Hubungi kami untuk menghapuskan kandungan semasa anda.</string>
+ <string name="let_us_help">Biar Kami Bantu</string>
+ <string name="site_settings_start_over_hint">Mulakan semula laman anda</string>
+ <string name="me_btn_app_settings">Tetapan Aplikasi</string>
+ <string name="start_over">Mula Semula</string>
+ <string name="editor_remove_failed_uploads">Buangkan muatnaik yang gagal</string>
+ <string name="editor_toast_failed_uploads">Sebahagian muatnaik media telah gagal. Anda tidak boleh menyimpan atau menerbitkan\n kiriman anda dalam keadaan ini. Adakah anda ingin membuang semua muatnaik media yang gagal?</string>
+ <string name="site_settings_advanced_header">Lanjutan</string>
+ <string name="comments_empty_list_filtered_trashed">Tiada ulasan dibuang</string>
+ <string name="comments_empty_list_filtered_pending">Tiada komen tertangguh</string>
+ <string name="comments_empty_list_filtered_approved">Tiada ulasan yang diluluskan</string>
+ <string name="button_done">Selesai</string>
+ <string name="button_skip">Langkau</string>
+ <string name="site_timeout_error">Tidak dapat menghubungi laman WordPress kerana ralat Timeout.</string>
+ <string name="xmlrpc_malformed_response_error">Tidak boleh berhubung. Pemasangan WordPress membalas dengan dokumen XML-RPC yang tidak sah.</string>
+ <string name="xmlrpc_missing_method_error">Tidak boleh berhubung. Kaedah XML-RPC yang diperlukan tiada pada pelayan.</string>
+ <string name="theme_all">Semua</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="theme_premium">Premium</string>
+ <string name="theme_free">Percuma</string>
+ <string name="post_format_image">Imej</string>
+ <string name="post_format_chat">Perbualan</string>
+ <string name="post_format_link">Pautan</string>
+ <string name="post_format_gallery">Galeri</string>
+ <string name="post_format_quote">Petikan</string>
+ <string name="post_format_standard">Piawai</string>
+ <string name="post_format_aside">Sisipan</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_events">Maklumat kursus dan acara di WordPress.com (atas talian &amp; perseorangan).</string>
+ <string name="notif_surveys">Peluang untuk terlibat dalam kajian &amp; kaji selidik WordPress.com.</string>
+ <string name="notif_tips">Tip untuk mendapat yang terbaik dari WordPress.com.</string>
+ <string name="notif_community">Komuniti</string>
+ <string name="notif_suggestions">Cadangan</string>
+ <string name="notif_research">Kajian</string>
+ <string name="replies_to_my_comments">Balasan kepada ulasan saya</string>
+ <string name="site_achievements">Pencapaian laman</string>
+ <string name="username_mentions">Nama pengguna yang disebut</string>
+ <string name="likes_on_my_posts">Suka pada kiriman saya</string>
+ <string name="site_follows">Laman diikuti</string>
+ <string name="likes_on_my_comments">Suka pada ulasan saya</string>
+ <string name="comments_on_my_site">Ulasan pada laman saya</string>
+ <string name="site_settings_list_editor_summary_other">%d butir</string>
+ <string name="site_settings_list_editor_summary_one">1 butir</string>
+ <string name="approve_auto">Semua pengguna</string>
+ <string name="approve_auto_if_previously_approved">Ulasan pengguna dikenali</string>
+ <string name="approve_manual">Tiada ulasan</string>
+ <string name="site_settings_paging_summary_other">%d ulasan setiap halaman</string>
+ <string name="site_settings_paging_summary_one">1 ulasan setiap halaman</string>
+ <string name="site_settings_multiple_links_summary_other">Perlukan kelulusan untuk lebih dari %d pautan</string>
+ <string name="site_settings_multiple_links_summary_one">Perlukan kelulusan untuk lebih dari 1 pautan</string>
+ <string name="site_settings_multiple_links_summary_zero">Perlukan kelulusan untuk lebih dari 0 pautan</string>
+ <string name="detail_approve_auto">Luluskan ulasan semua secara automatik.</string>
+ <string name="detail_approve_auto_if_previously_approved">Luluskan secara automatik sekiranya pengguna mempunyai ulasan yang diluluskan sebelum ini</string>
+ <string name="detail_approve_manual">Perlukan kelulusan manual bagi ulasan semua orang.</string>
+ <string name="days_quantity_other">%d hari</string>
+ <string name="days_quantity_one">1 hari</string>
+ <string name="filter_trashed_posts">Disampahkan</string>
+ <string name="filter_scheduled_posts">Dijadualkan</string>
+ <string name="filter_draft_posts">Draf</string>
+ <string name="filter_published_posts">Diterbitkan</string>
+ <string name="web_address">Alamat Web</string>
+ <string name="primary_site">Laman utama</string>
+ <string name="pending_email_change_snackbar">Klik pautan pengesahan dalam emel yang dihantar ke %1$s untuk mengesahkan alamat baharu anda</string>
+ <string name="editor_toast_uploading_please_wait">Anda sedang memuatnaik media. Sila tunggu sehingga selesai.</string>
+ <string name="error_refresh_comments_showing_older">Ulasan tidak dapat disegarkan buat masa ini - tunjukkan ulasan terdahulu</string>
+ <string name="editor_post_settings_set_featured_image">Tetapkan Imej Terencana</string>
+ <string name="editor_post_settings_featured_image">Imej Terencana</string>
+ <string name="new_editor_promo_desc">Aplikasi WordPress untuk Android kini dilengkapi dengan penyunting\n visual baharu yang cantik. Cubanya dengan membuat kiriman baharu.</string>
+ <string name="new_editor_promo_title">Penyuntung baharu</string>
+ <string name="new_editor_promo_button_label">Hebat, terima kasih!</string>
+ <string name="visual_editor_enabled">Penyunting Visual dibolehkan</string>
+ <string name="editor_content_placeholder">Kongsikan cerita anda di sini...</string>
+ <string name="editor_page_title_placeholder">Tajuk Halaman</string>
+ <string name="editor_post_title_placeholder">Tajuk Kiriman</string>
+ <string name="email_address">Alamat emel</string>
+ <string name="preference_show_visual_editor">Tunjukkan penyunting visual</string>
+ <string name="preference_editor">Penyunting</string>
+ <string name="dlg_sure_to_delete_comments">Hapuskan ulasan ini secara kekal?</string>
+ <string name="dlg_sure_to_delete_comment">Hapuskan ulasan ini secara kekal?</string>
+ <string name="mnu_comment_delete_permanently">Hapus</string>
+ <string name="comment_deleted_permanently">Ulasan dihapuskan</string>
+ <string name="mnu_comment_untrash">Pulihkan</string>
+ <string name="comments_empty_list_filtered_spam">Tiada ulasan Spam</string>
+ <string name="comment_status_all">Semua</string>
+ <string name="could_not_load_page">Tidak dapat memuatkan halaman</string>
+ <string name="off">Matikan</string>
+ <string name="interface_language">Bahasa Antara Muka</string>
+ <string name="about_the_app">Mengenai aplikasi</string>
+ <string name="error_post_account_settings">Tidak boleh menyimpan tetapan akaun anda</string>
+ <string name="error_post_my_profile">Tidak dapat menyimpan profil anda</string>
+ <string name="error_fetch_account_settings">Tidak boleh mendapatkan tetapan akaun anda</string>
+ <string name="error_fetch_my_profile">Tidak boleh mendapatkan profil anda</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, faham</string>
+ <string name="stats_widget_promo_desc">Tambah widget ke skrin muka depan anda untuk mencapai Statistik anda dalam satu klik.</string>
+ <string name="stats_widget_promo_title">Widget Statistik Skrin Laman Utama</string>
+ <string name="site_settings_unknown_language_code_error">Kod bahasa tidak dikenali</string>
+ <string name="site_settings_threading_dialog_description">Benarkan ulasan disarang dalam jalur.</string>
+ <string name="site_settings_threading_dialog_header">Jalur sehingga</string>
+ <string name="add_category">Tambah kategori</string>
+ <string name="remove">Buang</string>
+ <string name="disabled">Nyah boleh</string>
+ <string name="search">Cari</string>
+ <string name="site_settings_image_original_size">Saiz Asal</string>
+ <string name="privacy_private">Laman anda adalah kelihatan hanya kepada anda dan pengguna yang anda luluskan</string>
+ <string name="privacy_public_not_indexed">Laman anda adalah kelihatan kepada semua tetapi meminta enjin carian untuk tidak mengindeksnya</string>
+ <string name="privacy_public">Laman anda adalah kelihata kepada semua dan mungkin diindeks oleh enjin carian</string>
+ <string name="about_me_hint">Beberapa perkataan mengenai anda...</string>
+ <string name="about_me">Mengenai saya</string>
+ <string name="public_display_name_hint">Nama paparan akan kembali kepada nama pengguna anda jika ia tidak ditetapkan</string>
+ <string name="public_display_name">Nama paparan umum</string>
+ <string name="last_name">Nama akhir</string>
+ <string name="first_name">Nama pertama</string>
+ <string name="my_profile">Profil Saya</string>
+ <string name="site_privacy_public_desc">Benarkan enjin carian untuk mengindeks laman ini</string>
+ <string name="site_privacy_hidden_desc">Jangan galakkan enjin carian daripada mengindeks laman ini</string>
+ <string name="site_privacy_private_desc">Saya mahu laman saya menjadi sulit, boleh dilihat hanya kepada pengguna yang saya pilih</string>
+ <string name="cd_related_post_preview_image">Imej pratonton kiriman berkaitan</string>
+ <string name="error_post_remote_site_settings">Tidak boleh menyimpan maklumat laman</string>
+ <string name="error_fetch_remote_site_settings">Tidak boleh mendapatkan maklumat laman</string>
+ <string name="error_media_upload_connection">Ralat sambungan terjadi semasa memuat naik media</string>
+ <string name="site_settings_disconnected_toast">Terputus, penyuntingan dinyahboleh.</string>
+ <string name="site_settings_unsupported_version_error">Versi WordPress tidak disokong</string>
+ <string name="site_settings_multiple_links_dialog_description">Memerlukan kelulusan untuk ulasan yang mengandungi lebih daripada jumlah pautan ini.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Tutup secara automatik</string>
+ <string name="site_settings_close_after_dialog_description">Tutup ulasan secara automatik pada artikel.</string>
+ <string name="site_settings_paging_dialog_description">Pecahkan jalur ulasan kepada berbilang halaman.</string>
+ <string name="site_settings_paging_dialog_header">Ulasan setiap halaman</string>
+ <string name="site_settings_close_after_dialog_title">Tutup pengulasan</string>
+ <string name="site_settings_blacklist_description">Apabila ulasan mengandungi mana-mana perkataan berikut dalam kandungan, nama, URL, emel, atau IP, ia akan ditanda sebagai spam. Anda boleh mengisi sebahagian perkataan, oleh itu "press" akan padan dengan "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">Apabila ulasan mengandungi mana-mana perkataan ini dalam kandungan, nama, URL, emel atau IP, ia akan ditahan dibawah barisan penyederhanaan. Anda boleh mengisi sebahagian perkataan, oleh itu "press" akan padan dengan "WordPress."</string>
+ <string name="site_settings_list_editor_input_hint">Masukkan perkataan atau frasa</string>
+ <string name="site_settings_list_editor_no_items_text">Tiada butiran</string>
+ <string name="site_settings_learn_more_caption">Anda boleh membatalkan tetapan ini bagi kiriman individu.</string>
+ <string name="site_settings_rp_preview3_site">dalam "Naik Taraf"</string>
+ <string name="site_settings_rp_preview3_title">Fokus Naiktaraf: VideoPress untuk Perkahwinan</string>
+ <string name="site_settings_rp_preview2_site">dalam "Aplikasi"</string>
+ <string name="site_settings_rp_preview2_title">Aplikasi WordPress untuk Android Mendapat Wajah Baharu</string>
+ <string name="site_settings_rp_preview1_site">dalam "Mudah Alih"</string>
+ <string name="site_settings_rp_preview1_title">Kemas Kini iPhone/iPad Besar Kini Didapati</string>
+ <string name="site_settings_rp_show_images_title">Tunjuk Imej</string>
+ <string name="site_settings_rp_show_header_title">Tunjukkan Pengepala</string>
+ <string name="site_settings_rp_switch_summary">Kiriman Berkaitan memaparkan kandungan yang berkenaan daripada laman anda di bawah kiriman anda.</string>
+ <string name="site_settings_rp_switch_title">Tunjukkan Kiriman Berkaitan</string>
+ <string name="site_settings_delete_site_hint">Buang data laman anda dari aplikasi</string>
+ <string name="site_settings_blacklist_hint">Ulasan yang sepadan dengan penapis ditanda sebagai spam</string>
+ <string name="site_settings_moderation_hold_hint">Ulasan yang sepadan dengan penapis diletakkan dalam barisan penyederhanaan</string>
+ <string name="site_settings_multiple_links_hint">Abaikan had pautan daripada pengguna dikenali</string>
+ <string name="site_settings_whitelist_hint">Ulasan pengarang mesti mempunyai ulasan terdahulu yang diluluskan</string>
+ <string name="site_settings_user_account_required_hint">Pengguna mestilah berdaftar dan log masuk untuk mengulas</string>
+ <string name="site_settings_identity_required_hint">Penulis ulasan mesti mengisi nama dan emel</string>
+ <string name="site_settings_manual_approval_hint">Ulasan mestilah diluluskan secara manual</string>
+ <string name="site_settings_paging_hint">Papar ulsan dalam ketulan saiz yang ditetapkan</string>
+ <string name="site_settings_threading_hint">Benarkan ulasan bersarang ke dalaman tertentu</string>
+ <string name="site_settings_sort_by_hint">Menentukan aturan ulasan yang dipaparkan</string>
+ <string name="site_settings_close_after_hint">Jangan benarkan ulasan selepas waktu yang ditetapkan</string>
+ <string name="site_settings_receive_pingbacks_hint">Benarkan pemberitahuan pautan daripada blog-blog lain</string>
+ <string name="site_settings_send_pingbacks_hint">Cuba beritahu mana-mana blog yang dipautkan daripada artikel</string>
+ <string name="site_settings_allow_comments_hint">Benarkan pembaca mengirim ulasan</string>
+ <string name="site_settings_discussion_hint">Lihat dan ubah tetapan perbincangan laman anda</string>
+ <string name="site_settings_more_hint">Lihat semua tetapan Perbincangan yang ada</string>
+ <string name="site_settings_related_posts_hint">Tunjukkan atau sembunyikan kiriman berkaitan dalam pembaca</string>
+ <string name="site_settings_upload_and_link_image_hint">Bolehkan untuk selalu memuat naik imej saiz penuh</string>
+ <string name="site_settings_image_width_hint">Saiz semula imej dalam kiriman ke lebar ini</string>
+ <string name="site_settings_format_hint">Tetapkan format kiriman baharu</string>
+ <string name="site_settings_category_hint">Tetapkan kategori kiriman baharu</string>
+ <string name="site_settings_location_hint">Tambah data lokasi secara automatik ke kiriman anda</string>
+ <string name="site_settings_password_hint">Tukar kata laluan anda</string>
+ <string name="site_settings_username_hint">Akaun pengguna semasa</string>
+ <string name="site_settings_language_hint">Bahasa utama blog ini ditulis dalam</string>
+ <string name="site_settings_privacy_hint">Kawal siapa boleh melihat laman anda</string>
+ <string name="site_settings_address_hint">Menukar alamat anda pada masa ini tidak disokong</string>
+ <string name="site_settings_tagline_hint">Huraian pendek atau frasa menarik untuk menggambarkan blog anda</string>
+ <string name="site_settings_title_hint">Dalam beberapa perkataan, jelaskan apa laman ini berkenaan dengan</string>
+ <string name="site_settings_whitelist_known_summary">Ulasan daripada pengguna yang diketahui</string>
+ <string name="site_settings_whitelist_all_summary">Ulasan daripada semua pengguna</string>
+ <string name="site_settings_threading_summary">%d paras</string>
+ <string name="site_settings_privacy_private_summary">Sulit</string>
+ <string name="site_settings_privacy_hidden_summary">Tersembunyi</string>
+ <string name="site_settings_privacy_public_summary">Umum</string>
+ <string name="site_settings_delete_site_title">Hapuskan Laman</string>
+ <string name="site_settings_blacklist_title">Senarai hitam</string>
+ <string name="site_settings_moderation_hold_title">Tahan untuk Penyederhanaan</string>
+ <string name="site_settings_multiple_links_title">Pautan dalam ulasan</string>
+ <string name="site_settings_whitelist_title">Luluskan secara automatik</string>
+ <string name="site_settings_threading_title">Penjaluran</string>
+ <string name="site_settings_paging_title">Menomborkan Halaman</string>
+ <string name="site_settings_sort_by_title">Isih berdasarkan</string>
+ <string name="site_settings_account_required_title">Pengguna mesti dilog masuk</string>
+ <string name="site_settings_identity_required_title">Mesti sertakan nama dan emel</string>
+ <string name="site_settings_receive_pingbacks_title">Terima Ping Balik</string>
+ <string name="site_settings_send_pingbacks_title">Hantar Ping Balik</string>
+ <string name="site_settings_allow_comments_title">Benarkan Ulasan</string>
+ <string name="site_settings_default_format_title">Format Lalai</string>
+ <string name="site_settings_default_category_title">Kategori Lalai</string>
+ <string name="site_settings_location_title">Bolehkan Lokasi</string>
+ <string name="site_settings_address_title">Alamat</string>
+ <string name="site_settings_title_title">Tajuk Laman</string>
+ <string name="site_settings_tagline_title">Baris Slogan</string>
+ <string name="site_settings_this_device_header">Peranti ini</string>
+ <string name="site_settings_discussion_new_posts_header">Tetapan lalai bagi kiriman baharu</string>
+ <string name="site_settings_account_header">Akaun</string>
+ <string name="site_settings_writing_header">Menulis</string>
+ <string name="newest_first">Terbaharu dahulu</string>
+ <string name="site_settings_general_header">Am</string>
+ <string name="discussion">Perbincangan</string>
+ <string name="privacy">Privasi</string>
+ <string name="related_posts">Kiriman Berkaitan</string>
+ <string name="comments">Ulasan</string>
+ <string name="close_after">Tutup selepas</string>
+ <string name="oldest_first">Yang lama terdahulu</string>
+ <string name="media_error_no_permission_upload">Anda tidak mempunyai kebenaran memuat naik media ke laman tersebut</string>
+ <string name="unknown">Tidak diketahui</string>
+ <string name="never">Tidak sama sekali</string>
+ <string name="reader_err_get_post_not_found">Kiriman ini tidak lagi wujud</string>
+ <string name="reader_err_get_post_not_authorized">Anda tidak dibenarkan melihat kiriman ini</string>
+ <string name="reader_err_get_post_generic">Tidak boleh mendapatkan kiriman ini</string>
+ <string name="blog_name_no_spaced_allowed">Alaman laman tidak boleh mengandungi ruang</string>
+ <string name="invalid_username_no_spaces">Nama pengguna tidak mengandungi ruang</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Laman yang anda ikuti belum mengirim apa-apa baru-baru ini</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Tiada kiriman terkini</string>
+ <string name="edit_media">Sunting media</string>
+ <string name="media_details_copy_url_toast">URL disalin ke papan keratan</string>
+ <string name="media_details_copy_url">Salin URL</string>
+ <string name="media_details_label_date_uploaded">Dimuat naik</string>
+ <string name="media_details_label_date_added">Ditambah</string>
+ <string name="selected_theme">Tema Dipilih</string>
+ <string name="could_not_load_theme">Tidak dapat memuat tema</string>
+ <string name="theme_activation_error">Suatu kesilapan telah berlaku. Tidak dapat mengaktifkan tema.</string>
+ <string name="theme_by_author_prompt_append"> oleh %1$s</string>
+ <string name="theme_prompt">Terima kasih kerana memilih %1$s</string>
+ <string name="theme_details">Perincian</string>
+ <string name="theme_done">SELESAI</string>
+ <string name="theme_manage_site">URUS LAMAN</string>
+ <string name="theme_support">Sokongan</string>
+ <string name="theme_try_and_customize">Cuba dan Suai Langgan</string>
+ <string name="theme_view">Lihat</string>
+ <string name="theme_activate">Aktifkan</string>
+ <string name="title_activity_theme_support">Tema</string>
+ <string name="active">Aktif</string>
+ <string name="current_theme">Tema Semasa</string>
+ <string name="customize">Suai Langgan</string>
+ <string name="details">Perincian</string>
+ <string name="date_range_end_date">Tarikh Akhir</string>
+ <string name="support">Sokongan</string>
+ <string name="date_range_start_date">Tarikh Mula</string>
+ <string name="stats_referrers_spam_generic_error">Suatu kesilapan telah berlaku semasa operasi tersebut. Keadaan spam tidak diubah.</string>
+ <string name="stats_referrers_marking_not_spam">Ditanda sebagai bukan spam</string>
+ <string name="stats_referrers_marking_spam">Ditanda sebagai spam</string>
+ <string name="stats_referrers_unspam">Bukan spam</string>
+ <string name="theme_auth_error_authenticate">Gagal mengambil tema: pengesahan pengguna gagal</string>
+ <string name="post_published">Kiriman diterbitkan</string>
+ <string name="page_published">Halaman diterbitkan</string>
+ <string name="post_updated">Kiriman dikemas kini</string>
+ <string name="page_updated">Halaman dikemas kini</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Maaf, tiada tema ditemui</string>
+ <string name="media_dimensions">Dimensi: %s</string>
+ <string name="media_file_name">Nama fail: %s</string>
+ <string name="media_uploaded_on">Muat naik pada: %s</string>
+ <string name="media_file_type">Jenis fail: %s</string>
+ <string name="upload_queued">Digilirkan</string>
+ <string name="reader_label_gap_marker">Muatkan lebih banyak kiriman</string>
+ <string name="notifications_no_search_results">Tiada laman sepadan dengan \'%s\'</string>
+ <string name="search_sites">Cari laman</string>
+ <string name="unread">Belum dibaca</string>
+ <string name="notifications_empty_view_reader">Lihat Pembaca</string>
+ <string name="notifications_empty_action_followers_likes">Dapatkan perhatian: ulasan pada kiriman yang anda telah baca.</string>
+ <string name="notifications_empty_action_comments">Ikuti perbincangan: ulas pada kiriman dari blog yang anda ikuti.</string>
+ <string name="notifications_empty_action_unread">Nyalakan semula perbualan: tulis kiriman baharu.</string>
+ <string name="notifications_empty_action_all">Jadilah aktif! Ulasan pada kiriman dari blog yang anda ikuti.</string>
+ <string name="notifications_empty_likes">Belum ada disukai untuk ditunjuk... lagi.</string>
+ <string name="notifications_empty_followers">Belum ada pengikut untuk dilaporkan... lagi.</string>
+ <string name="notifications_empty_comments">Belum ada ulasan baharu... lagi.</string>
+ <string name="notifications_empty_unread">Anda telah lengkapkan semuanya!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Sila capai Statistik dalam aplikasi, dan cuba tambah widget kemudian</string>
+ <string name="stats_widget_error_readd_widget">Sila buang widget dan menambahnya semula</string>
+ <string name="stats_widget_error_no_visible_blog">Statistik tidak dapat dicapai tanpa blog yang kelihatan</string>
+ <string name="stats_widget_error_no_permissions">Akaun WordPress.com anda tidak dapat mencapai Statistik di blog ini</string>
+ <string name="stats_widget_error_no_account">Sila log masuk ke dalam WordPress</string>
+ <string name="stats_widget_error_generic">Statistik tidak dapat dimuatkan</string>
+ <string name="stats_widget_loading_data">Memuat data...</string>
+ <string name="stats_widget_name_for_blog">Statistik %1$s untuk hari ini</string>
+ <string name="stats_widget_name">Statistik WordPress Hari Ini</string>
+ <string name="add_location_permission_required">Kebenaran diperlukan untuk menambah lokasi</string>
+ <string name="add_media_permission_required">Kebenaran diperlukan untuk menambah media</string>
+ <string name="access_media_permission_required">Kebenaran diperlukan untuk mencapai media</string>
+ <string name="stats_enable_rest_api_in_jetpack">Untuk melihat statistik anda, bolehkan modul API JSON dalam Jetpack.</string>
+ <string name="error_open_list_from_notification">Kiriman atau halaman ini telah diterbitkan di laman lain</string>
+ <string name="reader_short_comment_count_multi">%s Ulasan</string>
+ <string name="reader_short_comment_count_one">1 Ulasan</string>
+ <string name="reader_label_submit_comment">HANTAR</string>
+ <string name="reader_hint_comment_on_post">Balas kiriman...</string>
+ <string name="reader_discover_visit_blog">Lawat %s</string>
+ <string name="reader_discover_attribution_blog">Asalnya dikirim pada %s</string>
+ <string name="reader_discover_attribution_author">Asalnya dikirim oleh %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Asalnya dikirim oleh %1$s pada %2$s</string>
+ <string name="reader_short_like_count_multi">%s Suka</string>
+ <string name="reader_label_follow_count">%,d pengikut</string>
+ <string name="reader_short_like_count_one">1 Suka</string>
+ <string name="reader_short_like_count_none">Suka</string>
+ <string name="reader_menu_tags">Sunting tag dan blog</string>
+ <string name="reader_title_post_detail">Kiriman Pembaca</string>
+ <string name="local_draft_explainer">Kiriman ini adalah draf setempat yang belum diterbitkan</string>
+ <string name="local_changes_explainer">Kiriman ini mempunyai penukaran setempat yang belum diterbitkan</string>
+ <string name="notifications_push_summary">Tetapan bagi pemberitahuan yang muncul pada peranti anda.</string>
+ <string name="notifications_email_summary">Tetapan bagi pemberitahuan yang dihantar ke emel yang dikaitkan ke akaun anda.</string>
+ <string name="notifications_tab_summary">Tetapan bagi pemberitahuan yang muncul dalam tab Pemberitahuan.</string>
+ <string name="notifications_disabled">Pemberitahuan aplikasi telah dilumpuhkan. Ketik di sini untuk membolehkannya dalam Tetapan.</string>
+ <string name="notification_types">Jenis Pemberitahuan</string>
+ <string name="error_loading_notifications">Tidak dapat memuat tetapan pemberitahuan</string>
+ <string name="replies_to_your_comments">Balasan kepada ulasan anda</string>
+ <string name="comment_likes">Ulasan disukai</string>
+ <string name="app_notifications">Pemberitahuan aplikasi</string>
+ <string name="email">Emel</string>
+ <string name="notifications_tab">Tab pemberitahuan</string>
+ <string name="notifications_comments_other_blogs">Ulasan di laman lain</string>
+ <string name="notifications_wpcom_updates">Pengemas Kinian WordPress.com</string>
+ <string name="notifications_other">Lain</string>
+ <string name="notifications_account_emails">Emal dari WordPress.com</string>
+ <string name="notifications_account_emails_summary">Kami akan selalu menghantar emel penting berkenaan akaun anda, tetapi anda juga boleh mendapat beberapa tambahan yang membantu.</string>
+ <string name="notifications_sights_and_sounds">Pandangan dan Bunyian</string>
+ <string name="your_sites">Laman Anda</string>
+ <string name="stats_insights_latest_post_trend">Telah %1$s sejak %2$s diterbitkan. Sini adalah prestasi kiriman ini setakat ini…</string>
+ <string name="stats_insights_latest_post_summary">Ringkasan Kiriman Terkini</string>
+ <string name="button_revert">Berbalik</string>
+ <string name="days_ago">%d hari yang lalu</string>
+ <string name="yesterday">Semalam</string>
+ <string name="connectionbar_no_connection">Tiada sambungan</string>
+ <string name="button_back">Kembali</string>
+ <string name="button_edit">Sunting</string>
+ <string name="stats_no_activity_this_period">Tiada aktiviti tempoh ini</string>
+ <string name="post_deleted">Kiriman dihapuskan</string>
+ <string name="post_trashed">Kiriman dihantar ke sampah</string>
+ <string name="trashed">Disampahkan</string>
+ <string name="page_deleted">Halaman dihapuskan</string>
+ <string name="button_stats">Statistik</string>
+ <string name="button_trash">Sampah</string>
+ <string name="button_preview">Pratonton</string>
+ <string name="button_view">Lihat</string>
+ <string name="button_publish">Terbit</string>
+ <string name="page_trashed">Halaman dihantar ke sampah</string>
+ <string name="my_site_no_sites_view_subtitle">Anda ingin menambah?</string>
+ <string name="my_site_no_sites_view_title">Anda belum mempunyai mana-mana laman WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustrasi</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Anda tidak dibenarkan mencapai blog ini</string>
+ <string name="reader_toast_err_follow_blog_not_found">Blog ini tidak dapat dijumpai</string>
+ <string name="undo">Kembali ke asal</string>
+ <string name="tabbar_accessibility_label_me">Saya</string>
+ <string name="tabbar_accessibility_label_my_site">Laman Saya</string>
+ <string name="editor_toast_changes_saved">Perubahan disimpan</string>
+ <string name="passcodelock_prompt_message">Masukkan PIN anda</string>
+ <string name="push_auth_expired">Permintaan telah tamat tempoh. Log masuk ke WordPress.com dan cuba semula.</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% yang dilihat</string>
+ <string name="ignore">Abaikan</string>
+ <string name="stats_insights_best_ever">Pandangan Terbaik Sampai Bila-Bila</string>
+ <string name="stats_insights_most_popular_hour">Jam paling popular </string>
+ <string name="stats_insights_most_popular_day">Hari paling popular</string>
+ <string name="stats_insights_popular">Hari dan jam paling popular</string>
+ <string name="stats_insights_today">Statistik Hari Ini</string>
+ <string name="stats_insights_all_time">Kiriman, pandangan, dan pelawat sepanjang masa</string>
+ <string name="stats_insights">Wawasan</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Untuk melihat statistik anda, log masuk ke akaun WordPress.com anda untuk menyambungkan JetPack.</string>
+ <string name="stats_other_recent_stats_moved_label">Mencari statistik Terkini Yang Lain anda? Kami telah memindahnya ke halaman Wawasan.</string>
+ <string name="me_disconnect_from_wordpress_com">Putuskan dari WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Sambung ke WordPress.com</string>
+ <string name="me_btn_login_logout">Log Masuk/Log Keluar</string>
+ <string name="site_picker_cant_hide_current_site">"%s" tidak disembunyikan kerana ianya laman semasa</string>
+ <string name="account_settings">Tetapan Akaun</string>
+ <string name="me_btn_support">Bantuan &amp; Sokongan</string>
+ <string name="site_picker_create_dotcom">Cipta laman WordPress.com</string>
+ <string name="site_picker_add_self_hosted">Tambah laman yang dihos sendiri</string>
+ <string name="site_picker_add_site">Tambah laman</string>
+ <string name="site_picker_edit_visibility">Tunjuk/sembunyi laman</string>
+ <string name="site_picker_title">Pilih laman</string>
+ <string name="my_site_btn_view_admin">Lihat Pentadbir</string>
+ <string name="my_site_btn_view_site">Lihat Laman</string>
+ <string name="my_site_btn_switch_site">Tukar Laman</string>
+ <string name="my_site_btn_blog_posts">Kiriman Blog</string>
+ <string name="my_site_header_look_and_feel">Rupa dan Bentuk</string>
+ <string name="my_site_header_publish">Terbit</string>
+ <string name="my_site_btn_site_settings">Tetapan</string>
+ <string name="my_site_header_configuration">Konfigurasi</string>
+ <string name="reader_label_new_posts_subtitle">Tap untuk menunjukkannya</string>
+ <string name="notifications_account_required">Log masuk ke WordPress.com untuk pemberitahuan</string>
+ <string name="stats_unknown_author">Pengarang Tidak Diketahui</string>
+ <string name="signout">Putuskan</string>
+ <string name="image_added">Imej ditambah</string>
+ <string name="deselect_all">Nyahpilih semua</string>
+ <string name="sign_out_wpcom_confirm">Memutuskan akaun anda akan membuang semua data WordPress.com bagi @%s dari peranti ini, termasuklah draf setempat dan perubahan setempat.</string>
+ <string name="hide">Sembunyi</string>
+ <string name="show">Tunjuk</string>
+ <string name="select_all">Pilih semua</string>
+ <string name="select_from_new_picker">Pilihan jamak dengan pemetik baharu</string>
+ <string name="no_media_sources">Tidak dapat mengambil media</string>
+ <string name="error_loading_images">Ralat memuat imej</string>
+ <string name="error_loading_videos">Ralat memuat video</string>
+ <string name="loading_blog_images">Mengambil imej</string>
+ <string name="loading_blog_videos">Mengambil video</string>
+ <string name="no_blog_images">Tiada imej</string>
+ <string name="no_device_images">Tiada imej</string>
+ <string name="stats_generic_error">Statistik Yang Diperlukan tidak dapat dimuatkan</string>
+ <string name="no_device_videos">Tiada video</string>
+ <string name="no_blog_videos">Tiada video</string>
+ <string name="error_loading_blog_images">Tidak dapat mengambil imej</string>
+ <string name="error_loading_blog_videos">Tidak dapat mengambil video</string>
+ <string name="loading_images">Memuat imej</string>
+ <string name="loading_videos">Memuat video</string>
+ <string name="no_media">Tiada media</string>
+ <string name="add_to_post">Tambah ke Kiriman</string>
+ <string name="error_publish_no_network">Tidak boleh menerbit semasa tiada sambungan. Disimpan sebagai draf.</string>
+ <string name="two_step_sms_sent">Periksa mesej teks anda untuk kod pengesahan.</string>
+ <string name="device">Peranti</string>
+ <string name="tab_title_device_images">Imej Peranti</string>
+ <string name="tab_title_device_videos">Video Peranti</string>
+ <string name="two_step_footer_label">Masukkan kod dari aplikasi pengesah anda.</string>
+ <string name="media_details_label_file_name">Nama fail</string>
+ <string name="media_details_label_file_type">Jenis fail</string>
+ <string name="editor_toast_invalid_path">Laluan fail tidak sah</string>
+ <string name="invalid_verification_code">Kod pengesahan tidak sah</string>
+ <string name="language">Bahasa</string>
+ <string name="tab_title_site_videos">Video Laman</string>
+ <string name="verification_code">Kod pengesahan</string>
+ <string name="verify">Sahkan</string>
+ <string name="two_step_footer_button">Hantar kod melalui mesej teks</string>
+ <string name="sign_in_jetpack">Log masuk ke akaun WordPress.com anda dan berhubung dengan Jetpack.</string>
+ <string name="auth_required">Log masuk semula untuk meneruskan.</string>
+ <string name="media_picker_title">Pilih media</string>
+ <string name="take_photo">Ambil gambar</string>
+ <string name="take_video">Ambil video</string>
+ <string name="tab_title_site_images">Imej Laman</string>
+ <string name="stats_view_authors">Pengarang</string>
+ <string name="error_notification_open">Tidak dapat membuka pemberitahuan</string>
+ <string name="comments_fetching">Mengambil ulasan...</string>
+ <string name="media_fetching">Mengambil media...</string>
+ <string name="pages_fetching">Mengambil halaman...</string>
+ <string name="posts_fetching">Mengambil kiriman...</string>
+ <string name="stats_empty_search_terms_desc">Pelajari dengan lanjut mengenai trafik carian anda dengan melihat pada istilah yang dicari oleh para pelawat anda untuk mencari laman anda.</string>
+ <string name="reader_empty_posts_request_failed">Tidak dapat mendapatkan kiriman</string>
+ <string name="publisher">Penerbit:</string>
+ <string name="stats_followers_total_email_paged">Menunjukkan %1$d - %2$d daripada %3$s Pengikut Emel</string>
+ <string name="stats_search_terms_unknown_search_terms">Istilah Carian Tidak Diketahui</string>
+ <string name="stats_followers_total_wpcom_paged">Menunjukkan %1$d - %2$d daripada %3$s Pengikut WordPress.com</string>
+ <string name="stats_empty_search_terms">Tiada istilah carian direkodkan</string>
+ <string name="stats_entry_search_terms">Istilah Carian</string>
+ <string name="stats_view_search_terms">Istilah Carian</string>
+ <string name="toast_err_post_uploading">Tidak dapat membuka kiriman semasa ianya dimuat naik</string>
+ <string name="error_copy_to_clipboard">Ralat berlaku semasa menyalin teks ke papan keratan</string>
+ <string name="logs_copied_to_clipboard">Log aplikasi telah disalin ke papan keratan</string>
+ <string name="stats_average_per_day">Purata Sehari</string>
+ <string name="stats_months_and_years">Bulan dan Tahun</string>
+ <string name="reader_label_new_posts">Kiriman baharu</string>
+ <string name="post_uploading">Memuat naik</string>
+ <string name="stats_total">Jumlah</string>
+ <string name="stats_overall">Keseluruhan</string>
+ <string name="stats_period">Tempoh</string>
+ <string name="reader_empty_posts_in_blog">Blog ini kosong</string>
+ <string name="stats_recent_weeks">Minggu-Minggu Terkini</string>
+ <string name="reader_page_recommended_blogs">Laman yang anda mungkin suka</string>
+ <string name="stats_followers_days">%1$d hari</string>
+ <string name="stats_followers_hours">%1$d jam</string>
+ <string name="stats_followers_minutes">%1$d minit</string>
+ <string name="stats_followers_months">%1$d bulan</string>
+ <string name="stats_followers_years">%1$d tahun</string>
+ <string name="stats_followers_a_day">Sehari</string>
+ <string name="stats_followers_a_minute_ago">seminit yang lalu</string>
+ <string name="stats_followers_a_month">Sebulan</string>
+ <string name="stats_followers_a_year">Setahun</string>
+ <string name="stats_followers_an_hour_ago">sejam lalu</string>
+ <string name="stats_entry_top_commenter">Pengarang</string>
+ <string name="stats_comments_by_authors">Oleh Pengarang</string>
+ <string name="stats_comments_by_posts_and_pages">Berdasarkan Kiriman &amp; Halaman</string>
+ <string name="stats_view_countries">Negara</string>
+ <string name="stats_empty_top_posts_desc">Temui kandungan anda yang paling dilihat, dan periksa prestasi kiriman dan halaman dari semasa ke semasa.</string>
+ <string name="stats_followers_email_selector">Emel</string>
+ <string name="stats_empty_geoviews_desc">Jelajahi senarai untuk melihat negara dan kawasan mana menjana trafik terbanyak ke laman anda.</string>
+ <string name="themes_fetching">Mengambil tema...</string>
+ <string name="stats_entry_followers">Pengikut</string>
+ <string name="stats_totals_publicize">Pengikut</string>
+ <string name="stats_view_followers">Pengikut</string>
+ <string name="stats_empty_tags_and_categories_desc">Dapatkan gambaran keseluruhan topik-topik yang paling popular di laman anda, seperti yang dipantulkan dalam kiriman teratas anda dari minggu lepas.</string>
+ <string name="stats_empty_comments_desc">Jika anda membenarkan ulasan di laman anda, jejak pengulas teratas anda dan termui kandungan apa yang mencetuskan perbincangan yang paling meriah, berdasarkan pada 1,000 ulasan yang terkini.</string>
+ <string name="stats_empty_video_desc">Jika anda telah memuat naik video menggunakan VidePress, ketahui berapa kali ianya ditonton.</string>
+ <string name="stats_empty_publicize_desc">Jejaki pengikut anda daripada pelbagai perkhidmatan perhubungan sosial menggunakan publisiti.</string>
+ <string name="stats_empty_followers_desc">Jejaki bilangan pengikut keseluruhan anda, dan berapa lama setiap orang telah mengikuti laman anda.</string>
+ <string name="stats_empty_referrers_desc">Pelajari dengan lanjut mengenai keterlihatan laman anda dengan melihat pada laman web dan enjin carian yang menghantar trafik tertinggi ke hala anda</string>
+ <string name="stats_likes">Disukai</string>
+ <string name="stats_entry_clicks_link">Pautan</string>
+ <string name="stats_empty_clicks_title">Tiada kllik direkodkan</string>
+ <string name="stats_empty_geoviews">Tiada negara direkodkan</string>
+ <string name="stats_empty_followers">Tiada pengikut</string>
+ <string name="stats_timeframe_years">Tahun</string>
+ <string name="stats_views">Pandangan</string>
+ <string name="stats_visitors">Pelawat</string>
+ <string name="stats_comments_total_comments_followers">Jumlah kiriman dengan pengikut ulasan: %1$s</string>
+ <string name="stats_empty_publicize">Tiada pengikut publisiti direkodkan</string>
+ <string name="stats_empty_video">Tiada video dimainkan</string>
+ <string name="stats_empty_top_authors_desc">Jejak pandangan pada setiap kiriman penyumbang, dan zum masuk untuk menemui kandungan paling popular mengikut setiap pengarang.</string>
+ <string name="stats_empty_tags_and_categories">Tiada kiriman atau halaman ditag yang dilihat</string>
+ <string name="stats_view_videos">Video</string>
+ <string name="stats_view_publicize">Publisiti</string>
+ <string name="stats_pagination_label">Halaman %1$s daripada %2$s</string>
+ <string name="stats_totals_followers">Sejak</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_publicize">Perkhidmatan</string>
+ <string name="stats_view_top_posts_and_pages">Kiriman &amp; Halaman</string>
+ <string name="stats_empty_clicks_desc">Bila kandungan anda termasuk pautan ke laman lain, anda akan dapat melihat mana satu yang paling diklik oleh pelawat anda.</string>
+ <string name="stats_empty_referrers_title">Tiada perujuk direkodkan</string>
+ <string name="stats_empty_top_posts_title">Tiada kiriman atau halaman dilihat</string>
+ <string name="stats_for">Statistik bagi %s</string>
+ <string name="stats_other_recent_stats_label">Lain Statistik Terkini</string>
+ <string name="stats_view_all">Lihat semua</string>
+ <string name="stats_view">Lihat</string>
+ <string name="stats_followers_seconds_ago">beberapa saat lalu</string>
+ <string name="stats_followers_total_email">Jumlah Pengikut Emel: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_total_wpcom">Jumlah Pengikut WordPress.com: %1$s</string>
+ <string name="ssl_certificate_details">Perincian</string>
+ <string name="cab_selected">%d dipilih</string>
+ <string name="confirm_delete_media">Hapuskan butir yang dipilih?</string>
+ <string name="confirm_delete_multi_media">Hapuskan butiran yang dipilih?</string>
+ <string name="delete_sure">Hapuskan draf ini</string>
+ <string name="delete_sure_page">Hapuskan halaman ini</string>
+ <string name="delete_sure_post">Hapuskan kiriman ini</string>
+ <string name="media_gallery_date_range">Paparkan media dari %1$s ke %2$s</string>
+ <string name="sure_to_remove_account">Buang laman ini?</string>
+ <string name="reader_label_comment_count_multi">%,d ulasan</string>
+ <string name="reader_title_photo_viewer">%1$d dari %2$d</string>
+ <string name="browse_our_faq_button">Layar Soalan Lazim kami</string>
+ <string name="agree_terms_of_service">Dengan mencipta akaun anda bersetuju dengan %1$sTerma Perkhidmatan%2$s yang mempesonakan</string>
+ <string name="error_publish_empty_post">Tidak boleh menerbitkan kiriman kosong</string>
+ <string name="comment">Ulasan</string>
+ <string name="comment_trashed">Ulasan disampahkan</string>
+ <string name="reader_label_comments_closed">Ulasan ditutup</string>
+ <string name="reader_label_comments_on">Ulasan dihidupkan</string>
+ <string name="create_new_blog_wpcom">Cipta blog WordPress.com</string>
+ <string name="faq_button">Soalan Lazim</string>
+ <string name="reader_label_like">Suka</string>
+ <string name="mnu_comment_liked">Disukai</string>
+ <string name="reader_empty_comments">Belum ada ulasan</string>
+ <string name="media_empty_list_custom_date">Tiada media dalam jangka masa ini</string>
+ <string name="more">Selanjutnya</string>
+ <string name="older_two_days">Lebih dari 2 hari</string>
+ <string name="older_last_week">Lebih dari seminggu</string>
+ <string name="stats_no_blog">Statistik tidak dapat dimuatkan bagi blog yang diperlukan</string>
+ <string name="select_a_blog">Pilih laman WordPress</string>
+ <string name="sending_content">Memuat naik %s kandungan</string>
+ <string name="uploading_total">Memuat naik %1$d daripada %2$d</string>
+ <string name="posts_empty_list">Belum ada kiriman. Kenapa tidak wujudkannya?</string>
+ <string name="comment_reply_to_user">Balasan kepada %s</string>
+ <string name="pages_empty_list">Belum ada halaman. Kenapa tidak wujudkannya?</string>
+ <string name="posting_post">Mengirim "%s"</string>
+ <string name="signing_out">Melog keluar…</string>
+ <string name="reader_empty_followed_blogs_title">Anda belum mengikuti mana-mana laman</string>
+ <string name="reader_empty_posts_liked">Anda belum menyukai mana-mana kiriman</string>
+ <string name="nux_help_description">Lawat pusat bantuan untuk mendapatkan jawapan kepada soalan lazim atau lawat forum untuk bertanyakan soalan baharu</string>
+ <string name="new_blog_wpcom_created">Blog WordPress.com diwujudkan!</string>
+ <string name="reader_empty_posts_in_tag">Tiada kiriman dengan tag ini</string>
+ <string name="reader_label_view_original">Lihat artikel asal</string>
+ <string name="reader_label_comment_count_single">Satu ulasan</string>
+ <string name="error_refresh_unauthorized_pages">Anda tidak mempunyai kebenaran melihat atau menyunting halaman</string>
+ <string name="error_refresh_unauthorized_comments">Anda tidak mempunyai kebenaran melihat atau menyunting ulasan</string>
+ <string name="older_month">Lebih dari sebulan</string>
+ <string name="error_refresh_unauthorized_posts">Anda tidak mempunyai kebenaran melihat atau menyunting kiriman</string>
+ <string name="reader_toast_err_generic">Tidak dapat melaksanakan tindakan ini</string>
+ <string name="reader_toast_err_block_blog">Tidak dapat menyekat blog ini</string>
+ <string name="reader_toast_blog_blocked">Kiriman daripada blog ini tidak akan ditunjukkan</string>
+ <string name="reader_menu_block_blog">Sekat blog ini</string>
+ <string name="contact_us">Hubungi kami</string>
+ <string name="hs__new_conversation_header">Bualan sokongan</string>
+ <string name="hs__conversation_header">Bualan sokongan</string>
+ <string name="hs__username_blank_error">Masukkan nama yang sah</string>
+ <string name="hs__invalid_email_error">Masukkan alamat emel yang sah</string>
+ <string name="hs__conversation_detail_error">Terangkan masalah yang kelihatan</string>
+ <string name="add_location">Tambah lokasi</string>
+ <string name="current_location">Lokasi semasa</string>
+ <string name="search_location">Cari</string>
+ <string name="search_current_location">Kesan</string>
+ <string name="edit_location">Sunting</string>
+ <string name="preference_send_usage_stats">Hantar statistik</string>
+ <string name="preference_send_usage_stats_summary">Hantar statistik penggunaan secara automatik untuk membantu kami menambahbaik WordPress untuk Android</string>
+ <string name="update_verb">Kemas kini</string>
+ <string name="schedule_verb">Jadual</string>
+ <string name="reader_title_subs">Tag &amp;amp; Blog</string>
+ <string name="reader_page_followed_tags">Tag yang diikuti</string>
+ <string name="reader_hint_add_tag_or_url">Masukkan tag atau URL untuk diikuti</string>
+ <string name="reader_label_followed_blog">Blog diikuti</string>
+ <string name="reader_label_tag_preview">Kiriman dengan tag %s</string>
+ <string name="reader_toast_err_get_blog_info">Tidak dapat memaparkan blog ini</string>
+ <string name="reader_toast_err_already_follow_blog">Anda sudah mengikuti blog ini</string>
+ <string name="reader_toast_err_follow_blog">Tidak dapat mengikut blog ini</string>
+ <string name="reader_toast_err_unfollow_blog">Tidak dapat untuk tidak mengikut blog ini</string>
+ <string name="reader_empty_recommended_blogs">Tiada blog yang disyorkan</string>
+ <string name="reader_page_followed_blogs">Laman yang diikuti</string>
+ <string name="reader_title_blog_preview">Blog Pembaca</string>
+ <string name="reader_title_tag_preview">Tag Pembaca</string>
+ <string name="media_empty_list">Tiada media</string>
+ <string name="ptr_tip_message">Tip: Tarik ke bawah untuk segar semula</string>
+ <string name="saving">Simpan...</string>
+ <string name="help">Bantuan</string>
+ <string name="forums">Forum</string>
+ <string name="help_center">Pusat bantuan</string>
+ <string name="ssl_certificate_error">Sijil SSL tidak sah</string>
+ <string name="ssl_certificate_ask_trust">Jika anda biasanya berhubung ke laman ini tanpa masalah, ralat ini mungkin bermakna bahawa seseorang sedang cuba menyamar sebagai laman ini, dan anda tidak sepatutnya teruskan. Adakah anda masih ingin mempercayai sijil ini?</string>
+ <string name="forgot_password">Hilang kata laluan anda?</string>
+ <string name="out_of_memory">Memori peranti tidak mencukupi</string>
+ <string name="adding_cat_success">Kategori berjaya ditambah</string>
+ <string name="cat_name_required">Medan nama kategori dikehendaki</string>
+ <string name="category_automatically_renamed">Nama kategori %1$s tidak sah. Ia telah dinamakan semula sebagai %2$s.</string>
+ <string name="no_account">Akaun WordPress tidak dijumpai, tambah akaun dan cuba lagi</string>
+ <string name="stats_bar_graph_empty">Tiada statistik tersedia</string>
+ <string name="reply_failed">Balasan gagal</string>
+ <string name="notifications_empty_list">Tiada notifikasi</string>
+ <string name="error_delete_post">Ralat berlaku apabila sedang menghapuskan %s</string>
+ <string name="error_refresh_posts">Kiriman tidak dapat disegar semula pada masa ini</string>
+ <string name="error_refresh_pages">Laman tidak dapat disegar semula pada masa ini</string>
+ <string name="error_refresh_notifications">Pemberitahuan tidak dapat disegar semula pada masa ini</string>
+ <string name="error_refresh_stats">Statistik tidak dapat disegar semula pada masa ini</string>
+ <string name="error_generic">Ralat berlaku</string>
+ <string name="error_moderate_comment">Ralat berlaku semasa penyederhanaan</string>
+ <string name="error_edit_comment">Ralat berlaku semasa mengedit komen</string>
+ <string name="error_upload">Ralat berlaku semasa memuat naik %s</string>
+ <string name="error_load_comment">Tidak dapat memuatkan komen</string>
+ <string name="error_downloading_image">Ralat memuat turun imej</string>
+ <string name="passcode_wrong_passcode">PIN Salah</string>
+ <string name="username_exists">Nama pengguna tersebut sudah wujud</string>
+ <string name="email_exists">Alamat emel tersebut sudah digunakan</string>
+ <string name="blog_name_required">Masukkan alamat laman</string>
+ <string name="blog_name_not_allowed">Alamat laman tersebut tidak dibenarkan</string>
+ <string name="blog_name_must_be_at_least_four_characters">Alamat laman mestilah sekurang-kurangnya 4 karakter</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Alamat laman mestilah kurang daripada 64 karakter</string>
+ <string name="blog_name_contains_invalid_characters">Alamat laman tidak boleh mengandungi karakter "_"</string>
+ <string name="blog_name_cant_be_used">Anda tidak boleh menggunakan alaman laman tersebut</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Alamat laman hanya boleh mengandungi huruf kecil (a-z) dan nombor</string>
+ <string name="blog_name_exists">Laman tersebut sudah wujud</string>
+ <string name="blog_name_reserved">Laman tersebut adalah tersimpan</string>
+ <string name="username_reserved_but_may_be_available">Nama pengguna tersebut adalah tersimpan tetapi mungkin akan tersedia dalam beberapa hari</string>
+ <string name="blog_name_reserved_but_may_be_available">Laman tersebut adalah tersimpan tetapi mungkin akan tersedia dalam beberapa hari</string>
+ <string name="username_or_password_incorrect">Nama pengguna atau kata laluan yang anda masukkan adalah tidak betul</string>
+ <string name="nux_cannot_log_in">Kami tidak dapat melog anda masuk</string>
+ <string name="invalid_password_message">Kata laluan mestilah mengandungi sekurang-kurangnya 4 karakter</string>
+ <string name="invalid_username_too_short">Nama pengguna mestilah lebih daripada 4 karakter</string>
+ <string name="invalid_username_too_long">Nama pengguna mestilah kurang daripada 61 karakter</string>
+ <string name="username_only_lowercase_letters_and_numbers">Nama pengguna hanya boleh mengandungi huruf kecil (a-z) dan nombor</string>
+ <string name="username_required">Masukkan nama pengguna</string>
+ <string name="username_not_allowed">Nama pengguna tidak dibenarkan</string>
+ <string name="username_must_be_at_least_four_characters">Nama pengguna mestilah sekurang-kurangnya 4 karakter</string>
+ <string name="username_contains_invalid_characters">Nama pengguna tidak boleh mengandungi karakter “_”</string>
+ <string name="username_must_include_letters">Nama pengguna mesti mengandungi sekurang-kurangnya 1 huruf (a-z)</string>
+ <string name="email_invalid">Masukkan alamat emel yang sah</string>
+ <string name="email_not_allowed">Alamat emel tersebut tidak dibenarkan</string>
+ <string name="no_network_message">Tiada rangkaian tersedia</string>
+ <string name="could_not_remove_account">Tidak dapat membuang blog</string>
+ <string name="gallery_error">Butir media tidak boleh didapatkan kembali</string>
+ <string name="blog_not_found">Ralat berlaku apabila mencapai blog ini</string>
+ <string name="wait_until_upload_completes">Tunggu sehingga muat naik selesai</string>
+ <string name="theme_fetch_failed">Gagal mengambil tema</string>
+ <string name="theme_set_failed">Gagal menetapkan tema</string>
+ <string name="theme_auth_error_message">Pastikan anda mempunyai hak untuk menetapkan tema</string>
+ <string name="mnu_comment_unspam">Bukan spam</string>
+ <string name="no_site_error">Tidak dapat menyambung ke laman WordPress</string>
+ <string name="adding_cat_failed">Tambah kategori telah gagal</string>
+ <string name="sdcard_message">Kad SD yang dipasang diperlukan untuk memuat naik media</string>
+ <string name="error_refresh_comments">Ulasan tidak dapat disegar semula pada masa ini</string>
+ <string name="comments_empty_list">Tiada ulasan</string>
+ <string name="stats_empty_comments">Belum ada ulasan</string>
+ <string name="invalid_url_message">Periksa sama ada URL yang dimasukkan adalah sah</string>
+ <string name="invalid_email_message">Alamat emel anda tidak sah</string>
+ <string name="media_gallery_num_columns">Bilangan lajur</string>
+ <string name="media_gallery_type_thumbnail_grid">Grid gambar kenit</string>
+ <string name="cannot_delete_multi_media_items">Sesetengah media tidak boleh dihapuskan pada masa ini. Cuba sebentar lagi.</string>
+ <string name="themes_live_preview">Pratonton secara langsung</string>
+ <string name="theme_current_theme">Tema semasa</string>
+ <string name="theme_premium_theme">Tema premium</string>
+ <string name="link_enter_url_text">Teks pautan (pilihan)</string>
+ <string name="create_a_link">Cipta pautan</string>
+ <string name="local_draft">Draf setempat</string>
+ <string name="xmlrpc_error">Tidak dapat dihubungi. Masukkan laluan penuh ke xmlrpc.php di laman anda dan cuba lagi.</string>
+ <string name="select_categories">Pilih kategori</string>
+ <string name="add_comment">Tambah komen</string>
+ <string name="connection_error">Ralat hubungan</string>
+ <string name="cancel_edit">Batal edit</string>
+ <string name="scaled_image_error">Masukkan nilai lebar berskala yang sah</string>
+ <string name="post_not_found">Ralat berlaku apabila kiriman dimuatkan. Segar semula kiriman anda dan cuba lagi.</string>
+ <string name="mnu_comment_trash">Sampah</string>
+ <string name="dlg_approving_comments">Meluluskan</string>
+ <string name="dlg_unapproving_comments">Tidak meluluskan</string>
+ <string name="dlg_trashing_comments">Hantar ke sampah</string>
+ <string name="dlg_confirm_trash_comments">Hantar ke sampah?</string>
+ <string name="trash_yes">Sampah</string>
+ <string name="trash">Sampah</string>
+ <string name="author_name">Nama pengarang</string>
+ <string name="author_email">Emel pengarang</string>
+ <string name="author_url">URL pengarang</string>
+ <string name="saving_changes">Simpan perubahan</string>
+ <string name="upload_failed">Muat naik gagal</string>
+ <string name="horizontal_alignment">Jajaran melintang</string>
+ <string name="file_not_found">Fail media untuk dimuat naik tidak dapat dicari. Adakah ia dihapuskan atau dipindahkan></string>
+ <string name="delete_post">Hapus kiriman</string>
+ <string name="delete_page">Hapus laman</string>
+ <string name="comment_status_approved">Diluluskan</string>
+ <string name="comment_status_unapproved">Menunggu</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Disampahkan</string>
+ <string name="mnu_comment_approve">Lulus</string>
+ <string name="mnu_comment_unapprove">Tidak diluluskan</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="category_slug">Slug kategori (pilihan)</string>
+ <string name="category_desc">Keterangan kategori (pilihan)</string>
+ <string name="category_parent">Induk kategori (pilihan):</string>
+ <string name="share_action_post">Kiriman baharu</string>
+ <string name="file_error_create">Tidak dapat mencipta fail sementara untuk muat naik media. Pastikan terdapat ruang yang cukup dalam peranti anda.</string>
+ <string name="location_not_found">Lokasi tidak diketahui</string>
+ <string name="open_source_licenses">Lesen sumber terbuka</string>
+ <string name="delete_draft">Hapus draf</string>
+ <string name="preview_page">Pratonton laman</string>
+ <string name="preview_post">Pratonton kiriman</string>
+ <string name="post_not_published">Status kiriman tidak diterbitkan</string>
+ <string name="page_not_published">Status laman tidak diterbitkan</string>
+ <string name="view_in_browser">Lihat di pelayar</string>
+ <string name="add_new_category">Tambah kategori baharu</string>
+ <string name="category_name">Nama kategori</string>
+ <string name="privacy_policy">Polisi privasi</string>
+ <string name="local_changes">Perubahan setempat</string>
+ <string name="add_account_blog_url">Alamat blog</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="fatal_db_error">Ralat berlaku semasa mencipta pangkalan data aplikasi. Cuba memasangnya semula.</string>
+ <string name="jetpack_message_not_admin">Pemalan Jetpack diperlukan bagi statistik. Hubungi pentadbir laman.</string>
+ <string name="reader_title_applog">Log aplikasi</string>
+ <string name="reader_share_link">Kongsi pautan</string>
+ <string name="reader_toast_err_add_tag">Tidak dapat menambah tag ini</string>
+ <string name="reader_toast_err_remove_tag">Tidak dapat membuang tag ini</string>
+ <string name="pending_review">Menunggu semak semula</string>
+ <string name="http_credentials">Kelayakan HTTP (pilihan)</string>
+ <string name="http_authorization_required">Kebenaran diperlukan</string>
+ <string name="post_format">Format kiriman</string>
+ <string name="new_post">Kiriman baharu</string>
+ <string name="new_media">Media baharu</string>
+ <string name="view_site">Lihat laman</string>
+ <string name="required_field">Medan yang diperlukan</string>
+ <string name="email_hint">Alamat emel</string>
+ <string name="site_address">Alamat laman anda yang dihos sendiri (URL)</string>
+ <string name="email_cant_be_used_to_signup">Anda tidak boleh menggunakan alamat emel tersebut untuk mendaftar. Kami menghadapi masalah dimana emel kami dihalang oleh perkhidmatan tersebut. Gunakan penyedia emel yang lain. </string>
+ <string name="email_reserved">Alamat emel tersebut telahpun digunakan. Periksa kota masuk anda untuk emel pengaktifan. Jika anda tidak mengaktifkannya anda boleh mencuba lagi dalam beberapa hari.</string>
+ <string name="blog_name_must_include_letters">Alamat laman mestilah mempunyai sekurang-kurangnya 1 abjad (a-z)</string>
+ <string name="blog_name_invalid">Alamat laman tidak sah</string>
+ <string name="blog_title_invalid">Tajuk laman tidak sah</string>
+ <string name="image_settings">Tetapan imej</string>
+ <string name="error_blog_hidden">Blog ini disembunyi dan tidak dapat dimuatkan. Bolehkan ia semula dalam tetapan dan cuba lagi.</string>
+ <string name="page_settings">Tetapan laman</string>
+ <string name="media_gallery_settings_title">Tetapan galeri</string>
+ <string name="post_settings">Tetapan kiriman</string>
+ <string name="remove_account">Buang laman</string>
+ <string name="blog_removed_successfully">Laman berjaya dibuang</string>
+ <string name="account_details">Perincian akaun</string>
+ <string name="sure_to_cancel_edit_comment">Batal menyunting komen ini?</string>
+ <string name="hint_comment_content">Ulasan</string>
+ <string name="comment_added">Ulasan berjaya ditambah</string>
+ <string name="toast_comment_unedited">Ulasan tidak berubah</string>
+ <string name="content_required">Ulasan diwajibkan</string>
+ <string name="trash_no">Jangan sampahkan</string>
+ <string name="edit_comment">Sunting ulasan</string>
+ <string name="media_gallery_edit">Sunting galeri</string>
+ <string name="edit_post">Sunting kiriman</string>
+ <string name="media_gallery_image_order">Aturan imej</string>
+ <string name="learn_more">Pelajari selanjutnya</string>
+ <string name="dlg_spamming_comments">Ditanda sebagai spam</string>
+ <string name="share_action_media">Pustaka media</string>
+ <string name="invalid_site_url_message">Periksa bahawa URL laman yang dimasukkan adalah sah</string>
+ <string name="notifications_empty_all">Belum ada pemberitahuan.</string>
+ <string name="media_error_no_permission">Anda tidak mempunyai kebenaran untuk melihat pustaka media</string>
+ <string name="deleting_post">Menghapuskan kiriman</string>
+ <string name="share_url_post">Kongsi kiriman</string>
+ <string name="share_url_page">Kongsi laman</string>
+ <string name="share_link">Kongsi pautan</string>
+ <string name="deleting_page">Menghapuskan halaman</string>
+ <string name="creating_your_site">Cipta laman anda</string>
+ <string name="creating_your_account">Mencipta akaun anda</string>
+ <string name="error_refresh_media">Sesuatu tidak kena semasa menyegar semula perpustakaan media. Cuba sebentar lagi.</string>
+ <string name="reader_empty_posts_in_tag_updating">Mengambil kiriman...</string>
+ <string name="reader_likes_you_and_multi">Anda dan %,d yang lain sukakannya</string>
+ <string name="reader_likes_multi">%,d orang sukakanya</string>
+ <string name="reader_toast_err_get_comment">Komen ini tidak boleh didapatkan</string>
+ <string name="reader_label_reply">Balas</string>
+ <string name="video">Video</string>
+ <string name="download">Memuat turun media</string>
+ <string name="cant_share_no_visible_blog">Anda tidak boleh berkongsi ke WordPress tanpa blog yang kelihatan</string>
+ <string name="comment_spammed">Ulasan ditanda sebagai spam</string>
+ <string name="select_time">Pilih masa</string>
+ <string name="reader_likes_you_and_one">Anda dan seorang lagi sukakannya</string>
+ <string name="select_date">Pilih tarikh</string>
+ <string name="pick_photo">Pilih gambar</string>
+ <string name="pick_video">Pilih video</string>
+ <string name="reader_toast_err_get_post">Tidak boleh mendapatkan kiriman ini</string>
+ <string name="validating_user_data">Pengesahan data pengguna</string>
+ <string name="validating_site_data">Pengesahan data laman</string>
+ <string name="account_two_step_auth_enabled">Pengesahan dua langkah telah dibolehkan untuk akaun ini. Lawat tetapan keselamatan anda di WordPress.com dan janakan kata laluan bagi aplikasi tersebut.</string>
+ <string name="reader_empty_followed_blogs_description">Tapi jangan khuatir, hanya ketik ikon di atas sebelah kanan untuk memulakan pengembaraan!</string>
+ <string name="nux_add_selfhosted_blog">Tambah laman yang dihos sendiri</string>
+ <string name="nux_oops_not_selfhosted_blog">Daftar masuk ke WordPress.com</string>
+ <string name="password_invalid">Anda perlukan kata laluan yang lebih selamat. Pastikan anda menggunakan 7 atau lebih aksara, campuran abjad kecil dan besar, nombor atau aksara khas.</string>
+ <string name="nux_tap_continue">Teruskan</string>
+ <string name="nux_welcome_create_account">Cipta akaun</string>
+ <string name="signing_in">Melog masuk...</string>
+ <string name="media_add_new_media_gallery">Cipta galeri</string>
+ <string name="empty_list_default">Senarai ini kosong</string>
+ <string name="select_from_media_library">Pilih dari perpustakaan media</string>
+ <string name="jetpack_message">Pemalam Jetpack diperlukan bagi statistik. Anda ingin memasang Jetpack?</string>
+ <string name="jetpack_not_found">Pemalam Jetpack tidak dijumpai</string>
+ <string name="reader_untitled_post">(Tanpa tajuk)</string>
+ <string name="reader_share_subject">Dikongsi dari %s</string>
+ <string name="reader_btn_share">Kongsi</string>
+ <string name="reader_btn_follow">Ikut</string>
+ <string name="reader_toast_err_url_intent">Tidak dapat membuka %s</string>
+ <string name="connecting_wpcom">Berhubung ke WordPress.com</string>
+ <string name="username_invalid">Nama pengguna tidak sah</string>
+ <string name="limit_reached">Had telah dicapai. Anda boleh mencuba lagi dalam masa seminit. Cubaan sebelum tempoh tersebut hanya akan meningkatkan masa menunggu anda sebelum larangan dibatalkan. Jika anda fikirkan ianya suatu kesilapan, hubungi sokongan.</string>
+ <string name="nux_tutorial_get_started_title">Mulakan!</string>
+ <string name="reader_btn_unfollow">Mengikuti</string>
+ <string name="reader_label_added_tag">%s ditambah</string>
+ <string name="reader_label_removed_tag">%s dibuang</string>
+ <string name="reader_likes_one">Seorang menyukainya</string>
+ <string name="reader_likes_only_you">Anda menyukainya</string>
+ <string name="reader_toast_err_comment_failed">Tidak dapat mengirim komen anda</string>
+ <string name="reader_toast_err_tag_exists">Anda telahpun mengikut tag ini</string>
+ <string name="reader_toast_err_tag_invalid">Ianya bukan tag yang sah</string>
+ <string name="reader_toast_err_share_intent">Tidak dapat dikongsi</string>
+ <string name="reader_toast_err_view_image">Tidak dapat melihat imej</string>
+ <string name="reader_hint_comment_on_comment">Balas ulasan...</string>
+ <string name="media_add_popup_title">Tambah ke pustaka media</string>
+ <string name="create_account_wpcom">Cipta akaun di WordPress.com</string>
+ <string name="reader_empty_followed_tags">Anda tidak mengikut apa-apa tag</string>
+ <string name="button_next">Seterusnya</string>
+ <string name="stats_totals_plays">Mainan</string>
+ <string name="passcode_turn_on">Pasangkan kunci PIN</string>
+ <string name="passcode_manage">Urus kunci PIN</string>
+ <string name="passcode_enter_passcode">Masukkan PIN anda</string>
+ <string name="passcode_enter_old_passcode">Masukkan PIN lama anda</string>
+ <string name="passcode_re_enter_passcode">Masukkan kembali PIN anda</string>
+ <string name="passcode_change_passcode">Tukar PIN</string>
+ <string name="passcode_set">Tetapan PIN</string>
+ <string name="passcode_preference_title">Kunci PIN</string>
+ <string name="passcode_turn_off">Matikan kunci PIN</string>
+ <string name="stats_entry_country">Negara</string>
+ <string name="stats_entry_posts_and_pages">Tajuk</string>
+ <string name="stats_entry_tags_and_categories">Topik</string>
+ <string name="stats_entry_authors">Pengarang</string>
+ <string name="stats_entry_referrers">Perujuk</string>
+ <string name="stats_totals_views">Paparan</string>
+ <string name="stats_totals_clicks">Klik</string>
+ <string name="post_excerpt">Petikan</string>
+ <string name="share_action_title">Tambah ke ...</string>
+ <string name="share_action">Kongsi</string>
+ <string name="stats">Statistik</string>
+ <string name="stats_view_visitors_and_views">Pelawat dan Paparan</string>
+ <string name="stats_view_clicks">Klik</string>
+ <string name="stats_view_referrers">Perujuk</string>
+ <string name="stats_timeframe_today">Hari ini</string>
+ <string name="stats_timeframe_yesterday">Kelmarin</string>
+ <string name="stats_timeframe_days">Hari</string>
+ <string name="stats_timeframe_weeks">Minggu</string>
+ <string name="stats_timeframe_months">Bulan</string>
+ <string name="media_edit_caption_text">Sari kata</string>
+ <string name="media_edit_description_text">Keterangan</string>
+ <string name="media_edit_title_hint">Masukkan tajuk di sini</string>
+ <string name="media_edit_caption_hint">Masukkan sari kata di sini</string>
+ <string name="media_edit_description_hint">Masukkan keterangan di sini</string>
+ <string name="media_edit_success">Telah dikemaskini</string>
+ <string name="themes">Tema</string>
+ <string name="all">Semua</string>
+ <string name="images">Imej</string>
+ <string name="unattached">Tidak terikat</string>
+ <string name="custom_date">Tarikh Disuai</string>
+ <string name="media_add_popup_capture_photo">Tangkap gambar</string>
+ <string name="media_add_popup_capture_video">Tangkap video</string>
+ <string name="media_gallery_image_order_random">Rawak</string>
+ <string name="media_gallery_image_order_reverse">Songsang</string>
+ <string name="media_gallery_type">Jenis</string>
+ <string name="media_gallery_type_squares">Segi empat sama</string>
+ <string name="media_gallery_type_tiled">Berjubin</string>
+ <string name="media_gallery_type_circles">Bulatan</string>
+ <string name="media_gallery_type_slideshow">Persembahan slaid</string>
+ <string name="media_edit_title_text">Tajuk</string>
+ <string name="themes_features_label">Ciri</string>
+ <string name="theme_activate_button">Aktifkan</string>
+ <string name="theme_activating_button">Mengaktifkan</string>
+ <string name="theme_set_success">Tema berjaya ditetapkan!</string>
+ <string name="themes_details_label">Perincian</string>
+ <string name="theme_auth_error_title">Gagal mengambil tema</string>
+ <string name="media_edit_failure">Gagal mengemas kini</string>
+ <string name="stats_view_tags_and_categories">Tag &amp; Kategori</string>
+ <string name="upload">Muat naik</string>
+ <string name="discard">Buang</string>
+ <string name="note_reply_successful">Balasan diterbitkan</string>
+ <string name="new_notifications">%d pemberitahuan baharu</string>
+ <string name="more_notifications">dan %d lagi.</string>
+ <string name="sign_in">Daftar masuk</string>
+ <string name="notifications">Pemberitahuan</string>
+ <string name="follows">Ikutan</string>
+ <string name="loading">Sedang memuat...</string>
+ <string name="httpuser">Nama pengguna HTTP</string>
+ <string name="httppassword">Kata laluan HTTP</string>
+ <string name="error_media_upload">Ralat berlaku semasa memuat naik media</string>
+ <string name="post_content">Kandungan (ketik untuk menambah teks dan media)</string>
+ <string name="publish_date">Terbit</string>
+ <string name="content_description_add_media">Tambah media</string>
+ <string name="incorrect_credentials">Nama pengguna atau kata laluan tidak betul.</string>
+ <string name="username">Nama pengguna</string>
+ <string name="password">Kata laluan</string>
+ <string name="reader">Pembaca</string>
+ <string name="featured">Guna sebagai imej yang diketengahkan</string>
+ <string name="featured_in_post">Sertakan imej dalam kandungan kiriman</string>
+ <string name="no_network_title">Tiada rangkaian yang tersedia</string>
+ <string name="pages">Laman</string>
+ <string name="caption">Sari kata (pilihan)</string>
+ <string name="width">Lebar</string>
+ <string name="posts">Kiriman</string>
+ <string name="anonymous">Tak bernama</string>
+ <string name="page">Laman</string>
+ <string name="post">Kiriman</string>
+ <string name="blogusername">namapenggunablog</string>
+ <string name="ok">OK</string>
+ <string name="scaled_image">Lebar imej berskala</string>
+ <string name="upload_scaled_image">Muat naik dan pautkan ke imej berskala</string>
+ <string name="scheduled">Dijadualkan</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Memuat naik...</string>
+ <string name="version">Versi</string>
+ <string name="tos">Terma Perkhidmatan</string>
+ <string name="app_title">WordPress untuk Android</string>
+ <string name="max_thumbnail_px_width">Lebar Lalai Imej</string>
+ <string name="image_alignment">Penjajaran</string>
+ <string name="refresh">Segar semula</string>
+ <string name="untitled">Tiada tajuk</string>
+ <string name="edit">Edit</string>
+ <string name="post_id">Kiriman</string>
+ <string name="page_id">Laman</string>
+ <string name="post_password">Kata laluan (pilihan)</string>
+ <string name="immediately">Segera</string>
+ <string name="quickpress_add_alert_title">Set nama jalan pintas</string>
+ <string name="settings">Tetapan</string>
+ <string name="today">Hari Ini</string>
+ <string name="share_url">Kongsi URL</string>
+ <string name="quickpress_window_title">Pilih blog untuk jalan pintas QuickPress</string>
+ <string name="quickpress_add_error">Nama jalan pintas tidak boleh kosong</string>
+ <string name="publish_post">Terbit</string>
+ <string name="draft">Draf</string>
+ <string name="post_private">Sulit</string>
+ <string name="upload_full_size_image">Muat naik dan pautkan ke imej penuh</string>
+ <string name="tags_separate_with_commas">Tag (pisahkan tag dengan koma)</string>
+ <string name="categories">Kategori</string>
+ <string name="title">Tajuk</string>
+ <string name="dlg_deleting_comments">Menghapuskan ulasan</string>
+ <string name="notification_blink">Kerlipkan lampu pemberitahuan</string>
+ <string name="notification_vibrate">Getar</string>
+ <string name="notification_sound">Bunyi pemberitahuan</string>
+ <string name="status">Status</string>
+ <string name="location">Lokasi</string>
+ <string name="sdcard_title">Kad SD Diperlukan</string>
+ <string name="select_video">Pilih video dari galeri</string>
+ <string name="media">Media</string>
+ <string name="delete">Hapus</string>
+ <string name="none">Tiada</string>
+ <string name="blogs">Blog</string>
+ <string name="select_photo">Pilih gambar daripada galeri</string>
+ <string name="error">Ralat</string>
+ <string name="cancel">Batal</string>
+ <string name="save">Simpan</string>
+ <string name="add">Tambah</string>
+ <string name="category_refresh_error">Ralat segar semula kategori</string>
+ <string name="preview">Pratonton</string>
+ <string name="on">on</string>
+ <string name="reply">Balas</string>
+ <string name="yes">Ya</string>
+ <string name="no">Tidak</string>
+ <string name="notification_settings">Tetapan Pemberitahuan</string>
+</resources>
diff --git a/WordPress/src/main/res/values-nb/strings.xml b/WordPress/src/main/res/values-nb/strings.xml
new file mode 100644
index 000000000..d2a03fcb5
--- /dev/null
+++ b/WordPress/src/main/res/values-nb/strings.xml
@@ -0,0 +1,398 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="reader_short_comment_count_multi">%s kommentarer</string>
+ <string name="reader_short_comment_count_one">1 kommentar</string>
+ <string name="reader_label_follow_count">%,d følgere</string>
+ <string name="local_draft_explainer">Dette innlegget er en lokal kladd som ikke er publisert</string>
+ <string name="local_changes_explainer">Dette innlegget har lokale endringer som ikke er publisert</string>
+ <string name="replies_to_your_comments">Svar på dine kommentarer</string>
+ <string name="email">E-post</string>
+ <string name="stats_insights_latest_post_trend">Det er %1$s siden %2$s ble publisert. Her ser du hvordan innlegget har klart seg så langt …</string>
+ <string name="yesterday">I går</string>
+ <string name="days_ago">%d dager siden</string>
+ <string name="post_deleted">Innlegg slettet</string>
+ <string name="button_back">Tilbake</string>
+ <string name="page_deleted">Side slettet</string>
+ <string name="button_stats">Statistikk</string>
+ <string name="button_preview">Forhåndsvis</string>
+ <string name="button_view">Vis</string>
+ <string name="button_edit">Rediger</string>
+ <string name="button_publish">Publiser</string>
+ <string name="button_trash">Papirkurv</string>
+ <string name="page_trashed">Siden er lagt i papirkurven</string>
+ <string name="stats_no_activity_this_period">Ingen aktivitet i denne perioden</string>
+ <string name="post_trashed">Innlegg lagt i papirkurven</string>
+ <string name="trashed">I papirkurven</string>
+ <string name="undo">Angre</string>
+ <string name="tabbar_accessibility_label_my_site">Mitt nettsted</string>
+ <string name="tabbar_accessibility_label_me">Meg</string>
+ <string name="editor_toast_changes_saved">Endringer ble lagret</string>
+ <string name="passcodelock_prompt_message">Skriv inn PIN</string>
+ <string name="ignore">Ignorer</string>
+ <string name="stats_insights_most_popular_hour">Mest populære time</string>
+ <string name="stats_insights_most_popular_day">Mest populære dag</string>
+ <string name="stats_insights_popular">Mest populære dag og time</string>
+ <string name="stats_insights_today">Dagens statistikk</string>
+ <string name="me_disconnect_from_wordpress_com">Koble fra WordPress.com</string>
+ <string name="me_btn_login_logout">Logg inn/Logg ut</string>
+ <string name="me_connect_to_wordpress_com">Koble til WordPress.com</string>
+ <string name="my_site_btn_view_site">Vis nettsted</string>
+ <string name="my_site_header_publish">Publiser</string>
+ <string name="my_site_btn_site_settings">Innstillinger</string>
+ <string name="notifications_account_required">Logg inn på WordPress.com for varsler</string>
+ <string name="image_added">Bilde lagt til</string>
+ <string name="show">Vis</string>
+ <string name="hide">Skjul</string>
+ <string name="select_all">Velg alle</string>
+ <string name="no_device_videos">Ingen videoer</string>
+ <string name="no_blog_images">Ingen bilder</string>
+ <string name="no_blog_videos">Ingen videoer</string>
+ <string name="no_device_images">Ingen bilder</string>
+ <string name="loading_blog_images">Henter bilder</string>
+ <string name="loading_blog_videos">Henter videoer</string>
+ <string name="no_media_sources">Kunne ikke hente media</string>
+ <string name="loading_videos">Laster filmer</string>
+ <string name="no_media">Ingen media</string>
+ <string name="loading_images">Laster bilder</string>
+ <string name="take_video">Lag en film</string>
+ <string name="add_to_post">Legg til innlegg</string>
+ <string name="tab_title_site_videos">Filmer på nettsted</string>
+ <string name="tab_title_site_images">Bilder på nettsted</string>
+ <string name="tab_title_device_videos">Filmer på enhet</string>
+ <string name="tab_title_device_images">Bilder på enhet</string>
+ <string name="two_step_footer_label">Skriv inn koden fra din autentiseringsapp.</string>
+ <string name="two_step_footer_button">Send kode via tekstmelding.</string>
+ <string name="two_step_sms_sent">Sjekk dine tekstmeldinger for verifikasjonskoden.</string>
+ <string name="sign_in_jetpack">Logg deg inn på din WordPress.com konto for å koble til Jetpack.</string>
+ <string name="auth_required">Logg på igjen for å fortsette.</string>
+ <string name="editor_toast_invalid_path">Ugyldig filsti</string>
+ <string name="verification_code">Verifikasjonskode</string>
+ <string name="invalid_verification_code">Ugyldig verifikasjonskode</string>
+ <string name="verify">Verifiser</string>
+ <string name="error_publish_no_network">Kan ikke publisere uten tilkobling. Laget som utkast.</string>
+ <string name="language">Språk</string>
+ <string name="media_picker_title">Velg media</string>
+ <string name="take_photo">Ta et bilde</string>
+ <string name="device">Enhet</string>
+ <string name="reader_empty_posts_request_failed">Ute av stand til å hente innlegg</string>
+ <string name="publisher">Skrevet av:</string>
+ <string name="stats_followers_total_email_paged">Viser %1$d – %2$d av %3$s følgere på e-post</string>
+ <string name="stats_followers_total_wpcom_paged">Viser %1$d – %2$d av %3$s følgere på WordPress.com</string>
+ <string name="error_notification_open">Kunne ikke åpne varsling</string>
+ <string name="stats_search_terms_unknown_search_terms">Ukjente Søkeord</string>
+ <string name="posts_fetching">Henter innlegg …</string>
+ <string name="comments_fetching">Henter kommentarer …</string>
+ <string name="media_fetching">Henter media …</string>
+ <string name="stats_view_authors">Forfattere</string>
+ <string name="stats_view_search_terms">Søkeord</string>
+ <string name="pages_fetching">Henter sider …</string>
+ <string name="reader_label_new_posts">Nye innlegg</string>
+ <string name="stats_total">Totalt</string>
+ <string name="post_uploading">Laster opp</string>
+ <string name="stats_months_and_years">Måneder og år</string>
+ <string name="stats_view_all">Vis alle</string>
+ <string name="stats_view">Vis</string>
+ <string name="stats_followers_email_selector">E-post</string>
+ <string name="stats_pagination_label">Side %1$s av %2$s</string>
+ <string name="stats_timeframe_years">År</string>
+ <string name="stats_view_followers">Følgere</string>
+ <string name="stats_totals_publicize">Følgere</string>
+ <string name="stats_view_videos">Videoer</string>
+ <string name="stats_entry_clicks_link">Lenke</string>
+ <string name="stats_likes">Liker</string>
+ <string name="stats_view_countries">Land</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_followers_years">%1$d år</string>
+ <string name="stats_followers_seconds_ago">sekunder siden</string>
+ <string name="stats_followers_an_hour_ago">en time siden</string>
+ <string name="stats_followers_total_wpcom">Totalt antall WordPress.com-følgere: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_days">%1$d dager</string>
+ <string name="stats_visitors">Besøkende</string>
+ <string name="stats_totals_followers">Siden</string>
+ <string name="stats_for">Statistikk for %s</string>
+ <string name="stats_followers_a_minute_ago">ett minutt siden</string>
+ <string name="stats_followers_a_year">Ett år</string>
+ <string name="stats_followers_a_month">En måned</string>
+ <string name="stats_followers_minutes">%1$d minutter</string>
+ <string name="stats_followers_hours">%1$d timer</string>
+ <string name="stats_followers_a_day">En dag</string>
+ <string name="stats_followers_months">%1$d måneder</string>
+ <string name="stats_entry_top_commenter">Forfatter</string>
+ <string name="themes_fetching">Henter temaer …</string>
+ <string name="ssl_certificate_details">Detaljer</string>
+ <string name="delete_sure_page">Slett denne siden</string>
+ <string name="delete_sure">Slett denne kladden</string>
+ <string name="delete_sure_post">Slett dette innlegget</string>
+ <string name="confirm_delete_multi_media">Slette valgte elementer?</string>
+ <string name="confirm_delete_media">Slette valgt element?</string>
+ <string name="reader_label_comment_count_single">En kommentar</string>
+ <string name="comment">Kommentar</string>
+ <string name="comment_trashed">Kommentar slettet</string>
+ <string name="reader_title_photo_viewer">%1$d av %2$d</string>
+ <string name="reader_label_comment_count_multi">%,d kommentarer</string>
+ <string name="reader_empty_comments">Ingen kommentarer enda</string>
+ <string name="reader_label_comments_closed">Kommentarer er lukket</string>
+ <string name="reader_empty_posts_liked">Du har ikke likt noen innlegg</string>
+ <string name="reader_label_like">Liker</string>
+ <string name="mnu_comment_liked">Likt</string>
+ <string name="signing_out">Logger ut …</string>
+ <string name="older_month">Eldre enn en måned</string>
+ <string name="older_two_days">Eldre enn 2 dager</string>
+ <string name="older_last_week">Eldre enn en uke</string>
+ <string name="reader_toast_err_generic">Kan ikke gjøre dette</string>
+ <string name="reader_toast_err_block_blog">Kan ikke blokkere denne bloggen</string>
+ <string name="reader_toast_blog_blocked">Innlegg fra denne bloggen vil ikke bli vist lenger.</string>
+ <string name="reader_menu_block_blog">Blokker denne bloggen.</string>
+ <string name="contact_us">Kontakt oss</string>
+ <string name="hs__conversation_detail_error">Beskriv problemet du opplever</string>
+ <string name="hs__new_conversation_header">Support-chat</string>
+ <string name="hs__conversation_header">Support-chat</string>
+ <string name="hs__username_blank_error">Skriv inn et gyldig brukernavn</string>
+ <string name="hs__invalid_email_error">Skriv inn en gyldig epost-adresse</string>
+ <string name="add_location">Legg til bosted</string>
+ <string name="current_location">Nåværende bosted</string>
+ <string name="search_location">Søk</string>
+ <string name="edit_location">Rediger</string>
+ <string name="search_current_location">Finn</string>
+ <string name="preference_send_usage_stats">Send statistikk</string>
+ <string name="preference_send_usage_stats_summary">Send automatisk brukerdata til å forbedre WordPress for Android</string>
+ <string name="update_verb">Oppdater</string>
+ <string name="schedule_verb">Publiser</string>
+ <string name="reader_title_subs">Stikkord &amp; blogger</string>
+ <string name="media_empty_list">Ingen media</string>
+ <string name="saving">Lagrer …</string>
+ <string name="help">Hjelp</string>
+ <string name="forgot_password">Mistet passordet ditt?</string>
+ <string name="forums">Fora</string>
+ <string name="passcode_wrong_passcode">Feil PIN</string>
+ <string name="mnu_comment_unspam">Ikke useriøs</string>
+ <string name="stats_empty_comments">Ingen kommentarer enda</string>
+ <string name="comments_empty_list">Ingen kommentarer</string>
+ <string name="stats_bar_graph_empty">Ingen statistikk tilgjengelig</string>
+ <string name="out_of_memory">Enheten er tom for minne</string>
+ <string name="media_gallery_edit">Rediger galleri</string>
+ <string name="media_gallery_num_columns">Antall kolonner</string>
+ <string name="comment_status_unapproved">Ventende</string>
+ <string name="comment_status_approved">Godkjent</string>
+ <string name="delete_page">Slett side</string>
+ <string name="delete_post">Slett innlegg</string>
+ <string name="comment_status_spam">Useriøst</string>
+ <string name="share_action_post">Nytt innlegg</string>
+ <string name="new_post">Nytt innlegg</string>
+ <string name="reader_share_link">Del lenke</string>
+ <string name="mnu_comment_spam">Useriøs</string>
+ <string name="local_changes">Lokale endringer</string>
+ <string name="hint_comment_content">Kommentar</string>
+ <string name="local_draft">Lokalt utkast</string>
+ <string name="add_new_category">Legg til ny kategori</string>
+ <string name="create_a_link">Lag en lenke</string>
+ <string name="share_action_media">Mediebibliotek</string>
+ <string name="edit_comment">Rediger kommentar</string>
+ <string name="email_hint">E-postadresse</string>
+ <string name="dlg_approving_comments">Godkjenner</string>
+ <string name="theme_current_theme">Gjeldende tema</string>
+ <string name="edit_post">Rediger innlegg</string>
+ <string name="add_comment">Legg til kommentar</string>
+ <string name="dlg_spamming_comments">Marker som useriøs</string>
+ <string name="post_format">Innleggsformat</string>
+ <string name="dlg_confirm_trash_comments">Send til papirkurv?</string>
+ <string name="mnu_comment_approve">Godkjenn</string>
+ <string name="page_settings">Sideinnstillinger</string>
+ <string name="trash">Papirkurv</string>
+ <string name="view_site">Vis nettsted</string>
+ <string name="trash_no">Ikke slett</string>
+ <string name="trash_yes">Slett</string>
+ <string name="upload_failed">Opplastning feilet</string>
+ <string name="preview_post">Forhåndsvis innlegg</string>
+ <string name="image_settings">Bildeinnstillinger</string>
+ <string name="preview_page">Forhåndsvis side</string>
+ <string name="media_gallery_settings_title">Galleri-innstillinger</string>
+ <string name="category_name">Kategorinavn</string>
+ <string name="category_slug">Permalenke for kategori (valgfritt)</string>
+ <string name="wordpress_blog">WordPress-blogg</string>
+ <string name="reader_toast_err_add_tag">Kunne ikke legge til dette stikkordet</string>
+ <string name="reader_toast_err_remove_tag">Kunne ikke fjerne dette stikkordet</string>
+ <string name="required_field">Påkrevd felt</string>
+ <string name="email_cant_be_used_to_signup">Du kan ikke registrere deg med denne e-postadressen. Vi har problemer med at de blokkerer e-poster fra oss. Vennligst prøv en annen e-postleverandør.</string>
+ <string name="share_url_post">Del innlegg</string>
+ <string name="share_url_page">Del side</string>
+ <string name="share_link">Del lenke</string>
+ <string name="deleting_post">Sletter innlegg</string>
+ <string name="deleting_page">Sletter side</string>
+ <string name="creating_your_account">Oppretter kontoen din</string>
+ <string name="creating_your_site">Oppretter ditt nettsted</string>
+ <string name="reader_empty_posts_in_tag_updating">Henter innlegg …</string>
+ <string name="comment_spammed">Kommentar merket som useriøs</string>
+ <string name="reader_label_reply">Svar</string>
+ <string name="download">Laster ned media</string>
+ <string name="reader_likes_you_and_multi">Du og %,d andre liker dette</string>
+ <string name="reader_likes_multi">%,d personer liker dette</string>
+ <string name="reader_toast_err_get_comment">Kunne ikke hente denne kommentaren</string>
+ <string name="video">Video</string>
+ <string name="pick_photo">Velg bilde</string>
+ <string name="select_date">Velg dato</string>
+ <string name="reader_likes_you_and_one">Du og en annen har likt dette</string>
+ <string name="pick_video">Velg video</string>
+ <string name="nux_tap_continue">Fortsett</string>
+ <string name="nux_welcome_create_account">Opprett konto</string>
+ <string name="nux_oops_not_selfhosted_blog">Logg inn på WordPress.com</string>
+ <string name="signing_in">Logger inn …</string>
+ <string name="reader_btn_share">Del</string>
+ <string name="reader_btn_follow">Følg</string>
+ <string name="jetpack_not_found">Fant ikke utvidelsen Jetpack</string>
+ <string name="reader_untitled_post">(Uten tittel)</string>
+ <string name="nux_tutorial_get_started_title">Kom igang!</string>
+ <string name="username_invalid">Ugyldig brukernavn</string>
+ <string name="create_account_wpcom">Opprett en konto på WordPress.com</string>
+ <string name="media_add_new_media_gallery">Lag nytt galleri</string>
+ <string name="connecting_wpcom">Kobler til WordPress.com</string>
+ <string name="reader_btn_unfollow">Følger</string>
+ <string name="jetpack_message">Jetpack er påkrevd for statistikk. Vil du installere Jetpack?</string>
+ <string name="reader_likes_only_you">Du liker dette</string>
+ <string name="reader_empty_followed_tags">Du følger ingen stikkord</string>
+ <string name="reader_toast_err_url_intent">Klarte ikke å åpne %s</string>
+ <string name="reader_toast_err_view_image">Klarte ikke å vise bilde</string>
+ <string name="reader_likes_one">En person liker dette</string>
+ <string name="reader_share_subject">Delt fra %s</string>
+ <string name="limit_reached">Midlertidig begrenset. Du kan prøve igjen om 1 minutt. Dersom du prøver igjen før dette, vil begrensningsperioden bli utvidet. Dersom du mener dette er en feil, vennligst ta kontakt med brukerstøtte.</string>
+ <string name="themes">Temaer</string>
+ <string name="all">Alle</string>
+ <string name="images">Bilder</string>
+ <string name="custom_date">Egendefinert Dato</string>
+ <string name="media_add_popup_capture_photo">Ta bilde</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="media_gallery_image_order_random">Tilfeldig</string>
+ <string name="media_gallery_image_order_reverse">Omvendt</string>
+ <string name="media_edit_title_text">Tittel</string>
+ <string name="media_edit_description_text">Beskrivelse</string>
+ <string name="media_edit_success">Oppdatert</string>
+ <string name="media_edit_failure">Oppdatering mislyktes</string>
+ <string name="themes_details_label">Detaljer</string>
+ <string name="theme_activate_button">Aktiver</string>
+ <string name="theme_activating_button">Aktiverer</string>
+ <string name="share_action">Del</string>
+ <string name="stats">Statistikk</string>
+ <string name="stats_timeframe_days">Dager</string>
+ <string name="stats_timeframe_weeks">Uker</string>
+ <string name="stats_timeframe_months">Måneder</string>
+ <string name="stats_entry_country">Land</string>
+ <string name="stats_entry_posts_and_pages">Tittel</string>
+ <string name="media_edit_caption_text">Bildetekst</string>
+ <string name="media_edit_title_hint">Skriv en tittel her</string>
+ <string name="media_edit_caption_hint">Skriv en bildetekst her</string>
+ <string name="media_gallery_type_circles">Sirkler</string>
+ <string name="post_excerpt">Ingress</string>
+ <string name="share_action_title">Legg til ...</string>
+ <string name="theme_auth_error_title">Kunne ikke hente temaer</string>
+ <string name="theme_set_success">Temaet er lagret!</string>
+ <string name="stats_timeframe_yesterday">I går</string>
+ <string name="stats_entry_tags_and_categories">Emne</string>
+ <string name="stats_entry_authors">Forfatter</string>
+ <string name="stats_totals_views">Visninger</string>
+ <string name="stats_totals_clicks">Klikk</string>
+ <string name="stats_totals_plays">Avspillinger</string>
+ <string name="unattached">Uten vedlegg</string>
+ <string name="media_add_popup_capture_video">Ta opp video</string>
+ <string name="media_gallery_type_squares">Kvadratisk</string>
+ <string name="media_gallery_type_tiled">Flislagt</string>
+ <string name="media_gallery_type_slideshow">Lysbildevisning</string>
+ <string name="media_edit_description_hint">Skriv en beskrivelse her</string>
+ <string name="themes_features_label">Funksjoner</string>
+ <string name="stats_view_visitors_and_views">Besøk og visninger</string>
+ <string name="stats_view_clicks">Klikk</string>
+ <string name="stats_view_tags_and_categories">Stikkord og kategorier</string>
+ <string name="stats_view_referrers">Henvisninger</string>
+ <string name="stats_timeframe_today">Idag</string>
+ <string name="stats_entry_referrers">Henvisninger</string>
+ <string name="passcode_manage">Håndter PIN kode</string>
+ <string name="passcode_enter_passcode">Skriv inn PIN</string>
+ <string name="passcode_enter_old_passcode">Skriv inn gammel PIN</string>
+ <string name="passcode_re_enter_passcode">Skriv inn PIN på nytt</string>
+ <string name="passcode_change_passcode">Bytt PIN</string>
+ <string name="passcode_set">PIN endret</string>
+ <string name="passcode_preference_title">Lås PIN</string>
+ <string name="passcode_turn_off">Slå av PIN-lås</string>
+ <string name="passcode_turn_on">Slå PIN-lås på</string>
+ <string name="upload">Last opp</string>
+ <string name="discard">Forkast</string>
+ <string name="sign_in">Logg inn</string>
+ <string name="notifications">Varsler</string>
+ <string name="note_reply_successful">Kommentar publisert</string>
+ <string name="follows">Følg</string>
+ <string name="new_notifications">%d nye varsler</string>
+ <string name="more_notifications">og %d flere.</string>
+ <string name="loading">Laster...</string>
+ <string name="httpuser">HTTP brukernavn</string>
+ <string name="httppassword">HTTP passord</string>
+ <string name="error_media_upload">Det oppsto en feil under opplasting av media.</string>
+ <string name="post_content">Innhold (trykk for å legge til tekst eller media)</string>
+ <string name="publish_date">Publiser</string>
+ <string name="content_description_add_media">Legg til media</string>
+ <string name="incorrect_credentials">Feil brukernavn eller passord</string>
+ <string name="password">Passord</string>
+ <string name="username">Brukernavn</string>
+ <string name="reader">Nettleser</string>
+ <string name="featured">Burk som framhevet bilde</string>
+ <string name="featured_in_post">Legg til bilde i innlegget</string>
+ <string name="no_network_title">Ingen nettverk tilgjengelige</string>
+ <string name="post">Innlegg</string>
+ <string name="pages">Sider</string>
+ <string name="caption">Bildetekst (valgfritt)</string>
+ <string name="width">Bredde</string>
+ <string name="posts">Innlegg</string>
+ <string name="anonymous">Anonym</string>
+ <string name="page">Side</string>
+ <string name="blogusername">brukernavn</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Last og og lenk til det redigerte bildet</string>
+ <string name="scaled_image">Bildebredde</string>
+ <string name="scheduled">Planlagt</string>
+ <string name="link_enter_url">URL</string>
+ <string name="version">Versjon</string>
+ <string name="tos">Lisens</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="image_alignment">Justering</string>
+ <string name="refresh">Oppdater</string>
+ <string name="untitled">Uten tittel</string>
+ <string name="edit">Redigér</string>
+ <string name="post_id">Innlegg</string>
+ <string name="page_id">Side</string>
+ <string name="post_password">Passord (valgfritt)</string>
+ <string name="immediately">Umiddelbart</string>
+ <string name="quickpress_add_alert_title">Lag et navn for snarvei</string>
+ <string name="today">I dag</string>
+ <string name="settings">Alternativer</string>
+ <string name="share_url">Del URL</string>
+ <string name="quickpress_window_title">Velg snarvei for Hurtigpublisering</string>
+ <string name="quickpress_add_error">Snarveien kan ikke være tom</string>
+ <string name="publish_post">Publiser</string>
+ <string name="draft">Kladd</string>
+ <string name="post_private">Privat</string>
+ <string name="upload_full_size_image">Last opp og lenk til full bildestørrelse</string>
+ <string name="title">Tittel</string>
+ <string name="tags_separate_with_commas">Stikkord (separer flere stikkord med komma)</string>
+ <string name="categories">Kategorier</string>
+ <string name="notification_vibrate">Vibrer</string>
+ <string name="notification_blink">Blinkende varsler</string>
+ <string name="status">Status</string>
+ <string name="sdcard_title">Krever SD kort</string>
+ <string name="select_video">Velg video fra galleriet</string>
+ <string name="location">Sted</string>
+ <string name="media">Media</string>
+ <string name="delete">Slett</string>
+ <string name="none">Ingen</string>
+ <string name="blogs">Blogger</string>
+ <string name="select_photo">Velg et foto fra galleriet</string>
+ <string name="yes">Ja</string>
+ <string name="reply">Svar</string>
+ <string name="on">på</string>
+ <string name="preview">Forhåndsvisning</string>
+ <string name="cancel">Avbryt</string>
+ <string name="save">Lagre</string>
+ <string name="add">Legg til</string>
+ <string name="notification_settings">Innstillinger for varslinger</string>
+ <string name="no">Nei</string>
+ <string name="error">Feil</string>
+ <string name="category_refresh_error">Feil ved innlasting av kategorier på nytt</string>
+</resources>
diff --git a/WordPress/src/main/res/values-nl/strings.xml b/WordPress/src/main/res/values-nl/strings.xml
new file mode 100644
index 000000000..b5270459f
--- /dev/null
+++ b/WordPress/src/main/res/values-nl/strings.xml
@@ -0,0 +1,1119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_viewer">Lezer</string>
+ <string name="site_settings_list_editor_action_mode_title">Geselecteerde %1$d</string>
+ <string name="error_fetch_users_list">Site-gebruikers ophalen mislukt</string>
+ <string name="plans_manage">Beheer je abonnement op\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Je hebt nog geen volgers.</string>
+ <string name="people_empty_list_filtered_email_followers">Je hebt nog geen e-mailvolgers.</string>
+ <string name="people_empty_list_filtered_followers">Je hebt nog geen volgers.</string>
+ <string name="people_empty_list_filtered_users">Je hebt nog geen gebruikers.</string>
+ <string name="people_dropdown_item_viewers">Lezers</string>
+ <string name="invite_message_usernames_limit">Nodig tot wel 10 e-mailadressen en/of WordPress.com-gebruikersnamen uit. Naar degenen die een gebruikersnaam nodig hebben, worden instructies gestuurd over hoe ze er een aanmaken.</string>
+ <string name="viewer_remove_confirmation_message">Als je deze lezer verwijdert, kan hij of zij deze site niet bezoeken.\n\nWil je deze lezer toch verwijderen?</string>
+ <string name="follower_remove_confirmation_message">Als deze volger wordt verwijderd, kan hij of zij geen meldingen meer ontvangen over deze site tot hij of zij opnieuw gaat volgen.\n\nWil je deze volger toch verwijderen?</string>
+ <string name="error_remove_follower">Volger verwijderen mislukt</string>
+ <string name="error_remove_viewer">Lezer verwijderen mislukt</string>
+ <string name="error_fetch_email_followers_list">E-mailvolgers site ophalen mislukt</string>
+ <string name="error_fetch_followers_list">Sitevolgers ophalen mislukt</string>
+ <string name="editor_failed_uploads_switch_html">Enkele media konden niet worden geüpload. Je kunt niet overschakelen naar HTML.\n in deze staat. Alle mislukte uploads verwijderen en doorgaan?</string>
+ <string name="format_bar_description_html">HTML</string>
+ <string name="visual_editor">Visual Editor</string>
+ <string name="image_thumbnail">Miniatuurweergave afbeelding</string>
+ <string name="format_bar_description_ul">Ongeordende lijst</string>
+ <string name="format_bar_description_ol">Geordende lijst</string>
+ <string name="format_bar_description_more">Meer invoeren</string>
+ <string name="format_bar_description_media">Media invoeren</string>
+ <string name="format_bar_description_strike">Doorstrepen</string>
+ <string name="format_bar_description_quote">Citaat blokkeren</string>
+ <string name="format_bar_description_link">Link invoeren</string>
+ <string name="format_bar_description_italic">Cursief</string>
+ <string name="format_bar_description_underline">Onderstrepen</string>
+ <string name="image_settings_save_toast">Wijzigingen opgeslagen</string>
+ <string name="image_caption">Bijschrift</string>
+ <string name="image_alt_text">Alt. tekst</string>
+ <string name="image_link_to">Link naar</string>
+ <string name="image_width">Breedte</string>
+ <string name="format_bar_description_bold">Vetgedrukt</string>
+ <string name="image_settings_dismiss_dialog_title">Niet-opgeslagen wijzigingen verwijderen?</string>
+ <string name="stop_upload_dialog_title">Stoppen met uploaden?</string>
+ <string name="stop_upload_button">Stoppen met upload</string>
+ <string name="alert_error_adding_media">Er is een fout opgetreden bij het invoeren van media</string>
+ <string name="alert_action_while_uploading">Je bent media aan het uploaden. Wacht totdat dit is afgerond.</string>
+ <string name="alert_insert_image_html_mode">Kan media niet direct invoeren in HTML. Schakel terug naar de visuele modus.</string>
+ <string name="uploading_gallery_placeholder">Galerij wordt geüpload …</string>
+ <string name="invite_sent">Uitnodiging verzonden</string>
+ <string name="tap_to_try_again">Tik om het opnieuw te proberen!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_message_info">(Optioneel) Je kunt een aangepast bericht invoeren van maximaal 500 tekens, dat wordt toegevoegd aan de uitnodiging van de gebruiker(s).</string>
+ <string name="invite_message_remaining_other">%d tekens resterend</string>
+ <string name="invite_message_remaining_one">1 teken resterend</string>
+ <string name="invite_message_remaining_zero">0 tekens resterend</string>
+ <string name="invite_invalid_email">Het e-mailadres \'%s\' is ongeldig</string>
+ <string name="invite_message_title">Aangepast bericht</string>
+ <string name="invite_already_a_member">Er is al een lid met de gebruikersnaam \'%s\'</string>
+ <string name="invite_username_not_found">Geen gebruiker gevonden met gebruikersnaam \'%s\'</string>
+ <string name="invite">Uitnodigen</string>
+ <string name="invite_names_title">Gebruikersnamen of e-mails</string>
+ <string name="signup_succeed_signin_failed">Je account is aangemaakt maar er is een fout opgetreden bij het\n aanmelden. Probeer je aan te melden met je nieuwe gebruikersnaam en wachtwoord.</string>
+ <string name="send_link">Stuur link</string>
+ <string name="my_site_header_external">Extern</string>
+ <string name="invite_people">Mensen uitnodigen</string>
+ <string name="label_clear_search_history">Zoekgeschiedenis wissen</string>
+ <string name="dlg_confirm_clear_search_history">Zoekgeschiedenis wissen?</string>
+ <string name="reader_empty_posts_in_search_description">Geen berichten gevonden voor %s in je taal.</string>
+ <string name="reader_label_post_search_running">Zoeken ...</string>
+ <string name="reader_label_related_posts">Gerelateerd leesmateriaal</string>
+ <string name="reader_empty_posts_in_search_title">Geen berichten gevonden.</string>
+ <string name="reader_label_post_search_explainer">Zoek alle openbare WordPress.com-blogs</string>
+ <string name="reader_hint_post_search">Zoek op WordPress.com</string>
+ <string name="reader_title_related_post_detail">Gerelateerd bericht</string>
+ <string name="reader_title_search_results">Zoeken naar %s</string>
+ <string name="preview_screen_links_disabled">Links uitgeschakeld op voorbeeldscherm</string>
+ <string name="draft_explainer">Dit bericht is een concept dat niet is gepubliceerd</string>
+ <string name="send">Verzenden</string>
+ <string name="person_remove_confirmation_title">Verwijder %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">De sites in deze lijst hebben recent niets geplaatst</string>
+ <string name="people">Mensen</string>
+ <string name="edit_user">Gebruiker bewerken</string>
+ <string name="role">Rol</string>
+ <string name="error_remove_user">Gebruiker verwijderen mislukt</string>
+ <string name="error_update_role">Gebruikersrol updaten mislukt</string>
+ <string name="gravatar_camera_and_media_permission_required">Machtigingen vereist om een foto te selecteren of te nemen</string>
+ <string name="error_updating_gravatar">Fout bij het updaten van je Gravatar</string>
+ <string name="error_locating_image">Fout bij het vinden van de bij te snijden afbeelding</string>
+ <string name="error_refreshing_gravatar">Fout bij het opnieuw laden van je Gravatar</string>
+ <string name="gravatar_tip">Nieuw! Tik op je Gravatar om deze te wijzigen!</string>
+ <string name="error_cropping_image">Fout bij het bijsnijden van de afbeelding</string>
+ <string name="launch_your_email_app">Start je e-mailapp op</string>
+ <string name="checking_email">E-mail wordt gecontroleerd</string>
+ <string name="not_on_wordpress_com">Niet op WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Momenteel niet beschikbaar. Voer je wachtwoord in</string>
+ <string name="check_your_email">Controleer je e-mail</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Ontvang een link die wordt verzonden naar je e-mail om je meteen aan te melden.</string>
+ <string name="logging_in">Inloggen</string>
+ <string name="enter_your_password_instead">Voer je wachtwoord in</string>
+ <string name="web_address_dialog_hint">Wordt openbaar getoond als je reageert.</string>
+ <string name="jetpack_not_connected_message">De Jetpack-plugin is geïnstalleerd, maar niet verbonden met WordPress.com. Wil je Jetpack verbinden?</string>
+ <string name="username_email">E-mail of gebruikersnaam</string>
+ <string name="jetpack_not_connected">Jetpack-plugin niet verbonden</string>
+ <string name="new_editor_reflection_error">Visual Editor is niet compatibel met uw apparaat. Het is\n automatisch uitgeschakeld.</string>
+ <string name="stats_insights_latest_post_no_title">(geen titel)</string>
+ <string name="capture_or_pick_photo">Neem of selecteer foto</string>
+ <string name="plans_post_purchase_text_themes">Je hebt nu onbeperkt toegang tot premium thema\'s. Bekijk een voorbeeld van ieder gewenst thema op je site om te beginnen.</string>
+ <string name="plans_post_purchase_button_themes">Door thema\'s bladeren</string>
+ <string name="plans_post_purchase_title_themes">Vind een perfect premium thema</string>
+ <string name="plans_post_purchase_button_video">Stel nieuw bericht op</string>
+ <string name="plans_post_purchase_text_video">Met VideoPress en de uitbreiding van je media-opslag kun je video\'s uploaden en hosten op je site.</string>
+ <string name="plans_post_purchase_title_video">Breng berichten tot leven met videobeelden</string>
+ <string name="plans_post_purchase_button_customize">Mijn site aanpassen</string>
+ <string name="plans_post_purchase_text_customize">Je hebt nu toegang tot bewerkingsfuncties voor aangepaste lettertypes, kleuren en CSS.</string>
+ <string name="plans_post_purchase_text_intro">Je site maakt salto\'s uit vreugde! Ontdek nu de nieuwe functies van je site en kies waar je wilt beginnen.</string>
+ <string name="plans_post_purchase_title_customize">Lettertypes en kleuren aanpassen</string>
+ <string name="plans_post_purchase_title_intro">Hij is nu van jou, heel veel plezier!</string>
+ <string name="plan">Abonnement</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Kan abonnementen niet laden.</string>
+ <string name="export_your_content_message">Je berichten, pagina\'s en instellingen worden naar je e-mailadres, %s, verzonden.</string>
+ <string name="export_your_content">Exporteer je content</string>
+ <string name="export_email_sent">Exporteer verzonden e-mail!</string>
+ <string name="exporting_content_progress">Content exporteren...</string>
+ <string name="premium_upgrades_message">Je hebt actieve Premium-upgrades op je site. Annuleer je upgrades voordat je je site verwijdert.</string>
+ <string name="checking_purchases">Aankopen worden gecontroleerd</string>
+ <string name="show_purchases">Geef aankopen weer</string>
+ <string name="premium_upgrades_title">Premium-upgrades</string>
+ <string name="purchases_request_error">Er is iets fout gegaan. Kon aankopen niet aanvragen.</string>
+ <string name="delete_site_progress">Site verwijderen...</string>
+ <string name="delete_site_summary">Deze actie kan niet ongedaan worden gemaakt. Door de site te verwijderen worden alle content, bijdragers en domeinen van de site verwijderd.</string>
+ <string name="delete_site_hint">Site verwijderen</string>
+ <string name="export_site_hint">Exporteer je site naar een XML-bestand</string>
+ <string name="are_you_sure">Weet je het zeker?</string>
+ <string name="export_site_summary">Als je het zeker weet, neem dan nu de tijd om je content te exporteren. Deze kan later niet meer worden hersteld.</string>
+ <string name="keep_your_content">Je content behouden</string>
+ <string name="domain_removal_hint">Domeinen die niet werken wanneer je je site verwijdert</string>
+ <string name="domain_removal_summary">Voorzichtig! Door je site te verwijderen, worden ook je onderstaande domeinen verwijderd.</string>
+ <string name="primary_domain">Primair domein</string>
+ <string name="domain_removal">Verwijdering domein</string>
+ <string name="error_deleting_site_summary">Er is een fout opgetreden bij het verwijderen van je site. Neem contact op met ondersteuning voor meer hulp</string>
+ <string name="error_deleting_site">Fout bij verwijderen site</string>
+ <string name="confirm_delete_site_prompt">Voer %1$s in het onderstaande veld in om dit te bevestigen. Je site wordt dan voorgoed verwijderd.</string>
+ <string name="site_settings_export_content_title">Content exporteren</string>
+ <string name="confirm_delete_site">Site verwijderen bevestigen</string>
+ <string name="contact_support">Neem contact op met ondersteuning</string>
+ <string name="start_over_text">Als je een site wilt, maar niet de berichten en pagina\'s die je nu hebt, kan ons ondersteuningsteam je berichten, pagina\'s, media en reacties voor je verwijderen.\n\nHiermee blijft je site en URL actief maar kan je van vooraf aan beginnen met het maken van content. Neem contact met ons op als je je huidige content wilt verwijderen.</string>
+ <string name="site_settings_start_over_hint">Begin opnieuw met je site</string>
+ <string name="let_us_help">Laat ons je helpen</string>
+ <string name="start_over">Begin opnieuw</string>
+ <string name="me_btn_app_settings">App-instellingen</string>
+ <string name="editor_remove_failed_uploads">Mislukte uploads verwijderen</string>
+ <string name="editor_toast_failed_uploads">Enkele media konden niet worden geüpload. Het opslaan of publiceren\n van je bericht is niet mogelijk in deze staat. Wil je alle mislukte uploads verwijderen?</string>
+ <string name="site_settings_advanced_header">Geavanceerd</string>
+ <string name="comments_empty_list_filtered_trashed">Geen verwijderde reacties</string>
+ <string name="comments_empty_list_filtered_pending">Geen reacties in behandeling</string>
+ <string name="comments_empty_list_filtered_approved">Geen goedgekeurde reacties</string>
+ <string name="button_done">Klaar</string>
+ <string name="button_skip">Overslaan</string>
+ <string name="site_timeout_error">Kon geen verbinding maken met de WordPress-site wegens een time-out.</string>
+ <string name="xmlrpc_malformed_response_error">Kon geen verbinding maken. De WordPress-installatie heeft gereageerd met een ongeldig XML-RPC-document.</string>
+ <string name="xmlrpc_missing_method_error">Kon geen verbinding maken. Vereiste XML-RPC-methoden ontbreken op de server.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string name="theme_free">Gratis</string>
+ <string name="theme_all">Alle</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Galerij</string>
+ <string name="post_format_image">Afbeelding</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Citeren</string>
+ <string name="post_format_standard">Standaard</string>
+ <string name="notif_events">Informatie over cursussen en evenementen van WordPress.com (online en op locatie).</string>
+ <string name="post_format_aside">Aside</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Mogelijkheden om deel te nemen aan onderzoeken en enquêtes van WordPress.com.</string>
+ <string name="notif_tips">Tips om WordPress.com optimaal te benutten.</string>
+ <string name="notif_community">Community</string>
+ <string name="replies_to_my_comments">Antwoorden op mijn reacties</string>
+ <string name="notif_suggestions">Suggesties</string>
+ <string name="notif_research">Onderzoek</string>
+ <string name="site_achievements">Prestaties voor de site</string>
+ <string name="username_mentions">Vermeldingen van gebruikersnaam</string>
+ <string name="likes_on_my_posts">Likes voor mijn berichten</string>
+ <string name="site_follows">Volgers van site</string>
+ <string name="likes_on_my_comments">Likes voor mijn reacties</string>
+ <string name="comments_on_my_site">Reacties op mijn site</string>
+ <string name="site_settings_list_editor_summary_other">%d items</string>
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="approve_auto_if_previously_approved">Reacties van bekende gebruikers</string>
+ <string name="approve_auto">Alle gebruikers</string>
+ <string name="approve_manual">Geen reacties</string>
+ <string name="site_settings_paging_summary_other">%d reacties per pagina</string>
+ <string name="site_settings_paging_summary_one">1 reactie per pagina</string>
+ <string name="site_settings_multiple_links_summary_other">Vereis goedkeuring voor meer dan %d links</string>
+ <string name="site_settings_multiple_links_summary_one">Vereis goedkeuring voor meer dan 1 link</string>
+ <string name="site_settings_multiple_links_summary_zero">Vereis goedkeuring voor meer dan 0 links</string>
+ <string name="detail_approve_auto">Keur de reacties van iedereen automatisch goed.</string>
+ <string name="detail_approve_auto_if_previously_approved">Keur de reactie automatisch goed indien de gebruiker eerder een goedgekeurde reactie heeft geplaatst</string>
+ <string name="detail_approve_manual">Vereist handmatige goedkeuring voor de reacties van iedereen.</string>
+ <string name="filter_trashed_posts">Verwijderd</string>
+ <string name="days_quantity_one">1 dag</string>
+ <string name="days_quantity_other">%d dagen</string>
+ <string name="filter_published_posts">Gepubliceerd</string>
+ <string name="filter_draft_posts">Concepten</string>
+ <string name="filter_scheduled_posts">Gepland</string>
+ <string name="pending_email_change_snackbar">Klik op de link in de verificatiemail die verzonden is naar %1$s om je nieuwe adres te bevestigen</string>
+ <string name="primary_site">Primaire site</string>
+ <string name="web_address">Webadres</string>
+ <string name="editor_toast_uploading_please_wait">Je bent media aan het uploaden. Wacht totdat dit is afgerond.</string>
+ <string name="error_refresh_comments_showing_older">Reacties konden momenteel niet worden vernieuwd - oudere reacties worden weergegeven</string>
+ <string name="editor_post_settings_set_featured_image">Stel aanbevolen afbeelding in</string>
+ <string name="editor_post_settings_featured_image">Aanbevolen afbeelding</string>
+ <string name="new_editor_promo_desc">De WordPress-app voor Android bevat een prachtige nieuwe visual\n editor. Probeer hem uit door een nieuw bericht te maken.</string>
+ <string name="new_editor_promo_title">Volledig nieuwe editor</string>
+ <string name="new_editor_promo_button_label">Geweldig, bedankt!</string>
+ <string name="visual_editor_enabled">Visual Editor ingeschakeld</string>
+ <string name="editor_content_placeholder">Deel hier je verhaal...</string>
+ <string name="editor_page_title_placeholder">Paginatitel</string>
+ <string name="editor_post_title_placeholder">Berichttitel</string>
+ <string name="email_address">E-mailadres</string>
+ <string name="preference_show_visual_editor">Visual Editor weergeven</string>
+ <string name="dlg_sure_to_delete_comments">Deze reacties permanent verwijderen?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Deze reactie permanent verwijderen?</string>
+ <string name="mnu_comment_delete_permanently">Verwijderen</string>
+ <string name="comment_deleted_permanently">Reactie verwijderd</string>
+ <string name="mnu_comment_untrash">Herstellen</string>
+ <string name="comments_empty_list_filtered_spam">Geen spamreacties</string>
+ <string name="could_not_load_page">Kan pagina niet laden</string>
+ <string name="comment_status_all">Alle</string>
+ <string name="interface_language">Interface-taal</string>
+ <string name="off">Uit</string>
+ <string name="about_the_app">Over de app</string>
+ <string name="error_post_account_settings">Opslaan van je accountinstellingen mislukt</string>
+ <string name="error_post_my_profile">Opslaan van je profiel mislukt</string>
+ <string name="error_fetch_account_settings">Ophalen van je accountinstellingen mislukt</string>
+ <string name="error_fetch_my_profile">Ophalen van je profiel mislukt</string>
+ <string name="stats_widget_promo_ok_btn_label">OK, duidelijk</string>
+ <string name="stats_widget_promo_desc">Voeg de widget toe aan je startscherm om in één klik toegang te krijgen tot je Statistieken.</string>
+ <string name="stats_widget_promo_title">Startscherm Statistieken Widget</string>
+ <string name="site_settings_unknown_language_code_error">Taalcode niet herkend</string>
+ <string name="site_settings_threading_dialog_description">Geneste reacties in threads toestaan.</string>
+ <string name="site_settings_threading_dialog_header">Thread omhoog naar</string>
+ <string name="remove">Verwijderen</string>
+ <string name="search">Zoeken</string>
+ <string name="add_category">Categorie toevoegen</string>
+ <string name="disabled">Uitgeschakeld</string>
+ <string name="site_settings_image_original_size">Oorspronkelijke grootte</string>
+ <string name="privacy_private">Je site is alleen zichtbaar voor jou en gebruikers die je goedkeurt</string>
+ <string name="privacy_public_not_indexed">Je site kan door iedereen worden bekeken, maar wordt niet geïndexeerd door zoekmachines</string>
+ <string name="privacy_public">Je site kan door iedereen worden bekeken en kan worden geïndexeerd door zoekmachines</string>
+ <string name="about_me_hint">Enkele woorden over jou…</string>
+ <string name="public_display_name_hint">De weergavenaam is standaard je gebruikersnaam, tenzij hij anders is ingesteld</string>
+ <string name="about_me">Over mij</string>
+ <string name="public_display_name">Openbare weergavenaam</string>
+ <string name="my_profile">Mijn Profiel</string>
+ <string name="first_name">Voornaam</string>
+ <string name="last_name">Achternaam</string>
+ <string name="site_privacy_public_desc">Laat zoekmachines deze site indexeren</string>
+ <string name="site_privacy_hidden_desc">Laat zoekmachines deze site niet indexeren</string>
+ <string name="site_privacy_private_desc">Ik wil dat mijn site privé blijft en dat ik zelf kies voor wie hij zichtbaar is</string>
+ <string name="cd_related_post_preview_image">Voorbeeldafbeelding van gerelateerd bericht bekijken</string>
+ <string name="error_post_remote_site_settings">Kan site-informatie niet opslaan</string>
+ <string name="error_fetch_remote_site_settings">Kan site-informatie niet ophalen</string>
+ <string name="error_media_upload_connection">Er is een verbindingsfout opgetreden bij het uploaden van media</string>
+ <string name="site_settings_disconnected_toast">Niet verbonden, bijwerken uitgeschakeld.</string>
+ <string name="site_settings_unsupported_version_error">Niet-ondersteunde versie van WordPress</string>
+ <string name="site_settings_multiple_links_dialog_description">Vereist goedkeuring voor reacties die meer dan dit aantal koppelingen bevatten.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatisch afsluiten</string>
+ <string name="site_settings_close_after_dialog_description">Sluit reacties op artikelen automatisch af.</string>
+ <string name="site_settings_paging_dialog_description">Deel reacties in threads op in meerdere pagina\'s.</string>
+ <string name="site_settings_paging_dialog_header">Reacties per pagina</string>
+ <string name="site_settings_close_after_dialog_title">Reacties afsluiten</string>
+ <string name="site_settings_blacklist_description">Wanneer een reactie een van deze woorden bevat in de inhoud, naam, URL, e-mailadres of IP-adres, wordt het gemarkeerd als spam. Ook delen van woorden worden gevonden, dus "press" levert ook "WordPress" op als resultaat.</string>
+ <string name="site_settings_hold_for_moderation_description">Wanneer een reactie een van deze woorden bevat in de inhoud, naam, URL, het e-mailadres of IP-adres, wordt hij in de wachtrij voor moderatie gezet. Ook delen van woorden worden gevonden, dus "press" levert ook "WordPress" op als resultaat.</string>
+ <string name="site_settings_list_editor_input_hint">Voer een woord of zinsdeel in</string>
+ <string name="site_settings_list_editor_no_items_text">Geen items</string>
+ <string name="site_settings_learn_more_caption">Je kunt deze instellingen ongedaan maken voor individuele berichten.</string>
+ <string name="site_settings_rp_preview3_site">in "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Upgradefocus: VideoPress voor bruiloften</string>
+ <string name="site_settings_rp_preview2_site">in "Apps"</string>
+ <string name="site_settings_rp_preview2_title">De WordPress voor Android-app krijgt een grote update</string>
+ <string name="site_settings_rp_preview1_site">in "Mobiel"</string>
+ <string name="site_settings_rp_preview1_title">Grote update voor iPhone/iPad nu beschikbaar</string>
+ <string name="site_settings_rp_show_images_title">Afbeeldingen weergeven</string>
+ <string name="site_settings_rp_show_header_title">Koptekst weergeven</string>
+ <string name="site_settings_rp_switch_summary">Gerelateerde berichten geeft relevante content van je site weer onder je berichten.</string>
+ <string name="site_settings_rp_switch_title">Gerelateerde berichten weergeven</string>
+ <string name="site_settings_delete_site_hint">Verwijder je sitegegevens van de app</string>
+ <string name="site_settings_blacklist_hint">Reacties die overeenkomen met een filter worden als spam gemarkeerd</string>
+ <string name="site_settings_moderation_hold_hint">Reacties die overeenkomen met een filter worden in de wachtrij voor moderatie gezet</string>
+ <string name="site_settings_multiple_links_hint">Negeert koppelingslimiet van bekende gebruikers</string>
+ <string name="site_settings_whitelist_hint">De reactie-auteur moet eerder een goedgekeurde reactie hebben geplaatst</string>
+ <string name="site_settings_user_account_required_hint">Gebruikers moeten geregistreerd en ingelogd zijn om een reactie te plaatsen</string>
+ <string name="site_settings_identity_required_hint">De reactie-auteur moet zijn of haar naam en e-mailadres invullen</string>
+ <string name="site_settings_manual_approval_hint">Reacties moeten handmatig worden goedgekeurd</string>
+ <string name="site_settings_paging_hint">Geef reacties weer in delen van een specifieke grootte</string>
+ <string name="site_settings_threading_hint">Geneste reacties toestaan tot een bepaalde diepte</string>
+ <string name="site_settings_sort_by_hint">Bepaalt de volgorde waarin reacties worden weergegeven</string>
+ <string name="site_settings_close_after_hint">Reacties niet meer toestaan na bepaalde tijd</string>
+ <string name="site_settings_receive_pingbacks_hint">Koppelingsmeldingen van andere blogs toestaan</string>
+ <string name="site_settings_send_pingbacks_hint">Probeer een melding te maken op alle blogs waaraan het artikel is gekoppeld</string>
+ <string name="site_settings_allow_comments_hint">Toestaan dat lezers reacties plaatsen</string>
+ <string name="site_settings_discussion_hint">Geef discussie-instellingen van je site weer en wijzig ze</string>
+ <string name="site_settings_more_hint">Geef alle beschikbare discussie-instellingen weer</string>
+ <string name="site_settings_related_posts_hint">Toon of verberg alle gerelateerde berichten in de reader</string>
+ <string name="site_settings_upload_and_link_image_hint">Inschakelen dat de afbeelding altijd op volledige grootte wordt geüpload</string>
+ <string name="site_settings_image_width_hint">De afmeting van afbeeldingen in berichten wijzigen naar deze breedte</string>
+ <string name="site_settings_format_hint">Een nieuwe post format instellen</string>
+ <string name="site_settings_category_hint">Een nieuwe berichtcategorie instellen</string>
+ <string name="site_settings_location_hint">Automatisch locatiegegevens aan je berichten toevoegen</string>
+ <string name="site_settings_password_hint">Wijzig je wachtwoord</string>
+ <string name="site_settings_username_hint">Huidig gebruikersaccount</string>
+ <string name="site_settings_language_hint">Taal waarin deze blog voornamelijk is geschreven</string>
+ <string name="site_settings_privacy_hint">Beheert wie je site kan zien</string>
+ <string name="site_settings_address_hint">Het wijzigen van je adres wordt momenteel niet ondersteund</string>
+ <string name="site_settings_tagline_hint">Een korte beschrijving of een pakkende zin om je blog te omschrijven</string>
+ <string name="site_settings_title_hint">Leg in een paar woorden uit waar deze site over gaat</string>
+ <string name="site_settings_whitelist_known_summary">Reacties van bekende gebruikers</string>
+ <string name="site_settings_whitelist_all_summary">Reacties van alle gebruikers</string>
+ <string name="site_settings_threading_summary">%d niveaus</string>
+ <string name="site_settings_privacy_private_summary">Privé</string>
+ <string name="site_settings_privacy_hidden_summary">Verborgen</string>
+ <string name="site_settings_delete_site_title">Site verwijderen</string>
+ <string name="site_settings_privacy_public_summary">Openbaar</string>
+ <string name="site_settings_blacklist_title">Blacklist</string>
+ <string name="site_settings_moderation_hold_title">In de wacht zetten voor moderatie</string>
+ <string name="site_settings_multiple_links_title">Koppelingen in reacties</string>
+ <string name="site_settings_whitelist_title">Automatisch goedkeuren</string>
+ <string name="site_settings_threading_title">Weergave threads</string>
+ <string name="site_settings_paging_title">Weergave pagina\'s</string>
+ <string name="site_settings_sort_by_title">Sorteren op</string>
+ <string name="site_settings_account_required_title">Gebruikers moeten aangemeld zijn</string>
+ <string name="site_settings_identity_required_title">Moet naam en e-mail bevatten</string>
+ <string name="site_settings_receive_pingbacks_title">Pingbacks ontvangen</string>
+ <string name="site_settings_send_pingbacks_title">Pingbacks verzenden</string>
+ <string name="site_settings_allow_comments_title">Reacties toestaan</string>
+ <string name="site_settings_default_format_title">Standaard indeling</string>
+ <string name="site_settings_default_category_title">Standaard categorie</string>
+ <string name="site_settings_location_title">Locatie inschakelen</string>
+ <string name="site_settings_address_title">Adres</string>
+ <string name="site_settings_title_title">Sitetitel</string>
+ <string name="site_settings_tagline_title">Slogan</string>
+ <string name="site_settings_this_device_header">Dit apparaat</string>
+ <string name="site_settings_discussion_new_posts_header">Standaardinstellingen voor nieuwe berichten</string>
+ <string name="site_settings_account_header">Account</string>
+ <string name="site_settings_writing_header">Schrijven</string>
+ <string name="newest_first">Nieuwste eerst</string>
+ <string name="site_settings_general_header">Algemeen</string>
+ <string name="discussion">Reacties</string>
+ <string name="privacy">Privacy</string>
+ <string name="related_posts">Gerelateerde berichten</string>
+ <string name="comments">Commentaar</string>
+ <string name="close_after">Hierna sluiten</string>
+ <string name="oldest_first">Oudste eerst</string>
+ <string name="media_error_no_permission_upload">Je hebt geen toestemming om media naar de site te uploaden</string>
+ <string name="never">Nooit</string>
+ <string name="unknown">Onbekend</string>
+ <string name="reader_err_get_post_not_found">Dit bericht bestaat niet meer</string>
+ <string name="reader_err_get_post_not_authorized">Je hebt geen rechten om dit bericht te lezen</string>
+ <string name="reader_err_get_post_generic">Kan dit bericht niet ophalen</string>
+ <string name="blog_name_no_spaced_allowed">Het adres van de site mag geen spaties bevatten</string>
+ <string name="invalid_username_no_spaces">De gebruikersnaam mag geen spaties bevatten</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">De sites die je volgt hebben recent niets geplaatst</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Geen recente berichten</string>
+ <string name="media_details_copy_url_toast">URL gekopieerd naar klembord</string>
+ <string name="edit_media">Media bewerken</string>
+ <string name="media_details_copy_url">URL kopiëren</string>
+ <string name="media_details_label_date_uploaded">Geüpload</string>
+ <string name="media_details_label_date_added">Toegevoegd</string>
+ <string name="selected_theme">Geselecteerd thema</string>
+ <string name="could_not_load_theme">Kan thema niet laden</string>
+ <string name="theme_activation_error">Er is iets fout gegaan. Kan thema niet activeren</string>
+ <string name="theme_by_author_prompt_append"> door %1$s</string>
+ <string name="theme_prompt">Bedankt dat je hebt gekozen voor %1$s</string>
+ <string name="theme_try_and_customize">Uitproberen en aanpassen</string>
+ <string name="theme_view">Weergeven</string>
+ <string name="theme_details">Details</string>
+ <string name="theme_support">Ondersteuning</string>
+ <string name="theme_done">GEREED</string>
+ <string name="theme_manage_site">SITE BEHEREN</string>
+ <string name="title_activity_theme_support">Thema\'s</string>
+ <string name="theme_activate">Activeren</string>
+ <string name="date_range_start_date">Startdatum</string>
+ <string name="date_range_end_date">Einddatum</string>
+ <string name="current_theme">Huidig thema</string>
+ <string name="customize">Aanpassen</string>
+ <string name="details">Details</string>
+ <string name="support">Ondersteuning</string>
+ <string name="active">Actief</string>
+ <string name="stats_referrers_spam_generic_error">Er is iets fout gegaan tijdens deze handeling. De status van de spam is ongewijzigd gebleven.</string>
+ <string name="stats_referrers_marking_not_spam">Markeren als geen spam</string>
+ <string name="stats_referrers_unspam">Geen spam</string>
+ <string name="stats_referrers_marking_spam">Markeren als spam</string>
+ <string name="theme_auth_error_authenticate">Fout bij het ophalen van thema\'s: authenticatie van gebruiker mislukt</string>
+ <string name="post_published">Bericht gepubliceerd</string>
+ <string name="page_published">Pagina gepubliceerd</string>
+ <string name="post_updated">Bericht bijgewerkt</string>
+ <string name="page_updated">Pagina bijgewerkt</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Helaas, geen thema\'s gevonden.</string>
+ <string name="media_file_name">Bestandsnaam: %s</string>
+ <string name="media_uploaded_on">Geüpload op: %s</string>
+ <string name="media_dimensions">Afmetingen: %s</string>
+ <string name="upload_queued">In wachtrij geplaatst</string>
+ <string name="media_file_type">Bestandstype: %s</string>
+ <string name="reader_label_gap_marker">Meer berichten laden</string>
+ <string name="notifications_no_search_results">Geen sites overeengekomen met \'%s\'</string>
+ <string name="search_sites">Zoek sites</string>
+ <string name="notifications_empty_view_reader">Reader weergeven</string>
+ <string name="unread">Ongelezen</string>
+ <string name="notifications_empty_action_followers_likes">Trek de aandacht: reageer op berichten die je hebt gelezen.</string>
+ <string name="notifications_empty_action_comments">Deelnemen aan een gesprek: reageer op berichten uit blogs die je volgt.</string>
+ <string name="notifications_empty_action_unread">Breng het gesprek weer op gang: schrijf een nieuw bericht.</string>
+ <string name="notifications_empty_action_all">Ga aan de slag! Reageer op berichten uit blogs die je volgt.</string>
+ <string name="notifications_empty_likes">Er zijn nog geen nieuwe likes.</string>
+ <string name="notifications_empty_followers">Er zijn nog geen nieuwe volgers.</string>
+ <string name="notifications_empty_comments">Nog geen nieuwe reacties.</string>
+ <string name="notifications_empty_unread">Je bent helemaal bij!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Bekijk de statistieken in de app en probeer later de widget toe te voegen</string>
+ <string name="stats_widget_error_readd_widget">Verwijder de widget en voeg deze opnieuw in</string>
+ <string name="stats_widget_error_no_visible_blog">Je hebt geen toegang tot de statistieken zonder een zichtbare blog</string>
+ <string name="stats_widget_error_no_permissions">Je account van WordPress.com heeft geen toegang tot de statistieken van deze blog</string>
+ <string name="stats_widget_error_no_account">Login bij WordPress</string>
+ <string name="stats_widget_error_generic">Statistieken kunnen niet worden geladen</string>
+ <string name="stats_widget_loading_data">Gegevens laden…</string>
+ <string name="stats_widget_name_for_blog">Huidige statistieken voor %1$s</string>
+ <string name="stats_widget_name">Huidige statistieken van WordPress</string>
+ <string name="add_location_permission_required">Toestemming vereist om locatie toe te voegen</string>
+ <string name="add_media_permission_required">Toestemming vereist om media toe te voegen</string>
+ <string name="access_media_permission_required">Toestemming vereist om media toe te voegen</string>
+ <string name="stats_enable_rest_api_in_jetpack">Om je statistieken te bekijken moet je de JSON API module in Jetpack activeren.</string>
+ <string name="error_open_list_from_notification">Dit bericht of deze pagina is gepubliceerd op een andere site</string>
+ <string name="reader_short_comment_count_multi">%s Reacties</string>
+ <string name="reader_short_comment_count_one">1 Reactie</string>
+ <string name="reader_label_submit_comment">VERSTUUR</string>
+ <string name="reader_hint_comment_on_post">Reageer op bericht…</string>
+ <string name="reader_discover_visit_blog">%s bezoeken</string>
+ <string name="reader_discover_attribution_blog">Oorspronkelijk geplaatst op %s</string>
+ <string name="reader_discover_attribution_author">Oorspronkelijk geplaatst door %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Oorspronkelijk geplaatst door %1$s op %2$s</string>
+ <string name="reader_short_like_count_multi">%s likes</string>
+ <string name="reader_short_like_count_one">1 like</string>
+ <string name="reader_label_follow_count">%,d volgers</string>
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_menu_tags">Tags en blogs bijwerken</string>
+ <string name="reader_title_post_detail">Reader-bericht</string>
+ <string name="local_draft_explainer">Dit bericht is een lokaal concept dat niet is gepubliceerd</string>
+ <string name="local_changes_explainer">Dit bericht bevat lokale wijzigingen die niet zijn gepubliceerd</string>
+ <string name="notifications_push_summary">Instellingen voor meldingen die op je apparaat verschijnen.</string>
+ <string name="notifications_email_summary">Instellingen voor meldingen die worden verstuurd naar het e-mailadres dat bij je account hoort.</string>
+ <string name="notifications_tab_summary">Instellingen voor meldingen die verschijnen in het tabblad Meldingen.</string>
+ <string name="notifications_disabled">Appmeldingen zijn uitgeschakeld. Tik hier om ze in te schakelen in Instellingen.</string>
+ <string name="notification_types">Soorten meldingen</string>
+ <string name="error_loading_notifications">Kan meldingsinstellingen niet laden</string>
+ <string name="replies_to_your_comments">Antwoorden op je reacties</string>
+ <string name="comment_likes">Likes voor reactie</string>
+ <string name="app_notifications">Appmeldingen</string>
+ <string name="notifications_tab">Tabblad Meldingen</string>
+ <string name="email">E-mailadres</string>
+ <string name="notifications_comments_other_blogs">Reacties op andere sites</string>
+ <string name="notifications_other">Anders</string>
+ <string name="notifications_wpcom_updates">Nieuws WordPress.com</string>
+ <string name="notifications_account_emails">E-mail van WordPress.com</string>
+ <string name="notifications_account_emails_summary">We sturen altijd belangrijke e-mails met betrekking tot je account, maar je kunt ook aanvullende nuttige berichten ontvangen.</string>
+ <string name="notifications_sights_and_sounds">Beelden en geluiden</string>
+ <string name="your_sites">Jouw sites</string>
+ <string name="stats_insights_latest_post_trend">Het is %1$s geleden sinds %2$s is gepubliceerd. Hier zie je hoe het bericht het tot nu toe heeft gedaan ...</string>
+ <string name="stats_insights_latest_post_summary">Samenvatting laatste bericht</string>
+ <string name="button_revert">Terugzetten</string>
+ <string name="days_ago">%d dagen geleden</string>
+ <string name="yesterday">Gisteren</string>
+ <string name="connectionbar_no_connection">Geen verbinging</string>
+ <string name="button_publish">Publiceren</string>
+ <string name="post_deleted">Bericht verwijderd</string>
+ <string name="post_trashed">Bericht naar de prullenbak verplaatst</string>
+ <string name="page_trashed">Pagina naar de prullenbak verplaatst.</string>
+ <string name="button_back">Terug</string>
+ <string name="page_deleted">Pagina verwijderd</string>
+ <string name="button_stats">Statistieken</string>
+ <string name="button_trash">Prullenbak</string>
+ <string name="button_edit">Bewerken</string>
+ <string name="stats_no_activity_this_period">Geen activiteit deze periode</string>
+ <string name="trashed">Verwijderd</string>
+ <string name="button_preview">Voorbeeld</string>
+ <string name="button_view">Bekijk</string>
+ <string name="my_site_no_sites_view_subtitle">Wil je er één toevoegen?</string>
+ <string name="my_site_no_sites_view_title">Je hebt nog geen WordPress-sites.</string>
+ <string name="my_site_no_sites_view_drake">Illustratie</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Je hebt geen rechten om dit blog te openen</string>
+ <string name="reader_toast_err_follow_blog_not_found">Dit blog kon niet worden gevonden</string>
+ <string name="undo">Ongedaan maken</string>
+ <string name="tabbar_accessibility_label_my_site">Mijn site</string>
+ <string name="tabbar_accessibility_label_me">Ik</string>
+ <string name="passcodelock_prompt_message">Je PIN invullen.</string>
+ <string name="editor_toast_changes_saved">De wijzigingen zijn opgeslagen.</string>
+ <string name="push_auth_expired">Je verzoek is verlopen. Je bij WordPress.com aanmelden en opnieuw proberen.</string>
+ <string name="stats_insights_best_ever">Best bekeken ooit</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% van weergaven</string>
+ <string name="ignore">Negeren</string>
+ <string name="stats_insights_most_popular_hour">De meest populaire tijd.</string>
+ <string name="stats_insights_most_popular_day">De meest populaire dag.</string>
+ <string name="stats_insights_today">De statistieken van vandaag.</string>
+ <string name="stats_insights_popular">De meest populaire dag en tijd.</string>
+ <string name="stats_insights_all_time">Alle berichten, weergaven en bezoekers</string>
+ <string name="stats_insights">Inzichten</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Om de statistieken te bekijken moet je bij je WordPress.com account aanmelden en deze verbinden met Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Op zoek naar je andere recente statistieken? We hebben ze verplaatst naar de pagina Inzichten.</string>
+ <string name="me_disconnect_from_wordpress_com">Loskoppelen van WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Verbinden met WordPress.com</string>
+ <string name="me_btn_login_logout">Inloggen/uitloggen</string>
+ <string name="site_picker_cant_hide_current_site">"%s" was niet verborgen omdat het de huidige site is</string>
+ <string name="me_btn_support">Help &amp; Support</string>
+ <string name="account_settings">Accountinstellingen</string>
+ <string name="site_picker_create_dotcom">WordPress.com site maken</string>
+ <string name="site_picker_edit_visibility">Sites tonen/verbergen</string>
+ <string name="site_picker_add_self_hosted">Een zelf-gehoste site toevoegen</string>
+ <string name="site_picker_add_site">Site toevoegen</string>
+ <string name="my_site_btn_switch_site">Site wisselen</string>
+ <string name="site_picker_title">Site kiezen</string>
+ <string name="my_site_btn_view_admin">Beheer bekijken</string>
+ <string name="my_site_btn_view_site">Bekijk site</string>
+ <string name="my_site_header_publish">Publiceren</string>
+ <string name="my_site_header_look_and_feel">Look and feel</string>
+ <string name="my_site_btn_blog_posts">Blogberichten</string>
+ <string name="my_site_btn_site_settings">Instellingen</string>
+ <string name="reader_label_new_posts_subtitle">Tik om ze te tonen</string>
+ <string name="my_site_header_configuration">Configuratie</string>
+ <string name="notifications_account_required">Meld je aan bij WordPress.com voor meldingen</string>
+ <string name="stats_unknown_author">Onbekende auteur</string>
+ <string name="signout">Verbinding verbreken</string>
+ <string name="image_added">Afbeelding toegevoegd</string>
+ <string name="sign_out_wpcom_confirm">Als je je account loskoppelt, worden alle WordPress.com gegevens van @%s verwijderd van dit apparaat, inclusief lokale concepten en lokale wijzigingen.</string>
+ <string name="select_all">Alles selecteren</string>
+ <string name="hide">Verbergen</string>
+ <string name="show">Tonen</string>
+ <string name="deselect_all">Alles deselecteren</string>
+ <string name="select_from_new_picker">Meerdere selecteren met de nieuwe picker </string>
+ <string name="stats_generic_error">Benodigde statistieken konden niet geladen worden</string>
+ <string name="no_media_sources">Media ophalen mislukt</string>
+ <string name="loading_blog_videos">Video\'s ophalen</string>
+ <string name="loading_blog_images">Afbeeldingen ophalen</string>
+ <string name="error_loading_images">Fout bij het laden van afbeeldingen</string>
+ <string name="error_loading_videos">Fout bij het laden van video\'s</string>
+ <string name="error_loading_blog_images">Ophalen van afbeeldingen mislukt</string>
+ <string name="error_loading_blog_videos">Ophalen van video\'s mislukt</string>
+ <string name="no_device_videos">Geen video\'s </string>
+ <string name="no_blog_images">Geen afbeeldingen</string>
+ <string name="no_blog_videos">Geen video\'s</string>
+ <string name="no_device_images">Geen afbeeldingen</string>
+ <string name="loading_images">Afbeeldingen laden</string>
+ <string name="no_media">Geen media</string>
+ <string name="loading_videos">Video\'s laden</string>
+ <string name="device">Apparaat</string>
+ <string name="language">Taal</string>
+ <string name="add_to_post">Toevoegen aan bericht</string>
+ <string name="media_picker_title">Media selecteren</string>
+ <string name="take_photo">Een foto maken</string>
+ <string name="take_video">Een video maken</string>
+ <string name="tab_title_device_images">Apparaatafbeeldingen</string>
+ <string name="tab_title_device_videos">Apparaatvideo\'s</string>
+ <string name="tab_title_site_images">Siteafbeeldingen</string>
+ <string name="tab_title_site_videos">Sitevideo\'s</string>
+ <string name="error_publish_no_network">Kan niet publiceren als er geen verbinding is. Opgeslagen als concept.</string>
+ <string name="editor_toast_invalid_path">Ongeldig bestandspad</string>
+ <string name="verification_code">Verificatiecode</string>
+ <string name="invalid_verification_code">Ongeldige verificatiecode</string>
+ <string name="verify">Verifiëren</string>
+ <string name="two_step_footer_label">Voer de code in vanuit je Authenticator-app.</string>
+ <string name="two_step_footer_button">Verzend code via sms</string>
+ <string name="two_step_sms_sent">Kijk in je sms\'jes voor de verificatiecode.</string>
+ <string name="sign_in_jetpack">Meld je aan bij je WordPress.com account om verbinding te maken met Jetpack.</string>
+ <string name="auth_required">Meld je opnieuw aan om door te gaan.</string>
+ <string name="media_details_label_file_name">Bestandsnaam</string>
+ <string name="media_details_label_file_type">Bestandstype</string>
+ <string name="posts_fetching">Fetching posts…</string>
+ <string name="media_fetching">Fetching media…</string>
+ <string name="pages_fetching">Fetching pages…</string>
+ <string name="toast_err_post_uploading">Unable to open post while it\'s uploading</string>
+ <string name="stats_view_search_terms">Zoektermen</string>
+ <string name="comments_fetching">Fetching comments…</string>
+ <string name="stats_empty_search_terms">Geen zoektermen geregistreerd</string>
+ <string name="stats_entry_search_terms">Search Term</string>
+ <string name="stats_view_authors">Auteurs</string>
+ <string name="stats_search_terms_unknown_search_terms">Onbekende zoektermen</string>
+ <string name="stats_followers_total_wpcom_paged">Showing %1$d - %2$d of %3$s WordPress.com Followers</string>
+ <string name="stats_empty_search_terms_desc">Ontdek meer informatie over je zoekverkeer door de termen te bekijken waarmee je bezoekers hebben gezocht naar je site.</string>
+ <string name="reader_empty_posts_request_failed">Unable to retrieve posts</string>
+ <string name="publisher">Publisher:</string>
+ <string name="error_notification_open">Could not open notification</string>
+ <string name="stats_followers_total_email_paged">Showing %1$d - %2$d of %3$s Email Followers</string>
+ <string name="stats_months_and_years">Maanden en jaren</string>
+ <string name="stats_recent_weeks">Laatste weken</string>
+ <string name="error_copy_to_clipboard">Er is een fout opgetreden tijdens het kopiëren van de tekst naar het klembord</string>
+ <string name="reader_label_new_posts">Nieuwe berichten</string>
+ <string name="reader_empty_posts_in_blog">Deze blog is leeg</string>
+ <string name="stats_average_per_day">Gemiddelde per dag</string>
+ <string name="stats_period">Periode</string>
+ <string name="logs_copied_to_clipboard">Applicatielogs zijn gekopieerd naar het klembord</string>
+ <string name="stats_total">Totaal</string>
+ <string name="stats_overall">Totaal</string>
+ <string name="post_uploading">Uploaden</string>
+ <string name="reader_page_recommended_blogs">Sites die je misschien ook leuk vindt</string>
+ <string name="stats_comments_total_comments_followers">Totaalaantal berichten met reactievolgers: %1$s</string>
+ <string name="stats_visitors">Bezoekers</string>
+ <string name="stats_likes">Waarderingen</string>
+ <string name="stats_pagination_label">Pagina %1$s van %2$s</string>
+ <string name="stats_timeframe_years">Jaren</string>
+ <string name="stats_views">Weergaven</string>
+ <string name="stats_view_followers">Volgers</string>
+ <string name="stats_view_countries">Landen</string>
+ <string name="stats_view_top_posts_and_pages">Berichten &amp; pagina\'s</string>
+ <string name="stats_view_videos">Video\'s</string>
+ <string name="stats_view_publicize">Publiceren</string>
+ <string name="stats_totals_publicize">Volgers</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_entry_followers">Volger</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Auteur</string>
+ <string name="stats_empty_geoviews_desc">Bekijk de lijst en ontdek welke landen en regio\'s het meeste verkeer naar je site genereren.</string>
+ <string name="stats_totals_followers">Sinds</string>
+ <string name="stats_empty_geoviews">Geen landen geregistreerd</string>
+ <string name="stats_empty_top_posts_title">Geen berichten of pagina\'s weergegeven</string>
+ <string name="stats_empty_top_posts_desc">Ontdek wat je meest bekeken content is en zie hoe afzonderlijke berichten en pagina\'s het doen in de loop der tijd.</string>
+ <string name="stats_empty_referrers_title">Geen geregistreerde referrers</string>
+ <string name="stats_empty_clicks_title">Geen klikken geregistreerd</string>
+ <string name="stats_empty_referrers_desc">Ontdek meer over de zichtbaarheid van je site door naar de websites en zoekmachines te kijken die het meeste verkeer jouw kant opsturen.</string>
+ <string name="stats_empty_clicks_desc">Als je content links naar andere sites bevat, zie je welke links je bezoekers het meest aanklikken.</string>
+ <string name="stats_empty_tags_and_categories">Geen getagde berichten of pagina\'s weergegeven</string>
+ <string name="stats_empty_top_authors_desc">Houd de weergaves van de berichten van elke bijdrager bij en zoom in om de populairste content van elke auteur te ontdekken.</string>
+ <string name="stats_empty_tags_and_categories_desc">Bekijk een overzicht van de populairste onderwerpen op je site zoals die weerspiegeld worden in je topberichten van de afgelopen week.</string>
+ <string name="stats_empty_comments_desc">Als je reacties toestaat op je site, kun je de personen met de meeste reacties bijhouden en ontdekken welke content zorgt voor de levendigste gesprekken, op basis van de meest recente 1000 reacties.</string>
+ <string name="stats_empty_video">Geen afgespeelde video\'s</string>
+ <string name="stats_empty_video_desc">Als je video\'s hebt geüpload met behulp van VideoPress, kun je zien hoe vaak ze zijn bekeken.</string>
+ <string name="stats_empty_publicize">Geen Publicize-volgers geregistreerd</string>
+ <string name="stats_empty_publicize_desc">Houd je volgers van verschillende sociale netwerkservices bij met behulp van Publicize.</string>
+ <string name="stats_empty_followers">Geen volgers</string>
+ <string name="stats_comments_by_authors">Op auteurs</string>
+ <string name="stats_comments_by_posts_and_pages">Op berichten en pagina\'s</string>
+ <string name="stats_empty_followers_desc">Houd je totaal aantal volgers bij en hoe lang elke volger je site al volgt.</string>
+ <string name="stats_followers_total_wpcom">Totaal aantal WordPress.com volgers: %1$s</string>
+ <string name="stats_followers_total_email">Totaal aantal e-mailvolgers: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">E-mail</string>
+ <string name="stats_followers_a_minute_ago">een minuut geleden</string>
+ <string name="stats_followers_seconds_ago">seconden geleden</string>
+ <string name="stats_followers_a_day">Per dag</string>
+ <string name="stats_followers_days">%1$d dagen</string>
+ <string name="stats_followers_hours">%1$d uren</string>
+ <string name="stats_followers_an_hour_ago">een uur geleden</string>
+ <string name="stats_followers_minutes">%1$d minuten</string>
+ <string name="stats_followers_months">%1$d maanden</string>
+ <string name="stats_followers_a_year">Per jaar</string>
+ <string name="stats_followers_years">%1$d jaren</string>
+ <string name="stats_followers_a_month">Per maand</string>
+ <string name="stats_view_all">Alles weergeven</string>
+ <string name="stats_view">Weergave</string>
+ <string name="stats_other_recent_stats_label">Andere recente statistieken</string>
+ <string name="themes_fetching">Thema\'s ophalen …</string>
+ <string name="stats_for">Statistieken voor %s</string>
+ <string name="ssl_certificate_details">Details</string>
+ <string name="sure_to_remove_account">Deze site verwijderen?</string>
+ <string name="delete_sure_post">Dit bericht verwijderen</string>
+ <string name="delete_sure">Dit concept verwijderen</string>
+ <string name="delete_sure_page">Deze pagina verwijderen</string>
+ <string name="confirm_delete_multi_media">Geselecteerde items verwijderen?</string>
+ <string name="confirm_delete_media">Geselecteerd item verwijderen?</string>
+ <string name="cab_selected">%d geselecteerd</string>
+ <string name="media_gallery_date_range">Media van %1$s tot %2$s weergeven</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Bekijk onze FAQ</string>
+ <string name="reader_label_comment_count_single">Eén reactie</string>
+ <string name="reader_empty_posts_liked">Je hebt nog geen enkel bericht geliked</string>
+ <string name="nux_help_description">Bezoek het help centrum voor antwoorden op algemene vragen or bezoek de forums om nieuwe te stellen</string>
+ <string name="agree_terms_of_service">Door het aanmaken van een account ga je akkoord met de fascinerende %1$sTerms of Service%2$s</string>
+ <string name="create_new_blog_wpcom">Creëer WordPress.com blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog gecreëerd!</string>
+ <string name="reader_empty_posts_in_tag">Geen berichten met deze tag</string>
+ <string name="reader_label_view_original">Bekijk oorspronkelijk artikel</string>
+ <string name="reader_label_like">Like</string>
+ <string name="reader_label_comments_on">Reacties op</string>
+ <string name="error_refresh_unauthorized_posts">Je hebt geen rechten om berichten te bekijken of te bewerken</string>
+ <string name="reader_title_photo_viewer">%1$d van %2$d</string>
+ <string name="error_refresh_unauthorized_pages">Je hebt geen rechten om pagina\'s te bekijken of te bewerken</string>
+ <string name="error_publish_empty_post">Kan een leeg bericht niet publiceren</string>
+ <string name="signing_out">Uitloggen…</string>
+ <string name="comment">Reactie</string>
+ <string name="comment_trashed">Reactie verwijderd</string>
+ <string name="older_month">Ouder dan een maand</string>
+ <string name="more">Meer</string>
+ <string name="older_two_days">Ouder dan 2 dagen</string>
+ <string name="older_last_week">Ouder dan een week</string>
+ <string name="select_a_blog">Selecteer een WordPress site</string>
+ <string name="comment_reply_to_user">Antwoord op %s</string>
+ <string name="stats_no_blog">Statistieken konden niet worden geladen voor het vereiste blog</string>
+ <string name="sending_content">Uploaden %s inhoud</string>
+ <string name="mnu_comment_liked">Liked</string>
+ <string name="uploading_total">Uploaden %1$d van %2$d</string>
+ <string name="media_empty_list_custom_date">Geen media in dit tijdsinterval</string>
+ <string name="posting_post">Publiceren "%s"</string>
+ <string name="pages_empty_list">Nog geen pagina\'s. Waarom maak je er niet een?</string>
+ <string name="posts_empty_list">Nog geen berichten. Waarom maak je er niet een?</string>
+ <string name="reader_empty_comments">Nog geen reacties</string>
+ <string name="reader_label_comment_count_multi">%,d reacties</string>
+ <string name="reader_label_comments_closed">Reacties zijn gesloten</string>
+ <string name="error_refresh_unauthorized_comments">Je hebt geen rechten om reacties te bekijken of te bewerken</string>
+ <string name="reader_empty_followed_blogs_title">Je volgt nog geen sites</string>
+ <string name="reader_menu_block_blog">Blokkeer deze blog</string>
+ <string name="reader_toast_err_generic">Deze actie kan niet uitgevoerd worden</string>
+ <string name="reader_toast_err_block_blog">Blokkeren van deze blog is mislukt</string>
+ <string name="reader_toast_blog_blocked">Berichten van deze blog zullen niet meer getoond worden</string>
+ <string name="contact_us">Contact</string>
+ <string name="hs__new_conversation_header">Ondersteuning chat</string>
+ <string name="hs__conversation_header">Ondersteuning chat</string>
+ <string name="hs__conversation_detail_error">Beschrijf het probleem dat je ziet</string>
+ <string name="hs__username_blank_error">Voer een geldige naam in</string>
+ <string name="hs__invalid_email_error">Voer een geldig e-mailadres in</string>
+ <string name="add_location">Locatie toevoegen</string>
+ <string name="current_location">Huidige Locatie</string>
+ <string name="search_location">Zoeken</string>
+ <string name="edit_location">Bewerken</string>
+ <string name="search_current_location">Locatie bepalen</string>
+ <string name="preference_send_usage_stats">Verstuur statistieken</string>
+ <string name="preference_send_usage_stats_summary">Stuur automatisch gebruiksstatistieken om ons te helpen WordPress voor Android te verbeteren</string>
+ <string name="update_verb">Bijwerken</string>
+ <string name="schedule_verb">Inplannen</string>
+ <string name="reader_title_subs">Tags &amp; Blogs</string>
+ <string name="reader_page_followed_tags">Gevolgde tags</string>
+ <string name="reader_hint_add_tag_or_url">Voer een tag of URL in om te volgen</string>
+ <string name="reader_label_followed_blog">Blog gevolgd</string>
+ <string name="reader_label_tag_preview">Posts getagd %s</string>
+ <string name="reader_toast_err_get_blog_info">Kan deze blog niet tonen</string>
+ <string name="reader_toast_err_already_follow_blog">Je volgt deze blog al</string>
+ <string name="reader_toast_err_follow_blog">Kan deze blog niet volgen</string>
+ <string name="reader_toast_err_unfollow_blog">Kan deze blog niet ontvolgen</string>
+ <string name="reader_empty_recommended_blogs">Geen aanbevolen blogs</string>
+ <string name="reader_title_blog_preview">Reader-blog</string>
+ <string name="reader_title_tag_preview">Reader-tag</string>
+ <string name="reader_page_followed_blogs">Gevolgde sites</string>
+ <string name="saving">Aan het opslaan...</string>
+ <string name="media_empty_list">Geen media</string>
+ <string name="ptr_tip_message">Tip: schuif omlaag om te vernieuwen</string>
+ <string name="help">Help</string>
+ <string name="forgot_password">Wachtwoord vergeten?</string>
+ <string name="forums">Forums</string>
+ <string name="help_center">Helpcentrum</string>
+ <string name="ssl_certificate_error">Ongeldig SSL-certificaat</string>
+ <string name="ssl_certificate_ask_trust">Als je normaal gesproken zonder problemen verbinding maakt met deze site kan deze fout betekenen dat iemand zich probeert voor te doen als deze site. Ga niet verder als dat zo is. Wil je het certificaat toch vertrouwen?</string>
+ <string name="could_not_remove_account">Kon site niet verwijderen</string>
+ <string name="out_of_memory">Apparaatgeheugen vol</string>
+ <string name="no_network_message">Er is geen netwerk beschikbaar</string>
+ <string name="no_site_error">Kan niet verbinden met de WordPress-site</string>
+ <string name="adding_cat_failed">Toevoegen van categorie mislukt</string>
+ <string name="adding_cat_success">Categorie toegevoegd</string>
+ <string name="cat_name_required">Het veld Categorienaam is verplicht</string>
+ <string name="category_automatically_renamed">Categorienaam %1$s is ongeldig. De categorie is hernoemd naar %2$s.</string>
+ <string name="no_account">Geen WordPress-account gevonden. Voeg een account toe en probeer het opnieuw</string>
+ <string name="sdcard_message">Er moet een SD-kaart zijn gekoppeld om media te uploaden</string>
+ <string name="stats_empty_comments">Er zijn nog geen reacties</string>
+ <string name="error_load_comment">Kan de reactie niet laden</string>
+ <string name="error_downloading_image">Fout bij downloaden van afbeelding</string>
+ <string name="passcode_wrong_passcode">Verkeerde PIN</string>
+ <string name="invalid_email_message">Je e-mailadres is niet geldig</string>
+ <string name="invalid_password_message">Wachtwoord moet minstens 4 tekens bevatten</string>
+ <string name="invalid_username_too_short">Gebruikersnaam moet langer dan 4 tekens zijn</string>
+ <string name="invalid_username_too_long">Gebruikersnaam moet korter dan 61 tekens zijn</string>
+ <string name="username_only_lowercase_letters_and_numbers">Gebruikersnaam kan alleen kleine letters (a-z) en cijfers bevatten</string>
+ <string name="username_required">Voer een gebruikersnaam in</string>
+ <string name="username_not_allowed">Gebruikersnaam niet toegestaan</string>
+ <string name="theme_fetch_failed">Fout bij het ophalen van thema\'s</string>
+ <string name="theme_set_failed">Fout bij het instellen van het thema</string>
+ <string name="theme_auth_error_message">Controleer of je rechten hebt om thema\'s in te stellen</string>
+ <string name="comments_empty_list">Geen reacties</string>
+ <string name="mnu_comment_unspam">Geen spam</string>
+ <string name="gallery_error">Media-item kan niet worden opgehaald</string>
+ <string name="blog_not_found">Er heeft zich een fout voorgedaan tijdens het openen van dit blog</string>
+ <string name="wait_until_upload_completes">Wacht tot de upload is voltooid</string>
+ <string name="stats_bar_graph_empty">Geen statistieken beschikbaar</string>
+ <string name="reply_failed">Reactie mislukt</string>
+ <string name="notifications_empty_list">Geen notificaties</string>
+ <string name="error_delete_post">Er heeft zich een fout voorgedaan bij het verwijderen van %s</string>
+ <string name="error_refresh_posts">Berichten kunnen momenteel niet worden ververst</string>
+ <string name="error_refresh_pages">Pagina\'s kunnen momenteel niet worden ververst</string>
+ <string name="error_refresh_notifications">Notificaties kunnen momenteel niet worden ververst</string>
+ <string name="error_refresh_comments">Reacties kunnen momenteel niet worden ververst</string>
+ <string name="error_refresh_stats">Statistieken kunnen momenteel niet worden ververst</string>
+ <string name="error_generic">Er heeft zich een fout voorgedaan</string>
+ <string name="error_moderate_comment">Er heeft zich een fout voorgedaan tijdens het modereren</string>
+ <string name="error_edit_comment">Er heeft zich een fout voorgedaan tijdens het bewerken van de reactie</string>
+ <string name="blog_name_cant_be_used">Dit site-adres kan niet worden gebruikt</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Het site-adres kan alleen uit kleine letters (a-z) en cijfers bestaan</string>
+ <string name="blog_name_exists">Die site bestaat al</string>
+ <string name="blog_name_reserved">Die site is gereserveerd</string>
+ <string name="username_must_be_at_least_four_characters">Gebruikersnaam moet tenminste 4 tekens lang zijn</string>
+ <string name="username_contains_invalid_characters">Gebruikersnaam kan niet het teken \'_\' bevatten</string>
+ <string name="error_upload">Er heeft zich een fout voorgedaan bij het uploaden van %s</string>
+ <string name="username_must_include_letters">Gebruikersnaam moet ten minste 1 letter (a-z) bevatten</string>
+ <string name="email_invalid">Voer een geldig e-mailadres in</string>
+ <string name="email_not_allowed">Dat e-mailadres is niet toegestaan</string>
+ <string name="username_exists">Die gebruikersnaam bestaat al</string>
+ <string name="email_exists">Dat e-mailadres is al in gebruik</string>
+ <string name="username_reserved_but_may_be_available">Die gebruikersnaam is momenteel gereserveerd, maar is over een paar dagen misschien wel beschikbaar.</string>
+ <string name="blog_name_required">Voer een site-adres in</string>
+ <string name="blog_name_not_allowed">Dat site-adres is niet toegestaan</string>
+ <string name="blog_name_must_be_at_least_four_characters">Site-adres moet minimaal 4 tekens lang zijn</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Het site-adres moet korter zijn dan 64 tekens</string>
+ <string name="blog_name_contains_invalid_characters">Site-adres kan niet het teken \'_\' bevatten</string>
+ <string name="blog_name_reserved_but_may_be_available">Die site is momenteel gereserveerd, maar is mogelijk binnen een paar dagen beschikbaar</string>
+ <string name="username_or_password_incorrect">De gebruikersnaam of het wachtwoord is niet correct</string>
+ <string name="nux_cannot_log_in">We kunnen je niet aanmelden</string>
+ <string name="invalid_url_message">Controleer of de ingevoerde URL geldig is</string>
+ <string name="blog_removed_successfully">Site is verwijderd</string>
+ <string name="remove_account">Site verwijderen</string>
+ <string name="xmlrpc_error">Verbinding mislukt. Voer het volledig pad in naar xmlrpc.php op je site en probeer het opnieuw.</string>
+ <string name="select_categories">Categorieën selecteren</string>
+ <string name="account_details">Accountgegevens</string>
+ <string name="edit_post">Bericht bewerken</string>
+ <string name="add_comment">Reactie toevoegen</string>
+ <string name="connection_error">Verbindingsfout</string>
+ <string name="cancel_edit">Bewerken annuleren</string>
+ <string name="scaled_image_error">Voer een geldige waarde in voor geschaalde breedte</string>
+ <string name="post_not_found">Er heeft zich een fout voorgedaan bij het laden van het bericht. Ververs je berichten en probeer het opnieuw.</string>
+ <string name="learn_more">Meer informatie</string>
+ <string name="media_gallery_settings_title">Galerij-instellingen</string>
+ <string name="media_gallery_image_order">Afbeeldingsvolgorde</string>
+ <string name="media_gallery_num_columns">Aantal kolommen</string>
+ <string name="media_gallery_type_thumbnail_grid">Miniatuurraster</string>
+ <string name="media_gallery_edit">Galerij bewerken</string>
+ <string name="media_error_no_permission">Je hebt geen rechten om de mediabibliotheek te bekijken</string>
+ <string name="cannot_delete_multi_media_items">Sommige media kan niet worden verwijderd. Probeer het later opnieuw.</string>
+ <string name="themes_live_preview">Live voorbeeld</string>
+ <string name="theme_current_theme">Huidig thema</string>
+ <string name="theme_premium_theme">Premium thema</string>
+ <string name="link_enter_url_text">Linktekst (optioneel)</string>
+ <string name="create_a_link">Een link maken</string>
+ <string name="page_settings">Pagina-instellingen</string>
+ <string name="local_draft">Lokaal concept</string>
+ <string name="upload_failed">Uploaden mislukt</string>
+ <string name="horizontal_alignment">Horizontale uitlijning</string>
+ <string name="file_not_found">Kan het mediabestand voor upload niet vinden. Is het verplaatst of verwijderd?</string>
+ <string name="post_settings">Berichtinstellingen</string>
+ <string name="delete_post">Bericht verwijderen</string>
+ <string name="delete_page">Pagina verwijderen</string>
+ <string name="comment_status_approved">Goedgekeurd</string>
+ <string name="comment_status_unapproved">In afwachting</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Weggegooid</string>
+ <string name="edit_comment">Reactie bewerken</string>
+ <string name="mnu_comment_approve">Goedkeuren</string>
+ <string name="mnu_comment_unapprove">Afkeuren</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Prullenbak</string>
+ <string name="dlg_approving_comments">Aan het goedkeuren</string>
+ <string name="dlg_unapproving_comments">Aan het afkeuren</string>
+ <string name="dlg_spamming_comments">Aan het markeren als spam</string>
+ <string name="dlg_trashing_comments">Aan het verplaatsen naar de prullenbak</string>
+ <string name="dlg_confirm_trash_comments">Verplaatsen naar de prullenbak?</string>
+ <string name="trash_yes">Verwijderen</string>
+ <string name="trash_no">Niet verwijderen</string>
+ <string name="trash">Prullenbak</string>
+ <string name="author_name">Naam auteur</string>
+ <string name="author_email">E-mail auteur</string>
+ <string name="author_url">URL auteur</string>
+ <string name="hint_comment_content">Reactie</string>
+ <string name="saving_changes">Wijzigingen aan het opslaan</string>
+ <string name="sure_to_cancel_edit_comment">Bewerken van deze reactie annuleren?</string>
+ <string name="content_required">Reactie is vereist</string>
+ <string name="toast_comment_unedited">Reactie is niet gewijzigd</string>
+ <string name="delete_draft">Concept verwijderen</string>
+ <string name="preview_page">Voorbeeld van pagina bekijken</string>
+ <string name="preview_post">Voorbeeld van bericht bekijken</string>
+ <string name="comment_added">Reactie toegevoegd</string>
+ <string name="post_not_published">Berichtstatus is niet gepubliceerd</string>
+ <string name="page_not_published">Paginastatus is niet gepubliceerd</string>
+ <string name="view_in_browser">Weergeven in browser</string>
+ <string name="add_new_category">Nieuwe categorie toevoegen</string>
+ <string name="category_name">Naam categorie</string>
+ <string name="category_slug">Tijdelijke aanduiding categorie (optioneel)</string>
+ <string name="category_desc">Beschrijving categorie (optioneel)</string>
+ <string name="category_parent">Bovenliggende categorie (optioneel)</string>
+ <string name="share_action_post">Nieuw bericht</string>
+ <string name="share_action_media">Mediabibliotheek</string>
+ <string name="file_error_create">Kan geen tijdelijk bestand maken voor het uploaden van media. Controleer of er genoeg vrije ruimte is op je apparaat.</string>
+ <string name="location_not_found">Onbekende locatie</string>
+ <string name="open_source_licenses">Open-sourcelicenties</string>
+ <string name="pending_review">In afwachting van controle</string>
+ <string name="http_credentials">HTTP-verificatiegegevens (optioneel)</string>
+ <string name="http_authorization_required">Autorisatie vereist</string>
+ <string name="post_format">Berichtindeling</string>
+ <string name="new_post">Nieuw bericht</string>
+ <string name="new_media">Nieuwe media</string>
+ <string name="view_site">Site weergeven</string>
+ <string name="privacy_policy">Privacybeleid</string>
+ <string name="local_changes">Lokale wijzigingen</string>
+ <string name="image_settings">Afbeeldingsinstellingen</string>
+ <string name="add_account_blog_url">Blogadres</string>
+ <string name="wordpress_blog">WordPress-blog</string>
+ <string name="error_blog_hidden">Dit blog is verborgen en kan niet worden geladen. Schakel het blog in instellingen in en probeer het opnieuw.</string>
+ <string name="fatal_db_error">Er heeft zich een fout voorgedaan bij het aanmaken van de database van de app. Probeer de app opnieuw te installeren.</string>
+ <string name="jetpack_message_not_admin">De Jetpack-plugin is vereist voor statistieken. Neem contact op met de sitebeheerder.</string>
+ <string name="reader_title_applog">Applicatielogboek</string>
+ <string name="reader_share_link">Link delen</string>
+ <string name="reader_toast_err_add_tag">Deze tag kan niet worden toegevoegd</string>
+ <string name="reader_toast_err_remove_tag">Deze tag kan niet worden verwijderd</string>
+ <string name="required_field">Vereist veld</string>
+ <string name="email_hint">E-mailadres</string>
+ <string name="site_address">Je zelf-gehoste adres (URL)</string>
+ <string name="email_cant_be_used_to_signup">Je kunt dat e-mailadres niet gebruiken om je aan te melden, omdat ze een aantal van onze e-mailberichten blokkeren. Probeer een andere e-mailaanbieder.</string>
+ <string name="email_reserved">Dat e-mailadres is al in gebruik. Zoek in je inbox naar een activeringsmail. Als je niet activeert, kun je het over een paar dagen opnieuw proberen.</string>
+ <string name="blog_name_must_include_letters">Site-adres moet ten minste 1 letter (a-z) bevatten</string>
+ <string name="blog_name_invalid">Ongeldig site-adres</string>
+ <string name="blog_title_invalid">Ongeldige sitetitel</string>
+ <string name="notifications_empty_all">Nog geen meldingen.</string>
+ <string name="invalid_site_url_message">Controleer of de ingevoerde site-URL geldig is</string>
+ <string name="share_url_page">Deel pagina</string>
+ <string name="share_url_post">Deel bericht</string>
+ <string name="deleting_page">Verwijderen van pagina</string>
+ <string name="deleting_post">Verwijderen van bericht</string>
+ <string name="share_link">Deel link</string>
+ <string name="creating_your_account">Je account aanmaken</string>
+ <string name="creating_your_site">Je site aanmaken</string>
+ <string name="reader_empty_posts_in_tag_updating">Berichten ophalen...</string>
+ <string name="error_refresh_media">Er is iets verkeerd gegaan met het vernieuwen van de mediabibliotheek. Probeer het later nog eens.</string>
+ <string name="reader_likes_you_and_multi">Jij en %,d anderen vinden dit leuk</string>
+ <string name="reader_likes_multi">%,d mensen vinden dit leuk</string>
+ <string name="reader_toast_err_get_comment">Deze reactie kon niet opgehaald worden</string>
+ <string name="reader_label_reply">Beantwoord</string>
+ <string name="video">Video</string>
+ <string name="download">Downloaden van media</string>
+ <string name="comment_spammed">Reactie als spam gemarkeerd</string>
+ <string name="cant_share_no_visible_blog">Je kan niet delen naar WordPress zonder een zichtbare blog</string>
+ <string name="pick_photo">Selecteer foto</string>
+ <string name="account_two_step_auth_enabled">Voor deze account is authenticatie in twee stappen actief. Bekijk je beveiligingsinstellingen op WordPress.com en genereer een applicatie-specifiek wachtwoord.</string>
+ <string name="pick_video">Selecteer video</string>
+ <string name="reader_toast_err_get_post">Niet in staat om dit bericht op te halen</string>
+ <string name="validating_user_data">Gebruikersgegevens valideren</string>
+ <string name="validating_site_data">Sitegegevens valideren</string>
+ <string name="select_time">Selecteer tijd</string>
+ <string name="reader_likes_you_and_one">Jij en één andere vinden dit leuk</string>
+ <string name="select_date">Selecteer datum</string>
+ <string name="reader_empty_followed_blogs_description">Geen zorgen, tap het tag icoon om te beginnen.</string>
+ <string name="nux_oops_not_selfhosted_blog">Log in op WordPress.com</string>
+ <string name="password_invalid">Je hebt een veiliger wachtwoord nodig. Let erop dat je 7 of meer karakters gebruikt, gebruik hoofd- en kleine letters, nummers of speciale karakters.</string>
+ <string name="nux_tap_continue">Ga door</string>
+ <string name="nux_welcome_create_account">Maak account aan</string>
+ <string name="nux_add_selfhosted_blog">Voeg zelf gehoste site toe</string>
+ <string name="signing_in">Inloggen…</string>
+ <string name="select_from_media_library">Selecteer uit de mediabibliotheek</string>
+ <string name="media_add_popup_title">Toevoegen aan mediabibliotheek</string>
+ <string name="reader_untitled_post">(geen titel)</string>
+ <string name="reader_btn_share">Delen</string>
+ <string name="reader_btn_follow">Volg</string>
+ <string name="reader_label_added_tag">%s toegevoegd</string>
+ <string name="reader_label_removed_tag">%s verwijderd</string>
+ <string name="reader_toast_err_url_intent">Onmogelijk %s te openen</string>
+ <string name="create_account_wpcom">Maak een account op Wordpress.com aan</string>
+ <string name="connecting_wpcom">Verbinden met Wordpress.com</string>
+ <string name="reader_toast_err_share_intent">Onmogelijk te delen</string>
+ <string name="reader_toast_err_view_image">Onmogelijk afbeelding te bekijken</string>
+ <string name="reader_empty_followed_tags">Je volgt geen enkele tags</string>
+ <string name="jetpack_message">De Jetpack plugin is nodig voor statistieken. Wil je Jetpack installeren?</string>
+ <string name="empty_list_default">Deze lijst is leeg</string>
+ <string name="media_add_new_media_gallery">Een nieuwe galerij aanmaken</string>
+ <string name="jetpack_not_found">Jetpack plugin niet gevonden</string>
+ <string name="reader_share_subject">gedeeld van %s</string>
+ <string name="reader_likes_one">1 persoon vindt dit leuk</string>
+ <string name="reader_likes_only_you">Je vindt dit leuk</string>
+ <string name="reader_toast_err_comment_failed">Je reactie kon niet gepost worden</string>
+ <string name="reader_toast_err_tag_exists">Je volgt deze tag al</string>
+ <string name="reader_toast_err_tag_invalid">Dat is geen geldige tag</string>
+ <string name="nux_tutorial_get_started_title">Begin!</string>
+ <string name="username_invalid">Ongeldige gebruikersnaam</string>
+ <string name="reader_btn_unfollow">Volgend</string>
+ <string name="limit_reached">Limiet bereikt. Je kan het na 1 minuut nogmaals proberen. Als je het eerder probeert dan zul je langer moeten wachten totdat de ban wordt opgeheven. Neem contact op met de helpdesk wanneer je denk dat dit niet klopt.</string>
+ <string name="reader_hint_comment_on_comment">Beantwoord reactie...</string>
+ <string name="button_next">Volgende</string>
+ <string name="images">Afbeeldingen</string>
+ <string name="media_add_popup_capture_photo">Maak een nieuwe foto</string>
+ <string name="media_add_popup_capture_video">Maak een nieuwe video</string>
+ <string name="all">Alle</string>
+ <string name="unattached">Niet gekoppeld</string>
+ <string name="custom_date">Aan te passen datum</string>
+ <string name="media_gallery_type_slideshow">Diavoorstelling</string>
+ <string name="media_edit_title_text">Titel</string>
+ <string name="media_edit_caption_text">Onderschrift</string>
+ <string name="theme_activating_button">Activeren</string>
+ <string name="theme_set_success">Thema succesvol ingesteld!</string>
+ <string name="theme_auth_error_title">Kon geen thema\'s ophalen</string>
+ <string name="post_excerpt">Samenvatting</string>
+ <string name="share_action_title">Voeg toe aan ...</string>
+ <string name="themes">Thema\'s</string>
+ <string name="media_gallery_image_order_random">Willekeurig</string>
+ <string name="media_gallery_image_order_reverse">Omgekeerd</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="media_gallery_type_squares">Vierkanten</string>
+ <string name="media_gallery_type_circles">Circels</string>
+ <string name="themes_details_label">Details</string>
+ <string name="themes_features_label">Functies</string>
+ <string name="theme_activate_button">Activeren</string>
+ <string name="share_action">Deel</string>
+ <string name="stats">Statistieken</string>
+ <string name="stats_view_visitors_and_views">Bezoekers en views</string>
+ <string name="stats_view_clicks">Kliks</string>
+ <string name="stats_view_tags_and_categories">Tags en categorieën </string>
+ <string name="stats_entry_country">Land</string>
+ <string name="stats_entry_posts_and_pages">Titel</string>
+ <string name="stats_entry_tags_and_categories">Onderwerp</string>
+ <string name="stats_entry_authors">Auteur</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_totals_views">Views</string>
+ <string name="stats_totals_clicks">Kliks</string>
+ <string name="stats_totals_plays">Afgespeeld</string>
+ <string name="passcode_preference_title">PIN slot</string>
+ <string name="passcode_turn_off">Zet PIN slot uit</string>
+ <string name="passcode_manage">Beheer PIN slot</string>
+ <string name="passcode_enter_passcode">Voer je PIN in</string>
+ <string name="passcode_enter_old_passcode">Voer je oude PIN in</string>
+ <string name="passcode_re_enter_passcode">Voer je PIN opnieuw in</string>
+ <string name="passcode_change_passcode">Verander PIN</string>
+ <string name="passcode_set">PIN ingesteld</string>
+ <string name="media_edit_description_text">Beschrijving</string>
+ <string name="media_edit_title_hint">Voer hier een titel in</string>
+ <string name="media_edit_caption_hint">Voer hier een onderschrift in</string>
+ <string name="media_edit_description_hint">Voer hier een beschrijving in</string>
+ <string name="media_edit_success">Bijgewerkt</string>
+ <string name="media_edit_failure">Bijwerken mislukt</string>
+ <string name="stats_timeframe_days">Dagen</string>
+ <string name="stats_timeframe_weeks">Weken</string>
+ <string name="stats_timeframe_months">Maanden</string>
+ <string name="stats_timeframe_yesterday">Gisteren</string>
+ <string name="stats_view_referrers">Top auteurs</string>
+ <string name="stats_timeframe_today">Vandaag</string>
+ <string name="media_gallery_type_tiled">Tegels</string>
+ <string name="passcode_turn_on">Zet PIN slot aan</string>
+ <string name="discard">Verwijder</string>
+ <string name="upload">Upload</string>
+ <string name="sign_in">Inloggen</string>
+ <string name="notifications">Notificaties</string>
+ <string name="note_reply_successful">Reactie gepubliceerd</string>
+ <string name="new_notifications">%d nieuwe notificaties</string>
+ <string name="more_notifications">en %d extra.</string>
+ <string name="follows">Volgt</string>
+ <string name="loading">Laden...</string>
+ <string name="httpuser">HTTP gebruikersnaam</string>
+ <string name="httppassword">HTTP wachtwoord</string>
+ <string name="error_media_upload">Er is een fout opgetreden tijdens het uploaden van media</string>
+ <string name="publish_date">Publiceer</string>
+ <string name="content_description_add_media">Media toevoegen</string>
+ <string name="post_content">Inhoud (Druk om tekst en media toe te voegen)</string>
+ <string name="incorrect_credentials">Incorrecte gebruikersnaam en wachtwoord.</string>
+ <string name="password">Wachtwoord</string>
+ <string name="username">Gebruikersnaam</string>
+ <string name="reader">Reader</string>
+ <string name="featured">Gebruiken als uitgelichte afbeelding</string>
+ <string name="featured_in_post">Plaats afbeelding in bericht</string>
+ <string name="no_network_title">Geen netwerk beschikbaar</string>
+ <string name="post">Bericht</string>
+ <string name="pages">Pagina\'s</string>
+ <string name="caption">Onderschrift (optioneel)</string>
+ <string name="width">Breedte</string>
+ <string name="posts">Berichten</string>
+ <string name="anonymous">Anoniem</string>
+ <string name="page">Pagina</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">Ok</string>
+ <string name="upload_scaled_image">Upload en link naar afbeelding</string>
+ <string name="scaled_image">Breedte geschaalde afbeelding</string>
+ <string name="scheduled">Gepland</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Uploaden …</string>
+ <string name="version">Versie</string>
+ <string name="tos">Eindgebruikersovereenkomst</string>
+ <string name="app_title">WordPress voor Android</string>
+ <string name="max_thumbnail_px_width">Standaardbreedte van afbeelding</string>
+ <string name="image_alignment">Uitlijning </string>
+ <string name="refresh">Vernieuw</string>
+ <string name="untitled">Zonder titel</string>
+ <string name="edit">Bewerk</string>
+ <string name="post_id">Bericht</string>
+ <string name="page_id">Pagina</string>
+ <string name="post_password">Wachtwoord (optioneel) </string>
+ <string name="immediately">Direct</string>
+ <string name="quickpress_add_alert_title">Geef een snelkoppelingsnaam</string>
+ <string name="today">Vandaag</string>
+ <string name="settings">Instellingen</string>
+ <string name="share_url">Deel URL</string>
+ <string name="quickpress_window_title">Selecteer een blog voor de QuickPress snelkoppeling</string>
+ <string name="quickpress_add_error">Snelkoppelingsnaam kan niet leeg zijn.</string>
+ <string name="publish_post">Publiceer</string>
+ <string name="post_private">Privé</string>
+ <string name="draft">Concept</string>
+ <string name="upload_full_size_image">Upload en link naar de volledige afbeelding</string>
+ <string name="title">Titel</string>
+ <string name="tags_separate_with_commas">Tags (gescheiden door komma\'s)</string>
+ <string name="categories">Categorieën </string>
+ <string name="dlg_deleting_comments">Reacties verwijderen</string>
+ <string name="notification_vibrate">Vibreren </string>
+ <string name="notification_blink">Flikker het notificatie licht</string>
+ <string name="notification_sound">Meldingsgeluid</string>
+ <string name="status">Status</string>
+ <string name="location">Locatie</string>
+ <string name="sdcard_title">SD kaart is vereist </string>
+ <string name="select_video">Selecteer een video van je galerij </string>
+ <string name="media">Media</string>
+ <string name="delete">Verwijderen</string>
+ <string name="none">Geen</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Selecteer een foto van je galerij.</string>
+ <string name="no">Nee</string>
+ <string name="yes">Ja</string>
+ <string name="reply">Beantwoord</string>
+ <string name="on">aan</string>
+ <string name="add">Voeg toe</string>
+ <string name="error">Fout</string>
+ <string name="cancel">Annuleer </string>
+ <string name="save">Bewaar</string>
+ <string name="category_refresh_error">Categorie vernieuwen fout</string>
+ <string name="preview">Voorbeeld</string>
+ <string name="notification_settings">Meldingsinstellingen</string>
+</resources>
diff --git a/WordPress/src/main/res/values-pl/strings.xml b/WordPress/src/main/res/values-pl/strings.xml
new file mode 100644
index 000000000..7e00cb008
--- /dev/null
+++ b/WordPress/src/main/res/values-pl/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Redaktor</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Współpracownik</string>
+ <string name="role_follower">Subskrybent</string>
+ <string name="role_viewer">Oglądający</string>
+ <string name="error_post_my_profile_no_connection">Brak połączenia, nie można zapisać twojego profilu</string>
+ <string name="alignment_right">Wyrównaj do prawej</string>
+ <string name="alignment_left">Wyrównaj do lewej</string>
+ <string name="alignment_none">Brak wyrównania</string>
+ <string name="site_settings_list_editor_action_mode_title">Wybrano %1$d</string>
+ <string name="error_fetch_users_list">Nie można pobrać użytkowników witryny</string>
+ <string name="plans_manage">Zarządzaj swoim planem na\nWorPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Witryna nie ma jeszcze oglądających</string>
+ <string name="title_follower">Obserwujący</string>
+ <string name="title_email_follower">Obserwujący przez e-mail</string>
+ <string name="people_fetching">Pobieranie użytkowników...</string>
+ <string name="people_empty_list_filtered_email_followers">Nie masz jeszcze żadnych obserwujących przez e-mail</string>
+ <string name="people_empty_list_filtered_followers">Nie masz jeszcze żadnych obserwujących.</string>
+ <string name="people_empty_list_filtered_users">Nie masz jeszcze żadnych użytkowników</string>
+ <string name="people_dropdown_item_viewers">Oglądający</string>
+ <string name="people_dropdown_item_email_followers">Obserwujący przez e-mail</string>
+ <string name="people_dropdown_item_followers">Obserwujący</string>
+ <string name="people_dropdown_item_team">Zespół</string>
+ <string name="invite_message_usernames_limit">Zaproś do 10 adresów email i/lub użytkowników WordPress.com. \nTym, którym będzie potrzebna nazwa użytkownika zostaną przesłane instrukcje jak ją utworzyć.</string>
+ <string name="viewer_remove_confirmation_message">Jeśli usuniesz tego oglądającego, nie będzie mógł odwiedzić tej witryny.\nCzy nadal chcesz usunąć tego oglądającego?</string>
+ <string name="follower_remove_confirmation_message">Jeśli usuniesz tego obserwującego, przestanie on otrzymywać powiadomienia o tej witrynie, chyba że zacznie ponownie obserwować.\nCzy wciąż chcesz usunąć tego obserwującego?</string>
+ <string name="follower_subscribed_since">Od %1$s</string>
+ <string name="reader_label_view_gallery">Zabacz Galerię</string>
+ <string name="error_remove_follower">Nie można usunąć obserwującego</string>
+ <string name="error_remove_viewer">Nie można usunąć oglądającego</string>
+ <string name="error_fetch_email_followers_list">Nie można pobrać obserwujących witrynę przez e-mail</string>
+ <string name="error_fetch_followers_list">Nie można usunąć obserwujących witrynę</string>
+ <string name="editor_failed_uploads_switch_html">Nie udało się przesłać niektórych mediów. Nie możesz przejść do trybu HTML w tym momencie. Usunąć wszystkie błędnie przesłane media i kontynuować?</string>
+ <string name="format_bar_description_html">Tryb HTML</string>
+ <string name="image_thumbnail">Miniaturka obrazka</string>
+ <string name="visual_editor">Edytor wizualny</string>
+ <string name="format_bar_description_ol">Lista numeryczna</string>
+ <string name="format_bar_description_ul">Lista nieuporządkowana</string>
+ <string name="format_bar_description_more">Wstaw więcej</string>
+ <string name="format_bar_description_media">Wstaw media</string>
+ <string name="format_bar_description_link">Wstaw odnośnik</string>
+ <string name="format_bar_description_strike">Przekreślenie</string>
+ <string name="format_bar_description_quote">Cytat blokowy</string>
+ <string name="format_bar_description_italic">Kursywa</string>
+ <string name="format_bar_description_underline">Podkreślenie</string>
+ <string name="image_width">Szerokość</string>
+ <string name="format_bar_description_bold">Pogrubienie</string>
+ <string name="image_link_to">Odnośnik do</string>
+ <string name="image_caption">Etykieta</string>
+ <string name="image_alt_text">Alternatywny tekst</string>
+ <string name="image_settings_save_toast">Zmiany zapisano</string>
+ <string name="image_settings_dismiss_dialog_title">Odrzucić niezapisane zmiany?</string>
+ <string name="stop_upload_button">Zatrzymaj przesyłanie</string>
+ <string name="stop_upload_dialog_title">Zatrzymać przesyłanie?</string>
+ <string name="alert_error_adding_media">Błąd podczas wstawiania mediów</string>
+ <string name="alert_action_while_uploading">Przesyłasz właśnie media. Poczekaj aż to zadanie się zakończy.</string>
+ <string name="alert_insert_image_html_mode">Nie można wstawić mediów bezpośrednio w trybie HTML. Przełącz się na tryb wizualny.</string>
+ <string name="uploading_gallery_placeholder">Przesyłanie galerii...</string>
+ <string name="invite_error_some_failed">Zaproszenie wysłano, ale wystąpiły błędy!</string>
+ <string name="invite_sent">Zaproszenie zostało poprawnie wysłane</string>
+ <string name="tap_to_try_again">Stuknij aby spróbować ponownie!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">Wystąpił błąd podczas wysyłania zaproszenia!</string>
+ <string name="invite_error_invalid_usernames_multiple">Nie można wysłać: wystąpiły nieprawidłowe nazwy użytkownika lub adresy email</string>
+ <string name="invite_error_invalid_usernames_one">Nie można wysłać: nieprawidłowa nazwa użytkownika lub email</string>
+ <string name="invite_error_no_usernames">Proszę dodać przynajmniej jednego użytkownika</string>
+ <string name="invite_message_info">(Opcjonalnie) Możesz wprowadzić tekst wiadomości zawierającej do 500 znaków, która będzie załączona do zaproszenia do użytkownika(ów).</string>
+ <string name="invite_message_remaining_other">Pozostało %d znaków</string>
+ <string name="invite_message_remaining_one">Pozostał 1 znak</string>
+ <string name="invite_message_remaining_zero">Pozostało 0 znaków</string>
+ <string name="invite_invalid_email">Adres e-mail \'%s\' jest nieprawidłowy</string>
+ <string name="invite_message_title">Wiadomość spersonalizowana</string>
+ <string name="invite_already_a_member">Istnieje już użytkownik \'%s\'</string>
+ <string name="invite_username_not_found">Nie odnaleziono użytkownika \'%s\'</string>
+ <string name="invite">Zaproś</string>
+ <string name="invite_names_title">Nazwy użytkowników lub adresy e-mail</string>
+ <string name="send_link">Wyślij odnośnik</string>
+ <string name="invite_people">Zaproś ludzi</string>
+ <string name="signup_succeed_signin_failed">Twoje konto zostało utworzone, ale wystąpił błąd. Spróbuj zalogować się przy pomocy swojej nowo utworzonej nazwy użytkownika i hasła.</string>
+ <string name="my_site_header_external">Na zewnątrz</string>
+ <string name="label_clear_search_history">Usuń historię wyszukiwania</string>
+ <string name="dlg_confirm_clear_search_history">Usunąć historię wyszukiwania?</string>
+ <string name="reader_empty_posts_in_search_description">Nie odnaleziono żadnych wpisów zawierających %s w twoim języku</string>
+ <string name="reader_label_post_search_running">Szukanie...</string>
+ <string name="reader_empty_posts_in_search_title">Nie odnaleziono wpisów</string>
+ <string name="reader_label_related_posts">Powiązana treść</string>
+ <string name="reader_label_post_search_explainer">Przeszukaj wszystkie publiczne blogi WordPress.com</string>
+ <string name="reader_hint_post_search">Przeszukaj WordPress.com</string>
+ <string name="reader_title_related_post_detail">Powiązany wpis</string>
+ <string name="reader_title_search_results">Szukaj %s</string>
+ <string name="preview_screen_links_disabled">Odnośniki są wyłączone na ekranie podglądu</string>
+ <string name="draft_explainer">Ten wpis jest szkicem, który nie został opublikowany</string>
+ <string name="send">Wyślij</string>
+ <string name="user_remove_confirmation_message">Jeśli usuniesz %1$s, użytkownik utraci dostęp do tej witryny, jednakże treści dodawane przez %1$s pozostaną na tej witrynie.\nCzy chciałbyś nadal usunąć tego użytkownika?</string>
+ <string name="person_removed">Pomyślnie usunięto %1$s</string>
+ <string name="person_remove_confirmation_title">Usuń %1$s</string>
+ <string name="people">Ludzie</string>
+ <string name="edit_user">Edytuj użytkownika</string>
+ <string name="role">Rola</string>
+ <string name="reader_empty_posts_in_custom_list">Witryny na tej liście nie opublikowały ostatnio żadnych treści</string>
+ <string name="error_remove_user">Nie można usunąć użytkownika</string>
+ <string name="error_update_role">Nie można zaktualizować roli użytkownika</string>
+ <string name="error_fetch_viewers_list">Nie można pobrać oglądających tej witryny</string>
+ <string name="gravatar_camera_and_media_permission_required">Uprawnienia wymagane do wyboru lub zrobienia zdjęcia</string>
+ <string name="error_updating_gravatar">Błąd aktualizacji twojego obrazka profilowego</string>
+ <string name="error_refreshing_gravatar">Błąd wczytywania twojego obrazka profilowego</string>
+ <string name="error_locating_image">Błąd lokalizacji skadrowanego obrazka</string>
+ <string name="error_cropping_image">Błąd kadrowania obrazka</string>
+ <string name="gravatar_tip">Nowe! Stuknij na swój obrazek profilowy aby go zmienić!</string>
+ <string name="launch_your_email_app">Uruchom aplikację do poczty email</string>
+ <string name="checking_email">Sprawdzanie emaili</string>
+ <string name="not_on_wordpress_com">Nie ma ciebie na WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Aktualnie niedostępne. Proszę wprowadzić hasło</string>
+ <string name="check_your_email">Sprawdź swoje konto email</string>
+ <string name="logging_in">Logowanie</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Uzyskaj odnośnik przesłany na twój adres email aby się szybko zalogować</string>
+ <string name="enter_your_password_instead">Wprowadź swoje hasło</string>
+ <string name="web_address_dialog_hint">Pokazany publicznie kiedy komentujesz</string>
+ <string name="username_email">Email lub użytkownik</string>
+ <string name="jetpack_not_connected_message">Wtyczka Jetpack jest zainstalowana, ale nie jest połączona z WordPress.com. Czy chcesz włączyć Jetpack?</string>
+ <string name="jetpack_not_connected">Wtyczka Jetpack nie jest podłączona</string>
+ <string name="new_editor_reflection_error">Edytor widoku nie jest kompatybilny z twoim urządzeniem. Został automatycznie wyłączony.</string>
+ <string name="stats_insights_latest_post_no_title">(brak tytułu)</string>
+ <string name="capture_or_pick_photo">Wykonaj lub wybierz zdjęcie</string>
+ <string name="plans_post_purchase_text_themes">Od teraz masz nieograniczony dostęp do motywów Premium. Podejrzyj dowolny motyw na swojej stronie aby rozpocząć.</string>
+ <string name="plans_post_purchase_button_themes">Przeglądaj Motywy</string>
+ <string name="plans_post_purchase_title_themes">Znajdź znakomity motyw premium</string>
+ <string name="plans_post_purchase_button_video">Rozpocznij nowy wpis</string>
+ <string name="plans_post_purchase_text_video">Możesz przesyłać i przechowywać pliki wideo na twojej witrynie przy pomocy VideoPress i twojego powiększonego magazynu na media.</string>
+ <string name="plans_post_purchase_title_video">Uatrakcyjnij wpisy za pomocą filmów</string>
+ <string name="plans_post_purchase_button_customize">Dostosuj moją stronę</string>
+ <string name="plans_post_purchase_text_customize">Od teraz masz dostęp do dedykowanych czcionek i kolorów, oraz możliwość edycji styli CSS. </string>
+ <string name="plans_post_purchase_title_customize">Dostosuj czcionki i kolory</string>
+ <string name="plans_post_purchase_text_intro">Twoja witryna wykonuje salta z ekscytacji! Przejrzyj nowe funkcje twojej witryny i wybierz gdzie chciałbyś rozpocząć.</string>
+ <string name="plans_post_purchase_title_intro">Gratulacje, blog jest do twojej dyspozycji!</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Plany</string>
+ <string name="export_your_content_message">Twoje wpisy, strony i ustawienia zostaną przesłane na adres %s.</string>
+ <string name="plans_loading_error">Błąd wczytywania planów</string>
+ <string name="export_your_content">Wyeksportuj zawartość</string>
+ <string name="exporting_content_progress">Eksportowanie zawartości</string>
+ <string name="export_email_sent">Wysłano email z danymi eksportowymi!</string>
+ <string name="show_purchases">Pokaż zakupy</string>
+ <string name="checking_purchases">Sprawdzanie zakupów</string>
+ <string name="premium_upgrades_message">Masz aktywowane plany premium na swojej witrynie. Anuluj plany przed usunięciem swojej witryny.</string>
+ <string name="premium_upgrades_title">Płatne podwyższenie planu</string>
+ <string name="purchases_request_error">Coś poszło nie tak. Nie można uruchomić zakupów.</string>
+ <string name="delete_site_progress">Usuwanie witryny...</string>
+ <string name="delete_site_hint">Usuń witrynę</string>
+ <string name="delete_site_summary">Nie można cofnąć tego działania. Usunięcie tej witryny usunie całą zawartość, wszystkich współpracowników i domeny tej witryny.</string>
+ <string name="export_site_hint">Wyeksportuj swoją witrynę do pliku XML</string>
+ <string name="are_you_sure">Czy jesteś pewien?</string>
+ <string name="export_site_summary">Jeśli jesteś pewien, upewnij się iż wyeksportowałeś zawartość swojej witryny. Późniejsze odzyskanie zawartości nie będzie możliwe.</string>
+ <string name="keep_your_content">Zachowaj zawartość swojej witryny</string>
+ <string name="domain_removal_hint">Domeny, które nie będą działały po usunięciu twojej witryny</string>
+ <string name="domain_removal_summary">Bądź ostrożny! Usunięcie twojej witryny usunie również poniższe domeny</string>
+ <string name="domain_removal">Usunięcie domeny</string>
+ <string name="primary_domain">Domena główna</string>
+ <string name="error_deleting_site_summary">Wystąpił błąd podczas usuwania twojej witryny. Proszę skontaktować się ze wsparciem technicznym</string>
+ <string name="error_deleting_site">Błąd podczas usuwania witryny</string>
+ <string name="site_settings_export_content_title">Eksportuj zawartość</string>
+ <string name="confirm_delete_site_prompt">Proszę wpisać %1$s w polu poniżej w celu potwierdzenia. Twoja witryna zostanie trwale usunięta.</string>
+ <string name="confirm_delete_site">Potwierdź usunięcie witryny</string>
+ <string name="contact_support">Skontaktuj się ze wsparciem</string>
+ <string name="start_over_text">Jeśli chcesz zachować witrynę ale nie potrzebujesz już wpisów i stron, które masz obecnie, nasz zespół wsparcia może usunąć twoje wpisy, strony, media i komentarze za ciebie.\n\nTwoja witryna i adres URL pozostaną aktywne, ale umożliwi tobie stworzenie witryny na nowo. Po prostu skontaktuj się z nami w celu wyczyszczenia zawartości.</string>
+ <string name="site_settings_start_over_hint">Rozpocznij tworzenie witryny od nowa</string>
+ <string name="let_us_help">Daj sobie pomóc</string>
+ <string name="me_btn_app_settings">Ustawienia aplikacji</string>
+ <string name="start_over">Zacznij od nowa</string>
+ <string name="editor_remove_failed_uploads">Usuń nieudane próby przesyłania</string>
+ <string name="editor_toast_failed_uploads">Nie udało się przesłać niektórych mediów. Nie możesz zapisać i opublikować\nwpisu w tym momencie. Czy chcesz usunąć wszystkie błędnie przesłane media?</string>
+ <string name="site_settings_advanced_header">Zaawansowane</string>
+ <string name="comments_empty_list_filtered_trashed">Brak komentarzy w koszu</string>
+ <string name="comments_empty_list_filtered_pending">Brak komentarzy oczekujących</string>
+ <string name="comments_empty_list_filtered_approved">Brak zatwierdzonych komentarzy</string>
+ <string name="button_done">Zrobione</string>
+ <string name="button_skip">Pomiń</string>
+ <string name="site_timeout_error">Błąd połączenia do WordPressa - przekroczono maksymalny dopuszczalny czas połączenia</string>
+ <string name="xmlrpc_malformed_response_error">Nie można się połączyć. Instalacja WordPressa odpowiedziała nieprawidłowym dokumentem XML-RPC</string>
+ <string name="xmlrpc_missing_method_error">Błąd połączenia. Brak wymaganych metod XML-RPC na serwerze.</string>
+ <string name="theme_free">Bezpłatny</string>
+ <string name="theme_all">Wszystkie</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Film</string>
+ <string name="alignment_center">Wyśrodkowany</string>
+ <string name="post_format_chat">Czat</string>
+ <string name="post_format_gallery">Galeria</string>
+ <string name="post_format_image">Obrazek</string>
+ <string name="post_format_quote">Cytat</string>
+ <string name="post_format_link">Odnośnik</string>
+ <string name="post_format_standard">Zwykły wpis</string>
+ <string name="notif_events">Informacje o kursach i wydarzeniach WordPress.com (online i osobiste)</string>
+ <string name="post_format_audio">Plik dźwiękowy</string>
+ <string name="post_format_aside">Notatka na marginesie</string>
+ <string name="notif_surveys">Możliwość uczestnictwa w badaniach i ankietach WordPress.com</string>
+ <string name="notif_tips">Wskazówki jak używać WordPress.com</string>
+ <string name="notif_community">Społeczność</string>
+ <string name="replies_to_my_comments">Odpowiedzi do moich komentarzy</string>
+ <string name="notif_suggestions">Propozycje</string>
+ <string name="notif_research">Badania</string>
+ <string name="site_achievements">Osiągnięcia witryny</string>
+ <string name="username_mentions">Wspomniana nazwa użytkow.</string>
+ <string name="likes_on_my_posts">Polubienia moich wpisów</string>
+ <string name="site_follows">Obserwowanie witryny</string>
+ <string name="likes_on_my_comments">Polubienia moich komentarzy</string>
+ <string name="comments_on_my_site">Komentarze na mojej witrynie</string>
+ <string name="site_settings_list_editor_summary_other">%d elementów</string>
+ <string name="site_settings_list_editor_summary_one">1 element</string>
+ <string name="approve_auto_if_previously_approved">Komentarze znanych użytkowników</string>
+ <string name="approve_auto">Wszystkich użytkowników</string>
+ <string name="approve_manual">Nie zatwierdzaj komentarzy automatycznie</string>
+ <string name="site_settings_paging_summary_other">%d komentarzy na stronie</string>
+ <string name="site_settings_paging_summary_one">1 komentarz na stronie</string>
+ <string name="site_settings_multiple_links_summary_other">Wymagane zatwierdzenie dla więcej niż %d odnośnika/ów</string>
+ <string name="site_settings_multiple_links_summary_one">Wymagaj zatwierdzenia dla więcej niż 1 odnośnika</string>
+ <string name="site_settings_multiple_links_summary_zero">Wymagaj zatwierdzenia dla więcej niż 0 odnośników</string>
+ <string name="detail_approve_auto">Automatycznie zatwierdzaj wszystkie komentarze</string>
+ <string name="detail_approve_auto_if_previously_approved">Automatycznie zatwierdzić, jeśli użytkownik posiada wcześniej zatwierdzony komentarz</string>
+ <string name="detail_approve_manual">Wymagaj ręcznego zatwierdzenia wszystkich komentarzy</string>
+ <string name="days_quantity_one">1 dzień</string>
+ <string name="filter_trashed_posts">W koszu</string>
+ <string name="days_quantity_other">%d dniach</string>
+ <string name="filter_draft_posts">Szkice</string>
+ <string name="filter_published_posts">Opublikowane</string>
+ <string name="filter_scheduled_posts">Zaplanowane</string>
+ <string name="primary_site">Główna witryna</string>
+ <string name="web_address">Adres internetowy</string>
+ <string name="pending_email_change_snackbar">Kliknij na odnośnik weryfikujący wewnątrz e-maila przesłanego do %1$s w celu potwierdzenia Twojego nowego adresu</string>
+ <string name="editor_toast_uploading_please_wait">Aktualnie przesyłasz media. Poczekaj aż proces się zakończy.</string>
+ <string name="error_refresh_comments_showing_older">Odświeżenie komentarzy nie było możliwe - pokazane są starsze komentarze</string>
+ <string name="editor_post_settings_set_featured_image">Ustaw obrazek wyróżniający</string>
+ <string name="editor_post_settings_featured_image">Obrazek wyróżniający</string>
+ <string name="new_editor_promo_desc">Aplikacja WordPress dla Androida zawiera nowy, piękny edytor wizualny. Wypróbuj go tworząc nowy wpis. </string>
+ <string name="new_editor_promo_title">Całkiem nowy edytor</string>
+ <string name="new_editor_promo_button_label">Wielkie dzięki!</string>
+ <string name="visual_editor_enabled">Edytor wizualny włączony</string>
+ <string name="editor_content_placeholder">Podziel się swoją historią...</string>
+ <string name="editor_page_title_placeholder">Tytuł strony</string>
+ <string name="editor_post_title_placeholder">Tytuł wpisu</string>
+ <string name="email_address">Adres e-mail</string>
+ <string name="preference_show_visual_editor">Pokaż edytor wizualny</string>
+ <string name="dlg_sure_to_delete_comments">Trwale usunąć te komentarze ?</string>
+ <string name="preference_editor">Edytor</string>
+ <string name="dlg_sure_to_delete_comment">Trwale usunąć ten komentarz ?</string>
+ <string name="mnu_comment_delete_permanently">Usuń</string>
+ <string name="comment_deleted_permanently">Komentarz usunięty</string>
+ <string name="mnu_comment_untrash">Przywróć</string>
+ <string name="comments_empty_list_filtered_spam">Brak komentarzy typu spam</string>
+ <string name="comment_status_all">Wszystkie</string>
+ <string name="could_not_load_page">Nie można wczytać strony</string>
+ <string name="interface_language">Język interfejsu</string>
+ <string name="off">Wyłączone</string>
+ <string name="about_the_app">O aplikacji</string>
+ <string name="error_post_account_settings">Nie udało się zapisać ustawień twojego konta</string>
+ <string name="error_post_my_profile">Nie udało się zapisać twojego profilu</string>
+ <string name="error_fetch_account_settings">Nie można pobrać ustawień twojego konta</string>
+ <string name="error_fetch_my_profile">Nie można pobrać twojego profilu</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, rozumiem</string>
+ <string name="stats_widget_promo_desc">Dodaj widżet do ekranu głównego, aby uzyskać dostęp do Statystyk jednym kliknięciem.</string>
+ <string name="stats_widget_promo_title">Widget statystyk strony głównej</string>
+ <string name="site_settings_unknown_language_code_error">Kod języka nie został rozpoznany</string>
+ <string name="site_settings_threading_dialog_description">Zezwól na zagnieżdżanie komentarzy w wątkach</string>
+ <string name="site_settings_threading_dialog_header">Wątek do</string>
+ <string name="add_category">Dodaj kategorię</string>
+ <string name="remove">Usuń</string>
+ <string name="search">Szukaj</string>
+ <string name="disabled">Wyłączone</string>
+ <string name="site_settings_image_original_size">Oryginalny rozmiar</string>
+ <string name="privacy_private">Twoja witryna jest widoczna jedynie przez ciebie i użytkowników przez ciebie zatwierdzonych</string>
+ <string name="privacy_public_not_indexed">Twoja witryna jest widoczna dla wszystkich, ale prosi wyszukiwarki o nie indeksowanie</string>
+ <string name="privacy_public">Twoja witryna jest widoczna dla wszystkich i może być indeksowana przez wyszukiwarki</string>
+ <string name="about_me_hint">Kilka słów o sobie...</string>
+ <string name="about_me">O mnie</string>
+ <string name="public_display_name_hint">Jeśli nie zostanie ustawiona, to nazwa wyświetlana będzie twoją nazwą użytkownika</string>
+ <string name="public_display_name">Nazwa wyświetlana publicznie</string>
+ <string name="my_profile">Mój profil</string>
+ <string name="first_name">Imię</string>
+ <string name="last_name">Nazwisko</string>
+ <string name="site_privacy_public_desc">Zezwól wyszukiwarkom na indeksowanie tej witryny</string>
+ <string name="site_privacy_hidden_desc">Zniechęcaj wyszukiwarki do indeksowania tej witryny</string>
+ <string name="site_privacy_private_desc">Chciałbym aby moja witryna była prywatna, widoczna jedynie dla użytkowników, których wybiorę</string>
+ <string name="cd_related_post_preview_image">Obrazek podglądu powiązanego wpisu</string>
+ <string name="error_post_remote_site_settings">Nie można było zapisać informacji o witrynie</string>
+ <string name="error_fetch_remote_site_settings">Nie można było pobrać informacji o witrynie</string>
+ <string name="error_media_upload_connection">Błąd połączenia podczas przesyłania mediów</string>
+ <string name="site_settings_disconnected_toast">Brak połączenia, możliwość edycji została zablokowana.</string>
+ <string name="site_settings_unsupported_version_error">Niewspierana wersja WordPressa</string>
+ <string name="site_settings_multiple_links_dialog_description">Wymagaj zatwierdzenia komentarzy, które zawierają więcej niż tą liczbę odnośników</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatycznie zamknij</string>
+ <string name="site_settings_close_after_dialog_description">Automatycznie zamknij komentowanie artykułów</string>
+ <string name="site_settings_paging_dialog_description">Podziel wątki komentarzy na kilka stron</string>
+ <string name="site_settings_paging_dialog_header">Komentarzy na strone</string>
+ <string name="site_settings_close_after_dialog_title">Zamknij komentowanie</string>
+ <string name="site_settings_blacklist_description">Jeśli komentarz zawiera jedno z poniższych słów w swojej zawartości, imieniu autora, odnośniku URL, adresie e-mail lub adresie IP, zostanie oznaczony jako spam. Możesz wprowadzić część słowa - np. słowo "press" jest dopasowane do "WordPress"</string>
+ <string name="site_settings_hold_for_moderation_description">Jeśli komentarz zawiera jedno z poniższych słów w swojej zawartości, imieniu autora, odnośniku URL, adresie e-mail lub adresie IP, zostanie umieszczone w kolejce do moderacji. Możesz wprowadzić część słowa - np. słowo "press" jest dopasowane do "WordPress"</string>
+ <string name="site_settings_list_editor_input_hint">Wprowadź słowo lub frazę</string>
+ <string name="site_settings_list_editor_no_items_text">Brak elementów</string>
+ <string name="site_settings_learn_more_caption">Możesz unieważnić te ustawienia dla poszczególnych wpisów.</string>
+ <string name="site_settings_rp_preview3_site">W trakcie upgrade\'u</string>
+ <string name="site_settings_rp_preview3_title">Przegląd najważniejszych wydarzeń: VideoPress dla wesel</string>
+ <string name="site_settings_rp_preview2_site">w "Aplikacjach"</string>
+ <string name="site_settings_rp_preview2_title">Aplikacja WordPress dla Androida przeszła duży face-lifting</string>
+ <string name="site_settings_rp_preview1_site">W sekcji "Urządzenia mobilne"</string>
+ <string name="site_settings_rp_preview1_title">Duża aktualizacja dla iPhone/iPad jest dostępna</string>
+ <string name="site_settings_rp_show_images_title">Pokaż obrazki</string>
+ <string name="site_settings_rp_show_header_title">Pokaż nagłówek</string>
+ <string name="site_settings_rp_switch_summary">Powiązane Wpisy powoduje wyświetlenie odpowiedniej zawartości z twojej witryny poniżej twoich wpisów</string>
+ <string name="site_settings_rp_switch_title">Pokaż powiązane wpisy</string>
+ <string name="site_settings_delete_site_hint">Usuwa dane twojej witryny z aplikacji</string>
+ <string name="site_settings_blacklist_hint">Komentarze odpowiadające szablonowi filtra są oznaczane jako spam</string>
+ <string name="site_settings_moderation_hold_hint">Komentarze odpowiadające szablonowi filtra są umieszcza w kolejce do moderacji</string>
+ <string name="site_settings_multiple_links_hint">Ignoruje limit odnośników od znanych tobie użytkowników</string>
+ <string name="site_settings_whitelist_hint">Autor komentarza musi mieć uprzednio zatwierdzony komentarz</string>
+ <string name="site_settings_user_account_required_hint">Użytkownicy muszą być zarejestrowani oraz zalogowani aby móc dodawać komentarze</string>
+ <string name="site_settings_identity_required_hint">Autor komentarza musi podać imię oraz e-mail</string>
+ <string name="site_settings_manual_approval_hint">Komentarze muszą zostać zatwierdzone ręcznie</string>
+ <string name="site_settings_paging_hint">Pokaż komentarze w kawałkach o określonym rozmiarze</string>
+ <string name="site_settings_threading_hint">Zezwalaj na zagnieżdżanie do określonej głębokości</string>
+ <string name="site_settings_sort_by_hint">Określa porządek w jakim prezentowane są komentarze</string>
+ <string name="site_settings_close_after_hint">Zablokuj możliwość komentowania po określonym czasie</string>
+ <string name="site_settings_receive_pingbacks_hint">Zezwól na powiadomienia o odnośnikach z innych blogów</string>
+ <string name="site_settings_send_pingbacks_hint">Spróbuj powiadomić wszystkie blogi, do których odnośniki zostały umieszczone w artykule</string>
+ <string name="site_settings_allow_comments_hint">Zezwól na komentowanie</string>
+ <string name="site_settings_discussion_hint">Pokaż i zmień ustawienia dyskusji twoich witryn</string>
+ <string name="site_settings_more_hint">Pokaż wszystkie dostępne ustawienia Dyskusji</string>
+ <string name="site_settings_related_posts_hint">Pokaż lub ukryj powiązane wpisy w czytniku</string>
+ <string name="site_settings_upload_and_link_image_hint">Uaktywnij aby zawsze przesyłać obrazek oryginalnym rozmiarze</string>
+ <string name="site_settings_image_width_hint">Zmienia rozmiar obrazka do zadanej szerokości</string>
+ <string name="site_settings_format_hint">Ustawia format dla nowych wpisów</string>
+ <string name="site_settings_category_hint">Ustawia kategorię dla nowego wpisu</string>
+ <string name="site_settings_location_hint">Automatycznie dodawaj dane lokalizacyjne do twoich wpisów</string>
+ <string name="site_settings_password_hint">Zmień hasło</string>
+ <string name="site_settings_username_hint">Konto aktualnego użytkownika</string>
+ <string name="site_settings_language_hint">Główny język, w którym ten blog jest prowadzony</string>
+ <string name="site_settings_privacy_hint">Ustawienia kto może oglądać twoją witrynę</string>
+ <string name="site_settings_address_hint">Zmiana twojego adresu nie jest aktualnie wspierana</string>
+ <string name="site_settings_tagline_hint">Krótki opis lub zajawka opisująca twojego bloga</string>
+ <string name="site_settings_title_hint">W kilku słowach, wyjaśnij o czym jest ta witryna</string>
+ <string name="site_settings_whitelist_known_summary">Komentarze od znanych tobie użytkowników</string>
+ <string name="site_settings_whitelist_all_summary">Komentarze wszystkich użytkowników</string>
+ <string name="site_settings_threading_summary">%d poziomów</string>
+ <string name="site_settings_privacy_private_summary">Prywatna</string>
+ <string name="site_settings_privacy_hidden_summary">Ukryta</string>
+ <string name="site_settings_delete_site_title">Usuń witrynę</string>
+ <string name="site_settings_privacy_public_summary">Publiczna</string>
+ <string name="site_settings_blacklist_title">Czarna lista</string>
+ <string name="site_settings_moderation_hold_title">Wstrzymaj do moderacji</string>
+ <string name="site_settings_multiple_links_title">Odnośniki w komentarzach</string>
+ <string name="site_settings_whitelist_title">Automatyczne zatwierdzaj</string>
+ <string name="site_settings_threading_title">Wątki dyskusji</string>
+ <string name="site_settings_paging_title">Stronicowanie</string>
+ <string name="site_settings_sort_by_title">Sortuj</string>
+ <string name="site_settings_account_required_title">Użytkow. muszą być zalog.</string>
+ <string name="site_settings_identity_required_title">Muszą zawierać imię i email</string>
+ <string name="site_settings_receive_pingbacks_title">Odbieraj pingbacki</string>
+ <string name="site_settings_send_pingbacks_title">Wysyłaj pingbacki</string>
+ <string name="site_settings_allow_comments_title">Włącz komentowanie</string>
+ <string name="site_settings_default_format_title">Domyślny format wpisów</string>
+ <string name="site_settings_default_category_title">Domyślna kategoria wpisów</string>
+ <string name="site_settings_location_title">Włącz lokalizację</string>
+ <string name="site_settings_address_title">Adres</string>
+ <string name="site_settings_title_title">Tytuł witryny</string>
+ <string name="site_settings_tagline_title">Opis</string>
+ <string name="site_settings_this_device_header">To urządzenie</string>
+ <string name="site_settings_discussion_new_posts_header">Ustawienia domyślne dla nowych wpisów</string>
+ <string name="site_settings_account_header">Konto</string>
+ <string name="site_settings_writing_header">Pisanie</string>
+ <string name="newest_first">Od najnowszych</string>
+ <string name="site_settings_general_header">Ogólne</string>
+ <string name="discussion">Dyskusja</string>
+ <string name="comments">Komentarze</string>
+ <string name="oldest_first">Od najstarszych</string>
+ <string name="related_posts">Powiązane wpisy</string>
+ <string name="close_after">Zamknij po</string>
+ <string name="privacy">Prywatność witryny</string>
+ <string name="media_error_no_permission_upload">Nie masz uprawnień do przesyłania mediów na tą witrynę</string>
+ <string name="never">Nigdy</string>
+ <string name="unknown">Nieznane</string>
+ <string name="reader_err_get_post_not_found">Ten wpis już nie istnieje</string>
+ <string name="reader_err_get_post_not_authorized">Nie posiadasz uprawień do podglądu tego wpisu</string>
+ <string name="reader_err_get_post_generic">Nie można odzyskać tego wpisu</string>
+ <string name="blog_name_no_spaced_allowed">Adres witryny nie może zawierać spacji</string>
+ <string name="invalid_username_no_spaces">Nazwa użytkownika nie może zawierać spacji</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Witryny, które obserwujesz niczego ostatnio nie opublikowały</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Brak ostatnich wpisów</string>
+ <string name="edit_media">Edytuj media</string>
+ <string name="media_details_copy_url_toast">Adres URL skopiowano do schowka</string>
+ <string name="media_details_copy_url">Kopiuj adres URL</string>
+ <string name="media_details_label_date_uploaded">Przesłano</string>
+ <string name="media_details_label_date_added">Dodano</string>
+ <string name="selected_theme">Wybrany motyw</string>
+ <string name="could_not_load_theme">Nie można wczytać szablonu</string>
+ <string name="theme_activation_error">Coś poszło nie tak. Nie można było uaktywnić motywu</string>
+ <string name="theme_by_author_prompt_append">o %1$s</string>
+ <string name="theme_prompt">Dziękujemy za wybranie %1$s</string>
+ <string name="theme_view">Podgląd</string>
+ <string name="theme_try_and_customize">Wypróbuj i dostosuj</string>
+ <string name="theme_details">Szczegóły</string>
+ <string name="theme_support">Wsparcie</string>
+ <string name="theme_done">Wykonano</string>
+ <string name="theme_manage_site">ZARZĄDZAJ WITRYNĄ</string>
+ <string name="title_activity_theme_support">Motywy</string>
+ <string name="theme_activate">Aktywuj</string>
+ <string name="current_theme">Aktualny motyw</string>
+ <string name="support">Wsparcie</string>
+ <string name="date_range_start_date">Data rozpoczęcia</string>
+ <string name="date_range_end_date">Data zakończenia</string>
+ <string name="details">Szczegóły</string>
+ <string name="active">Aktywny</string>
+ <string name="customize">Dostosuj</string>
+ <string name="stats_referrers_spam_generic_error">Coś poszło nie tak podczas operacji. Stan spamu nie został zmieniony</string>
+ <string name="stats_referrers_marking_not_spam">Oznacz jako nie spam</string>
+ <string name="stats_referrers_marking_spam">Oznacz jako spam</string>
+ <string name="stats_referrers_unspam">Nie spam</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="post_published">Wpis opublikowany</string>
+ <string name="page_published">Strona opublikowana</string>
+ <string name="post_updated">Wpis uaktualniony</string>
+ <string name="page_updated">Strona uaktualniona</string>
+ <string name="theme_auth_error_authenticate">Błąd pobierania motywów: błąd uwierzytelnienia użytkownika</string>
+ <string name="theme_no_search_result_found">Przepraszamy, nie znaleziono motywów.</string>
+ <string name="media_file_name">Nazwa pliku: %s</string>
+ <string name="media_dimensions">Wymiary: %s</string>
+ <string name="media_uploaded_on">Przesłano %s</string>
+ <string name="upload_queued">W kolejce</string>
+ <string name="media_file_type">Typ pliku: %s</string>
+ <string name="reader_label_gap_marker">Wczytaj więcej wpisów</string>
+ <string name="notifications_no_search_results">\'%s\' nie pasuje do żadnej witryny</string>
+ <string name="search_sites">Szukaj witryn</string>
+ <string name="notifications_empty_view_reader">Zobacz Czytnik</string>
+ <string name="unread">Nieprzeczytane</string>
+ <string name="notifications_empty_action_followers_likes">Bądź powiadomiony: komentuj wpisy, które przeczytałeś.</string>
+ <string name="notifications_empty_action_comments">Dołącz do konwersacji: komentuj wpisy z blogów, które śledzisz</string>
+ <string name="notifications_empty_action_unread">Ożyw dyskusję: dodaj nowy wpis.</string>
+ <string name="notifications_empty_action_all">Bądź aktywny! Skomentuj wpisy z blogów, które śledzisz.</string>
+ <string name="notifications_empty_likes">Brak nowych polubień... na razie.</string>
+ <string name="notifications_empty_followers">Brak nowych śledzących... jeszcze.</string>
+ <string name="notifications_empty_comments">Brak nowych komentarzy... jeszcze.</string>
+ <string name="notifications_empty_unread">Jesteś na bieżąco!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Proszę wejdź w statystki w aplikacji i spróbuj dodać widget później</string>
+ <string name="stats_widget_error_readd_widget">Proszę usuń widget i dodaj go ponownie</string>
+ <string name="stats_widget_error_no_visible_blog">Brak dostępu dla statystyk bez widocznego bloga</string>
+ <string name="stats_widget_error_no_permissions">Twoje konto WordPress.com nie ma dostępu do statystyk na tym blogu</string>
+ <string name="stats_widget_error_no_account">Proszę zalogować się do WordPressa</string>
+ <string name="stats_widget_error_generic">Statystyki nie mogły być wczytane</string>
+ <string name="stats_widget_loading_data">Wczytywanie danych...</string>
+ <string name="stats_widget_name_for_blog">Dzisiejsze statystyki dla %1$s</string>
+ <string name="stats_widget_name">Dzisiejsze statystyki WordPressa</string>
+ <string name="add_location_permission_required">Uprawnienie wymagane do dodania lokalizacji</string>
+ <string name="add_media_permission_required">Uprawnienia wymagane aby dodać media</string>
+ <string name="access_media_permission_required">Uprawnienia wymagane aby uzyskać dostęp do mediów</string>
+ <string name="stats_enable_rest_api_in_jetpack">Aby podejrzeć statystyki, uaktywnij moduł API JSON w Jetpack.</string>
+ <string name="error_open_list_from_notification">Ten wpis lub strona zostały opublikowane na innej witrynie</string>
+ <string name="reader_short_comment_count_multi">%s Komentarzy</string>
+ <string name="reader_short_comment_count_one">1 Komentarz</string>
+ <string name="reader_label_submit_comment">WYŚLIJ</string>
+ <string name="reader_hint_comment_on_post">Dodaj komentarz do wpisu...</string>
+ <string name="reader_discover_visit_blog">Odwiedź %s</string>
+ <string name="reader_discover_attribution_blog">Oryginalnie opublikowano %s</string>
+ <string name="reader_discover_attribution_author">Pierwotnie opublikowano przez %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Po raz pierwszy opublikowano przez %1$s na %2$s</string>
+ <string name="reader_short_like_count_multi">%s polubień</string>
+ <string name="reader_short_like_count_one">1 polubienie</string>
+ <string name="reader_label_follow_count">%,d osób śledzi</string>
+ <string name="reader_short_like_count_none">Polubienie</string>
+ <string name="reader_menu_tags">Edytuj tagi i blogi</string>
+ <string name="reader_title_post_detail">Wpis czytnika</string>
+ <string name="local_draft_explainer">Ten wpis posiada lokalny szkic, który nie został opublikowany</string>
+ <string name="local_changes_explainer">Ten wpis posiada lokalne zmiany, które nie zostały opuplikowane</string>
+ <string name="notifications_push_summary">Ustawienia powiadomień, które występują na twoim urządzeniu</string>
+ <string name="notifications_email_summary">Ustawienia powiadomień, które są wysłane na adres email przywiązany do twojego konta.</string>
+ <string name="notifications_tab_summary">Ustawienia powiadomień, które pojawiają się w zakładce Powiadomienia.</string>
+ <string name="notifications_disabled">Powiadomienia aplikacji zostały wyłączony. Naciśnij tutaj aby je uaktywnić w Ustawieniach.</string>
+ <string name="notification_types">Typy powiadomień</string>
+ <string name="error_loading_notifications">Nie można wczytać ustawień powiadomień</string>
+ <string name="replies_to_your_comments">Odpowiedzi na twoje komentarze</string>
+ <string name="comment_likes">Polubienia komentarzy</string>
+ <string name="app_notifications">Powiadomienia aplikacji</string>
+ <string name="notifications_tab">Zakładka powiadomień</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Komentarze na innych witrynach</string>
+ <string name="notifications_other">Inne</string>
+ <string name="notifications_wpcom_updates">Aktualizacje WordPress.com</string>
+ <string name="notifications_account_emails">Email od WordPress.com</string>
+ <string name="notifications_account_emails_summary">Wysyłamy zawsze istotne wiadomości dotyczące twojego konta, ale czasem wysyłamy również przydatne informacje</string>
+ <string name="your_sites">Twoje witryny</string>
+ <string name="notifications_sights_and_sounds">Widoki i Dźwięki</string>
+ <string name="stats_insights_latest_post_trend">Minęło %1$s od czasu kiedy wpis %2$s został opublikowany. Oto informacje o jego popularności od tego czasu...</string>
+ <string name="stats_insights_latest_post_summary">Podsumowanie ostatniego wpisu</string>
+ <string name="button_revert">Cofnij</string>
+ <string name="yesterday">Wczoraj</string>
+ <string name="days_ago">%d dni temu</string>
+ <string name="connectionbar_no_connection">Brak połączenia</string>
+ <string name="button_edit">Edytuj</string>
+ <string name="button_publish">Opublikuj</string>
+ <string name="button_preview">Podgląd</string>
+ <string name="post_trashed">Wpis został przeniesiony do kosza</string>
+ <string name="post_deleted">Wpis usunięty</string>
+ <string name="button_stats">Statystyki</string>
+ <string name="button_back">Wstecz</string>
+ <string name="page_trashed">Strona przeniesiona do kosza</string>
+ <string name="stats_no_activity_this_period">Brak aktywności w tym okresie</string>
+ <string name="page_deleted">Strona usunięta</string>
+ <string name="button_trash">Kosz</string>
+ <string name="button_view">Podgląd</string>
+ <string name="trashed">W koszu</string>
+ <string name="my_site_no_sites_view_subtitle">Chciałbyś dodać jedną?</string>
+ <string name="my_site_no_sites_view_title">Nie masz jeszcze żadnych witryn WordPressa</string>
+ <string name="my_site_no_sites_view_drake">Ilustracja</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Nie masz uprawnień do tego blogu</string>
+ <string name="reader_toast_err_follow_blog_not_found">Nie można odnaleźć blogu</string>
+ <string name="undo">Cofnij</string>
+ <string name="tabbar_accessibility_label_my_site">Moja witryna</string>
+ <string name="tabbar_accessibility_label_me">Ja</string>
+ <string name="passcodelock_prompt_message">Wprowadź PIN</string>
+ <string name="editor_toast_changes_saved">Zmiany zapisane</string>
+ <string name="push_auth_expired">Zapytanie wygasło. Zaloguj się na WordPress.com i spróbuj ponownie.</string>
+ <string name="ignore">Ignoruj</string>
+ <string name="stats_insights_best_ever">Najwięcej wyświetleń</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% wyświetleń</string>
+ <string name="stats_insights_most_popular_hour">Najbardziej popularna godzina</string>
+ <string name="stats_insights_most_popular_day">Najpopularniejszy dzień</string>
+ <string name="stats_insights_today">Dzisiejsze statystyki</string>
+ <string name="stats_insights_popular">Najbardziej popularny dzień i godzina</string>
+ <string name="stats_insights_all_time">Wszystkie wpisy, odsłony i odwiedzający</string>
+ <string name="stats_insights">Trendy</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Aby zobaczyć swoje statystyki, zaloguj się na konto na WordPress.com, do którego podłączyłeś Jetpacka.</string>
+ <string name="stats_other_recent_stats_moved_label">Szukasz swoich pozostałych ostatnich statystyk? Przenieśliśmy je do strony "Trendy"</string>
+ <string name="me_disconnect_from_wordpress_com">Rozłącz z WordPress.com</string>
+ <string name="me_btn_login_logout">Zaloguj/Wyloguj</string>
+ <string name="me_connect_to_wordpress_com">Połącz z WordPress.com</string>
+ <string name="account_settings">Ustawienia konta</string>
+ <string name="me_btn_support">Pomoc i wsparcie</string>
+ <string name="site_picker_cant_hide_current_site">"%s" została ukryta ponieważ jest to aktualna witryna</string>
+ <string name="site_picker_create_dotcom">Utwórz witrynę na WordPress.com</string>
+ <string name="site_picker_add_site">Dodaj witrynę</string>
+ <string name="site_picker_edit_visibility">Pokaż/ukryj witryny</string>
+ <string name="site_picker_add_self_hosted">Dodaj witrynę z własnym hostingiem</string>
+ <string name="site_picker_title">Wybierz witrynę</string>
+ <string name="my_site_btn_view_site">Zobacz witrynę</string>
+ <string name="my_site_btn_view_admin">Pokaż Administratora</string>
+ <string name="my_site_btn_switch_site">Przełącz witrynę</string>
+ <string name="my_site_btn_site_settings">Ustawienia</string>
+ <string name="my_site_header_publish">Opublikuj</string>
+ <string name="my_site_btn_blog_posts">Wpisy na blogu</string>
+ <string name="my_site_header_look_and_feel">Wygląd</string>
+ <string name="my_site_header_configuration">Konfiguracja</string>
+ <string name="reader_label_new_posts_subtitle">Stuknij aby je pokazać</string>
+ <string name="notifications_account_required">Zaloguj do WordPress.com aby uzyskać dostęp do powiadomień</string>
+ <string name="stats_unknown_author">Nieznany autor</string>
+ <string name="image_added">Dodano obrazek</string>
+ <string name="signout">Rozłącz</string>
+ <string name="show">Pokaż</string>
+ <string name="hide">Ukryj</string>
+ <string name="select_all">Wybierz wszystko</string>
+ <string name="deselect_all">Odznacz wszystko</string>
+ <string name="sign_out_wpcom_confirm">Odłączenie twojego konta usunie z tego urządzenia wszystkie dane @%s pochodzące z WordPress.com, łącznie z lokalnymi szkicami i lokalnymi zmianami treści.</string>
+ <string name="select_from_new_picker">Wielokrotny wybór nowym narzędziem</string>
+ <string name="no_media_sources">Nie udało się załadować mediów</string>
+ <string name="loading_blog_videos">Ładowanie filmów</string>
+ <string name="error_loading_videos">Błąd podczas ładowania filmów</string>
+ <string name="error_loading_images">Błąd podczas ładowania zdjęć</string>
+ <string name="loading_blog_images">Ładowanie zdjęć</string>
+ <string name="error_loading_blog_videos">Nie udało się pobrać filmów</string>
+ <string name="error_loading_blog_images">Nie udało się pobrać zdjęć</string>
+ <string name="no_device_images">Brak zdjęć</string>
+ <string name="no_blog_videos">Brak filmów</string>
+ <string name="no_blog_images">Brak zdjęć</string>
+ <string name="no_device_videos">Brak filmów</string>
+ <string name="stats_generic_error">Nie udało się załadować wymaganych statystyk</string>
+ <string name="no_media">Brak mediów</string>
+ <string name="loading_images">Ładowanie zdjęć</string>
+ <string name="loading_videos">Ładowanie filmów</string>
+ <string name="auth_required">Zaloguj się ponownie, by kontynuować.</string>
+ <string name="sign_in_jetpack">Zaloguj się do swojego konta na WordPress.com, by włączyć Jetpack.</string>
+ <string name="two_step_footer_button">Wyślij kod przez SMS</string>
+ <string name="two_step_sms_sent">Sprawdź swoje SMSy w poszukiwaniu kodu weryfikacyjnego.</string>
+ <string name="two_step_footer_label">Wprowadź kod z aplikacji do uwierzytelniania.</string>
+ <string name="device">Urządzenie</string>
+ <string name="media_picker_title">Wybierz media</string>
+ <string name="add_to_post">Dodaj do wpisu</string>
+ <string name="tab_title_device_images">Zdjęcia z urządzenia</string>
+ <string name="tab_title_device_videos">Filmy z urządzenia</string>
+ <string name="take_video">Nagraj film</string>
+ <string name="tab_title_site_videos">Filmy z witryny</string>
+ <string name="tab_title_site_images">Zdjęcia z witryny</string>
+ <string name="verify">Weryfikuj</string>
+ <string name="error_publish_no_network">Publikacja nie jest możliwa bez połączenia z Internetem. Treść zapisano jako szkic.</string>
+ <string name="editor_toast_invalid_path">Niewłaściwa ścieżka do pliku</string>
+ <string name="invalid_verification_code">Niewłaściwy kod weryfikacyjny</string>
+ <string name="verification_code">Kod weryfikacyjny</string>
+ <string name="take_photo">Zrób zdjęcie</string>
+ <string name="language">Język</string>
+ <string name="media_details_label_file_name">Nazwa pliku</string>
+ <string name="media_details_label_file_type">Typ pliku</string>
+ <string name="media_fetching">Ładowanie mediów...</string>
+ <string name="posts_fetching">Ładowanie wpisów...</string>
+ <string name="toast_err_post_uploading">Nie można otworzyć wpisu, gdy jest on przesyłany</string>
+ <string name="pages_fetching">Ładowanie stron...</string>
+ <string name="comments_fetching">Ładowanie komentarzy...</string>
+ <string name="stats_view_authors">Autorzy</string>
+ <string name="stats_view_search_terms">Wyrażenia</string>
+ <string name="stats_entry_search_terms">Wyrażenie</string>
+ <string name="stats_empty_search_terms">Nie zarejestrowano żadnych wyszukiwanych wyrażeń.</string>
+ <string name="stats_empty_search_terms_desc">Dowiedz się więcej na temat ruchu na Twojej stronie przez przeglądanie wyrażeń, których szukali, by trafić na Twoją witrynę.</string>
+ <string name="stats_followers_total_wpcom_paged">Wyświetlanie od %1$d do %2$d z %3$s obserwujących przez WordPress.com</string>
+ <string name="stats_search_terms_unknown_search_terms">Nieznane kryteria wyszukiwania</string>
+ <string name="error_notification_open">Nie udało się otworzyć powiadomienia</string>
+ <string name="stats_followers_total_email_paged">Wyświetlanie od %1$d do %2$d z %3$s obserwujących przez email</string>
+ <string name="publisher">Wydawca:</string>
+ <string name="reader_empty_posts_request_failed">Nie udało się pobrać wpisów</string>
+ <string name="stats_months_and_years">Miesiące i lata</string>
+ <string name="reader_label_new_posts">Nowe wpisy</string>
+ <string name="stats_total">Suma</string>
+ <string name="logs_copied_to_clipboard">Dziennik aplikacji został skopiowany do schowka</string>
+ <string name="error_copy_to_clipboard">Wystąpił błąd podczas kopiowania tekstu do schowka</string>
+ <string name="stats_recent_weeks">Ostatnie tygodnie</string>
+ <string name="stats_average_per_day">Średnia na dzień</string>
+ <string name="reader_empty_posts_in_blog">Ten blog jest pusty</string>
+ <string name="stats_period">Okres</string>
+ <string name="stats_overall">Ogólnie</string>
+ <string name="post_uploading">Dodawanie</string>
+ <string name="reader_page_recommended_blogs">Blogi, które mogą ciebie zainteresować</string>
+ <string name="stats_views">Wyświetleń</string>
+ <string name="stats_visitors">Odwiedzających</string>
+ <string name="stats_pagination_label">Strona %1$s z %2$s</string>
+ <string name="stats_timeframe_years">Lata</string>
+ <string name="stats_view_countries">Kraje</string>
+ <string name="stats_likes">Polubienia</string>
+ <string name="stats_view_followers">Śledzący</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_top_posts_and_pages">Wpisy i Strony</string>
+ <string name="stats_view_videos">Filmy</string>
+ <string name="stats_entry_clicks_link">Odnośnik</string>
+ <string name="stats_totals_publicize">Obserwujących</string>
+ <string name="stats_entry_followers">Obserwujący</string>
+ <string name="stats_entry_publicize">Usługa</string>
+ <string name="stats_entry_video_plays">Film</string>
+ <string name="stats_empty_geoviews_desc">Przejrzyj listę by dowiedzieć się, które kraje lub regiony przynoszą Twojej stronie najwięcej ruchu.</string>
+ <string name="stats_empty_geoviews">Nie zarejestrowano żadnych krajów</string>
+ <string name="stats_empty_top_posts_title">Nie zarejestrowano żadnych wyświetleń stron lub wpisów</string>
+ <string name="stats_empty_top_posts_desc">Dowiedz się, jaka treść na Twojej witrynie jest najczęściej czytana oraz sprawdź, jak konkretne wpisy i strony są odwiedzane w przedziałach czasowych.</string>
+ <string name="stats_empty_referrers_title">Nie zarejestrowano żadnych źródeł ruchu</string>
+ <string name="stats_empty_clicks_title">Nie zarejestrowano żadnych kliknięć</string>
+ <string name="stats_empty_referrers_desc">Dowiedz się więcej na temat widoczności swojej witryny dzięki sprawdzeniu, jakie strony i wyszukiwarki kierują na nią większość ruchu.</string>
+ <string name="stats_empty_clicks_desc">Jeśli Twoja treść zawiera odnośniki do innych witryn, zobaczysz, które z nich są najczęściej otwierane.</string>
+ <string name="stats_empty_tags_and_categories">Brak wyświetleń otagowanych wpisów lub stron</string>
+ <string name="stats_empty_top_authors_desc">Śledź liczby wyświetleń wpisów każdego autora i przyjrzyj się dokładnie, by poznać najpopularniejszą treść danego autora.</string>
+ <string name="stats_followers_months">%1$d miesięcy</string>
+ <string name="stats_followers_total_wpcom">Liczba śledzących z WordPress.com: %1$s</string>
+ <string name="stats_comments_by_posts_and_pages">Wg Wpisów i Stron</string>
+ <string name="stats_comments_by_authors">Wg Autorów</string>
+ <string name="stats_followers_total_email">Śledzących przez e-mail: %1$s</string>
+ <string name="stats_empty_followers_desc">Śledź ogólną liczbę obserwujących oraz daty, od których są Twoimi czytelnikami.</string>
+ <string name="stats_empty_publicize_desc">Śledź swoich czytelników z różnych serwisów społecznościowych, którzy używają Publicize.</string>
+ <string name="stats_empty_publicize">Brak śledzących przez Publicize</string>
+ <string name="stats_empty_video">Brak obejrzeń filmów</string>
+ <string name="stats_empty_video_desc">Jeśli dodajesz swoje filmy przy pomocy VideoPress, możesz zobaczyć ich liczby wyświetleń.</string>
+ <string name="stats_empty_comments_desc">Jeśli zezwalasz na zamieszczanie komentarzy na swojej stronie, możesz poznać najczęściej komentujących oraz najczęściej komentowaną treść, w oparciu o 1000 ostatnich komentarzy.</string>
+ <string name="stats_empty_tags_and_categories_desc">Poznaj najbardziej popularne tematy na swojej stronie, wyłonione w oparciu o najpopularniejsze wpisy z zeszłego tygodnia.</string>
+ <string name="stats_empty_followers">Brak obserwujących</string>
+ <string name="stats_other_recent_stats_label">Inne najnowsze statystyki</string>
+ <string name="themes_fetching">Pobieranie motywów...</string>
+ <string name="stats_totals_followers">Od</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_days">%1$d dni</string>
+ <string name="stats_followers_a_minute_ago">minutę temu</string>
+ <string name="stats_followers_seconds_ago">sekundy temu</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_a_day">Dzień</string>
+ <string name="stats_followers_hours">%1$d godzin</string>
+ <string name="stats_followers_an_hour_ago">godzinę temu</string>
+ <string name="stats_followers_years">%1$d lat</string>
+ <string name="stats_followers_a_month">Miesiąc</string>
+ <string name="stats_followers_a_year">Rok</string>
+ <string name="stats_view">Zobacz</string>
+ <string name="stats_followers_minutes">%1$d minut</string>
+ <string name="stats_view_all">Zobacz wszystkie</string>
+ <string name="stats_for">Statystyki dla %s</string>
+ <string name="stats_comments_total_comments_followers">Liczba wpisów skomentowanych przez obserwujących: %1$s</string>
+ <string name="ssl_certificate_details">Szczegóły</string>
+ <string name="delete_sure_post">Usuń ten wpis</string>
+ <string name="delete_sure">Usuń ten szkic</string>
+ <string name="delete_sure_page">Usuń tę stronę</string>
+ <string name="confirm_delete_multi_media">Usunąć zaznaczone elementy?</string>
+ <string name="confirm_delete_media">Usunąć zaznaczony element?</string>
+ <string name="cab_selected">Liczba zaznaczonych: %d</string>
+ <string name="media_gallery_date_range">Wyświetlanie mediów z okresu od %1$s do %2$s</string>
+ <string name="sure_to_remove_account">Usunąć tego bloga?</string>
+ <string name="reader_empty_comments">Nie ma jeszcze komentarzy</string>
+ <string name="reader_empty_posts_in_tag">Brak wpisów z tym tagiem</string>
+ <string name="reader_label_view_original">Zobacz oryginalny artykuł</string>
+ <string name="reader_label_like">Lubię to</string>
+ <string name="reader_label_liked_by">Polubiony przez</string>
+ <string name="reader_label_comment_count_single">Jeden komentarz</string>
+ <string name="more">Więcej</string>
+ <string name="create_new_blog_wpcom">Stwórz blog na WordPress.com</string>
+ <string name="reader_title_photo_viewer">%1$d z %2$d</string>
+ <string name="error_publish_empty_post">Nie można opublikować pustego wpisu</string>
+ <string name="reader_label_comment_count_multi">Liczba komentarzy: %,d</string>
+ <string name="reader_label_comments_closed">Możliwość komentowania jest wyłączona</string>
+ <string name="signing_out">Wylogowywanie się...</string>
+ <string name="reader_empty_posts_liked">Nie lubisz jeszcze żadnych wpisów</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Przejrzyj nasze FAQ</string>
+ <string name="nux_help_description">Odwiedź centrum pomocy, aby uzyskać odpowiedzi na najczęściej zadawane pytania lub odwiedź forum, aby zadać nowe</string>
+ <string name="older_last_week">Starsze niż tydzień</string>
+ <string name="older_two_days">Starsze niż 2 dni</string>
+ <string name="older_month">Starsze niż miesiąc</string>
+ <string name="agree_terms_of_service">Tworząc konto wyrażasz zgodę na postanowienia fascynujących %1$sWarunków korzystania z usługi%2$s</string>
+ <string name="new_blog_wpcom_created">Stworzono blog na WordPress.com</string>
+ <string name="reader_label_comments_on">Komentarze do</string>
+ <string name="error_refresh_unauthorized_posts">Nie masz uprawnień, by oglądać lub edytować wpisy</string>
+ <string name="error_refresh_unauthorized_pages">Nie masz uprawnień, by oglądać lub edytować strony</string>
+ <string name="error_refresh_unauthorized_comments">Nie masz uprawnień, by oglądać lub edytować komentarze</string>
+ <string name="stats_no_blog">Nie udało się załadować statystyk dla wskazanego bloga</string>
+ <string name="select_a_blog">Wybierz witrynę WordPressa</string>
+ <string name="sending_content">Wysyłanie %s treści</string>
+ <string name="uploading_total">Wysyłanie %1$d z %2$d</string>
+ <string name="comment">Komentarz</string>
+ <string name="comment_trashed">Komentarz przeniesiono do kosza</string>
+ <string name="posts_empty_list">Brak wpisów. Może jakiś opublikujesz?</string>
+ <string name="comment_reply_to_user">Odpowiedz na komentarz %s</string>
+ <string name="pages_empty_list">Brak stron. Może jakąś opublikujesz?</string>
+ <string name="media_empty_list_custom_date">Brak mediów w tym przedziale czasowym</string>
+ <string name="posting_post">Publikowanie "%s"</string>
+ <string name="reader_empty_followed_blogs_title">Nie obserwujesz jeszcze żadnych blogów</string>
+ <string name="mnu_comment_liked">Polubiony</string>
+ <string name="reader_menu_block_blog">Zablokuj ten blog</string>
+ <string name="reader_toast_err_block_blog">Nie można zablokować tego blogu</string>
+ <string name="reader_toast_blog_blocked">Wpisy z tego blogu nie będą już widoczne</string>
+ <string name="reader_toast_err_generic">Nie można wykonać tego działania</string>
+ <string name="contact_us">Kontakt</string>
+ <string name="hs__new_conversation_header">Chat pomocy technicznej</string>
+ <string name="hs__conversation_header">Chat pomocy technicznej</string>
+ <string name="hs__username_blank_error">Wpisz poprawną nazwę użytkownika</string>
+ <string name="hs__invalid_email_error">Wpisz poprawny adres email</string>
+ <string name="hs__conversation_detail_error">Opisz problem, który zaobserwowano</string>
+ <string name="add_location">Dodaj lokalizację</string>
+ <string name="current_location">Obecna lokalizacja</string>
+ <string name="search_location">Szukaj</string>
+ <string name="edit_location">Edytuj</string>
+ <string name="search_current_location">Zlokalizuj</string>
+ <string name="preference_send_usage_stats_summary">Automatycznie wysyłaj statystyki użytkowania, by pomóc nam ulepszać WordPressa dla Androida</string>
+ <string name="preference_send_usage_stats">Wysyłaj statystyki</string>
+ <string name="update_verb">Zaktualizuj</string>
+ <string name="schedule_verb">Zaplanuj</string>
+ <string name="reader_title_subs">Tagi i blogi</string>
+ <string name="reader_page_followed_tags">Obserwowane tagi</string>
+ <string name="reader_toast_err_already_follow_blog">Już śledzisz ten blog</string>
+ <string name="reader_label_tag_preview">Wpisy otagowane %s</string>
+ <string name="reader_empty_recommended_blogs">Brak polecanych blogów</string>
+ <string name="reader_toast_err_get_blog_info">Nie udało się pobrać informacji o blogu</string>
+ <string name="reader_toast_err_follow_blog">Nie udało się śledzić tego bloga</string>
+ <string name="reader_label_followed_blog">Blog obserwowany</string>
+ <string name="reader_toast_err_unfollow_blog">Nie udało się wyłączyć śledzenia tego blogu</string>
+ <string name="reader_title_tag_preview">Tag Czytnika</string>
+ <string name="reader_page_followed_blogs">Obserwowane blogi</string>
+ <string name="reader_title_blog_preview">Blog Czytnika</string>
+ <string name="reader_hint_add_tag_or_url">Wpisz adres lub tag aby obserwować</string>
+ <string name="saving">Zapisywanie...</string>
+ <string name="media_empty_list">Brak mediów</string>
+ <string name="ptr_tip_message">Porada: przeciągnij w dół aby odświeżyć</string>
+ <string name="help">Pomoc</string>
+ <string name="forgot_password">Nie pamiętasz hasła?</string>
+ <string name="forums">Fora</string>
+ <string name="help_center">Centrum pomocy</string>
+ <string name="ssl_certificate_error">Niepoprawny certyfikat SSL</string>
+ <string name="ssl_certificate_ask_trust">Jeśli zazwyczaj łączysz się z tą stroną bez problemów, ten błąd może oznaczać, że ktoś próbuje się pod nią podszyć i nie powinieneś kontynuować. Czy mimo to chcesz zaufać temu certyfikatowi?</string>
+ <string name="out_of_memory">Brak pamięci</string>
+ <string name="theme_auth_error_message">Upewnij się, że masz uprawnienia do zmiany motywu</string>
+ <string name="comments_empty_list">Brak komentarzy</string>
+ <string name="mnu_comment_unspam">To nie jest spam</string>
+ <string name="adding_cat_failed">Nie udało się dodać kategorii</string>
+ <string name="adding_cat_success">Pomyślnie dodano kategorię</string>
+ <string name="cat_name_required">Nazwa kategorii jest wymagana</string>
+ <string name="category_automatically_renamed">Nazwa kategorii %1$s jest nieprawidłowa. Została zmieniona na %2$s.</string>
+ <string name="no_account">Brak konta WordPress, dodaj konto i spróbuj ponownie</string>
+ <string name="theme_set_failed">Nie udało się włączyć motywu</string>
+ <string name="stats_empty_comments">Brak komentarzy</string>
+ <string name="stats_bar_graph_empty">Statystyki nie są dostępne</string>
+ <string name="notifications_empty_list">Brak powiadomień</string>
+ <string name="error_delete_post">Wystąpił błąd podczas usuwania %s</string>
+ <string name="error_refresh_posts">W tej chwili nie można odświeżyć wpisów</string>
+ <string name="passcode_wrong_passcode">Niewłaściwy PIN</string>
+ <string name="invalid_email_message">Twój adres email nie jest poprawny</string>
+ <string name="invalid_password_message">Hasło musi mieć co najmniej 4 znaki</string>
+ <string name="invalid_username_too_short">Nazwa użytkownika musi być dłuższa niż 4 znaki</string>
+ <string name="invalid_username_too_long">Nazwa użytkownika musi być krótsza niż 61 znaków</string>
+ <string name="username_must_include_letters">Nazwa użytkownika musi zawierać przynajmniej jedną literę (a-z)</string>
+ <string name="email_invalid">Wpisz poprawny adres email</string>
+ <string name="error_generic">Wystąpił błąd</string>
+ <string name="username_only_lowercase_letters_and_numbers">Nazwa użytkownika może zawierać tylko małe litery (a-z) oraz cyfry</string>
+ <string name="username_required">Wpisz nazwę użytkownika</string>
+ <string name="username_must_be_at_least_four_characters">Nazwa użytkownika musi zawierać conajmniej 4 znaki</string>
+ <string name="username_contains_invalid_characters">Nazwa użytkownika nie może zawierać znaku “_”</string>
+ <string name="username_exists">Ta nazwa użytkownika już istnieje</string>
+ <string name="email_exists">Ten adres email już został użyty</string>
+ <string name="username_reserved_but_may_be_available">Ta nazwa użytkownika jest chwilowo zarezerowana, ale może być dostępna w ciągu kilku dni</string>
+ <string name="blog_name_required">Wpisz adres strony</string>
+ <string name="no_network_message">Brak dostępnej sieci</string>
+ <string name="no_site_error">Nie można nawiązać połączenia ze stroną WordPressa</string>
+ <string name="blog_not_found">Wystąpił błąd podczas dostępu do tego bloga</string>
+ <string name="error_refresh_pages">Strony nie mogły zostać odświeżone w tej chwili</string>
+ <string name="error_refresh_notifications">Powiadomienia nie mogły zostać odświeżone w tej chwili</string>
+ <string name="error_refresh_comments">Komentarze nie mogły zostać odświeżone w tej chwili</string>
+ <string name="blog_name_reserved">Ta strona jest zajęta</string>
+ <string name="username_not_allowed">Niedozwolona nazwa użytkownika</string>
+ <string name="blog_name_not_allowed">Ten adres strony nie jest dozwolony</string>
+ <string name="email_not_allowed">Ten adres email jest niedozwolony</string>
+ <string name="error_moderate_comment">Przy próbie moderacji wystąpił błąd.</string>
+ <string name="error_edit_comment">Przy edytowaniu komentarza wystąpił błąd.</string>
+ <string name="error_upload">Wystąpił błąd podczas wgrywania %s</string>
+ <string name="blog_name_reserved_but_may_be_available">Ta strona jest aktualnie zarezerwowana, ale może być dostępna za kilka dni</string>
+ <string name="blog_name_contains_invalid_characters">Adres strony nie może zawierać znaku “_”</string>
+ <string name="blog_name_cant_be_used">Nie możesz użyć tego adresu</string>
+ <string name="wait_until_upload_completes">Poczekaj, aż pliki zostaną wysłane</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Adres strony może składać się tylko z małych liter (a-z) i cyfr</string>
+ <string name="error_refresh_stats">Statystyki chwilowo nie mogły zostać wyświetlone</string>
+ <string name="blog_name_exists">Witryna o takiej nazwie już istnieje</string>
+ <string name="nux_cannot_log_in">Nie możemy zalogować cię na twoje konto</string>
+ <string name="error_downloading_image">Błąd pobierania obrazka</string>
+ <string name="error_load_comment">Nie można było wczytać komentarza</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Adres witryny musi być krótszy niż 64 znaki</string>
+ <string name="blog_name_must_be_at_least_four_characters">Adres witryny musi być dłuższy niż 4 znaki</string>
+ <string name="gallery_error">Nie udało się pobrać medium</string>
+ <string name="theme_fetch_failed">Nie udało się pobrać motywów</string>
+ <string name="reply_failed">Wystąpił błąd przy próbie odpowiedzi</string>
+ <string name="sdcard_message">Do wysyłania mediów niezbędne jest podłączenie karty SD</string>
+ <string name="username_or_password_incorrect">Niepoprawna nazwa użytkownika lub hasło</string>
+ <string name="could_not_remove_account">Nie można usunąć bloga</string>
+ <string name="invalid_url_message">Sprawdź, czy wprowadzony adres URL jest poprawny</string>
+ <string name="trash">Usuń</string>
+ <string name="mnu_comment_unapprove">Odrzuć</string>
+ <string name="blog_title_invalid">Nieprawidłowy tytuł strony</string>
+ <string name="blog_name_invalid">Nieprawidłowy adres strony</string>
+ <string name="blog_name_must_include_letters">Adres strony musi zawierać przynajmniej jedną literę (a-z)</string>
+ <string name="email_reserved">Ten adres email został już użyty. Sprawdź czy nie masz w skrzynce odbiorczej maila aktywacyjnego. Jeśli go nie aktywujesz możesz spróbować znów za klika dni.</string>
+ <string name="required_field">Wymagane pole</string>
+ <string name="email_hint">Adres email</string>
+ <string name="jetpack_message_not_admin">Wtyczka Jetpack jest wymagana dla statystyk. Skontaktuj się z administratorem strony.</string>
+ <string name="post_format">Format wpisu</string>
+ <string name="new_post">Nowy wpis</string>
+ <string name="new_media">Nowe media</string>
+ <string name="view_site">Zobacz stronę</string>
+ <string name="privacy_policy">Polityka prywatności</string>
+ <string name="local_changes">Zmiany lokalne</string>
+ <string name="image_settings">Ustawienia obrazka</string>
+ <string name="add_account_blog_url">Adres blogu</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="error_blog_hidden">Ten blog jest ukryty i nie mógł zostać wczytany. Włącz go ponownie w ustawieniach i spróbuj ponownie.</string>
+ <string name="fatal_db_error">Wystąpił błąd podczas tworzenia bazy danych. Spróbuj przeinstalować aplikacje.</string>
+ <string name="view_in_browser">Zobacz w przeglądarce</string>
+ <string name="add_new_category">Dodaj nową kategorię</string>
+ <string name="category_name">Nazwa kategorii</string>
+ <string name="category_slug">Uproszczona nazwa kategorii (opcjonalnie)</string>
+ <string name="category_desc">Opis kategorii (opcjonalnie)</string>
+ <string name="category_parent">Rodzic kategorii (opcjonalnie)</string>
+ <string name="share_action_post">Nowy wpis</string>
+ <string name="share_action_media">Biblioteka mediów</string>
+ <string name="account_details">Szczegóły konta</string>
+ <string name="cancel_edit">Anuluj edycję</string>
+ <string name="add_comment">Dodaj komentarz</string>
+ <string name="connection_error">Błąd połączenia</string>
+ <string name="media_gallery_settings_title">Ustawienia galerii</string>
+ <string name="media_gallery_edit">Edytuj galerię</string>
+ <string name="local_draft">Szkic lokalny</string>
+ <string name="page_settings">Ustawienia strony</string>
+ <string name="post_settings">Ustawienia wpisu</string>
+ <string name="delete_post">Usuń wpis</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="edit_comment">Edytuj komentarz</string>
+ <string name="delete_page">Usuń stronę</string>
+ <string name="hint_comment_content">Komentarz</string>
+ <string name="saving_changes">Zapisywanie zmian</string>
+ <string name="delete_draft">Usuń szkic</string>
+ <string name="toast_comment_unedited">Komentarz nie został zmieniony</string>
+ <string name="reader_share_link">Udostępnij link</string>
+ <string name="learn_more">Dowiedz się więcej</string>
+ <string name="upload_failed">Wysyłanie nie powiodło się</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="mnu_comment_approve">Zatwierdź</string>
+ <string name="page_not_published">Strona nie jest opublikowana</string>
+ <string name="themes_live_preview">Podgląd na żywo</string>
+ <string name="theme_current_theme">Aktualny motyw</string>
+ <string name="author_name">Autor</string>
+ <string name="author_url">Adres strony autora</string>
+ <string name="media_gallery_image_order">Kolejność obrazów</string>
+ <string name="media_gallery_type_thumbnail_grid">Siatka miniatur</string>
+ <string name="media_error_no_permission">Nie masz uprawnień do przeglądania biblioteki mediów</string>
+ <string name="cannot_delete_multi_media_items">Niektóre media nie mogły zostać w tej chwili usunięte. Spróbuj ponownie później. \n</string>
+ <string name="scaled_image_error">Wpisz poprawną wartość skalowanej szerokości.</string>
+ <string name="content_required">Wymagany jest komentarz</string>
+ <string name="file_error_create">Nie udało się utworzyć tymczasowego pliku do wgrania. Upewnij się, że na Twoim urządzeniu jest wystarczająco dużo wolnego miejsca.</string>
+ <string name="open_source_licenses">Licencje otwartego oprogramowania</string>
+ <string name="pending_review">Oczekuje na przegląd</string>
+ <string name="http_credentials">Logowanie HTTP (opcjonalne)</string>
+ <string name="http_authorization_required">Wymagana autoryzacja</string>
+ <string name="reader_toast_err_add_tag">Nie udało się dodać tagu</string>
+ <string name="reader_toast_err_remove_tag">Nie udało się usunąć tagu</string>
+ <string name="site_address">Adres strony na Twoim hostingu</string>
+ <string name="email_cant_be_used_to_signup">Nie możesz użyć tego adresu email do rejestracji, ponieważ jego serwer blokuje naszą korespondencję. Użyj innego usługodawcy emailów.</string>
+ <string name="reader_title_applog">Dziennik aplikacji</string>
+ <string name="trash_no">Nie usuwaj</string>
+ <string name="dlg_spamming_comments">Uznane za spam</string>
+ <string name="dlg_approving_comments">Zatwierdzanie</string>
+ <string name="comment_status_unapproved">Oczekujące</string>
+ <string name="theme_premium_theme">Motyw Premium</string>
+ <string name="sure_to_cancel_edit_comment">Anulować edytowanie tego komentarza?</string>
+ <string name="dlg_trashing_comments">Wysyłanie do kosza</string>
+ <string name="mnu_comment_trash">Kosz</string>
+ <string name="create_a_link">Stwórz link</string>
+ <string name="media_gallery_num_columns">Liczba kolumn</string>
+ <string name="edit_post">Edytuj wpis</string>
+ <string name="author_email">E-mail autora</string>
+ <string name="select_categories">Wybierz kategorie</string>
+ <string name="trash_yes">Wyrzuć</string>
+ <string name="dlg_confirm_trash_comments">Wyrzucić do kosza?</string>
+ <string name="dlg_unapproving_comments">Cofanie zatwierdzenia</string>
+ <string name="preview_page">Podejrzyj stronę</string>
+ <string name="preview_post">Podejrzyj wpis</string>
+ <string name="comment_added">Komentarz został dodany</string>
+ <string name="post_not_published">Wpis nie jest opublikowany</string>
+ <string name="location_not_found">Nieznana lokalizacja</string>
+ <string name="xmlrpc_error">Nie udało się nawiązać połączenia. Wprowadź pełną ścieżkę do pliku xmlrpc.php Twojej strony i spróbuj ponownie.</string>
+ <string name="post_not_found">Wykryto błąd podczas ładowania wpisu. Odśwież listę wpisów i spróbuj ponownie.</string>
+ <string name="horizontal_alignment">Wyrównanie w poziomie</string>
+ <string name="file_not_found">Nie znaleziono pliku do wysłania. Został usunięty lub przeniesiony?</string>
+ <string name="notifications_empty_all">Brak powiadomień... na razie. </string>
+ <string name="invalid_site_url_message">Sprawdź, czy wpisany adres URL jest prawidłowy</string>
+ <string name="remove_account">Usuń bloga</string>
+ <string name="blog_removed_successfully">Blog usunięto</string>
+ <string name="link_enter_url_text">Tekst odnośnika (opcjonalny)</string>
+ <string name="comment_status_trash">W koszu</string>
+ <string name="comment_status_approved">Zatwierdzony</string>
+ <string name="deleting_page">Kasowanie strony</string>
+ <string name="deleting_post">Kasowanie wpisu</string>
+ <string name="share_url_post">Udostępnij wpis</string>
+ <string name="share_url_page">Udostępnij stronę</string>
+ <string name="share_link">Udostępnij odnośnik</string>
+ <string name="creating_your_account">Tworzenie twojego konta</string>
+ <string name="creating_your_site">Tworzenie twojej strony</string>
+ <string name="reader_empty_posts_in_tag_updating">Ładowanie wpisów...</string>
+ <string name="error_refresh_media">Coś poszło nie tak podczas odświeżania biblioteki mediów. Spróbuj ponownie później.</string>
+ <string name="reader_toast_err_get_comment">Nie można pobrać tego komentarza</string>
+ <string name="reader_label_reply">Odpowiedz</string>
+ <string name="video">Wideo</string>
+ <string name="download">Pobieranie mediów</string>
+ <string name="comment_spammed">Komentarz został oznaczony jako spam</string>
+ <string name="cant_share_no_visible_blog">Nie możesz udostępnić do WordPressa bez widocznego bloga</string>
+ <string name="reader_likes_you_and_multi">Ty i %,d innych lubią to</string>
+ <string name="reader_likes_multi">%,d osób lubi to</string>
+ <string name="select_time">Wybierz czas</string>
+ <string name="reader_likes_you_and_one">Ty i jedna inna osoba lubicie to</string>
+ <string name="reader_toast_err_get_post">Nie można pobrać tego wpisu</string>
+ <string name="select_date">Wybierz datę</string>
+ <string name="pick_photo">Wybierz zdjęcie</string>
+ <string name="account_two_step_auth_enabled">To konto ma włączoną dwustopniową weryfikację. Odwiedź ustawienia bezpieczeństwa na WordPress.com i wygeneruj hasło aplikacji.</string>
+ <string name="pick_video">Wybierz wideo</string>
+ <string name="validating_user_data">Sprawdzanie danych użytkownika</string>
+ <string name="validating_site_data">Sprawdzanie danych strony</string>
+ <string name="reader_empty_followed_blogs_description">Nie martw się - stuknij ikonę w prawym górnym rogu, aby rozpocząć przeglądanie!</string>
+ <string name="password_invalid">Potrzebujesz bardziej bezpiecznego hasła. Upewnij się, że użyłeś 7 lub więcej znaków, wielkich i małych liter, liczb albo znaków specjalnych.</string>
+ <string name="nux_tap_continue">Kontynuuj</string>
+ <string name="nux_welcome_create_account">Utwórz konto</string>
+ <string name="nux_add_selfhosted_blog">Dodaj samodzielnie hostowaną stronę</string>
+ <string name="nux_oops_not_selfhosted_blog">Zaloguj się do WordPress.com</string>
+ <string name="signing_in">Logowanie...</string>
+ <string name="media_add_popup_title">Dodaj do biblioteki mediów</string>
+ <string name="media_add_new_media_gallery">Tworzenie galerii</string>
+ <string name="empty_list_default">Lista jest pusta</string>
+ <string name="jetpack_not_found">Wtyczka Jetpack nie została znaleziona</string>
+ <string name="reader_untitled_post">(Brak tytułu)</string>
+ <string name="jetpack_message">Wtyczka Jetpack jest wymagana, aby uzyskać dostęp do statystyk. Zainstalować wtyczkę?</string>
+ <string name="reader_share_subject">Udostępnione przez %s</string>
+ <string name="reader_btn_share">Udostępnij</string>
+ <string name="reader_btn_follow">Śledź</string>
+ <string name="reader_btn_unfollow">Śledzone</string>
+ <string name="reader_label_removed_tag">Usunięto %s</string>
+ <string name="reader_label_added_tag">Dodano %s</string>
+ <string name="button_next">Następny</string>
+ <string name="username_invalid">Nieprawidłowa nazwa użytkownika</string>
+ <string name="connecting_wpcom">Połączono z WordPress.com</string>
+ <string name="reader_likes_only_you">Ty to lubisz</string>
+ <string name="nux_tutorial_get_started_title">Rozpocznij!</string>
+ <string name="reader_toast_err_view_image">Nie można zobaczyć obrazu</string>
+ <string name="reader_toast_err_url_intent">Nie można otworzyć %s</string>
+ <string name="reader_empty_followed_tags">Nie śledzisz żadnych tagów</string>
+ <string name="limit_reached">Wykorzystano limit. Spróbuj ponownie za 1 minutę. Podjęcie ponownej próby przed upływem podanego czasu spowoduj jedynie wydłużenie czasu oczekiwania. Jeśli uważasz, że jest to błąd, skontaktuj się z nami.</string>
+ <string name="reader_likes_one">Jedna osoba lubi to</string>
+ <string name="reader_toast_err_comment_failed">Nie udało się opublikować twojego komentarza</string>
+ <string name="reader_toast_err_tag_exists">Śledzisz już ten tag</string>
+ <string name="reader_toast_err_tag_invalid">To nie jest poprawny tag</string>
+ <string name="reader_toast_err_share_intent">Nie udało się udostępnić</string>
+ <string name="create_account_wpcom">Stwórz konto na WordPress.com</string>
+ <string name="reader_hint_comment_on_comment">Odpowiedz na komentarz...</string>
+ <string name="select_from_media_library">Wybierz z biblioteki mediów</string>
+ <string name="themes">Motywy</string>
+ <string name="all">Wszystkie</string>
+ <string name="unattached">Niezałączone</string>
+ <string name="media_add_popup_capture_video">Nagraj film</string>
+ <string name="media_gallery_image_order_random">Losowa</string>
+ <string name="media_gallery_type">Typ</string>
+ <string name="media_gallery_type_tiled">Kafelki</string>
+ <string name="media_gallery_type_squares">Kwadraty</string>
+ <string name="media_gallery_type_slideshow">Pokaz slajdów</string>
+ <string name="media_edit_title_text">Tytuł</string>
+ <string name="media_edit_description_text">Opis</string>
+ <string name="media_edit_title_hint">Wprowadź tytuł</string>
+ <string name="media_edit_caption_hint">Wprowadź etykietę</string>
+ <string name="media_edit_description_hint">Wprowadź opis</string>
+ <string name="media_edit_success">Zaktualizowano</string>
+ <string name="media_edit_failure">Nie udało się zaktualizować</string>
+ <string name="themes_details_label">Szczegóły</string>
+ <string name="themes_features_label">Funkcje</string>
+ <string name="share_action">Udostępnij</string>
+ <string name="stats">Statystyki</string>
+ <string name="stats_view_visitors_and_views">Odwiedzający i wyświetlenia</string>
+ <string name="stats_view_clicks">Kliknięcia</string>
+ <string name="stats_view_tags_and_categories">Tagi i kategorie</string>
+ <string name="stats_timeframe_today">Dzisiaj</string>
+ <string name="stats_timeframe_yesterday">Wczoraj</string>
+ <string name="stats_timeframe_days">Dni</string>
+ <string name="stats_timeframe_weeks">Tygodnie</string>
+ <string name="stats_timeframe_months">Miesiące</string>
+ <string name="stats_entry_country">Kraj</string>
+ <string name="stats_entry_posts_and_pages">Tytuł</string>
+ <string name="stats_entry_tags_and_categories">Temat</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_totals_views">Wizyty</string>
+ <string name="stats_totals_clicks">Kliknięcia</string>
+ <string name="stats_totals_plays">Odtworzenia</string>
+ <string name="passcode_enter_passcode">Podaj swój PIN</string>
+ <string name="passcode_enter_old_passcode">Podaj swój stary PIN</string>
+ <string name="passcode_re_enter_passcode">Powtórz swój PIN</string>
+ <string name="passcode_change_passcode">Zmień PIN</string>
+ <string name="passcode_set">Ustaw PIN</string>
+ <string name="theme_set_success">Pomyślnie ustawiono motyw!</string>
+ <string name="theme_auth_error_title">Nie udało się pobrać motywów.</string>
+ <string name="custom_date">Własna data</string>
+ <string name="media_add_popup_capture_photo">Wykonaj zdjęcie</string>
+ <string name="media_gallery_image_order_reverse">Odwrócona</string>
+ <string name="media_gallery_type_circles">Kręgi</string>
+ <string name="theme_activating_button">Aktywowanie</string>
+ <string name="theme_activate_button">Aktywuj</string>
+ <string name="share_action_title">Dodaj do...</string>
+ <string name="post_excerpt">Fragment</string>
+ <string name="stats_view_referrers">Polecający</string>
+ <string name="stats_entry_referrers">Polecający</string>
+ <string name="passcode_manage">Zarządzaj blokadą PIN</string>
+ <string name="passcode_preference_title">Blokada PIN</string>
+ <string name="passcode_turn_off">Wyłącz blokadę PIN</string>
+ <string name="passcode_turn_on">Włącz blokadę PIN</string>
+ <string name="media_edit_caption_text">Etykieta</string>
+ <string name="images">Obrazki</string>
+ <string name="discard">Odrzuć</string>
+ <string name="upload">Załaduj</string>
+ <string name="notifications">Powiadomienia</string>
+ <string name="note_reply_successful">Odpowiedź opublikowana</string>
+ <string name="new_notifications">%d nowych powiadomień</string>
+ <string name="more_notifications">i %d więcej.</string>
+ <string name="sign_in">Zaloguj się</string>
+ <string name="follows">Obserwuje</string>
+ <string name="loading">Ładowanie...</string>
+ <string name="httpuser">Nazwa użytkownika protokołu HTTP</string>
+ <string name="httppassword">Hasło protokołu HTTP</string>
+ <string name="error_media_upload">Napotkano błąd podczas wysyłania multimediów.</string>
+ <string name="publish_date">Opublikuj</string>
+ <string name="post_content">Treść (dotknij, aby dodać tekst i multimedia)</string>
+ <string name="content_description_add_media">Dodaj multimedia</string>
+ <string name="incorrect_credentials">Nieprawidłowa nazwa użytkownika lub hasło.</string>
+ <string name="username">Nazwa użytkownika</string>
+ <string name="password">Hasło</string>
+ <string name="reader">Czytnik</string>
+ <string name="no_network_title">Brak dostępnej sieci</string>
+ <string name="pages">Strony</string>
+ <string name="width">Szerokość</string>
+ <string name="posts">Wpisy</string>
+ <string name="anonymous">Anonimowy</string>
+ <string name="page">Strona</string>
+ <string name="post">Wpis</string>
+ <string name="featured_in_post">Załącz obraz w treści wpisu</string>
+ <string name="featured">Ustaw ikonę wpisu</string>
+ <string name="caption">Nagłówek (opcjonalny)</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="scaled_image">Zmień szerokosć obrazu</string>
+ <string name="upload_scaled_image">Załaduj i wprowadź adres do przeskalowanego obrazu</string>
+ <string name="scheduled">Zaplanowane</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Przesyłanie</string>
+ <string name="version">Wersja</string>
+ <string name="app_title">WordPress dla Androida</string>
+ <string name="tos">Warunki korzystania</string>
+ <string name="max_thumbnail_px_width">Domyślna szerokość obrazka</string>
+ <string name="image_alignment">Wyrównanie</string>
+ <string name="refresh">Odśwież</string>
+ <string name="untitled">Bez tytułu</string>
+ <string name="edit">Edytuj</string>
+ <string name="page_id">Strona</string>
+ <string name="post_id">Wpis</string>
+ <string name="post_password">Hasło (opcjonalnie)</string>
+ <string name="immediately">Natychmiast</string>
+ <string name="quickpress_add_alert_title">Wpisz nazwę skrótu</string>
+ <string name="today">Dzisiaj</string>
+ <string name="settings">Ustawienia</string>
+ <string name="share_url">Udostępnij URL</string>
+ <string name="quickpress_window_title">Wybierz blog dla skrótu QuickPress</string>
+ <string name="quickpress_add_error">Nazwa skrótu nie może być pusta</string>
+ <string name="draft">Szkic</string>
+ <string name="post_private">Prywatny</string>
+ <string name="publish_post">Opublikuj</string>
+ <string name="upload_full_size_image">Prześlij i link.do pełn.obraz.</string>
+ <string name="categories">Kategorie</string>
+ <string name="title">Tytuł</string>
+ <string name="tags_separate_with_commas">Tagi (oddzielone przecinkami)</string>
+ <string name="dlg_deleting_comments">Usuwanie komentarzy</string>
+ <string name="notification_vibrate">Wibracja</string>
+ <string name="notification_sound">Dźwięk powiadomienia</string>
+ <string name="notification_blink">Mruganie diody LED</string>
+ <string name="status">Status</string>
+ <string name="location">Lokalizacja</string>
+ <string name="sdcard_title">Wymagana karta pamięci SD</string>
+ <string name="select_video">Wybierz wideo z galerii</string>
+ <string name="media">Multimedia</string>
+ <string name="delete">Usuń</string>
+ <string name="none">Brak</string>
+ <string name="blogs">Blogi</string>
+ <string name="select_photo">Wybierz obraz z galerii</string>
+ <string name="add">Dodaj</string>
+ <string name="yes">Tak</string>
+ <string name="no">Nie</string>
+ <string name="category_refresh_error">Błąd odświeżania kategorii</string>
+ <string name="cancel">Anuluj</string>
+ <string name="save">Zapisz</string>
+ <string name="error">Błąd</string>
+ <string name="reply">Odpisz</string>
+ <string name="preview">Podgląd</string>
+ <string name="notification_settings">Ustawienia powiadomień</string>
+ <string name="on">we wpisie</string>
+</resources>
diff --git a/WordPress/src/main/res/values-pt-rBR/strings.xml b/WordPress/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 000000000..6cde5666f
--- /dev/null
+++ b/WordPress/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrador</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Colaborador</string>
+ <string name="role_follower">Seguidor</string>
+ <string name="role_viewer">Leitor</string>
+ <string name="error_post_my_profile_no_connection">Sem conexão, não foi possível salvar seu perfil</string>
+ <string name="alignment_left">Esquerda</string>
+ <string name="alignment_right">Direita</string>
+ <string name="alignment_none">Nenhum</string>
+ <string name="site_settings_list_editor_action_mode_title">Selecionado %1$d</string>
+ <string name="error_fetch_users_list">Não foi possível recuperar os usuários do site</string>
+ <string name="plans_manage">Gerenciar o seu plano em\nWordPress.com/plans</string>
+ <string name="title_follower">Seguidor</string>
+ <string name="people_empty_list_filtered_viewers">Você não tem nenhum espectador ainda.</string>
+ <string name="title_email_follower">Seguidor por e-mail</string>
+ <string name="people_fetching">Buscando usuários...</string>
+ <string name="people_empty_list_filtered_email_followers">Você não tem nenhum seguidores por e-mail ainda.</string>
+ <string name="people_empty_list_filtered_followers">Você não tem seguidores ainda.</string>
+ <string name="people_empty_list_filtered_users">Você não tem usuários ainda.</string>
+ <string name="people_dropdown_item_viewers">Espectadores</string>
+ <string name="people_dropdown_item_email_followers">Seguidores por e-mail</string>
+ <string name="people_dropdown_item_followers">Seguidores</string>
+ <string name="people_dropdown_item_team">Equipe</string>
+ <string name="invite_message_usernames_limit">Convite até 10 endereços de e-mail e/ou usuários do WordPress.com. Serão enviadas instruções sobre como criar um nome de usuário para aqueles que necessitarem.</string>
+ <string name="viewer_remove_confirmation_message">Caso você remova este espectador(a), ele ou ela não serão capazes de visitar este site.\n\nVocê gostaria ainda de remover este espectador(a)?</string>
+ <string name="follower_remove_confirmation_message">Caso removido, este seguidor irá parar de receber notificações sobre este site, menos que ele comece a seguir novamente.\n\nVocê gostaria ainda de remover este seguidor?</string>
+ <string name="follower_subscribed_since">Desde de %1$s</string>
+ <string name="reader_label_view_gallery">Ver galeria</string>
+ <string name="error_remove_follower">Não foi possível remover o seguidor</string>
+ <string name="error_remove_viewer">Não foi possível remover o espectador</string>
+ <string name="error_fetch_email_followers_list">Não foi possível recuperar os seguidores por e-mail do site</string>
+ <string name="error_fetch_followers_list">Não foi possível recuperar os seguidos do site</string>
+ <string name="editor_failed_uploads_switch_html">Alguns envios de mídia falharam. Você pode mudar para o modo de HTML no lugar. Remover todos os envios falhados e continuar?</string>
+ <string name="format_bar_description_html">Modo HTML</string>
+ <string name="visual_editor">Editor visual</string>
+ <string name="image_thumbnail">Imagem em míniatura</string>
+ <string name="format_bar_description_ul">Lista não ordenada</string>
+ <string name="format_bar_description_ol">Lista ordenada</string>
+ <string name="format_bar_description_media">Inserir mídia</string>
+ <string name="format_bar_description_more">Inserir "leia mais"</string>
+ <string name="format_bar_description_strike">Riscado</string>
+ <string name="format_bar_description_quote">Bloco de citação</string>
+ <string name="format_bar_description_link">Inserir link</string>
+ <string name="format_bar_description_italic">Itálico</string>
+ <string name="format_bar_description_underline">Sublinhado</string>
+ <string name="image_settings_save_toast">Salvar alterações</string>
+ <string name="image_caption">Legenda</string>
+ <string name="image_alt_text">Texto alternativo</string>
+ <string name="image_link_to">Link para</string>
+ <string name="image_width">Largura</string>
+ <string name="format_bar_description_bold">Negrito</string>
+ <string name="image_settings_dismiss_dialog_title">Descartar alterações não salvas?</string>
+ <string name="stop_upload_dialog_title">Parar envio?</string>
+ <string name="stop_upload_button">Parar envio</string>
+ <string name="alert_error_adding_media">Um erro ocorreu algo inserir o arquivo de mídia</string>
+ <string name="alert_action_while_uploading">Você esta no momento enviando um arquivo de mídia. Aguarde até o processo ser completo.</string>
+ <string name="alert_insert_image_html_mode">Não é possível inserir arquivo de mídia diretamente no modo HTML. Mude de volta para o modo visual.</string>
+ <string name="uploading_gallery_placeholder">Enviando galeria...</string>
+ <string name="invite_sent">Convite enviado com sucesso.</string>
+ <string name="tap_to_try_again">Toque para tentar novamente!</string>
+ <string name="invite_error_some_failed">Convite enviado, mas ocorreram erros!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">Um erro ocorreu ao tentar enviar o convite!</string>
+ <string name="invite_error_invalid_usernames_multiple">Não pôde enviar: Contém nome de usuários ou e-mails inválidos</string>
+ <string name="invite_error_invalid_usernames_one">Não pôde enviar: Um nome de usuário ou e-mail é inválido</string>
+ <string name="invite_error_no_usernames">Adicione pelo menos um nome de usuário</string>
+ <string name="invite_message_info">(Opcional) Você pode inserir uma mensagem personalizada de até 500 caracteres que será incluída no convite para os usuários.</string>
+ <string name="invite_message_remaining_other">%d caracteres restantes</string>
+ <string name="invite_message_remaining_one">1 caractere restante</string>
+ <string name="invite_message_remaining_zero">0 caracteres restantes</string>
+ <string name="invite_invalid_email">O endereço de e-mail \'%s\' é inválido</string>
+ <string name="invite_message_title">Mensagem personalizada</string>
+ <string name="invite_already_a_member">Já existe um membro com o nome de usuário \'%s\'</string>
+ <string name="invite_username_not_found">Nenhum usuário foi encontrado para o nome de usuário \'%s\'</string>
+ <string name="invite">Convidar</string>
+ <string name="invite_names_title">Nome de usuários ou e-mails</string>
+ <string name="send_link">Enviar link</string>
+ <string name="my_site_header_external">Externo</string>
+ <string name="invite_people">Convidar pessoas</string>
+ <string name="signup_succeed_signin_failed">A sua conta foi criada, mas ocorreu um erro ao autenticar você. Tente entrar novamente com o seu novo nome de usuário e senha.</string>
+ <string name="label_clear_search_history">Limpar histórico de pesquisa</string>
+ <string name="dlg_confirm_clear_search_history">Limpar histórico de pesquisa?</string>
+ <string name="reader_empty_posts_in_search_description">Nenhum post na sua língua foi encontrado para %s</string>
+ <string name="reader_label_post_search_running">Pesquisando...</string>
+ <string name="reader_label_related_posts">Leitura relacionada</string>
+ <string name="reader_empty_posts_in_search_title">Nenhum post foi encontrado</string>
+ <string name="reader_label_post_search_explainer">Pesquisar em todos os blogs públicos do WordPress.com</string>
+ <string name="reader_hint_post_search">Pesquisar no WordPress.com</string>
+ <string name="reader_title_search_results">Pesquisar por %s</string>
+ <string name="reader_title_related_post_detail">Post relacionado</string>
+ <string name="preview_screen_links_disabled">Os links são desabilitados na tela de visualização</string>
+ <string name="draft_explainer">Este post é um rascunho que ainda não foi publicado</string>
+ <string name="send">Enviar</string>
+ <string name="person_removed">%1$s removido com sucesso</string>
+ <string name="user_remove_confirmation_message">Caso você remova %1$s, o usuário não irá mais ser capaz de acessar este site, mas o conteúdo que foi criado por %1$s será mantido no site.\n\nAinda gostaria de remover este usuário?</string>
+ <string name="person_remove_confirmation_title">Remover %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Os sites desta lista não publicaram nada recentemente</string>
+ <string name="people">Pessoa</string>
+ <string name="edit_user">Editar usuário</string>
+ <string name="role">Função</string>
+ <string name="error_remove_user">Não foi possível remover usuário</string>
+ <string name="error_update_role">Não foi possível atualizar a função do usuário</string>
+ <string name="error_fetch_viewers_list">Não foi possível recuperar os espectadores do site</string>
+ <string name="gravatar_camera_and_media_permission_required">São necessárias permissões para selecionar ou tirar uma foto</string>
+ <string name="error_updating_gravatar">Erro ao enviar o seu Gravatar</string>
+ <string name="error_refreshing_gravatar">Erro ao carregar o seu Gravatar</string>
+ <string name="error_locating_image">Erro ao localizar o recorte da imagem</string>
+ <string name="gravatar_tip">Novo! Toque no seu Gravatar para mudá-lo!</string>
+ <string name="error_cropping_image">Erro ao cortar a imagem</string>
+ <string name="not_on_wordpress_com">Não esta no WordPress.com?</string>
+ <string name="launch_your_email_app">Inicie o seu aplicativo de e-mail</string>
+ <string name="checking_email">Verificando e-mail</string>
+ <string name="magic_link_unavailable_error_message">Atualmente indisponível. Digite sua senha</string>
+ <string name="check_your_email">Verifique seu e-mail</string>
+ <string name="logging_in">Fazendo login</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Enviar um link para o seu e-mail para fazer login instantaneamente</string>
+ <string name="enter_your_password_instead">Digite sua senha ao invés disso</string>
+ <string name="web_address_dialog_hint">Exibido ao público quando você comentar.</string>
+ <string name="username_email">E-mail ou nome de usuário</string>
+ <string name="jetpack_not_connected_message">O plugin Jetpack esta instalado, mas não esta conectado com o WordPress.com. Você deseja conectar o Jetpack?</string>
+ <string name="jetpack_not_connected">O plugin Jetpack não esta conectado</string>
+ <string name="new_editor_reflection_error">O editor visual não é compatível com seu dispositivo. Ele foi\n desativado automaticamente.</string>
+ <string name="stats_insights_latest_post_no_title">(sem título)</string>
+ <string name="capture_or_pick_photo">Tirar or selecionar uma foto</string>
+ <string name="plans_post_purchase_text_themes">Você agora possui acesso ilimitado a Temas Premium. Visualize qualquer tema no seu site para começar.</string>
+ <string name="plans_post_purchase_button_themes">Pesquisar temas</string>
+ <string name="plans_post_purchase_title_themes">Encontre um tema premium perfeito</string>
+ <string name="plans_post_purchase_button_video">Comece um novo post</string>
+ <string name="plans_post_purchase_text_video">Você pode enviar e hospedar vídeos no seu site com VideoPress e seu armazenamento de mídia expandido.</string>
+ <string name="plans_post_purchase_title_video">Dê vida aos seus posts com vídeos</string>
+ <string name="plans_post_purchase_button_customize">Personalizar meu site</string>
+ <string name="plans_post_purchase_text_customize">Você agora possui acesso a fontes personalizadas, cores personalizadas e o poder de editar CSS personalizado.</string>
+ <string name="plans_post_purchase_text_intro">Seu site está pulando de alegria! Explore os novos recursos de seu site e escolha por onde quer começar.</string>
+ <string name="plans_post_purchase_title_customize">Personalizar fontes e cores</string>
+ <string name="plans_post_purchase_title_intro">É todo seu. Vamos nessa!</string>
+ <string name="export_your_content_message">Seus posts, páginas e configurações serão enviados para você em %s</string>
+ <string name="plan">Plano</string>
+ <string name="plans">Planos</string>
+ <string name="plans_loading_error">Não foi possível carregar os planos</string>
+ <string name="export_your_content">Exporte seu conteúdo</string>
+ <string name="exporting_content_progress">Exportando conteúdo...</string>
+ <string name="export_email_sent">E-mail sobre a exportação enviado!</string>
+ <string name="checking_purchases">Verificando compras</string>
+ <string name="show_purchases">Mostrar compras</string>
+ <string name="premium_upgrades_message">Você ativou upgrades Premium em seu site. Por favor, cancele seus upgrades antes de excluir seu site.</string>
+ <string name="premium_upgrades_title">Upgrades premium</string>
+ <string name="purchases_request_error">Algo deu errado. Não foi possível carregar as compras.</string>
+ <string name="delete_site_progress">Excluindo site...</string>
+ <string name="delete_site_summary">Essa ação não pode ser desfeita. Excluir seu site irá remover todo o conteúdo, contribuidores e domínios desse site.</string>
+ <string name="delete_site_hint">Excluir site</string>
+ <string name="are_you_sure">Você tem certeza?</string>
+ <string name="export_site_hint">Exporte seu conteúdo para um arquivo XML</string>
+ <string name="export_site_summary">Caso tenha certeza, por favor, invista algum tempo agora para exportar seu conteúdo. Não será possível recuperá-lo no futuro.</string>
+ <string name="keep_your_content">Mantenha seu conteúdo</string>
+ <string name="domain_removal_hint">Domínios que não funcionarão assim que você remover seu site</string>
+ <string name="domain_removal_summary">Cuidado! Excluir seu site também excluirá o(s) domínio(s) listado(s) abaixo.</string>
+ <string name="primary_domain">Domínio primário</string>
+ <string name="domain_removal">Remover domínio</string>
+ <string name="error_deleting_site_summary">Houve um erro ao excluir seu site. Por favor, entre em contato com o suporte.</string>
+ <string name="error_deleting_site">Erro ao excluir o site</string>
+ <string name="site_settings_export_content_title">Exportar conteúdo</string>
+ <string name="confirm_delete_site_prompt">Por favor, digite %1$s no campo abaixo para confirmar. Então seu site desaparecerá para sempre.</string>
+ <string name="confirm_delete_site">Confirme a exclusão do site</string>
+ <string name="contact_support">Contatar o suporte</string>
+ <string name="start_over_text">Caso você queira um site mas não queira manter nenhum post ou página que você tem agora, nosso time de suporte pode excluir seus posts, páginas, mídia e comentários por você.\n\nIsso manterá o endereço atual de seu site ativo, mas dará um site novinho em folha para seu novo conteúdo. Entre em contato conosco para ter seu conteúdo atual removido. </string>
+ <string name="site_settings_start_over_hint">Apaga seu conteúdo e começa seu site do zero</string>
+ <string name="let_us_help">Deixe-nos ajudar</string>
+ <string name="start_over">Começar novamente</string>
+ <string name="me_btn_app_settings">Configurações do aplicativo</string>
+ <string name="editor_remove_failed_uploads">Remover envios com falha</string>
+ <string name="editor_toast_failed_uploads">Alguns envios de mídia falharam. Você não pode salvar ou publicar\nseu post nesse estado. Você deseja remover todas as mídias que falharam?</string>
+ <string name="site_settings_advanced_header">Avançado</string>
+ <string name="comments_empty_list_filtered_trashed">Nenhum comentário na lixeira</string>
+ <string name="comments_empty_list_filtered_pending">Nenhum comentário pendente</string>
+ <string name="comments_empty_list_filtered_approved">Nenhum comentário aprovado</string>
+ <string name="button_done">Pronto</string>
+ <string name="button_skip">Pular</string>
+ <string name="site_timeout_error">Não foi possível conectar-se ao Site WordPress devido a um erro no tempo limite.</string>
+ <string name="xmlrpc_malformed_response_error">Não foi possível conectar. A instalação do WordPress respondeu com um documento XML-RPC inválido.</string>
+ <string name="xmlrpc_missing_method_error">Não foi possível conectar. Alguns métodos XML-RPC necessários estão faltando no servidor.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Vídeo</string>
+ <string name="theme_free">Gratuito</string>
+ <string name="theme_all">Tudo</string>
+ <string name="theme_premium">Premium</string>
+ <string name="alignment_center">Centro</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Galeria</string>
+ <string name="post_format_image">Imagem</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Citação</string>
+ <string name="post_format_standard">Padrão</string>
+ <string name="notif_events">Informações sobre cursos e eventos (online e presenciais) do WordPress.com.</string>
+ <string name="post_format_aside">Notas</string>
+ <string name="post_format_audio">Áudio</string>
+ <string name="notif_surveys">Oportunidades para participar das pesquisas e enquetes do WordPress.com.</string>
+ <string name="notif_tips">Dicas para aproveitar o máximo do WordPress.com</string>
+ <string name="notif_community">Comunidade</string>
+ <string name="replies_to_my_comments">Respostas aos meus comentários</string>
+ <string name="notif_suggestions">Sugestões</string>
+ <string name="notif_research">Pesquisa</string>
+ <string name="site_achievements">Conquistas do site</string>
+ <string name="username_mentions">Menções ao nome de usuário</string>
+ <string name="likes_on_my_posts">Curtidas nos meus posts</string>
+ <string name="site_follows">Seguidores do site</string>
+ <string name="likes_on_my_comments">Curtidas nos meus comentários</string>
+ <string name="comments_on_my_site">Comentários no meu site</string>
+ <string name="site_settings_list_editor_summary_other">%d itens</string>
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="approve_auto_if_previously_approved">Comentários de usuários conhecidos</string>
+ <string name="approve_auto">Todos os usuários</string>
+ <string name="approve_manual">Sem comentários</string>
+ <string name="site_settings_paging_summary_other">%d comentários por página</string>
+ <string name="site_settings_paging_summary_one">1 comentário por página</string>
+ <string name="site_settings_multiple_links_summary_other">Exige aprovação para mais de %d links</string>
+ <string name="site_settings_multiple_links_summary_one">Exige aprovação para mais de 1 link</string>
+ <string name="site_settings_multiple_links_summary_zero">Exige aprovação para mais de 0 links</string>
+ <string name="detail_approve_auto">Aprovar automaticamente todos os comentários.</string>
+ <string name="detail_approve_auto_if_previously_approved">Aprovar automaticamente se o usuário tiver um comentário aprovado anteriormente.</string>
+ <string name="detail_approve_manual">Exigir aprovação manual para todos os comentários.</string>
+ <string name="filter_trashed_posts">Na lixeira</string>
+ <string name="days_quantity_one">1 dia</string>
+ <string name="days_quantity_other">%d dias</string>
+ <string name="filter_published_posts">Publicado</string>
+ <string name="filter_draft_posts">Rascunhos</string>
+ <string name="filter_scheduled_posts">Agendado</string>
+ <string name="primary_site">Site principal</string>
+ <string name="web_address">Endereço da web</string>
+ <string name="pending_email_change_snackbar">Clique no link de verificação no e-mail enviado para %1$s para confirmar seu novo endereço</string>
+ <string name="editor_toast_uploading_please_wait">Você está fazendo upload de mídia no momento. Aguarde até que o processo seja concluído.</string>
+ <string name="error_refresh_comments_showing_older">Não foi possível atualizar os comentários no momento. Comentários mais antigos estão sendo exibidos</string>
+ <string name="editor_post_settings_set_featured_image">Configurar imagem destacada</string>
+ <string name="editor_post_settings_featured_image">Imagem destacada</string>
+ <string name="new_editor_promo_desc">O aplicativo do WordPress para Android agora inclui um belo visual repaginado.\n editor. Experimente criando um novo post.</string>
+ <string name="new_editor_promo_title">Novo editor</string>
+ <string name="new_editor_promo_button_label">Ótimo, obrigado!</string>
+ <string name="visual_editor_enabled">Editor visual ativado</string>
+ <string name="editor_content_placeholder">Compartilhe sua história aqui...</string>
+ <string name="editor_page_title_placeholder">Título da página</string>
+ <string name="editor_post_title_placeholder">Título do post</string>
+ <string name="email_address">Endereço de e-mail</string>
+ <string name="preference_show_visual_editor">Exibir editor visual</string>
+ <string name="dlg_sure_to_delete_comments">Excluir estes comentários permanentemente?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Excluir este comentário permanentemente?</string>
+ <string name="mnu_comment_delete_permanently">Excluir</string>
+ <string name="comment_deleted_permanently">Comentário excluído</string>
+ <string name="mnu_comment_untrash">Restaurar</string>
+ <string name="comments_empty_list_filtered_spam">Nenhum comentário spam</string>
+ <string name="could_not_load_page">Não foi possível carregar a página</string>
+ <string name="comment_status_all">Tudo</string>
+ <string name="interface_language">Idioma da interface</string>
+ <string name="off">Desativado</string>
+ <string name="about_the_app">Sobre o aplicativo</string>
+ <string name="error_post_account_settings">Não foi possível salvar as configurações da sua conta</string>
+ <string name="error_post_my_profile">Não foi possível salvar seu perfil</string>
+ <string name="error_fetch_account_settings">Não foi possível recuperar as configurações da sua conta</string>
+ <string name="error_fetch_my_profile">Não foi possível recuperar seu perfil</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, entendido</string>
+ <string name="stats_widget_promo_desc">Adicione o widget à sua tela inicial para acessar as Estatísticas com apenas um clique.</string>
+ <string name="stats_widget_promo_title">Widget de Estatísticas para tela inicial</string>
+ <string name="site_settings_unknown_language_code_error">Código de idioma não reconhecido</string>
+ <string name="site_settings_threading_dialog_description">Permitir que os comentários sejam agrupados em tópicos.</string>
+ <string name="site_settings_threading_dialog_header">Agrupar até</string>
+ <string name="remove">Remover</string>
+ <string name="search">Buscar</string>
+ <string name="add_category">Adicionar categoria</string>
+ <string name="disabled">Desativado</string>
+ <string name="site_settings_image_original_size">Tamanho original</string>
+ <string name="privacy_private">Seu site é visível apenas para você e usuários aprovados</string>
+ <string name="privacy_public_not_indexed">Seu site está visível para todos mas solicita aos mecanismos de busca para não indexá-lo</string>
+ <string name="privacy_public">Seu site está visível para todos e pode ser indexado por mecanismos de busca</string>
+ <string name="about_me_hint">Algumas palavras sobre você...</string>
+ <string name="about_me">Sobre mim</string>
+ <string name="public_display_name_hint">O nome de exibição será o seu nome de usuário se não for configurado</string>
+ <string name="public_display_name">Nome para exibição pública</string>
+ <string name="first_name">Nome</string>
+ <string name="last_name">Sobrenome</string>
+ <string name="my_profile">Meu perfil</string>
+ <string name="site_privacy_public_desc">Permitir que mecanismos de busca indexem este site</string>
+ <string name="site_privacy_hidden_desc">Evitar que mecanismos de busca indexem este site</string>
+ <string name="site_privacy_private_desc">Quero que meu site fique privado, visível apenas para os usuários que eu escolher</string>
+ <string name="cd_related_post_preview_image">Imagem de visualização do post relacionado</string>
+ <string name="error_post_remote_site_settings">Não foi possível salvar as informações do site</string>
+ <string name="error_fetch_remote_site_settings">Não foi possível recuperar as informações do site</string>
+ <string name="error_media_upload_connection">Ocorreu um erro de conexão durante o envio de mídia</string>
+ <string name="site_settings_disconnected_toast">Sem conexão, edição desabilitada.</string>
+ <string name="site_settings_unsupported_version_error">Versão do WordPress não suportada</string>
+ <string name="site_settings_multiple_links_dialog_description">Exigir aprovação para comentários que incluem mais do que este número de links.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Fechar automaticamente</string>
+ <string name="site_settings_close_after_dialog_description">Encerrar automaticamente comentários nos artigos.</string>
+ <string name="site_settings_paging_dialog_description">Dividir tópicos de comentários em várias páginas.</string>
+ <string name="site_settings_paging_dialog_header">Comentários por página</string>
+ <string name="site_settings_close_after_dialog_title">Encerrar comentários</string>
+ <string name="site_settings_blacklist_description">Quando um comentário tiver uma destas palavras em seu conteúdo, nome, URL, e-mail ou IP, ele será marcado como spam. Você pode inserir partes de palavras, então "press" corresponderá a "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">Quando um comentário contiver qualquer uma destas palavras em seu conteúdo, nome, URL, endereço de e-mail ou IP, o mesmo será retido na fila de moderação. Você pode inserir partes de palavras, então "press" corresponderá a "WordPress".</string>
+ <string name="site_settings_list_editor_input_hint">Insira uma palavra ou frase</string>
+ <string name="site_settings_list_editor_no_items_text">Nenhum item</string>
+ <string name="site_settings_learn_more_caption">É possível substituir essas configurações para posts individuais.</string>
+ <string name="site_settings_rp_preview3_site">em "Upgrade"</string>
+ <string name="site_settings_rp_preview3_title">Foco da atualização: VideoPress para casamentos</string>
+ <string name="site_settings_rp_preview2_site">em "Aplicativos"</string>
+ <string name="site_settings_rp_preview2_title">O aplicativo WordPress para Android foi melhorado</string>
+ <string name="site_settings_rp_preview1_site">em "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Grande atualização para iPhone/iPad disponível</string>
+ <string name="site_settings_rp_show_images_title">Mostrar imagens</string>
+ <string name="site_settings_rp_show_header_title">Mostrar cabeçalho</string>
+ <string name="site_settings_rp_switch_summary">Posts Relacionados exibe conteúdo relevante do seu site abaixo dos seus posts.</string>
+ <string name="site_settings_rp_switch_title">Mostrar posts relacionados</string>
+ <string name="site_settings_delete_site_hint">Remove os dados do seu site do aplicativo</string>
+ <string name="site_settings_blacklist_hint">Comentários que corresponderem a um filtro serão marcados como spam</string>
+ <string name="site_settings_moderation_hold_hint">Comentários que corresponderem a um filtro serão colocados em uma fila de moderação</string>
+ <string name="site_settings_multiple_links_hint">Ignora o limite de links de usuários conhecidos</string>
+ <string name="site_settings_whitelist_hint">O autor do comentário tem que ter um comentário aprovado anteriormente</string>
+ <string name="site_settings_user_account_required_hint">Usuários devem estar registrados e logados para comentar</string>
+ <string name="site_settings_identity_required_hint">O autor do comentário tem que preencher o nome e email</string>
+ <string name="site_settings_manual_approval_hint">Os comentários devem ser aprovados manualmente</string>
+ <string name="site_settings_paging_hint">Exibir comentários em grupos de tamanho especificado</string>
+ <string name="site_settings_threading_hint">Permitir comentários agrupados em uma certa profundidade</string>
+ <string name="site_settings_sort_by_hint">Determina a ordem de exibição dos comentários</string>
+ <string name="site_settings_close_after_hint">Desabilitar comentários após um tempo específico</string>
+ <string name="site_settings_receive_pingbacks_hint">Permitir notificações de link de outros blogs</string>
+ <string name="site_settings_send_pingbacks_hint">Tentar notificar blogs com links a partir do artigo</string>
+ <string name="site_settings_allow_comments_hint">Permitir que leitores publiquem comentários</string>
+ <string name="site_settings_discussion_hint">Exibir e alterar as configurações de discussão dos sites</string>
+ <string name="site_settings_more_hint">Exibir todas as configurações de Discussão disponíveis</string>
+ <string name="site_settings_related_posts_hint">Mostrar ou ocultar posts relacionados no leitor</string>
+ <string name="site_settings_upload_and_link_image_hint">Permitir que imagens sejam sempre enviadas no tamanho máximo</string>
+ <string name="site_settings_image_width_hint">Redimensiona as imagens nos posts para esta largura</string>
+ <string name="site_settings_format_hint">Define o formato do novo post</string>
+ <string name="site_settings_category_hint">Define a categoria do novo post</string>
+ <string name="site_settings_location_hint">Adicionar automaticamente os dados de localização aos posts</string>
+ <string name="site_settings_password_hint">Alterar sua senha</string>
+ <string name="site_settings_username_hint">Conta de usuário atual</string>
+ <string name="site_settings_language_hint">Idioma em que este blog é normalmente escrito</string>
+ <string name="site_settings_privacy_hint">Controle quem pode ver seu site</string>
+ <string name="site_settings_address_hint">No momento, você não pode alterar o endereço</string>
+ <string name="site_settings_tagline_hint">Uma breve descrição ou uma frase de efeito para descrever o seu blog</string>
+ <string name="site_settings_title_hint">Em poucas palavras, explique sobre o que é este site</string>
+ <string name="site_settings_whitelist_known_summary">Comentários de usuários conhecidos</string>
+ <string name="site_settings_whitelist_all_summary">Comentários de todos os usuários</string>
+ <string name="site_settings_threading_summary">%d níveis</string>
+ <string name="site_settings_privacy_private_summary">Privado</string>
+ <string name="site_settings_privacy_hidden_summary">Oculto</string>
+ <string name="site_settings_privacy_public_summary">Público</string>
+ <string name="site_settings_delete_site_title">Excluir site</string>
+ <string name="site_settings_blacklist_title">Lista negra</string>
+ <string name="site_settings_moderation_hold_title">Aguardar moderação</string>
+ <string name="site_settings_multiple_links_title">Links nos comentários</string>
+ <string name="site_settings_whitelist_title">Aprovar automaticamente</string>
+ <string name="site_settings_threading_title">Threading</string>
+ <string name="site_settings_paging_title">Paginação</string>
+ <string name="site_settings_sort_by_title">Ordenar por</string>
+ <string name="site_settings_account_required_title">Os usuários devem estar conectados</string>
+ <string name="site_settings_identity_required_title">É necessário incluir nome e e-mail</string>
+ <string name="site_settings_receive_pingbacks_title">Receber pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Enviar pingbacks</string>
+ <string name="site_settings_allow_comments_title">Habilitar comentários</string>
+ <string name="site_settings_default_format_title">Formato padrão</string>
+ <string name="site_settings_default_category_title">Categoria padrão</string>
+ <string name="site_settings_location_title">Habilitar localização</string>
+ <string name="site_settings_address_title">Endereço</string>
+ <string name="site_settings_title_title">Título do site</string>
+ <string name="site_settings_tagline_title">Tagline</string>
+ <string name="site_settings_this_device_header">Este dispositivo</string>
+ <string name="site_settings_discussion_new_posts_header">Padrões para novos posts</string>
+ <string name="site_settings_writing_header">Escrevendo</string>
+ <string name="site_settings_account_header">Conta</string>
+ <string name="site_settings_general_header">Geral</string>
+ <string name="newest_first">Mais novos primeiro</string>
+ <string name="related_posts">Posts relacionados</string>
+ <string name="comments">Comentários</string>
+ <string name="discussion">Discussão</string>
+ <string name="privacy">Privacidade</string>
+ <string name="close_after">Encerrar após</string>
+ <string name="oldest_first">Mais antigos primeiro</string>
+ <string name="media_error_no_permission_upload">Você não tem permissão para enviar mídia para o site</string>
+ <string name="never">Nunca</string>
+ <string name="unknown">Desconhecido</string>
+ <string name="reader_err_get_post_not_found">Este post não existe mais</string>
+ <string name="reader_err_get_post_not_authorized">Você não possui autorização para ver este post</string>
+ <string name="reader_err_get_post_generic">Falha ao carregar esse post</string>
+ <string name="blog_name_no_spaced_allowed">O endereço do site não pode conter espaços</string>
+ <string name="invalid_username_no_spaces">O nome de usuário não pode conter espaços</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Os sites que você segue não publicaram nada recentemente</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Não há posts recentes</string>
+ <string name="edit_media">Editar mídia</string>
+ <string name="media_details_copy_url_toast">A URL foi copiada para a área de transferência</string>
+ <string name="media_details_copy_url">Copiar URL</string>
+ <string name="media_details_label_date_uploaded">Upload completo</string>
+ <string name="media_details_label_date_added">Adicionada</string>
+ <string name="selected_theme">Tema selecionado</string>
+ <string name="could_not_load_theme">Não foi possível carregar o tema</string>
+ <string name="theme_activation_error">Algo deu errado. Não foi possível ativar o tema</string>
+ <string name="theme_by_author_prompt_append"> por %1$s</string>
+ <string name="theme_prompt">Obrigado por escolher %1$s</string>
+ <string name="theme_view">Visualizar</string>
+ <string name="theme_details">Detalhes</string>
+ <string name="theme_support">Suporte</string>
+ <string name="theme_done">Pronto</string>
+ <string name="theme_manage_site">Gerenciar site</string>
+ <string name="theme_try_and_customize">Testar e personalizar</string>
+ <string name="title_activity_theme_support">Temas</string>
+ <string name="theme_activate">Ativar</string>
+ <string name="details">Detalhes</string>
+ <string name="support">Suporte</string>
+ <string name="active">Ativo</string>
+ <string name="date_range_start_date">Data inicial</string>
+ <string name="date_range_end_date">Data final</string>
+ <string name="current_theme">Tema atual</string>
+ <string name="customize">Personalizar</string>
+ <string name="stats_referrers_spam_generic_error">Algo de errado aconteceu durante a operação. O estado de spam não foi mudado.</string>
+ <string name="stats_referrers_marking_not_spam">Marcar como não spam</string>
+ <string name="stats_referrers_unspam">Não é spam</string>
+ <string name="stats_referrers_marking_spam">Marcar como spam</string>
+ <string name="post_published">Post publicado</string>
+ <string name="page_published">Página publicada</string>
+ <string name="post_updated">Post atualizado</string>
+ <string name="page_updated">Página atualizada</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_auth_error_authenticate">Falha ao carregar temas: autenticação de usuário falhou</string>
+ <string name="theme_no_search_result_found">Desculpe, nenhum tema encontrado.</string>
+ <string name="media_file_name">Nome do arquivo: %s</string>
+ <string name="media_dimensions">Dimensões: %s</string>
+ <string name="media_uploaded_on">Enviado em: %s</string>
+ <string name="media_file_type">Tipo de arquivo: %s</string>
+ <string name="upload_queued">Enfileirado</string>
+ <string name="reader_label_gap_marker">Carregar mais posts</string>
+ <string name="notifications_no_search_results">Nenhum site corresponde a \'%s\'</string>
+ <string name="search_sites">Pesquisar sites</string>
+ <string name="notifications_empty_view_reader">Visualizar Leitor</string>
+ <string name="unread">Não lida</string>
+ <string name="notifications_empty_action_followers_likes">Seja notado: comente em posts que você leu.</string>
+ <string name="notifications_empty_action_comments">Participe de uma conversa: comente em posts de blogs que você segue.</string>
+ <string name="notifications_empty_action_unread">Reinicie a conversa: escrever um novo post.</string>
+ <string name="notifications_empty_action_all">Seja ativo! Comente em posts de blogs que você segue.</string>
+ <string name="notifications_empty_likes">Nenhuma nova curtida para exibir... ainda.</string>
+ <string name="notifications_empty_followers">Nenhum novo seguidore para relatar... ainda.</string>
+ <string name="notifications_empty_comments">Nenhum novo comentário... ainda.</string>
+ <string name="notifications_empty_unread">Você está atualizado!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Por favor, acesse as Estatísticas no aplicativo e tente adicionar o widget mais tarde</string>
+ <string name="stats_widget_error_readd_widget">Por favor, remova o widget e adicione-o novamente</string>
+ <string name="stats_widget_error_no_visible_blog">Estatísticas não podem ser carregadas sem um blog visível</string>
+ <string name="stats_widget_error_no_permissions">Sua conta no WordPress.com não pode acessar as estatísticas deste blog</string>
+ <string name="stats_widget_error_no_account">Por favor, faça login no WordPress</string>
+ <string name="stats_widget_error_generic">As Estatísticas não puderam ser carregadas</string>
+ <string name="stats_widget_loading_data">Carregando dados...</string>
+ <string name="stats_widget_name_for_blog">Estatísticas de hoje para %1$s</string>
+ <string name="stats_widget_name">Estatísticas de hoje do WordPress</string>
+ <string name="add_location_permission_required">Permissões necessárias para adicionar localização</string>
+ <string name="add_media_permission_required">Permissões necessárias para adicionar mídia</string>
+ <string name="access_media_permission_required">Permissões necessárias para acessar mídia</string>
+ <string name="stats_enable_rest_api_in_jetpack">Para ver suas estatísticas, ative o modulo JSON API do Jetpack.</string>
+ <string name="error_open_list_from_notification">Este post ou página foi publicado em outro site</string>
+ <string name="reader_short_comment_count_multi">%s comentários</string>
+ <string name="reader_short_comment_count_one">1 comentário</string>
+ <string name="reader_label_submit_comment">ENVIAR</string>
+ <string name="reader_hint_comment_on_post">Responder ao post...</string>
+ <string name="reader_discover_visit_blog">Acessar %s</string>
+ <string name="reader_discover_attribution_blog">Originalmente publicado no %s</string>
+ <string name="reader_discover_attribution_author">Originalmente publicado por %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Originalmente publicado por %1$s no %2$s</string>
+ <string name="reader_short_like_count_multi">%s curtidas</string>
+ <string name="reader_short_like_count_one">1 curtida</string>
+ <string name="reader_label_follow_count">%,d seguidores</string>
+ <string name="reader_short_like_count_none">Curtir</string>
+ <string name="reader_menu_tags">Editar tags e blogs</string>
+ <string name="reader_title_post_detail">Post do leitor</string>
+ <string name="local_draft_explainer">Este post é um rascunho local que ainda não foi publicado</string>
+ <string name="local_changes_explainer">Este post teve alterações locais que ainda não foram publicadas</string>
+ <string name="notifications_push_summary">Configurações de notificações que são exibidas em seu dispositivo.</string>
+ <string name="notifications_email_summary">Configurações de notificações que são enviadas ao e-mail vinculado à sua conta.</string>
+ <string name="notifications_tab_summary">Configurações de notificações que são exibidas na guia Notificações.</string>
+ <string name="notifications_disabled">Notificações de aplicativo foram desativadas. Toque aqui para ativá-las em Configurações.</string>
+ <string name="notification_types">Tipos de notificação</string>
+ <string name="error_loading_notifications">Não foi possível carregar as configurações de notificação</string>
+ <string name="replies_to_your_comments">Respostas aos seus comentários</string>
+ <string name="comment_likes">Curtidas do comentário</string>
+ <string name="app_notifications">Notificações de aplicativo</string>
+ <string name="notifications_tab">Guia Notificações</string>
+ <string name="email">E-mail</string>
+ <string name="notifications_comments_other_blogs">Comentários em outros sites</string>
+ <string name="notifications_other">Outro</string>
+ <string name="notifications_wpcom_updates">Novidades do WordPress.com</string>
+ <string name="notifications_account_emails">Email do WordPress.com</string>
+ <string name="notifications_account_emails_summary">Sempre enviaremos emails importantes sobre a sua conta, mas você também pode ter acesso a conteúdo extra.</string>
+ <string name="notifications_sights_and_sounds">Aparência e sons</string>
+ <string name="your_sites">Seus Sites</string>
+ <string name="stats_insights_latest_post_trend">Faz %1$s desde que %2$s foi publicado. Este foi o desempenho do post até agora...</string>
+ <string name="stats_insights_latest_post_summary">Resumo dos posts mais recentes</string>
+ <string name="button_revert">Reverter</string>
+ <string name="days_ago">Há %d dias</string>
+ <string name="yesterday">Ontem</string>
+ <string name="connectionbar_no_connection">Sem conexão</string>
+ <string name="page_trashed">Página enviada para a lixeira</string>
+ <string name="post_deleted">Post excluído</string>
+ <string name="post_trashed">Post enviado para a lixeira</string>
+ <string name="stats_no_activity_this_period">Nenhuma atividade no período</string>
+ <string name="trashed">Na lixeira</string>
+ <string name="button_back">Voltar</string>
+ <string name="page_deleted">Página excluída</string>
+ <string name="button_stats">Estatísticas</string>
+ <string name="button_trash">Enviar para a lixeira</string>
+ <string name="button_preview">Visualizar</string>
+ <string name="button_view">Exibir</string>
+ <string name="button_edit">Editar</string>
+ <string name="button_publish">Publicar</string>
+ <string name="my_site_no_sites_view_subtitle">Deseja adicionar um?</string>
+ <string name="my_site_no_sites_view_title">Você não possui nenhum site do WordPress ainda.</string>
+ <string name="my_site_no_sites_view_drake">Ilustração</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Você não está autorizado a acessar este blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">Este blog não foi encontrado</string>
+ <string name="undo">Desfazer</string>
+ <string name="tabbar_accessibility_label_my_site">Meu site</string>
+ <string name="tabbar_accessibility_label_me">Eu</string>
+ <string name="passcodelock_prompt_message">Digite seu PIN</string>
+ <string name="editor_toast_changes_saved">Modificações salvas</string>
+ <string name="push_auth_expired">A solicitação expirou. Faça login no WordPress.com para tentar novamente.</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de visualizações</string>
+ <string name="stats_insights_best_ever">Melhores visualizações</string>
+ <string name="ignore">Ignorar</string>
+ <string name="stats_insights_most_popular_hour">Horário com maior número de visitações</string>
+ <string name="stats_insights_most_popular_day">Dia com maior número de visitações</string>
+ <string name="stats_insights_today">Estatísticas de hoje</string>
+ <string name="stats_insights_popular">Dia e horário com maior número de visitantes</string>
+ <string name="stats_insights_all_time">Todos os posts, visualizações e visitantes</string>
+ <string name="stats_insights">Informações</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Para visualizar suas estatísticas, faça login na conta do WordPress.com que você usou para se conectar ao Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Buscando suas outras estatísticas recentes? Nós as movemos para a página Informações.</string>
+ <string name="me_disconnect_from_wordpress_com">Desconectar do WordPress.com</string>
+ <string name="me_btn_login_logout">Entrar/Sair</string>
+ <string name="me_connect_to_wordpress_com">Conectar ao WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">"%s" não está escondido porque é o site atual</string>
+ <string name="account_settings">Configurações da conta</string>
+ <string name="me_btn_support">Ajuda e Suporte</string>
+ <string name="site_picker_create_dotcom">Criar um site no WordPress.com</string>
+ <string name="site_picker_add_self_hosted">Adicionar site auto-hospedado</string>
+ <string name="site_picker_add_site">Adicionar site</string>
+ <string name="site_picker_edit_visibility">Mostrar/esconder sites</string>
+ <string name="my_site_btn_view_site">Ver site</string>
+ <string name="my_site_btn_switch_site">Mudar de site</string>
+ <string name="my_site_btn_view_admin">Ver Painel</string>
+ <string name="site_picker_title">Escolher site</string>
+ <string name="my_site_btn_blog_posts">Posts do blog</string>
+ <string name="my_site_btn_site_settings">Configurações</string>
+ <string name="my_site_header_look_and_feel">Aparência</string>
+ <string name="my_site_header_publish">Publicar</string>
+ <string name="reader_label_new_posts_subtitle">Toque para mostrar</string>
+ <string name="my_site_header_configuration">Configuração</string>
+ <string name="notifications_account_required">Fazer login no WordPress.com para notificações</string>
+ <string name="stats_unknown_author">Autor desconhecido</string>
+ <string name="image_added">Imagem adicionada</string>
+ <string name="signout">Desconectar</string>
+ <string name="sign_out_wpcom_confirm">Ao desconectar sua conta, os dados do @%s WordPress.com neste dispositivo serão removidos, incluindo rascunhos e alterações locais.</string>
+ <string name="deselect_all">Desmarcar todos</string>
+ <string name="select_all">Selecionar todos</string>
+ <string name="show">Mostrar</string>
+ <string name="hide">Esconder</string>
+ <string name="select_from_new_picker">Selecione vários itens com o novo seletor</string>
+ <string name="no_blog_images">Sem imagens</string>
+ <string name="error_loading_blog_images">Impossível obter imagens</string>
+ <string name="error_loading_blog_videos">Impossível obter vídeos</string>
+ <string name="no_device_images">Sem imagens</string>
+ <string name="no_blog_videos">Sem vídeos</string>
+ <string name="error_loading_images">Erro ao carregar imagens</string>
+ <string name="loading_blog_images">Obtendo imagens</string>
+ <string name="error_loading_videos">Erro ao carregar vídeos</string>
+ <string name="no_media_sources">Não foi possível obter mídia</string>
+ <string name="loading_blog_videos">Obtendo vídeos</string>
+ <string name="stats_generic_error">As estatísticas requisitadas não puderam ser carregadas</string>
+ <string name="no_device_videos">Sem vídeos</string>
+ <string name="loading_images">Carregando imagens</string>
+ <string name="loading_videos">Carregando vídeos</string>
+ <string name="no_media">Sem mídia</string>
+ <string name="auth_required">Faça login novamente para continuar.</string>
+ <string name="sign_in_jetpack">Faça login no WordPress.com para se conectar ao Jetpack.</string>
+ <string name="two_step_footer_button">Enviar o código por mensagem de texto</string>
+ <string name="two_step_footer_label">Insira o código do seu aplicativo de autenticação</string>
+ <string name="verify">Verificar</string>
+ <string name="verification_code">Código de verificação</string>
+ <string name="editor_toast_invalid_path">Caminho do arquivo inválido</string>
+ <string name="error_publish_no_network">Não é possível publicar enquanto não houver conexão. Salvo como rascunho.</string>
+ <string name="tab_title_device_videos">Vídeos do dispositivo</string>
+ <string name="tab_title_device_images">Imagens do dispositivo</string>
+ <string name="take_video">Gravar um vídeo</string>
+ <string name="take_photo">Tirar uma foto</string>
+ <string name="add_to_post">Adicionar ao post</string>
+ <string name="language">Idioma</string>
+ <string name="device">Dispositivo</string>
+ <string name="two_step_sms_sent">Verifique suas mensagens de texto para o código de verificação.</string>
+ <string name="invalid_verification_code">Código de verificação inválido</string>
+ <string name="tab_title_site_videos">Vídeos do site</string>
+ <string name="tab_title_site_images">Imagens do site</string>
+ <string name="media_picker_title">Selecionar mídia</string>
+ <string name="media_details_label_file_type">Tipo de arquivo</string>
+ <string name="media_details_label_file_name">Nome do arquivo</string>
+ <string name="error_notification_open">Não foi possível abrir a notificação</string>
+ <string name="publisher">Editor:</string>
+ <string name="reader_empty_posts_request_failed">Não foi possível recuperar os posts</string>
+ <string name="stats_search_terms_unknown_search_terms">Termos de busca desconhecidos</string>
+ <string name="stats_followers_total_wpcom_paged">Exibindo de %1$d a %2$d de %3$s Seguidores WordPress.com</string>
+ <string name="stats_view_search_terms">Termos de busca</string>
+ <string name="stats_empty_search_terms_desc">Saiba mais sobre o seu tráfego de busca ao verificar os termos que seus visitantes pesquisaram para encontrar o seu site.</string>
+ <string name="stats_empty_search_terms">Nenhum termo de busca registrado</string>
+ <string name="stats_entry_search_terms">Termo de busca</string>
+ <string name="stats_view_authors">Autores</string>
+ <string name="pages_fetching">Obtendo páginas...</string>
+ <string name="comments_fetching">Obtendo comentários...</string>
+ <string name="toast_err_post_uploading">Não é possível abrir o post enquanto o upload está em andamento</string>
+ <string name="posts_fetching">Obtendo posts...</string>
+ <string name="media_fetching">Obtendo mídia…</string>
+ <string name="stats_followers_total_email_paged">Exibindo de %1$d a %2$d de %3$s seguidores de e-mail</string>
+ <string name="stats_period">Período</string>
+ <string name="post_uploading">Fazendo upload</string>
+ <string name="logs_copied_to_clipboard">O logs do aplicativo foram copiados para a área de transferência</string>
+ <string name="reader_label_new_posts">Novos posts</string>
+ <string name="reader_empty_posts_in_blog">Este blog está vazio</string>
+ <string name="stats_recent_weeks">Semanas recentes</string>
+ <string name="stats_average_per_day">Média por dia</string>
+ <string name="error_copy_to_clipboard">Ocorreu um erro ao copiar o texto para a área de transferência</string>
+ <string name="stats_months_and_years">Meses e anos</string>
+ <string name="stats_overall">Geral</string>
+ <string name="stats_total">Total</string>
+ <string name="reader_page_recommended_blogs">Talvez você goste destes sites</string>
+ <string name="stats_comments_total_comments_followers">Total de posts com comentários de seguidores: %1$s</string>
+ <string name="stats_visitors">Visitantes</string>
+ <string name="stats_pagination_label">Página %1$s de %2$s</string>
+ <string name="stats_timeframe_years">Anos</string>
+ <string name="stats_views">Visualizações</string>
+ <string name="stats_view_countries">Países</string>
+ <string name="stats_likes">Curtidas</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Seguidores</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_view_top_posts_and_pages">Posts &amp; Páginas</string>
+ <string name="stats_view_videos">Vídeos</string>
+ <string name="stats_entry_publicize">Serviço</string>
+ <string name="stats_entry_followers">Seguidor</string>
+ <string name="stats_totals_publicize">Seguidores</string>
+ <string name="stats_entry_video_plays">Vídeo</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_empty_geoviews_desc">Explore a lista para descobrir que países e regiões geram mais visitas para o seu site.</string>
+ <string name="stats_totals_followers">Desde</string>
+ <string name="stats_empty_geoviews">Nenhum país registrado</string>
+ <string name="stats_empty_top_posts_title">Nenhum post ou página visualizado</string>
+ <string name="stats_empty_top_posts_desc">Descubra qual seu conteúdo mais visualizado e verifique o desempenho de posts e páginas individuais ao longo do tempo.</string>
+ <string name="stats_empty_referrers_title">Nenhum referrer registrado</string>
+ <string name="stats_empty_clicks_title">Nenhum clique registrado</string>
+ <string name="stats_empty_referrers_desc">Descubra os sites e termos de pesquisa que mais direcionam visitantes para o seu site, e aprenda mais sobre sua visibilidade.</string>
+ <string name="stats_empty_tags_and_categories">Nenhum post ou página marcado com tags foi visualizado.</string>
+ <string name="stats_empty_clicks_desc">Caso seu conteúdo tenha links para outros sites, você verá quais os links mais clicados por seus visitantes.</string>
+ <string name="stats_empty_top_authors_desc">Monitore as visualizações dos posts de cada colaborador e descubra em detalhes os conteúdos mais populares por autor.</string>
+ <string name="stats_empty_tags_and_categories_desc">Obtenha uma visão geral dos tópicos mais populares do seu site, de acordo com os principais posts da semana passada.</string>
+ <string name="stats_empty_comments_desc">Caso você permita comentários no site, monitore seus principais comentadores e descubra que tipo de conteúdo gera mais interesse, com base em seus 1.000 comentários mais recentes.</string>
+ <string name="stats_empty_video_desc">Se você tiver feito upload de vídeos com o VideoPress, descubra quantas vezes eles foram assistidos.</string>
+ <string name="stats_empty_video">Nenhum vídeo reproduzido</string>
+ <string name="stats_empty_publicize">Nenhum seguidor do publicize registrado</string>
+ <string name="stats_empty_followers">Nenhum seguidor</string>
+ <string name="stats_empty_publicize_desc">Monitore seus seguidores em várias redes sociais usando o publicize.</string>
+ <string name="stats_comments_by_posts_and_pages">Por posts &amp; páginas</string>
+ <string name="stats_empty_followers_desc">Monitore seu número de seguidores e há quanto tempo cada um deles segue seu site.</string>
+ <string name="stats_comments_by_authors">Por autores</string>
+ <string name="stats_followers_total_wpcom">Total de seguidores do WordPress.com: %1$s</string>
+ <string name="stats_followers_seconds_ago">segundos atrás</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_hours">%1$d horas</string>
+ <string name="stats_followers_a_day">Um dia</string>
+ <string name="stats_followers_days">%1$d dias</string>
+ <string name="stats_followers_a_minute_ago">um minuto atrás</string>
+ <string name="stats_followers_a_year">Um ano</string>
+ <string name="stats_followers_years">%1$d anos</string>
+ <string name="stats_followers_a_month">Um mês</string>
+ <string name="stats_followers_minutes">%1$d minutos</string>
+ <string name="stats_followers_an_hour_ago">uma hora atrás</string>
+ <string name="stats_for">Estatísticas para %s</string>
+ <string name="stats_other_recent_stats_label">Outras estatísticas recentes</string>
+ <string name="stats_view_all">Ver todos</string>
+ <string name="stats_view">Exibir</string>
+ <string name="stats_followers_months">%1$d meses</string>
+ <string name="themes_fetching">Obtendo temas…</string>
+ <string name="stats_followers_email_selector">E-mail</string>
+ <string name="stats_followers_total_email">Total de seguidores por e-mail: %1$s</string>
+ <string name="ssl_certificate_details">Detalhes</string>
+ <string name="sure_to_remove_account">Remover este site?</string>
+ <string name="delete_sure_post">Excluir este post</string>
+ <string name="delete_sure">Excluir este rascunho</string>
+ <string name="delete_sure_page">Excluir esta página</string>
+ <string name="confirm_delete_multi_media">Excluir itens selecionados?</string>
+ <string name="confirm_delete_media">Excluir item selecionado?</string>
+ <string name="cab_selected">%d selecionados</string>
+ <string name="media_gallery_date_range">Exibindo mídias de %1$s a %2$s</string>
+ <string name="faq_button">Perguntas frequentes</string>
+ <string name="agree_terms_of_service">Ao criar sua conta, você está concordando com os nossos %1$sTermos de Serviço%2$s</string>
+ <string name="create_new_blog_wpcom">Crie um blog do WordPress.com</string>
+ <string name="new_blog_wpcom_created">Seu blog do WordPress.com foi criado!</string>
+ <string name="reader_empty_comments">Nenhum comentário</string>
+ <string name="reader_empty_posts_in_tag">Nenhum post com esta tag</string>
+ <string name="reader_label_comment_count_multi">%,d comentários</string>
+ <string name="reader_label_view_original">Ver o artigo original</string>
+ <string name="reader_label_like">Curtir</string>
+ <string name="reader_label_liked_by">Curtido por</string>
+ <string name="reader_label_comment_count_single">Um comentário</string>
+ <string name="reader_label_comments_closed">Os comentários estão desativados</string>
+ <string name="reader_label_comments_on">Comentários sobre</string>
+ <string name="reader_title_photo_viewer">%1$d de %2$d</string>
+ <string name="error_publish_empty_post">Não é possível publicar um post em branco</string>
+ <string name="error_refresh_unauthorized_posts">Você não tem permissão para visualizar ou editar posts</string>
+ <string name="error_refresh_unauthorized_pages">Você não tem permissão para visualizar ou editar páginas</string>
+ <string name="error_refresh_unauthorized_comments">Você não tem permissão para visualizar ou editar comentários</string>
+ <string name="older_month">Há mais de um mês</string>
+ <string name="more">Mais</string>
+ <string name="older_two_days">Há mais de 2 dias</string>
+ <string name="older_last_week">Há mais de uma semana</string>
+ <string name="stats_no_blog">Não foi possível carregar as estatísticas do blog</string>
+ <string name="select_a_blog">Selecione um site do WordPress</string>
+ <string name="uploading_total">Fazendo upload de %1$d de %2$d</string>
+ <string name="mnu_comment_liked">Curtiu</string>
+ <string name="comment">Comentário</string>
+ <string name="comment_trashed">Comentário enviado para a lixeira</string>
+ <string name="posts_empty_list">Nenhum post. Por que não criar um?</string>
+ <string name="comment_reply_to_user">Responder %s</string>
+ <string name="pages_empty_list">Nenhuma página. Por que não criar uma?</string>
+ <string name="media_empty_list_custom_date">Nenhuma mídia no intervalo de tempo especificado</string>
+ <string name="posting_post">Publicando "%s"</string>
+ <string name="signing_out">Saindo…</string>
+ <string name="reader_empty_posts_liked">Você não curtiu posts ainda</string>
+ <string name="browse_our_faq_button">Veja nossa seção de Perguntas Frequentes</string>
+ <string name="nux_help_description">Visite a central de ajuda para obter respostas para perguntas frequentes, ou visite os fóruns para fazer novas perguntas.</string>
+ <string name="sending_content">Fazendo upload do conteúdo de %s</string>
+ <string name="reader_empty_followed_blogs_title">Você ainda não está seguindo um site</string>
+ <string name="reader_menu_block_blog">Bloquear este blog</string>
+ <string name="reader_toast_err_block_blog">Não é possível bloquear este blog</string>
+ <string name="reader_toast_err_generic">Não é possível executar esta ação</string>
+ <string name="reader_toast_blog_blocked">Os posts deste blog não serão mais exibidos</string>
+ <string name="contact_us">Contate nos</string>
+ <string name="hs__conversation_detail_error">Descreva o problema que está experimentando</string>
+ <string name="hs__new_conversation_header">Chat de Suporte</string>
+ <string name="hs__conversation_header">Chat de Suporte</string>
+ <string name="hs__username_blank_error">Digite um nome válido</string>
+ <string name="hs__invalid_email_error">Digite um e-mail válido</string>
+ <string name="add_location">Adicionar localização</string>
+ <string name="current_location">Localização atual</string>
+ <string name="search_location">Pesquisa</string>
+ <string name="search_current_location">Localizar</string>
+ <string name="edit_location"> Editar</string>
+ <string name="preference_send_usage_stats">Enviar estatísticas</string>
+ <string name="preference_send_usage_stats_summary">Envie estatísticas de uso automaticamente para nos ajudar a melhorar o WordPress para Android</string>
+ <string name="update_verb">Atualizar</string>
+ <string name="schedule_verb">Agendar</string>
+ <string name="reader_title_subs">Tags e Blogs</string>
+ <string name="reader_page_followed_tags">Tags seguidas</string>
+ <string name="reader_label_followed_blog">Seguindo o blog</string>
+ <string name="reader_label_tag_preview">Posts com tag %s</string>
+ <string name="reader_toast_err_get_blog_info">Não é possível mostrar o blog</string>
+ <string name="reader_toast_err_already_follow_blog">Você já segue o blog</string>
+ <string name="reader_toast_err_follow_blog">Não é possível seguir o blog</string>
+ <string name="reader_toast_err_unfollow_blog">Não é possível deixar de seguir o blog</string>
+ <string name="reader_empty_recommended_blogs">Nenhum blog recomendado</string>
+ <string name="reader_title_blog_preview">Blog do Leitor</string>
+ <string name="reader_title_tag_preview">Tag do Leitor</string>
+ <string name="reader_page_followed_blogs">Sites seguidos</string>
+ <string name="reader_hint_add_tag_or_url">Insira um URL ou tag para seguir</string>
+ <string name="saving">Salvando...</string>
+ <string name="media_empty_list">Nenhuma mídia</string>
+ <string name="ptr_tip_message">Dica: Arraste para baixo para atualizar</string>
+ <string name="help">Ajuda</string>
+ <string name="forgot_password">Perdeu a senha?</string>
+ <string name="forums">Fóruns</string>
+ <string name="help_center">Central de ajuda</string>
+ <string name="ssl_certificate_error">Certificado SSL inválido</string>
+ <string name="ssl_certificate_ask_trust">Se você geralmente se conecta a este site sem problemas, talvez este erro signifique que alguém está tentando imitar o site, e você não deve continuar. Deseja confiar no certificado mesmo assim?</string>
+ <string name="notifications_empty_list">Sem notificações</string>
+ <string name="out_of_memory">Dispositivo sem memória</string>
+ <string name="no_network_message">Não há rede disponível</string>
+ <string name="gallery_error">O item de mídia não pode ser recuperado</string>
+ <string name="blog_not_found">Ocorreu um erro ao acessar este blog</string>
+ <string name="wait_until_upload_completes">Aguarde até que o upload esteja completo</string>
+ <string name="comments_empty_list">Sem comentários</string>
+ <string name="mnu_comment_unspam">Não é spam</string>
+ <string name="theme_auth_error_message">Tenha certeza que você tem privilégio para definir um tema</string>
+ <string name="no_site_error">Não foi possível se conectar ao site do WordPress</string>
+ <string name="adding_cat_failed">Problema ao adicionar categoria</string>
+ <string name="could_not_remove_account">Não foi possível remover o site</string>
+ <string name="stats_bar_graph_empty">Nenhuma estatística disponível</string>
+ <string name="invalid_url_message">Verifique se a URL do blog inserida é válida</string>
+ <string name="error_generic">Ocorreu um erro</string>
+ <string name="error_edit_comment">Ocorreu um erro ao editar o comentário</string>
+ <string name="error_load_comment">Não foi possível carregar o comentário</string>
+ <string name="passcode_wrong_passcode">PIN errado</string>
+ <string name="username_not_allowed">Nome de usuário não permitido</string>
+ <string name="adding_cat_success">Categoria adicionada com êxito</string>
+ <string name="cat_name_required">O campo de nome da categoria é obrigatório</string>
+ <string name="category_automatically_renamed">O nome da categoria %1$s não é válido. Ela foi renomeada para %2$s.</string>
+ <string name="no_account">Não foi encontrada uma conta WordPress, adicione uma conta e tente novamente</string>
+ <string name="theme_fetch_failed">Falha ao buscar temas</string>
+ <string name="theme_set_failed">Falha ao definir tema</string>
+ <string name="sdcard_message">Um cartão SD montado é necessário para fazer upload de mídia</string>
+ <string name="reply_failed">Ocorreu um erro na resposta</string>
+ <string name="stats_empty_comments">Nenhum comentário</string>
+ <string name="error_delete_post">Ocorreu um erro ao excluir o(a) %s</string>
+ <string name="error_refresh_posts">Não foi possível atualizar os posts no momento</string>
+ <string name="error_refresh_pages">Não foi possível atualizar as páginas no momento</string>
+ <string name="error_refresh_notifications">Não foi possível atualizar as notificações no momento</string>
+ <string name="error_refresh_comments">Não foi possível atualizar os comentários no momento</string>
+ <string name="error_refresh_stats">Não foi possível atualizar as estatísticas no momento</string>
+ <string name="error_moderate_comment">Ocorreu um erro ao moderar</string>
+ <string name="error_upload">Ocorreu um erro ao carregar o(a) %s</string>
+ <string name="error_downloading_image">Erro ao baixar a imagem</string>
+ <string name="invalid_password_message">A senha deve ter 4 caracteres no mínimo</string>
+ <string name="invalid_username_too_short">O nome de usuário deve ter mais de 4 caracteres</string>
+ <string name="invalid_username_too_long">O nome de usuário deve ter 61 caracteres no máximo</string>
+ <string name="username_only_lowercase_letters_and_numbers">O nome de usuário pode ter apenas letras minúsculas (a-z) e números</string>
+ <string name="username_required">Digite um nome de usuário</string>
+ <string name="username_must_be_at_least_four_characters">O nome de usuário deve ter 4 caracteres no mínimo</string>
+ <string name="username_contains_invalid_characters">O nome de usuário não pode ter o caractere “_”</string>
+ <string name="username_must_include_letters">O nome de usuário deve ter pelo menos uma letra (a-z)</string>
+ <string name="username_exists">O nome de usuário já existe</string>
+ <string name="username_reserved_but_may_be_available">No momento, o nome de usuário está reservado, mas pode estar disponível em alguns dias</string>
+ <string name="blog_name_required">Digite um endereço de site</string>
+ <string name="blog_name_not_allowed">O endereço do site não é permitido</string>
+ <string name="blog_name_must_be_at_least_four_characters">O endereço do site deve ter 4 caracteres no mínimo</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">O endereço do site 64 caracteres</string>
+ <string name="blog_name_contains_invalid_characters">O endereço do site não deve ter o caractere “_”</string>
+ <string name="blog_name_cant_be_used">Não é possível usar esse endereço de site</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">O endereço do site deve ter apenas letras minúsculas (a-z) e números</string>
+ <string name="blog_name_exists">O site já existe</string>
+ <string name="blog_name_reserved">O site está reservado</string>
+ <string name="blog_name_reserved_but_may_be_available">No momento, o site está reservado, mas pode estar disponível em alguns dias</string>
+ <string name="username_or_password_incorrect">O nome de usuário ou a senha digitada está incorreta</string>
+ <string name="nux_cannot_log_in">Não é possível conectar</string>
+ <string name="invalid_email_message">Seu endereço de e-mail é inválido</string>
+ <string name="email_invalid">Digite um endereço de e-mail válido</string>
+ <string name="email_not_allowed">O endereço de e-mail não é permitido</string>
+ <string name="email_exists">O endereço de e-mail já está em uso</string>
+ <string name="blog_removed_successfully">Site removido com sucesso</string>
+ <string name="remove_account">Remover site</string>
+ <string name="select_categories">Selecionar categorias</string>
+ <string name="account_details">Detalhes da conta</string>
+ <string name="edit_post">Editar post</string>
+ <string name="add_comment">Adicionar comentário</string>
+ <string name="connection_error">Erro de conexão</string>
+ <string name="cancel_edit">Cancelar edição</string>
+ <string name="post_not_found">Ocorreu um erro ao carregar o post. Atualize seus posts e tente novamente.</string>
+ <string name="learn_more">Saiba mais</string>
+ <string name="media_gallery_settings_title">Configurações da galeria</string>
+ <string name="media_gallery_image_order">Ordem da imagem</string>
+ <string name="media_gallery_num_columns">Número de colunas</string>
+ <string name="media_gallery_edit">Editar galeria</string>
+ <string name="theme_current_theme">Tema atual</string>
+ <string name="create_a_link">Criar um link</string>
+ <string name="page_settings">Configurações de página</string>
+ <string name="local_draft">Rascunho local</string>
+ <string name="horizontal_alignment">Alinhamento horizontal</string>
+ <string name="delete_post">Excluir post</string>
+ <string name="delete_page">Excluir página</string>
+ <string name="comment_status_approved">Aprovado</string>
+ <string name="comment_status_unapproved">Pendente</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="edit_comment">Editar comentário</string>
+ <string name="mnu_comment_approve">Aprovar</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="dlg_approving_comments">Aprovando</string>
+ <string name="dlg_spamming_comments">Marcando como spam</string>
+ <string name="author_name">Nome do autor</string>
+ <string name="author_url">URL do autor</string>
+ <string name="hint_comment_content">Comentário</string>
+ <string name="saving_changes">Salvando alterações</string>
+ <string name="delete_draft">Excluir rascunho</string>
+ <string name="preview_page">Visualizar página</string>
+ <string name="preview_post">Visualizar post</string>
+ <string name="comment_added">Comentário adicionado com êxito</string>
+ <string name="add_new_category">Adicionar nova categoria</string>
+ <string name="share_action_post">Novo post</string>
+ <string name="share_action_media">Biblioteca de mídia</string>
+ <string name="pending_review">Análise pendente</string>
+ <string name="http_credentials">Credenciais HTTP (opcional)</string>
+ <string name="new_post">Novo post</string>
+ <string name="new_media">Nova mídia</string>
+ <string name="privacy_policy">Política de privacidade</string>
+ <string name="image_settings">Configurações de imagem</string>
+ <string name="add_account_blog_url">Endereço do blog</string>
+ <string name="reader_title_applog">Log do aplicativo</string>
+ <string name="reader_share_link">Compartilhar link</string>
+ <string name="required_field">Campo obrigatório</string>
+ <string name="blog_name_invalid">Endereço de site inválido</string>
+ <string name="xmlrpc_error">Não foi possível conectar. Digite o caminho completo para xmlrpc.php em seu site e tente novamente.</string>
+ <string name="scaled_image_error">Insira um valor de largura dimensionado válido</string>
+ <string name="media_gallery_type_thumbnail_grid">Grade de miniaturas</string>
+ <string name="media_error_no_permission">Você não tem permissões para visualizar a biblioteca de mídia</string>
+ <string name="cannot_delete_multi_media_items">No momento, não é possível excluir algumas mídias. Tente novamente mais tarde.</string>
+ <string name="themes_live_preview">Visualização ao vivo</string>
+ <string name="theme_premium_theme">Tema Premium</string>
+ <string name="link_enter_url_text">Vincular texto (opcional)</string>
+ <string name="upload_failed">Falha ao carregar</string>
+ <string name="file_not_found">Não foi possível encontrar o arquivo de mídia para carregar. Ele foi excluído ou movido?</string>
+ <string name="post_settings">Configurações do post</string>
+ <string name="comment_status_trash">Na lixeira</string>
+ <string name="mnu_comment_unapprove">Desaprovar</string>
+ <string name="mnu_comment_trash">Lixeira</string>
+ <string name="dlg_unapproving_comments">Desaprovando</string>
+ <string name="dlg_trashing_comments">Enviando para a lixeira</string>
+ <string name="dlg_confirm_trash_comments">Enviar para a lixeira?</string>
+ <string name="trash_yes">Lixeira</string>
+ <string name="trash_no">Não é lixo</string>
+ <string name="trash">Lixeira</string>
+ <string name="sure_to_cancel_edit_comment">Cancelar edição do comentário?</string>
+ <string name="content_required">Comentário obrigatório</string>
+ <string name="toast_comment_unedited">Comentário não foi alterado</string>
+ <string name="view_in_browser">Exibir no navegador</string>
+ <string name="category_name">Nome da categoria</string>
+ <string name="category_slug">Slug da categoria (opcional)</string>
+ <string name="category_desc">Descrição da categoria (opcional)</string>
+ <string name="category_parent">Categoria secundária (opcional):</string>
+ <string name="file_error_create">Não foi possível criar um arquivo temporário para carregar a mídia. Certifique-se de que há espaço livro suficiente em seu dispositivo.</string>
+ <string name="location_not_found">Localização desconhecida</string>
+ <string name="open_source_licenses">Abrir licenças da origem</string>
+ <string name="http_authorization_required">Autorização obrigatória</string>
+ <string name="post_format">Formato do post</string>
+ <string name="view_site">Exibir site</string>
+ <string name="local_changes">Alterações de local</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="error_blog_hidden">Este blog está oculto e não foi possível carregá-lo. Habilite-o em configurações e tente novamente.</string>
+ <string name="fatal_db_error">Ocorreu um erro ao criar o banco de dados do aplicativo. Tente reinstalar o aplicativo.</string>
+ <string name="jetpack_message_not_admin">O plug-in Jetpack é necessário para obter estatísticas. Contate o administrador do site.</string>
+ <string name="reader_toast_err_add_tag">Não é possível adicionar a tag</string>
+ <string name="reader_toast_err_remove_tag">Não é possível remover a tag</string>
+ <string name="site_address">Seu endereço auto-hospedado (URL)</string>
+ <string name="blog_name_must_include_letters">O endereço do site deve ter pelo menos uma letra (a-z)</string>
+ <string name="blog_title_invalid">Título do site inválido</string>
+ <string name="notifications_empty_all">Nenhuma notificação... ainda.</string>
+ <string name="invalid_site_url_message">Verifique se a URL do site inserida é válida</string>
+ <string name="page_not_published">A página não está publicada</string>
+ <string name="post_not_published">O post não está publicado</string>
+ <string name="author_email">E-mail do autor</string>
+ <string name="email_hint">Endereço de e-mail</string>
+ <string name="email_cant_be_used_to_signup">Não é possível usar esse endereço de e-mail para se inscrever. Esse provedor bloqueia alguns de nossos e-mails. Use outro provedor de e-mail.</string>
+ <string name="email_reserved">Este endereço de e-mail já está em uso Verifique se há um e-mail de ativação na sua caixa de entrada. Se você não ativar, poderá tentar novamente em alguns dias.</string>
+ <string name="deleting_page">Excluindo página</string>
+ <string name="deleting_post">Excluindo post</string>
+ <string name="share_url_post">Compartilhar post</string>
+ <string name="share_url_page">Compartilhar página</string>
+ <string name="share_link">Compartilhar link</string>
+ <string name="creating_your_account">Criando sua conta</string>
+ <string name="creating_your_site">Criando seu site</string>
+ <string name="error_refresh_media">Alguma coisa deu errado durante a atualização da biblioteca de mídia. Tente novamente mais tarde.</string>
+ <string name="reader_empty_posts_in_tag_updating">Coletando posts...</string>
+ <string name="reader_likes_you_and_multi">Você e %,d outros curtiram isso</string>
+ <string name="reader_likes_multi">%,d pessoas curtiram isso</string>
+ <string name="reader_toast_err_get_comment">Não foi possível recuperar esse comentário</string>
+ <string name="reader_label_reply">Responder</string>
+ <string name="video">Vídeo</string>
+ <string name="download">Fazendo download de mídia</string>
+ <string name="comment_spammed">Comentário marcado como spam</string>
+ <string name="cant_share_no_visible_blog">Você não pode compartilhar no WordPress sem um blog visível</string>
+ <string name="select_time">Selecione o tempo</string>
+ <string name="reader_likes_you_and_one">Você e mais um gostaram disso</string>
+ <string name="select_date">Selecione a data</string>
+ <string name="pick_photo">Selecione a foto</string>
+ <string name="account_two_step_auth_enabled">Essa conta possui a autenticação em duas etapas ativada. Visite suas configurações de segurança no WordPress.com e gere uma senha específica para aplicativos.</string>
+ <string name="pick_video">Selecionar vídeo</string>
+ <string name="reader_toast_err_get_post">Não foi possível recuperar esse post</string>
+ <string name="validating_user_data">Validando dados de usuário</string>
+ <string name="validating_site_data">Validando dados do site</string>
+ <string name="reader_empty_followed_blogs_description">Não se preocupe! Toque no ícone na parte superior direita para começar a explorar!</string>
+ <string name="password_invalid">Você precisa de uma senha mais segura. Use mais de 7 caracteres, misture letras maiúsculas com minúsculas, números ou caracteres especiais.</string>
+ <string name="nux_tap_continue">Continuar</string>
+ <string name="nux_welcome_create_account">Criar conta</string>
+ <string name="nux_add_selfhosted_blog">Adicionar site hospedado</string>
+ <string name="nux_oops_not_selfhosted_blog">Entrar no WordPress.com</string>
+ <string name="signing_in">Entrando...</string>
+ <string name="media_add_popup_title">Adicionar à biblioteca de mídia</string>
+ <string name="media_add_new_media_gallery">Criar galeria</string>
+ <string name="select_from_media_library">Selecione a partir de biblioteca de mídia</string>
+ <string name="jetpack_message">O plugin Jetpack é necessário para as estatísticas. Você quer instalar o Jetpack?</string>
+ <string name="jetpack_not_found">Plugin Jetpack não encontrado</string>
+ <string name="reader_btn_follow">Seguir </string>
+ <string name="reader_btn_share">Compartilhar</string>
+ <string name="reader_label_removed_tag">Excluído %s</string>
+ <string name="reader_likes_one">Uma pessoa curte isto</string>
+ <string name="reader_likes_only_you">Você curte isto</string>
+ <string name="reader_toast_err_comment_failed">Não foi possível postar o seu comentário </string>
+ <string name="reader_toast_err_tag_exists">Você já segue essa tag</string>
+ <string name="reader_toast_err_tag_invalid">Esta tag não é válida</string>
+ <string name="reader_toast_err_share_intent">Não é possível compartilhar </string>
+ <string name="reader_toast_err_view_image">Não é possível visualizar a imagem </string>
+ <string name="reader_toast_err_url_intent">Não é possível abrir %s</string>
+ <string name="button_next">Próximo </string>
+ <string name="reader_untitled_post">(Sem título)</string>
+ <string name="reader_share_subject">Compartilhado de %s</string>
+ <string name="reader_btn_unfollow">Seguindo</string>
+ <string name="connecting_wpcom">Conectando ao WordPress.com</string>
+ <string name="reader_hint_comment_on_comment">Responda para comentar</string>
+ <string name="nux_tutorial_get_started_title">Iniciar Tutorial</string>
+ <string name="username_invalid">Nome de usuário inválido</string>
+ <string name="limit_reached">Limite atingido. Você pode tentar novamente em 1 minuto. Tentando novamente antes só vai aumentar o tempo de espera antes do impedimento ser levantado. Se você acha que isso é um erro, contate o suporte.</string>
+ <string name="empty_list_default">Esta lista está vazia</string>
+ <string name="reader_label_added_tag">Adicionado %s</string>
+ <string name="reader_empty_followed_tags">Você não segue nenhuma tag</string>
+ <string name="create_account_wpcom">Crie uma conta no WordPress.com</string>
+ <string name="images">Imagens</string>
+ <string name="all">Tudo</string>
+ <string name="unattached">Desanexado</string>
+ <string name="media_add_popup_capture_photo">Capturar foto</string>
+ <string name="media_add_popup_capture_video">Capturar vídeo</string>
+ <string name="themes">Temas</string>
+ <string name="media_edit_title_text">Título</string>
+ <string name="media_edit_caption_text">Legenda</string>
+ <string name="media_edit_description_text">Descrição</string>
+ <string name="media_gallery_type">Tipo</string>
+ <string name="media_edit_description_hint">Escreva a descrição aqui</string>
+ <string name="media_edit_success">Atualizado</string>
+ <string name="media_edit_failure">Falha na atualização</string>
+ <string name="themes_details_label">Detalhes</string>
+ <string name="share_action_title">Adicionar para...</string>
+ <string name="stats_view_tags_and_categories">Tags e Categorias</string>
+ <string name="stats_view_clicks">Cliques</string>
+ <string name="stats_timeframe_today">Hoje</string>
+ <string name="stats_view_referrers">Referências</string>
+ <string name="stats_timeframe_yesterday">Ontem</string>
+ <string name="stats_timeframe_days">Dias</string>
+ <string name="stats_timeframe_weeks">Semanas</string>
+ <string name="stats_timeframe_months">Meses</string>
+ <string name="stats_entry_country">Pais</string>
+ <string name="stats_entry_posts_and_pages">Titulo</string>
+ <string name="stats_entry_tags_and_categories">Tópico</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_totals_views">Vistas</string>
+ <string name="stats_totals_clicks">Cliques</string>
+ <string name="stats_entry_referrers">Referências</string>
+ <string name="stats_totals_plays">Tocados</string>
+ <string name="media_gallery_type_squares">Quadrados</string>
+ <string name="media_gallery_type_circles">Circulos</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="media_edit_caption_hint">Digite uma legenda aqui</string>
+ <string name="media_edit_title_hint">Digite um título aqui</string>
+ <string name="media_gallery_image_order_reverse">Ao contrário</string>
+ <string name="post_excerpt">Resumo</string>
+ <string name="share_action">Compartilhar</string>
+ <string name="theme_activate_button">Ativado</string>
+ <string name="theme_activating_button">Ativando</string>
+ <string name="themes_features_label">Recursos</string>
+ <string name="media_gallery_type_tiled">Mosaico</string>
+ <string name="passcode_enter_passcode">Entre seu PIN</string>
+ <string name="passcode_enter_old_passcode">Entre seu PIN antigo</string>
+ <string name="passcode_re_enter_passcode">Entre seu PIN novamente</string>
+ <string name="passcode_change_passcode">Mudar PIN</string>
+ <string name="passcode_set">Definir PIN</string>
+ <string name="custom_date">Data personalizada</string>
+ <string name="media_gallery_image_order_random">Aleatório</string>
+ <string name="theme_auth_error_title">Falhou ao obter temas</string>
+ <string name="theme_set_success">Tema configurado com sucesso!</string>
+ <string name="passcode_turn_off">Desligar o bloqueio do código PIN</string>
+ <string name="passcode_turn_on">Ligar o bloqueio do código PIN</string>
+ <string name="passcode_preference_title">Código PIN</string>
+ <string name="passcode_manage">Gerenciar seu código PIN.</string>
+ <string name="stats_view_visitors_and_views">Visitantes e Visualizações</string>
+ <string name="stats">Estatísticas</string>
+ <string name="upload">Enviar</string>
+ <string name="discard">Descartar</string>
+ <string name="notifications">Notificações</string>
+ <string name="note_reply_successful">Resposta publicada</string>
+ <string name="new_notifications">%d novas notificações</string>
+ <string name="more_notifications">e %d mais.</string>
+ <string name="sign_in">Entrar</string>
+ <string name="follows">Segue</string>
+ <string name="loading">Carregando...</string>
+ <string name="httpuser">Usuário HTTP</string>
+ <string name="httppassword">Senha HTTP</string>
+ <string name="error_media_upload">Ocorreu um erro durante o envio de mídia</string>
+ <string name="post_content">Conteúdo (Toque para adicionar texto e mídia)</string>
+ <string name="publish_date">Publicar</string>
+ <string name="content_description_add_media">Adicionar mídia</string>
+ <string name="incorrect_credentials">Usuário ou senha incorreta.</string>
+ <string name="password">Senha</string>
+ <string name="username">Usuário</string>
+ <string name="reader">Leitor</string>
+ <string name="featured_in_post">Incluir imagem no conteúdo do post</string>
+ <string name="pages">Páginas</string>
+ <string name="caption">Legenda (opcional)</string>
+ <string name="width">Largura</string>
+ <string name="posts">Posts</string>
+ <string name="anonymous">Anônimo</string>
+ <string name="page">Página</string>
+ <string name="post">Post</string>
+ <string name="no_network_title">Não há rede disponível </string>
+ <string name="featured">Usar uma Imagem Destacada</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">usrenamedoblog</string>
+ <string name="scaled_image">Largura da imagem</string>
+ <string name="upload_scaled_image">Enviar e link para a imagem redimensionada</string>
+ <string name="scheduled">Agendado</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Enviando...</string>
+ <string name="version">Versão</string>
+ <string name="tos">Termos de Serviço</string>
+ <string name="app_title">WordPress para Android</string>
+ <string name="max_thumbnail_px_width">Largura padrão de imagem</string>
+ <string name="image_alignment">Alinhamento</string>
+ <string name="refresh">Atualizar</string>
+ <string name="untitled">Sem Título</string>
+ <string name="edit">Editar</string>
+ <string name="post_id">Post</string>
+ <string name="page_id">Página</string>
+ <string name="post_password">Senha (opcional)</string>
+ <string name="immediately">Imediatamente</string>
+ <string name="quickpress_add_alert_title">Definir o nome do atalho</string>
+ <string name="today">Hoje</string>
+ <string name="settings">Configurações</string>
+ <string name="share_url">Compartilhar URL</string>
+ <string name="quickpress_window_title">Selecione blog para atalho QuickPress</string>
+ <string name="quickpress_add_error">Nome do atalho não pode ser vazio</string>
+ <string name="publish_post">Publicar</string>
+ <string name="draft">Rascunho</string>
+ <string name="post_private">Privado</string>
+ <string name="upload_full_size_image">Fazer upload e obter link para a imagem no tamanho original</string>
+ <string name="title">Título</string>
+ <string name="tags_separate_with_commas">Tags (separe tags com vírgulas)</string>
+ <string name="categories">Categorias</string>
+ <string name="dlg_deleting_comments">Deletando comentários</string>
+ <string name="notification_vibrate">Vibrar</string>
+ <string name="notification_blink">Piscar luz de notificação</string>
+ <string name="notification_sound">Som de notificação</string>
+ <string name="status">Status</string>
+ <string name="location">Localização</string>
+ <string name="sdcard_title">Cartão SD necessário</string>
+ <string name="select_video">Selecionar um vídeo da galeria</string>
+ <string name="media">Mídia</string>
+ <string name="delete">Excluir</string>
+ <string name="none">Nenhum</string>
+ <string name="blogs">Blogs</string>
+ <string name="select_photo">Selecione uma foto da galeria</string>
+ <string name="yes">Sim</string>
+ <string name="no">Não</string>
+ <string name="error">Erro</string>
+ <string name="cancel">Cancelar</string>
+ <string name="save">Salvar</string>
+ <string name="add">Adicionar</string>
+ <string name="category_refresh_error">Erro na atualização da categoria</string>
+ <string name="on">em</string>
+ <string name="reply">Responder</string>
+ <string name="preview">Pré-visualizar</string>
+ <string name="notification_settings">Configurações de notificações</string>
+</resources>
diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml
new file mode 100644
index 000000000..383034eab
--- /dev/null
+++ b/WordPress/src/main/res/values-ro/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Contributor</string>
+ <string name="role_follower">Urmăritor</string>
+ <string name="role_viewer">Vizitator</string>
+ <string name="error_post_my_profile_no_connection">Nicio conexiune, nu-ți pot salva profilul</string>
+ <string name="alignment_none">Niciuna</string>
+ <string name="alignment_left">Stânga</string>
+ <string name="alignment_right">Dreapta</string>
+ <string name="site_settings_list_editor_action_mode_title">Selectate %1$d</string>
+ <string name="error_fetch_users_list">N-am putut aduce utilizatorii sitului</string>
+ <string name="plans_manage">Administrează-ți planul la\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Nu ai încă niciun vizitator.</string>
+ <string name="title_follower">Urmăritor</string>
+ <string name="title_email_follower">Urmăritor prin email</string>
+ <string name="people_fetching">Se aduc utilizatori...</string>
+ <string name="people_empty_list_filtered_email_followers">Nu ai încă niciun urmăritor prin email.</string>
+ <string name="people_empty_list_filtered_followers">Nu ai încă niciun urmăritor.</string>
+ <string name="people_empty_list_filtered_users">Nu ai încă niciun utilizator.</string>
+ <string name="people_dropdown_item_email_followers">Urmăritori prin email</string>
+ <string name="people_dropdown_item_viewers">Vizitatori</string>
+ <string name="people_dropdown_item_followers">Urmăritori</string>
+ <string name="people_dropdown_item_team">Echipă</string>
+ <string name="invite_message_usernames_limit">Invită până la 10 adrese email și/sau nume de utilizator WordPress.com. Celor care au nevoie de un nume de utilizator li se vor trimite instrucțiuni despre cum să creeze unul.</string>
+ <string name="viewer_remove_confirmation_message">Dacă înlături acest vizitator, el sau ea nu va mai putea vizita acest sit.\n\nMai vrei să înlături acest vizitator?</string>
+ <string name="follower_remove_confirmation_message">Dacă-l înlături, acest urmăritor nu va mai primi notificări despre acest sit, decât dacă-l reurmărește.\n\nMai vrei să înlături acest urmăritor?</string>
+ <string name="follower_subscribed_since">De la %1$s</string>
+ <string name="reader_label_view_gallery">Vizualizează galeria</string>
+ <string name="error_remove_follower">N-am putut înlătura urmăritorul</string>
+ <string name="error_remove_viewer">N-am putut înlătura vizitatorul</string>
+ <string name="error_fetch_email_followers_list">N-am putut aduce urmăritorii prin email ai sitului</string>
+ <string name="error_fetch_followers_list">N-am putut aduce urmăritorii sitului</string>
+ <string name="editor_failed_uploads_switch_html">Unele încărcări media au eșuat. Nu poți comuta la modul HTML\n în această situație. Înlături toate încărcările eșuate și continui?</string>
+ <string name="format_bar_description_html">Mod HTML</string>
+ <string name="visual_editor">Editor vizual</string>
+ <string name="image_thumbnail">Miniatură imagine</string>
+ <string name="format_bar_description_ol">Listă ordonată</string>
+ <string name="format_bar_description_ul">Listă neordonată</string>
+ <string name="format_bar_description_more">Inserează mai multe</string>
+ <string name="format_bar_description_media">Inserează media</string>
+ <string name="format_bar_description_strike">Barare</string>
+ <string name="format_bar_description_quote">Bloc citat</string>
+ <string name="format_bar_description_link">Inserează legătură</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Subliniere</string>
+ <string name="image_settings_save_toast">Modificări salvate</string>
+ <string name="image_caption">Text asociat</string>
+ <string name="image_alt_text">Text alternativ</string>
+ <string name="image_link_to">Legătură la</string>
+ <string name="image_width">Lățime</string>
+ <string name="format_bar_description_bold">Aldin</string>
+ <string name="image_settings_dismiss_dialog_title">Renunți la modificările nesalvate?</string>
+ <string name="stop_upload_dialog_title">Oprești încărcarea?</string>
+ <string name="stop_upload_button">Oprește încărcarea</string>
+ <string name="alert_error_adding_media">A apărut o eroare în timpul inserării media</string>
+ <string name="alert_action_while_uploading">Acum încarci media. Te rog așteaptă până când aceasta se termină.</string>
+ <string name="alert_insert_image_html_mode">Nu se poate insera media direct în modul HTML. Te rog comută înapoi la modul vizual.</string>
+ <string name="uploading_gallery_placeholder">Se încarcă galeria...</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Invitație trimisă cu succes</string>
+ <string name="tap_to_try_again">Atinge pentru a încerca din nou!</string>
+ <string name="invite_error_some_failed">Invitația a fost trimisă, dar a apărut cel puțin o eroare!</string>
+ <string name="invite_error_sending">A apărut o eroare în timpul încercării de trimitere a invitației!</string>
+ <string name="invite_error_invalid_usernames_multiple">Nu pot trimite: există nume de utilizator sau emailuri invalide</string>
+ <string name="invite_error_invalid_usernames_one">Nu pot trimite: un nume utilizator sau un email este invalid</string>
+ <string name="invite_error_no_usernames">Te rog adaugă cel puțin un nume utilizator</string>
+ <string name="invite_message_info">(Opțional) Poți introduce un mesaj personalizat de până la 500 de caractere care va fi inclus în invitația utilizatorului/utilizatorilor.</string>
+ <string name="invite_message_remaining_other">%d caractere rămase</string>
+ <string name="invite_message_remaining_one">1 caracter rămas</string>
+ <string name="invite_message_remaining_zero">Niciun caracter rămas</string>
+ <string name="invite_invalid_email">Adresa email \'%s\' este invalidă</string>
+ <string name="invite_message_title">Mesaj personalizat</string>
+ <string name="invite_already_a_member">Există deja un membru cu numele de utilizator \'%s\'</string>
+ <string name="invite_username_not_found">N-a fost găsit niciun utilizator cu numele \'%s\'</string>
+ <string name="invite">Invită</string>
+ <string name="invite_names_title">Nume de utilizatori sau emailuri</string>
+ <string name="send_link">Trimite legătura</string>
+ <string name="my_site_header_external">Extern</string>
+ <string name="invite_people">Invită persoane</string>
+ <string name="signup_succeed_signin_failed">Contul tău a fost creat, dar a apărut o eroare în timp ce te autentificam.\n Încearcă să te autentifici cu numele de utilizator și parola nou create.</string>
+ <string name="label_clear_search_history">Șterge istoric căutări</string>
+ <string name="dlg_confirm_clear_search_history">Ștergi istoricul căutărilor?</string>
+ <string name="reader_empty_posts_in_search_description">Nu s-a găsit niciun articol a lui %s în limba ta</string>
+ <string name="reader_label_post_search_running">Se caută...</string>
+ <string name="reader_label_related_posts">Lectură similară</string>
+ <string name="reader_empty_posts_in_search_title">Niciun articol găsit</string>
+ <string name="reader_label_post_search_explainer">Caută în toate blogurile publice WordPress.com</string>
+ <string name="reader_hint_post_search">Caută în WordPress.com</string>
+ <string name="reader_title_related_post_detail">Articol similar</string>
+ <string name="reader_title_search_results">Caută după %s</string>
+ <string name="preview_screen_links_disabled">Legăturile sunt dezactivate pe ecranul de previzualizare</string>
+ <string name="draft_explainer">Acest articol este o ciornă care nu a fost publicată</string>
+ <string name="send">Trimite</string>
+ <string name="person_removed">%1$s înlăturat cu succes</string>
+ <string name="user_remove_confirmation_message">Dacă-l înlături pe %1$s, acest utilizator nu va mai putea accesa acest sit, dar orice conținut care a fost creat de %1$s va rămâne pe sit.\n\nMai vrei să înlături acest utilizator?</string>
+ <string name="person_remove_confirmation_title">Înlătură %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Siturile din această listă nu au publicat nimic recent</string>
+ <string name="people">Oameni</string>
+ <string name="role">Rol</string>
+ <string name="edit_user">Editare utilizator</string>
+ <string name="error_remove_user">Nu s-a putut înlătura utilizatorul</string>
+ <string name="error_update_role">Nu s-a putut actualiza rolul utilizatorului</string>
+ <string name="error_fetch_viewers_list">N-am putut aduce vizitatorii sitului</string>
+ <string name="gravatar_camera_and_media_permission_required">Permisiuni necesare pentru a selecta sau a face o fotografie</string>
+ <string name="error_updating_gravatar">Eroare la actualizare Gravatar</string>
+ <string name="error_locating_image">Eroare de poziționare a imaginii decupate</string>
+ <string name="error_refreshing_gravatar">Eroare la reîncărcare Gravatar</string>
+ <string name="error_cropping_image">Eroare la decuparea imaginii</string>
+ <string name="gravatar_tip">Nou! Atinge-ți Gravatarul ca să-l schimbi!</string>
+ <string name="launch_your_email_app">Lansează-ți aplicația de email</string>
+ <string name="checking_email">Verificarea emailului</string>
+ <string name="not_on_wordpress_com">Nu ești pe WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">În prezent indisponibilă. Te rog să-ți introduci parola</string>
+ <string name="check_your_email">Verifică-ți emailul</string>
+ <string name="logging_in">Autentificat</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Obține o legătură trimisă pe emailul tău pentru conectare instantanee.</string>
+ <string name="enter_your_password_instead">Introdu-ți parola în schimb</string>
+ <string name="web_address_dialog_hint">Făcută publică atunci când comentezi.</string>
+ <string name="jetpack_not_connected_message">Modulul Jetpack este instalat, dar nu este conectat cu WordPress.com. Vrei să conectezi Jetpack?</string>
+ <string name="username_email">Email sau nume utilizator</string>
+ <string name="jetpack_not_connected">Modulul Jetpack nu este conectat</string>
+ <string name="new_editor_reflection_error">Editorul vizual nu este compatibil cu dispozitivul tău. A fost\n dezactivat automat.</string>
+ <string name="stats_insights_latest_post_no_title">(fără titlu)</string>
+ <string name="capture_or_pick_photo">Selectează sau fă o fotografie</string>
+ <string name="plans_post_purchase_text_themes">Acum ai acces nelimitat la teme Premium. Pentru a începe previzualizează orice temă pe situl tău.</string>
+ <string name="plans_post_purchase_button_themes">Răsfoiește temele</string>
+ <string name="plans_post_purchase_title_themes">Găsește tema Premium perfectă</string>
+ <string name="plans_post_purchase_button_video">Începe un articol nou</string>
+ <string name="plans_post_purchase_text_video">Poți încărca și găzdui videouri pe situl tău cu VideoPress și spațiul tău extins de stocare media.</string>
+ <string name="plans_post_purchase_title_video">Animează articole cu video</string>
+ <string name="plans_post_purchase_button_customize">Personalizează situl meu</string>
+ <string name="plans_post_purchase_text_customize">Acum ai acces la fonturi personalizate, culori personalizate și posibilități de editare CSS personalizat.</string>
+ <string name="plans_post_purchase_text_intro">Situl tău face tumbe de încântare! Acum explorează noile funcționalități ale sitului tău și alege de unde ai vrea să începi.</string>
+ <string name="plans_post_purchase_title_customize">Personalizează fonturile și culorile</string>
+ <string name="plans_post_purchase_title_intro">Este cu totul al tău, dă-i drumul!</string>
+ <string name="export_your_content_message">Articolele, paginile și setările îți vor fi trimise pe email la %s.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Planuri</string>
+ <string name="plans_loading_error">Nu pot încărca planurile</string>
+ <string name="export_your_content">Exportă conținutul tău</string>
+ <string name="exporting_content_progress">Exportare conținut...</string>
+ <string name="export_email_sent">Emailul de export a fost trimis!</string>
+ <string name="premium_upgrades_message">Ai actualizări premium active pe situl tău. Te rog anulează actualizările înainte de a șterge situl tău.</string>
+ <string name="show_purchases">Arată achizițiile</string>
+ <string name="checking_purchases">Verificare achiziții</string>
+ <string name="premium_upgrades_title">Actualizări Premium</string>
+ <string name="purchases_request_error">Ceva a fost greșit. Nu pot cere achizițiile.</string>
+ <string name="delete_site_progress">Ștergere sit...</string>
+ <string name="delete_site_summary">Această acțiune nu poate fi inversată. Ștergerea sitului tău va înlătura tot conținutul, contributorii și domeniile lui.</string>
+ <string name="delete_site_hint">Ștergere sit</string>
+ <string name="export_site_hint">Exportă situl tău într-un fișier XML</string>
+ <string name="are_you_sure">Sigur?</string>
+ <string name="export_site_summary">Dacă nu te răzgândești, te rog asigură-te că ți-ai făcut timp să exporți conținutul până acum. El nu va putea fi recuperat în viitor.</string>
+ <string name="keep_your_content">Păstrează conținutul tău</string>
+ <string name="domain_removal_hint">Domeniile ce nu vor mai funcționa după ce ai înlăturat situl tău</string>
+ <string name="domain_removal_summary">Atenție! Ștergerea sitului tău va înlătura, de asemenea, domeniul sau domeniile tale listate mai jos</string>
+ <string name="primary_domain">Domeniul primar</string>
+ <string name="domain_removal">Înlăturare domeniu</string>
+ <string name="error_deleting_site_summary">A apărut o eroare la ștergerea sitului tău. Te rog contactează suportul pentru mai multă asistență</string>
+ <string name="error_deleting_site">Eroare la ștergere sit</string>
+ <string name="confirm_delete_site_prompt">Te rog scrie %1$s în câmpul de mai jos pentru confirmare. Situl tău va fi dus apoi pentru totdeauna.</string>
+ <string name="site_settings_export_content_title">Export conținut</string>
+ <string name="contact_support">Contactează suportul</string>
+ <string name="confirm_delete_site">Confirmă ștergerea sitului</string>
+ <string name="start_over_text">Dacă vrei un sit dar nu mai vrei niciun articol ori pagină avute până acum, echipa noastră de suport poate să șteargă articolele și paginile tale, fișierele media și comentariile pentru tine.\n\nAsta va păstra situl și URL-ul active, dar îți va da posibilitatea unui start proaspăt în crearea conținutului tău. Contactează-ne doar dacă vrei să-ți curățăm conținutul curent.</string>
+ <string name="site_settings_start_over_hint">Reîncepe din nou situl</string>
+ <string name="let_us_help">Lasă-ne să te ajutăm</string>
+ <string name="me_btn_app_settings">Setări aplicație</string>
+ <string name="start_over">Reîncepe</string>
+ <string name="editor_remove_failed_uploads">Înlătură încărcările eșuate</string>
+ <string name="editor_toast_failed_uploads">Unele încărcări de media au eșuat. Poți salva sau publica\narticolul tău pe sit. Vrei să înlături toate fișierele media eșuate?</string>
+ <string name="comments_empty_list_filtered_trashed">Niciun comentariu la gunoi</string>
+ <string name="site_settings_advanced_header">Avansat</string>
+ <string name="comments_empty_list_filtered_pending">Niciun comentariu în așteptare</string>
+ <string name="comments_empty_list_filtered_approved">Niciun comentariu aprobat</string>
+ <string name="button_done">Gata</string>
+ <string name="button_skip">Sari</string>
+ <string name="site_timeout_error">Nu ne-am putut conecta la situl WordPress datorită unei erori de expirare timp.</string>
+ <string name="xmlrpc_malformed_response_error">Nu ne-am putut conecta. Instalarea WordPress a răspuns cu un document XML-RPC invalid.</string>
+ <string name="xmlrpc_missing_method_error">Nu ne-am putut conecta. Metodele XML-RPC necesare lipsesc de pe server.</string>
+ <string name="post_format_status">Stare</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Centru</string>
+ <string name="theme_free">Gratis</string>
+ <string name="theme_all">Toate</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_gallery">Galerie</string>
+ <string name="post_format_image">Imagine</string>
+ <string name="post_format_link">Legătură</string>
+ <string name="post_format_quote">Citat</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="post_format_chat">Discuție</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="post_format_aside">Notă</string>
+ <string name="notif_events">Informații despre cursurile și evenimente WordPress.com (online și în persoană).</string>
+ <string name="notif_surveys">Oportunități de a participa în cercetarea și sondaje WordPress.com.</string>
+ <string name="notif_tips">Sfaturi pentru a profita la maxim de WordPress.com.</string>
+ <string name="notif_community">Comunitate</string>
+ <string name="notif_research">Cercetare</string>
+ <string name="replies_to_my_comments">Răspunsuri la comentariile mele</string>
+ <string name="notif_suggestions">Sugestii</string>
+ <string name="site_achievements">Realizări sit</string>
+ <string name="username_mentions">Mențiuni nume utilizator</string>
+ <string name="likes_on_my_posts">Aprecieri pentru articolele mele</string>
+ <string name="site_follows">Urmăriri sit</string>
+ <string name="likes_on_my_comments">Aprecieri pentru comentariile mele</string>
+ <string name="comments_on_my_site">Comentarii pe situl meu</string>
+ <string name="site_settings_list_editor_summary_other">%d elemente</string>
+ <string name="site_settings_list_editor_summary_one">1 element</string>
+ <string name="approve_auto">Toți utilizatorii</string>
+ <string name="approve_auto_if_previously_approved">Comentariile utilizatorilor cunoscuți</string>
+ <string name="approve_manual">Niciun comentariu</string>
+ <string name="site_settings_paging_summary_other">%d comentarii per pagină</string>
+ <string name="site_settings_paging_summary_one">1 comentariu per pagină</string>
+ <string name="site_settings_multiple_links_summary_other">Necesită aprobare dacă are mai mult de %d legături</string>
+ <string name="site_settings_multiple_links_summary_one">Necesită aprobare dacă are mai mult de o legătură</string>
+ <string name="site_settings_multiple_links_summary_zero">Necesită aprobare dacă are mai mult de 0 legături</string>
+ <string name="detail_approve_auto">Aprobare automată a tuturor comentariilor.</string>
+ <string name="detail_approve_auto_if_previously_approved">Aprobare automată dacă utilizatorul are un comentariu aprobat anterior</string>
+ <string name="detail_approve_manual">Necesită aprobare manuală pentru comentariile oricui.</string>
+ <string name="days_quantity_one">1 zi</string>
+ <string name="days_quantity_other">%d zile</string>
+ <string name="filter_trashed_posts">Aruncat la gunoi</string>
+ <string name="filter_draft_posts">Ciorne</string>
+ <string name="filter_scheduled_posts">Programată</string>
+ <string name="filter_published_posts">Publicat</string>
+ <string name="pending_email_change_snackbar">Dă clic pe legătura de verificare din emailul trimis la %1$s pentru a confirma noua ta adresă</string>
+ <string name="primary_site">Sit primar</string>
+ <string name="web_address">Adresă web</string>
+ <string name="editor_toast_uploading_please_wait">Acum încarci media. Te rog așteaptă până ce asta se termină.</string>
+ <string name="error_refresh_comments_showing_older">Comentariile nu pot fi reîmprospătate acum - sunt arătate comentarii mai vechi</string>
+ <string name="editor_post_settings_set_featured_image">Setează imaginea reprezentativă</string>
+ <string name="editor_post_settings_featured_image">Imagine reprezentativă</string>
+ <string name="new_editor_promo_desc">aplicația WordPress pentru Android include acum un un nou și frumos \neditor vizual. Încearcă-l creând un nou articol.</string>
+ <string name="new_editor_promo_title">Editor nou nouț</string>
+ <string name="new_editor_promo_button_label">Super, mulțumesc!</string>
+ <string name="visual_editor_enabled">Editor vizual validat</string>
+ <string name="editor_content_placeholder">Partajează aici povestea ta...</string>
+ <string name="editor_page_title_placeholder">Titlu pagină</string>
+ <string name="editor_post_title_placeholder">Titlu articol</string>
+ <string name="email_address">Adresă email</string>
+ <string name="preference_show_visual_editor">Arată editorul vizual</string>
+ <string name="dlg_sure_to_delete_comments">Șterg permanent aceste comentarii?</string>
+ <string name="preference_editor">Editor</string>
+ <string name="dlg_sure_to_delete_comment">Șterg permanent acest comentariu?</string>
+ <string name="mnu_comment_delete_permanently">Ștergere</string>
+ <string name="comment_deleted_permanently">Comentariu șters</string>
+ <string name="mnu_comment_untrash">Restaurare</string>
+ <string name="comments_empty_list_filtered_spam">Niciun comentariu spam</string>
+ <string name="could_not_load_page">Nu pot încărca pagina</string>
+ <string name="comment_status_all">Tot</string>
+ <string name="interface_language">Limba interfeței</string>
+ <string name="off">Oprit</string>
+ <string name="about_the_app">Despre aplicație</string>
+ <string name="error_post_account_settings">N-am putut salva setările tale de cont</string>
+ <string name="error_post_my_profile">N-am putut salva profilul tău</string>
+ <string name="error_fetch_account_settings">N-am putut aduce setările tale de cont</string>
+ <string name="error_fetch_my_profile">N-am putut aduce profilul tău</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, am înțețes</string>
+ <string name="stats_widget_promo_desc">Adaugă piesa ecranului tău de pornire pentru a accesa statisticile dintr-un clic.</string>
+ <string name="stats_widget_promo_title">Piesă de statistici pe ecranul de pornire</string>
+ <string name="site_settings_unknown_language_code_error">Cod de limbă nerecunoscut</string>
+ <string name="site_settings_threading_dialog_description">Permite imbricarea comentariilor în fire.</string>
+ <string name="site_settings_threading_dialog_header">Pe fire până la</string>
+ <string name="remove">Înlătură</string>
+ <string name="search">Caută</string>
+ <string name="add_category">Adaugă categorie</string>
+ <string name="disabled">Dezactivat</string>
+ <string name="site_settings_image_original_size">Mărime originală</string>
+ <string name="privacy_private">Situl tău este vizibil doar pentru tine și pentru utilizatorii pe care i-ai aprobat</string>
+ <string name="privacy_public_not_indexed">Situl tău este vizibil pentru toată lumea, dar cere motoarelor de căutare să nu-l indexeze</string>
+ <string name="privacy_public">Situl tău este vizibil pentru toată lumea și poate fi indexat de motoarele de căutare</string>
+ <string name="about_me_hint">Câteva cuvinte despre tine...</string>
+ <string name="public_display_name_hint">Numele afișat va fi implicit numele utilizator dacă nu este setat</string>
+ <string name="about_me">Despre mine</string>
+ <string name="public_display_name">Numele afișat public</string>
+ <string name="my_profile">Profilul meu</string>
+ <string name="first_name">Prenume</string>
+ <string name="last_name">Nume</string>
+ <string name="site_privacy_public_desc">Permite motoarelor de căutare să indexeze acest sit</string>
+ <string name="site_privacy_hidden_desc">Descurajează motoarele de căutare de la indexarea acestui sit</string>
+ <string name="site_privacy_private_desc">Aș vrea ca situl meu să fie privat, vizibil doar utilizatorilor pe care i-am ales</string>
+ <string name="cd_related_post_preview_image">Imagine previzualizare articol similar</string>
+ <string name="error_post_remote_site_settings">N-am putut salva informațiile despre sit</string>
+ <string name="error_fetch_remote_site_settings">N-am putut primi informațiile despre sit</string>
+ <string name="error_media_upload_connection">A apărut o eroare în timp ce se încărca media</string>
+ <string name="site_settings_disconnected_toast">Deconectat, editare dezactivată.</string>
+ <string name="site_settings_unsupported_version_error">Versiune de WordPress nesuportată</string>
+ <string name="site_settings_multiple_links_dialog_description">Necesită aprobarea pentru comentarii ce includ mai mult decât acest număr de legături.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Închidere automată</string>
+ <string name="site_settings_close_after_dialog_description">Închide automat comentariile articolelor.</string>
+ <string name="site_settings_paging_dialog_description">Împarte firele de comentarii în mai multe pagini.</string>
+ <string name="site_settings_paging_dialog_header">Comentarii per pagină</string>
+ <string name="site_settings_close_after_dialog_title">Închidere comentare</string>
+ <string name="site_settings_blacklist_description">Când un comentariu conține oricare dintre aceste cuvinte în conținutul său, în nume, URL, email sau IP, va fi marcat ca spam. Poți introduce cuvinte parțiale, așa că "press" se va potrivi și pentru "WordPress".</string>
+ <string name="site_settings_hold_for_moderation_description">Când un comentariu conține oricare dintre aceste cuvinte în conținutul său, în nume, URL, email sau IP, va fi reținut în coada de moderare. Poți introduce cuvinte parțiale, așa că "press" se va potrivi și pentru "WordPress".</string>
+ <string name="site_settings_list_editor_input_hint">Introdu un cuvânt sau frază</string>
+ <string name="site_settings_list_editor_no_items_text">Niciun element</string>
+ <string name="site_settings_learn_more_caption">Poți suprascrie aceste setări pentru fiecare articol.</string>
+ <string name="site_settings_rp_preview3_site">în "Actualizare"</string>
+ <string name="site_settings_rp_preview3_title">Focalizare actualizare: VideoPress pentru nunți</string>
+ <string name="site_settings_rp_preview2_site">în "Applicații"</string>
+ <string name="site_settings_rp_preview2_title">Aplicația WordPress pentru Android a primit o mare actualizare a aspectului</string>
+ <string name="site_settings_rp_preview1_site">în "Mobile"</string>
+ <string name="site_settings_rp_preview1_title">Este disponibilă o importantă actualizare pentru iPhone/iPad</string>
+ <string name="site_settings_rp_show_images_title">Arată imaginile</string>
+ <string name="site_settings_rp_show_header_title">Arată antetul</string>
+ <string name="site_settings_rp_switch_summary">Articole similare afișează conținut relevant de pe situl tău sub articolele tale.</string>
+ <string name="site_settings_rp_switch_title">Arată articolele similare</string>
+ <string name="site_settings_delete_site_hint">Înlătură datele sitului tău din aplicație</string>
+ <string name="site_settings_blacklist_hint">Comentariile care corespund unui filtru sunt marcate ca spam</string>
+ <string name="site_settings_moderation_hold_hint">Comentariile care corespund unui filtru sunt puse în coada de moderare</string>
+ <string name="site_settings_multiple_links_hint">Ignoră limita de legături pentru utilizatorii cunoscuți</string>
+ <string name="site_settings_whitelist_hint">Autorul comentariului trebuie să mai aibă un comentariu anterior aprobat</string>
+ <string name="site_settings_user_account_required_hint">Utilizatorul trebuie să fie înregistrat și autentificat pentru a comenta</string>
+ <string name="site_settings_identity_required_hint">Autorul comentariului trebuie să completeze numele și emailul său</string>
+ <string name="site_settings_manual_approval_hint">Comentariile trebuie să fie aprobate manual</string>
+ <string name="site_settings_paging_hint">Afișează comentariile în tranșe de o anumită mărime</string>
+ <string name="site_settings_threading_hint">Permite imbricarea comentariilor până la un anumit nivel</string>
+ <string name="site_settings_sort_by_hint">Determină ordinea afișării comentariilor</string>
+ <string name="site_settings_close_after_hint">Interzicere comentarii după un anumit timp</string>
+ <string name="site_settings_receive_pingbacks_hint">Permite notificări despre legături de la alte bloguri</string>
+ <string name="site_settings_send_pingbacks_hint">Încearcă să notifici orice bloguri cu legături din articol</string>
+ <string name="site_settings_allow_comments_hint">Permite cititorilor să publice comentarii</string>
+ <string name="site_settings_discussion_hint">Vezi și modifică setările pentru discuții ale siturilor tale</string>
+ <string name="site_settings_more_hint">Vezi toate setările disponibile pentru discuții</string>
+ <string name="site_settings_related_posts_hint">Arată sau ascunde articolele similare în cititor</string>
+ <string name="site_settings_upload_and_link_image_hint">Validează ca întotdeauna imaginile să fie încărcate la dimensiune completă</string>
+ <string name="site_settings_image_width_hint">Rescalează imaginile în articole la această lățime</string>
+ <string name="site_settings_format_hint">Setare format nou de articol</string>
+ <string name="site_settings_category_hint">Setare categorie nouă de articol</string>
+ <string name="site_settings_location_hint">Adaugă automat informații despre locație articolelor tale</string>
+ <string name="site_settings_password_hint">Schimbă-ți parola</string>
+ <string name="site_settings_username_hint">Cont utilizator actual</string>
+ <string name="site_settings_language_hint">Limba principală în care se scrie pe blogul tău</string>
+ <string name="site_settings_privacy_hint">Controlează cine poate vedea situl tău</string>
+ <string name="site_settings_address_hint">Schimbarea adresei nu este încă suportată</string>
+ <string name="site_settings_tagline_hint">O scurtă descriere sau o frază de impact pentru a descrie blogul tău</string>
+ <string name="site_settings_title_hint">Explică, în câteva cuvinte, despre ce este acest sit</string>
+ <string name="site_settings_whitelist_known_summary">Comentarii de la utilizatori necunoscuți</string>
+ <string name="site_settings_whitelist_all_summary">Comentarii de la toți utilizatorii</string>
+ <string name="site_settings_threading_summary">%d nivele</string>
+ <string name="site_settings_privacy_private_summary">Privat</string>
+ <string name="site_settings_privacy_hidden_summary">Ascuns</string>
+ <string name="site_settings_delete_site_title">Șterge sit</string>
+ <string name="site_settings_privacy_public_summary">Public</string>
+ <string name="site_settings_blacklist_title">Lista neagră</string>
+ <string name="site_settings_moderation_hold_title">Reținere pentru moderare</string>
+ <string name="site_settings_multiple_links_title">Legături în comentarii</string>
+ <string name="site_settings_whitelist_title">Aprobare automată</string>
+ <string name="site_settings_threading_title">Fire de discuție</string>
+ <string name="site_settings_paging_title">Paginare</string>
+ <string name="site_settings_sort_by_title">Sortează după</string>
+ <string name="site_settings_account_required_title">Utilizatorul trebuie să fie autentificat</string>
+ <string name="site_settings_identity_required_title">Trebuie să includă nume și email</string>
+ <string name="site_settings_receive_pingbacks_title">Primire pingback-uri</string>
+ <string name="site_settings_send_pingbacks_title">trimite pingback-uri</string>
+ <string name="site_settings_allow_comments_title">Permite comentarii</string>
+ <string name="site_settings_default_format_title">Format implicit</string>
+ <string name="site_settings_default_category_title">Categorie implicită</string>
+ <string name="site_settings_location_title">Activare Locație</string>
+ <string name="site_settings_address_title">Adresă</string>
+ <string name="site_settings_title_title">Titlu sit</string>
+ <string name="site_settings_tagline_title">Slogan</string>
+ <string name="site_settings_this_device_header">Acest dispozitiv</string>
+ <string name="site_settings_discussion_new_posts_header">Implicite pentru articole noi</string>
+ <string name="site_settings_account_header">Cont</string>
+ <string name="site_settings_writing_header">Scriere</string>
+ <string name="newest_first">Cele mai noi la început</string>
+ <string name="site_settings_general_header">Generale</string>
+ <string name="discussion">Discuție</string>
+ <string name="privacy">Confidențialitate</string>
+ <string name="related_posts">Articole similare</string>
+ <string name="comments">Comentarii</string>
+ <string name="close_after">Închise după</string>
+ <string name="oldest_first">Cele mai vechi la început</string>
+ <string name="media_error_no_permission_upload">Nu ai permisiunea de a încărca media pe sit</string>
+ <string name="never">Niciodată</string>
+ <string name="unknown">Necunoscut</string>
+ <string name="reader_err_get_post_not_found">Acest articol nu mai există</string>
+ <string name="reader_err_get_post_not_authorized">Nu ești autorizat să vezi acest articol</string>
+ <string name="reader_err_get_post_generic">Acest articol nu poate fi regăsit</string>
+ <string name="blog_name_no_spaced_allowed">Adresa sitului nu poate conține spații</string>
+ <string name="invalid_username_no_spaces">Numele utilizator nu poate conține spații</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Siturile pe care le urmărești n-au publicat nimic recent</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Niciun articol recent</string>
+ <string name="media_details_copy_url_toast">URL copiat în clipbord</string>
+ <string name="edit_media">Editare media</string>
+ <string name="media_details_copy_url">Copiere URL</string>
+ <string name="media_details_label_date_uploaded">Încărcat</string>
+ <string name="media_details_label_date_added">Adăugat</string>
+ <string name="selected_theme">Tema selectată</string>
+ <string name="could_not_load_theme">Nu pot încărca tema</string>
+ <string name="theme_activation_error">Ceva a mers prost. Nu pot activa tema</string>
+ <string name="theme_by_author_prompt_append">de %1$s</string>
+ <string name="theme_prompt">Îți mulțumim că ai ales %1$s</string>
+ <string name="theme_try_and_customize">Încearcă și personalizează</string>
+ <string name="theme_view">Vezi</string>
+ <string name="theme_details">Detalii</string>
+ <string name="theme_support">Suport</string>
+ <string name="theme_done">GATA</string>
+ <string name="theme_manage_site">ADMINISTRARE SIT</string>
+ <string name="title_activity_theme_support">Teme</string>
+ <string name="theme_activate">Activată</string>
+ <string name="date_range_start_date">Dată de început</string>
+ <string name="date_range_end_date">Dată de sfârșit</string>
+ <string name="current_theme">Temă curentă</string>
+ <string name="customize">Personalizare</string>
+ <string name="details">Detalii</string>
+ <string name="support">Suport</string>
+ <string name="active">Activă</string>
+ <string name="stats_referrers_spam_generic_error">Ceva a mers prost în timpul operației. Starea de spam n-a fost schimbată.</string>
+ <string name="stats_referrers_marking_not_spam">Marcare ca ne-spam</string>
+ <string name="stats_referrers_unspam">Nu e spam</string>
+ <string name="stats_referrers_marking_spam">Marcare ca spam</string>
+ <string name="post_published">Articol publicat</string>
+ <string name="page_published">Pagină publicată</string>
+ <string name="post_updated">Articol actualizat</string>
+ <string name="page_updated">Pagină actualizată</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_auth_error_authenticate">Aducerea temelor a eșuat: eroare autentificare utilizator</string>
+ <string name="theme_no_search_result_found">Regret, nicio temă găsită.</string>
+ <string name="media_file_name">Nume fișier: %s</string>
+ <string name="media_uploaded_on">Încărcat la: %s</string>
+ <string name="media_dimensions">Dimensiuni: %s</string>
+ <string name="upload_queued">În coadă</string>
+ <string name="media_file_type">Tip fișier: %s</string>
+ <string name="reader_label_gap_marker">Încarcă mai multe articole</string>
+ <string name="notifications_no_search_results">Niciun site nu s-a potrivit cu \'%s\'</string>
+ <string name="search_sites">Căutare situri</string>
+ <string name="notifications_empty_view_reader">Vezi Cititorul</string>
+ <string name="unread">Necitite</string>
+ <string name="notifications_empty_action_followers_likes">Remarcă-te: comentează articolele pe care le-ai citit.</string>
+ <string name="notifications_empty_action_comments">Alătură-te conversației: comentează articolele blogurilor pe care le urmărești.</string>
+ <string name="notifications_empty_action_unread">Reaprinde conversația: scrie un nou articol.</string>
+ <string name="notifications_empty_action_all">Fii activ! Comentează articolele de pe blogurile pe care le urmărești.</string>
+ <string name="notifications_empty_likes">Nicio nouă legătură de arătat... încă.</string>
+ <string name="notifications_empty_followers">Niciun nou urmăritor de raportat... încă.</string>
+ <string name="notifications_empty_comments">Niciun nou comentariu... încă.</string>
+ <string name="notifications_empty_unread">Le-ai prins pe toate!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Te rog accesează statisticile în aplicație și încearcă adăugarea piesei mai târziu</string>
+ <string name="stats_widget_error_readd_widget">Te rog îndepărtează piesa și re-atașeaz-o.</string>
+ <string name="stats_widget_error_no_visible_blog">Statisticile nu pot fi accesate fără un blog vizibil</string>
+ <string name="stats_widget_error_no_permissions">Contul tău WordPress.com nu poate accesa statisticile pe acest blog</string>
+ <string name="stats_widget_error_no_account">Te rog autentifică-te la WordPress</string>
+ <string name="stats_widget_error_generic">Statisticile n-au putut fi încărcate</string>
+ <string name="stats_widget_loading_data">Încarc date...</string>
+ <string name="stats_widget_name_for_blog">Statisticile zilei pentru %1$s</string>
+ <string name="stats_widget_name">Statisticile WordPress ale zilei </string>
+ <string name="add_location_permission_required">Ai nevoie de permisiune pentru a adăuga locația</string>
+ <string name="add_media_permission_required">Permisiuni necesare pentru a adăuga media</string>
+ <string name="access_media_permission_required">Permisiuni necesare pentru a accesa media</string>
+ <string name="stats_enable_rest_api_in_jetpack">Pentru a vedea statisticile tale, validează extensia JSON API din Jetpack.</string>
+ <string name="error_open_list_from_notification">Acest articol sau pagină a fost publicat(ă) pe alt sit</string>
+ <string name="reader_short_comment_count_multi">%s comentarii</string>
+ <string name="reader_short_comment_count_one">1 comentariu</string>
+ <string name="reader_label_submit_comment">TRIMITE</string>
+ <string name="reader_hint_comment_on_post">Răspunde la articol...</string>
+ <string name="reader_discover_visit_blog">Vizitează %s</string>
+ <string name="reader_discover_attribution_blog">Inițial publicat pe %s</string>
+ <string name="reader_discover_attribution_author">Inițial publicat de %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Inițial publicat de %1$s pe %2$s</string>
+ <string name="reader_short_like_count_multi">%s aprecieri</string>
+ <string name="reader_short_like_count_one">1 apreciere</string>
+ <string name="reader_label_follow_count">%,d urmăritori</string>
+ <string name="reader_short_like_count_none">Aprecieri</string>
+ <string name="reader_menu_tags">Editare etichete și bloguri</string>
+ <string name="reader_title_post_detail">Cititor articol</string>
+ <string name="local_draft_explainer">Acest articol este o ciornă locală care nu a fost publicată</string>
+ <string name="local_changes_explainer">Acest articol are modificări locale care nu au fost publicate</string>
+ <string name="notifications_push_summary">Setări pentru notificările ce apar pe dispozitivul tău mobil.</string>
+ <string name="notifications_email_summary">Setări pentru notificările ce sunt trimise la emailul atașat contului tău.</string>
+ <string name="notifications_tab_summary">Setările pentru notificările ce apar în fila Notificări.</string>
+ <string name="notifications_disabled">Notificările aplicației au fost dezactivate. Apăsați aici pentru a le activa în Setări.</string>
+ <string name="notification_types">Tipuri notificare</string>
+ <string name="error_loading_notifications">Nu s-au putut încărca setările notificărilor</string>
+ <string name="replies_to_your_comments">Răspunsuri la comentariile tale</string>
+ <string name="comment_likes">Aprecieri comentariu</string>
+ <string name="email">Email</string>
+ <string name="notifications_tab">Filă notificări</string>
+ <string name="app_notifications">Notificări aplicație</string>
+ <string name="notifications_comments_other_blogs">Comentarii pe alte situri</string>
+ <string name="notifications_other">Alte</string>
+ <string name="notifications_wpcom_updates">Actualizări WordPress.com</string>
+ <string name="notifications_account_emails">Email de la WordPress.com</string>
+ <string name="notifications_account_emails_summary">Vom trimite întotdeauna emailuri importante cu privire la contul tău, dar vei putea, de asemenea, să primești și altele utile, în plus.</string>
+ <string name="your_sites">Siturile tale</string>
+ <string name="notifications_sights_and_sounds">Imagini și sunete</string>
+ <string name="stats_insights_latest_post_trend">A trecut %1$s de când %2$s a fost publicat. Iată performanțele articolului până în prezent ...</string>
+ <string name="stats_insights_latest_post_summary">Rezumatul ultimului articol</string>
+ <string name="button_revert">Revenire</string>
+ <string name="yesterday">Ieri</string>
+ <string name="days_ago">%d zile în urmă</string>
+ <string name="connectionbar_no_connection">Nicio conexiune</string>
+ <string name="page_trashed">Pagină trimisă la gunoi</string>
+ <string name="post_deleted">Articol șters</string>
+ <string name="post_trashed">Articol trimis la gunoi</string>
+ <string name="stats_no_activity_this_period">Nicio activitate în această perioadă</string>
+ <string name="trashed">Aruncat la gunoi</string>
+ <string name="button_back">Înapoi</string>
+ <string name="page_deleted">Pagină ștearsă</string>
+ <string name="button_stats">Statistici</string>
+ <string name="button_trash">Gunoi</string>
+ <string name="button_preview">Previzualizare</string>
+ <string name="button_view">Vizualizare</string>
+ <string name="button_edit">Editare</string>
+ <string name="button_publish">Publică</string>
+ <string name="my_site_no_sites_view_subtitle">Vrei să adaugi unul?</string>
+ <string name="my_site_no_sites_view_title">Încă nu ai niciun sit WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustrație</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Nu ești autorizat să accesezi acest blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">Acest blog nu a fost găsit</string>
+ <string name="undo">Revin</string>
+ <string name="tabbar_accessibility_label_my_site">Situl meu</string>
+ <string name="tabbar_accessibility_label_me">Eu</string>
+ <string name="editor_toast_changes_saved">Modificări salvate</string>
+ <string name="passcodelock_prompt_message">Introdu PIN-ul tău</string>
+ <string name="push_auth_expired">Cererea a expirat. Autentifică-te pe WordPress.com pentru a încerca din nou.</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% de vizualizări</string>
+ <string name="ignore">Ignoră</string>
+ <string name="stats_insights_best_ever">Cele mai multe vizualizări dintotdeauna</string>
+ <string name="stats_insights_most_popular_hour">Cea mai populară oră</string>
+ <string name="stats_insights_most_popular_day">Cea mai populară zi</string>
+ <string name="stats_insights_popular">Cea mai populară zi și oră</string>
+ <string name="stats_insights_today">Statisticile zilei</string>
+ <string name="stats_insights_all_time">Articole, vizualizări și vizitatori dintotdeauna.</string>
+ <string name="stats_insights">Perspective</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Pentru a-ți vedea statisticile, autentifică-te pe WordPress.com în contul pe care-l folosești pentru a te conecta la Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Ești interesat de alte statistici recente? Le-am mutat pe pagina de Perspective.</string>
+ <string name="me_disconnect_from_wordpress_com">Deconectare de la WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Conectare la WordPress.com</string>
+ <string name="me_btn_login_logout">Autentificare/Deautentificare</string>
+ <string name="site_picker_cant_hide_current_site">"%s" n-a fost ascuns pentru că este situl curent</string>
+ <string name="account_settings">Setări cont</string>
+ <string name="me_btn_support">Ajutor și suport</string>
+ <string name="site_picker_create_dotcom">Creează un sit WordPress.com</string>
+ <string name="site_picker_edit_visibility">Arată/ascunde situri</string>
+ <string name="site_picker_add_self_hosted">Adaugă sit auto-găzduit</string>
+ <string name="site_picker_add_site">Adaugă sit</string>
+ <string name="my_site_btn_switch_site">Comută sit</string>
+ <string name="site_picker_title">Alege sit</string>
+ <string name="my_site_btn_view_admin">Vezi administrator</string>
+ <string name="my_site_btn_view_site">Vezi sit</string>
+ <string name="my_site_header_publish">Publicare</string>
+ <string name="my_site_header_look_and_feel">Aspect și afect</string>
+ <string name="my_site_btn_site_settings">Setări</string>
+ <string name="my_site_btn_blog_posts">Articole blog</string>
+ <string name="reader_label_new_posts_subtitle">Atinge pentru a le arăta</string>
+ <string name="my_site_header_configuration">Configurare</string>
+ <string name="notifications_account_required">Autentifică-te în WordPress.com pentru notificări</string>
+ <string name="stats_unknown_author">Autor necunoscut</string>
+ <string name="signout">Deconectare</string>
+ <string name="image_added">Imagine adăugată</string>
+ <string name="sign_out_wpcom_confirm">Deconectarea contului tău va înlătura toate datele WordPress.com despre @%s de pe acest dispozitiv, inclusiv schițele și modificările locale.</string>
+ <string name="hide">Ascunde</string>
+ <string name="select_all">Selectează tot</string>
+ <string name="show">Arată</string>
+ <string name="deselect_all">Deselectează tot</string>
+ <string name="select_from_new_picker">Fă o selecție multiplă cu noul selector</string>
+ <string name="error_loading_videos">Eroare încărcare video-uri</string>
+ <string name="error_loading_images">Eroare încărcare imagini</string>
+ <string name="no_device_images">Nicio imagine</string>
+ <string name="no_blog_images">Nicio imagine</string>
+ <string name="no_blog_videos">Niciun video</string>
+ <string name="stats_generic_error">Statisticile cerute nu pot fi încărcate</string>
+ <string name="no_device_videos">Niciun video</string>
+ <string name="no_media_sources">Nu s-a putut aduce media</string>
+ <string name="loading_blog_videos">Se aduc videouri</string>
+ <string name="loading_blog_images">Se aduc imagini</string>
+ <string name="error_loading_blog_videos">Nu se pot aduce videouri</string>
+ <string name="error_loading_blog_images">Nu se pot aduce imagini</string>
+ <string name="no_media">Nicio media</string>
+ <string name="loading_images">Încărcare imagini</string>
+ <string name="loading_videos">Încărcare videouri</string>
+ <string name="auth_required">Autentifică-te din nou pentru a continua.</string>
+ <string name="sign_in_jetpack">Autentifică-te în contul tău de WordPress.com pentru conectare la Jetpack.</string>
+ <string name="two_step_sms_sent">Uită-te la mesajele text după codul de verificare.</string>
+ <string name="two_step_footer_button">Trimite codul via mesaj text</string>
+ <string name="two_step_footer_label">Introdu codul din aplicația ta de autentificare.</string>
+ <string name="verify">Verifică</string>
+ <string name="invalid_verification_code">Cod de verificare invalid</string>
+ <string name="verification_code">Cod de verificare</string>
+ <string name="editor_toast_invalid_path">Cale fișier invalidă</string>
+ <string name="error_publish_no_network">Nu pot publica cât timp nu există o conexiune. Salvat ca ciornă.</string>
+ <string name="tab_title_site_images">Imagini sit</string>
+ <string name="tab_title_device_videos">Videouri din dispozitiv</string>
+ <string name="tab_title_device_images">Imagini din dispozitiv</string>
+ <string name="take_video">Fă un video</string>
+ <string name="media_picker_title">Selectare media</string>
+ <string name="take_photo">Fă o poză</string>
+ <string name="add_to_post">Adaugă la articol</string>
+ <string name="language">Limbă</string>
+ <string name="device">Dispozitiv</string>
+ <string name="media_details_label_file_type">Tip fișier</string>
+ <string name="media_details_label_file_name">Nume fișier</string>
+ <string name="tab_title_site_videos">Videouri sit</string>
+ <string name="toast_err_post_uploading">Nu pot deschide articolul când încă se încarcă</string>
+ <string name="stats_view_search_terms">Termeni căutați</string>
+ <string name="stats_empty_search_terms">Niciun termen de căutare nu a fost înregistrat</string>
+ <string name="stats_entry_search_terms">Termen căutat</string>
+ <string name="stats_view_authors">Autori</string>
+ <string name="stats_followers_total_wpcom_paged">Urmăritori pe WordPress.com de la %1$d - %2$d din %3$s</string>
+ <string name="stats_empty_search_terms_desc">Învață mai multe despre traficul generat de căutări uitându-te la termenii căutați de vizitatorii tăi pentru a-ți găsi situl.</string>
+ <string name="publisher">Editor:</string>
+ <string name="error_notification_open">Nu pot deschide notificarea</string>
+ <string name="stats_followers_total_email_paged">Urmăritori pe email de la %1$d - %2$d din %3$s</string>
+ <string name="stats_search_terms_unknown_search_terms">Termeni de căutare necunoscuți</string>
+ <string name="media_fetching">Se aduc media...</string>
+ <string name="posts_fetching">Se aduc articole...</string>
+ <string name="comments_fetching">Se aduc comentarii...</string>
+ <string name="pages_fetching">Se aduc pagini...</string>
+ <string name="reader_empty_posts_request_failed">Nu se pot regăsi articole</string>
+ <string name="stats_months_and_years">Luni și ani</string>
+ <string name="reader_empty_posts_in_blog">Acest blog e gol</string>
+ <string name="error_copy_to_clipboard">A apărut o eroare în timp ce copiam textul în clipbord</string>
+ <string name="logs_copied_to_clipboard">Jurnalele aplicației au fost copiate în clipbord</string>
+ <string name="stats_overall">În ansamblu</string>
+ <string name="stats_total">Total</string>
+ <string name="post_uploading">Încărcare</string>
+ <string name="reader_page_recommended_blogs">Situri care s-ar putea să-ți placă</string>
+ <string name="stats_period">Perioadă</string>
+ <string name="reader_label_new_posts">Articole noi</string>
+ <string name="stats_average_per_day">Medie pe zi</string>
+ <string name="stats_recent_weeks">Săptămâni recente</string>
+ <string name="stats_comments_total_comments_followers">Total articole cu urmăritori ai comentarilor: %1$s</string>
+ <string name="stats_views">Vizualizări</string>
+ <string name="stats_view_followers">Urmăritori</string>
+ <string name="stats_view_publicize">Publicizare</string>
+ <string name="stats_entry_publicize">Serviciu</string>
+ <string name="stats_entry_followers">Urmăritor</string>
+ <string name="stats_totals_publicize">Urmăritori</string>
+ <string name="stats_view_top_posts_and_pages">Articole și pagini</string>
+ <string name="stats_view_videos">Videouri</string>
+ <string name="stats_empty_geoviews">Nicio țară înregistrată</string>
+ <string name="stats_empty_geoviews_desc">Explorează lista pentru a vedea care țări și regiuni generează cel mai mare trafic pe situl tău.</string>
+ <string name="stats_totals_followers">De la</string>
+ <string name="stats_empty_top_posts_desc">Descoperă care este cel mai vizionat conținut al tău și verifică modul în care articole și paginile individuale care au performat de-a lungul timpului.</string>
+ <string name="stats_empty_referrers_title">Nicio referință înregistrată</string>
+ <string name="stats_empty_top_posts_title">Niciun articol sau pagină văzute</string>
+ <string name="stats_empty_clicks_title">Niciun clic înregistrat</string>
+ <string name="stats_empty_referrers_desc">Află mai multe despre vizibilitatea sitului tău din lista siturilor și motoarelor de căutare care ți-au trimis cel mai mare trafic</string>
+ <string name="stats_empty_tags_and_categories">Niciun articol etichetat sau pagină văzută</string>
+ <string name="stats_empty_top_authors_desc">Urmărește vizualizările fiecărui articol al contributorilor și adâncește-te în descoperirea celui mai popular conținut al fiecărui autor.</string>
+ <string name="stats_empty_tags_and_categories_desc">Fă-ți o idee despre cele mai populare subiecte de pe situl tău, așa cum sunt reflectate în articolele tale de top din ultima săptămână.</string>
+ <string name="stats_empty_video">Niciun video rulat</string>
+ <string name="stats_empty_video_desc">Dacă ai încărcat videouri folosind VideoPress, află de câte ori au fost acestea privite.</string>
+ <string name="stats_empty_comments_desc">Dacă permiți comentarii pe situl tău, urmărește care sunt comentatorii de top și descoperă ce conținut i-a incitat cele mai animate conversații, bazate pe cele mai recente 1000 de comentarii.</string>
+ <string name="stats_empty_publicize">Niciun urmăritor prin publicizare n-a fost înregistrat</string>
+ <string name="stats_empty_publicize_desc">Ține socoteala urmăritorilor tăi ce utilizează serviciile diferitelor rețele de socializare folosind publicizarea.</string>
+ <string name="stats_empty_followers">Niciun urmăritor</string>
+ <string name="stats_empty_followers_desc">Ține socoteala numărului total de urmăritori ai tăi și cât timp au urmărit fiecare situl tău.</string>
+ <string name="stats_comments_by_posts_and_pages">După articole și pagini</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_total_wpcom">Total urmăritori de pe WordPress.com: %1$s</string>
+ <string name="stats_comments_by_authors">După autori</string>
+ <string name="stats_followers_total_email">Total urmăritori pe email: %1$s</string>
+ <string name="stats_followers_seconds_ago">acum o secundă</string>
+ <string name="stats_followers_a_minute_ago">acum un minut</string>
+ <string name="stats_followers_an_hour_ago">acum o oră</string>
+ <string name="stats_followers_a_month">O lună</string>
+ <string name="stats_followers_a_year">Un an</string>
+ <string name="stats_other_recent_stats_label">Alte statistici recente</string>
+ <string name="stats_pagination_label">Pagina %1$s din %2$s</string>
+ <string name="stats_for">Statistici pentru %s</string>
+ <string name="stats_view">Vizualizare</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_timeframe_years">Ani</string>
+ <string name="stats_entry_clicks_link">Legătură</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_view_countries">Țări</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_followers_a_day">O zi</string>
+ <string name="stats_followers_months">%1$d luni</string>
+ <string name="stats_followers_years">%1$d ani</string>
+ <string name="stats_followers_days">%1$d zile</string>
+ <string name="stats_followers_hours">%1$d ore</string>
+ <string name="stats_followers_minutes">%1$d minute</string>
+ <string name="stats_visitors">Vizitatori</string>
+ <string name="stats_likes">Aprecieri</string>
+ <string name="stats_view_all">Vezi tot</string>
+ <string name="themes_fetching">Se aduc teme...</string>
+ <string name="stats_empty_clicks_desc">Când conținutul tău include legături la alte situri, vei vedea pe care dintre ele vizitatorii tăi au dat cele mai multe clicuri.</string>
+ <string name="ssl_certificate_details">Detalii</string>
+ <string name="sure_to_remove_account">Înlătur acest sit?</string>
+ <string name="delete_sure_post">Șterge acest articol</string>
+ <string name="confirm_delete_multi_media">Ștergi elementele selectate?</string>
+ <string name="delete_sure">Șterge această ciornă</string>
+ <string name="delete_sure_page">Șterge această pagină</string>
+ <string name="cab_selected">%d selectate</string>
+ <string name="confirm_delete_media">Ștergi elementul selectat?</string>
+ <string name="media_gallery_date_range">Afișare media de la %1$s la %2$s</string>
+ <string name="posting_post">Public "%s"</string>
+ <string name="signing_out">Deautentificare....</string>
+ <string name="media_empty_list_custom_date">Nicio media în acest interval de timp</string>
+ <string name="pages_empty_list">Nicio pagină încă. De ce să nu creezi una?</string>
+ <string name="comment_reply_to_user">Răspunde la %s</string>
+ <string name="posts_empty_list">Niciun articol încă. De ce să nu creezi unul?</string>
+ <string name="comment">Comentariu</string>
+ <string name="comment_trashed">Comentariu aruncat la gunoi</string>
+ <string name="sending_content">Încarc conținutul %s</string>
+ <string name="uploading_total">Încarc %1$d din %2$d</string>
+ <string name="select_a_blog">Selectează un sit WordPress</string>
+ <string name="older_two_days">Mai vechi de 2 zile</string>
+ <string name="older_last_week">Mai vechi de o săptămână</string>
+ <string name="older_month">Mai vechi de o lună</string>
+ <string name="more">Mai mult</string>
+ <string name="error_refresh_unauthorized_pages">Nu ai permisiunea de a vedea sau edita pagini</string>
+ <string name="error_refresh_unauthorized_comments">Nu ai permisiunea de a vedea sau edita comentarii</string>
+ <string name="error_refresh_unauthorized_posts">Nu ai permisiunea de a vedea sau edita articole</string>
+ <string name="reader_label_comment_count_single">Un comentariu</string>
+ <string name="reader_label_comments_closed">Comentariile sunt închise</string>
+ <string name="reader_label_comments_on">Comentarii deschise</string>
+ <string name="error_publish_empty_post">Nu pot publica un articol gol</string>
+ <string name="reader_label_view_original">Vezi articolul original</string>
+ <string name="reader_label_liked_by">Plăcut de</string>
+ <string name="reader_label_comment_count_multi">%,d comentarii</string>
+ <string name="reader_empty_posts_in_tag">Niciun articol cu această etichetă</string>
+ <string name="reader_empty_comments">Niciun comentariu încă</string>
+ <string name="create_new_blog_wpcom">Crează un blog la WordPress.com</string>
+ <string name="new_blog_wpcom_created">Blogul de la WordPress.com a fost creat!</string>
+ <string name="agree_terms_of_service">Prin crearea acestui cont ești de acord cu fascinantele %1$sCondiții de utilizare%2$s</string>
+ <string name="nux_help_description">Vizitează centrul de ajutor pentru a primi răspunsuri la întrebările obișnuite sau vizitează forumurile pentru a pune unele noi</string>
+ <string name="browse_our_faq_button">Răsfoiește Î&amp;R noastre</string>
+ <string name="faq_button">Î&amp;R</string>
+ <string name="reader_title_photo_viewer">%1$d din %2$d</string>
+ <string name="reader_empty_posts_liked">Nu ți-a plăcut niciun articol</string>
+ <string name="reader_empty_followed_blogs_title">Încă nu urmărești niciun sit</string>
+ <string name="stats_no_blog">Statisticile nu au putut fi încărcate pentru blogul solicitat</string>
+ <string name="mnu_comment_liked">Apreciat</string>
+ <string name="reader_label_like">Apreciere</string>
+ <string name="reader_menu_block_blog">Blochează acest blog</string>
+ <string name="reader_toast_err_block_blog">Nu pot bloca acest blog</string>
+ <string name="reader_toast_err_generic">Nu pot face acestă acțiune</string>
+ <string name="reader_toast_blog_blocked">Articolele din acest blog nu vor mai fi arătate</string>
+ <string name="hs__invalid_email_error">Introdu o adresă de email validă</string>
+ <string name="hs__username_blank_error">Introdu un nume valid</string>
+ <string name="hs__conversation_header">Chat cu suportul</string>
+ <string name="hs__new_conversation_header">Chat cu suportul</string>
+ <string name="hs__conversation_detail_error">Descrie problema pe care o ai</string>
+ <string name="contact_us">Contactează-ne</string>
+ <string name="current_location">Locație curentă</string>
+ <string name="add_location">Adaugă locație</string>
+ <string name="search_current_location">Localizează</string>
+ <string name="edit_location">Editare</string>
+ <string name="search_location">Caută</string>
+ <string name="preference_send_usage_stats_summary">Trimite automat statistici pentru a ajuta la îmbunătățirea aplicației WordPress pentru Android</string>
+ <string name="preference_send_usage_stats">Trimite statistici</string>
+ <string name="schedule_verb">Programare</string>
+ <string name="update_verb">Actualizează</string>
+ <string name="reader_empty_recommended_blogs">Niciun blog recomandat</string>
+ <string name="reader_toast_err_unfollow_blog">Nu pot neurmărí acest blog</string>
+ <string name="reader_toast_err_follow_blog">Nu pot urmărí acest blog</string>
+ <string name="reader_label_tag_preview">Articole etichetate %s</string>
+ <string name="reader_toast_err_get_blog_info">Nu pot să arăt acest blog</string>
+ <string name="reader_toast_err_already_follow_blog">Deja urmărești acest blog</string>
+ <string name="reader_page_followed_tags">Etichete urmărite</string>
+ <string name="reader_title_subs">Etichete și bloguri</string>
+ <string name="reader_title_blog_preview">Previzualizare blog</string>
+ <string name="reader_title_tag_preview">Etichetă cititor</string>
+ <string name="reader_page_followed_blogs">Situri urmărite</string>
+ <string name="reader_hint_add_tag_or_url">Introdu un URL sau o etichetă pentru urmărire</string>
+ <string name="reader_label_followed_blog">Blog urmărit</string>
+ <string name="ptr_tip_message">Sfat: Trage în jos pentru reîmprospătare</string>
+ <string name="saving">Salvare...</string>
+ <string name="media_empty_list">Nicio media</string>
+ <string name="forgot_password">Ți-ai pierdut parola?</string>
+ <string name="forums">Forumuri</string>
+ <string name="ssl_certificate_ask_trust">Dacă în mod uzual te conectezi fără probleme la acest sit, această eroare poate să însemne că cineva încearcă să se substituie sitului și n-ar trebui să mai continui. Preferi să te încrezi în certificat oricum?</string>
+ <string name="help_center">Centru de ajutor</string>
+ <string name="ssl_certificate_error">Certificat SSL invalid</string>
+ <string name="help">Ajutor</string>
+ <string name="could_not_remove_account">Nu am putut înlătura situl</string>
+ <string name="nux_cannot_log_in">Nu putem să te autentificăm</string>
+ <string name="blog_name_reserved">Acel sit este rezervat</string>
+ <string name="username_or_password_incorrect">Numele de utilizator sau parola introduse de tine sunt incorecte</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Adresa de sit trebuie să fie mai scurtă de 64 de caractere</string>
+ <string name="blog_name_not_allowed">Acea adresă de sit nu e permisă</string>
+ <string name="username_reserved_but_may_be_available">Acel nume de utilizator este rezervat pentru moment dar s-ar putea să fie disponibil peste câteva zile</string>
+ <string name="email_not_allowed">Acea adresă de email nu e permisă</string>
+ <string name="username_exists">Acel nume de utilizator există deja</string>
+ <string name="email_exists">Acea adresă de email este deja folosită</string>
+ <string name="blog_name_reserved_but_may_be_available">Acel sit este rezervat pentru moment dar s-ar putea să fie disponibil peste câteva zile</string>
+ <string name="blog_name_exists">Acel sit există deja</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Adresa de sit poate conține doar litere mici (a-z) și numere</string>
+ <string name="blog_name_cant_be_used">S-ar putea să nu folosești acea adresă de sit</string>
+ <string name="blog_name_contains_invalid_characters">Adresa de sit nu poate conține caracterul "_"</string>
+ <string name="blog_name_must_be_at_least_four_characters">Adresa de sit trebuie să aibă cel puțin 4 caractere</string>
+ <string name="blog_name_required">Introdu o adresă de sit</string>
+ <string name="email_invalid">Introdu o adresă de email validă</string>
+ <string name="username_contains_invalid_characters">Numele de utilizator nu poate conține caracterul "_"</string>
+ <string name="username_must_include_letters">Numele de utilizator trebuie să aibă cel puțin o literă (a-z)</string>
+ <string name="username_required">Introdu un nume de utilizator</string>
+ <string name="username_not_allowed">Nume de utilizator nepermis</string>
+ <string name="username_must_be_at_least_four_characters">Numele de utilizator trebuie să aibă cel puțin 4 caractere</string>
+ <string name="username_only_lowercase_letters_and_numbers">Numele de utilizator trebuie să conțină doar litere mici (a-z) și numere</string>
+ <string name="invalid_username_too_long">Numele de utilizator trebuie să fie mai scurt de 61 de caractere</string>
+ <string name="invalid_username_too_short">Numele de utilizator trebuie să fie mai lung de 4 caractere</string>
+ <string name="invalid_password_message">Parola trebuie să conțină cel puțin 4 caractere</string>
+ <string name="error_downloading_image">Eroare la descărcarea imaginii</string>
+ <string name="invalid_email_message">Adresa ta de email nu e validă</string>
+ <string name="error_upload">A apărut o eroare în timpul în timpul încărcării %s</string>
+ <string name="error_edit_comment">A apărut o eroare în timpul editării comentariului</string>
+ <string name="error_moderate_comment">A apărut o eroare în timpul moderării</string>
+ <string name="error_generic">A apărut o eroare</string>
+ <string name="error_refresh_stats">Statisticile n-au putut fi reîmprospătate în acest moment</string>
+ <string name="error_refresh_comments">Comentariile n-au putut fi reîmprospătate în acest moment</string>
+ <string name="error_refresh_notifications">Notificările n-au putut fi reîmprospătate în acest moment</string>
+ <string name="error_refresh_pages">Paginile n-au putut fi reîmprospătate în acest moment</string>
+ <string name="error_refresh_posts">Articolele n-au putut fi reîmprospătate în acest moment</string>
+ <string name="error_delete_post">A apărut o eroare în timpul ștergerii %s</string>
+ <string name="notifications_empty_list">Nicio notificare</string>
+ <string name="reply_failed">Răspuns eșuat</string>
+ <string name="stats_bar_graph_empty">Nicio statistică disponibilă</string>
+ <string name="stats_empty_comments">Niciun comentariu deocamdată</string>
+ <string name="sdcard_message">Este necesar un card SD montat pentru a încărca media</string>
+ <string name="no_account">Niciun cont WordPress nu a fost găsit, adaugă unul și încearcă din nou</string>
+ <string name="cat_name_required">Câmpul cu numele categoriei este obligatoriu</string>
+ <string name="category_automatically_renamed">Numele categoriei %1$s nu e valid. A fost redenumită %2$s.</string>
+ <string name="adding_cat_success">Categoria a fost adăugată cu succes</string>
+ <string name="adding_cat_failed">Adăugarea categoriei a eșuat</string>
+ <string name="no_site_error">Nu m-am putut conecta la situl WordPress</string>
+ <string name="mnu_comment_unspam">Nu e spam</string>
+ <string name="comments_empty_list">Fără comentarii</string>
+ <string name="theme_auth_error_message">Asigură-te că ai dreptul să setezi teme</string>
+ <string name="passcode_wrong_passcode">PIN greșit</string>
+ <string name="blog_not_found">A apărut o eroare la accesarea blogului</string>
+ <string name="wait_until_upload_completes">Așteaptă până la terminarea încărcării</string>
+ <string name="no_network_message">Nu e nicio rețea disponibilă</string>
+ <string name="invalid_url_message">Verifică dacă URL-ul introdus este valid</string>
+ <string name="error_load_comment">Nu s-a putut încărca comentariul</string>
+ <string name="theme_set_failed">Setarea temei a eșuat</string>
+ <string name="out_of_memory">Dispozitivul nu mai are spațiu în memorie</string>
+ <string name="theme_fetch_failed">Aducerea temelor a eșuat</string>
+ <string name="gallery_error">Elementul media nu a putut fi readus</string>
+ <string name="blog_removed_successfully">Sit înlăturat cu succes</string>
+ <string name="remove_account">Înlătură situl</string>
+ <string name="jetpack_message_not_admin">Modulul Jetpack este necesar pentru statistici. Contactează administratorul sitului.</string>
+ <string name="error_blog_hidden">Acest blog este ascuns și nu poate fi încărcat. Validează-l din nou din setări și încearcă din nou.</string>
+ <string name="fatal_db_error">A apărut o eroare în timp ce se creea baza de date a aplicației. Încearcă reinstalarea ei.</string>
+ <string name="image_settings">Setări imagine</string>
+ <string name="add_account_blog_url">Adresă blog</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="local_changes">Schimbări locale</string>
+ <string name="privacy_policy">Politică de confidențialitate</string>
+ <string name="view_site">Vizualizează situl</string>
+ <string name="new_media">Media nouă</string>
+ <string name="new_post">Articol nou</string>
+ <string name="post_format">Format articol</string>
+ <string name="http_authorization_required">Autorizație necesară</string>
+ <string name="http_credentials">Acreditări HTTP (opțional)</string>
+ <string name="pending_review">Revizie în așteptare</string>
+ <string name="open_source_licenses">Licențe Open source</string>
+ <string name="location_not_found">Locație necunoscută</string>
+ <string name="file_error_create">Nu am putut crea fișierul temporar pentru a încărca media. Asigură-te că este destul spațiu liber pe dispozitiv.</string>
+ <string name="category_parent">Părintele categoriei (opțional):</string>
+ <string name="category_desc">Descriere categorie (opțional)</string>
+ <string name="category_slug">Descriptor categorie (opțional)</string>
+ <string name="category_name">Nume categorie</string>
+ <string name="reader_share_link">Partajare legătură</string>
+ <string name="share_action_media">Bibliotecă media</string>
+ <string name="share_action_post">Articol nou</string>
+ <string name="page_not_published">Starea paginii nu e publicată</string>
+ <string name="view_in_browser">Vezi în browser</string>
+ <string name="add_new_category">Adaugă categorie nouă</string>
+ <string name="post_not_published">Starea articolului nu e publicat</string>
+ <string name="blog_name_invalid">Adresă de sit invalidă</string>
+ <string name="blog_name_must_include_letters">Adresa de sit trebuie să aibă măcar o literă (a-z)</string>
+ <string name="email_reserved">Acea adresă de email este deja folosită. Verifică-ți căsuța de intrare dacă ai primit un email de activare. Dacă nu faci activarea poți să mai încerci din nou în câteva zile.</string>
+ <string name="site_address">Adresa ta auto-găzduire (URL)</string>
+ <string name="email_cant_be_used_to_signup">Nu poți folosi acestă adresă de email pentru a te înscrie. Avem probleme cu ele blocându-ne emailurile noastre. Folosește alt furmizor de email.</string>
+ <string name="required_field">Câmp obligatoriu</string>
+ <string name="email_hint">Adresă email</string>
+ <string name="comment_added">Comentariul a fost adăugat cu succes</string>
+ <string name="preview_page">Previzualizare pagină</string>
+ <string name="preview_post">Previzualizare articol</string>
+ <string name="content_required">Comentariul e necesar</string>
+ <string name="toast_comment_unedited">Comentariul nu s-a schimbat</string>
+ <string name="author_url">URL autor</string>
+ <string name="hint_comment_content">Comentariu</string>
+ <string name="saving_changes">Salvează schimbările</string>
+ <string name="trash_no">Nu-l arunca</string>
+ <string name="trash">Coș de gunoi</string>
+ <string name="author_name">Nume autor</string>
+ <string name="author_email">Email autor</string>
+ <string name="trash_yes">Coș de gunoi</string>
+ <string name="dlg_spamming_comments">Marcare ca spam</string>
+ <string name="dlg_trashing_comments">Trimis la coșul de gunoi</string>
+ <string name="dlg_confirm_trash_comments">Trimit la coșul de gunoi?</string>
+ <string name="dlg_approving_comments">În aprobare</string>
+ <string name="dlg_unapproving_comments">În dezaprobare</string>
+ <string name="mnu_comment_approve">Aprobă</string>
+ <string name="mnu_comment_unapprove">Dezaprobă</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Coș de gunoi</string>
+ <string name="comment_status_approved">Aprobat</string>
+ <string name="comment_status_unapproved">În așteptare</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Aruncat la gunoi</string>
+ <string name="edit_comment">Editează comentariul</string>
+ <string name="file_not_found">Nu am găsit fișierul media pentru încărcare. A fost șters sau mutat?</string>
+ <string name="delete_post">Șterge articol</string>
+ <string name="delete_page">Șterge pagină</string>
+ <string name="link_enter_url_text">Legătură text (opțional)</string>
+ <string name="create_a_link">Crează o nouă legătură</string>
+ <string name="upload_failed">Încărcare eșuată</string>
+ <string name="horizontal_alignment">Aliniere orizontală</string>
+ <string name="themes_live_preview">Previzualizare live</string>
+ <string name="theme_current_theme">Tema curentă</string>
+ <string name="theme_premium_theme">Temă premium</string>
+ <string name="media_error_no_permission">Nu ai permisiunea de a vizualiza bibioteca media</string>
+ <string name="cannot_delete_multi_media_items">Unele elemente media nu pot fi șterse acum. Încearcă din nou mai târziu.</string>
+ <string name="media_gallery_edit">Editează galeria</string>
+ <string name="media_gallery_num_columns">Număr de coloane</string>
+ <string name="media_gallery_type_thumbnail_grid">Grilă miniaturi</string>
+ <string name="media_gallery_settings_title">Setări galerie</string>
+ <string name="media_gallery_image_order">Ordine imagini</string>
+ <string name="learn_more">Află mai mult</string>
+ <string name="post_not_found">A apărut o eroare la încărcarea articolului. Reîmprospătează articolele și încearcă din nou.</string>
+ <string name="cancel_edit">Abandonează editarea</string>
+ <string name="scaled_image_error">Introdu o valoare validă de lățime</string>
+ <string name="connection_error">Eroare de conectare</string>
+ <string name="edit_post">Editează articolul</string>
+ <string name="add_comment">Adaugă comentariu</string>
+ <string name="xmlrpc_error">Nu mă pot conecta. Introdu calea completă spre xmlrpc.php de pe situl tău și mai încearcă odată.</string>
+ <string name="notifications_empty_all">Nicio notificare... încă.</string>
+ <string name="invalid_site_url_message">Verifică dacă URL-ul de sit introdus e corect</string>
+ <string name="blog_title_invalid">Titlu sit invalid</string>
+ <string name="reader_title_applog">Jurnal aplicație</string>
+ <string name="reader_toast_err_add_tag">Nu se poate adăuga această etichetă</string>
+ <string name="reader_toast_err_remove_tag">Nu se poate șterge această etichetă</string>
+ <string name="sure_to_cancel_edit_comment">Anulezi editarea acestui comentariu?</string>
+ <string name="page_settings">Setări pagină</string>
+ <string name="post_settings">Setări articol</string>
+ <string name="local_draft">Ciornă locală</string>
+ <string name="select_categories">Selectare categorii</string>
+ <string name="account_details">Detalii cont</string>
+ <string name="delete_draft">Șterge ciorna</string>
+ <string name="share_url_post">Partajare articol</string>
+ <string name="share_url_page">Partajare pagină</string>
+ <string name="share_link">Partajare legătură</string>
+ <string name="deleting_post">Ștergere articol</string>
+ <string name="deleting_page">Ștergere pagină</string>
+ <string name="creating_your_site">Creez situl tău</string>
+ <string name="creating_your_account">Creez contul tău</string>
+ <string name="error_refresh_media">Ceva nu a mers bine când s-a reîmprospătat biblioteca media. Încearcă din nou mai târziu.</string>
+ <string name="reader_empty_posts_in_tag_updating">Se aduc articole...</string>
+ <string name="comment_spammed">Comentariu marcat ca spam</string>
+ <string name="reader_label_reply">Răspunde</string>
+ <string name="cant_share_no_visible_blog">Nu poți partaja pe WordPress fără ca blogul să fie vizibil</string>
+ <string name="download">Descarc media</string>
+ <string name="video">Video</string>
+ <string name="reader_likes_you_and_multi">Tu și alții %,d apreciați asta</string>
+ <string name="reader_likes_multi">%,d persoane apreciază asta</string>
+ <string name="reader_toast_err_get_comment">Nu se poate aduce acest comentariu</string>
+ <string name="select_date">Selectare dată</string>
+ <string name="select_time">Selectează timpul</string>
+ <string name="account_two_step_auth_enabled">Acest cont presupune autentificarea în doi pași. Intră pe setările tale de securitate la WordPress.com și generează o parolă specifică aplicației.</string>
+ <string name="reader_empty_followed_blogs_description">Dar nu-ți fă griji, trebuie doar să atingi iconul din dreapta sus pentru a începe explorarea!</string>
+ <string name="reader_likes_you_and_one">Tu și încă unul apreciați asta</string>
+ <string name="pick_photo">Selectare fotografie</string>
+ <string name="pick_video">Selectare video</string>
+ <string name="validating_site_data">Validare date sit</string>
+ <string name="validating_user_data">Validare date utilizator</string>
+ <string name="reader_toast_err_get_post">Nu se poate aduce acest articol</string>
+ <string name="nux_add_selfhosted_blog">Adaugă un sit auto-găzduit</string>
+ <string name="nux_oops_not_selfhosted_blog">Autentifică-te la WordPress.com</string>
+ <string name="nux_welcome_create_account">Crează un cont</string>
+ <string name="nux_tap_continue">Continuă</string>
+ <string name="password_invalid">Ai nevoie de o parolă mai sigură. Asigură-te că folosești mai mult de 7 caractere, cu litere mici și majuscule amestecate, cu numere și caractere speciale.</string>
+ <string name="signing_in">Se conectează...</string>
+ <string name="nux_tutorial_get_started_title">Poți începe!</string>
+ <string name="limit_reached">S-a atins limita. Poți încerca din nou peste un minut. A încerca înainte de acesta nu va face decât să mărească timpul pe care trebuie să-l aștepți ca blocajul să fie ridicat. Dacă crezi că aceasta este o eroare, contactează suportul.</string>
+ <string name="username_invalid">Nume de utilizator invalid</string>
+ <string name="create_account_wpcom">Crează un cont la WordPress.com</string>
+ <string name="reader_empty_followed_tags">Nu urmărești nicio etichetă</string>
+ <string name="empty_list_default">Această listă e goală</string>
+ <string name="reader_share_subject">Partajat de la %s</string>
+ <string name="reader_btn_share">Partajare</string>
+ <string name="connecting_wpcom">Se conectează la WordPress.com</string>
+ <string name="reader_toast_err_tag_exists">Urmărești deja eticheta asta</string>
+ <string name="reader_toast_err_tag_invalid">Aceea nu e o etichetă validă</string>
+ <string name="reader_likes_only_you">Îți place acesta</string>
+ <string name="reader_label_added_tag">%s adăugat</string>
+ <string name="reader_btn_unfollow">Urmăresc</string>
+ <string name="reader_btn_follow">Urmărește</string>
+ <string name="reader_untitled_post">(Fără titlu)</string>
+ <string name="jetpack_not_found">Modulul Jetpack nu e găsit</string>
+ <string name="select_from_media_library">Selectează din biblioteca media</string>
+ <string name="media_add_popup_title">Adaugă la biblioteca media</string>
+ <string name="reader_hint_comment_on_comment">Răspunde la comentariu...</string>
+ <string name="reader_label_removed_tag">%s înlăturat</string>
+ <string name="reader_likes_one">O persoană apreciază asta</string>
+ <string name="reader_toast_err_share_intent">Nu se poate partaja</string>
+ <string name="reader_toast_err_view_image">Nu se poate vedea imaginea</string>
+ <string name="reader_toast_err_url_intent">Nu se poate deschide %s</string>
+ <string name="jetpack_message">Modulul Jetpack e necesar pentru statistici. Vrei să instalezi Jetpack?</string>
+ <string name="reader_toast_err_comment_failed">Nu s-a putut publica comentariul tău</string>
+ <string name="button_next">Următor</string>
+ <string name="media_add_new_media_gallery">Creează galerie</string>
+ <string name="stats_view_tags_and_categories">Etichete și categorii</string>
+ <string name="passcode_preference_title">PIN blocat</string>
+ <string name="passcode_turn_on">Pune blocarea PIN-ului</string>
+ <string name="passcode_turn_off">Scoate blocarea PIN-ului</string>
+ <string name="passcode_change_passcode">Modifică PIN</string>
+ <string name="passcode_set">PIN setat</string>
+ <string name="passcode_enter_passcode">Introdu PIN-ul tău</string>
+ <string name="passcode_enter_old_passcode">Introdu vechiul tău PIN</string>
+ <string name="passcode_manage">Gestionează PIN-ul de blocare</string>
+ <string name="share_action">Partajare</string>
+ <string name="stats_totals_clicks">Clicuri</string>
+ <string name="stats_totals_plays">Rulări</string>
+ <string name="stats_totals_views">Vizualizări</string>
+ <string name="stats_entry_referrers">Referință</string>
+ <string name="stats_entry_tags_and_categories">Subiect</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_posts_and_pages">Titlu</string>
+ <string name="stats_entry_country">Țară</string>
+ <string name="stats_timeframe_yesterday">Ieri</string>
+ <string name="stats_timeframe_days">Zile</string>
+ <string name="stats_timeframe_weeks">Săptămâni</string>
+ <string name="stats_timeframe_months">Luni</string>
+ <string name="stats_view_visitors_and_views">Vizitatori și vizualizări</string>
+ <string name="stats_view_clicks">Clicuri</string>
+ <string name="stats_view_referrers">Referințe</string>
+ <string name="stats_timeframe_today">Azi</string>
+ <string name="stats">statistici</string>
+ <string name="share_action_title">Adaug la...</string>
+ <string name="post_excerpt">Rezumat</string>
+ <string name="theme_set_success">Temă setată cu succes!</string>
+ <string name="theme_auth_error_title">Eșec în extragerea temelor</string>
+ <string name="theme_activate_button">Activare</string>
+ <string name="media_edit_success">Actualizat</string>
+ <string name="media_edit_failure">Actualizare eșuată</string>
+ <string name="themes_details_label">Detalii</string>
+ <string name="media_edit_caption_text">Text asociat</string>
+ <string name="media_edit_description_text">Descriere</string>
+ <string name="media_edit_title_text">Titlu</string>
+ <string name="media_gallery_type_tiled">Placat</string>
+ <string name="media_gallery_type_circles">Cercuri</string>
+ <string name="media_add_popup_capture_photo">Fotografie luată</string>
+ <string name="media_add_popup_capture_video">Video luat</string>
+ <string name="media_gallery_image_order_reverse">Invers</string>
+ <string name="media_gallery_type">Tip</string>
+ <string name="media_gallery_type_squares">Pătrate</string>
+ <string name="unattached">Neatașate</string>
+ <string name="all">Toate</string>
+ <string name="themes">Teme</string>
+ <string name="passcode_re_enter_passcode">Reintrodu PIN-ul tău</string>
+ <string name="media_edit_title_hint">Introdu un titlu aici</string>
+ <string name="media_edit_caption_hint">Introdu un text asociat aici</string>
+ <string name="media_edit_description_hint">Introdu o descriere aici</string>
+ <string name="themes_features_label">Funcționalități</string>
+ <string name="theme_activating_button">Se activează</string>
+ <string name="images">Imagini</string>
+ <string name="custom_date">Dată personalizată</string>
+ <string name="media_gallery_image_order_random">Aleatoriu</string>
+ <string name="media_gallery_type_slideshow">Carusel</string>
+ <string name="upload">Încărcare</string>
+ <string name="discard">Renunță</string>
+ <string name="more_notifications">și încă %d.</string>
+ <string name="note_reply_successful">Răspuns publicat</string>
+ <string name="notifications">Notificări</string>
+ <string name="sign_in">Autentificare</string>
+ <string name="follows">Urmăriri</string>
+ <string name="new_notifications">%d noi notificări</string>
+ <string name="loading">Se încarcă...</string>
+ <string name="httpuser">nume utilizator HTTP</string>
+ <string name="httppassword">parolă HTTP</string>
+ <string name="error_media_upload">A apărut o eroare la încărcarea media</string>
+ <string name="content_description_add_media">Adaugă media</string>
+ <string name="publish_date">Publicare</string>
+ <string name="post_content">Conținut (atinge pentru a adăuga text și media)</string>
+ <string name="incorrect_credentials">Nume de utilizator sau parolă incorecte.</string>
+ <string name="username">Nume utilizator</string>
+ <string name="password">Parolă</string>
+ <string name="reader">Cititor</string>
+ <string name="width">Lățime</string>
+ <string name="featured">Folosită ca imagine reprezentativă</string>
+ <string name="featured_in_post">Include imaginea în conținutul articolului</string>
+ <string name="caption">Text asociat (opțional)</string>
+ <string name="pages">Pagini</string>
+ <string name="anonymous">Anonim</string>
+ <string name="post">Articol</string>
+ <string name="page">Pagină</string>
+ <string name="posts">Articole</string>
+ <string name="no_network_title">Nu e disponibilă nicio rețea.</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">nume utilizator blog</string>
+ <string name="scaled_image">Lățime imagine scalată</string>
+ <string name="upload_scaled_image">Încarcă și leagă la imaginea scalată</string>
+ <string name="scheduled">Programat</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Încărcare...</string>
+ <string name="app_title">WordPress pentru Android</string>
+ <string name="version">Versiune</string>
+ <string name="tos">Termenii de funcționare a serviciului</string>
+ <string name="max_thumbnail_px_width">Lățime imagine implicită</string>
+ <string name="image_alignment">Aliniere</string>
+ <string name="refresh">Reîmprospătare</string>
+ <string name="untitled">Fără titlu</string>
+ <string name="edit">Editare</string>
+ <string name="page_id">Pagină</string>
+ <string name="post_id">Articol</string>
+ <string name="post_password">Parolă (opțional)</string>
+ <string name="immediately">Imediat</string>
+ <string name="quickpress_add_alert_title">Setare nume scurtătură</string>
+ <string name="settings">Setări</string>
+ <string name="today">Azi</string>
+ <string name="share_url">Partajare URL</string>
+ <string name="quickpress_window_title">Selecție blog pentru scurtătură QuickPress</string>
+ <string name="quickpress_add_error">Numele scurtăturii nu poate fi gol</string>
+ <string name="post_private">Privat</string>
+ <string name="publish_post">Publicare</string>
+ <string name="draft">Ciornă</string>
+ <string name="upload_full_size_image">Încarcă și leagă la imaginea completă</string>
+ <string name="categories">Categorii</string>
+ <string name="title">Titlu</string>
+ <string name="tags_separate_with_commas">Etichete (separă etichetele cu virgule)</string>
+ <string name="dlg_deleting_comments">Ștergere comentarii</string>
+ <string name="notification_vibrate">Vibrare</string>
+ <string name="notification_blink">Clipește lumina de notificare</string>
+ <string name="notification_sound">Sunet pentru notificare</string>
+ <string name="status">Stare</string>
+ <string name="sdcard_title">Cardul SD e necesar</string>
+ <string name="location">Locație</string>
+ <string name="select_video">Selectează un video din galerie</string>
+ <string name="media">Media</string>
+ <string name="delete">Șterge</string>
+ <string name="none">Niciuna</string>
+ <string name="blogs">Bloguri</string>
+ <string name="select_photo">Selectează o poză din galerie</string>
+ <string name="preview">Previzualizare</string>
+ <string name="reply">Răspunde</string>
+ <string name="on">pornit</string>
+ <string name="yes">Da</string>
+ <string name="no">Nu</string>
+ <string name="error">Eroare</string>
+ <string name="category_refresh_error">Eroare reîmprospătare categorii</string>
+ <string name="save">Salvare</string>
+ <string name="add">Adaugă</string>
+ <string name="cancel">Anulare</string>
+ <string name="notification_settings">Setări notificare</string>
+</resources>
diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..e48d32e43
--- /dev/null
+++ b/WordPress/src/main/res/values-ru/strings.xml
@@ -0,0 +1,1143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Администратор</string>
+ <string name="role_editor">Редактор</string>
+ <string name="role_author">Автор</string>
+ <string name="role_contributor">Участник</string>
+ <string name="role_follower">Читатель</string>
+ <string name="role_viewer">Обозреватель</string>
+ <string name="error_post_my_profile_no_connection">Нет сети — не удалось сохранить профиль.</string>
+ <string name="alignment_none">Нет</string>
+ <string name="alignment_left">Слева</string>
+ <string name="alignment_right">Справа</string>
+ <string name="site_settings_list_editor_action_mode_title">Выбрано %1$d</string>
+ <string name="error_fetch_users_list">Не удалось получить список пользователей сайта</string>
+ <string name="plans_manage">Управление тарифными планами:\nwordpress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">У вас пока нет обозревателей.</string>
+ <string name="people_fetching">Получение списка пользователей…</string>
+ <string name="title_follower">Читатель</string>
+ <string name="title_email_follower">Подписчик по электронной почте</string>
+ <string name="people_empty_list_filtered_email_followers">У вас пока нет подписчиков по электронной почте.</string>
+ <string name="people_empty_list_filtered_followers">У вас пока нет подписчиков.</string>
+ <string name="people_empty_list_filtered_users">В вашем списке пока нет пользователей.</string>
+ <string name="people_dropdown_item_email_followers">Подписчики по электронной почте</string>
+ <string name="people_dropdown_item_viewers">Обозреватели</string>
+ <string name="people_dropdown_item_followers">Подписчики</string>
+ <string name="people_dropdown_item_team">Команда</string>
+ <string name="invite_message_usernames_limit">Вы можете пригласить до 10 пользователей с уникальными адресами эл. почты или учётными данными WordPress.com. Незарегистрированным пользователям будут отправлены инструкции по созданию учётной записи.</string>
+ <string name="viewer_remove_confirmation_message">Если удалить этого обозревателя, он больше не сможет зайти на ваш сайт.\n\nВсё равно удалить этого обозревателя?</string>
+ <string name="follower_remove_confirmation_message">Если удалить этого читателя, он больше не будет получать уведомления об изменениях на этом сайте, пока не восстановит подписку.\n\nВсё равно удалить этого читателя?</string>
+ <string name="reader_label_view_gallery">Просмотр галереи</string>
+ <string name="error_remove_follower">Не удалось удалить читателя</string>
+ <string name="error_remove_viewer">Не удалось удалить обозревателя</string>
+ <string name="error_fetch_email_followers_list">Не удалось получить список подписчиков по электронной почте</string>
+ <string name="error_fetch_followers_list">Не удалось получить список читателей сайта</string>
+ <string name="editor_failed_uploads_switch_html">Некоторые загрузки завершились с ошибкой. В этом состоянии нельзя\nпереключиться в режим HTML. Удалить неудачные загрузки и продолжить?</string>
+ <string name="format_bar_description_html">Режим HTML</string>
+ <string name="visual_editor">Визуальный редактор</string>
+ <string name="image_thumbnail">Миниатюра</string>
+ <string name="format_bar_description_ul">Маркированный список</string>
+ <string name="format_bar_description_ol">Нумерованный список</string>
+ <string name="format_bar_description_more">Вставить тег «далее»</string>
+ <string name="format_bar_description_media">Вставить файл</string>
+ <string name="format_bar_description_strike">Перечёркнутый</string>
+ <string name="format_bar_description_quote">Цитата</string>
+ <string name="format_bar_description_link">Вставить ссылку</string>
+ <string name="format_bar_description_italic">Курсив</string>
+ <string name="format_bar_description_underline">Подчёркнутый</string>
+ <string name="image_settings_save_toast">Изменения сохранены</string>
+ <string name="image_caption">Подпись</string>
+ <string name="image_alt_text">Атрибут alt</string>
+ <string name="image_link_to">Ссылка</string>
+ <string name="image_width">Ширина</string>
+ <string name="format_bar_description_bold">Жирный</string>
+ <string name="image_settings_dismiss_dialog_title">Отменить несохранённые изменения?</string>
+ <string name="stop_upload_dialog_title">Остановить загрузку?</string>
+ <string name="stop_upload_button">Остановить загрузку</string>
+ <string name="alert_error_adding_media">При добавлении файла произошла ошибка</string>
+ <string name="alert_action_while_uploading">Сейчас идёт загрузка файла. Подождите, пока процесс завершится.</string>
+ <string name="alert_insert_image_html_mode">В режиме HTML нельзя вставлять файлы. Переключитесь обратно в визуальный режим.</string>
+ <string name="uploading_gallery_placeholder">Загрузка галереи...</string>
+ <string name="invite_error_some_failed">Приглашение отправлено, но возникли ошибки!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Приглашение успешно отправлено</string>
+ <string name="tap_to_try_again">Тапните, чтобы попробовать ещё раз!</string>
+ <string name="invite_error_sending">При отправке приглашения произошла ошибка!</string>
+ <string name="invite_error_invalid_usernames_multiple">Не удалось отправить: Несколько имён пользователей или адресов e-mail некорректны</string>
+ <string name="invite_error_invalid_usernames_one">Не удалось отправить: Некорректное имя пользователя или адрес e-mail</string>
+ <string name="invite_error_no_usernames">Пожалуйста, добавьте хотя бы одно имя пользователя</string>
+ <string name="invite_message_info">(Дополнительно) Можно ввести произвольное сообщение длиной до 500 символов, которое будет включено в приглашение.</string>
+ <string name="invite_message_remaining_other">символов осталось: %d</string>
+ <string name="invite_message_remaining_one">символов осталось: 1</string>
+ <string name="invite_message_remaining_zero">символов осталось: 0</string>
+ <string name="invite_invalid_email">Адрес e-mail «%s» некорректен</string>
+ <string name="invite_message_title">Произвольное сообщение</string>
+ <string name="invite_already_a_member">Пользователь «%s» уже является участником</string>
+ <string name="invite_username_not_found">Пользователь «%s» не найден</string>
+ <string name="invite">Пригласить</string>
+ <string name="invite_names_title">Имена пользователей или адреса e-mail</string>
+ <string name="signup_succeed_signin_failed">Ваша учётная запись создана, но при попытке входа произошла ошибка.\nПопробуйте войти с вашим недавно созданным именем пользователя и паролем.</string>
+ <string name="send_link">Отправить ссылку</string>
+ <string name="my_site_header_external">Внешние ссылки</string>
+ <string name="invite_people">Пригласить людей</string>
+ <string name="label_clear_search_history">Очистить историю поиска</string>
+ <string name="dlg_confirm_clear_search_history">Очистить историю поиска?</string>
+ <string name="reader_empty_posts_in_search_description">По запросу «%s» на вашем языке записей не найдено</string>
+ <string name="reader_label_post_search_running">Поиск...</string>
+ <string name="reader_label_related_posts">Похожие записи</string>
+ <string name="reader_empty_posts_in_search_title">Записей не найдено</string>
+ <string name="reader_label_post_search_explainer">Поиск по всем открытым блогам на WordPress.com</string>
+ <string name="reader_hint_post_search">Поиск по WordPress.com</string>
+ <string name="reader_title_related_post_detail">Похожая запись</string>
+ <string name="reader_title_search_results">Поиск по запросу «%s»</string>
+ <string name="preview_screen_links_disabled">На экране предпросмотра ссылки отключены</string>
+ <string name="draft_explainer">Это черновик, который ещё не был опубликован</string>
+ <string name="send">Отправить</string>
+ <string name="user_remove_confirmation_message">Если вы удалите пользователя %1$s, он больше не сможет посещать этот сайт, но всё содержимое, созданное пользователем %1$s, останется на сайте.\n\nВсё равно удалить этого пользователя?</string>
+ <string name="person_removed">Выполнено удаление %1$s</string>
+ <string name="person_remove_confirmation_title">Удалить %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">На сайтах из этого списка за последнее время не было ничего опубликовано</string>
+ <string name="people">Люди</string>
+ <string name="edit_user">Изменить пользователя</string>
+ <string name="role">Роль</string>
+ <string name="error_remove_user">Не удалось удалить пользователя</string>
+ <string name="error_fetch_viewers_list">Не получить список пользователей сайта</string>
+ <string name="error_update_role">Не удалось обновить роль пользователя</string>
+ <string name="gravatar_camera_and_media_permission_required">Разрешения необходимые для выбора или создания фотографии</string>
+ <string name="error_updating_gravatar">При обновлении граватара произошла ошибка</string>
+ <string name="error_locating_image">Не удалось найти обрезанное изображение</string>
+ <string name="error_refreshing_gravatar">При перезагрузке граватара произошла ошибка</string>
+ <string name="gravatar_tip">Новая функция! Чтобы изменить граватар, просто нажмите его.</string>
+ <string name="error_cropping_image">При обрезке изображения произошла ошибка</string>
+ <string name="launch_your_email_app">Запустите приложение эл. почты</string>
+ <string name="checking_email">Проверка адреса эл. почты</string>
+ <string name="not_on_wordpress_com">Нет учетной записи на WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">В настоящий момент ссылка недоступна. Введите пароль</string>
+ <string name="check_your_email">Проверьте электронную почту</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Получите эл. письмо со ссылкой для моментального входа.</string>
+ <string name="logging_in">Вход в систему</string>
+ <string name="enter_your_password_instead">Введите пароль.</string>
+ <string name="web_address_dialog_hint">Отображается для всех пользователей, когда вы добавляете комментарии.</string>
+ <string name="jetpack_not_connected_message">Плагин Jetpack установлен, но не подключен к WordPress.com. Подключить Jetpack?</string>
+ <string name="username_email">Адрес эл. почты или имя пользователя</string>
+ <string name="jetpack_not_connected">Плагин Jetpack не подключен</string>
+ <string name="new_editor_reflection_error">Визуальный редактор не поддерживается на вашем устройстве. Он был\n автоматически отключен.</string>
+ <string name="stats_insights_latest_post_no_title">(без заголовка)</string>
+ <string name="capture_or_pick_photo">Сделайте снимок или выберите фото</string>
+ <string name="plans_post_purchase_text_themes">У вас есть неограниченный доступ к премиум-темам. Для начала вы можете оценить любую тему на своем сайте, открыв её в режиме предварительного просмотра.</string>
+ <string name="plans_post_purchase_button_themes">Просмотр тем</string>
+ <string name="plans_post_purchase_title_themes">Найдите идеальную премиум-тему</string>
+ <string name="plans_post_purchase_button_video">Создать новую запись</string>
+ <string name="plans_post_purchase_text_video">С помощью модуля VideoPress и расширенного хранилища медиафайлов вы можете загружать видеоролики и размещать их на своем сайте.</string>
+ <string name="plans_post_purchase_title_video">Оживите свои записи — добавьте видео</string>
+ <string name="plans_post_purchase_button_customize">Настроить мой сайт</string>
+ <string name="plans_post_purchase_text_customize">Теперь вы можете применять пользовательские шрифты, цвета и таблицы CSS.</string>
+ <string name="plans_post_purchase_text_intro">Теперь вашему сайту доступны любые вершины! Изучите новые возможности вашего сайта и выберите, с чего начать.</string>
+ <string name="plans_post_purchase_title_customize">Настроить шрифты и цвета</string>
+ <string name="plans_post_purchase_title_intro">Все в ваших руках! Действуйте!</string>
+ <string name="export_your_content_message">Ваши записи, страницы и настройки будут отправлены вам по электронной почте %s.</string>
+ <string name="plan">Тарифный план</string>
+ <string name="plans">Тарифные планы</string>
+ <string name="plans_loading_error">Не удалось загрузить тарифные планы</string>
+ <string name="export_your_content">Экспортируйте ваше содержимое</string>
+ <string name="exporting_content_progress">Выполняется экспорт содержимого…</string>
+ <string name="export_email_sent">Сообщение об экспорте отправлено.</string>
+ <string name="premium_upgrades_message">На вашем сайте есть активированные платные услуги. Прежде чем удалить свой сайт, отключите платные услуги.</string>
+ <string name="show_purchases">Показать покупки</string>
+ <string name="checking_purchases">Просмотр покупок</string>
+ <string name="premium_upgrades_title">Платные услуги</string>
+ <string name="purchases_request_error">Произошла ошибка. Не удалось отправить запрос на покупки.</string>
+ <string name="delete_site_progress">Удаление сайта…</string>
+ <string name="delete_site_summary">Это действие нельзя отменить. В случае удаления сайта будет удалено все его содержимое, домены и сведения о соавторах.</string>
+ <string name="delete_site_hint">Удалить сайт</string>
+ <string name="export_site_hint">Экспортировать сайт в XML-файл</string>
+ <string name="are_you_sure">Вы уверены?</string>
+ <string name="export_site_summary">Если да, то прежде чем продолжить, выполните экспорт содержимого. Его нельзя будет восстановить.</string>
+ <string name="keep_your_content">Сохранить содержимое</string>
+ <string name="domain_removal_hint">После удаления сайта домены не будут работать.</string>
+ <string name="domain_removal_summary">Будьте внимательны! В случае удаления сайта будут удалены перечисленные ниже домены.</string>
+ <string name="primary_domain">Основной домен</string>
+ <string name="domain_removal">Удаление домена</string>
+ <string name="error_deleting_site_summary">При удалении сайта произошла ошибка. Обратитесь в службу поддержки.</string>
+ <string name="error_deleting_site">Ошибка при удалении сайта</string>
+ <string name="confirm_delete_site_prompt">Для подтверждения введите %1$s в поле ниже. Ваш сайт будет безвозвратно удален.</string>
+ <string name="site_settings_export_content_title">Экспортировать содержимое</string>
+ <string name="contact_support">Обратиться в службу поддержки</string>
+ <string name="confirm_delete_site">Подтвердить удаление сайта</string>
+ <string name="start_over_text">Если вы хотите сохранить сайт, но при этом удалить с него все записи, страницы, медиафайлы и комментарии, обратитесь в службу поддержки.\n\nВ этом случае вы сохраните свой сайт и URL-адрес и сможете заново создавать содержимое. Свяжитесь с нами, чтобы мы удалили все содержимое сайта.</string>
+ <string name="site_settings_start_over_hint">Запустите свой сайт заново</string>
+ <string name="let_us_help">Обратитесь к нам за помощью</string>
+ <string name="me_btn_app_settings">Настройки приложений</string>
+ <string name="start_over">Начать сначала</string>
+ <string name="editor_remove_failed_uploads">Удалить невыполненные загрузки</string>
+ <string name="editor_toast_failed_uploads">Не удалось загрузить медиафайлы. Вы не можете сохранять или публиковать\n записи на этом сайте. Удалить все незагруженные медиафайлы?</string>
+ <string name="comments_empty_list_filtered_trashed">Нет комментариев в корзине</string>
+ <string name="site_settings_advanced_header">Дополнительно</string>
+ <string name="comments_empty_list_filtered_pending">Нет комментариев на утверждении</string>
+ <string name="comments_empty_list_filtered_approved">Нет одобренных комментариев</string>
+ <string name="button_done">Готово</string>
+ <string name="button_skip">Пропустить</string>
+ <string name="site_timeout_error">Не удалось подключиться к сайту WordPress из-за ошибки времени ожидания.</string>
+ <string name="xmlrpc_malformed_response_error">Не удалось подключиться. При установке WordPress отправлен недействительный документ XML-RPC.</string>
+ <string name="xmlrpc_missing_method_error">Не удалось подключиться. На сервере отсутствуют необходимые методы XML-RPC.</string>
+ <string name="post_format_status">Статус</string>
+ <string name="post_format_video">Видео</string>
+ <string name="theme_free">Бесплатно</string>
+ <string name="theme_all">Все</string>
+ <string name="theme_premium">Премиум</string>
+ <string name="post_format_chat">Чат</string>
+ <string name="post_format_gallery">Галерея</string>
+ <string name="post_format_image">Изображение</string>
+ <string name="post_format_link">Ссылка</string>
+ <string name="post_format_quote">Цитата</string>
+ <string name="post_format_standard">Стандартный</string>
+ <string name="notif_events">Информация об учебных курсах и мероприятиях WordPress.com (очных и онлайн).</string>
+ <string name="post_format_aside">Заметка</string>
+ <string name="post_format_audio">Аудио</string>
+ <string name="notif_surveys">Возможности участия в исследованиях и опросах WordPress.com.</string>
+ <string name="notif_tips">Советы по эффективному использованию WordPress.com.</string>
+ <string name="notif_community">Сообщество</string>
+ <string name="replies_to_my_comments">Ответы на мои комментарии</string>
+ <string name="notif_suggestions">Предложения</string>
+ <string name="notif_research">Исследования</string>
+ <string name="site_achievements">Достижения сайта</string>
+ <string name="username_mentions">Упоминания имени пользователя</string>
+ <string name="likes_on_my_posts">Отметки «Нравится» к моим записям</string>
+ <string name="site_follows">Читатели сайта</string>
+ <string name="likes_on_my_comments">Отметки «Нравится» к моим комментариям</string>
+ <string name="comments_on_my_site">Комментарии на моём сайте</string>
+ <string name="site_settings_list_editor_summary_other">Всего элементов: %d</string>
+ <string name="site_settings_list_editor_summary_one">1 элемент</string>
+ <string name="approve_auto_if_previously_approved">Комментарии известных пользователей</string>
+ <string name="approve_auto">Все пользователи</string>
+ <string name="approve_manual">Нет комментариев</string>
+ <string name="site_settings_paging_summary_other">Количество комментариев на страницу: %d</string>
+ <string name="site_settings_paging_summary_one">1 комментарий на страницу</string>
+ <string name="site_settings_multiple_links_summary_other">Требуется проверка, если количество ссылок превышает %d</string>
+ <string name="site_settings_multiple_links_summary_one">Требуется проверка при наличии более 1 ссылки</string>
+ <string name="site_settings_multiple_links_summary_zero">Требуется проверка при наличии более 0 ссылок</string>
+ <string name="detail_approve_auto">Автоматически одобрять все комментарии.</string>
+ <string name="detail_approve_auto_if_previously_approved">Автоматически одобрять комментарии пользователя, оставившего ранее одобренные комментарии</string>
+ <string name="detail_approve_manual">Включить обязательный этап одобрения для всех комментариев.</string>
+ <string name="filter_trashed_posts">Удален</string>
+ <string name="days_quantity_one">1 день</string>
+ <string name="days_quantity_other">%d дн.</string>
+ <string name="filter_published_posts">Опубликовано</string>
+ <string name="filter_draft_posts">Черновики</string>
+ <string name="filter_scheduled_posts">Запланированные</string>
+ <string name="pending_email_change_snackbar">Нажмите на ссылку в электронном письме, отправленном на %1$s, чтобы подтвердить новый адрес</string>
+ <string name="primary_site">Основной сайт</string>
+ <string name="web_address">Интернет-адрес</string>
+ <string name="editor_toast_uploading_please_wait">В данный момент выполняется загрузка медиафайлов. Дождитесь окончания загрузки.</string>
+ <string name="error_refresh_comments_showing_older">Не удалось обновить ленту комментариев — отображены старые комментарии</string>
+ <string name="editor_post_settings_set_featured_image">Настроить избранное изображение</string>
+ <string name="editor_post_settings_featured_image">Избранное изображение</string>
+ <string name="new_editor_promo_desc">Приложение WordPress для Android теперь имеет новое визуальное оформление\n редактор. Попробуйте с его помощью создать новую запись.</string>
+ <string name="new_editor_promo_title">Новый редактор</string>
+ <string name="new_editor_promo_button_label">Спасибо!</string>
+ <string name="visual_editor_enabled">Визуальный редактор включен</string>
+ <string name="editor_content_placeholder">Поделитесь своей историей здесь…</string>
+ <string name="editor_page_title_placeholder">Заголовок страницы</string>
+ <string name="editor_post_title_placeholder">Заголовок записи</string>
+ <string name="email_address">Адрес электронной почты</string>
+ <string name="preference_show_visual_editor">Показать визуальный редактор</string>
+ <string name="dlg_sure_to_delete_comments">Удалить эти комментарии без возможности восстановления?</string>
+ <string name="preference_editor">Редактор</string>
+ <string name="dlg_sure_to_delete_comment">Удалить этот комментарий без возможности восстановления?</string>
+ <string name="mnu_comment_delete_permanently">Удалить</string>
+ <string name="comment_deleted_permanently">Комментарий удалён</string>
+ <string name="mnu_comment_untrash">Восстановить</string>
+ <string name="comments_empty_list_filtered_spam">Нет комментариев, помеченных как спам</string>
+ <string name="could_not_load_page">Не удалось загрузить страницу</string>
+ <string name="comment_status_all">Все</string>
+ <string name="interface_language">Язык интерфейса</string>
+ <string name="off">Выкл.</string>
+ <string name="about_the_app">О приложении</string>
+ <string name="error_post_account_settings">Не удалось сохранить настройки учётной записи.</string>
+ <string name="error_post_my_profile">Не удалось сохранить профиль.</string>
+ <string name="error_fetch_account_settings">Не удалось загрузить настройки учетной записи.</string>
+ <string name="error_fetch_my_profile">Не удалось загрузить профиль.</string>
+ <string name="stats_widget_promo_ok_btn_label">ОК</string>
+ <string name="stats_widget_promo_desc">Добавив виджет на домашний экран, вы сможете переходить к своей статистике одним нажатием.</string>
+ <string name="stats_widget_promo_title">Виджет статистики для домашнего экрана</string>
+ <string name="site_settings_unknown_language_code_error">Код языка не распознан.</string>
+ <string name="site_settings_threading_dialog_description">Разрешить вложение комментариев в ветках.</string>
+ <string name="site_settings_threading_dialog_header">Ветки до</string>
+ <string name="remove">Удалить</string>
+ <string name="search">Поиск</string>
+ <string name="add_category">Добавить рубрику</string>
+ <string name="disabled">Отключено</string>
+ <string name="site_settings_image_original_size">Исходный размер</string>
+ <string name="privacy_private">Ваш сайт доступен только вам и выбранным вами пользователям.</string>
+ <string name="privacy_public_not_indexed">Ваш сайт доступен всем посетителям, но для него выбран параметр «Попросить поисковые системы не индексировать сайт».</string>
+ <string name="privacy_public">Ваш сайт доступен всем и может индексироваться поисковыми системами.</string>
+ <string name="about_me_hint">Несколько слов о себе…</string>
+ <string name="public_display_name_hint">Если отображаемое имя не задано, вместо него по умолчанию используется имя пользователя.</string>
+ <string name="about_me">Обо мне</string>
+ <string name="public_display_name">Отображаемое для всех имя</string>
+ <string name="my_profile">Мой профиль</string>
+ <string name="first_name">Имя</string>
+ <string name="last_name">Фамилия</string>
+ <string name="site_privacy_public_desc">Разрешить поисковым системам индексировать сайт</string>
+ <string name="site_privacy_hidden_desc">Попросить поисковые системы не индексировать сайт</string>
+ <string name="site_privacy_private_desc">Я хочу, чтобы мой блог был частным, а доступ к нему имели только те пользователи, которых я выберу.</string>
+ <string name="cd_related_post_preview_image">Изображение для предварительного просмотра похожих записей</string>
+ <string name="error_post_remote_site_settings">Не удалось сохранить данные сайта.</string>
+ <string name="error_fetch_remote_site_settings">Не удалось получить данные сайта.</string>
+ <string name="error_media_upload_connection">Ошибка подключения при загрузке медиафайла</string>
+ <string name="site_settings_disconnected_toast">Отключено, редактирование невозможно.</string>
+ <string name="site_settings_unsupported_version_error">Неподдерживаемая версия WordPress</string>
+ <string name="site_settings_multiple_links_dialog_description">Включить обязательный этап одобрения для комментариев, которые содержат больше указанного числа ссылок.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Автоматически закрывать обсуждение</string>
+ <string name="site_settings_close_after_dialog_description">Автоматически закрывать обсуждение статей.</string>
+ <string name="site_settings_paging_dialog_description">Выделять ветки комментариев на отдельные страницы.</string>
+ <string name="site_settings_paging_dialog_header">Комментариев на странице:</string>
+ <string name="site_settings_close_after_dialog_title">Закрыть обсуждение</string>
+ <string name="site_settings_blacklist_description">Комментарии, содержащие любые из этих слов в тексте, имени автора, URL-адресе, адресе электронной почты или IP-адресе, будут помечены как спам. Можно вводить фрагменты слов, то есть по слову press будет найдено WordPress.</string>
+ <string name="site_settings_hold_for_moderation_description">Комментарии, содержащие любые из этих слов в тексте, имени автора, URL-адресе, адресе электронной почты или IP-адресе, будут помещены в очередь на модерацию. Можно вводить фрагменты слов, то есть по слову press будет найдено WordPress.</string>
+ <string name="site_settings_list_editor_input_hint">Введите слово или фразу</string>
+ <string name="site_settings_list_editor_no_items_text">Нет элементов</string>
+ <string name="site_settings_learn_more_caption">Можно изменять эти настройки для отдельных записей.</string>
+ <string name="site_settings_rp_preview3_site">в разделе «Обновление»</string>
+ <string name="site_settings_rp_preview3_title">Специальное обновление: VideoPress для свадеб</string>
+ <string name="site_settings_rp_preview2_site">в разделе «Приложения»</string>
+ <string name="site_settings_rp_preview2_title">Грандиозное обновление приложения WordPress для Android</string>
+ <string name="site_settings_rp_preview1_site">в разделе «Для мобильных устройств»</string>
+ <string name="site_settings_rp_preview1_title">Доступно большое обновление для iPhone и iPad</string>
+ <string name="site_settings_rp_show_images_title">Показывать изображения</string>
+ <string name="site_settings_rp_show_header_title">Показывать заголовок</string>
+ <string name="site_settings_rp_switch_summary">В разделе «Похожие записи» под вашими записями отображается соответствующее содержимое с вашего сайта.</string>
+ <string name="site_settings_rp_switch_title">Показывать похожие записи</string>
+ <string name="site_settings_delete_site_hint">Удаление данных вашего сайта из приложения</string>
+ <string name="site_settings_blacklist_hint">Комментарии, соответствующие фильтру, будут помечены как спам.</string>
+ <string name="site_settings_moderation_hold_hint">Комментарии, соответствующие фильтру, будут помещены в очередь на модерацию.</string>
+ <string name="site_settings_multiple_links_hint">Удаление ограничения по числу ссылок, размещаемых известными пользователями</string>
+ <string name="site_settings_whitelist_hint">Автор комментария должен иметь ранее одобренные комментарии.</string>
+ <string name="site_settings_user_account_required_hint">Чтобы оставить комментарий, пользователь должен быть зарегистрирован и авторизован.</string>
+ <string name="site_settings_identity_required_hint">Автор комментария должен указать имя и эл. адрес.</string>
+ <string name="site_settings_manual_approval_hint">Комментарий должен пройти обязательное одобрение.</string>
+ <string name="site_settings_paging_hint">Показ комментариев блоками определенного размера</string>
+ <string name="site_settings_threading_hint">Ограничить максимальный уровень вложенных комментариев</string>
+ <string name="site_settings_sort_by_hint">Определение порядка отображения комментариев</string>
+ <string name="site_settings_close_after_hint">Закрывать обсуждение через определенное время</string>
+ <string name="site_settings_receive_pingbacks_hint">Разрешить уведомления со ссылками из других блогов</string>
+ <string name="site_settings_send_pingbacks_hint">Отправлять уведомления во все блоги, на которые есть ссылки в статье</string>
+ <string name="site_settings_allow_comments_hint">Разрешить читателям оставлять комментарии</string>
+ <string name="site_settings_discussion_hint">Просмотр и изменение настроек ваших сайтов в разделе «Обсуждение»</string>
+ <string name="site_settings_more_hint">Показывать все настройки в разделе «Обсуждение»</string>
+ <string name="site_settings_related_posts_hint">Отображение похожих записей в приложении «Чтиво»</string>
+ <string name="site_settings_upload_and_link_image_hint">Загружать полноразмерные изображения по умолчанию</string>
+ <string name="site_settings_image_width_hint">Изменение ширины изображений в записи</string>
+ <string name="site_settings_format_hint">Добавление нового формата записи</string>
+ <string name="site_settings_category_hint">Добавление новой рубрики для записей</string>
+ <string name="site_settings_location_hint">Автоматически добавлять к записям данные о местоположении</string>
+ <string name="site_settings_password_hint">Изменить пароль</string>
+ <string name="site_settings_username_hint">Использующаяся учетная запись</string>
+ <string name="site_settings_language_hint">Язык, который используется в блоге в первую очередь</string>
+ <string name="site_settings_privacy_hint">Ограничение доступа к вашему блогу</string>
+ <string name="site_settings_address_hint">Изменение адреса пока не поддерживается.</string>
+ <string name="site_settings_tagline_hint"> или удачная фраза, характеризующая ваш блог</string>
+ <string name="site_settings_title_hint">Объясните в нескольких словах, о чем этот блог.</string>
+ <string name="site_settings_whitelist_known_summary">Комментарии известных пользователей</string>
+ <string name="site_settings_whitelist_all_summary">Все комментарии</string>
+ <string name="site_settings_threading_summary">%d уров.</string>
+ <string name="site_settings_privacy_private_summary">Скрытая</string>
+ <string name="site_settings_privacy_hidden_summary">Скрыто</string>
+ <string name="site_settings_delete_site_title">Удалить сайт</string>
+ <string name="site_settings_privacy_public_summary">Открытая</string>
+ <string name="site_settings_blacklist_title">Черный список</string>
+ <string name="site_settings_moderation_hold_title">Отправить на модерацию</string>
+ <string name="site_settings_multiple_links_title">Ссылки в комментариях</string>
+ <string name="site_settings_whitelist_title">Одобрять автоматически</string>
+ <string name="site_settings_threading_title">Ветвление</string>
+ <string name="site_settings_paging_title">Разбиение на страницы</string>
+ <string name="site_settings_sort_by_title">Приоритет сортировки</string>
+ <string name="site_settings_account_required_title">Пользователи должны выполнять вход</string>
+ <string name="site_settings_identity_required_title">Необходимо указывать имя и эл. адрес</string>
+ <string name="site_settings_receive_pingbacks_title">Получать уведомления</string>
+ <string name="site_settings_send_pingbacks_title">Отправлять уведомления</string>
+ <string name="site_settings_allow_comments_title">Разрешить комментарии</string>
+ <string name="site_settings_default_format_title">Основной формат</string>
+ <string name="site_settings_default_category_title">Основная рубрика</string>
+ <string name="site_settings_location_title">Вкл. определение местоположения</string>
+ <string name="site_settings_address_title">Адрес</string>
+ <string name="site_settings_title_title">Название сайта</string>
+ <string name="site_settings_tagline_title">Ключевая фраза</string>
+ <string name="site_settings_this_device_header">На этом устройстве</string>
+ <string name="site_settings_discussion_new_posts_header">Параметры по умолчанию для новых записей</string>
+ <string name="site_settings_account_header">Учетная запись</string>
+ <string name="site_settings_writing_header">Написание</string>
+ <string name="newest_first">Сначала новые</string>
+ <string name="site_settings_general_header">Общие</string>
+ <string name="discussion">Обсуждение</string>
+ <string name="privacy">Политика конфиденциальности</string>
+ <string name="related_posts">Похожие записи</string>
+ <string name="comments">Комментарии</string>
+ <string name="close_after">Закрыть обсуждение через</string>
+ <string name="oldest_first">Сначала старые</string>
+ <string name="media_error_no_permission_upload">У вас нет разрешения на загрузку файлов на этот сайт</string>
+ <string name="never">Никогда</string>
+ <string name="unknown">Неизвестно</string>
+ <string name="reader_err_get_post_not_found">Эта запись больше не существует</string>
+ <string name="reader_err_get_post_not_authorized">У вас нет прав на просмотр этой записи</string>
+ <string name="reader_err_get_post_generic">Не удается загрузить эту запись</string>
+ <string name="blog_name_no_spaced_allowed">В адресе сайта не должно быть пробелов</string>
+ <string name="invalid_username_no_spaces">Имя пользователя не должно содержать пробелы</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">На сайтах, на которые вы подписаны, в последнее время не публиковались новые записи</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Нет свежих записей</string>
+ <string name="media_details_copy_url_toast">URL-адрес скопирован в буфер обмена</string>
+ <string name="edit_media">Изменить файл мультимедиа</string>
+ <string name="media_details_copy_url">Копировать URL</string>
+ <string name="media_details_label_date_uploaded">Загружено</string>
+ <string name="media_details_label_date_added">Добавлено</string>
+ <string name="selected_theme">Выбранная тема</string>
+ <string name="could_not_load_theme">Не удалось загрузить тему</string>
+ <string name="theme_activation_error">Произошла ошибка. Не удалось активировать тему</string>
+ <string name="theme_by_author_prompt_append"> Автор: %1$s</string>
+ <string name="theme_prompt">Благодарим вас за выбор %1$s</string>
+ <string name="theme_try_and_customize">Попробовать и настроить</string>
+ <string name="theme_view">Просмотр</string>
+ <string name="theme_details">Подробнее</string>
+ <string name="theme_support">Поддержка</string>
+ <string name="theme_done">ГОТОВО</string>
+ <string name="theme_manage_site">УПРАВЛЕНИЕ САЙТОМ</string>
+ <string name="title_activity_theme_support">Темы</string>
+ <string name="theme_activate">Активировать</string>
+ <string name="date_range_start_date">Начальная дата</string>
+ <string name="date_range_end_date">Конечная дата</string>
+ <string name="current_theme">Текущая тема</string>
+ <string name="customize">Настроить</string>
+ <string name="details">Подробнее</string>
+ <string name="support">Поддержка</string>
+ <string name="active">Активно</string>
+ <string name="stats_referrers_spam_generic_error">При выполнении операции произошла ошибка. Состояние спама не изменилось.</string>
+ <string name="stats_referrers_marking_not_spam">Пометить как не спам</string>
+ <string name="stats_referrers_unspam">Не спам</string>
+ <string name="stats_referrers_marking_spam">Пометить как спам</string>
+ <string name="theme_auth_error_authenticate">Не удалось получить темы: не удалось проверить пользователя</string>
+ <string name="post_published">Запись опубликована</string>
+ <string name="page_published">Страница опубликована</string>
+ <string name="post_updated">Запись обновлена</string>
+ <string name="page_updated">Страница обновлена</string>
+ <string name="stats_referrers_spam">Спам</string>
+ <string name="theme_no_search_result_found">К сожалению, тем не найдено.</string>
+ <string name="media_file_name">Имя файла: %s</string>
+ <string name="media_uploaded_on">Загружен: %s</string>
+ <string name="media_dimensions">Размеры: %s</string>
+ <string name="upload_queued">В очереди</string>
+ <string name="media_file_type">Тип файла: %s</string>
+ <string name="reader_label_gap_marker">Загрузить другие записи</string>
+ <string name="notifications_no_search_results">Сайтов по запросу «%s» не найдено</string>
+ <string name="search_sites">Поиск сайтов</string>
+ <string name="notifications_empty_view_reader">Просмотреть Чтиво</string>
+ <string name="unread">Непрочитанное</string>
+ <string name="notifications_empty_action_followers_likes">Заявите о себе: комментируйте записи, которые вы прочитали.</string>
+ <string name="notifications_empty_action_comments">Вступите в разговор: комментируйте записи из блогов, на которые вы подписаны.</string>
+ <string name="notifications_empty_action_unread">Предоставьте тему для обсуждения: напишите новую запись.</string>
+ <string name="notifications_empty_action_all">Будьте активнее! Комментируйте записи из блогов, на которые вы подписаны.</string>
+ <string name="notifications_empty_likes">Новых оценок пока нет.</string>
+ <string name="notifications_empty_followers">Новых подписчиков пока нет.</string>
+ <string name="notifications_empty_comments">Новых комментариев пока нет.</string>
+ <string name="notifications_empty_unread">Вы всё прочитали!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Откройте статистику в приложении и попробуйте добавить виджет позже</string>
+ <string name="stats_widget_error_readd_widget">Пожалуйста, удалите виджет и добавьте его заново</string>
+ <string name="stats_widget_error_no_visible_blog">Без публичного блога доступа к статистике нет</string>
+ <string name="stats_widget_error_no_permissions">Статистика этого блога недоступна для вашей учётной записи WordPress.com</string>
+ <string name="stats_widget_error_no_account">Пожалуйста, войдите в WordPress</string>
+ <string name="stats_widget_error_generic">Не удалось загрузить статистику</string>
+ <string name="stats_widget_loading_data">Загрузка данных…</string>
+ <string name="stats_widget_name_for_blog">Сегодняшняя статистика %1$s</string>
+ <string name="stats_widget_name">Сегодняшняя статистика WordPress</string>
+ <string name="add_location_permission_required">Чтобы добавить местоположение, требуется подтверждение</string>
+ <string name="add_media_permission_required">Для добавления медиафайлов необходимы права доступа</string>
+ <string name="access_media_permission_required">Для доступа к медиафайлам необходимы права доступа</string>
+ <string name="stats_enable_rest_api_in_jetpack">Чтобы просмотреть статистику, включите модуль JSON API в Jetpack.</string>
+ <string name="error_open_list_from_notification">Эта запись или страница была опубликована на другом сайте</string>
+ <string name="reader_short_comment_count_multi">Комментариев: %s</string>
+ <string name="reader_short_comment_count_one">Один комментарий</string>
+ <string name="reader_label_submit_comment">ОТПРАВИТЬ</string>
+ <string name="reader_hint_comment_on_post">Ответить на запись...</string>
+ <string name="reader_discover_visit_blog">Перейти на %s</string>
+ <string name="reader_discover_attribution_blog">Изначально опубликовано на сайте %s</string>
+ <string name="reader_discover_attribution_author">Изначально опубликовано автором %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Изначально опубликовано автором %1$s на сайте %2$s</string>
+ <string name="reader_short_like_count_multi">%s оценок</string>
+ <string name="reader_short_like_count_one">1 оценка</string>
+ <string name="reader_label_follow_count">%,d подписчиков</string>
+ <string name="reader_short_like_count_none">Оценить</string>
+ <string name="reader_menu_tags">Редактировать метки и блоги</string>
+ <string name="reader_title_post_detail">Запись из Чтива</string>
+ <string name="local_draft_explainer">Эта запись — локальный черновик, который ещё не был опубликован</string>
+ <string name="local_changes_explainer">В этой записи есть локальные изменения, которые ещё не опубликованы</string>
+ <string name="notifications_push_summary">Настройки уведомлений, отображаемых на вашем устройстве.</string>
+ <string name="notifications_email_summary">Настройки уведомлений, отправляемых на e-mail из вашей учётной записи.</string>
+ <string name="notifications_tab_summary">Настройки уведомлений, отображаемых на вкладке «Уведомления».</string>
+ <string name="notifications_disabled">Уведомления в приложении отключены. Щёлкните здесь, чтобы включить их в настройках.</string>
+ <string name="notification_types">Типы уведомлений</string>
+ <string name="error_loading_notifications">Не удалось загрузить настройки уведомлений</string>
+ <string name="replies_to_your_comments">Ответы на ваши комментарии</string>
+ <string name="comment_likes">Оценки комментариев</string>
+ <string name="app_notifications">Уведомления в приложении</string>
+ <string name="notifications_tab">Вкладка «Уведомления»</string>
+ <string name="email">E-mail</string>
+ <string name="notifications_comments_other_blogs">Комментарии на других сайтах</string>
+ <string name="notifications_wpcom_updates">Обновления WordPress.com</string>
+ <string name="notifications_other">Другое</string>
+ <string name="notifications_account_emails">Письма с WordPress.com</string>
+ <string name="notifications_account_emails_summary">Важные письма, касающиеся вашей учётной записи, мы отправляем всегда, но можно также включить и другие полезные уведомления.</string>
+ <string name="notifications_sights_and_sounds">Визуализация и звуки</string>
+ <string name="your_sites">Ваши сайты</string>
+ <string name="stats_insights_latest_post_trend">С момента публикации %2$s прошло %1$s. Вот данные о популярности записи...</string>
+ <string name="stats_insights_latest_post_summary">Данные о последней записи</string>
+ <string name="button_revert">Отменить</string>
+ <string name="days_ago">%d дней назад</string>
+ <string name="yesterday">Вчера</string>
+ <string name="connectionbar_no_connection">Нет соединения</string>
+ <string name="page_trashed">Страница помещена в корзину</string>
+ <string name="post_deleted">Запись удалена</string>
+ <string name="post_trashed">Запись помещена в корзину</string>
+ <string name="stats_no_activity_this_period">Нет активности за указанный период</string>
+ <string name="trashed">Удалено</string>
+ <string name="button_back">Назад</string>
+ <string name="page_deleted">Страница удалена</string>
+ <string name="button_stats">Статистика</string>
+ <string name="button_trash">Корзина</string>
+ <string name="button_preview">Просмотреть</string>
+ <string name="button_view">Просмотреть</string>
+ <string name="button_edit">Изменить</string>
+ <string name="button_publish">Опубликовать</string>
+ <string name="my_site_no_sites_view_subtitle">Добавить сайт?</string>
+ <string name="my_site_no_sites_view_title">У вас пока нет сайтов WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Изображение</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">У вас нет разрешения на доступ к этому блогу.</string>
+ <string name="reader_toast_err_follow_blog_not_found">Не удаётся найти этот блог.</string>
+ <string name="undo">Назад</string>
+ <string name="tabbar_accessibility_label_my_site">Мой сайт</string>
+ <string name="tabbar_accessibility_label_me">Я</string>
+ <string name="passcodelock_prompt_message">Введите ваш PIN-код</string>
+ <string name="editor_toast_changes_saved">Обновления сохранены</string>
+ <string name="push_auth_expired">Время запроса истекло. Авторизуйтесь на WordPress.com и попробуйте еще раз.</string>
+ <string name="stats_insights_best_ever">Наибольшее количество просмотров</string>
+ <string name="ignore">Пропустить</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% просмотров</string>
+ <string name="stats_insights_most_popular_hour">Самое популярное время</string>
+ <string name="stats_insights_most_popular_day">Самый популярный день</string>
+ <string name="stats_insights_popular">Самый популярный день и час</string>
+ <string name="stats_insights_today">Cтатистические данные на сегодня</string>
+ <string name="stats_insights_all_time">Записи, просмотры и посетители за все время</string>
+ <string name="stats_insights">Тенденции</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Чтобы просмотреть статистику, войдите в учетную запись WordPress.com, которую вы использовали для подключения Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Ищете «Другую недавнюю статистику»? Мы перенесли эти данные на страницу «Тенденции».</string>
+ <string name="me_disconnect_from_wordpress_com">Отключение от WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Подключение к WordPress.com</string>
+ <string name="me_btn_login_logout">Вход/выход</string>
+ <string name="account_settings">Настройки учётной записи</string>
+ <string name="me_btn_support">Помощь и техническая поддержка</string>
+ <string name="site_picker_cant_hide_current_site">"%s" не был скрыт, так как это текущий сайт.</string>
+ <string name="site_picker_create_dotcom">Создать сайт на WordPress.com</string>
+ <string name="site_picker_add_site">Добавить веб-сайт</string>
+ <string name="site_picker_add_self_hosted">Добавить автономный веб-сайт</string>
+ <string name="site_picker_edit_visibility">Показать/скрыть веб-сайты</string>
+ <string name="my_site_btn_view_admin">Просмотреть приложение «Администратор»</string>
+ <string name="my_site_btn_view_site">Просмотреть веб-сайт</string>
+ <string name="site_picker_title">Выбрать сайт</string>
+ <string name="my_site_btn_switch_site">Сменить сайт</string>
+ <string name="my_site_btn_blog_posts">Записи в блоге</string>
+ <string name="my_site_btn_site_settings">Настройки</string>
+ <string name="my_site_header_look_and_feel">Внешний вид</string>
+ <string name="my_site_header_publish">Опубликовать</string>
+ <string name="my_site_header_configuration">Конфигурация</string>
+ <string name="reader_label_new_posts_subtitle">Нажмите, чтобы показать записи.</string>
+ <string name="notifications_account_required">Войти в WordPress.com для получения уведомлений</string>
+ <string name="stats_unknown_author">Неизвестный автор</string>
+ <string name="image_added">Изображение добавлено.</string>
+ <string name="signout">Отключить</string>
+ <string name="deselect_all">Снять выделение</string>
+ <string name="show">Показать</string>
+ <string name="hide">Скрыть</string>
+ <string name="select_all">Выделить все</string>
+ <string name="sign_out_wpcom_confirm">Отключение учётной записи приведет к удалению всех данных @%s WordPress.com с этого устройства, в том числе локальных черновиков и локальных изменений.</string>
+ <string name="select_from_new_picker">Множественный выбор с помощью нового диалога</string>
+ <string name="stats_generic_error">Не удалось загрузить необходимую статистику</string>
+ <string name="no_device_videos">Нет видео</string>
+ <string name="no_blog_images">Нет изображений</string>
+ <string name="no_blog_videos">Нет видео</string>
+ <string name="no_device_images">Нет изображений</string>
+ <string name="error_loading_blog_images">Не удалось загрузить изображения</string>
+ <string name="error_loading_blog_videos">Не удалось загрузить видео</string>
+ <string name="error_loading_images">Ошибка при загрузке изображений</string>
+ <string name="error_loading_videos">Ошибка при загрузке видео</string>
+ <string name="loading_blog_images">Загрузка изображений</string>
+ <string name="loading_blog_videos">Загрузка видео</string>
+ <string name="no_media_sources">Не удалось загрузить медиафайлы</string>
+ <string name="loading_videos">Загрузка видео</string>
+ <string name="loading_images">Загрузка изображений</string>
+ <string name="no_media">Нет медиафайлов</string>
+ <string name="device">Устройство</string>
+ <string name="language">Язык</string>
+ <string name="add_to_post">Добавить в запись</string>
+ <string name="media_picker_title">Выбрать файл</string>
+ <string name="take_photo">Сделать фото</string>
+ <string name="take_video">Записать видео</string>
+ <string name="tab_title_device_images">Изображения на устройстве</string>
+ <string name="tab_title_device_videos">Видео на устройстве</string>
+ <string name="tab_title_site_images">Изображения с сайта</string>
+ <string name="tab_title_site_videos">Видео с сайта</string>
+ <string name="media_details_label_file_name">Имя файла</string>
+ <string name="media_details_label_file_type">Тип файла</string>
+ <string name="error_publish_no_network">Нельзя опубликовать без подключения к сети. Сохранено как черновик.</string>
+ <string name="editor_toast_invalid_path">Неверный путь к файлу</string>
+ <string name="verification_code">Код подтверждения</string>
+ <string name="invalid_verification_code">Неверный код подтверждения</string>
+ <string name="verify">Подтвердить</string>
+ <string name="two_step_footer_label">Введите код из вашего приложения аутентификации.</string>
+ <string name="two_step_footer_button">Отправить код SMS-сообщением</string>
+ <string name="two_step_sms_sent">Проверьте ваши SMS-сообщения и найдите код подтверждения.</string>
+ <string name="sign_in_jetpack">Войдите в вашу учётную запись WordPress.com для подключения к Jetpack.</string>
+ <string name="auth_required">Войдите заново для продолжения.</string>
+ <string name="reader_empty_posts_request_failed">Не удалось загрузить записи.</string>
+ <string name="publisher">Автор:</string>
+ <string name="error_notification_open">Не удалось открыть уведомление.</string>
+ <string name="stats_followers_total_email_paged">Подписчики по эл. почте: %1$d–%2$d из %3$s</string>
+ <string name="stats_search_terms_unknown_search_terms">Неизвестные поисковые запросы</string>
+ <string name="stats_followers_total_wpcom_paged">Подписчики на WordPress.com: %1$d–%2$d из %3$s</string>
+ <string name="stats_empty_search_terms_desc">Узнайте больше о поисковом трафике, просмотрев ключевые слова, которые посетители вашего сайта использовали, чтобы найти его.</string>
+ <string name="stats_empty_search_terms">Нет данных о поисковых запросах</string>
+ <string name="stats_entry_search_terms">Поисковый запрос</string>
+ <string name="stats_view_authors">Авторы</string>
+ <string name="stats_view_search_terms">Поисковые запросы</string>
+ <string name="comments_fetching">Загрузка комментариев…</string>
+ <string name="pages_fetching">Загрузка страниц…</string>
+ <string name="toast_err_post_uploading">Невозможно открыть запись во время загрузки.</string>
+ <string name="posts_fetching">Загрузка записей…</string>
+ <string name="media_fetching">Загрузка медиафайлов…</string>
+ <string name="post_uploading">Идет отправка на сервер.</string>
+ <string name="stats_total">Итого</string>
+ <string name="stats_overall">Все время</string>
+ <string name="stats_period">Период</string>
+ <string name="logs_copied_to_clipboard">Журналы приложения скопированы в буфер обмена.</string>
+ <string name="reader_label_new_posts">Новые записи</string>
+ <string name="reader_empty_posts_in_blog">Этот блог пуст.</string>
+ <string name="stats_average_per_day">В среднем за день</string>
+ <string name="stats_recent_weeks">Последние недели</string>
+ <string name="error_copy_to_clipboard">Произошла ошибка при копировании текста в буфер обмена.</string>
+ <string name="reader_page_recommended_blogs">Сайты, подобранные для вас</string>
+ <string name="stats_months_and_years">Месяцы и года</string>
+ <string name="themes_fetching">Получение тем…</string>
+ <string name="stats_for">Статистика для %s</string>
+ <string name="stats_other_recent_stats_label">Другая недавняя статистика</string>
+ <string name="stats_view_all">Посмотреть все</string>
+ <string name="stats_view">Просмотр</string>
+ <string name="stats_followers_months">%1$d месяцев</string>
+ <string name="stats_followers_a_year">Год</string>
+ <string name="stats_followers_years">%1$d лет</string>
+ <string name="stats_followers_a_month">Месяц</string>
+ <string name="stats_followers_minutes">%1$d минуты</string>
+ <string name="stats_followers_an_hour_ago">час назад</string>
+ <string name="stats_followers_hours">%1$d часов</string>
+ <string name="stats_followers_a_day">День</string>
+ <string name="stats_followers_days">%1$d дней</string>
+ <string name="stats_followers_a_minute_ago">минуту назад</string>
+ <string name="stats_followers_seconds_ago">несколько секунд назад</string>
+ <string name="stats_followers_total_email">Общее количество подписчиков по электронной почте: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Электронная почта</string>
+ <string name="stats_followers_total_wpcom">Общее количество подписчиков WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Общее число записей с подписками на комментарии: %1$s</string>
+ <string name="stats_comments_by_authors">По авторам</string>
+ <string name="stats_comments_by_posts_and_pages">По записям и страницам</string>
+ <string name="stats_empty_followers_desc">Отслеживайте общее число подписчиков и продолжительность подписки каждого из них на ваш сайт.</string>
+ <string name="stats_empty_followers">Подписчики отсутствуют.</string>
+ <string name="stats_empty_publicize_desc">Отслеживайте действия своих подписчиков из различных служб социальных сетей с помощью функции Publicize.</string>
+ <string name="stats_empty_publicize">Подписчиков на публикации не зарегистрировано.</string>
+ <string name="stats_empty_video">Ни одно видео не воспроизведено.</string>
+ <string name="stats_empty_video_desc">При загрузке видео с помощью VideoPress можно узнать число их просмотров.</string>
+ <string name="stats_empty_comments_desc">Если комментарии на сайте разрешены, можно отслеживать действия популярных комментаторов и определять содержимое, вызывающее самые оживленные обсуждения, на основе последней 1000 комментариев.</string>
+ <string name="stats_empty_tags_and_categories_desc">Получите общие сведения о самых популярных темах вашего сайта, отраженные в популярных записях за прошлую неделю.</string>
+ <string name="stats_empty_top_authors_desc">Отслеживайте число просмотров записей каждого публикатора и получайте более подробные сведения, чтобы определить самое популярное содержимое каждого автора.</string>
+ <string name="stats_empty_tags_and_categories">Записи и страницы с метками не просматривались.</string>
+ <string name="stats_empty_clicks_desc">Если содержимое включает в себя ссылки на другие сайты, можно будет просмотреть, по каким из них посетители переходят чаще всего.</string>
+ <string name="stats_empty_referrers_desc">Узнайте больше о видимости своего сайта, оценив веб-сайты и поисковые системы, отправляющие наибольший объем трафика</string>
+ <string name="stats_empty_clicks_title">Переходов не зарегистрировано.</string>
+ <string name="stats_empty_referrers_title">Источников перехода не зарегистрировано.</string>
+ <string name="stats_empty_top_posts_title">Записи и страницы не просматривались.</string>
+ <string name="stats_empty_top_posts_desc">Узнайте, какое содержимое пользователи просматривают чаще всего, и проверьте эффективность отдельных записей и страниц с течением времени.</string>
+ <string name="stats_totals_followers">С</string>
+ <string name="stats_empty_geoviews">Стран не зарегистрировано.</string>
+ <string name="stats_empty_geoviews_desc">Просмотрите список, чтобы узнать, какие страны и регионы привлекают максимальный объем трафика на ваш сайт.</string>
+ <string name="stats_entry_video_plays">Видео</string>
+ <string name="stats_entry_top_commenter">Автор</string>
+ <string name="stats_entry_publicize">Служба</string>
+ <string name="stats_entry_followers">Читатель</string>
+ <string name="stats_totals_publicize">Подписчики</string>
+ <string name="stats_entry_clicks_link">Ссылка</string>
+ <string name="stats_view_top_posts_and_pages">Записи и страницы</string>
+ <string name="stats_view_videos">Видео</string>
+ <string name="stats_view_publicize">Публикация</string>
+ <string name="stats_view_followers">Подписчики</string>
+ <string name="stats_view_countries">Страны</string>
+ <string name="stats_likes">Любимое</string>
+ <string name="stats_pagination_label">Страница %1$s из %2$s</string>
+ <string name="stats_timeframe_years">Годы</string>
+ <string name="stats_views">Просмотров</string>
+ <string name="stats_visitors">Посетителей</string>
+ <string name="ssl_certificate_details">Подробнее</string>
+ <string name="delete_sure_post">Удалить запись</string>
+ <string name="delete_sure">Удалить черновик</string>
+ <string name="delete_sure_page">Удалить страницу</string>
+ <string name="confirm_delete_multi_media">Удалить выбранные элементы?</string>
+ <string name="confirm_delete_media">Удалить выбранный элемент?</string>
+ <string name="cab_selected">Выбрано: %d</string>
+ <string name="media_gallery_date_range">Отображаются файлы за период %1$s — %2$s</string>
+ <string name="sure_to_remove_account">Удалить этот сайт?</string>
+ <string name="reader_empty_followed_blogs_title">Вы еще не подписаны ни на один сайт.</string>
+ <string name="reader_empty_posts_liked">Вы еще не поставили отметку «Нравится» ни одной записи.</string>
+ <string name="faq_button">Часто задаваемые вопросы</string>
+ <string name="browse_our_faq_button">Поиск в списке часто задаваемых вопросов</string>
+ <string name="nux_help_description">Перейдите в справочный центр, чтобы получить ответы на часто задаваемые вопросы, или обратитесь к другим участникам форума.</string>
+ <string name="agree_terms_of_service">Создавая учетную запись, вы соглашаетесь с %1$sУсловиями использования%2$s.</string>
+ <string name="create_new_blog_wpcom">Создать блог на WordPress.com</string>
+ <string name="new_blog_wpcom_created">Блог на WordPress.com успешно создан!</string>
+ <string name="reader_empty_comments">Пока нет комментариев</string>
+ <string name="reader_empty_posts_in_tag">Нет записей с этой меткой.</string>
+ <string name="reader_label_comment_count_multi">Комментариев: %,d</string>
+ <string name="reader_label_view_original">Просмотреть исходную публикацию</string>
+ <string name="reader_label_like">Нравится</string>
+ <string name="reader_label_comment_count_single">Один комментарий</string>
+ <string name="reader_label_comments_closed">Обсуждение закрыто</string>
+ <string name="reader_label_comments_on">Комментарии включены</string>
+ <string name="reader_title_photo_viewer">%1$d из %2$d</string>
+ <string name="error_publish_empty_post">Нельзя опубликовать пустую запись.</string>
+ <string name="error_refresh_unauthorized_posts">У вас нет прав на просмотр или редактирование записей.</string>
+ <string name="error_refresh_unauthorized_pages">У вас нет прав на просмотр или редактирование страниц.</string>
+ <string name="error_refresh_unauthorized_comments">У вас нет прав на просмотр или редактирование комментариев.</string>
+ <string name="older_month">Больше месяца назад</string>
+ <string name="more">Больше</string>
+ <string name="older_two_days">Больше двух дней назад</string>
+ <string name="older_last_week">Больше недели</string>
+ <string name="stats_no_blog">Невозможно загрузить статистику для этого блога.</string>
+ <string name="select_a_blog">Выберите сайт WordPress</string>
+ <string name="sending_content">Загрузка содержимого: %s</string>
+ <string name="uploading_total">Загружается %1$d из %2$d</string>
+ <string name="mnu_comment_liked">Понравилось</string>
+ <string name="comment">Комментировать</string>
+ <string name="comment_trashed">Комментарий перемещен в корзину</string>
+ <string name="posts_empty_list">Пока нет записей. Создайте первую!</string>
+ <string name="comment_reply_to_user">Ответить автору %s</string>
+ <string name="pages_empty_list">Пока нет страниц. Создайте первую!</string>
+ <string name="media_empty_list_custom_date">За этот период нет медиафайлов.</string>
+ <string name="posting_post">Публикуется материал "%s"</string>
+ <string name="signing_out">Выход из системы...</string>
+ <string name="reader_toast_err_generic">Не удалось совершить это действие</string>
+ <string name="reader_toast_err_block_blog">Не удалось заблокировать этот блог</string>
+ <string name="reader_toast_blog_blocked">Записи из этого блога больше не будут отображаться</string>
+ <string name="reader_menu_block_blog">Заблокировать этот блог</string>
+ <string name="contact_us">Обратная связь</string>
+ <string name="hs__conversation_detail_error">Опишите проблему, с которой вы столкнулись</string>
+ <string name="hs__new_conversation_header">Чат поддержки</string>
+ <string name="hs__conversation_header">Чат поддержки</string>
+ <string name="hs__username_blank_error">Введите корректное имя</string>
+ <string name="hs__invalid_email_error">Введите корректный адрес e-mail</string>
+ <string name="add_location">Добавить местоположение</string>
+ <string name="current_location">Текущее местоположение</string>
+ <string name="search_location">Поиск</string>
+ <string name="edit_location">Изменить</string>
+ <string name="search_current_location">Найти</string>
+ <string name="preference_send_usage_stats">Отправить статистику</string>
+ <string name="preference_send_usage_stats_summary">Автоматически отправлять статистику использования для улучшения WordPress для Android</string>
+ <string name="update_verb">Обновить</string>
+ <string name="schedule_verb">Запланировать</string>
+ <string name="reader_title_blog_preview">Блог из Чтива</string>
+ <string name="reader_title_tag_preview">Метка из Чтива</string>
+ <string name="reader_title_subs">Метки и блоги</string>
+ <string name="reader_page_followed_tags">Отслеживаемые метки</string>
+ <string name="reader_page_followed_blogs">Отслеживаемые сайты</string>
+ <string name="reader_hint_add_tag_or_url">Введите URL или метку для подписки</string>
+ <string name="reader_label_followed_blog">Блог отслеживается</string>
+ <string name="reader_label_tag_preview">Записи с меткой %s</string>
+ <string name="reader_toast_err_get_blog_info">Не удалось отобразить блог</string>
+ <string name="reader_toast_err_already_follow_blog">Вы уже подписаны на этот блог</string>
+ <string name="reader_toast_err_follow_blog">Не удалось подписаться на блог</string>
+ <string name="reader_toast_err_unfollow_blog">Не удалось отписаться</string>
+ <string name="reader_empty_recommended_blogs">Нет рекомендованных блогов</string>
+ <string name="saving">Сохранение...</string>
+ <string name="media_empty_list">Нет медиа</string>
+ <string name="ptr_tip_message">Потяните вниз, чтобы обновить</string>
+ <string name="help">Помощь</string>
+ <string name="forgot_password">Забыли пароль?</string>
+ <string name="forums">Форумы</string>
+ <string name="help_center">Центр помощи</string>
+ <string name="ssl_certificate_error">Недействительный сертификат SSL</string>
+ <string name="ssl_certificate_ask_trust">Если вы обычно заходите на этот сайт без проблем, эта ошибка может означать, что кто-то пытается подделать сайт и вам не стоит сюда заходить. Хотите ли вы доверять ssl сертификату?</string>
+ <string name="out_of_memory">Устройству не хватает памяти</string>
+ <string name="no_network_message">Нет доступной сети.</string>
+ <string name="could_not_remove_account">Невозможно удалить сайт</string>
+ <string name="gallery_error">Медиа-файл не может быть восстановлен</string>
+ <string name="blog_not_found">Ошибка при попытке подключиться к блогу</string>
+ <string name="wait_until_upload_completes">Подождите конца загрузки</string>
+ <string name="theme_fetch_failed">Ошибка выбора темы.</string>
+ <string name="theme_set_failed">Невозможно установить тему</string>
+ <string name="theme_auth_error_message">Убедитесь, что у вас достаточно прав для установки темы</string>
+ <string name="comments_empty_list">Нет комментариев</string>
+ <string name="mnu_comment_unspam">Нет спама</string>
+ <string name="no_site_error">Невозможно присоединиться к сайту WordPress</string>
+ <string name="adding_cat_failed">Добавление категории не удалось</string>
+ <string name="adding_cat_success">Категория успешно добавлена</string>
+ <string name="cat_name_required">Поле "название категории" является обязательным</string>
+ <string name="category_automatically_renamed">Название категории %1$s не является правильным. Она будет переименована в %2$s.</string>
+ <string name="no_account">Не найдено аккаунта WordPress. Добавьте аккаунт и попробуйте снова</string>
+ <string name="sdcard_message">Для загрузки медиа нужно подключить SD карту</string>
+ <string name="stats_empty_comments">Нет комментариев</string>
+ <string name="stats_bar_graph_empty">Статистика недоступна</string>
+ <string name="invalid_url_message">Убедитесь, что введен допустимый URL-адрес</string>
+ <string name="reply_failed">Ошибочный ответ</string>
+ <string name="notifications_empty_list">Нет уведомлений</string>
+ <string name="error_delete_post">Произошла ошибка при удалении %s</string>
+ <string name="error_refresh_posts">Пост не может быть обновлен в данный момент</string>
+ <string name="error_refresh_pages">Страница не может быть обновлена в данный момент</string>
+ <string name="error_refresh_notifications">Уведомление не могут быть обновлены в данный момент</string>
+ <string name="error_refresh_comments">Комментарии не могут быть обновлены в данный момент</string>
+ <string name="error_refresh_stats">Статистика не может быть обновлена в данный момент</string>
+ <string name="error_generic">Произошла ошибка</string>
+ <string name="error_moderate_comment">Во время модерации произошла ошибка</string>
+ <string name="error_edit_comment">Во время редактирования комментария произошла ошибка</string>
+ <string name="error_upload">Произошла ошибка при загрузке %s</string>
+ <string name="error_load_comment">Невозможно загрузить комментарии</string>
+ <string name="error_downloading_image">Ошибка при загрузке картинки</string>
+ <string name="passcode_wrong_passcode">Неправильный PIN</string>
+ <string name="invalid_email_message">Ваш адрес email не подходит</string>
+ <string name="invalid_password_message">Пароль должен содержать не меньше 4 знаков</string>
+ <string name="invalid_username_too_short">Имя должно содержать не меньше 4 знаков</string>
+ <string name="invalid_username_too_long">Имя должно содержать не больше 61 знака</string>
+ <string name="username_only_lowercase_letters_and_numbers">Имя пользователя должно содержать только латиницу (a-z) и цифры</string>
+ <string name="username_required">Введите имя пользователя</string>
+ <string name="username_not_allowed">Данное имя запрещено</string>
+ <string name="username_must_be_at_least_four_characters">Имя пользователя должно быть не менее 4 символов</string>
+ <string name="username_contains_invalid_characters">Имя пользователя не должно содержать подчёркивание</string>
+ <string name="username_must_include_letters">Имя пользователя должно содержать хотя бы 1 букву (a-z)</string>
+ <string name="email_invalid">Введите правильный email адрес</string>
+ <string name="email_not_allowed">Непозволительный email</string>
+ <string name="username_exists">Это имя пользователя уже существует</string>
+ <string name="email_exists">Этот email уже используется</string>
+ <string name="username_reserved_but_may_be_available">Это имя пользователя в настоящее время зарезервировано, но может стать доступным через пару дней</string>
+ <string name="blog_name_required">Введите адрес сайта</string>
+ <string name="blog_name_not_allowed">Адрес сайта в неправильном виде</string>
+ <string name="blog_name_must_be_at_least_four_characters">Адрес сайта не может быть короче 4 знаков</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Адрес сайта должен быть короче 64 символов</string>
+ <string name="blog_name_contains_invalid_characters">Адрес сайта не должен содержать знак подчёркивания "_"</string>
+ <string name="blog_name_cant_be_used">Вы не можете использовать этот адрес сайта</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Адрес сайта может содержать только строчные буквы (a-z) и цифры</string>
+ <string name="blog_name_exists">Такой сайт уже существует</string>
+ <string name="blog_name_reserved">Этот сайт зарезервирован</string>
+ <string name="blog_name_reserved_but_may_be_available">Это имя сайта сейчас зарезервировано, но может стать доступным через пару дней</string>
+ <string name="username_or_password_incorrect">Имя пользователя или пароль введены неверно</string>
+ <string name="nux_cannot_log_in">Невозможно войти</string>
+ <string name="xmlrpc_error">Невозможно подключиться: введите полный путь к xmlrpc.php на вашем сайте и попробуйте снова.</string>
+ <string name="select_categories">Выберите категории</string>
+ <string name="account_details">Сведения об аккаунте</string>
+ <string name="edit_post">Редактировать запись</string>
+ <string name="add_comment">Добавить комментарий</string>
+ <string name="connection_error">Ошибка соединения</string>
+ <string name="cancel_edit">Отменить изменения</string>
+ <string name="scaled_image_error">Введите допустимое значение ширины</string>
+ <string name="post_not_found">Произошла ошибка при загрузке поста. Обновите ваши записи и повторите попытку.</string>
+ <string name="learn_more">Узнать больше</string>
+ <string name="media_gallery_settings_title">Настройки галереи</string>
+ <string name="media_gallery_image_order">Порядок изображений</string>
+ <string name="media_gallery_num_columns">Число столбцов</string>
+ <string name="media_gallery_type_thumbnail_grid">Эскиз сетки</string>
+ <string name="media_gallery_edit">Редактировать галерею</string>
+ <string name="media_error_no_permission">У вас нет прав для просмотра медиа-библиотеки</string>
+ <string name="cannot_delete_multi_media_items">Некоторые медиа-файлы не могут быть удалены сейчас. Попробуйте позже.</string>
+ <string name="themes_live_preview">Предварительный просмотр</string>
+ <string name="theme_current_theme">Текущая тема</string>
+ <string name="theme_premium_theme">Премиум тема</string>
+ <string name="link_enter_url_text">Текст ссылки(необязательно)</string>
+ <string name="create_a_link">Создать ссылку</string>
+ <string name="page_settings">Настройки страницы</string>
+ <string name="local_draft">Локальные черновики</string>
+ <string name="upload_failed">Ошибка загрузки</string>
+ <string name="horizontal_alignment">Горизонтальное выравнивание</string>
+ <string name="file_not_found">Не удалось найти файл для загрузки: он был удален или перемещен.</string>
+ <string name="post_settings">Настройки записи</string>
+ <string name="delete_post">Удалить запись</string>
+ <string name="delete_page">Удалить страницу</string>
+ <string name="comment_status_approved">Одобрен</string>
+ <string name="comment_status_unapproved">На рассмотрении</string>
+ <string name="comment_status_spam">Спам</string>
+ <string name="comment_status_trash">Удалён</string>
+ <string name="edit_comment">Редактировать комментарий</string>
+ <string name="mnu_comment_approve">Одобрить</string>
+ <string name="mnu_comment_unapprove">Не одобрить</string>
+ <string name="mnu_comment_spam">Спам</string>
+ <string name="mnu_comment_trash">Удалить</string>
+ <string name="dlg_approving_comments">Одобрить</string>
+ <string name="dlg_unapproving_comments">Не одобрить</string>
+ <string name="dlg_spamming_comments">Отметить как спам</string>
+ <string name="dlg_trashing_comments">Удалить</string>
+ <string name="dlg_confirm_trash_comments">Удалить?</string>
+ <string name="trash_yes">Да</string>
+ <string name="trash_no">Не удалять</string>
+ <string name="trash">Удалённые</string>
+ <string name="author_name">Имя автора</string>
+ <string name="author_email">Email автора</string>
+ <string name="author_url">URL автора</string>
+ <string name="hint_comment_content">Комментировать</string>
+ <string name="saving_changes">Сохранить изменения</string>
+ <string name="sure_to_cancel_edit_comment">Отменить редактирование комментария?</string>
+ <string name="content_required">Требуется комментарий</string>
+ <string name="toast_comment_unedited">Комментарий не изменялся</string>
+ <string name="remove_account">Удалить сайт</string>
+ <string name="blog_removed_successfully">Сайт удачно удалён</string>
+ <string name="delete_draft">Удалить черновик</string>
+ <string name="preview_page">Предпросмотр страницы</string>
+ <string name="preview_post">Предпросмотр записи</string>
+ <string name="comment_added">Комментарий добавлен</string>
+ <string name="post_not_published">Неопубликовано</string>
+ <string name="page_not_published">Неопубликовано</string>
+ <string name="view_in_browser">Открыть в браузере</string>
+ <string name="add_new_category">Добавить категорию</string>
+ <string name="category_name">Имя категории</string>
+ <string name="category_slug">Короткая категория(необязательно)</string>
+ <string name="category_desc">Описание категории(необязательно)</string>
+ <string name="category_parent">Родительская категория(необязательно)</string>
+ <string name="share_action_post">Новая запись</string>
+ <string name="share_action_media">Медиа-библиотека</string>
+ <string name="file_error_create">Не удалось создать временный файл для выгрузки медиа. Убедитесь, что имеется достаточно свободного места на вашем устройстве.</string>
+ <string name="location_not_found">Неизвестное местоположение</string>
+ <string name="open_source_licenses">Лицензия Open Source</string>
+ <string name="invalid_site_url_message">Убедитесь, что введен допустимый URL-адрес сайта</string>
+ <string name="pending_review">Ожидающие рецензии</string>
+ <string name="http_credentials">Учетные данные HTTP(необязательно)</string>
+ <string name="http_authorization_required">Требуется авторизация</string>
+ <string name="post_format">Формат записи</string>
+ <string name="notifications_empty_all">Уведомлений пока нет.</string>
+ <string name="new_post">Новая запись</string>
+ <string name="new_media">Новое медиа</string>
+ <string name="view_site">Просмотреть сайт</string>
+ <string name="privacy_policy">Политика конфиденциальности</string>
+ <string name="local_changes">Локальные изменения</string>
+ <string name="image_settings">Настройки изображений</string>
+ <string name="add_account_blog_url">Адрес блога</string>
+ <string name="wordpress_blog">Блог WordPress</string>
+ <string name="error_blog_hidden">Этот блог является скрытым и не может быть загружен. Откройте его через настройки и попробуйте снова.</string>
+ <string name="fatal_db_error">Произошла ошибка при создании базы данных приложения- попробуйте его переустановить.</string>
+ <string name="jetpack_message_not_admin">Для статистики нужен плагин Jetpack- свяжитесь с администрацией сайта.</string>
+ <string name="reader_title_applog">Лог приложения</string>
+ <string name="reader_share_link">Поделиться ссылкой</string>
+ <string name="reader_toast_err_add_tag">Не удается добавить эту метку</string>
+ <string name="reader_toast_err_remove_tag">Не удается удалить эту метку</string>
+ <string name="required_field">Обязательное поле</string>
+ <string name="email_hint">Email адрес</string>
+ <string name="site_address">Адрес вашего сайта</string>
+ <string name="email_cant_be_used_to_signup">Вы не можете использовать этот адрес для регистрации- у нас есть проблемы с блокированием некоторых наших сообщений электронной почты. Используйте email другого поставщика услуг электронной почты.</string>
+ <string name="email_reserved">Этот адрес уже используется. Проверьте свою почту на наличие письма с кодом активации. Если вы не активировали запись, вы можете попробовать еще раз через несколько дней.</string>
+ <string name="blog_name_must_include_letters">Адрес сайта должен иметь не меньше одной буквы (a-z)</string>
+ <string name="blog_name_invalid">Недопустимый адрес сайта</string>
+ <string name="blog_title_invalid">Недопустимый заголовок сайта</string>
+ <string name="deleting_page">Удаление страницы</string>
+ <string name="deleting_post">Удаление записи</string>
+ <string name="share_url_post">Поделиться записью</string>
+ <string name="share_url_page">Поделиться страницей</string>
+ <string name="share_link">Поделиться ссылкой</string>
+ <string name="creating_your_account">Создайте аккаунт</string>
+ <string name="creating_your_site">Создайте свой сайт</string>
+ <string name="reader_empty_posts_in_tag_updating">Ищем записи...</string>
+ <string name="error_refresh_media">Упс. Что-то пошло не так при обновлении медиатеки. Попробуйте позже.</string>
+ <string name="reader_likes_you_and_multi">Вам и %,d другим понравилось это.</string>
+ <string name="reader_likes_multi">Понравилось %,d людям</string>
+ <string name="reader_toast_err_get_comment">Не удается загрузить этот комментарий</string>
+ <string name="reader_label_reply">Ответ</string>
+ <string name="video">Видео</string>
+ <string name="download">Загрузить медиа</string>
+ <string name="comment_spammed">Комментарий помечен как спам.</string>
+ <string name="cant_share_no_visible_blog">Вы не можете поделиться через WordPress без видимого блога</string>
+ <string name="select_time">Выберите время</string>
+ <string name="reader_likes_you_and_one">Вам и ещё одному понравилось это</string>
+ <string name="reader_empty_followed_blogs_description">Просто нажмите на иконку справа вверху, чтобы найти что-нибудь интересное!</string>
+ <string name="select_date">Выберите дату</string>
+ <string name="pick_photo">Выберите фото</string>
+ <string name="account_two_step_auth_enabled">Включена двушаговая авторизация- зайдите в настройки безопасности на WordPress.com и сгенерируйте специальный ключ для приложения.</string>
+ <string name="pick_video">Выберите видео</string>
+ <string name="reader_toast_err_get_post">Не удаётся загрузить эту запись</string>
+ <string name="validating_user_data">Проверка данных пользователя</string>
+ <string name="validating_site_data">Проверка данных сайта</string>
+ <string name="password_invalid">Требуется более сложный пароль. Используйте пароль длинной 7 и более символов, содержащий прописные и строчные буквы, цифры а так же спецсимволы.</string>
+ <string name="nux_tap_continue">Продолжить</string>
+ <string name="nux_welcome_create_account">Создать аккаунт</string>
+ <string name="signing_in">Вход в систему...</string>
+ <string name="nux_add_selfhosted_blog">Добавить автономный сайт</string>
+ <string name="nux_oops_not_selfhosted_blog">Войти в WordPress.com</string>
+ <string name="media_add_popup_title">Добавить в медиа-библиотеку</string>
+ <string name="media_add_new_media_gallery">Создать галерею</string>
+ <string name="empty_list_default">Этот список пуст</string>
+ <string name="select_from_media_library">Выбрать из медиа-библиотеки</string>
+ <string name="jetpack_message">Для статистики нужен плагин Jetpack. Хотите установить его?</string>
+ <string name="jetpack_not_found">Плагин Jetpack не найден</string>
+ <string name="reader_untitled_post">(Без названия)</string>
+ <string name="reader_share_subject">Поделиться через %s</string>
+ <string name="reader_btn_share">Поделиться</string>
+ <string name="reader_btn_follow">Подписаться</string>
+ <string name="reader_btn_unfollow">Отписаться</string>
+ <string name="reader_hint_comment_on_comment">Ответить на комментарий…</string>
+ <string name="reader_label_added_tag">Добавить %s</string>
+ <string name="reader_label_removed_tag">Удалить %s</string>
+ <string name="reader_likes_one">Одному человеку нравится это</string>
+ <string name="reader_likes_only_you">Вам нравится это</string>
+ <string name="reader_toast_err_comment_failed">Не удалось оставить комментарий</string>
+ <string name="reader_toast_err_tag_exists">Вы уже подписаны на эту метку</string>
+ <string name="reader_toast_err_tag_invalid">Это неверная метка</string>
+ <string name="reader_toast_err_share_intent">Не в состоянии поделиться</string>
+ <string name="reader_toast_err_view_image">Не удаётся просмотреть изображение</string>
+ <string name="reader_toast_err_url_intent">Не удаётся открыть %s</string>
+ <string name="reader_empty_followed_tags">Вы не подписаны ни на одну метку.</string>
+ <string name="create_account_wpcom">Создайте учетную запись WordPress.com</string>
+ <string name="button_next">Далее</string>
+ <string name="connecting_wpcom">Подключиться к WordPress.com</string>
+ <string name="username_invalid">Неверное имя</string>
+ <string name="limit_reached">Достигнут лимит- попробуйте через минуту. При неудачных попытках время будет увеличиваться. Если вы думаете, что это ошибка, обратитесь к техподдержке.</string>
+ <string name="nux_tutorial_get_started_title">Начать работу!</string>
+ <string name="themes">Темы</string>
+ <string name="all">Всё</string>
+ <string name="images">Изображения</string>
+ <string name="unattached">Неприкреплено</string>
+ <string name="custom_date">Пользовательская дата</string>
+ <string name="media_add_popup_capture_photo">Добавить фото</string>
+ <string name="media_add_popup_capture_video">Добавить видео</string>
+ <string name="media_gallery_image_order_random">Случайный</string>
+ <string name="media_gallery_image_order_reverse">Обратить</string>
+ <string name="media_gallery_type">Тип</string>
+ <string name="media_gallery_type_squares">Квадраты</string>
+ <string name="media_gallery_type_tiled">Плитка</string>
+ <string name="media_gallery_type_circles">Круги</string>
+ <string name="media_gallery_type_slideshow">Показ слайдов</string>
+ <string name="media_edit_title_text">Название</string>
+ <string name="media_edit_caption_text">Заголовок</string>
+ <string name="media_edit_description_text">Описание</string>
+ <string name="media_edit_title_hint">Введите название</string>
+ <string name="media_edit_caption_hint">Введите заголовок</string>
+ <string name="media_edit_description_hint">Введите описание</string>
+ <string name="media_edit_success">Обновлено</string>
+ <string name="media_edit_failure">Ошибка обновления</string>
+ <string name="themes_details_label">Подробнее</string>
+ <string name="themes_features_label">Функции</string>
+ <string name="theme_activate_button">Активировать</string>
+ <string name="theme_activating_button">Активировать</string>
+ <string name="theme_set_success">Тема успешно установлена!</string>
+ <string name="theme_auth_error_title">Ошибка получения тем</string>
+ <string name="post_excerpt">Отрывок</string>
+ <string name="share_action_title">Добавить в ...</string>
+ <string name="share_action">Поделиться</string>
+ <string name="stats">Статистика</string>
+ <string name="stats_view_visitors_and_views">Посетители и Просмотры</string>
+ <string name="stats_view_clicks">Нажатия</string>
+ <string name="stats_view_tags_and_categories">Теги и Рубрики</string>
+ <string name="stats_view_referrers">Ссылающиеся</string>
+ <string name="stats_timeframe_today">Сегодня</string>
+ <string name="stats_timeframe_yesterday">Вчера</string>
+ <string name="stats_timeframe_days">Дни</string>
+ <string name="stats_timeframe_weeks">Недели</string>
+ <string name="stats_timeframe_months">Месяцы</string>
+ <string name="stats_entry_country">Страна</string>
+ <string name="stats_entry_posts_and_pages">Заголовок</string>
+ <string name="stats_entry_tags_and_categories">Запись</string>
+ <string name="stats_entry_authors">Автор</string>
+ <string name="stats_entry_referrers">Ссылающийся</string>
+ <string name="stats_totals_views">Просмотры</string>
+ <string name="stats_totals_clicks">Клики</string>
+ <string name="stats_totals_plays">Проиграно раз</string>
+ <string name="passcode_manage">Управление PIN</string>
+ <string name="passcode_enter_passcode">Введите ваш PIN</string>
+ <string name="passcode_enter_old_passcode">Введите ваш старый PIN</string>
+ <string name="passcode_re_enter_passcode">Введите ваш PIN заново</string>
+ <string name="passcode_change_passcode">Изменить PIN</string>
+ <string name="passcode_set">PIN установлен</string>
+ <string name="passcode_preference_title">Блокировка PIN</string>
+ <string name="passcode_turn_off">Выключить блокировку PIN</string>
+ <string name="passcode_turn_on">Включить блокировку PIN</string>
+ <string name="upload">Загрузка</string>
+ <string name="discard">Отменить</string>
+ <string name="sign_in">Войти</string>
+ <string name="notifications">Уведомления</string>
+ <string name="note_reply_successful">Ответ опубликован</string>
+ <string name="follows">Подписки</string>
+ <string name="new_notifications">Новые уведомления: %d</string>
+ <string name="more_notifications">и %d ещё.</string>
+ <string name="loading">Загрузка…</string>
+ <string name="httpuser">Имя для HTTP-авторизации</string>
+ <string name="httppassword">Пароль для HTTP-авторизации</string>
+ <string name="error_media_upload">Произошла ошибка при загрузке медиафайла</string>
+ <string name="post_content">Содержимое (нажмите, чтобы добавить текст и медиа)</string>
+ <string name="publish_date">Опубликовано</string>
+ <string name="content_description_add_media">Добавить медиа</string>
+ <string name="incorrect_credentials">Неправильное имя или пароль.</string>
+ <string name="password">Пароль</string>
+ <string name="username">Имя</string>
+ <string name="reader">Читать</string>
+ <string name="featured">Использовать как миниатюру</string>
+ <string name="featured_in_post">Включить изображение в текст записи</string>
+ <string name="no_network_title">Сеть недоступна</string>
+ <string name="pages">Страницы</string>
+ <string name="caption">Подпись (необязательно)</string>
+ <string name="width">Ширина</string>
+ <string name="posts">Записи</string>
+ <string name="anonymous">Аноним</string>
+ <string name="page">Страница</string>
+ <string name="post">Запись</string>
+ <string name="blogusername">имя пользователя</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Загружать и ссылаться на уменьшенное изображение</string>
+ <string name="scaled_image">Ширина уменьшенного изображения</string>
+ <string name="scheduled">Запланировано</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Загрузка</string>
+ <string name="version">Версия</string>
+ <string name="tos">Условия сервиса</string>
+ <string name="app_title">WordPress для Android</string>
+ <string name="max_thumbnail_px_width">Ширина изображения по умолчанию</string>
+ <string name="image_alignment">Выравнивание</string>
+ <string name="refresh">Обновить</string>
+ <string name="untitled">Без заголовка</string>
+ <string name="edit">Изменить</string>
+ <string name="post_id">Запись</string>
+ <string name="page_id">Страница</string>
+ <string name="post_password">Пароль (необязательно)</string>
+ <string name="immediately">Немедленно</string>
+ <string name="quickpress_add_alert_title">Установить короткое имя</string>
+ <string name="today">Сегодня</string>
+ <string name="settings">Настройки</string>
+ <string name="share_url">Поделиться ссылкой</string>
+ <string name="quickpress_window_title">Выберите блог для QuickPress</string>
+ <string name="quickpress_add_error">Ярлык не может быть пустым</string>
+ <string name="publish_post">Опубликовать</string>
+ <string name="draft">Черновик</string>
+ <string name="post_private">Личное</string>
+ <string name="upload_full_size_image">Загружать и ссылаться на полное изображение</string>
+ <string name="title">Заголовок</string>
+ <string name="tags_separate_with_commas">Метки (разделяйте запятыми)</string>
+ <string name="categories">Рубрики</string>
+ <string name="dlg_deleting_comments">Удаление комментариев</string>
+ <string name="notification_blink">Уведомление мигающей лампочкой</string>
+ <string name="notification_sound">Уведомление звуком</string>
+ <string name="notification_vibrate">Вибрация</string>
+ <string name="status">Состояние</string>
+ <string name="location">Местонахождение</string>
+ <string name="sdcard_title">Требуется SD-карта</string>
+ <string name="select_video">Выберите видео из галереи</string>
+ <string name="media">Медиа</string>
+ <string name="delete">Удалить</string>
+ <string name="none">Пусто</string>
+ <string name="blogs">Блоги</string>
+ <string name="select_photo">Выберите фото из галереи</string>
+ <string name="error">Ошибка</string>
+ <string name="cancel">Отмена</string>
+ <string name="save">Сохранить</string>
+ <string name="add">Добавить</string>
+ <string name="category_refresh_error">Ошибка обновления категорий</string>
+ <string name="preview">Предварительный просмотр</string>
+ <string name="on">на</string>
+ <string name="reply">Ответ</string>
+ <string name="notification_settings">Настройки уведомлений</string>
+ <string name="yes">Да</string>
+ <string name="no">Нет</string>
+</resources>
diff --git a/WordPress/src/main/res/values-sk/strings.xml b/WordPress/src/main/res/values-sk/strings.xml
new file mode 100644
index 000000000..955a2afb2
--- /dev/null
+++ b/WordPress/src/main/res/values-sk/strings.xml
@@ -0,0 +1,793 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="export_your_content">Exportujem Váš obsah...</string>
+ <string name="exporting_content_progress">Exportujem obsah...</string>
+ <string name="are_you_sure">Ste si istý?</string>
+ <string name="export_site_hint">Exportovať Vašu stránku ako XML súbor</string>
+ <string name="keep_your_content">Ponechať Váš obsah</string>
+ <string name="me_btn_app_settings">Nastavenia aplikácie</string>
+ <string name="comments_empty_list_filtered_trashed">Žiadne komentáre zahodené</string>
+ <string name="comments_empty_list_filtered_approved">Žiadne komentáre schválené</string>
+ <string name="button_skip">Preskočiť</string>
+ <string name="post_format_video">Video</string>
+ <string name="post_format_gallery">Galéria</string>
+ <string name="post_format_image">Obrázok</string>
+ <string name="post_format_link">Odkaz</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_community">Komunita</string>
+ <string name="notif_research">Výskum</string>
+ <string name="site_settings_list_editor_summary_one">1 položka</string>
+ <string name="days_quantity_one">1 deň</string>
+ <string name="filter_trashed_posts">Zahodené</string>
+ <string name="filter_scheduled_posts">Naplánované</string>
+ <string name="filter_draft_posts">Koncepty</string>
+ <string name="web_address">Webová adresa</string>
+ <string name="editor_post_settings_set_featured_image">Nastaviť ilustračný obrázok</string>
+ <string name="editor_post_settings_featured_image">Ilustračný obrázok</string>
+ <string name="error_post_account_settings">Nepodarilo sa uložiť nastavenia vášho účtu</string>
+ <string name="error_post_my_profile">Nepodarilo sa uložiť váš profil</string>
+ <string name="error_fetch_account_settings">Nepodarilo sa načítať nastavenia vášho účtu</string>
+ <string name="error_fetch_my_profile">Nepodarilo sa načítať váš profil</string>
+ <string name="site_settings_unknown_language_code_error">Kód jazyka nebol rozpoznaný</string>
+ <string name="site_settings_threading_dialog_description">Povoliť vnorenie komentárov do vlákien.</string>
+ <string name="site_settings_threading_dialog_header">Vlákna do úrovne</string>
+ <string name="remove">Odstrániť</string>
+ <string name="search">Hľadať</string>
+ <string name="add_category">Pridať kategóriu</string>
+ <string name="site_settings_image_original_size">Pôvodná veľkosť</string>
+ <string name="about_me_hint">Pár slov o vás...</string>
+ <string name="first_name">Meno</string>
+ <string name="last_name">Priezvisko</string>
+ <string name="site_privacy_public_desc">Povoliť indexovanie vašej stránky vyhľadávačmi</string>
+ <string name="site_privacy_hidden_desc">Vypnúť indexovanie vašej stránky vyhľadávačmi</string>
+ <string name="blog_name_no_spaced_allowed">Adresa webovej stránky nemôže obsahovať medzery</string>
+ <string name="invalid_username_no_spaces">Používateľské meno nemôže obsahovať medzery</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Webové stránky, ktoré sledujete v poslednej dobe nezverejnili nič.</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Žiadne nové články</string>
+ <string name="media_details_copy_url_toast">URL adresa skopírovaná do schránky</string>
+ <string name="edit_media">Upraviť súbory</string>
+ <string name="media_details_copy_url">Kopírovať URL adresu</string>
+ <string name="media_details_label_date_uploaded">Nahraté</string>
+ <string name="media_details_label_date_added">Pridané</string>
+ <string name="selected_theme">Zvolená téma</string>
+ <string name="could_not_load_theme">Nie je možné načítať tému</string>
+ <string name="theme_activation_error">Niečo nezafungovalo. Nie je možné aktivovať tému.</string>
+ <string name="theme_by_author_prompt_append"> od %1$s</string>
+ <string name="theme_prompt">Ďakujeme, že ste si vybrali %1$s</string>
+ <string name="theme_try_and_customize">Vyskúšajte a prispôsobte si</string>
+ <string name="theme_view">Zobraziť</string>
+ <string name="theme_details">Detaily</string>
+ <string name="theme_support">Podpora</string>
+ <string name="theme_done">HOTOVO</string>
+ <string name="theme_manage_site">SPRAVOVAŤ STRÁNKU</string>
+ <string name="title_activity_theme_support">Témy</string>
+ <string name="theme_activate">Aktivovať</string>
+ <string name="date_range_start_date">Začiatok</string>
+ <string name="date_range_end_date">Koniec</string>
+ <string name="current_theme">Aktuálna téma</string>
+ <string name="customize">Prispôsobiť</string>
+ <string name="details">Detaily</string>
+ <string name="support">Podpora</string>
+ <string name="active">Aktívna</string>
+ <string name="stats_referrers_spam_generic_error">Niečo sa nepodarilo. Označenie za spam nebolo zmenené.</string>
+ <string name="stats_referrers_marking_not_spam">Odznačuje sa ako spam</string>
+ <string name="stats_referrers_unspam">Nie je spam</string>
+ <string name="stats_referrers_marking_spam">Označuje sa ako spam</string>
+ <string name="theme_auth_error_authenticate">Nepodarilo sa načítať témy: nepodarilo sa autentifikovať používateľa</string>
+ <string name="post_published">Článok bol publikovaný</string>
+ <string name="page_published">Stránka bola publikovaná</string>
+ <string name="post_updated">Článok bol aktualizovaný</string>
+ <string name="page_updated">Stránka bola aktualizovaná</string>
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="theme_no_search_result_found">Neboli nájdené žiadne témy.</string>
+ <string name="media_file_name">Názov súboru: %s</string>
+ <string name="media_uploaded_on">Nahrané: %s</string>
+ <string name="media_dimensions">Rozmery: %s</string>
+ <string name="upload_queued">Zaradené do fronty</string>
+ <string name="media_file_type">Typ súboru: %s</string>
+ <string name="reader_label_gap_marker">Načítať ďalšie články</string>
+ <string name="notifications_no_search_results">Žiadne stránky sa nezhodujú s \'%s\'</string>
+ <string name="search_sites">Nájsť webové stránky</string>
+ <string name="unread">Neprečítané</string>
+ <string name="notifications_empty_view_reader">Zobraziť čítačku</string>
+ <string name="notifications_empty_action_followers_likes">Všimnite si: komentujte príspevky, ktoré ste prečítali.</string>
+ <string name="notifications_empty_action_comments">Pridajte sa ku konverzácii: komentujte príspevky z blogov, ktoré sledujete.</string>
+ <string name="notifications_empty_action_unread">Začnite konverzáciu: napíšte nový príspevok.</string>
+ <string name="notifications_empty_action_all">Tvorte! Komentujte príspevky z blogov, ktoré sledujete.</string>
+ <string name="notifications_empty_likes">Nemáte žiadne nové lajky... zatiaľ.</string>
+ <string name="notifications_empty_followers">Žiadny nový sledujúci... zatiaľ.</string>
+ <string name="notifications_empty_comments">Žiadne nové komentáre... zatiaľ.</string>
+ <string name="notifications_empty_unread">Všetky sú prečítané!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Prosím získajte štatistiku v aplikácii a pokúste sa pridať widgety neskôr</string>
+ <string name="stats_widget_error_readd_widget">Prosím odstráňte widget a neskôr ho znovu pridajte</string>
+ <string name="stats_widget_error_no_visible_blog">Žiaden prístup k štatistike bez viditeľného blogu</string>
+ <string name="stats_widget_error_no_permissions">Váš účet na WordPress.com nemá prístup k štatistikám na tomto blogu</string>
+ <string name="stats_widget_error_no_account">Prosím prihláste sa do WordPress</string>
+ <string name="stats_widget_error_generic">Nemožno načítať štatistku</string>
+ <string name="stats_widget_loading_data">Načítavanie údajov...</string>
+ <string name="stats_widget_name_for_blog">Dnešná štatistika pre %1$s</string>
+ <string name="stats_widget_name">Dnešná štatistika WordPress</string>
+ <string name="add_location_permission_required">Vyžaduje sa súhlas k pridaniu polohy</string>
+ <string name="add_media_permission_required">Vyžadujú sa súhlasy k pridaniu súborov</string>
+ <string name="access_media_permission_required">Vyžadujú sa súhlasy na prístup k súborom</string>
+ <string name="stats_enable_rest_api_in_jetpack">Povoľte modul JSON API v Jetpack k náhľadu vašej štatistiky.</string>
+ <string name="error_open_list_from_notification">Tento článok alebo stránka boli zverejnené na inej webovej stránke</string>
+ <string name="reader_short_comment_count_multi">%s komentárov</string>
+ <string name="reader_short_comment_count_one">1 komentár</string>
+ <string name="reader_label_submit_comment">ODOSLAŤ</string>
+ <string name="reader_hint_comment_on_post">Pridať komentár k článku...</string>
+ <string name="reader_discover_visit_blog">Návštevnosť %s</string>
+ <string name="reader_discover_attribution_blog">Pôvodne publikované v %s</string>
+ <string name="reader_discover_attribution_author">Pôvodne publikované autorom %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Pôvodne publikované autorom %1$s v %2$s</string>
+ <string name="reader_short_like_count_multi">počet lajkov: %s</string>
+ <string name="reader_label_follow_count">počet sledujúcich: %,d</string>
+ <string name="reader_short_like_count_one">1 lajk</string>
+ <string name="reader_short_like_count_none">lajk</string>
+ <string name="reader_menu_tags">Upraviť značky a blogy</string>
+ <string name="reader_title_post_detail">Článok čítačky</string>
+ <string name="local_draft_explainer">Tento článok je koncept, ktorý nebol zverejnený</string>
+ <string name="local_changes_explainer">Tento má úpravy, ktoré neboli zverejnené</string>
+ <string name="notifications_push_summary">Nastavenia upozornení zobrazujúcich sa na vašom zariadení.</string>
+ <string name="notifications_email_summary">Nastavenia upozornení odosielaných na e-mail, ktorý je spojený s vašim účtom.</string>
+ <string name="notifications_tab_summary">Nastavenia upozornení zobrazujúcich sa v záložke Upozornenia.</string>
+ <string name="notifications_disabled">Upozornenia aplikácie nie sú dostupné. Poklepte prstami na tomto mieste, aby sa zobrazili v Nastaveniach.</string>
+ <string name="notification_types">Typy upozornení</string>
+ <string name="error_loading_notifications">Nenačítali sa nastavenia upozornení</string>
+ <string name="replies_to_your_comments">Odpovede na vaše komentáre</string>
+ <string name="comment_likes">Lajky komentárov</string>
+ <string name="email">E-mail</string>
+ <string name="app_notifications">Upozornenia aplikácie</string>
+ <string name="notifications_tab">Záložka Upozornenia</string>
+ <string name="notifications_comments_other_blogs">Komentáre na ďalších webových stránkach</string>
+ <string name="notifications_wpcom_updates">Aktualizácie WordPress.com</string>
+ <string name="notifications_other">Iné</string>
+ <string name="notifications_account_emails">E-mail od WordPress.com</string>
+ <string name="notifications_account_emails_summary">Budeme vám posielať dôležité e-maily o vašom účte, ale rovnako môžete dostať aj iné, užitočné správy.</string>
+ <string name="your_sites">Vaše webové stránky</string>
+ <string name="notifications_sights_and_sounds">Sights and Sounds</string>
+ <string name="stats_insights_latest_post_trend">Prešlo %1$s od zverejnenia %2$s. Môžete vidieť ako doteraz príspevok obstál...</string>
+ <string name="stats_insights_latest_post_summary">Súhrn najnovších príspevkov</string>
+ <string name="button_revert">Vrátiť</string>
+ <string name="yesterday">Včera</string>
+ <string name="days_ago">pred %d dňami</string>
+ <string name="connectionbar_no_connection">Žiadne pripojenie</string>
+ <string name="button_trash">Kôš</string>
+ <string name="button_edit">Upraviť</string>
+ <string name="button_stats">Štatistiky</string>
+ <string name="button_preview">Náhľad</string>
+ <string name="button_view">Zobraziť</string>
+ <string name="page_deleted">Stránka vymazaná</string>
+ <string name="button_back">Späť</string>
+ <string name="trashed">Odstránené</string>
+ <string name="page_trashed">Stránka presunutá do koša</string>
+ <string name="stats_no_activity_this_period">Žiadna činnosť v toto obdobie</string>
+ <string name="button_publish">Zverejniť</string>
+ <string name="post_deleted">Článok vymazaný</string>
+ <string name="post_trashed">Článok presunutý do koša</string>
+ <string name="my_site_no_sites_view_subtitle">Prajete si pridať ďalšie?</string>
+ <string name="my_site_no_sites_view_title">Nemáte žiadne webové stránky WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustrácie</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Nemáte prístup do tohto blogu</string>
+ <string name="reader_toast_err_follow_blog_not_found">Blog sa nenašiel</string>
+ <string name="undo">Zrušiť zmenu</string>
+ <string name="tabbar_accessibility_label_my_site">Moja webová stránka</string>
+ <string name="tabbar_accessibility_label_me">Ja</string>
+ <string name="passcodelock_prompt_message">Zadajte svoj PIN</string>
+ <string name="editor_toast_changes_saved">Zmeny uložené</string>
+ <string name="push_auth_expired">Žiadosť vypršala. Prihláste sa do WordPress.com a skúste znovu.</string>
+ <string name="stats_insights_best_ever">Najlepšie zobrazenia</string>
+ <string name="ignore">Ignorovať</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% zobrazení</string>
+ <string name="stats_insights_most_popular_hour">Najpopulárnejšia hodina</string>
+ <string name="stats_insights_most_popular_day">Najobľúbenejší deň</string>
+ <string name="stats_insights_popular">Najobľúbenejší deň a hodina</string>
+ <string name="stats_insights_today">Dnešné štatistiky</string>
+ <string name="stats_insights_all_time">Články, zobrazenia a návštevníci počas celej doby</string>
+ <string name="stats_insights">Prehľady</string>
+ <string name="stats_sign_in_jetpack_different_com_account">K zobrazeniu vašej štatistiky sa prihláste s WordPress.com účtom, s ktorým ste sa pripojili k Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Hľadáte ďalšie nedávne štatistiky? Presunuli sme ich na stránku Prehľady.</string>
+ <string name="me_disconnect_from_wordpress_com">Odpojiť z WordPress.com</string>
+ <string name="me_connect_to_wordpress_com">Pripojiť sa k WordPress.com</string>
+ <string name="me_btn_login_logout">Prihlásiť/Odhlásiť</string>
+ <string name="me_btn_support">Pomoc a podpora</string>
+ <string name="site_picker_cant_hide_current_site">"%s" nebola skrytá, pretože to je aktuálna webová stránka</string>
+ <string name="site_picker_create_dotcom">Vytvoriť webovú stránku WordPress.com</string>
+ <string name="site_picker_add_site">Pridať webovú stránku</string>
+ <string name="site_picker_edit_visibility">Zobraziť/skryť webové stránky</string>
+ <string name="site_picker_add_self_hosted">Pridať webovú stránku s vlastným hostingom</string>
+ <string name="my_site_btn_view_site">Zobraziť webovú stránku</string>
+ <string name="site_picker_title">Vybrať webovú stránku</string>
+ <string name="my_site_btn_switch_site">Prepnúť webovú stránku</string>
+ <string name="my_site_btn_view_admin">Zobraziť administráciu</string>
+ <string name="my_site_btn_site_settings">Nastavenia</string>
+ <string name="my_site_header_publish">Zverejniť</string>
+ <string name="my_site_btn_blog_posts">Články blogu</string>
+ <string name="my_site_header_look_and_feel">Vzhľad</string>
+ <string name="my_site_header_configuration">Kanfigurácia</string>
+ <string name="reader_label_new_posts_subtitle">Ťuknite k ich zobrazeniu</string>
+ <string name="notifications_account_required">Prihláste sa do WordPress.com pre upozornenia</string>
+ <string name="stats_unknown_author">Neznámy autor</string>
+ <string name="image_added">Obrázok pridaný</string>
+ <string name="signout">Odpojiť sa</string>
+ <string name="show">Zobraziť</string>
+ <string name="hide">Skryť</string>
+ <string name="deselect_all">Odznačiť všetky</string>
+ <string name="select_all">Vybrať všetky</string>
+ <string name="sign_out_wpcom_confirm">Po odpojení sa vám z účtu vymažú všetky @%s’s WordPress.com údaje z tohto zariadenia, vrátane lokálnych konceptov a zmien.</string>
+ <string name="select_from_new_picker">Viacnásobný výber v novej možnosti výberu</string>
+ <string name="stats_generic_error">Požadované štatistiky nemohli byť načítané</string>
+ <string name="loading_blog_images">Načítavajú sa obrázky</string>
+ <string name="no_media_sources">Nie je možné načítať súbory</string>
+ <string name="loading_blog_videos">Načítavajú sa videá</string>
+ <string name="error_loading_blog_videos">Nie je možné načítať videá</string>
+ <string name="error_loading_videos">Nie je možné načítať videá</string>
+ <string name="error_loading_blog_images">Nie je možné načítať obrázky</string>
+ <string name="error_loading_images">Pri načítavaní videa sa vyskytla chyba</string>
+ <string name="no_device_videos">Žiadne videá</string>
+ <string name="no_blog_videos">Žiadne videá</string>
+ <string name="no_blog_images">Žiadne obrázky</string>
+ <string name="no_device_images">Žiadne obrázky</string>
+ <string name="no_media">Žiadne súbory</string>
+ <string name="loading_images">Nahrávajú sa obrázky</string>
+ <string name="loading_videos">Nahrávajú sa videá</string>
+ <string name="media_picker_title">Vybrať súbory</string>
+ <string name="sign_in_jetpack">Prihláste sa do účtu WordPress.com a pripojte sa na Jetpack.</string>
+ <string name="two_step_sms_sent">Vložte overovací kód z SMS správy.</string>
+ <string name="take_photo">Spraviť fotku</string>
+ <string name="error_publish_no_network">Nedá sa publikovať, nefunguje pripojenie. Uložené ako koncept.</string>
+ <string name="two_step_footer_label">Vložte kód z aplikácie Authenticator.</string>
+ <string name="tab_title_site_images">Obrázky na stránke</string>
+ <string name="take_video">Nahrať video</string>
+ <string name="verify">Overiť</string>
+ <string name="media_details_label_file_type">Typ súboru</string>
+ <string name="auth_required">Pre pokračovanie sa opäť prihlláste.</string>
+ <string name="two_step_footer_button">Odoslanie kódu textovou správou.</string>
+ <string name="verification_code">Overovací kód</string>
+ <string name="invalid_verification_code">Nesprávny overovací kód</string>
+ <string name="editor_toast_invalid_path">Nesprávna cesta súboru</string>
+ <string name="tab_title_site_videos">Videá na stánke</string>
+ <string name="add_to_post">Pridať k príspevku</string>
+ <string name="tab_title_device_images">Obrázky v zariadení</string>
+ <string name="tab_title_device_videos">Videá v zariadení</string>
+ <string name="language">Jazyk</string>
+ <string name="device">Zariadenie</string>
+ <string name="media_details_label_file_name">Názov súboru</string>
+ <string name="stats_followers_total_wpcom_paged">WordPress.com odberatelia %1$d - %2$d z %3$s</string>
+ <string name="stats_followers_total_email_paged">Zobrazuje sa %1$d - %2$d z %3$s odberateľov</string>
+ <string name="media_fetching">Načítavajú sa súbory...</string>
+ <string name="comments_fetching">Načítavajú sa komentáre...</string>
+ <string name="posts_fetching">Načítavajú sa príspevky...</string>
+ <string name="pages_fetching">Načítavajú sa stránky...</string>
+ <string name="publisher">Vydavateľ:</string>
+ <string name="stats_empty_search_terms_desc">Zistite, aké kľúčové slová zvyšujú návštevnosť stránky.</string>
+ <string name="stats_empty_search_terms">Slovo nenájdené</string>
+ <string name="reader_empty_posts_request_failed">Nie je možné obnoviť príspevky</string>
+ <string name="toast_err_post_uploading">Nie je možné otvoriť príspevky počas odovzdávania</string>
+ <string name="stats_search_terms_unknown_search_terms">Neznáme hľadané výrazy</string>
+ <string name="stats_view_search_terms">Hľadané výrazy</string>
+ <string name="stats_view_authors">Autori</string>
+ <string name="stats_entry_search_terms">Hľadaný výraz</string>
+ <string name="error_notification_open">Nie je možné otvoríť oznámenie</string>
+ <string name="logs_copied_to_clipboard">Záznamy aplikácie boli skopírované do schránky</string>
+ <string name="error_copy_to_clipboard">Pri kopírovaní textu do schránky nastala chyba </string>
+ <string name="post_uploading">Nahráva sa</string>
+ <string name="stats_recent_weeks">Posledné týždne</string>
+ <string name="stats_months_and_years">Mesiace a roky</string>
+ <string name="reader_empty_posts_in_blog">Tento blog je prázdny</string>
+ <string name="stats_average_per_day">Priemer za deň</string>
+ <string name="stats_overall">Celkom</string>
+ <string name="reader_label_new_posts">Nové príspevky</string>
+ <string name="stats_period">Obdobie</string>
+ <string name="stats_total">Spolu</string>
+ <string name="reader_page_recommended_blogs">Webové stránky, ktoré sa vám možno páčia</string>
+ <string name="stats_empty_top_authors_desc">Sledujte zobrazenia jednotlivých príspevkov autorov a zistite, ktorý obsah je od jednotlivých autorov populárny.</string>
+ <string name="stats_empty_comments_desc">Ak povolíte komentáre na stránke, môžete sledovať najaktívnejších komentátorov a na základe posledných 1000 komentárov zistiť, aký obsah podnecuje živú diskusiu.</string>
+ <string name="stats_empty_referrers_desc">Zistite viac o viditeľnosti stránky prostredníctvom webových stránok a vyhľadávačov, ktoré zvyšujú jej návštevnosť</string>
+ <string name="stats_followers_minutes">minút: %1$d</string>
+ <string name="stats_empty_video">Žiadne prehraté videá</string>
+ <string name="stats_pagination_label">Strana %1$s z %2$s</string>
+ <string name="stats_empty_publicize_desc">Sledujte celkový počet odberateľov prichádzajúcich zo sociálych sietí prostredníctvom propagácie.</string>
+ <string name="stats_empty_followers_desc">Sledujte celkový počet odberateľov a dobu odberu vašej stránky.</string>
+ <string name="stats_comments_by_posts_and_pages">Podľa príspevkov a stránok </string>
+ <string name="stats_empty_publicize">Žiadni známi odberatelia</string>
+ <string name="stats_empty_video_desc">Ak ste nahrali videá pomocou VideoPress, zistite, koľkokrát boli videá zobrazené.</string>
+ <string name="stats_empty_clicks_desc">Ak váš obsah obsahuje odkazy na iné stránky, pozrite sa, na ktoré návštevníci najviac klikajú.</string>
+ <string name="stats_view_publicize">Propagovať</string>
+ <string name="stats_likes">Počet lajkov</string>
+ <string name="stats_empty_geoviews_desc">Pozrite si zoznam krajín, ktoré produkujú najvyššiu návštevnosť vašej stránky.</string>
+ <string name="stats_empty_top_posts_desc">Zistite, čo je váš najúspešnejší obsah a skontrolujte, ako sa darí jednotlivým príspevkom a stránkam v priebehu času.</string>
+ <string name="stats_empty_referrers_title">Žiadne odkazy</string>
+ <string name="stats_empty_followers">Žiadny odberatelia</string>
+ <string name="stats_empty_tags_and_categories">Žiadne označené príspevky alebo zobrazené stránky</string>
+ <string name="stats_empty_tags_and_categories_desc">Získajte prehľad najpopulárnejších tém na stránke podľa príspevkov z uplynulého týždňa.</string>
+ <string name="stats_followers_total_wpcom">Počet odberateľov WordPress.com: %1$s</string>
+ <string name="stats_followers_total_email">Počet odberateľov emailov: %1$s</string>
+ <string name="stats_totals_publicize">Odberatelia</string>
+ <string name="stats_entry_followers">Odberateľ</string>
+ <string name="stats_view_followers">Odberatelia</string>
+ <string name="stats_other_recent_stats_label">Ostatné štatistiky</string>
+ <string name="stats_followers_years">počet rokov: %1$d</string>
+ <string name="stats_followers_months">počet mesiacov: %1$d</string>
+ <string name="stats_followers_hours">hodín: %1$d</string>
+ <string name="stats_view">Zobraziť</string>
+ <string name="stats_followers_a_year">Rok</string>
+ <string name="stats_visitors">Návštevníci</string>
+ <string name="stats_views">Zobrazenia</string>
+ <string name="stats_timeframe_years">Roky</string>
+ <string name="stats_view_top_posts_and_pages">Príspevky a stránky</string>
+ <string name="stats_view_countries">Krajiny</string>
+ <string name="stats_view_videos">Videá</string>
+ <string name="stats_entry_clicks_link">Odkaz</string>
+ <string name="stats_comments_by_authors">Od autorov</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_seconds_ago">Pred niekoľkými sekundami</string>
+ <string name="stats_followers_a_day">Deň</string>
+ <string name="stats_followers_days">%1$d dní</string>
+ <string name="stats_followers_a_minute_ago">Pred minútou</string>
+ <string name="stats_entry_publicize">Služba</string>
+ <string name="stats_followers_a_month">Mesiac</string>
+ <string name="stats_followers_an_hour_ago">Pred hodinou</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_empty_geoviews">Nie sú zaznamenané žiadne krajiny</string>
+ <string name="themes_fetching">Načítavajú sa témy</string>
+ <string name="stats_empty_top_posts_title">Neboli žiadne návštevy príspevkov alebo stránok</string>
+ <string name="stats_view_all">Zobraziť všetko</string>
+ <string name="stats_for">Štatistiky pre %s</string>
+ <string name="stats_totals_followers">Od</string>
+ <string name="stats_empty_clicks_title">Nie sú zaznamenané žiadne kliknutia</string>
+ <string name="stats_comments_total_comments_followers">Celkový počet článkov s odberateľmi: %1$s</string>
+ <string name="ssl_certificate_details">Detaily</string>
+ <string name="media_gallery_date_range">Zobrazuje médiá od %1$s do %2$s</string>
+ <string name="delete_sure_page">Zmazať túto podstránku</string>
+ <string name="delete_sure">Vymazať tento koncept</string>
+ <string name="delete_sure_post">Vymazať tento príspevok</string>
+ <string name="cab_selected">%d vybraných</string>
+ <string name="confirm_delete_media">Zmazať vybranú položku?</string>
+ <string name="confirm_delete_multi_media">Zmazať vybrané položky?</string>
+ <string name="sure_to_remove_account">Odstrániť webovú stránku?</string>
+ <string name="posts_empty_list">Žiadne príspevky. Želáte si nejaký vytvoriť?</string>
+ <string name="reader_label_comment_count_multi">odpovedí: %,d</string>
+ <string name="pages_empty_list">Zatiaľ nemáte žiadnu stránku. Želáte si nejakú vytvoriť?</string>
+ <string name="nux_help_description">Pre odpovede na časté otázky navštívte naše centrum pomoci alebo fórum</string>
+ <string name="error_refresh_unauthorized_pages">Nemáte oprávnenie prezerať alebo upravovať stránky</string>
+ <string name="error_refresh_unauthorized_comments">Nemáte oprávnenie prezerať alebo upravovať komentáre</string>
+ <string name="reader_label_comments_on">Komentáre k</string>
+ <string name="comment">Komentár</string>
+ <string name="sending_content">Nahrávanie obsahu %s</string>
+ <string name="media_empty_list_custom_date">Žiadne súbory v tomto časovom intervale</string>
+ <string name="posting_post">Publikuje sa "%s"</string>
+ <string name="agree_terms_of_service">Založením účtu súhlasíte s našimi okúzľujúcimi %1$sPodmienkami používania%2$s</string>
+ <string name="comment_reply_to_user">Odpovedať užívateľovi %s</string>
+ <string name="reader_empty_posts_in_tag">Neexistuje žiadny príspevok s touto značkou</string>
+ <string name="error_publish_empty_post">Nie je možné zverejniť prázdny príspevok</string>
+ <string name="uploading_total">Nahrávanie %1$d z %2$d</string>
+ <string name="stats_no_blog">Nebolo možné načítať štatistiky pre vybraný blog</string>
+ <string name="signing_out">Odhlasuje sa...</string>
+ <string name="comment_trashed">Komentár bol odstránený</string>
+ <string name="reader_label_view_original">Pozrieť originál článku</string>
+ <string name="error_refresh_unauthorized_posts">Nemáte oprávnenie prezerať alebo upravovať príspevky</string>
+ <string name="reader_title_photo_viewer">%1$d z %2$d</string>
+ <string name="reader_label_comments_closed">Už nie je možné pridávať komentáre</string>
+ <string name="reader_label_comment_count_single">jeden komentár</string>
+ <string name="reader_empty_comments">Zatiaľ nie sú žiadne komentáre</string>
+ <string name="select_a_blog">Vybrať WordPress stránku</string>
+ <string name="browse_our_faq_button">Prehľadávať časté otázky</string>
+ <string name="create_new_blog_wpcom">Vytvoriť WordPress blog</string>
+ <string name="faq_button">Časté otázky</string>
+ <string name="older_month">Staršie ako mesiac</string>
+ <string name="older_last_week">Staršie ako týždeň</string>
+ <string name="older_two_days">Staršie ako 2 dni</string>
+ <string name="more">Viac</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog bol vytvorený!</string>
+ <string name="reader_empty_followed_blogs_title">Nesledujete zatiaľ žiadne webové stránky</string>
+ <string name="reader_label_like">Lajk</string>
+ <string name="mnu_comment_liked">Lajknuté</string>
+ <string name="reader_empty_posts_liked">Zatiaľ ste nelajkli žiaden príspevok</string>
+ <string name="reader_toast_blog_blocked">Príspevky z tomhto blogu sa už nebudú zobrazovať</string>
+ <string name="reader_toast_err_generic">Nie je možné vykonať túto akciu</string>
+ <string name="reader_toast_err_block_blog">Tento blog nie je možné zablokovať</string>
+ <string name="reader_menu_block_blog">Zablokovať tento blog</string>
+ <string name="hs__conversation_header">Chat technickej podpory</string>
+ <string name="hs__new_conversation_header">Chat technickej podpory</string>
+ <string name="hs__username_blank_error">Vložte platné meno</string>
+ <string name="hs__conversation_detail_error">Popíšte váš problém</string>
+ <string name="hs__invalid_email_error">Vložte platnú emailovú adresu</string>
+ <string name="contact_us">Kontaktujte nás</string>
+ <string name="add_location">Pridať lokalitu</string>
+ <string name="current_location">Aktuálna pozícia</string>
+ <string name="search_current_location">Vyhľadať aktuálnu lokalitu</string>
+ <string name="search_location">Hľadať</string>
+ <string name="edit_location">Upraviť</string>
+ <string name="preference_send_usage_stats_summary">Automaticky odosielať štatistiky užívania pre zlepšenie WordPress pre Android</string>
+ <string name="preference_send_usage_stats">Poslať štatistiky</string>
+ <string name="schedule_verb">Rozvrh</string>
+ <string name="update_verb">Aktualizovať</string>
+ <string name="reader_toast_err_unfollow_blog">Nie je možné sledovať tento blog</string>
+ <string name="reader_label_tag_preview">Príspevok označený: %s</string>
+ <string name="reader_label_followed_blog">Sledovaný blog</string>
+ <string name="reader_page_followed_tags">Sledované značky</string>
+ <string name="reader_toast_err_already_follow_blog">Tento blog už sledujete</string>
+ <string name="reader_toast_err_get_blog_info">Tento blog nie je možné zobraziť</string>
+ <string name="reader_toast_err_follow_blog">Tento blog nie je možné sledovať</string>
+ <string name="reader_title_subs">Značky a blogy</string>
+ <string name="reader_empty_recommended_blogs">Žiadne odporúčané blogy</string>
+ <string name="reader_page_followed_blogs">Sledované webové stránky</string>
+ <string name="reader_title_tag_preview">Značka čítačky</string>
+ <string name="reader_title_blog_preview">Blog čítačky</string>
+ <string name="reader_hint_add_tag_or_url">Zadajte adresu URL alebo značku, ktorú chcete sledovať</string>
+ <string name="media_empty_list">Žiadne súbory</string>
+ <string name="ptr_tip_message">Potiahnite smerom nadol pre obnovu</string>
+ <string name="saving">Ukladá sa ...</string>
+ <string name="ssl_certificate_ask_trust">Ak sa bežne pripájate bez problémov, táto chyba môže znamenať, že sa niekto pokúša prevziať vašu identitu. Neodporúčame vám pokračovať. Chcete aj napriek tomu dôverovať tomuto certifikátu?</string>
+ <string name="ssl_certificate_error">Neplatný SSL certifikát</string>
+ <string name="forgot_password">Stratili ste heslo?</string>
+ <string name="help_center">Centrum Pomocníka</string>
+ <string name="forums">Fóra</string>
+ <string name="help">Pomocník</string>
+ <string name="theme_auth_error_message">Uistite sa, či máte prava nastaviť tému</string>
+ <string name="blog_name_reserved_but_may_be_available">Stránka je momentálne rezervovaná, no možno sa o pár dní uvoľní</string>
+ <string name="gallery_error">Súbor nebolo možné načítať</string>
+ <string name="blog_name_cant_be_used">Nemôžete použiť túto adresu stránky</string>
+ <string name="username_reserved_but_may_be_available">Toto používateľské meno je momentálne rezervované, no o niekoľko dní môže byť voľné</string>
+ <string name="username_or_password_incorrect">Používateľské meno alebo heslo nie je správne</string>
+ <string name="blog_name_contains_invalid_characters">Adresa stránky nemôže obsahovať znak “_”</string>
+ <string name="username_not_allowed">Používateľské meno nie je povolené</string>
+ <string name="blog_name_exists">Stránka už existuje</string>
+ <string name="nux_cannot_log_in">Prihlásenie nie je možné</string>
+ <string name="error_delete_post">Počas mazania %s nastala chyba</string>
+ <string name="blog_name_reserved">Táto adresa je obsadená</string>
+ <string name="out_of_memory">Nedostatok voľného miesta v pamäti</string>
+ <string name="blog_not_found">Počas prístupu k blogu sa vyskytla chyba</string>
+ <string name="error_refresh_notifications">Notifikácie nebolo možné znova načítať</string>
+ <string name="blog_name_not_allowed">Táto adresa stránky nie je povolená</string>
+ <string name="error_upload">Počas nahrávania %s nastala chyba</string>
+ <string name="error_moderate_comment">Počas moderovania nastala chyba</string>
+ <string name="error_refresh_stats">Štatistiky nebolo možné znova načítať</string>
+ <string name="error_refresh_comments">Komentáre nebolo možné znova načítať</string>
+ <string name="error_refresh_pages">Stránky nebolo možné znova načítať</string>
+ <string name="error_refresh_posts">Príspevky nebolo možné znova načítať</string>
+ <string name="notifications_empty_list">Žiadne notifikácie</string>
+ <string name="reply_failed">Odpveď nebola úspešná</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Adresa stránky musí mať menej ako 64 znakov</string>
+ <string name="blog_name_must_be_at_least_four_characters">Adresa stránky musí mať aspoň 4 znaky</string>
+ <string name="email_exists">Táto emailová adresa sa už používa</string>
+ <string name="username_exists">Toto používateľské meno už existuje</string>
+ <string name="email_not_allowed">Táto emailová adresa nie je povolená</string>
+ <string name="username_contains_invalid_characters">Používateľské meno nesmie obsahovať znak "_"</string>
+ <string name="username_only_lowercase_letters_and_numbers">Používateľské meno môže obsahovať iba malé písmená (a-z) a číslice</string>
+ <string name="username_required">Zadajte používateľské meno</string>
+ <string name="invalid_username_too_short">Používateľské meno musí byť dlhšie ako 4 znaky</string>
+ <string name="invalid_username_too_long">Používateľské meno musí byť kratšie ako 61 znakov</string>
+ <string name="invalid_password_message">Heslo musí obsahovať minimálne 4 znaky</string>
+ <string name="passcode_wrong_passcode">Nesprávny PIN</string>
+ <string name="invalid_email_message">Zadaná emailová adresa nie je platná</string>
+ <string name="error_edit_comment">Počas úpravy komentára sa vyskytla chyba</string>
+ <string name="error_load_comment">Nepodarilo sa načítať komentár</string>
+ <string name="error_generic">Vyskytla sa chyba</string>
+ <string name="category_automatically_renamed">Názov kategórie %1$s je neplatný. Bola premenovaná na %2$s.</string>
+ <string name="cat_name_required">Vložte názov kategórie</string>
+ <string name="adding_cat_success">Kategória bola úspešne pridaná</string>
+ <string name="adding_cat_failed">Chyba pri pridávaní kategórie</string>
+ <string name="no_site_error">Nie je možné pripojiť sa na WordPress stránku</string>
+ <string name="username_must_be_at_least_four_characters">Používateľské meno musí mať aspoň 4 znaky</string>
+ <string name="comments_empty_list">Žiadne komentáre</string>
+ <string name="mnu_comment_unspam">Žiadny spam</string>
+ <string name="theme_set_failed">Nepodarilo sa nastaviť tému</string>
+ <string name="wait_until_upload_completes">Čakajte, kým bude nahrávanie dokončené</string>
+ <string name="no_network_message">Nie sú k dispozícii žiadne siete</string>
+ <string name="blog_name_required">Vložte adresu stránky</string>
+ <string name="username_must_include_letters">Používatelské meno musí obsahovať min. 1 písmeno (a-z)</string>
+ <string name="email_invalid">Vložte platnú emailovú adresu</string>
+ <string name="stats_empty_comments">Bez komentárov</string>
+ <string name="stats_bar_graph_empty">Štatistiky nie sú dostupné</string>
+ <string name="sdcard_message">Pre nahratie súboru je potrebné vložiť SD kartu</string>
+ <string name="no_account">Účet WordPress nenájdený, vytvorte nový účet</string>
+ <string name="error_downloading_image">Chyba pri sťahovaní obrázka</string>
+ <string name="theme_fetch_failed">Chyba pri načítavaní témy</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Adresa stránky môže obsahovať iba malé písmená (a-z) alebo čísla</string>
+ <string name="could_not_remove_account">Webová stránka sa nedá odstrániť</string>
+ <string name="cannot_delete_multi_media_items">Niektoré súbory nie je možné teraz zmazať. skúste neskôr.</string>
+ <string name="xmlrpc_error">Nie je možné pripojiť sa. Zadajte adresu xmlrpc.php na vašej stránke a skúste znova.</string>
+ <string name="scaled_image_error">Zadajte správnu hodnotu šírky</string>
+ <string name="email_reserved">Emailová adresa už bola použitá. Skontrolujte si, či ste dostali email s aktivačným odkazom. Ak sa vám to nepodarí teraz, môžete skúsiť o pár dní.</string>
+ <string name="file_not_found">Nie je možné nájsť súbor pre nahratie. Nebol zmazaný alebo presunutý?</string>
+ <string name="email_cant_be_used_to_signup">Nemôžete použiť túto emailovú adresu na prihlásenie. Vyskytol sa problém s blokovanám niektorých našich emailov. Použite iného poskytovateľa emailu.</string>
+ <string name="http_credentials">HTTP poverenia (voliteľné)</string>
+ <string name="file_error_create">Nie je možné vytvoriť dočasný súbor. Uistite sa, či máte dostatok miesta vo vašom zariadení.</string>
+ <string name="dlg_unapproving_comments">Ruší sa schválenie</string>
+ <string name="error_blog_hidden">Blog je skrytý a nemôže byť načítaný. Povoľte ho v nastaveniach a skúste znova.</string>
+ <string name="site_address">URL adresa vašej stránky</string>
+ <string name="reader_title_applog">Záznamy aplikácie</string>
+ <string name="media_gallery_type_thumbnail_grid">Mriežka s náhľadmi</string>
+ <string name="local_draft">Lokálny koncept </string>
+ <string name="account_details">Detaily účtu</string>
+ <string name="blog_name_must_include_letters">Adresa stránky musí mať aspoň 1 písmeno</string>
+ <string name="blog_title_invalid">Neplatný nadpis stránky</string>
+ <string name="dlg_approving_comments">Schvaľovanie</string>
+ <string name="new_media">Nový súbor</string>
+ <string name="media_error_no_permission">Nemáte práva na zobrazenie knižnice súborov</string>
+ <string name="share_action_media">Knižnica súborov</string>
+ <string name="jetpack_message_not_admin">Pre zobrazenie štatistík sa vyžaduje plugin Jetpack. Kontaktujte administrátora stránky.</string>
+ <string name="pending_review">Čaká na vybavenie</string>
+ <string name="fatal_db_error">Počas vytvárania databázy sa vyskytla chyba. Preinštalujte aplikáciu.</string>
+ <string name="privacy_policy">Ochrana osobných údajov</string>
+ <string name="local_changes">Miestne zmeny</string>
+ <string name="link_enter_url_text">Text odkazu (voliteľný)</string>
+ <string name="post_not_found">Počas načítania príspevku sa vyskytla chyba. Obnovte príspevky a skúste znova.</string>
+ <string name="open_source_licenses">Open source licencie</string>
+ <string name="trash_no">Nevymazávať</string>
+ <string name="trash">Kôš</string>
+ <string name="dlg_trashing_comments">Vymazáva sa</string>
+ <string name="themes_live_preview">Živý náhľad</string>
+ <string name="page_not_published">Stránka nie je zverejnená</string>
+ <string name="post_not_published">Príspevok nie je zverejnený</string>
+ <string name="blog_name_invalid">Neplatná adresa</string>
+ <string name="email_hint">Emailová adresa</string>
+ <string name="reader_toast_err_remove_tag">Túto značku nie je možné odstrániť</string>
+ <string name="reader_toast_err_add_tag">Túto značku nie je možné pridať</string>
+ <string name="reader_share_link">Zdieľať odkaz</string>
+ <string name="add_account_blog_url">Adresa blogu</string>
+ <string name="image_settings">Nastavenie obrázka</string>
+ <string name="http_authorization_required">Požaduje sa autorizácia</string>
+ <string name="view_site">Pozrieť stránku</string>
+ <string name="new_post">Nový príspevok</string>
+ <string name="post_format">Formát príspevku</string>
+ <string name="location_not_found">Neznáma lokalita</string>
+ <string name="share_action_post">Nový príspevok</string>
+ <string name="category_desc">Popis kategórie (voliteľné)</string>
+ <string name="category_slug">Slug kategórie (voliteľné)</string>
+ <string name="view_in_browser">Pozrieť v prehliadači</string>
+ <string name="add_new_category">Pridať novú kategóriu</string>
+ <string name="category_name">Názov kategórie</string>
+ <string name="comment_added">Komentár bol úspešne pridaný</string>
+ <string name="category_parent">Nadradená kategória (voliteľné):</string>
+ <string name="preview_post">Pozrieť náhľad príspevku</string>
+ <string name="delete_draft">Vymazať koncept</string>
+ <string name="preview_page">Náhľad stránky</string>
+ <string name="dlg_confirm_trash_comments">Odstrániť?</string>
+ <string name="trash_yes">Kôš</string>
+ <string name="toast_comment_unedited">Komentáre neboli zmenené</string>
+ <string name="content_required">Vyžaduje sa komentár</string>
+ <string name="sure_to_cancel_edit_comment">Zrušiť úpravu tohto komentáru?</string>
+ <string name="saving_changes">Ukladajú sa zmeny</string>
+ <string name="author_url">URL adresa autora</string>
+ <string name="hint_comment_content">Komentár</string>
+ <string name="author_email">Email autora</string>
+ <string name="author_name">Meno autora</string>
+ <string name="dlg_spamming_comments">Označené ako spam</string>
+ <string name="mnu_comment_unapprove">Neschváliť</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_trash">Kôš</string>
+ <string name="comment_status_unapproved">Čakajúci na schválenie</string>
+ <string name="edit_comment">Upraviť komentár</string>
+ <string name="mnu_comment_approve">Schváliť</string>
+ <string name="comment_status_trash">Vymazané</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_approved">Schválené</string>
+ <string name="delete_page">Vymazať stránku</string>
+ <string name="delete_post">Vymazať príspevok</string>
+ <string name="post_settings">Nastavenia príspevku</string>
+ <string name="horizontal_alignment">Horizontálne zarovnanie</string>
+ <string name="page_settings">Nastavenia stránky</string>
+ <string name="theme_premium_theme">Prémiová téma</string>
+ <string name="upload_failed">Nahratie sa nepodarilo</string>
+ <string name="create_a_link">Vytvoriť odkaz</string>
+ <string name="media_gallery_num_columns">Počet stĺpcov</string>
+ <string name="theme_current_theme">Súčasná téma</string>
+ <string name="select_categories">Vyberte si kategórie</string>
+ <string name="media_gallery_edit">Upraviť galériu</string>
+ <string name="media_gallery_image_order">Poradie obrázkov</string>
+ <string name="learn_more">Zistiť viac</string>
+ <string name="media_gallery_settings_title">Nastavenia galérie</string>
+ <string name="cancel_edit">Zrušiť úpravu</string>
+ <string name="connection_error">Chyba pripojenia</string>
+ <string name="add_comment">Pridať komentár</string>
+ <string name="edit_post">Upraviť príspevok</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="required_field">Povinné pole</string>
+ <string name="remove_account">Odstrániť webovú stránku</string>
+ <string name="blog_removed_successfully">Webová stránka bola úspešne odstránená</string>
+ <string name="notifications_empty_all">Žiadne upozornenia. Zatiaľ.</string>
+ <string name="deleting_page">Odstraňuje sa stránka</string>
+ <string name="deleting_post">Odstraňuje sa príspevok</string>
+ <string name="share_url_page">Zdieľať stránku</string>
+ <string name="share_link">Zdieľať odkaz</string>
+ <string name="share_url_post">Zdieľať príspevok</string>
+ <string name="creating_your_site">Stránka sa vytvára</string>
+ <string name="creating_your_account">Váš účet sa vytvára</string>
+ <string name="error_refresh_media">Počas načítania knižnice súborov sa vyskytla chyba, zopakujte to neskôr.</string>
+ <string name="reader_empty_posts_in_tag_updating">Načítavajú sa príspevky</string>
+ <string name="reader_likes_you_and_multi">Vy a niekoľkí ďalší (%,d) ste to lajkli</string>
+ <string name="reader_likes_multi">počet ľudí, ktorým sa to páči: %,d</string>
+ <string name="download">Sťahujú sa súbory</string>
+ <string name="comment_spammed">Komentáre označené ako spam</string>
+ <string name="reader_toast_err_get_comment">Nedá sa nahrať tento komentár</string>
+ <string name="reader_label_reply">Odpovedať</string>
+ <string name="video">Video</string>
+ <string name="cant_share_no_visible_blog">Nemôžete zdieľať na WordPress bez viditeľného blogu</string>
+ <string name="account_two_step_auth_enabled">Tento účet má povolenú dvojkrokové overovanie. Pre vytvorenie hesla pre aplikácie si otvorte bezpečnostné nastavenia na WordPress.com.</string>
+ <string name="reader_likes_you_and_one">Lajkli ste to vy a niekto ďalší</string>
+ <string name="reader_toast_err_get_post">Nie je možné načítať príspevok</string>
+ <string name="validating_site_data">Validujú sa údaje na stránke</string>
+ <string name="validating_user_data">Validujú sa používateľské dáta</string>
+ <string name="pick_video">Vyberte video</string>
+ <string name="select_time">Vyberte čas</string>
+ <string name="pick_photo">Vyberte forografiu</string>
+ <string name="select_date">Vyberte dátum</string>
+ <string name="reader_empty_followed_blogs_description">Neznepokojujte sa a ťuknite na ikonu vpravo hore a poznávajte!</string>
+ <string name="password_invalid">Zadajte bezpečnejšie heslo. Uistite sa, že má viac ako 7 znakov a obsahuje veľké písmená, malé písmená a čísla alebo špeciálne znaky.</string>
+ <string name="nux_add_selfhosted_blog">Pridajte vlastnú webovú stránku</string>
+ <string name="signing_in">Prihlasovanie...</string>
+ <string name="nux_welcome_create_account">Vytvoriť účet</string>
+ <string name="nux_tap_continue">Pokračovať</string>
+ <string name="nux_oops_not_selfhosted_blog">Prihlásiť sa do WordPress.com</string>
+ <string name="reader_label_added_tag">pridané: %s</string>
+ <string name="reader_likes_one">Jeden lajk</string>
+ <string name="limit_reached">Dosiahli ste limit. Môžete skúsiť o 1 minútu. Pokus o opakovanie skôr ako o minútu len predĺži čas čakania. Ak si myslíte, že nastala chyba, kontaktujte technickú podporu.</string>
+ <string name="reader_share_subject">Zdieľané z %s</string>
+ <string name="reader_empty_followed_tags">Nesledujete žiadne značky</string>
+ <string name="reader_likes_only_you">Páči sa vám to</string>
+ <string name="nux_tutorial_get_started_title">Začíname!</string>
+ <string name="reader_untitled_post">(bez názvu)</string>
+ <string name="select_from_media_library">Vybrať z knižnice súborov</string>
+ <string name="reader_btn_unfollow">Sledujem</string>
+ <string name="reader_btn_follow">Sledovať</string>
+ <string name="reader_toast_err_view_image">Nie je možné zobraziť obrázok</string>
+ <string name="reader_toast_err_share_intent">Nie je možné zdieľať</string>
+ <string name="reader_toast_err_tag_invalid">Toto nie je platná značka</string>
+ <string name="reader_toast_err_tag_exists">Túto značku už sledujete</string>
+ <string name="reader_toast_err_comment_failed">Váš komentár nemohol byť odoslaný</string>
+ <string name="jetpack_not_found">Modul Jetpack nebol nájdený</string>
+ <string name="reader_label_removed_tag">Odstránené %s</string>
+ <string name="jetpack_message">Pre štatistiky je potrebný modul Jetpack. Chcete nainštalovať Jetpack?</string>
+ <string name="username_invalid">Nesprávne použivateľské meno</string>
+ <string name="reader_toast_err_url_intent">Nie je možné otvoriť %s</string>
+ <string name="reader_btn_share">Zdielať</string>
+ <string name="empty_list_default">Tento zoznam je prázdny</string>
+ <string name="media_add_new_media_gallery">Vytvoriť galériu</string>
+ <string name="media_add_popup_title">Pridať do knižnice súborov</string>
+ <string name="create_account_wpcom">Vytvoriť účet na WordPress.com</string>
+ <string name="connecting_wpcom">Pripája sa na WordPress.com</string>
+ <string name="reader_hint_comment_on_comment">Odpovedajte na komentár...</string>
+ <string name="stats_view_referrers">Odkazujúci</string>
+ <string name="stats_entry_referrers">Odkazujúci</string>
+ <string name="media_gallery_type_squares">Štvorce</string>
+ <string name="media_gallery_type_tiled">Dlaždice</string>
+ <string name="media_gallery_type_slideshow">Prezent8cia</string>
+ <string name="media_gallery_image_order_reverse">Opačné poradie</string>
+ <string name="stats_view_visitors_and_views">Návštevníci a zobrazenia</string>
+ <string name="stats_view_clicks">Kliknutia</string>
+ <string name="media_gallery_type_circles">Kruhy</string>
+ <string name="post_excerpt">Zhrnutie</string>
+ <string name="media_add_popup_capture_video">Natočiť video</string>
+ <string name="media_gallery_type">Typ</string>
+ <string name="custom_date">Rozsah dátumov</string>
+ <string name="media_add_popup_capture_photo">Spraviť fotku</string>
+ <string name="unattached">Nepriložené</string>
+ <string name="stats">Štatistiky</string>
+ <string name="media_gallery_image_order_random">Náhodné</string>
+ <string name="all">Všetko</string>
+ <string name="stats_timeframe_yesterday">Včera</string>
+ <string name="stats_timeframe_today">Dnes</string>
+ <string name="share_action">Zdielať</string>
+ <string name="share_action_title">Pridať ...</string>
+ <string name="theme_auth_error_title">Preberanie témy sa nepodarilo</string>
+ <string name="theme_set_success">Téma úspešne nastavená!</string>
+ <string name="theme_activating_button">Aktivuje sa</string>
+ <string name="theme_activate_button">Aktivovať</string>
+ <string name="themes_features_label">Funkcie</string>
+ <string name="themes_details_label">Detaily</string>
+ <string name="media_edit_failure">Aktualizácia sa nepodarila</string>
+ <string name="media_edit_success">Aktualizované</string>
+ <string name="media_edit_description_hint">Zadajte popis</string>
+ <string name="media_edit_caption_hint">Zadajte titulok</string>
+ <string name="media_edit_title_hint">Zadajte nadpis</string>
+ <string name="media_edit_description_text">Popis</string>
+ <string name="media_edit_caption_text">Titulok</string>
+ <string name="media_edit_title_text">Nadpis</string>
+ <string name="stats_totals_clicks">Kliknutí</string>
+ <string name="stats_totals_plays">Prehratí</string>
+ <string name="stats_totals_views">Zobrazení</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_tags_and_categories">Téma</string>
+ <string name="stats_entry_posts_and_pages">Nadpis</string>
+ <string name="stats_entry_country">Krajina</string>
+ <string name="stats_timeframe_months">Mesiacov</string>
+ <string name="stats_timeframe_weeks">Týždňov</string>
+ <string name="stats_timeframe_days">Dní</string>
+ <string name="passcode_preference_title">Uzamknutie PIN kódom</string>
+ <string name="passcode_change_passcode">Zmeniť PIN</string>
+ <string name="passcode_set">Nastaviť PIN</string>
+ <string name="passcode_re_enter_passcode">Znova zadajte PIN kód</string>
+ <string name="images">Obrázky</string>
+ <string name="themes">Témy</string>
+ <string name="passcode_enter_old_passcode">Zadajte starý PIN kód</string>
+ <string name="passcode_enter_passcode">Zadajte PIN kód</string>
+ <string name="passcode_manage">Spravovať funkciu overovanie PIN kódom</string>
+ <string name="passcode_turn_on">Zapnúť funkciu overovanie PIN kódom</string>
+ <string name="passcode_turn_off">Vypnúť funkciu overovanie PIN kódom</string>
+ <string name="stats_view_tags_and_categories">Značky a kategórie</string>
+ <string name="upload">Nahrať</string>
+ <string name="notifications">Upozornenia</string>
+ <string name="sign_in">Prihlásiť sa</string>
+ <string name="more_notifications">a %d ďalším.</string>
+ <string name="new_notifications">Počet nových upozornení: %d</string>
+ <string name="note_reply_successful">Odpoveď zverejnená</string>
+ <string name="follows">Sledovania</string>
+ <string name="loading">Načítava sa...</string>
+ <string name="httpuser">HTTP používateľské meno</string>
+ <string name="httppassword">HTTP heslo</string>
+ <string name="error_media_upload">Pri nahrávaní multimédií sa vyskytla chyba</string>
+ <string name="content_description_add_media">Pridať súbor</string>
+ <string name="post_content">Obsah (kliknutím pridajte text a súbor)</string>
+ <string name="publish_date">Zverejniť</string>
+ <string name="incorrect_credentials">Nesprávne používatelské meno alebo heslo.</string>
+ <string name="username">Používatelské meno</string>
+ <string name="password">Heslo</string>
+ <string name="reader">Čitateľ</string>
+ <string name="pages">Podstránky</string>
+ <string name="featured_in_post">Vložiť obrázok do príspevku</string>
+ <string name="caption">Titulok (voliteľný)</string>
+ <string name="no_network_title">Sieť nedostupná</string>
+ <string name="anonymous">Anonymný</string>
+ <string name="posts">Príspevky</string>
+ <string name="width">Šírka</string>
+ <string name="post">Príspevok</string>
+ <string name="page">Stránka</string>
+ <string name="featured">Použiť ilustračný obrázok</string>
+ <string name="blogusername">Používateľské meno</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Nahrať a zobraziť odkaz na zmenšený obrázok</string>
+ <string name="scaled_image">Šírka zmenšeného obrázku</string>
+ <string name="scheduled">Načasované</string>
+ <string name="link_enter_url">URL adresa</string>
+ <string name="version">Verzia</string>
+ <string name="app_title">WordPress pre Android</string>
+ <string name="tos">Zmluvné podmienky</string>
+ <string name="image_alignment">Zarovnanie</string>
+ <string name="refresh">Obnoviť</string>
+ <string name="untitled">Bez názvu</string>
+ <string name="edit">Upraviť</string>
+ <string name="page_id">Strana</string>
+ <string name="post_id">Príspevok</string>
+ <string name="immediately">Okamžite</string>
+ <string name="post_password">Heslo (voliteľné)</string>
+ <string name="quickpress_add_alert_title">Nastaviť skrátené meno</string>
+ <string name="today">Dnes</string>
+ <string name="settings">Nastavenia</string>
+ <string name="share_url">Zdielať URL adresu</string>
+ <string name="quickpress_window_title">Vyberte blog pre QuickPress skratku</string>
+ <string name="quickpress_add_error">Názov skratky nemôže byť prázdny</string>
+ <string name="draft">Koncept</string>
+ <string name="post_private">Súkromné</string>
+ <string name="publish_post">Zverejniť</string>
+ <string name="upload_full_size_image">Nahrať a odkázať na obrázok v plnom rozlíšení</string>
+ <string name="categories">Kategórie</string>
+ <string name="tags_separate_with_commas">Značky (oddeľte značky čiarkami)</string>
+ <string name="title">Názov</string>
+ <string name="notification_vibrate">Vibrovať</string>
+ <string name="notification_blink">Blikať oznamovacie svetlo</string>
+ <string name="status">Stav</string>
+ <string name="location">Umiestnenie</string>
+ <string name="sdcard_title">Vyžadovaná SD karta</string>
+ <string name="select_video">Vyberte video z galérie</string>
+ <string name="media">Súbory</string>
+ <string name="delete">Zmazať</string>
+ <string name="none">Žiadny</string>
+ <string name="blogs">Blogy</string>
+ <string name="select_photo">Vybrať foografiu z galérie</string>
+ <string name="reply">Odpovedať</string>
+ <string name="preview">Preview</string>
+ <string name="on">na</string>
+ <string name="cancel">Zrušiť</string>
+ <string name="save">Uložiť</string>
+ <string name="add">Pridať</string>
+ <string name="notification_settings">Nastavenie upozornení</string>
+ <string name="yes">Áno</string>
+ <string name="no">Nie</string>
+ <string name="error">Chyba</string>
+ <string name="category_refresh_error">Chyba aktualizací kategorií</string>
+</resources>
diff --git a/WordPress/src/main/res/values-sq/strings.xml b/WordPress/src/main/res/values-sq/strings.xml
new file mode 100644
index 000000000..b1e79f7ab
--- /dev/null
+++ b/WordPress/src/main/res/values-sq/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Përgjegjës</string>
+ <string name="role_editor">Redaktor</string>
+ <string name="role_author">Autor</string>
+ <string name="role_contributor">Kontribues</string>
+ <string name="role_follower">Ndjekës</string>
+ <string name="role_viewer">Shikues</string>
+ <string name="error_post_my_profile_no_connection">S&amp;#8217;ka lidhje, s&amp;#8217;u ruajt dot profili juaj</string>
+ <string name="alignment_none">Pa drejtim</string>
+ <string name="alignment_left">Majtas</string>
+ <string name="alignment_right">Djathtas</string>
+ <string name="site_settings_list_editor_action_mode_title">U përzgjodh %1$d</string>
+ <string name="error_fetch_users_list">S&amp;#8217;u morën dot përdorues të sajtit</string>
+ <string name="plans_manage">Administroni planin tuaj te\nWordPress.com/plans</string>
+ <string name="people_fetching">Po sillen përdoruesit…</string>
+ <string name="title_follower">Ndjekës</string>
+ <string name="title_email_follower">Ndjekës Me Email</string>
+ <string name="people_empty_list_filtered_viewers">Ende s&amp;#8217;keni ndonjë parës.</string>
+ <string name="people_empty_list_filtered_email_followers">Ende s&amp;#8217;keni ndonjë ndjekës me email.</string>
+ <string name="people_empty_list_filtered_followers">Ende s&amp;#8217;keni ndonjë ndjekës.</string>
+ <string name="people_empty_list_filtered_users">Ende s&amp;#8217;keni ndonjë përdorues.</string>
+ <string name="people_dropdown_item_email_followers">Ndjekës Me Email</string>
+ <string name="people_dropdown_item_viewers">Parës</string>
+ <string name="people_dropdown_item_followers">Ndjekës</string>
+ <string name="people_dropdown_item_team">Ekipi</string>
+ <string name="invite_message_usernames_limit">Ftoni deri në 10 adresa email dhe/ose emra përdoruesish WordPress.com. Atyre që kanë nevojë për emër përdoruesi, do t&amp;#8217;u dërgohen udhëzime se si të krijojnë një të tillë.</string>
+ <string name="viewer_remove_confirmation_message">Nëse e hiqni këtë parës, ai ose ajo s&amp;#8217;do të jetë në gjendje ta vizitojë këtë sajt.\n\nDo të donit ta hiqnit këtë parës, sido qoftë?</string>
+ <string name="follower_remove_confirmation_message">Në u heqtë, ku ndjekës do të reshtë së marri njoftime mbi këtë sajt, veç në u ribëftë ndjekës.\n\nDo të donit ta hiqnit këtë ndjekës, sido qoftë?</string>
+ <string name="follower_subscribed_since">Që prej %1$s</string>
+ <string name="reader_label_view_gallery">Shihni Galerinë</string>
+ <string name="error_remove_follower">S&amp;#8217;u hoq dot ndjekësi</string>
+ <string name="error_remove_viewer">S&amp;#8217;u hoq dot shikuesi</string>
+ <string name="error_fetch_email_followers_list">S&amp;#8217;u morën dot ndjekës të sajtit me email</string>
+ <string name="error_fetch_followers_list">S&amp;#8217;u morën dot ndjekës të sajtit</string>
+ <string name="editor_failed_uploads_switch_html">Disa ngarkime mediash dështuan. S&amp;#8217;mund të kaloni nën mënyrën HTML\n në këtë gjendje. Të hiqen krejt ngarkimet e dështuara dhe të vazhdohet?</string>
+ <string name="format_bar_description_html">Mënyra HTML</string>
+ <string name="visual_editor">Përpunues pamor</string>
+ <string name="image_thumbnail">Miniaturë figure</string>
+ <string name="format_bar_description_ul">Listë e parenditur</string>
+ <string name="format_bar_description_ol">Listë e renditur</string>
+ <string name="format_bar_description_more">Futni më tepër</string>
+ <string name="format_bar_description_media">Futni media</string>
+ <string name="format_bar_description_strike">Hequrvije</string>
+ <string name="format_bar_description_link">Futni lidhje</string>
+ <string name="format_bar_description_quote">Bllok citimi</string>
+ <string name="format_bar_description_italic">Të pjerrëta</string>
+ <string name="format_bar_description_underline">Nënvijë</string>
+ <string name="image_settings_save_toast">Ndryshimet u ruajtën</string>
+ <string name="image_caption">Titull</string>
+ <string name="image_alt_text">Tekst alternativ</string>
+ <string name="image_link_to">Lidhje për te</string>
+ <string name="format_bar_description_bold">Të trasha</string>
+ <string name="image_width">Gjerësi</string>
+ <string name="image_settings_dismiss_dialog_title">Të hidhen tej ndryshimet e paruajtura?</string>
+ <string name="stop_upload_dialog_title">Të ndalet ngarkimi?</string>
+ <string name="stop_upload_button">Ndale Ngarkimin</string>
+ <string name="alert_error_adding_media">Ndodhi një gabim gjatë futjes së medias</string>
+ <string name="alert_action_while_uploading">Po ngarkoni media. Ju lutemi, pritni deri sa kjo të plotësohet.</string>
+ <string name="alert_insert_image_html_mode">S&amp;#8217;futen dot media drejt e nën mënyrën HTML. Ju lutemi, kaloni nën mënyrën pamore.</string>
+ <string name="uploading_gallery_placeholder">Po ngarkohet galeria…</string>
+ <string name="invite_error_some_failed">Ftesa u dërgua por pati gabim(e)!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Ftesa u dërgua me sukses</string>
+ <string name="tap_to_try_again">Prekeni që të riprovohet!</string>
+ <string name="invite_error_sending">Ndodhi një gabim teksa provohej të dërgohej ftesa!</string>
+ <string name="invite_error_invalid_usernames_multiple">S&amp;#8217;dërgohet dot: Ka emra përdoruesish ose email-e të pavlefshëm</string>
+ <string name="invite_error_invalid_usernames_one">S&amp;#8217;dërgohet dot: Një emër përdoruesi ose email është i pavlefshëm</string>
+ <string name="invite_error_no_usernames">Ju lutemi, shtoni të paktën një emër përdoruesi</string>
+ <string name="invite_message_info">(Në daçi) Mund të jepni një mesazh të përshtatur, deri në 500 shenja, i cili do të përfshihet te ftesa për përdoruesin(t).</string>
+ <string name="invite_message_remaining_other">%d shenja ende</string>
+ <string name="invite_message_remaining_one">1 shenjë ende</string>
+ <string name="invite_message_remaining_zero">0 shenja</string>
+ <string name="invite_invalid_email">Adresa email \'%s\' është e pavlefshme</string>
+ <string name="invite_message_title">Mesazh i Personalizuar</string>
+ <string name="invite_already_a_member">Ka tashmë një anëtar me emër përdoruesi \'%s\'</string>
+ <string name="invite_username_not_found">S&amp;#8217;u gjet përdorues me emër përdoruesi \'%s\'</string>
+ <string name="invite">Ftoji</string>
+ <string name="invite_names_title">Emra përdoruesish ose Email-e</string>
+ <string name="send_link">Dërgo lidhje</string>
+ <string name="my_site_header_external">I jashtëm</string>
+ <string name="invite_people">Ftoni Njerëz</string>
+ <string name="signup_succeed_signin_failed">Llogaria juaj u krijua, por ndodhi një gabim teksa po bënin hyrjen tuaj.\nProvoni të hyni me emrin e përdoruesit dhe fjalëkalimin tuaj të ri të sapokrijuar.</string>
+ <string name="label_clear_search_history">Spastro historikun e kërkimeve</string>
+ <string name="dlg_confirm_clear_search_history">Të spastrohet historiku?</string>
+ <string name="reader_empty_posts_in_search_description">S&amp;#8217;u gjetën postime në gjuhën tuaj për %s</string>
+ <string name="reader_label_post_search_running">Po kërkohet…</string>
+ <string name="reader_label_related_posts">Lexime të Afërta</string>
+ <string name="reader_empty_posts_in_search_title">S&amp;#8217;u gjetën postime</string>
+ <string name="reader_label_post_search_explainer">Kërkoni në krejt blogjet publike WordPress.com</string>
+ <string name="reader_hint_post_search">Kërkoni te WordPress.com</string>
+ <string name="reader_title_related_post_detail">Postim i Afërt</string>
+ <string name="reader_title_search_results">Kërkoni për %s</string>
+ <string name="preview_screen_links_disabled">Në skenën paraparje lidhjet janë të çaktivizuara</string>
+ <string name="draft_explainer">Ky postim është një skicë që s&amp;#8217;është botuar</string>
+ <string name="send">Dërgoje</string>
+ <string name="person_removed">U hoq me sukses %1$s</string>
+ <string name="user_remove_confirmation_message">Nëse e hiqni %1$s, ai përdorues nuk do të jetë më në gjendje të hyjë në këtë sajt, por çfarëdo lënde e krijuar nga %1$s do të mbetet te sajti.\n\nDoni ende ta hiqni këtë përdorues?</string>
+ <string name="person_remove_confirmation_title">Hiqe %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Sajtet në këtë listë s&amp;#8217;kanë postuar ndonjë gjë tani së fundi</string>
+ <string name="people">Persona</string>
+ <string name="edit_user">Përpunoni Përdoruesin</string>
+ <string name="role">Rol</string>
+ <string name="error_remove_user">S&amp;#8217;e hoqi dot përdoruesin</string>
+ <string name="error_update_role">S&amp;#8217;përditësoi dot rol përdoruesi</string>
+ <string name="error_fetch_viewers_list">S&amp;#8217;mori dot shikues sajti</string>
+ <string name="gravatar_camera_and_media_permission_required">Që të mund të përzgjidhni ose bëni një foto, lypset leje</string>
+ <string name="error_updating_gravatar">Gabim teksa përditësohej Gravatari juaj</string>
+ <string name="error_locating_image">Gabim në ngarkimin e figurës së qethur</string>
+ <string name="error_refreshing_gravatar">Gabim në ringarkimin e Gravatarit tuaj</string>
+ <string name="gravatar_tip">E re! Prekeni Gravatarin tuaj që ta ndryshoni!</string>
+ <string name="error_cropping_image">Gabim teksa qethej figura</string>
+ <string name="launch_your_email_app">Nisni aplikacionin tuaj për email</string>
+ <string name="checking_email">Po kontrollohet email-i</string>
+ <string name="not_on_wordpress_com">Jo në WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Jo e mundshme këtë çast. Ju lutemi, jepni fjalëkalimin tuaj</string>
+ <string name="check_your_email">Kontrolloni email-in tuaj</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Merrni një lidhje dërguar te email-i juaj, që të regjistroheni në çast</string>
+ <string name="logging_in">Po hyhet</string>
+ <string name="enter_your_password_instead">Në vend të kësaj, jepni fjalëkalimin tuaj</string>
+ <string name="web_address_dialog_hint">E shfaqur publikisht kur komentoni.</string>
+ <string name="jetpack_not_connected_message">Shtojca Jetpack është e instaluar, por jo e lidhur me WordPress.com-in. Doni të lidhet Jetpack-u?</string>
+ <string name="username_email">Email ose emër përdoruesi</string>
+ <string name="jetpack_not_connected">Shtojca Jetpack s&amp;#8217;është e lidhur</string>
+ <string name="new_editor_reflection_error">Përpunuesi pamor s&amp;#8217;është i përputhshëm me pajisjen tuaj.\n U çaktivizua vetvetiu.</string>
+ <string name="stats_insights_latest_post_no_title">(pa titull)</string>
+ <string name="capture_or_pick_photo">Bëni ose përzgjidhni foto</string>
+ <string name="plans_post_purchase_button_themes">Shfletoni Tema</string>
+ <string name="plans_post_purchase_text_themes">Tani keni hyrje të pakufizuar te temat Me Pagesë. Për t&amp;#8217;ia filluar, parashihni te sajti juaj cilëndo temë.</string>
+ <string name="plans_post_purchase_title_themes">Gjeni një temë të përsosur, nga ato me Pagesë</string>
+ <string name="plans_post_purchase_button_video">Nisni një postim të ri</string>
+ <string name="plans_post_purchase_text_video">Video në sajtin tuaj mund të ngarkoni dhe strehoni përmes VideoPress-it dhe depos tuaj të zgjeruar për media.</string>
+ <string name="plans_post_purchase_title_video">Jepuni jetë postimeve, përmes vidoesh</string>
+ <string name="plans_post_purchase_button_customize">Përshtatje e Sajtit tim</string>
+ <string name="plans_post_purchase_text_customize">Tani mund të përdorni shkronja vetjake, ngjyra vetjake, dhe aftësi përpunimi për CSS vetjake.</string>
+ <string name="plans_post_purchase_title_customize">Përshtatni Shkronja &amp; Ngjyra</string>
+ <string name="plans_post_purchase_text_intro">Sajti juaj po bën salto nga gëzimi! Tani eksploroni veçoritë e reja të sajtit tuaj dhe zgjidhni nga doni t&amp;#8217;ia filloni.</string>
+ <string name="plans_post_purchase_title_intro">Është i juaji, jepini!</string>
+ <string name="plans">Plane</string>
+ <string name="plan">Plan</string>
+ <string name="export_your_content_message">Postimet, faqet dhe rregullimet tuaja do t&amp;#8217;ju dërgohen me email te %s.</string>
+ <string name="plans_loading_error">S&amp;#8217;arrihet të ngarkohen planet</string>
+ <string name="export_your_content">Eksportoni lëndën tuaj</string>
+ <string name="exporting_content_progress">Po eksportohet lëndë…</string>
+ <string name="export_email_sent">Email-i i eksportimit u dërgua!</string>
+ <string name="show_purchases">Shfaqi blerjet</string>
+ <string name="premium_upgrades_message">Në sajtin tuaj keni përmirësime me pagesë aktive. Ju lutemi, anuloni përmirësimet, përpara se të fshini sajtin tuaj.</string>
+ <string name="checking_purchases">Po kontrollohen blerjet</string>
+ <string name="premium_upgrades_title">Përmirësime Me Pagesë</string>
+ <string name="purchases_request_error">Diç shkoi ters. S&amp;#8217;u kërkuan dot blerjet.</string>
+ <string name="delete_site_progress">Po fshihet sajti…</string>
+ <string name="delete_site_summary">Ky veprim s&amp;#8217;mund të zhbëhet. Fshirja e sajtit tuaj do të thotë heqje e krejt lëndës, kontribuesve dhe e përkatësive.</string>
+ <string name="delete_site_hint">Fshije sajtin</string>
+ <string name="export_site_hint">Eksportojeni sajtin tuaj si një kartelë XML</string>
+ <string name="are_you_sure">Jeni i Sigurt?</string>
+ <string name="export_site_summary">Nëse jeni i sigurt, ju lutemi, mos harroni të eksportoni lëndën tuaj tani. Nuk mund të rimerret më në të ardhmen.</string>
+ <string name="keep_your_content">Ruani Lëndën Tuaj</string>
+ <string name="domain_removal_hint">Përkatësitë që nuk do të funksionojnë më, pasi të hiqni sajtin tuaj</string>
+ <string name="domain_removal_summary">Kujdes! Fshirja e sajtit tuaj do të shkaktojë edhe heqjen e përkatësisë(ve) të radhitura më poshtë.</string>
+ <string name="primary_domain">Përkatësi Parësore</string>
+ <string name="domain_removal">Heqje Përkatësie</string>
+ <string name="error_deleting_site_summary">Pati një gabim në fshirjen e sajtit tuaj. Ju lutemi, lidhuni me ata të asistencës për më tepër</string>
+ <string name="error_deleting_site">Gabim në fshirjen e sajtit</string>
+ <string name="confirm_delete_site_prompt">Ju lutemi, shtypni %1$s në fushën më poshtë, që ta ripohoni. Sajti juaj mandej do të zhduket përgjithmonë.</string>
+ <string name="site_settings_export_content_title">Eksportim lënde</string>
+ <string name="contact_support">Lidhuni me asistencën</string>
+ <string name="confirm_delete_site">Ripohoni Fshirje Sajti</string>
+ <string name="start_over_text">Nëse doni një sajt, por nuk doni më ndonjë nga postimet dhe faqet që keni tani, ekipi ynë i asistencës mund të fshijë për ju postimet, faqet, mediat dhe komentet e sajtit tuaj.\n\nKjo do t&amp;#8217;i mbajë sajtin tuaj dhe URL-në e tij aktive, por do t&amp;#8217;ju ofrojë një fillim të ri për krijimin e lëndës. Që të spastrohet lënda juaj, thjesht lidhuni me ne.</string>
+ <string name="let_us_help">Le T&amp;#8217;ju Ndihmojmë</string>
+ <string name="site_settings_start_over_hint">Fillojeni sajtin nga e para</string>
+ <string name="me_btn_app_settings">Rregullime Aplikacioni</string>
+ <string name="start_over">Fillojani Nga e Para</string>
+ <string name="editor_remove_failed_uploads">Hiqi ngarkimet e dështuara</string>
+ <string name="editor_toast_failed_uploads">Disa ngarkime mediash dështuan. S&amp;#8217;mund ta ruani apo botoni\n postimin tuaj në këtë gjendje. Do të donit të hiqen krejt mediat e dështuara?</string>
+ <string name="comments_empty_list_filtered_trashed">S&amp;#8217;ka komente të shpënë në Hedhurina</string>
+ <string name="site_settings_advanced_header">Të mëtejshme</string>
+ <string name="comments_empty_list_filtered_pending">S&amp;#8217;ka komente në pritje të Shqyrtimit</string>
+ <string name="comments_empty_list_filtered_approved">S&amp;#8217;ka komente të Miratuara</string>
+ <string name="button_done">U bë</string>
+ <string name="button_skip">Anashkaloje</string>
+ <string name="site_timeout_error">S&amp;#8217;u lidh dot me sajtin WordPress, për shkak gabimi Mbarimi Kohe.</string>
+ <string name="xmlrpc_malformed_response_error">S&amp;#8217;u lidh dot. Instalimi i WordPress-it u përgjigj me një dokument XML-RPC të pavlefshëm.</string>
+ <string name="xmlrpc_missing_method_error">S&amp;#8217;u lidh dot. Metodat e domosdoshme XML-RPC mungojnë te shërbyesi.</string>
+ <string name="post_format_status">Gjendje</string>
+ <string name="post_format_video">Video</string>
+ <string name="alignment_center">Në qendër</string>
+ <string name="theme_free">Falas</string>
+ <string name="theme_all">Krejt</string>
+ <string name="theme_premium">Me pagesë</string>
+ <string name="post_format_chat">Fjalosje</string>
+ <string name="post_format_gallery">Galeri</string>
+ <string name="post_format_image">Figurë</string>
+ <string name="post_format_link">Lidhje</string>
+ <string name="post_format_quote">Citim</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Të dhëna mbi kurse dhe veprimtari WordPress.com (në Internet &amp; në rrugë klasike).</string>
+ <string name="post_format_aside">Anësore</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="notif_surveys">Mundësi për të marrë pjesë në kërkime &amp; pyetësorë nga WordPress.com.</string>
+ <string name="notif_tips">Këshilla për përfitimin e maksimumit nga WordPress.com.</string>
+ <string name="notif_community">Bashkësi</string>
+ <string name="replies_to_my_comments">Përgjigje ndaj komenteve të mia</string>
+ <string name="notif_suggestions">Këshilla</string>
+ <string name="notif_research">Punë kërkimore</string>
+ <string name="site_achievements">Arritje sajti</string>
+ <string name="username_mentions">Përmendje të emrit të përdoruesit</string>
+ <string name="likes_on_my_posts">Pëlqime të postimeve të mia</string>
+ <string name="site_follows">Ndjekje të sajtit</string>
+ <string name="likes_on_my_comments">Pëlqime të komenteve të mia</string>
+ <string name="comments_on_my_site">Komente te sajti im</string>
+ <string name="site_settings_list_editor_summary_other">%d objekte</string>
+ <string name="site_settings_list_editor_summary_one">1 objekt</string>
+ <string name="approve_auto_if_previously_approved">Komente përdoruesish të njohur</string>
+ <string name="approve_auto">Krejt përdoruesit</string>
+ <string name="approve_manual">Pa komente</string>
+ <string name="site_settings_paging_summary_other">%d komente për faqe</string>
+ <string name="site_settings_paging_summary_one">1 koment për faqe</string>
+ <string name="site_settings_multiple_links_summary_other">Kërko miratim për më tepër se %d lidhje</string>
+ <string name="site_settings_multiple_links_summary_one">Kërko miratim për më tepër se 1 lidhje</string>
+ <string name="site_settings_multiple_links_summary_zero">Kërko miratim për më tepër se 0 lidhje</string>
+ <string name="detail_approve_auto">Mirato vetvetiu komentet e gjithkujt.</string>
+ <string name="detail_approve_auto_if_previously_approved">Miratoje vetvetiu, nëse përdoruesi ka një koment të mëparshëm të miratuar</string>
+ <string name="detail_approve_manual">Kërkoni miratim dorazi për komentet e kujtdo.</string>
+ <string name="filter_trashed_posts">Të shpënë te hedhurinat</string>
+ <string name="days_quantity_one">1 ditë</string>
+ <string name="days_quantity_other">%d ditë</string>
+ <string name="filter_published_posts">Të botuara</string>
+ <string name="filter_draft_posts">Skica</string>
+ <string name="filter_scheduled_posts">E planifikuar për</string>
+ <string name="pending_email_change_snackbar">Që të ripohoni adresën tuaj të re, klikoni lidhjen e verifikimit te email-i i dërguar për %1$s</string>
+ <string name="primary_site">Sajti parësor</string>
+ <string name="web_address">Adresë Web</string>
+ <string name="editor_toast_uploading_please_wait">Po ngarkoni media. Ju lutemi, pritni deri sa kjo të plotësohet.</string>
+ <string name="error_refresh_comments_showing_older">Komentet s&amp;#8217;u freskuan dot këtë herë - po shfaqen komente më të vjetër</string>
+ <string name="editor_post_settings_set_featured_image">Vëre Si Figurë të Zgjedhur</string>
+ <string name="editor_post_settings_featured_image">Figurë e Zgjedhur</string>
+ <string name="new_editor_promo_desc">WordPress-i për Android tani përfshin një përpunues pamor të ri.\n Provojeni duke krijuar një postim të ri.</string>
+ <string name="new_editor_promo_title">Përpunues i ri fringo</string>
+ <string name="new_editor_promo_button_label">Bukur, faleminderit!</string>
+ <string name="visual_editor_enabled">Përpunuesi Pamor u aktivizua</string>
+ <string name="editor_content_placeholder">Ndajeni shembullin tuaj me ta…</string>
+ <string name="editor_page_title_placeholder">Titull Faqeje</string>
+ <string name="editor_post_title_placeholder">Titull Postimi</string>
+ <string name="email_address">Adresë email</string>
+ <string name="preference_show_visual_editor">Shfaq përpunuesin pamor</string>
+ <string name="dlg_sure_to_delete_comments">Të fshihen përgjithmonë këto komente?</string>
+ <string name="preference_editor">Përpunues</string>
+ <string name="dlg_sure_to_delete_comment">Të fshihet përgjithmonë ky koment?</string>
+ <string name="mnu_comment_delete_permanently">Fshije</string>
+ <string name="comment_deleted_permanently">Koment u fshi</string>
+ <string name="mnu_comment_untrash">Riktheje</string>
+ <string name="comments_empty_list_filtered_spam">Pa komente mesazh të padëshiruar</string>
+ <string name="comment_status_all">Krejt</string>
+ <string name="could_not_load_page">S&amp;#8217;u ngarkua dot faqja</string>
+ <string name="interface_language">Gjuhë Ndërfaqeje</string>
+ <string name="off">Off</string>
+ <string name="about_the_app">Rreth aplikacionit</string>
+ <string name="error_post_account_settings">S&amp;#8217;u ruajtën dot rregullimet e llogarisë tuaj</string>
+ <string name="error_post_my_profile">S&amp;#8217;u ruajt dot profili juaj</string>
+ <string name="error_fetch_account_settings">S&amp;#8217;u morën dot rregullimet e llogarisë tuaj</string>
+ <string name="error_fetch_my_profile">S&amp;#8217;u mor dot profili juaj</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, e mora vesh</string>
+ <string name="stats_widget_promo_desc">Shtojeni widget-in te skena juaj e kreut që të hyni te Statistikat tuaja me një klikim.</string>
+ <string name="stats_widget_promo_title">Widget Statistikash te Kreu</string>
+ <string name="site_settings_unknown_language_code_error">Kod gjuhe i papranuar</string>
+ <string name="site_settings_threading_dialog_description">Lejo komente të folezëzuar në rrjedha.</string>
+ <string name="site_settings_threading_dialog_header">Rrjedhë deri në</string>
+ <string name="remove">Hiqe</string>
+ <string name="search">Kërkoni</string>
+ <string name="add_category">Shtoni kategori</string>
+ <string name="disabled">E çaktivizuar</string>
+ <string name="site_settings_image_original_size">Madhësia Origjinale</string>
+ <string name="privacy_private">Sajti juaj është i dukshëm vetëm për ju dhe për përdorues që miratoni</string>
+ <string name="privacy_public_not_indexed">Sajti juaj është i dukshëm për këdo, por kërkon që motorët e kërkimeve të mos e indeksojnë</string>
+ <string name="privacy_public">Sajti juaj është i dukshëm për këdo dhe mund të indeksohet nga motorë kërkimesh</string>
+ <string name="about_me_hint">Pak fjalë rreth jush…</string>
+ <string name="public_display_name_hint">Nëse nuk caktohet një i tillë, si emër ekrani do të përdoret emri juaj i përdoruesit</string>
+ <string name="about_me">Rreth meje</string>
+ <string name="public_display_name">Emri i shfaqur botërisht</string>
+ <string name="my_profile">Profili Im</string>
+ <string name="first_name">Emri</string>
+ <string name="last_name">Mbiemri</string>
+ <string name="site_privacy_public_desc">Lejoji motorët e kërkimeve ta indeksojnë këtë sajt</string>
+ <string name="site_privacy_hidden_desc">Kërkoju motorëve të kërkimit të mos e indeksojnë këtë sajt</string>
+ <string name="site_privacy_private_desc">Do të doja që sajti im të jetë privat, i dukshëm vetëm për përdorues që i zgjedh unë</string>
+ <string name="cd_related_post_preview_image">Figurë paraparje postimi të afërt</string>
+ <string name="error_post_remote_site_settings">S&amp;#8217;u ruajtën dot të dhëna sajti</string>
+ <string name="error_fetch_remote_site_settings">S&amp;#8217;u morën dot të dhëna sajti</string>
+ <string name="error_media_upload_connection">Ndodhi një gabim ndërlidhjeje gjatë ngarkimit të medias</string>
+ <string name="site_settings_disconnected_toast">I shkëputur, përpunimet janë të çaktivizuara.</string>
+ <string name="site_settings_unsupported_version_error">Version WordPress i pambuluar</string>
+ <string name="site_settings_multiple_links_dialog_description">Kërko miratim për komente që përmbajnë më tepër lidhje se sa kaq.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Mbylli vetvetiu</string>
+ <string name="site_settings_close_after_dialog_description">Mbylli vetvetiu komentet në artikuj.</string>
+ <string name="site_settings_paging_dialog_description">Copëzojini rrjedhat e komenteve në disa faqe.</string>
+ <string name="site_settings_paging_dialog_header">Komente për faqe</string>
+ <string name="site_settings_close_after_dialog_title">Mbylle komentimin</string>
+ <string name="site_settings_blacklist_description">Kur një koment përmban cilëndo nga këto fjalë në lëndën e vet, emrin, URL-në, email-in, ose IP-në e tij, do të shënohet si i padëshiruar. Mund të jepni edhe copa fjalësh, fjala vjen "press" do të ketë efekt edhe për "WordPress."</string>
+ <string name="site_settings_hold_for_moderation_description">Kur një koment përmban cilëndo nga këto fjalë në lëndën e vet, emrin, URL-në, email-in, ose IP-në e tij, do të mbahet në radhën e moderimeve. Mund të jepni edhe copa fjalësh, fjala vjen "press" do të ketë efekt edhe për "WordPress."</string>
+ <string name="site_settings_list_editor_input_hint">Jepni një fjalë ose togfjalësh</string>
+ <string name="site_settings_list_editor_no_items_text">Pa objekte</string>
+ <string name="site_settings_learn_more_caption">Për postime individuale mund t&amp;#8217;i anashkaloni këto rregullime.</string>
+ <string name="site_settings_rp_preview3_site">te "Përmirësojeni"</string>
+ <string name="site_settings_rp_preview3_title">Përmirësoni Focus-in: VideoPress Për Dasma</string>
+ <string name="site_settings_rp_preview2_site">te "Aplikacione"</string>
+ <string name="site_settings_rp_preview2_title">Aplikacioni WordPress për Android i Ndërron Pamjen Goxha</string>
+ <string name="site_settings_rp_preview1_site">në "Celular"</string>
+ <string name="site_settings_rp_preview1_title">Gati Tani një Përditësim i Madh për iPhone/iPad</string>
+ <string name="site_settings_rp_show_images_title">Shfaqni Figura</string>
+ <string name="site_settings_rp_show_header_title">Shfaq Kryet</string>
+ <string name="site_settings_rp_switch_summary">Postimet e Afërta shfaqin nën postimet tuaja lëndë të afërt prej sajtit tuaj.</string>
+ <string name="site_settings_rp_switch_title">Shfaq Postime të Afërta</string>
+ <string name="site_settings_delete_site_hint">Heq prej aplikacionit të dhëna mbi sajtin tuaj</string>
+ <string name="site_settings_blacklist_hint">Komentet me përputhje me një filtër shënohen si të padëshiruar</string>
+ <string name="site_settings_moderation_hold_hint">Komentet me përputhje me ndonjë nga filtra vendosen në radhën e moderimit</string>
+ <string name="site_settings_multiple_links_hint">Për përdorues të njohur shpërfille kufirin e lidhjeve</string>
+ <string name="site_settings_whitelist_hint">Autori i komentit duhet të kenë një koment të miratuar më parë</string>
+ <string name="site_settings_user_account_required_hint">Përdoruesit duhet të jenë të regjistruar dhe të kenë bërë hyrjen</string>
+ <string name="site_settings_identity_required_hint">Autori i komentit duhet të plotësojë emrin dhe email-in</string>
+ <string name="site_settings_manual_approval_hint">Komentet duhet të miratohen dorazi</string>
+ <string name="site_settings_paging_hint">Shfaq komente në copa të madhësisë së dhënë</string>
+ <string name="site_settings_threading_hint">Lejo komente të folezëzuar deri në një nivel të dhënë</string>
+ <string name="site_settings_sort_by_hint">Përcakton rendin sipas të cilit shfaqen komentet</string>
+ <string name="site_settings_close_after_hint">Mos lejo më komente pas një kohe të caktuar</string>
+ <string name="site_settings_receive_pingbacks_hint">Lejo njoftime lidhjesh nga blogje të tjerë</string>
+ <string name="site_settings_send_pingbacks_hint">Përpjekje për të njoftuar cilindo blog me lidhje te artikulli</string>
+ <string name="site_settings_allow_comments_hint">Lejoju lexuesve të postojnë komente</string>
+ <string name="site_settings_discussion_hint">Shihni dhe ndryshoni rregullimet mbi diskutimet në sajtin tuaj</string>
+ <string name="site_settings_more_hint">Shihni krejt rregullimet e mundshme për Diskutimet</string>
+ <string name="site_settings_related_posts_hint">Shfaqni ose fshihni te lexuesi postime të afërta</string>
+ <string name="site_settings_upload_and_link_image_hint">Aktivizojeni që të ngarkohet përherë figura në madhësi të plotë</string>
+ <string name="site_settings_image_width_hint">Figurat në postime ripërmasoji sa kjo gjerësi</string>
+ <string name="site_settings_format_hint">Cakton format të ri postimesh</string>
+ <string name="site_settings_category_hint">Cakton kategori të re postimesh</string>
+ <string name="site_settings_location_hint">Shto vevetiu të dhëna vendi te postimet tuaja</string>
+ <string name="site_settings_password_hint">Ndryshoni fjalëkalimin tuaj</string>
+ <string name="site_settings_username_hint">Llogaria e përdoruesit të tanishëm</string>
+ <string name="site_settings_language_hint">Gjuha në të cilën shkruhet kryesisht në këtë blog</string>
+ <string name="site_settings_privacy_hint">Kontrolloni cilët mund të shohin sajtin tuaj</string>
+ <string name="site_settings_address_hint">Hëpërhë nuk mbulohet ndryshimi i adresës suaj</string>
+ <string name="site_settings_tagline_hint">Një përshkrim i shkurtër ose një togfjalësh ndjellës për të përshkruar blogun tuaj</string>
+ <string name="site_settings_title_hint">Shpjegoni, me pak fjalë, se për çfarë është ky sajt</string>
+ <string name="site_settings_whitelist_known_summary">Komente prej përdoruesish të njohur</string>
+ <string name="site_settings_whitelist_all_summary">Komente prej krejt përdoruesit</string>
+ <string name="site_settings_threading_summary">%d nivele</string>
+ <string name="site_settings_privacy_private_summary">Privat</string>
+ <string name="site_settings_privacy_hidden_summary">I fshehur</string>
+ <string name="site_settings_delete_site_title">Fshije Sajtin</string>
+ <string name="site_settings_privacy_public_summary">Publik</string>
+ <string name="site_settings_blacklist_title">Listë e zezë</string>
+ <string name="site_settings_moderation_hold_title">Mbaje për Moderim</string>
+ <string name="site_settings_multiple_links_title">Lidhje në komente</string>
+ <string name="site_settings_whitelist_title">Miratoji vetvetiu</string>
+ <string name="site_settings_threading_title">Në rrjedha</string>
+ <string name="site_settings_paging_title">Faqosje</string>
+ <string name="site_settings_sort_by_title">Renditini sipas</string>
+ <string name="site_settings_account_required_title">Përdoruesi duhet të ketë bërë hyrjen</string>
+ <string name="site_settings_identity_required_title">Duhet të përfshijë emër dhe email</string>
+ <string name="site_settings_receive_pingbacks_title">Merr Pingback-e</string>
+ <string name="site_settings_send_pingbacks_title">Dërgo Pingback-e</string>
+ <string name="site_settings_allow_comments_title">Lejo Komente</string>
+ <string name="site_settings_default_format_title">Format Parazgjedhje</string>
+ <string name="site_settings_default_category_title">Kategori Parazgjedhje</string>
+ <string name="site_settings_location_title">Aktivizoni Vendndodhje</string>
+ <string name="site_settings_address_title">Adresë</string>
+ <string name="site_settings_title_title">Titull Sajti</string>
+ <string name="site_settings_tagline_title">Përmbledhtas</string>
+ <string name="site_settings_this_device_header">Këtë pajisje</string>
+ <string name="site_settings_discussion_new_posts_header">Parazgjedhje për postime të reja</string>
+ <string name="site_settings_account_header">Llogari</string>
+ <string name="site_settings_writing_header">Shkrim</string>
+ <string name="newest_first">Më të rejat së pari</string>
+ <string name="site_settings_general_header">Të përgjithshme</string>
+ <string name="discussion">Diskutim</string>
+ <string name="privacy">Privatësi</string>
+ <string name="related_posts">Postime të afërt</string>
+ <string name="comments">Komente</string>
+ <string name="close_after">Mbylle pas</string>
+ <string name="oldest_first">Më të vjetrat së pari</string>
+ <string name="media_error_no_permission_upload">S&amp;#8217;keni leje të ngarkoni media në këtë sajt</string>
+ <string name="never">Kurrë</string>
+ <string name="unknown">E panjohur</string>
+ <string name="reader_err_get_post_not_found">Ky postim s&amp;#8217;ekziston më</string>
+ <string name="reader_err_get_post_not_authorized">S&amp;#8217;jeni i autorizuar ta shihni këtë postim</string>
+ <string name="reader_err_get_post_generic">S&amp;#8217;arrihet të merret ky postim</string>
+ <string name="blog_name_no_spaced_allowed">Adresa e sajtit s&amp;#8217;mund të përmbajë hapësira</string>
+ <string name="invalid_username_no_spaces">Emri i përdoruesit s&amp;#8217;mund të përmbajë hapësira</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Sajtet që ndiqni s&amp;#8217;kanë postuar gjë tani afër</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Pa postime së fundi</string>
+ <string name="media_details_copy_url_toast">URL-ja u kopjua në të papastër</string>
+ <string name="edit_media">Përpunoni media</string>
+ <string name="media_details_copy_url">Kopjojei URL-në</string>
+ <string name="media_details_label_date_uploaded">U ngarkua</string>
+ <string name="media_details_label_date_added">U shtua</string>
+ <string name="selected_theme">Temë e Përzgjedhur</string>
+ <string name="could_not_load_theme">S&amp;#8217;u ngarkua dot tema</string>
+ <string name="theme_activation_error">Diç shkoi ters. S&amp;#8217;u aktivizua dot tema</string>
+ <string name="theme_by_author_prompt_append">nga %1$s</string>
+ <string name="theme_prompt">Faleminderit që zgjidhni %1$s</string>
+ <string name="theme_try_and_customize">Provojeni &amp; Përshtateni</string>
+ <string name="theme_view">Shihni</string>
+ <string name="theme_details">Hollësi</string>
+ <string name="theme_support">Asistencë</string>
+ <string name="theme_done">U BË</string>
+ <string name="theme_manage_site">ADMINISTRONI SAJTIN</string>
+ <string name="title_activity_theme_support">Tema</string>
+ <string name="theme_activate">Aktivizoje</string>
+ <string name="date_range_start_date">Datë Fillimi</string>
+ <string name="date_range_end_date">Datë Përfundimi</string>
+ <string name="current_theme">Tema e Tanishme</string>
+ <string name="customize">Përshtateni</string>
+ <string name="details">Hollësi</string>
+ <string name="support">Asistencë</string>
+ <string name="active">Aktive</string>
+ <string name="stats_referrers_spam_generic_error">Diç shkoi ters gjatë veprimit. Gjendja për të padëshiruar nuk ndryshoi.</string>
+ <string name="stats_referrers_marking_not_spam">Po shënohet si jo i padëshiruar</string>
+ <string name="stats_referrers_unspam">Jo i padëshiruar</string>
+ <string name="stats_referrers_marking_spam">Po shënohet si i padëshiruar</string>
+ <string name="theme_auth_error_authenticate">Dështoi në prurjen e temave: dështoi në mirëfilltësimin e përdoruesit</string>
+ <string name="post_published">Postimi u botua</string>
+ <string name="page_published">Faqja u botua</string>
+ <string name="post_updated">Postimi u përditësua</string>
+ <string name="page_updated">Faqja u përditësua</string>
+ <string name="stats_referrers_spam">I padëshiruar</string>
+ <string name="theme_no_search_result_found">Na ndjeni, s&amp;#8217;u gjetën tema.</string>
+ <string name="media_uploaded_on">Ngarkuar më: %s</string>
+ <string name="media_dimensions">Përmasa: %s</string>
+ <string name="media_file_name">Emër kartele: %s</string>
+ <string name="media_file_type">Lloj kartele: %s</string>
+ <string name="upload_queued">Në radhë</string>
+ <string name="reader_label_gap_marker">Ngarko më tepër postime</string>
+ <string name="notifications_no_search_results">S&amp;#8217;ka sajte me përputhje për \'%s\'</string>
+ <string name="search_sites">Kërkoni në sajte</string>
+ <string name="unread">Të palexuara</string>
+ <string name="notifications_empty_view_reader">Shihni Lexuesin</string>
+ <string name="notifications_empty_action_followers_likes">Dilni në pah: komentoni në postime që keni lexuar.</string>
+ <string name="notifications_empty_action_comments">Hyni në një bisedë: komentoni postime prej blogjesh që ndiqni.</string>
+ <string name="notifications_empty_action_unread">Rindizni bisedën: shkruani një postim të ri.</string>
+ <string name="notifications_empty_action_all">Aktivizohuni! Komentoni në postime prej blogjesh që ndiqni.</string>
+ <string name="notifications_empty_likes">Ende pa pëlqime të reja.</string>
+ <string name="notifications_empty_followers">Ende pa ndjekës të rinj.</string>
+ <string name="notifications_empty_comments">Ende pa komente të reja.</string>
+ <string name="notifications_empty_unread">Po ecni mirë!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Ju lutemi, kaloni te Statistika te aplikacioni, dhe provoni ta shtoni widget-in më vonë</string>
+ <string name="stats_widget_error_readd_widget">Ju lutemi, hiqeni widget-in dhe shtojeni nga e para</string>
+ <string name="stats_widget_error_no_visible_blog">Te statistika shyhet dot pa një blog të dukshëm</string>
+ <string name="stats_widget_error_no_permissions">Llogaria juaj WordPress.com s&amp;#8217;mund të hyjë te Statistikat në këtë blog</string>
+ <string name="stats_widget_error_no_account">Ju lutemi, futuni në WordPress</string>
+ <string name="stats_widget_error_generic">S&amp;#8217;u ngarkuan dot statistika</string>
+ <string name="stats_widget_loading_data">Po ngarkohen të dhëna…</string>
+ <string name="stats_widget_name_for_blog">Statistikat e Sotme për %1$s</string>
+ <string name="stats_widget_name">Statistikat e Sotme për WordPress-in</string>
+ <string name="add_location_permission_required">Që të shtoni vend, lypset leje</string>
+ <string name="add_media_permission_required">Që të shtoni media, lypset leje</string>
+ <string name="access_media_permission_required">Që të hyni në media, lypset leje</string>
+ <string name="stats_enable_rest_api_in_jetpack">Që të shihni statistika tuaja, aktivizoni modulin API JSON te Jetpack-u.</string>
+ <string name="error_open_list_from_notification">Ky postim ose faqe është botuar në tjetër sajt</string>
+ <string name="reader_short_comment_count_multi">%s Komente</string>
+ <string name="reader_short_comment_count_one">1 Koment</string>
+ <string name="reader_label_submit_comment">DËRGOJE</string>
+ <string name="reader_hint_comment_on_post">Përgjigjjuni postimit…</string>
+ <string name="reader_discover_visit_blog">Vizitoni %s</string>
+ <string name="reader_discover_attribution_blog">Postuar fillimisht në %s</string>
+ <string name="reader_discover_attribution_author">Postuar fillimisht nga: %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Postuar fillimisht nga %1$s më %2$s</string>
+ <string name="reader_short_like_count_multi">%s Pëlqime</string>
+ <string name="reader_short_like_count_one">1 Pëlqim</string>
+ <string name="reader_label_follow_count">%,d ndjekës</string>
+ <string name="reader_short_like_count_none">Pëlqim</string>
+ <string name="reader_menu_tags">Përpunoni etiketa dhe blogje</string>
+ <string name="reader_title_post_detail">Postim Te Lexuesi</string>
+ <string name="local_draft_explainer">Ky postim ka një skicë lokale që s&amp;#8217;është botuar</string>
+ <string name="local_changes_explainer">Ky postim ka ndryshime vendore që s&amp;#8217;janë botuar</string>
+ <string name="notifications_push_summary">Rregullime për njoftime që shfaqen në pajisjen tuaj.</string>
+ <string name="notifications_email_summary">Rregullime për njoftime që dërgohen drejt email-it përshoqëruar llogarisë tuaj.</string>
+ <string name="notifications_tab_summary">Rregullime për njoftime që shfaqen te skeda Njoftime.</string>
+ <string name="notifications_disabled">Njoftimet nga aplikacioni janë çaktivizuar. Prekeni këtu që të aktivizohen te Rregullimet.</string>
+ <string name="notification_types">Lloje Njoftimesh</string>
+ <string name="error_loading_notifications">S&amp;#8217;ngarkoi dot rregullime njoftimesh</string>
+ <string name="replies_to_your_comments">Përgjigje ndaj komenteve tuaja</string>
+ <string name="comment_likes">Pëlqime komenti</string>
+ <string name="app_notifications">Njoftime aplikacioni</string>
+ <string name="notifications_tab">Skedë njoftimesh</string>
+ <string name="email">Email</string>
+ <string name="notifications_comments_other_blogs">Komente në sajte të tjera</string>
+ <string name="notifications_wpcom_updates">Përditësime WordPress.com</string>
+ <string name="notifications_other">Tjetër</string>
+ <string name="notifications_account_emails">Email nga WordPress.com</string>
+ <string name="notifications_account_emails_summary">Do të dërgojmë përherë email-e të rëndësishëm lidhur me llogarinë tuaj, por mundeni gjithashtu të merrni ca ekstra të dobishme.</string>
+ <string name="notifications_sights_and_sounds">Panorama dhe Tinguj</string>
+ <string name="your_sites">Sajtet Tuaj</string>
+ <string name="stats_insights_latest_post_trend">U bënë %1$s qëkur qe botuar %2$s. Ja se si ka ecur postimi deri tani…</string>
+ <string name="stats_insights_latest_post_summary">Përmbledhje Postimesh Më të Reja</string>
+ <string name="button_revert">Anuloji ndryshimet</string>
+ <string name="days_ago">%d ditë më parë</string>
+ <string name="yesterday">Dje</string>
+ <string name="connectionbar_no_connection">Pa lidhje</string>
+ <string name="page_trashed">Faqja u kalua te hedhurinat</string>
+ <string name="post_deleted">Postimi u fshi</string>
+ <string name="post_trashed">Postimi u kalua te hedhurinat</string>
+ <string name="trashed">Të hedhur te hedhurinat</string>
+ <string name="button_back">Mbrapsht</string>
+ <string name="page_deleted">Faqja u fshi</string>
+ <string name="button_stats">Statistika</string>
+ <string name="button_trash">Hedhurina</string>
+ <string name="button_preview">Paraparje</string>
+ <string name="button_view">Shiheni</string>
+ <string name="button_edit">Përpunojeni</string>
+ <string name="button_publish">Botojeni</string>
+ <string name="stats_no_activity_this_period">S&amp;#8217;ka veprimtari për këtë periudhë</string>
+ <string name="my_site_no_sites_view_subtitle">Do të donit të shtoni një të tillë?</string>
+ <string name="my_site_no_sites_view_title">S&amp;#8217;keni ende ndonjë sajt WordPress.</string>
+ <string name="my_site_no_sites_view_drake">Ilustrim</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">S&amp;#8217;jeni i autorizuar të hyni në këtë blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">Ky blog s&amp;#8217;u gjet dot</string>
+ <string name="undo">Zhbëje</string>
+ <string name="tabbar_accessibility_label_me">Unë</string>
+ <string name="tabbar_accessibility_label_my_site">Sajti Im</string>
+ <string name="editor_toast_changes_saved">Ndryshimet u ruajtën</string>
+ <string name="passcodelock_prompt_message">Jepni PIN-in tuaj</string>
+ <string name="push_auth_expired">Kërkesa skadoi. Hyni te WordPress.com që të riprovoni.</string>
+ <string name="stats_insights_best_ever">Parjet Më të Mira Ndonjëherë</string>
+ <string name="ignore">Shpërfille</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% e parjeve</string>
+ <string name="stats_insights_most_popular_hour">Ora më popullore</string>
+ <string name="stats_insights_most_popular_day">Dita më popullore</string>
+ <string name="stats_insights_today">Statistikat e Sotme</string>
+ <string name="stats_insights_popular">Dita dhe ora më popullore</string>
+ <string name="stats_insights_all_time">Postimet, parjet dhe vizitorët gjatë gjithë kohës</string>
+ <string name="stats_insights">Tendenca</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Që të shihni statistikat tuaja, bëni hyrjen te llogaria WordPress.com që përdorët për t&amp;#8217;ia përshoqëruar Jetpack-ut.</string>
+ <string name="stats_other_recent_stats_moved_label">Po kërkoni për të Tjera Statistika tuajat Së Fundi? I kemi kaluar te faqja Tendeca.</string>
+ <string name="me_disconnect_from_wordpress_com">Shkëputu nga WordPress.com</string>
+ <string name="me_btn_login_logout">Hyrje/Dalje</string>
+ <string name="me_connect_to_wordpress_com">Lidhu te WordPress.com</string>
+ <string name="account_settings">Rregullime Llogarie</string>
+ <string name="me_btn_support">Ndihmë &amp; Asistencë</string>
+ <string name="site_picker_cant_hide_current_site">"%s" s&amp;#8217;u fsheh ngaqë është sajti i tanishëm</string>
+ <string name="site_picker_create_dotcom">Ndërtoni një sajt te WordPress.com</string>
+ <string name="site_picker_add_self_hosted">Shto sajt të vetëstrehuar</string>
+ <string name="site_picker_edit_visibility">Shfaq/fshih sajte</string>
+ <string name="site_picker_add_site">Shtoni sajt</string>
+ <string name="my_site_btn_view_admin">Shihni Përgjegjësin</string>
+ <string name="my_site_btn_view_site">Shihni Sajtin</string>
+ <string name="site_picker_title">Zgjidhni një sajt</string>
+ <string name="my_site_btn_switch_site">Këmbe Sajt</string>
+ <string name="my_site_btn_blog_posts">Postime Blogu</string>
+ <string name="my_site_btn_site_settings">Rregullime</string>
+ <string name="my_site_header_look_and_feel">Pamja dhe Ndjesitë</string>
+ <string name="my_site_header_publish">Botoje</string>
+ <string name="my_site_header_configuration">Formësim</string>
+ <string name="reader_label_new_posts_subtitle">Prekeni që të shfaqen</string>
+ <string name="notifications_account_required">Për njoftime, bëni hyrjen te WordPress.com</string>
+ <string name="stats_unknown_author">Autor i Panjohur</string>
+ <string name="image_added">Figura u shtua</string>
+ <string name="signout">Shkëputu</string>
+ <string name="show">Shfaqe</string>
+ <string name="hide">Fshihe</string>
+ <string name="select_all">Përzgjidhe krejt</string>
+ <string name="sign_out_wpcom_confirm">Shkëputja e llogarisë suaj do të heqë prej kësaj pajisje krejt të dhënat WordPress.com për @%s, përfshi skica dhe ndryshime vendore.</string>
+ <string name="deselect_all">Përzgjidhe/Shpërzgjidhe krejt</string>
+ <string name="select_from_new_picker">Përzgjidhni shumëfish me marrësin e ri</string>
+ <string name="error_loading_images">Gabim në ngarkim figurash</string>
+ <string name="error_loading_videos">Gabim në ngarkim videosh</string>
+ <string name="loading_blog_images">Po sillen figura</string>
+ <string name="loading_blog_videos">Po sillen video</string>
+ <string name="stats_generic_error">Statistikat e kërkuara s&amp;#8217;u ngarkuan dot</string>
+ <string name="no_device_videos">S&amp;#8217;ka video</string>
+ <string name="no_blog_images">S&amp;#8217;ka figura</string>
+ <string name="no_blog_videos">S&amp;#8217;ka video</string>
+ <string name="no_device_images">S&amp;#8217;ka figura</string>
+ <string name="error_loading_blog_images">S&amp;#8217;arrin të sjellë figura</string>
+ <string name="error_loading_blog_videos">S&amp;#8217;arrin të sjellë video</string>
+ <string name="no_media_sources">S&amp;#8217;solli dot media</string>
+ <string name="loading_videos">Po ngarkohen video</string>
+ <string name="loading_images">Po ngarkohen figura</string>
+ <string name="no_media">S&amp;#8217;ka media</string>
+ <string name="device">Pajisje</string>
+ <string name="language">Gjuhë</string>
+ <string name="add_to_post">Shtoje në Postim</string>
+ <string name="media_picker_title">Përzgjidhni media</string>
+ <string name="take_photo">Bëni një foto</string>
+ <string name="take_video">Bëni një video</string>
+ <string name="tab_title_device_images">Figura Te Pajisja</string>
+ <string name="tab_title_device_videos">Video Te Pajisja</string>
+ <string name="tab_title_site_images">Figura Te Sajti</string>
+ <string name="tab_title_site_videos">Video Figura Te Sajti</string>
+ <string name="editor_toast_invalid_path">Shteg kartele i pavlefshëm</string>
+ <string name="verification_code">Kod verifikimi</string>
+ <string name="invalid_verification_code">Kod verifikimi i pavlefshëm</string>
+ <string name="verify">Verifikoni</string>
+ <string name="two_step_footer_label">Jepni kodin prej aplikacionit tuaj mirëfilltësues.</string>
+ <string name="two_step_footer_button">Dërgoje kodin përmes mesazhi tekst</string>
+ <string name="two_step_sms_sent">Kontrolloni mesazhet tuaj tekst për kodin e verifikimit.</string>
+ <string name="sign_in_jetpack">Që të lidheni me Jetpack-n, bëni hyrjen te llogaria juaj WordPress.com.</string>
+ <string name="auth_required">Që të vazhdoni, duhet të hyni.</string>
+ <string name="media_details_label_file_name">Emër kartele</string>
+ <string name="media_details_label_file_type">Lloj kartele</string>
+ <string name="error_publish_no_network">S&amp;#8217;mund të ketë botim, kur s&amp;#8217;ka lidhje. U ruajt si skicë.</string>
+ <string name="publisher">Botues:</string>
+ <string name="stats_followers_total_email_paged">Po shfaqen %1$d - %2$d nga %3$s Ndjekës Me Email</string>
+ <string name="stats_search_terms_unknown_search_terms">Terma të Panjohur Kërkimesh</string>
+ <string name="stats_followers_total_wpcom_paged">Po shfaqen %1$d - %2$d nga %3$s Ndjekës në WordPress.com</string>
+ <string name="stats_entry_search_terms">Term Kërkimi</string>
+ <string name="stats_view_authors">Autorë</string>
+ <string name="stats_view_search_terms">Terma Kërkimesh</string>
+ <string name="comments_fetching">Po sillen komentet…</string>
+ <string name="pages_fetching">Po sillen faqet…</string>
+ <string name="posts_fetching">Po sillen postimet…</string>
+ <string name="media_fetching">Po sillet media…</string>
+ <string name="stats_empty_search_terms_desc">Mësoni më tepër mbi trafikun tuaj të kërkimeve, duke vështruar te termat për të cilët vizitorët kanë kërkuar dhe gjetën sajtin tuaj.</string>
+ <string name="reader_empty_posts_request_failed">S&amp;#8217;arrin të marrë dot postimet</string>
+ <string name="error_notification_open">S&amp;#8217;u hap dot njoftimi</string>
+ <string name="stats_empty_search_terms">S&amp;#8217;ka terma kërkimesh të regjistruar</string>
+ <string name="toast_err_post_uploading">S&amp;#8217;arrin të hapë postim teksa ngarkohet</string>
+ <string name="post_uploading">Ngarkim</string>
+ <string name="stats_total">Gjithsej</string>
+ <string name="stats_overall">Gjithsej</string>
+ <string name="stats_period">Periudhë</string>
+ <string name="logs_copied_to_clipboard">Regjistrat e aplikacionit u kopjuan te e papastra</string>
+ <string name="reader_label_new_posts">Postime të reja</string>
+ <string name="reader_empty_posts_in_blog">Ku blog është i zbrazët</string>
+ <string name="stats_average_per_day">Mesatare Ditore</string>
+ <string name="stats_recent_weeks">Javët e Fundit</string>
+ <string name="error_copy_to_clipboard">Ndodhi një gabim gjatë kopjimit të tekstit te e papastra</string>
+ <string name="stats_months_and_years">Muaj dhe Vite</string>
+ <string name="reader_page_recommended_blogs">Sajte që mund t&amp;#8217;ju pëlqejnë</string>
+ <string name="themes_fetching">Po sillen temat…</string>
+ <string name="stats_for">Statistika për %s</string>
+ <string name="stats_other_recent_stats_label">Të Tjera Statistika Së Fundi</string>
+ <string name="stats_view_all">Shihini krejt</string>
+ <string name="stats_view">Shihni</string>
+ <string name="stats_followers_months">%1$d muaj</string>
+ <string name="stats_followers_years">%1$d vjet</string>
+ <string name="stats_followers_minutes">%1$d minuta</string>
+ <string name="stats_followers_an_hour_ago">një orë më parë</string>
+ <string name="stats_followers_hours">%1$d orë</string>
+ <string name="stats_followers_days">%1$d ditë</string>
+ <string name="stats_followers_a_minute_ago">një minutë më parë</string>
+ <string name="stats_followers_seconds_ago">sekonda më parë</string>
+ <string name="stats_followers_total_email">Ndjekës Me Email Gjithsej: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Ndjekës WordPress.com Gjithsej: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Postime gjithsej me ndjekës komentesh: %1$s</string>
+ <string name="stats_comments_by_authors">Sipas Autorësh</string>
+ <string name="stats_comments_by_posts_and_pages">Sipas Postimesh &amp;amp; Faqesh</string>
+ <string name="stats_totals_followers">Që prej</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Autor</string>
+ <string name="stats_entry_publicize">Shërbim</string>
+ <string name="stats_entry_followers">Ndjekës</string>
+ <string name="stats_totals_publicize">Ndjekës</string>
+ <string name="stats_entry_clicks_link">Lidhje</string>
+ <string name="stats_view_top_posts_and_pages">Postime &amp;amp; Faqe</string>
+ <string name="stats_view_videos">Video</string>
+ <string name="stats_view_publicize">Publicizoje</string>
+ <string name="stats_view_followers">Ndjekës</string>
+ <string name="stats_view_countries">Vende</string>
+ <string name="stats_likes">Pëlqime</string>
+ <string name="stats_pagination_label">Faqja %1$s nga %2$s</string>
+ <string name="stats_timeframe_years">Vite</string>
+ <string name="stats_views">Parje</string>
+ <string name="stats_visitors">Vizitorë</string>
+ <string name="stats_followers_a_year">Një vit</string>
+ <string name="stats_followers_a_month">Një muaj</string>
+ <string name="stats_followers_a_day">Një ditë</string>
+ <string name="stats_empty_video_desc">Nëse keni ngarkuar video duke përdorur VideoPress-in, shihni se sa herë janë parë ato.</string>
+ <string name="stats_empty_top_authors_desc">Gjurmoni parjet për postimet e çdo pjesëmarrësi, dhe shihni më nga afër për të zbuluar lëndën më popullore sipas secilit autor.</string>
+ <string name="stats_empty_clicks_desc">Kur lënda juaj përfshin lidhje drejt sajtesh të tjerë, do të shihni se mbi cilët klikojnë më tepër vizitorët tuaj.</string>
+ <string name="stats_empty_referrers_desc">Mësoni më tepër rreth vizibilitetit të sajtit tuaj, duke vështruar sajtet dhe motorët e kërkimeve që dërgojnë më tepër trafik drejt jush</string>
+ <string name="stats_empty_top_posts_desc">Zbuloni se cila është lënda juaj që shohin më shumë, dhe kontrolloni se si ecin përgjatë kohës postimet dhe faqet individuale.</string>
+ <string name="stats_empty_geoviews_desc">Eksploroni listën që të shihni se cilat vende dhe rajone prodhojnë shumicën e trafikut te sajti juaj.</string>
+ <string name="stats_empty_comments_desc">Nëse lejoni komente në sajtin tuaj, gjurmoni komentuesit tuaj kryesues dhe zbuloni se cilat tema ndezin bisedat më të gjalla, bazuar në 1,000 komentet më të reja.</string>
+ <string name="stats_empty_tags_and_categories_desc">Merrni një përmbledhje të temave më popullore te sajti juaj, siç i pasqyrojnë ato postimet tuaja kryesuese prej javës së kaluar.</string>
+ <string name="stats_empty_followers_desc">Ndiqni numrin e përgjithshëm të ndjekësve tuaj, dhe se për sa kohë e ka ndjekur sajtin tuaj secili.</string>
+ <string name="stats_empty_followers">S&amp;#8217;ka ndjekës</string>
+ <string name="stats_empty_publicize_desc">Ndiqni numrin e ndjekësve tuaj prej shërbimesh të ndryshme rrjetesh shoqërore që kanë përdorur publicizim.</string>
+ <string name="stats_empty_publicize">S&amp;#8217;ka regjistrim ndjekësish me publicizim</string>
+ <string name="stats_empty_video">S&amp;#8217;ka video të luajtura</string>
+ <string name="stats_empty_tags_and_categories">S&amp;#8217;ka parje postimesh apo faqesh të etiketuara</string>
+ <string name="stats_empty_clicks_title">S&amp;#8217;ka klikime të regjistruara</string>
+ <string name="stats_empty_referrers_title">S&amp;#8217;ka referues të regjistruar</string>
+ <string name="stats_empty_top_posts_title">S&amp;#8217;ka parje postimesh apo faqesh</string>
+ <string name="stats_empty_geoviews">S&amp;#8217;ka vende të regjistruara</string>
+ <string name="ssl_certificate_details">Hollësi</string>
+ <string name="delete_sure_post">Fshije këtë postim</string>
+ <string name="delete_sure">Fshije këtë skicë</string>
+ <string name="delete_sure_page">Fshije këtë faqe</string>
+ <string name="confirm_delete_multi_media">Të fshihen objektet e përzgjedhur?</string>
+ <string name="confirm_delete_media">Të fshihet objekti i përzgjedhur?</string>
+ <string name="media_gallery_date_range">Po shfaqen mediat nga %1$s në %2$s</string>
+ <string name="sure_to_remove_account">Të hiqet ky sajt?</string>
+ <string name="cab_selected">%d të përzgjedhur</string>
+ <string name="reader_empty_posts_liked">S\'keni pëlqyer ende ndonjë postim</string>
+ <string name="faq_button">FAQ</string>
+ <string name="browse_our_faq_button">Shfletoni FAQ-en tonë</string>
+ <string name="create_new_blog_wpcom">Krijoni blog WordPress.com</string>
+ <string name="new_blog_wpcom_created">Blogu WordPress.com u krijua!</string>
+ <string name="reader_empty_comments">Ende pa komente</string>
+ <string name="reader_label_comment_count_multi">%,d komente</string>
+ <string name="reader_label_view_original">Shihni artikullin origjinal</string>
+ <string name="reader_label_like">Pëlqim</string>
+ <string name="reader_label_comment_count_single">Një koment</string>
+ <string name="reader_label_comments_closed">Komentet janë mbyllur</string>
+ <string name="reader_label_comments_on">Me komentet hapur</string>
+ <string name="older_month">Më të vjetër se një muaj</string>
+ <string name="older_two_days">Më të vjetër se 2 ditë</string>
+ <string name="older_last_week">Më të vjetër se një javë</string>
+ <string name="select_a_blog">Përzgjidhni një sajt WordPress</string>
+ <string name="uploading_total">Po ngarkohet %1$d nga %2$d</string>
+ <string name="comment">Koment</string>
+ <string name="comment_trashed">Komenti u hodh te hedhurinat</string>
+ <string name="posts_empty_list">Ende pa postime. Pse nuk krijoni një?</string>
+ <string name="comment_reply_to_user">Përgjigjiuni %s</string>
+ <string name="pages_empty_list">Ende pa faqe. Pse nuk krijoni një?</string>
+ <string name="signing_out">Po dilet…</string>
+ <string name="nux_help_description">Vizitoni qendrën e ndihmës që të merrni përgjigje për pyetje të rëndomta, ose vizitoni forumet që të bëni pyetje të reja</string>
+ <string name="reader_title_photo_viewer">%1$d nga %2$d</string>
+ <string name="sending_content">Po ngarkohet lëndë %s</string>
+ <string name="mnu_comment_liked">U pëlqye</string>
+ <string name="posting_post">Po postohet "%s"</string>
+ <string name="agree_terms_of_service">Duke krijuar një llogari, pajtoheni me %1$sKushtet tërheqëse të Shërbimit%2$s</string>
+ <string name="more">Më tepër</string>
+ <string name="reader_empty_followed_blogs_title">S&amp;#8217;ndiqni ende ndonjë sajt</string>
+ <string name="reader_empty_posts_in_tag">S&amp;#8217;ka postime me këtë etiketë</string>
+ <string name="error_publish_empty_post">S&amp;#8217;botohet dot një postim i zbrazët</string>
+ <string name="error_refresh_unauthorized_posts">S&amp;#8217;keni leje të shihni ose të përpunoni postime</string>
+ <string name="error_refresh_unauthorized_pages">S&amp;#8217;keni leje të shihni ose të përpunoni faqe</string>
+ <string name="error_refresh_unauthorized_comments">S&amp;#8217;keni leje të shihni ose të përpunoni postime komente</string>
+ <string name="stats_no_blog">S&amp;#8217;u ngarkuan dot statistikat për blogun e kërkuar</string>
+ <string name="media_empty_list_custom_date">S&amp;#8217;ka media në këtë interval kohor</string>
+ <string name="reader_label_liked_by">Pëlqyer Nga</string>
+ <string name="reader_toast_err_generic">I pazoti të kryejë këtë veprim</string>
+ <string name="reader_toast_err_block_blog">I pazoti të bllokojë këtë blog</string>
+ <string name="reader_menu_block_blog">Bllokoje këtë blog</string>
+ <string name="reader_toast_blog_blocked">Postimet prej këtij blogu s&amp;#8217;do të shfaqen më</string>
+ <string name="contact_us">Lidhuni me ne</string>
+ <string name="hs__conversation_detail_error">Përshkruani problemin që po shihni</string>
+ <string name="hs__new_conversation_header">Asistencë me bisedë</string>
+ <string name="hs__conversation_header">Asistencë me bisedë</string>
+ <string name="hs__invalid_email_error">Jepni një adresë email të vlefshme</string>
+ <string name="hs__username_blank_error">Jepni një emër të vlefshëm</string>
+ <string name="add_location">Shtoni vend</string>
+ <string name="current_location">Vendi i tanishëm</string>
+ <string name="search_location">Kërko</string>
+ <string name="edit_location">Përpunoni</string>
+ <string name="search_current_location">Lokalizoje</string>
+ <string name="preference_send_usage_stats">Dërgo statistika</string>
+ <string name="preference_send_usage_stats_summary">Dërgo vetvetiu statistika përdorimi për të na ndihmuar ta përmirësojmë WordPress-in për Android</string>
+ <string name="update_verb">Përditësoje</string>
+ <string name="schedule_verb">Planifikoje</string>
+ <string name="reader_title_blog_preview">Blogu i Lexuesit</string>
+ <string name="reader_title_tag_preview">Etiketë Lexuesi</string>
+ <string name="reader_title_subs">Etiketa &amp; Blogje</string>
+ <string name="reader_page_followed_tags">Etiketa të ndjekura</string>
+ <string name="reader_page_followed_blogs">Sajte te ndjekur</string>
+ <string name="reader_hint_add_tag_or_url">Jepni një URL ose etiketë që të ndiqet</string>
+ <string name="reader_label_tag_preview">Postime të etiketuara me %s</string>
+ <string name="reader_toast_err_get_blog_info">I pazoti ta shfaqë këtë blog</string>
+ <string name="reader_toast_err_already_follow_blog">E ndiqni tashmë këtë blog</string>
+ <string name="reader_toast_err_follow_blog">I pazoti të ndjekë këtë blog</string>
+ <string name="reader_toast_err_unfollow_blog">I pazoti të ndërpresë ndjekjen e këtij sajti</string>
+ <string name="reader_label_followed_blog">Blogu u ndoq</string>
+ <string name="reader_empty_recommended_blogs">S&amp;#8217;ka blogje të këshilluar</string>
+ <string name="media_empty_list">S\'ka media</string>
+ <string name="ptr_tip_message">Ndihmës: Tërhiqeni për poshtë që të rifreskohet</string>
+ <string name="saving">Po ruhet…</string>
+ <string name="help">Ndihmë</string>
+ <string name="forgot_password">Humbët fjalëkalimin tuaj?</string>
+ <string name="forums">Forume</string>
+ <string name="help_center">Qendër ndihme</string>
+ <string name="ssl_certificate_error">Dëshmi SSL e pavlefshme</string>
+ <string name="ssl_certificate_ask_trust">Nëse zakonisht lidheni te ky sajt pa probleme, ky gabim mund të thotë që dikush po provon të hiqet si ky sajt, dhe s&amp;#8217;duhet të vazhdoni. Doni të besohet dëshmia, sido qoftë?</string>
+ <string name="blog_not_found">Ndodhi një gabim gjatë hyrjes te ky blog</string>
+ <string name="wait_until_upload_completes">Pritni derisa të plotësohet ngarkimi</string>
+ <string name="theme_fetch_failed">Dështoi sjellja e temave</string>
+ <string name="theme_set_failed">Dështoi në caktim teme</string>
+ <string name="theme_auth_error_message">Sigurohuni që keni privilegje për caktim temash</string>
+ <string name="mnu_comment_unspam">Jo i padëshiruar</string>
+ <string name="adding_cat_failed">Shtimi i kategorisë dështoi</string>
+ <string name="adding_cat_success">Kategoria u shtua me sukses</string>
+ <string name="cat_name_required">Emri i kategorisë është i domosdoshëm</string>
+ <string name="sdcard_message">Për të ngarkuar media, lypset një kartë SD e montuar</string>
+ <string name="stats_empty_comments">Ende pa komente</string>
+ <string name="reply_failed">Përgjigja dështoi</string>
+ <string name="error_generic">Ndodhi një gabim</string>
+ <string name="error_moderate_comment">Ndodhi një gabim gjatë moderimit</string>
+ <string name="error_edit_comment">Ndodhi një gabim gjatë përpunimit të komentit</string>
+ <string name="error_upload">Ndodhi një gabim gjatë ngarkimit të %s</string>
+ <string name="error_load_comment">S\'u ngarkua dot komenti</string>
+ <string name="error_downloading_image">Gabim në shkarkim figure</string>
+ <string name="passcode_wrong_passcode">PIN i gabuar</string>
+ <string name="invalid_password_message">Fjalëkalimi duhet të përmbajë të paktën 4 shenja</string>
+ <string name="invalid_username_too_short">Emri i përdoruesit duhet të jetë më i gjatë se 4 shenja</string>
+ <string name="invalid_username_too_long">Emri i përdoruesit duhet të jetë më i shkurtër se 61 shenja</string>
+ <string name="username_only_lowercase_letters_and_numbers">Emri i përdoruesit mund të përmbajë vetëm shkronja të vogla (a-z) dhe numra</string>
+ <string name="username_required">Jepni emër përdoruesi</string>
+ <string name="username_must_be_at_least_four_characters">Emri i përdoruesit duhet të jetë e pakta 4 shenja i gjatë</string>
+ <string name="username_must_include_letters">Emri i përdoruesit duhet të ketë të paktën 1 shkronjë (a-z)</string>
+ <string name="email_invalid">Jepni një adresë email të vlefshme</string>
+ <string name="username_exists">Ai emër përdoruesi ekziston tashmë</string>
+ <string name="username_reserved_but_may_be_available">Hëpërhë ky emër përdoruesi është ruajtur për dikë, por mund të jetë i lirë pas pak ditësh</string>
+ <string name="blog_name_required">Jepni një adresë sajti</string>
+ <string name="blog_name_must_be_at_least_four_characters">Adresa e sajtit duhet të jetë e pakta 4 shenja e gjatë</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Adresa e sajtit duhet të jetë më e shkurtër se 64 shenja</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Adresa e sajtit mund të përmbajë vetëm shkronja të vogla (a-z) dhe numra</string>
+ <string name="blog_name_exists">Ai sajt ekziston tashmë</string>
+ <string name="blog_name_reserved">Ai sajt është i rezervuar</string>
+ <string name="blog_name_reserved_but_may_be_available">Hëpërhë ky sajt është ruajtur për dikë, por mund të jetë i lirë pas pak ditësh</string>
+ <string name="username_or_password_incorrect">Emri i përdoruesit apo fjalëkalimi që dhatë është i pasaktë</string>
+ <string name="email_exists">Ajo adresë email është tashmë në përdorim</string>
+ <string name="error_delete_post">Ndodhi një gabim teksa fshihej %s</string>
+ <string name="invalid_url_message">Kontrolloni nëse URL-ja e dhënë është e vlefshme</string>
+ <string name="out_of_memory">Mbarim kujtese te pajisja</string>
+ <string name="no_network_message">S&amp;#8217;ka rrjet gati</string>
+ <string name="could_not_remove_account">S&amp;#8217;u hoq dot sajti</string>
+ <string name="gallery_error">S&amp;#8217;u mor dot objekti media</string>
+ <string name="comments_empty_list">S&amp;#8217;ka komente</string>
+ <string name="no_site_error">S&amp;#8217;u lidh dot te sajti WordPress</string>
+ <string name="category_automatically_renamed">Emri i kategorisë %1$s s&amp;#8217;është i vlefshëm. I riemërtua si %2$s.</string>
+ <string name="no_account">S&amp;#8217;u gjet llogari WordPress, shtoni një llogari dhe riprovoni</string>
+ <string name="stats_bar_graph_empty">S&amp;#8217;ka statistika gati</string>
+ <string name="notifications_empty_list">S&amp;#8217;ka njoftime</string>
+ <string name="error_refresh_posts">Postimet s&amp;#8217;u rifreskuan dot tani</string>
+ <string name="error_refresh_pages">Postimet s&amp;#8217;u rifreskuan dot tani</string>
+ <string name="error_refresh_notifications">Njoftimet s&amp;#8217;u rifreskuan dot tani</string>
+ <string name="error_refresh_comments">Komentet s&amp;#8217;u rifreskuan dot tani</string>
+ <string name="error_refresh_stats">Statistikat s&amp;#8217;u rifreskuan dot tani</string>
+ <string name="invalid_email_message">Adresa juaj email s&amp;#8217;është e vlefshme</string>
+ <string name="username_not_allowed">Emër përdoruesi që s&amp;#8217;lejohet</string>
+ <string name="username_contains_invalid_characters">Emri i përdoruesit s&amp;#8217;duhet të përmbajnë shenjën “_”</string>
+ <string name="email_not_allowed">Ajo adresë email s&amp;#8217;lejohet</string>
+ <string name="blog_name_not_allowed">Ajo adresë sajti s&amp;#8217;lejohet</string>
+ <string name="blog_name_contains_invalid_characters">Adresa e sajtit s&amp;#8217;duhet të përmbajnë shenjën “_”</string>
+ <string name="blog_name_cant_be_used">S&amp;#8217;mund të përdorni atë adresë sajti</string>
+ <string name="nux_cannot_log_in">S&amp;#8217;bëjmë dot hyrjen për ju</string>
+ <string name="select_categories">Përzgjidhni kategori</string>
+ <string name="account_details">Hollësi llogarie</string>
+ <string name="edit_post">Përpunojeni postimin</string>
+ <string name="add_comment">Shtoni koment</string>
+ <string name="connection_error">Gabim lidhjeje</string>
+ <string name="cancel_edit">Anuloni përpunimin</string>
+ <string name="scaled_image_error">Jepni një vlerë të vlefshme gjerësie të ripërmasuar</string>
+ <string name="learn_more">Mësoni më tepër</string>
+ <string name="media_gallery_settings_title">Rregullime galerie</string>
+ <string name="media_gallery_image_order">Renditje figurash</string>
+ <string name="media_gallery_num_columns">Numër shtyllash</string>
+ <string name="media_gallery_type_thumbnail_grid">Rrjetë miniaturash</string>
+ <string name="media_gallery_edit">Përpunoni galeri</string>
+ <string name="themes_live_preview">Paraparje &lt;em>Live&lt;/em></string>
+ <string name="theme_current_theme">Tema e tanishme</string>
+ <string name="theme_premium_theme">Temë me pagesë</string>
+ <string name="link_enter_url_text">Tekst lidhjeje (opsional)</string>
+ <string name="create_a_link">Krijoni një lidhje</string>
+ <string name="page_settings">Rregullime faqeje</string>
+ <string name="local_draft">Skicë vendore</string>
+ <string name="upload_failed">Ngarkimi dështoi</string>
+ <string name="horizontal_alignment">Drejtim horizontal</string>
+ <string name="post_settings">Rregullime postimi</string>
+ <string name="delete_post">Fshije postimin</string>
+ <string name="delete_page">Fshije faqen</string>
+ <string name="comment_status_approved">Të miratuar</string>
+ <string name="comment_status_unapproved">Në pritje</string>
+ <string name="comment_status_spam">I padëshiruar</string>
+ <string name="comment_status_trash">U hodh te hedhurinat</string>
+ <string name="edit_comment">Përpunoni komentin</string>
+ <string name="mnu_comment_approve">Miratoje</string>
+ <string name="mnu_comment_unapprove">Shmiratoje</string>
+ <string name="mnu_comment_spam">I padëshiruar</string>
+ <string name="mnu_comment_trash">Hedhurina</string>
+ <string name="dlg_approving_comments">Po miratohet</string>
+ <string name="dlg_spamming_comments">Po shënohet si i padëshiruar</string>
+ <string name="dlg_trashing_comments">Po kalohet te hedhurinat</string>
+ <string name="dlg_confirm_trash_comments">Të kalohet te hedhurinat?</string>
+ <string name="trash_yes">Hidhe te hedhurinat</string>
+ <string name="trash_no">Mos e hidh te hedhurinat</string>
+ <string name="trash">Hedhurina</string>
+ <string name="author_name">Emër autori</string>
+ <string name="author_email">Email autori</string>
+ <string name="author_url">URL autori</string>
+ <string name="hint_comment_content">Koment</string>
+ <string name="saving_changes">Po ruhen ndryshimet</string>
+ <string name="sure_to_cancel_edit_comment">Të anulohet përpunimi i këtij dokumenti?</string>
+ <string name="content_required">Komenti është i domosdoshëm</string>
+ <string name="remove_account">Hiqe sajtin</string>
+ <string name="blog_removed_successfully">Sajti u hoq me sukses</string>
+ <string name="delete_draft">Fshini skicën</string>
+ <string name="preview_page">Parashihni faqen</string>
+ <string name="preview_post">Parashihni postimin</string>
+ <string name="comment_added">Komenti u shtua me sukses</string>
+ <string name="view_in_browser">Shiheni në shfletues</string>
+ <string name="add_new_category">Shtoni kategori të re</string>
+ <string name="category_name">Emër kategorie</string>
+ <string name="category_slug">Identifikues kategorie (opsional)</string>
+ <string name="category_desc">Përshkrim kategorie (opsional)</string>
+ <string name="share_action_post">Postim i ri</string>
+ <string name="share_action_media">Bibliotekë media</string>
+ <string name="location_not_found">Vendndodhje e panjohur</string>
+ <string name="open_source_licenses">Licenca burimi të hapur</string>
+ <string name="pending_review">Në pritje të shqyrtimit</string>
+ <string name="post_format">Format postimesh</string>
+ <string name="new_post">Postim i ri</string>
+ <string name="new_media">Media e re</string>
+ <string name="view_site">Shiheni sajtin</string>
+ <string name="privacy_policy">Rregulla privatësie</string>
+ <string name="local_changes">Ndryshime vendore</string>
+ <string name="image_settings">Rregullime figurash</string>
+ <string name="add_account_blog_url">Adresë blogu</string>
+ <string name="wordpress_blog">Blog WordPress</string>
+ <string name="fatal_db_error">Ndodhi një gabim gjatë krijimit të bazës së të dhënave për aplikacionin. Provoni të riinstaloni aplikacionin.</string>
+ <string name="jetpack_message_not_admin">Për statistika lypset shtojca Jetpack. Lidhuni me përgjegjësin e sistemit.</string>
+ <string name="reader_title_applog">Regjistër aplikacioni</string>
+ <string name="reader_share_link">Ndani lidhjen me të tjerët</string>
+ <string name="reader_toast_err_add_tag">I pazoti të shtojë këtë etiketë</string>
+ <string name="reader_toast_err_remove_tag">I pazoti ta heqë këtë etiketë</string>
+ <string name="required_field">Fushë e domosdoshme</string>
+ <string name="email_hint">Adresë email</string>
+ <string name="site_address">Adresa për të vetëstrehuarin tuaj (URL)</string>
+ <string name="blog_name_must_include_letters">Adresa e sajtit duhet të ketë të paktën 1 shkronjë (a-z)</string>
+ <string name="blog_name_invalid">Adresë sajti e pavlefshme</string>
+ <string name="blog_title_invalid">Titull sajti i pavlefshëm</string>
+ <string name="post_not_found">Ndodhi një gabim teksa ngarkohej postimi. Rifreskoni postimet tuaja dhe riprovoni.</string>
+ <string name="dlg_unapproving_comments">Po shmiratohet</string>
+ <string name="invalid_site_url_message">Kontrolloni nëse URL-ja e sajtit të dhënë është e vlefshme</string>
+ <string name="xmlrpc_error">S&amp;#8217;u lidh dot. Jepni shtegun e plotë për te xmlrpc.php në sajtin tuaj dhe riprovoni.</string>
+ <string name="media_error_no_permission">S&amp;#8217;keni leje të shihni mediatekën</string>
+ <string name="cannot_delete_multi_media_items">Disa media s&amp;#8217;fshihen dot hëpërhë. Riprovoni më vonë.</string>
+ <string name="file_not_found">S&amp;#8217;u gjet dot kartela media për ngarkim. Mos është fshirë apo lëvizur?</string>
+ <string name="toast_comment_unedited">Komenti s&amp;#8217;ndryshoi</string>
+ <string name="post_not_published">Postimi s&amp;#8217;është i botuar</string>
+ <string name="page_not_published">Faqja s&amp;#8217;është e botuar</string>
+ <string name="category_parent">Prind kategorie (opsional):</string>
+ <string name="file_error_create">S&amp;#8217;u krijua dot kartelë e përkohshme për ngarkim medie. Sigurohuni që në pajisjen tuaj ka hapësirë të lirë të mjaftueshme.</string>
+ <string name="http_credentials">Kredenciale HTTP (opsional)</string>
+ <string name="http_authorization_required">Lypset autorizim</string>
+ <string name="notifications_empty_all">Ende pa njoftime.</string>
+ <string name="error_blog_hidden">Ky blog është i fshehur dhe s&amp;#8217;u ngarkua dot. Aktivizojeni sërish te rregullimet dhe riprovoni.</string>
+ <string name="email_cant_be_used_to_signup">S&amp;#8217;mund të përdorni për regjistrim atë adresë email. Kemi probleme me ta, se bllokojnë disa nga email-et tanë. Ju lutemi, përdorni një mundësues tjetër email-esh.</string>
+ <string name="email_reserved">Kjo adresë email është tashmë e përdorur. Kontrolloni email-et e marrë për një email aktivizimi. Nëse s&amp;#8217;e aktivizoni, mund të riprovoni pas pak ditësh.</string>
+ <string name="deleting_page">Po fshihet faqe</string>
+ <string name="deleting_post">Po fshihet postim</string>
+ <string name="share_url_post">Ndajeni postimin me të tjerët</string>
+ <string name="share_url_page">Ndajeni faqen me të tjerët</string>
+ <string name="share_link">Ndajeni lidhjen me të tjerët</string>
+ <string name="creating_your_account">Po krijohet llogaria juaj</string>
+ <string name="creating_your_site">Po krijohet sajti juaj</string>
+ <string name="reader_empty_posts_in_tag_updating">Po sillen postime…</string>
+ <string name="error_refresh_media">Diç shkoi ters teksa rifreskohej mediateka. Riprovoni më vonë.</string>
+ <string name="reader_likes_multi">Kjo u pëlqen %,d personave</string>
+ <string name="reader_toast_err_get_comment">I pazoti ta marrë këtë koment</string>
+ <string name="reader_label_reply">Përgjigjjuni</string>
+ <string name="video">Video</string>
+ <string name="comment_spammed">Komenti u shënua si i padëshiruar</string>
+ <string name="reader_likes_you_and_multi">Këtë e pëlqeni ju dhe %,d të tjerë</string>
+ <string name="download">Po shkarkohet media</string>
+ <string name="cant_share_no_visible_blog">S&amp;#8217;mund të ndani me të tjerët në WordPress pa një blog të dukshëm</string>
+ <string name="select_time">Përzgjidhni kohë</string>
+ <string name="reader_likes_you_and_one">Këtë e pëlqeni ju dhe një tjetër</string>
+ <string name="select_date">Përzgjidhni datë</string>
+ <string name="pick_photo">Përzgjidhni foto</string>
+ <string name="pick_video">Përzgjidhni video</string>
+ <string name="reader_toast_err_get_post">I pazoti të marrë këtë postim</string>
+ <string name="validating_user_data">Po vleftësohen të dhëna përdoruesi</string>
+ <string name="validating_site_data">Po vleftësohen të dhëna sajti</string>
+ <string name="reader_empty_followed_blogs_description">Por mos u shqetësoni, thjesht prekeni ikonën sipër djathtas që të filloni eksplorimin!</string>
+ <string name="account_two_step_auth_enabled">Kjo llogari e ka të aktivizuar mirëfilltësimin dyhapësh. Vizitoni rregullimet tuaja mbi sigurinë te WordPress.com dhe prodhoni një fjalëkalim vetëm për aplikacion.</string>
+ <string name="nux_tap_continue">Vazhdo</string>
+ <string name="nux_welcome_create_account">Krijoni llogari</string>
+ <string name="signing_in">Po hyhet…</string>
+ <string name="nux_add_selfhosted_blog">Shtoni sajt të vetëstrehuar</string>
+ <string name="password_invalid">Ju duhet një fjalëkalim më i sigurt. Sigurohuni se përdorni 7 shenja e sipër, shkronja të mëdha dhe të vogla, numra ose shenja speciale.</string>
+ <string name="nux_oops_not_selfhosted_blog">Futuni te WordPress.com</string>
+ <string name="media_add_popup_title">Shtojeni te mediateka</string>
+ <string name="media_add_new_media_gallery">Krijo galeri</string>
+ <string name="empty_list_default">Kjo listë është e zbrazët</string>
+ <string name="select_from_media_library">Përzgjidhni nga mediateka</string>
+ <string name="reader_untitled_post">(Pa titull)</string>
+ <string name="reader_share_subject">Ndarë nga %s</string>
+ <string name="reader_btn_share">Ndajeni me të tjerët</string>
+ <string name="reader_btn_follow">Ndiqe</string>
+ <string name="reader_hint_comment_on_comment">Përgjigjjuni komentit…</string>
+ <string name="reader_label_added_tag">U shtua %s</string>
+ <string name="reader_label_removed_tag">U hoq %s</string>
+ <string name="reader_likes_one">Kjo i pëlqen një personi</string>
+ <string name="reader_likes_only_you">E pëlqeni</string>
+ <string name="reader_toast_err_tag_exists">E ndiqni tashmë këtë etiketë</string>
+ <string name="create_account_wpcom">Krijoni një llogari te WordPress.com</string>
+ <string name="connecting_wpcom">Po lidhet te WordPress.com</string>
+ <string name="username_invalid">Emër përdoruesi i pavlefshëm</string>
+ <string name="nux_tutorial_get_started_title">Nisjani!</string>
+ <string name="jetpack_message">Për statistika lypset shtojca Jetpack? Doni të instalohet Jetpack-u?</string>
+ <string name="limit_reached">U mbërrit në kufi. Mund të riprovoni pas 1 minute. Riprovimi para kësaj vetëm sa do të shtojë kohën që ju duhet të prisni para se të hiqet përzënia. Nëse mendoni se ky është një gabim, lidhuni me asistencën.</string>
+ <string name="button_next">Pasuesi</string>
+ <string name="jetpack_not_found">S&amp;#8217;u gjet shtojcë Jetpack</string>
+ <string name="reader_btn_unfollow">Ndjekur prej jush</string>
+ <string name="reader_toast_err_comment_failed">S&amp;#8217;u postua dot komenti juaj</string>
+ <string name="reader_toast_err_tag_invalid">Kjo s&amp;#8217;është etiketë e vlefshme</string>
+ <string name="reader_toast_err_share_intent">S&amp;#8217;arrin të ndajë me të tjerët</string>
+ <string name="reader_toast_err_view_image">S&amp;#8217;arrin të shihet figura</string>
+ <string name="reader_toast_err_url_intent">S&amp;#8217;arrin të hapë %s</string>
+ <string name="reader_empty_followed_tags">S&amp;#8217;ndiqni ndonjë etiketë</string>
+ <string name="themes">Tema</string>
+ <string name="all">Krejt</string>
+ <string name="images">Figura</string>
+ <string name="unattached">Të pabashkëngjitura</string>
+ <string name="custom_date">Datë Vetjake</string>
+ <string name="media_add_popup_capture_photo">Bëni foto</string>
+ <string name="media_add_popup_capture_video">Regjistroni video</string>
+ <string name="media_gallery_image_order_random">Kuturu</string>
+ <string name="media_gallery_type">Lloj</string>
+ <string name="media_gallery_type_squares">Katrorë</string>
+ <string name="media_gallery_type_tiled">Tjegullëzuar</string>
+ <string name="media_gallery_type_circles">Rrathë</string>
+ <string name="media_gallery_type_slideshow">Diapozitiva</string>
+ <string name="media_edit_title_text">Titull</string>
+ <string name="media_edit_caption_text">Legjendë</string>
+ <string name="media_edit_description_text">Përshkrim</string>
+ <string name="media_edit_title_hint">Këtu jepni titull</string>
+ <string name="media_edit_caption_hint">Këtu jepni legjendë</string>
+ <string name="media_edit_description_hint">Këtu jepni përshkrim</string>
+ <string name="media_edit_success">U përditësua</string>
+ <string name="themes_details_label">Hollësi</string>
+ <string name="themes_features_label">Veçori</string>
+ <string name="theme_activate_button">Aktivizoje</string>
+ <string name="theme_activating_button">Po aktivizohet</string>
+ <string name="theme_set_success">Tema u caktua me sukses!</string>
+ <string name="theme_auth_error_title">Dështoi sjellja e temës</string>
+ <string name="post_excerpt">Copëz</string>
+ <string name="share_action_title">Shtojeni te …</string>
+ <string name="share_action">Ndajeni me të tjerët</string>
+ <string name="stats">Statistika</string>
+ <string name="stats_view_visitors_and_views">Vizitorë dhe Parje</string>
+ <string name="stats_view_clicks">Klikime</string>
+ <string name="stats_view_tags_and_categories">Etiketa &amp; Kategori</string>
+ <string name="stats_view_referrers">Referues</string>
+ <string name="stats_timeframe_today">Sot</string>
+ <string name="stats_timeframe_yesterday">Dje</string>
+ <string name="stats_timeframe_days">Ditë</string>
+ <string name="stats_timeframe_weeks">Javë</string>
+ <string name="stats_timeframe_months">Muaj</string>
+ <string name="stats_entry_country">Vend</string>
+ <string name="stats_entry_posts_and_pages">Titull</string>
+ <string name="stats_entry_tags_and_categories">Temë</string>
+ <string name="stats_entry_authors">Autor</string>
+ <string name="stats_entry_referrers">Referues</string>
+ <string name="stats_totals_views">Parje</string>
+ <string name="stats_totals_clicks">Klikime</string>
+ <string name="stats_totals_plays">Luajtje</string>
+ <string name="passcode_manage">Administroni kyçje PIN-i</string>
+ <string name="passcode_enter_old_passcode">Jepni PIN-in tuaj të vjetër</string>
+ <string name="passcode_re_enter_passcode">Rijepni PIN-in tuaj</string>
+ <string name="passcode_change_passcode">Ndërroni PIN-in</string>
+ <string name="passcode_preference_title">Kyçje PIN-i</string>
+ <string name="passcode_turn_off">Aktivizo kyçje PIN-i</string>
+ <string name="passcode_turn_on">Çaktivizo kyçje PIN-i</string>
+ <string name="media_gallery_image_order_reverse">Përmbyse</string>
+ <string name="passcode_set">PIN u caktua</string>
+ <string name="media_edit_failure">S&amp;#8217;u përditësua dot</string>
+ <string name="passcode_enter_passcode">Jepni PIN-in tuaj</string>
+ <string name="upload">Ngarkoje</string>
+ <string name="discard">Hidhe Tej</string>
+ <string name="sign_in">Hyni</string>
+ <string name="notifications">Njoftime</string>
+ <string name="follows">Ndjekje</string>
+ <string name="new_notifications">%d njoftime të reja</string>
+ <string name="more_notifications">dhe%d të tjera.</string>
+ <string name="note_reply_successful">Përgjigja u botua</string>
+ <string name="loading">Po ngarkohet…</string>
+ <string name="httpuser">Emër përdoruesi HTTP</string>
+ <string name="httppassword">Fjalëkalim HTTP</string>
+ <string name="error_media_upload">Ndodhi një gabim gjatë ngarkimit të medias</string>
+ <string name="post_content">Lëndë (prekeni që të shtoni tekst dhe media)</string>
+ <string name="publish_date">Botoje</string>
+ <string name="content_description_add_media">Shtoni media</string>
+ <string name="incorrect_credentials">Emër përdoruesi ose fjalëkalim i pasaktë.</string>
+ <string name="password">Fjalëkalim</string>
+ <string name="username">Emër përdoruesi</string>
+ <string name="reader">Lexues</string>
+ <string name="featured">Përdore si figurë të zgjedhur</string>
+ <string name="featured_in_post">Përfshi figura në lëndë postimi</string>
+ <string name="pages">Faqe</string>
+ <string name="caption">Përshkrim (në dëshirë)</string>
+ <string name="width">Gjerësi</string>
+ <string name="posts">Postime</string>
+ <string name="anonymous">I paemër</string>
+ <string name="page">Faqe</string>
+ <string name="post">Postim</string>
+ <string name="no_network_title">S&amp;#8217;ka rrjet gati</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">emërpërdoruesiblogu</string>
+ <string name="upload_scaled_image">Ngarkoje dhe lidhe te figura e ripërmasuar</string>
+ <string name="scaled_image">Gjerësi figure të ripërmasuar</string>
+ <string name="scheduled">E planifikuar</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Po ngarkohet…</string>
+ <string name="version">Version</string>
+ <string name="tos">Kushte Shërbimi</string>
+ <string name="app_title">WordPress për Android</string>
+ <string name="max_thumbnail_px_width">Gjerësi Parazgjedhje Figure</string>
+ <string name="image_alignment">Drejtim</string>
+ <string name="refresh">Rifresko</string>
+ <string name="untitled">Pa titull</string>
+ <string name="edit">Përpunojeni</string>
+ <string name="post_id">Postim</string>
+ <string name="page_id">Faqe</string>
+ <string name="post_password">Fjalëkalim (opsional)</string>
+ <string name="immediately">Menjëherë</string>
+ <string name="quickpress_add_alert_title">Caktoni emër shkurtoreje</string>
+ <string name="today">Sot</string>
+ <string name="settings">Rregullime</string>
+ <string name="share_url">Ndani URL-në me të tjerët</string>
+ <string name="quickpress_window_title">Përzgjidhni blog për shkurtore QuickPress</string>
+ <string name="quickpress_add_error">Emri i shkurtores s&amp;#8217;mund të jetë i zbrazët</string>
+ <string name="publish_post">Botoje</string>
+ <string name="draft">Skicë</string>
+ <string name="post_private">Private</string>
+ <string name="upload_full_size_image">Ngarkoje dhe lidhe te figura e plotë</string>
+ <string name="title">Titull</string>
+ <string name="tags_separate_with_commas">Etiketa (etiketat ndajini me presje)</string>
+ <string name="categories">Kategori</string>
+ <string name="dlg_deleting_comments">Po fshihen komente</string>
+ <string name="notification_blink">Xixëllo dritëzë njoftimesh</string>
+ <string name="notification_vibrate">Dridhu</string>
+ <string name="notification_sound">Tingull njoftimesh</string>
+ <string name="status">Gjendje</string>
+ <string name="location">Vendndodhje</string>
+ <string name="sdcard_title">Lypset Kartë SD</string>
+ <string name="select_video">Përzgjidhni një video te galeria</string>
+ <string name="media">Media</string>
+ <string name="delete">Fshije</string>
+ <string name="none">Asnjë</string>
+ <string name="blogs">Blogje</string>
+ <string name="select_photo">Përzgjidhni një foto te galeria</string>
+ <string name="error">Gabim</string>
+ <string name="cancel">Anuloje</string>
+ <string name="save">Ruaje</string>
+ <string name="add">Shtoni</string>
+ <string name="category_refresh_error">Gabim rifreskimi kategorish</string>
+ <string name="preview">Paraparje</string>
+ <string name="on">on</string>
+ <string name="reply">Përgjigjjuni</string>
+ <string name="yes">Po</string>
+ <string name="no">Jo</string>
+ <string name="notification_settings">Rregullime Njoftimesh</string>
+</resources>
diff --git a/WordPress/src/main/res/values-sr/strings.xml b/WordPress/src/main/res/values-sr/strings.xml
new file mode 100644
index 000000000..356a74598
--- /dev/null
+++ b/WordPress/src/main/res/values-sr/strings.xml
@@ -0,0 +1,677 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="error_open_list_from_notification">Чланак или страна је објављена на другом веб месту</string>
+ <string name="reader_short_comment_count_multi">%s коментара</string>
+ <string name="reader_short_comment_count_one">1 коментар</string>
+ <string name="reader_label_submit_comment">ПОШАЉИ</string>
+ <string name="reader_hint_comment_on_post">Одговор на чланак…</string>
+ <string name="reader_discover_visit_blog">Посети %s</string>
+ <string name="reader_discover_attribution_blog">Изворно објављено на %s</string>
+ <string name="reader_discover_attribution_author">Изворно објављено од %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Изворно објављено од %1$s %2$s</string>
+ <string name="reader_short_like_count_multi">%s свиђања</string>
+ <string name="reader_short_like_count_one">1 свиђање</string>
+ <string name="reader_short_like_count_none">Свиђање</string>
+ <string name="reader_label_follow_count">%,d пратиоца</string>
+ <string name="reader_menu_tags">Уређивање ознака и блогова</string>
+ <string name="reader_title_post_detail">Чланак читача</string>
+ <string name="local_draft_explainer">Овај чланак је локални нацрт који није објављен</string>
+ <string name="local_changes_explainer">Овај чланак има локалне измене које нису објављене</string>
+ <string name="notifications_push_summary">Подешавања за обавештења која се појављују на вашем уређају.</string>
+ <string name="notifications_email_summary">Подешавања за обавештења која су послата на е-пошту повезану са вашим налогом.</string>
+ <string name="notifications_tab_summary">Подешавања за обавештења која се појављују у језичку Обавештења.</string>
+ <string name="notifications_disabled">Обавештења апликације су онемогућена. Притисните овде да их омогућите у подешавањима.</string>
+ <string name="notification_types">Врсте обавештења</string>
+ <string name="error_loading_notifications">Није могуће учитати подешавања обавештења</string>
+ <string name="replies_to_your_comments">Одговори на ваше коментаре</string>
+ <string name="comment_likes">Свиђања коментара</string>
+ <string name="email">Е-пошта</string>
+ <string name="app_notifications">Обавештења апликације</string>
+ <string name="notifications_tab">Језичак обавештења</string>
+ <string name="notifications_comments_other_blogs">Коментари на другим веб местима</string>
+ <string name="notifications_wpcom_updates">WordPress.com ажурирања</string>
+ <string name="notifications_other">Друго</string>
+ <string name="notifications_account_emails">Е-пошта од WordPress.com</string>
+ <string name="notifications_account_emails_summary">Увек ћемо слати важну е-пошту везану за ваш налог, али такође можете да добијете неку додатну помоћ.</string>
+ <string name="your_sites">Ваша веб места</string>
+ <string name="notifications_sights_and_sounds">Слике и звуци</string>
+ <string name="stats_insights_latest_post_trend">Прошло је %1$s од када је %2$s је објављен. Ево какав је учинак чланка до сада…</string>
+ <string name="stats_insights_latest_post_summary">Резиме последњег чланка</string>
+ <string name="button_revert">Врати</string>
+ <string name="yesterday">Јуче</string>
+ <string name="days_ago">пре %d дана</string>
+ <string name="connectionbar_no_connection">Нема везе</string>
+ <string name="page_trashed">Страна је послата на отпад</string>
+ <string name="post_deleted">Чланак је обрисан</string>
+ <string name="post_trashed">Чланак је послат на отпад</string>
+ <string name="stats_no_activity_this_period">Нема активности за овај период</string>
+ <string name="trashed">На отпаду</string>
+ <string name="button_back">Назад</string>
+ <string name="page_deleted">Страна је обрисана</string>
+ <string name="button_stats">Статистике</string>
+ <string name="button_trash">Отпад</string>
+ <string name="button_preview">Предпреглед</string>
+ <string name="button_view">Поглед</string>
+ <string name="button_edit">Уређивање</string>
+ <string name="button_publish">Објављивање</string>
+ <string name="my_site_no_sites_view_subtitle">Да ли бисте желели да додате једно?</string>
+ <string name="my_site_no_sites_view_title">Још увек немате ниједно Вордпресово веб место.</string>
+ <string name="my_site_no_sites_view_drake">Илустрација</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Нисте овлашћени да приступите овом блогу</string>
+ <string name="reader_toast_err_follow_blog_not_found">Овај блог не може бити пронађен</string>
+ <string name="undo">Врати</string>
+ <string name="tabbar_accessibility_label_me">Ја</string>
+ <string name="tabbar_accessibility_label_my_site">Моје веб место</string>
+ <string name="passcodelock_prompt_message">Унесите свој PIN</string>
+ <string name="editor_toast_changes_saved">Измене су сачуване</string>
+ <string name="push_auth_expired">Захтев је истекао. Пријавите се на WordPress.com да бисте покушали поново.</string>
+ <string name="ignore">Занемари</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% прегледа</string>
+ <string name="stats_insights_best_ever">Најбољи погледи икада</string>
+ <string name="stats_insights_most_popular_hour">Најпопуларнији час</string>
+ <string name="stats_insights_most_popular_day">Најпопуларнији дан</string>
+ <string name="stats_insights_popular">Најпопуларнији дан и час</string>
+ <string name="stats_insights_today">Данашње статистике</string>
+ <string name="stats_insights_all_time">Чланци, погледи и посетиоци целог времена</string>
+ <string name="stats_insights">Увиди</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Да бисте видели своје статистике, пријавите се на WordPress.com налог који сте користили да се повежете са Џетпеком.</string>
+ <string name="stats_other_recent_stats_moved_label">Тражите своје друге скорашње статистике? Померили смо их на страницу Увиди.</string>
+ <string name="me_disconnect_from_wordpress_com">Прекини везу са WordPress.com</string>
+ <string name="me_btn_login_logout">Пријава/одјава</string>
+ <string name="me_connect_to_wordpress_com">Повежи са WordPress.com</string>
+ <string name="me_btn_support">Помоћ и подршка</string>
+ <string name="site_picker_cant_hide_current_site">"%s" није сакривен зато што је тренутно веб место</string>
+ <string name="site_picker_create_dotcom">Направи WordPress.com веб место</string>
+ <string name="site_picker_add_self_hosted">Додај самостално угошћено веб место</string>
+ <string name="site_picker_add_site">Додај веб место</string>
+ <string name="site_picker_edit_visibility">Прикажи/сакриј веб места</string>
+ <string name="my_site_btn_switch_site">Промени веб место</string>
+ <string name="my_site_btn_view_site">Види веб место</string>
+ <string name="site_picker_title">Одабери веб место</string>
+ <string name="my_site_btn_view_admin">Види управљање</string>
+ <string name="my_site_header_look_and_feel">Изглед и осећај</string>
+ <string name="my_site_header_publish">Објави</string>
+ <string name="my_site_btn_blog_posts">Чланци блога</string>
+ <string name="my_site_btn_site_settings">Подешавања</string>
+ <string name="my_site_header_configuration">Поставка</string>
+ <string name="reader_label_new_posts_subtitle">Притисните да бисте их показали</string>
+ <string name="notifications_account_required">Пријавите се на WordPress.com за пријаве</string>
+ <string name="stats_unknown_author">Непознат аутор</string>
+ <string name="image_added">Слика је додата</string>
+ <string name="signout">Прекини везу</string>
+ <string name="deselect_all">Одбаци све</string>
+ <string name="hide">Сакриј</string>
+ <string name="sign_out_wpcom_confirm">Прекидање везе са својим налогом ће уклонити све @%s’s WordPress.com податке са овог уређаја, укључујући локалне нацрте и локалне измене.</string>
+ <string name="select_all">Изабери све</string>
+ <string name="show">Прикажи</string>
+ <string name="select_from_new_picker">Изаберите више опција помоћу новог бирача</string>
+ <string name="stats_generic_error">Тражене статистике се не могу учитати</string>
+ <string name="no_media_sources">Није могуће добавити медије</string>
+ <string name="loading_blog_videos">Добављам видео снимке</string>
+ <string name="loading_blog_images">Добављам слике</string>
+ <string name="error_loading_videos">Грешка приликом учитавања видео снимака</string>
+ <string name="error_loading_images">Грешка приликом учитавања слика</string>
+ <string name="error_loading_blog_videos">Није могуће добавити видео снимке</string>
+ <string name="no_blog_videos">Нема видео снимака</string>
+ <string name="no_device_images">Нема слика</string>
+ <string name="error_loading_blog_images">Није могуће добавити слике</string>
+ <string name="no_blog_images">Нема слика</string>
+ <string name="no_device_videos">Нема видео снимака</string>
+ <string name="no_media">Не постоје медији</string>
+ <string name="loading_images">Учитавам слике</string>
+ <string name="loading_videos">Учитавам видео снимке</string>
+ <string name="sign_in_jetpack">Пријавите се на свој WordPress.com налог да бисте се повезали на Џетпек.</string>
+ <string name="two_step_sms_sent">Потражите верификациони код међу вашим текстуалним порукама.</string>
+ <string name="auth_required">Пријавите се поново како бисте наставили.</string>
+ <string name="two_step_footer_button">Пошаљите код путем текстуалне поруке</string>
+ <string name="two_step_footer_label">Унесите код са своје апликације за аутентикацију.</string>
+ <string name="verify">Провери</string>
+ <string name="invalid_verification_code">Погрешан верификациони код</string>
+ <string name="verification_code">Код за потврђивање</string>
+ <string name="editor_toast_invalid_path">Неисправна путања до датотеке</string>
+ <string name="error_publish_no_network">Није могуће објавити услед недостатка интернет везе. Сачувано као нацрт.</string>
+ <string name="tab_title_site_videos">Видео снимци са веб места</string>
+ <string name="tab_title_site_images">Слике са веб места</string>
+ <string name="tab_title_device_videos">Видео снимци на уређају</string>
+ <string name="tab_title_device_images">Слике на уређају</string>
+ <string name="take_video">Направи видео снимак</string>
+ <string name="take_photo">Направи фотографију</string>
+ <string name="media_picker_title">Изабери медије</string>
+ <string name="add_to_post">Додајте у чланак</string>
+ <string name="language">Језик</string>
+ <string name="device">Уређај</string>
+ <string name="reader_empty_posts_request_failed">Није могуће добавити чланке</string>
+ <string name="pages_fetching">Добављам стране...</string>
+ <string name="stats_empty_search_terms_desc">Сазнајте више о посетама са веб претраживача прегледом термина које су посетиоци користили да пронађу ваше веб место.</string>
+ <string name="media_fetching">Добављам медије...</string>
+ <string name="posts_fetching">Добављам чланке...</string>
+ <string name="toast_err_post_uploading">Није могуће отворити чланак док се отпремљује</string>
+ <string name="stats_view_search_terms">Термини за претрагу</string>
+ <string name="comments_fetching">Добављам коментаре...</string>
+ <string name="stats_view_authors">Аутори</string>
+ <string name="stats_entry_search_terms">Термин за претрагу</string>
+ <string name="stats_empty_search_terms">Нема забележених термина за претрагу</string>
+ <string name="stats_followers_total_wpcom_paged">Приказано %1$d - %2$d од %3$s пратилаца преко Wordpress.com</string>
+ <string name="stats_search_terms_unknown_search_terms">Непознати термини за претрагу</string>
+ <string name="stats_followers_total_email_paged">Приказано %1$d - %2$d од %3$s пратилаца преко е-поште</string>
+ <string name="error_notification_open">Није могуће отворити обавештење</string>
+ <string name="publisher">Објављивач:</string>
+ <string name="stats_months_and_years">Месеци и године</string>
+ <string name="error_copy_to_clipboard">Дошло је до грешке приликом копирања текста у оставу</string>
+ <string name="stats_average_per_day">Дневни просек</string>
+ <string name="stats_recent_weeks">Претходних седмица</string>
+ <string name="reader_label_new_posts">Нови чланци</string>
+ <string name="reader_empty_posts_in_blog">Овај блог је празан</string>
+ <string name="logs_copied_to_clipboard">Извештај о променама у апликацији је копиран у оставу</string>
+ <string name="stats_period">Период</string>
+ <string name="stats_overall">Просек</string>
+ <string name="stats_total">Укупно</string>
+ <string name="post_uploading">Отпремљујем</string>
+ <string name="reader_page_recommended_blogs">Веб места која би могла да вам се свиде</string>
+ <string name="stats_empty_referrers_desc">Сазнајте више о видљивости свог веб места пратећи веб претраживаче и друга веб места која вам шаљу највише посетилаца</string>
+ <string name="stats_empty_publicize_desc">Пратите своје пратиоце са разних друштвених мрежа користећи Publicize.</string>
+ <string name="stats_empty_publicize">Нема забележених Publicize пратилаца</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_empty_referrers_title">Нема забележених извора упућивања</string>
+ <string name="stats_empty_clicks_desc">Када ваш садржај укључује везе ка другим веб местима, видећете које од њих ваши посетиоци највише притискају.</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_pagination_label">Страна %1$s од %2$s</string>
+ <string name="stats_view_top_posts_and_pages">Чланци и стране</string>
+ <string name="stats_empty_top_posts_desc">Сазнајте који ваш садржај има највише прегледа и проверите учинак појединих чланака и страна.</string>
+ <string name="stats_empty_tags_and_categories">Нема прегледа означених чланака или страна</string>
+ <string name="stats_empty_top_posts_title">Нема прегледаних страна или чланака</string>
+ <string name="stats_comments_by_posts_and_pages">По странама и чланцима</string>
+ <string name="stats_empty_geoviews_desc">Прегледајте списак како бисте сазнали из којих држава и региона долази највише посетилаца на ваше веб место.</string>
+ <string name="stats_empty_tags_and_categories_desc">Прегледајте које су најпопуларније теме на вашем веб месту према томе који су били најпопуларнији чланци прошле седмице.</string>
+ <string name="stats_empty_followers_desc">Пратите колико у просеку имате пратилаца, као и колико дуго сваки од њих прати ваше веб место.</string>
+ <string name="stats_empty_video_desc">Уколико сте објавили видео снимке користећи Видеопрес, погледајте колико су пута били прегледани.</string>
+ <string name="stats_visitors">Посетиоци</string>
+ <string name="stats_views">Прегледа</string>
+ <string name="stats_timeframe_years">Година</string>
+ <string name="stats_likes">Свиђања</string>
+ <string name="stats_view_countries">Државе</string>
+ <string name="stats_view_followers">Пратиоци</string>
+ <string name="stats_entry_clicks_link">Веза</string>
+ <string name="stats_view_videos">Видео снимци</string>
+ <string name="stats_totals_publicize">Пратиоци</string>
+ <string name="stats_entry_publicize">Сервис</string>
+ <string name="stats_entry_followers">Пратилац</string>
+ <string name="stats_entry_top_commenter">Аутор</string>
+ <string name="stats_entry_video_plays">Видео снимак</string>
+ <string name="stats_empty_geoviews">Нема забележених држава</string>
+ <string name="stats_totals_followers">Од</string>
+ <string name="stats_empty_clicks_title">Нема забележених притисака</string>
+ <string name="stats_empty_top_authors_desc">Пратите прегледе чланака сваког од сарадника и погледајте детаље како бисте сазнали који је најпопуларнији садржај сваког од аутора.</string>
+ <string name="stats_empty_video">Нема прегледаних видео снимака</string>
+ <string name="stats_empty_comments_desc">Ако сте дозволили коментаре на свом веб месту, пратите посетиоце који остављају највише коментара и сазнајте који садржај подстиче најактивније разговоре, засновано на последњих 1000 коментара.</string>
+ <string name="stats_empty_followers">Без пратилаца</string>
+ <string name="stats_comments_by_authors">По ауторима</string>
+ <string name="stats_followers_total_wpcom">Укупан број WordPress.com пратилаца: %1$s</string>
+ <string name="stats_followers_email_selector">Е-пошта</string>
+ <string name="stats_followers_total_email">Укупан број пратилаца преко е-поште: %1$s</string>
+ <string name="stats_followers_seconds_ago">пре пар секунди</string>
+ <string name="stats_followers_a_minute_ago">пре минут</string>
+ <string name="stats_followers_days">%1$d дана</string>
+ <string name="stats_followers_a_day">Дан</string>
+ <string name="stats_followers_hours">%1$d сати</string>
+ <string name="stats_followers_an_hour_ago">пре сат времена</string>
+ <string name="stats_followers_minutes">%1$d минута</string>
+ <string name="stats_followers_a_month">Месец</string>
+ <string name="stats_followers_years">%1$d година</string>
+ <string name="stats_followers_a_year">Година</string>
+ <string name="stats_view_all">Прикажи све</string>
+ <string name="stats_view">Прикажи</string>
+ <string name="stats_followers_months">%1$d месеци</string>
+ <string name="stats_for">Статистике за %s</string>
+ <string name="stats_other_recent_stats_label">Друге недавне статистике</string>
+ <string name="themes_fetching">Добављам теме...</string>
+ <string name="stats_comments_total_comments_followers">Укупан број чланака са пратиоцима коментара: %1$s</string>
+ <string name="ssl_certificate_details">Детаљи</string>
+ <string name="delete_sure_page">Обриши ову страну</string>
+ <string name="media_gallery_date_range">Приказ медија од %1$s до %2$s</string>
+ <string name="cab_selected">%d изабрано</string>
+ <string name="confirm_delete_media">Обрисати изабрану ставку?</string>
+ <string name="confirm_delete_multi_media">Обрисати изабране ставке?</string>
+ <string name="delete_sure">Обриши овај нацрт</string>
+ <string name="delete_sure_post">Обриши овај чланак</string>
+ <string name="sure_to_remove_account">Уклонити ово веб место?</string>
+ <string name="browse_our_faq_button">Прегледајте наша ЧПП</string>
+ <string name="agree_terms_of_service">Прављењем налога прихватате фасцинантне %1$sУслове сервиса%2$s</string>
+ <string name="faq_button">ЧПП</string>
+ <string name="pages_empty_list">Не постоји ниједна страна. Зашто не бисте направили нову?</string>
+ <string name="reader_empty_posts_liked">Нисте означили да вам се свиђа ниједан чланак</string>
+ <string name="error_refresh_unauthorized_pages">Немате дозволу да прегледате или уређујете стране</string>
+ <string name="signing_out">Одјављивање...</string>
+ <string name="posting_post">Објављивање "%s"</string>
+ <string name="media_empty_list_custom_date">Не постоје медији у том временском периоду</string>
+ <string name="comment_reply_to_user">Одговор %s</string>
+ <string name="comment_trashed">Коментар померен на отпад</string>
+ <string name="posts_empty_list">Не постоје чланци. Зашто не бисте направили нови?</string>
+ <string name="comment">Коментар</string>
+ <string name="mnu_comment_liked">Свиђа се</string>
+ <string name="uploading_total">Отпремљивање %1$d од %2$d</string>
+ <string name="sending_content">Отпремљивање %s садржаја</string>
+ <string name="select_a_blog">Одаберите Вордпрес веб место</string>
+ <string name="stats_no_blog">Статистике не могу бити учитане за тражени блог</string>
+ <string name="older_two_days">Старије од два дана</string>
+ <string name="older_last_week">Старије од недељу дана</string>
+ <string name="older_month">Старије од месец дана</string>
+ <string name="more">Више</string>
+ <string name="error_refresh_unauthorized_comments">Немате дозволу да прегледате или уређујете коментаре</string>
+ <string name="error_refresh_unauthorized_posts">Немате дозволу да прегледате или уређујете чланке</string>
+ <string name="error_publish_empty_post">Није могуће објавити празан чланак</string>
+ <string name="reader_title_photo_viewer">%1$d од %2$d</string>
+ <string name="reader_label_comments_on">Коментарисање је отворено</string>
+ <string name="reader_label_comments_closed">Коментарисање је затворено</string>
+ <string name="reader_label_comment_count_single">Један коментар</string>
+ <string name="reader_label_like">Свиђа ми се</string>
+ <string name="reader_label_view_original">Прикажи изворни чланак</string>
+ <string name="reader_label_comment_count_multi">%,d коментара</string>
+ <string name="reader_empty_posts_in_tag">Нема чланака са том ознаком</string>
+ <string name="reader_empty_comments">Нема коментара</string>
+ <string name="new_blog_wpcom_created">Блог на WordPress.com је направљен!</string>
+ <string name="create_new_blog_wpcom">Направите блог на WordPress.com</string>
+ <string name="nux_help_description">Посетите центар за помоћ како бисте пронашли одговоре на честа питања или посетите форум и поставите питање</string>
+ <string name="reader_empty_followed_blogs_title">Још увек не пратите ниједно веб место</string>
+ <string name="reader_toast_err_generic">Није могуће обавити ову радњу</string>
+ <string name="reader_menu_block_blog">Блокирај овај блог</string>
+ <string name="reader_toast_blog_blocked">Чланци са овог блога више неће бити приказивани</string>
+ <string name="reader_toast_err_block_blog">Није могуће блокирати овај блог</string>
+ <string name="hs__username_blank_error">Унесите исправно име</string>
+ <string name="hs__invalid_email_error">Унесите исправну адресу е-поште</string>
+ <string name="hs__conversation_detail_error">Опишите проблем који видите</string>
+ <string name="hs__new_conversation_header">Ћаскање са подршком</string>
+ <string name="hs__conversation_header">Ћаскање са подршком</string>
+ <string name="contact_us">Контактирајте нас</string>
+ <string name="add_location">Додајте локацију</string>
+ <string name="current_location">Тренутна локација</string>
+ <string name="search_current_location">Лоцирај</string>
+ <string name="edit_location">Уреди</string>
+ <string name="search_location">Претрага</string>
+ <string name="preference_send_usage_stats_summary">Аутоматски пошаљи статистике ради унапређења Вордпреса за Андроид</string>
+ <string name="preference_send_usage_stats">Пошаљи статистике</string>
+ <string name="update_verb">Ажурирај</string>
+ <string name="schedule_verb">Заказати</string>
+ <string name="reader_empty_recommended_blogs">Нема препоручених блогова</string>
+ <string name="reader_toast_err_unfollow_blog">Није могуће зауставити праћење овог блога</string>
+ <string name="reader_toast_err_follow_blog">Није могуће пратити овај блог</string>
+ <string name="reader_toast_err_already_follow_blog">Већ пратите овај блог</string>
+ <string name="reader_toast_err_get_blog_info">Није могуће приказати овај блог</string>
+ <string name="reader_label_tag_preview">Чланци означени са %s</string>
+ <string name="reader_label_followed_blog">Блог се прати</string>
+ <string name="reader_title_subs">Ознаке и блогови</string>
+ <string name="reader_page_followed_tags">Ознаке које пратите</string>
+ <string name="reader_title_tag_preview">Ознака читача</string>
+ <string name="reader_title_blog_preview">Блог читача</string>
+ <string name="reader_page_followed_blogs">Веб места која пратите</string>
+ <string name="ptr_tip_message">Савет: Повуците надоле да бисте освежили</string>
+ <string name="saving">Чувам...</string>
+ <string name="media_empty_list">Нема медија</string>
+ <string name="ssl_certificate_error">Неисправна SSL потврда</string>
+ <string name="help_center">Центар за помоћ</string>
+ <string name="forums">Форуми</string>
+ <string name="forgot_password">Изгубили сте лозинку?</string>
+ <string name="help">Помоћ</string>
+ <string name="ssl_certificate_ask_trust">Уколико се уобичајено на ово веб место повезујете без проблема, ова грешка може да значи да неко други покушава да се представи као ваше веб место и у том случају не би требало да наставите. Желите ли ипак да верујете овој потврди?</string>
+ <string name="mnu_comment_unspam">Није непожељен коментар</string>
+ <string name="error_refresh_posts">Чланци тренутно не могу бити освежени</string>
+ <string name="username_only_lowercase_letters_and_numbers">Корисничко име може да садржи само мала слова енглеске латинице (a-z) и бројеве</string>
+ <string name="sdcard_message">Уграђена SD картица је неопходна за отпремање медија</string>
+ <string name="username_must_include_letters">Корисничко име мора имати најмање једно слово енглеске латинице (a-z)</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Адреса веб места може да се састоји само од малих слова енглеске латинице (a-z) и бројева</string>
+ <string name="nux_cannot_log_in">Не можемо вас пријавити</string>
+ <string name="username_or_password_incorrect">Неисправно корисничко име или лозинка</string>
+ <string name="blog_name_reserved_but_may_be_available">То веб место је тренутно резервисано али ће можда бити доступно за пар дана</string>
+ <string name="blog_name_reserved">То веб место је резервисано</string>
+ <string name="blog_name_exists">То веб место већ постоји</string>
+ <string name="blog_name_cant_be_used">Не смете употребити ту адресу веб места</string>
+ <string name="blog_name_contains_invalid_characters">Адреса веб места не сме да садржи знак "_"</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Адреса веб места мора бити дужине мање од 64 знака</string>
+ <string name="blog_name_must_be_at_least_four_characters">Адреса веб места мора бити дужине најмање 4 знака</string>
+ <string name="blog_name_not_allowed">Та адреса веб места није дозвољена</string>
+ <string name="blog_name_required">Унесите адресу веб места</string>
+ <string name="email_not_allowed">Та адреса е-поште није дозвољена</string>
+ <string name="username_exists">То корисничко име већ постоји</string>
+ <string name="email_exists">Та адреса е-поште је већ у употреби</string>
+ <string name="username_reserved_but_may_be_available">То корисничко име је тренутно резервисано али ће можда бити слободно за пар дана</string>
+ <string name="email_invalid">Унесите исправну адресу е-поште</string>
+ <string name="username_contains_invalid_characters">Корисничко име не сме да садржи знак "_"</string>
+ <string name="username_not_allowed">Недозвољено корисничко име</string>
+ <string name="username_must_be_at_least_four_characters">Корисничко име мора садржати бар 4 знака</string>
+ <string name="username_required">Унесите корисничко име</string>
+ <string name="invalid_username_too_long">Корисничко име мора бити краће од 61 знакова</string>
+ <string name="invalid_username_too_short">Корисничко име мора бити дуже од 4 знака</string>
+ <string name="invalid_email_message">Ваша адреса е-поште није исправна</string>
+ <string name="invalid_password_message">Лозинка мора да садржи најмање 4 знака</string>
+ <string name="passcode_wrong_passcode">Погрешан ПИН</string>
+ <string name="error_downloading_image">Грешка приликом преузимања слике</string>
+ <string name="error_load_comment">Није могуће учитати коментар</string>
+ <string name="error_upload">Дошло је до грешке током отпремања %s</string>
+ <string name="error_edit_comment">Дошло је до грешке током уређивања коментара</string>
+ <string name="error_moderate_comment">Дошло је до грешке током уређивања</string>
+ <string name="error_refresh_stats">Статистике тренутно не могу бити освежене</string>
+ <string name="error_generic">Дошло је до грешке</string>
+ <string name="error_refresh_comments">Коментари тренутно не могу бити освежени</string>
+ <string name="error_refresh_notifications">Обавештења тренутно не могу бити освежена</string>
+ <string name="error_delete_post">Дошло је до грешке током брисања %s</string>
+ <string name="error_refresh_pages">Стране тренутно не могу бити освежене</string>
+ <string name="comments_empty_list">Нема коментара</string>
+ <string name="theme_auth_error_message">Потврдите да имате дозволу да постављате теме</string>
+ <string name="theme_set_failed">Неуспешно постављање теме</string>
+ <string name="theme_fetch_failed">Неуспешно добављање тема</string>
+ <string name="wait_until_upload_completes">Сачекајте да се отпремање заврши</string>
+ <string name="blog_not_found">Дошло је до грешке током приступања овом блогу</string>
+ <string name="gallery_error">Медијска ставка не може бити повраћена</string>
+ <string name="no_network_message">Нема доступне мреже</string>
+ <string name="out_of_memory">Уређај нема доступне меморије</string>
+ <string name="notifications_empty_list">Нема обавештења</string>
+ <string name="reply_failed">Одговарање неуспешно</string>
+ <string name="stats_bar_graph_empty">Нема статистика</string>
+ <string name="stats_empty_comments">Нема коментара</string>
+ <string name="no_site_error">Није могуће повезати се на Вордпрес веб место</string>
+ <string name="no_account">Није пронађен Вордпрес налог. Додајте налог и покушајте поново</string>
+ <string name="category_automatically_renamed">Име категорије %1$s није исправно. Преименована је у %2$s.</string>
+ <string name="cat_name_required">Име категорије је неопходно</string>
+ <string name="adding_cat_success">Категорија успешно додата</string>
+ <string name="adding_cat_failed">Додавање категорије неуспешно</string>
+ <string name="could_not_remove_account">Није могуће уклонити веб место</string>
+ <string name="view_site">Погледај веб место</string>
+ <string name="site_address">Адреса блога (URL)</string>
+ <string name="page_not_published">Стање стране није објављено</string>
+ <string name="post_not_found">Дошло је до грешке током учитавања чланка. Освежите списак чланака и пробајте поново.</string>
+ <string name="comment_status_spam">Непожељни коментар</string>
+ <string name="media_gallery_num_columns">Број ступаца</string>
+ <string name="media_gallery_settings_title">Подешавања галерије</string>
+ <string name="dlg_spamming_comments">Означавам као непожељно</string>
+ <string name="mnu_comment_spam">Непожељни коментар</string>
+ <string name="post_format">Облик чланка</string>
+ <string name="horizontal_alignment">Водоравно поравнање</string>
+ <string name="fatal_db_error">Дошло је до грешке током прављења базе података апликације. Покушајте поново са инсталацијом апликације.</string>
+ <string name="reader_title_applog">Белешка апликације</string>
+ <string name="view_in_browser">Погледајте у прегледачу веба</string>
+ <string name="file_error_create">Није могуће направити привремену датотеку за отпремање медија. Потврдите да имате довољно слободног простора за складиштење на свом уређају.</string>
+ <string name="email_cant_be_used_to_signup">Не можете користити ту адресу е-поште да бисте отворили налог. Имамо проблема са тим добављачем јер блокирају неку нашу е-пошту. Употребите адресу е-поште другог добављача.</string>
+ <string name="blog_name_must_include_letters">Адреса веб места мора имати најмање једно слово енглеске латинице (a-z)</string>
+ <string name="scaled_image_error">Унесите исправну вредност сразмерне ширине</string>
+ <string name="blog_title_invalid">Неисправан наслов веб места</string>
+ <string name="blog_name_invalid">Неисправна адреса веб места</string>
+ <string name="email_hint">Адреса е-поште</string>
+ <string name="category_parent">Надређена категорија (опционо):</string>
+ <string name="share_action_post">Нови чланак</string>
+ <string name="category_desc">Опис категорије (опционо)</string>
+ <string name="category_slug">Подложак категорије (опционо)</string>
+ <string name="add_new_category">Додај нову категорију</string>
+ <string name="category_name">Име категорије</string>
+ <string name="post_not_published">Стање чланка није објављено</string>
+ <string name="comment_added">Коментар успешно додат</string>
+ <string name="required_field">Неопходно поље</string>
+ <string name="reader_toast_err_remove_tag">Није могуће уклонити ову ознаку</string>
+ <string name="reader_toast_err_add_tag">Није могуће додати ову ознаку</string>
+ <string name="reader_share_link">Подели везу</string>
+ <string name="jetpack_message_not_admin">Џетпек додатак је неопходан ѕа статистике. Контактирајте управника веб места.</string>
+ <string name="wordpress_blog">Вордпрес блог</string>
+ <string name="error_blog_hidden">Овај блог је сакривен и не може се учитати. У подешавањима поново омогућите да буде видљив и пробајте поново.</string>
+ <string name="add_account_blog_url">Адреса блога</string>
+ <string name="open_source_licenses">Дозволе Отвореног кода</string>
+ <string name="location_not_found">Непозната локација</string>
+ <string name="post_settings">Подешавања чланка</string>
+ <string name="local_draft">Локални нацрт</string>
+ <string name="upload_failed">Отпремање неуспешно</string>
+ <string name="file_not_found">Није могуће пронаћи медијску датотеку за отпремање. Да ли је можда обрисана или померена?</string>
+ <string name="create_a_link">Направите везу</string>
+ <string name="link_enter_url_text">Текст везе (опционо)</string>
+ <string name="cannot_delete_multi_media_items">Неки медији тренутно не могу бити обрисани. Пробајте поново касније.</string>
+ <string name="themes_live_preview">Преглед уживо</string>
+ <string name="theme_current_theme">Тренутна тема</string>
+ <string name="theme_premium_theme">Премијум тема</string>
+ <string name="image_settings">Подешавања слике</string>
+ <string name="local_changes">Локалне измене</string>
+ <string name="privacy_policy">Полиса приватности</string>
+ <string name="new_media">Нови медиј</string>
+ <string name="new_post">Нови чланак</string>
+ <string name="http_authorization_required">Неопходно овлашћење</string>
+ <string name="http_credentials">HTTP акредитиви (опционо)</string>
+ <string name="pending_review">Чека преглед</string>
+ <string name="preview_post">Прегледајте чланак</string>
+ <string name="delete_draft">Обриши нацрт</string>
+ <string name="toast_comment_unedited">Коментар није промењен</string>
+ <string name="content_required">Коментар је неопходан</string>
+ <string name="sure_to_cancel_edit_comment">Одустајете од уређивања овог коментара?</string>
+ <string name="saving_changes">Чувам измене</string>
+ <string name="hint_comment_content">Коментар</string>
+ <string name="author_url">URL адреса аутора</string>
+ <string name="author_email">Е-пошта аутора</string>
+ <string name="trash">Отпад</string>
+ <string name="author_name">Име аутора</string>
+ <string name="trash_no">Не шаљи на отпад</string>
+ <string name="delete_post">Обриши чланак</string>
+ <string name="share_action_media">Библиотека медија</string>
+ <string name="media_error_no_permission">Немате дозволу да прегледате библиотеку медија</string>
+ <string name="media_gallery_edit">Уредите галерију</string>
+ <string name="xmlrpc_error">Није могуће повезати се. Унесите пуну путању до xmlrpc.php на свом веб месту и покушајте поново.</string>
+ <string name="email_reserved">Та адреса веб-поште је већ у употреби. Проверите своје поштанско сандуче за поруку за активацију. Уколико не активирате, постаће доступна за неколико дана.</string>
+ <string name="trash_yes">Отпад</string>
+ <string name="dlg_confirm_trash_comments">Помери на отпад?</string>
+ <string name="dlg_trashing_comments">Померам на отпад</string>
+ <string name="dlg_unapproving_comments">Поништавам одобрење</string>
+ <string name="dlg_approving_comments">Одобравам</string>
+ <string name="mnu_comment_trash">Отпад</string>
+ <string name="mnu_comment_unapprove">Поништи одобрење</string>
+ <string name="mnu_comment_approve">Одобри</string>
+ <string name="edit_comment">Уредите коментар</string>
+ <string name="comment_status_trash">У отпаду</string>
+ <string name="comment_status_unapproved">На чекању</string>
+ <string name="comment_status_approved">Одобрено</string>
+ <string name="media_gallery_type_thumbnail_grid">Мрежа умањених слика</string>
+ <string name="media_gallery_image_order">Редослед слика</string>
+ <string name="learn_more">Сазнајте више</string>
+ <string name="cancel_edit">Одустани од уређивања</string>
+ <string name="connection_error">Грешка у успостављању везе</string>
+ <string name="add_comment">Додајте коментар</string>
+ <string name="edit_post">Уредите чланак</string>
+ <string name="account_details">Детаљи налога</string>
+ <string name="delete_page">Обришите страну</string>
+ <string name="preview_page">Прегледајте страну</string>
+ <string name="page_settings">Подешавања стране</string>
+ <string name="select_categories">Одаберите категорије</string>
+ <string name="remove_account">Уклони веб место</string>
+ <string name="blog_removed_successfully">Веб место је успешно уклоњено</string>
+ <string name="share_link">Подели везу</string>
+ <string name="share_url_post">Поделите чланак</string>
+ <string name="deleting_post">Брисање чланка</string>
+ <string name="deleting_page">Брисање стране</string>
+ <string name="share_url_page">Поделите страну</string>
+ <string name="creating_your_site">Прављење веб места</string>
+ <string name="creating_your_account">Прави се ваш налог</string>
+ <string name="error_refresh_media">Дошло је до грешке током освеживања библиотеке медија. Покушајте касније.</string>
+ <string name="reader_empty_posts_in_tag_updating">Добављам чланке...</string>
+ <string name="reader_likes_you_and_multi">Вама и %,d других се ово свиђа</string>
+ <string name="reader_likes_multi">%,d људи се ово свиђа</string>
+ <string name="cant_share_no_visible_blog">Не можете делити на Вордпрес без видљивог блога</string>
+ <string name="download">Преузимање медија</string>
+ <string name="video">Видео снимак</string>
+ <string name="reader_label_reply">Одговор</string>
+ <string name="reader_toast_err_get_comment">Није могуће добавити овај коментар</string>
+ <string name="comment_spammed">Коментар означен као непожељан</string>
+ <string name="validating_site_data">Проверавам исправност података веб места</string>
+ <string name="validating_user_data">Проверавам исправност корисничких података</string>
+ <string name="account_two_step_auth_enabled">Овај налог има омогућену потврду идентитета у два корака. Посетите своја подешавања сигурности на WordPress.com и направите посебну лозинку за ову апликацију.</string>
+ <string name="reader_likes_you_and_one">Вама и још некоме се ово свиђа</string>
+ <string name="reader_toast_err_get_post">Није могуће добавити овај чланак</string>
+ <string name="select_time">Одаберите време</string>
+ <string name="select_date">Одаберите датум</string>
+ <string name="pick_photo">Одаберите фотографију</string>
+ <string name="pick_video">Одаберите видео снимак</string>
+ <string name="reader_empty_followed_blogs_description">Не брините, потребно је само да додирнете иконицу горе десно да бисте започели истраживање!</string>
+ <string name="nux_welcome_create_account">Направи налог</string>
+ <string name="nux_add_selfhosted_blog">Додај самостално угошћено веб место</string>
+ <string name="password_invalid">Потребна вам је сигурнија лозинка. Користите 7 или више знакова, употребите и велика и мала слова, бројеве и посебне знакове.</string>
+ <string name="nux_oops_not_selfhosted_blog">Пријавите се на WordPress.com</string>
+ <string name="signing_in">Пријављивање...</string>
+ <string name="nux_tap_continue">Настави</string>
+ <string name="reader_empty_followed_tags">Не пратите ниједну ознаку</string>
+ <string name="limit_reached">Ограничење достигнуто. Пробајте поново за 1 минут. Покушавање пре тога ће само повећати време које ћете морати да сачекате пре него што забрана истекне. Уколико мислите да је ово грешка, контактирајте службу за подршку.</string>
+ <string name="empty_list_default">Овај списак је празан</string>
+ <string name="media_add_new_media_gallery">Направи галерију</string>
+ <string name="nux_tutorial_get_started_title">Почните!</string>
+ <string name="username_invalid">Неисправно корисничко име</string>
+ <string name="connecting_wpcom">Повезивање на WordPress.com</string>
+ <string name="create_account_wpcom">Направите налог на WordPress.com</string>
+ <string name="reader_toast_err_url_intent">Није могуће отворити %s</string>
+ <string name="reader_toast_err_view_image">Није могуће прегледати слику</string>
+ <string name="reader_btn_share">Поделите</string>
+ <string name="reader_share_subject">Подељено од стране %s</string>
+ <string name="reader_untitled_post">(Без наслова)</string>
+ <string name="jetpack_not_found">Додатак Џетпек није пронађен</string>
+ <string name="jetpack_message">Џетпек додатак је неопходан за статистике. Да ли желите да инсталирате Џетпек?</string>
+ <string name="reader_toast_err_share_intent">Није могуће делити</string>
+ <string name="reader_toast_err_tag_invalid">Ово није исправна ознака</string>
+ <string name="reader_toast_err_tag_exists">Већ пратите ову ознаку</string>
+ <string name="reader_toast_err_comment_failed">Није могуће објавити ваш коментар</string>
+ <string name="reader_likes_only_you">Свиђа вам се ово</string>
+ <string name="reader_likes_one">Једној особи се ово свиђа</string>
+ <string name="reader_label_removed_tag">Уклоњен %s</string>
+ <string name="reader_label_added_tag">Додат %s</string>
+ <string name="reader_btn_unfollow">Престани са праћењем</string>
+ <string name="reader_btn_follow">Прати</string>
+ <string name="media_add_popup_title">Додајте у библиотеку медија</string>
+ <string name="select_from_media_library">Одаберите из библиотеке медија</string>
+ <string name="reader_hint_comment_on_comment">Одговор на коментар…</string>
+ <string name="media_add_popup_capture_photo">Фотографиши</string>
+ <string name="media_gallery_type_slideshow">Приказ слајдова</string>
+ <string name="media_gallery_type">Врста</string>
+ <string name="post_excerpt">Исечак</string>
+ <string name="themes_features_label">Особине</string>
+ <string name="media_add_popup_capture_video">Сними видео снимак</string>
+ <string name="theme_activating_button">Уклучивање</string>
+ <string name="theme_activate_button">Укључи</string>
+ <string name="themes_details_label">Детаљи</string>
+ <string name="media_edit_failure">Ажурирање неуспешно</string>
+ <string name="media_edit_success">Ажуриран</string>
+ <string name="media_gallery_type_tiled">Поплочан</string>
+ <string name="custom_date">Прилагођени датум</string>
+ <string name="unattached">Непридружено</string>
+ <string name="themes">Теме</string>
+ <string name="all">Све</string>
+ <string name="images">Слике</string>
+ <string name="passcode_turn_on">Укључи ПИН закључавање</string>
+ <string name="passcode_turn_off">Искључи ПИН закључавање</string>
+ <string name="passcode_preference_title">ПИН кључ</string>
+ <string name="passcode_set">ПИН подешен</string>
+ <string name="passcode_change_passcode">Промена ПИН-а</string>
+ <string name="passcode_re_enter_passcode">Поново унесите свој ПИН</string>
+ <string name="passcode_enter_old_passcode">Унесите свој стари ПИН</string>
+ <string name="passcode_enter_passcode">Унесите свој ПИН</string>
+ <string name="passcode_manage">Управљајте ПИН лозинком</string>
+ <string name="stats_view_clicks">Притискања</string>
+ <string name="stats_view_visitors_and_views">Посетиоци и прегледи</string>
+ <string name="stats">Статистике</string>
+ <string name="share_action">Подели</string>
+ <string name="share_action_title">Додај на...</string>
+ <string name="theme_set_success">Успешно постављена тема!</string>
+ <string name="stats_totals_plays">Пуштања</string>
+ <string name="stats_totals_clicks">Притискања</string>
+ <string name="stats_totals_views">Прегледи</string>
+ <string name="stats_entry_authors">Аутор</string>
+ <string name="stats_entry_tags_and_categories">Тема</string>
+ <string name="stats_entry_country">Држава</string>
+ <string name="stats_timeframe_months">Месеци</string>
+ <string name="stats_timeframe_yesterday">Јуче</string>
+ <string name="stats_timeframe_days">Дани</string>
+ <string name="stats_timeframe_today">Данас</string>
+ <string name="stats_entry_referrers">Извор упућивања</string>
+ <string name="stats_view_referrers">Извори упућивања</string>
+ <string name="media_edit_description_hint">Унесите опис овде</string>
+ <string name="media_edit_caption_hint">Унесите натпис овде</string>
+ <string name="media_edit_title_hint">Унесите наслов овде</string>
+ <string name="media_edit_caption_text">Натпис</string>
+ <string name="media_edit_description_text">Опис</string>
+ <string name="media_edit_title_text">Наслов</string>
+ <string name="media_gallery_type_circles">Кругови</string>
+ <string name="media_gallery_type_squares">Квадрати</string>
+ <string name="media_gallery_image_order_reverse">Обрнутим редоследом</string>
+ <string name="media_gallery_image_order_random">Насумично</string>
+ <string name="stats_timeframe_weeks">Недељу дана</string>
+ <string name="stats_entry_posts_and_pages">Наслов</string>
+ <string name="theme_auth_error_title">Неуспешно добављање тема</string>
+ <string name="stats_view_tags_and_categories">Ознаке и категорије</string>
+ <string name="upload">Отпремање</string>
+ <string name="sign_in">Пријави се</string>
+ <string name="notifications">Обавештења</string>
+ <string name="more_notifications">и %d још.</string>
+ <string name="new_notifications">%d нових обавештења</string>
+ <string name="note_reply_successful">Успешно одговорено</string>
+ <string name="loading">Учитавање...</string>
+ <string name="httppassword">HTTP лозинка</string>
+ <string name="httpuser">HTTP корисничко име</string>
+ <string name="error_media_upload">Дошло је до грешке током отпремања медија</string>
+ <string name="publish_date">Објављено</string>
+ <string name="content_description_add_media">Додајте медије</string>
+ <string name="post_content">Садржај (додирните да додате текст и медије)</string>
+ <string name="incorrect_credentials">Неисправно корисничко име или лозинка.</string>
+ <string name="username">Корисничко име</string>
+ <string name="password">Лозинка</string>
+ <string name="reader">Читач</string>
+ <string name="post">Чланак</string>
+ <string name="anonymous">Анониман</string>
+ <string name="posts">Чланци</string>
+ <string name="width">Ширина</string>
+ <string name="caption">Натпис (опционо)</string>
+ <string name="no_network_title">Нема доступне мреже</string>
+ <string name="featured_in_post">Укључи слику у садржај чланка</string>
+ <string name="featured">Користи као препоручену слику</string>
+ <string name="pages">Стране</string>
+ <string name="page">Страна</string>
+ <string name="blogusername">korisnickoimebloga</string>
+ <string name="ok">У реду</string>
+ <string name="scaled_image">Ширина сразмерне слике</string>
+ <string name="upload_scaled_image">Отпреми и повежи са сразмерном сликом</string>
+ <string name="scheduled">Заказано</string>
+ <string name="link_enter_url">URL</string>
+ <string name="tos">Услови сервиса</string>
+ <string name="version">Издање</string>
+ <string name="app_title">Вордпрес за Андроид</string>
+ <string name="image_alignment">Поравнање</string>
+ <string name="refresh">Освежи</string>
+ <string name="untitled">Без имена</string>
+ <string name="edit">Уреди</string>
+ <string name="post_id">Чланак</string>
+ <string name="page_id">Страна</string>
+ <string name="immediately">Одмах</string>
+ <string name="post_password">Лозинка (опционо)</string>
+ <string name="quickpress_add_alert_title">Подеси име пречице</string>
+ <string name="settings">Подешавања</string>
+ <string name="today">Данас</string>
+ <string name="share_url">Подели URL</string>
+ <string name="quickpress_window_title">Одаберите блог за QuickPress пречицу</string>
+ <string name="quickpress_add_error">Име пречице не може бити празно</string>
+ <string name="post_private">Приватно</string>
+ <string name="draft">Нацрт</string>
+ <string name="publish_post">Објави</string>
+ <string name="upload_full_size_image">Отпреми и повежи са целокупном сликом</string>
+ <string name="tags_separate_with_commas">Ознаке (одвојите их запетама)</string>
+ <string name="categories">Категорије</string>
+ <string name="title">Наслов</string>
+ <string name="notification_vibrate">Вибрирај</string>
+ <string name="notification_blink">Трепћи светло за обавештење</string>
+ <string name="status">Стање</string>
+ <string name="select_video">Одабери видео снимак из галерије</string>
+ <string name="sdcard_title">SD картица неопходна</string>
+ <string name="location">Локација</string>
+ <string name="media">Медији</string>
+ <string name="delete">Обриши</string>
+ <string name="none">Ниједан</string>
+ <string name="blogs">Блогови</string>
+ <string name="select_photo">Одабери фотографију из галерије</string>
+ <string name="no">Не</string>
+ <string name="yes">Да</string>
+ <string name="reply">Одговор</string>
+ <string name="on">укључен</string>
+ <string name="preview">Преглед</string>
+ <string name="category_refresh_error">Грешка приликом освежавања категорије</string>
+ <string name="add">Додај</string>
+ <string name="save">Сачувај</string>
+ <string name="error">Грешка</string>
+ <string name="cancel">Откажи</string>
+ <string name="notification_settings">Подешавања обавештења</string>
+</resources>
diff --git a/WordPress/src/main/res/values-sv/strings.xml b/WordPress/src/main/res/values-sv/strings.xml
new file mode 100644
index 000000000..a4e3474a9
--- /dev/null
+++ b/WordPress/src/main/res/values-sv/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">Administratör</string>
+ <string name="role_editor">Redaktör</string>
+ <string name="role_author">Författare</string>
+ <string name="role_contributor">Medarbetare</string>
+ <string name="role_follower">Prenumerant</string>
+ <string name="role_viewer">Läsare</string>
+ <string name="error_post_my_profile_no_connection">Ingen nätverksanslutning, kunde inte spara din profil</string>
+ <string name="alignment_none">Ingen</string>
+ <string name="alignment_left">Vänster</string>
+ <string name="alignment_right">Höger</string>
+ <string name="site_settings_list_editor_action_mode_title">Vald %1$d</string>
+ <string name="error_fetch_users_list">Kunde inte hämta webbplatsens användare</string>
+ <string name="plans_manage">Uppgradera ditt paket på\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Du har inga läsare än.</string>
+ <string name="people_fetching">Hämtar användare...</string>
+ <string name="title_follower">Följare</string>
+ <string name="title_email_follower">Följare via e-post</string>
+ <string name="people_empty_list_filtered_email_followers">Du har inga följare via e-post än.</string>
+ <string name="people_empty_list_filtered_followers">Du har inga följare än.</string>
+ <string name="people_empty_list_filtered_users">Du har inga användare än.</string>
+ <string name="people_dropdown_item_email_followers">Följare via e-post</string>
+ <string name="people_dropdown_item_viewers">Läsare</string>
+ <string name="people_dropdown_item_followers">Följare</string>
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="invite_message_usernames_limit">Bjud in upp till 10 e-postadresser eller användare på WordPress.com. De som behöver ett användarnamn får instruktioner skickade till dem.</string>
+ <string name="viewer_remove_confirmation_message">Om du tar bort denna läsare kommer hen inte kunna besöka denna webbplats.\n\nÄr du säker på att du vill ta bort denna läsare?</string>
+ <string name="follower_remove_confirmation_message">Om följaren tas bort kommer hen inte längre få notiser från denna webbplats såvida de inte följer webbplatsen på nytt.\n\nVill du fortfarande ta bort denna följare?</string>
+ <string name="follower_subscribed_since">Sedan %1$s</string>
+ <string name="reader_label_view_gallery">Visa galleri</string>
+ <string name="error_remove_follower">Kunde inte ta bort följare</string>
+ <string name="error_remove_viewer">Kunde inte ta bort läsare</string>
+ <string name="error_fetch_email_followers_list">Kunde inte hämta följare via e-post</string>
+ <string name="error_fetch_followers_list">Kunde inte hämta följare</string>
+ <string name="editor_failed_uploads_switch_html">Vissa mediauppladdningar har misslyckats. Du kan inte skifta till HTML-läget\n i detta tillstånd. Vill du ta bort alla misslyckade uppladdningar och fortsätta?</string>
+ <string name="format_bar_description_html">HTML-läge</string>
+ <string name="visual_editor">Visuell redigerare</string>
+ <string name="image_thumbnail">Miniatyrbild</string>
+ <string name="format_bar_description_ul">Oordnad lista</string>
+ <string name="format_bar_description_ol">Ordnad lista</string>
+ <string name="format_bar_description_more">Infoga mer</string>
+ <string name="format_bar_description_media">Infoga media</string>
+ <string name="format_bar_description_strike">Genomstrykning</string>
+ <string name="format_bar_description_quote">Citatblock</string>
+ <string name="format_bar_description_link">Infoga länk</string>
+ <string name="format_bar_description_italic">Kursiv</string>
+ <string name="format_bar_description_underline">Understreck</string>
+ <string name="image_settings_save_toast">Ändringar sparade</string>
+ <string name="image_caption">Bildtext</string>
+ <string name="image_alt_text">Alt-text</string>
+ <string name="image_link_to">Länka till</string>
+ <string name="image_width">Bredd</string>
+ <string name="format_bar_description_bold">Fetstil</string>
+ <string name="image_settings_dismiss_dialog_title">Förkasta ändringar som inte sparats?</string>
+ <string name="stop_upload_dialog_title">Stoppa uppladdning?</string>
+ <string name="stop_upload_button">Stoppa uppladdning</string>
+ <string name="alert_error_adding_media">Ett fel uppstod när media skulle infogas</string>
+ <string name="alert_action_while_uploading">Du laddar för närvarande upp media. Vänligen vänta till det är klart.</string>
+ <string name="alert_insert_image_html_mode">Media kan inte infogas direkt i HTML-läget. Vänligen växla tillbaka till det visuella läget.</string>
+ <string name="uploading_gallery_placeholder">Laddar upp galleri...</string>
+ <string name="invite_error_some_failed">Inbjudan skickades men fel uppstod!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Inbjudan skickad</string>
+ <string name="tap_to_try_again">Tryck för att försöka igen!</string>
+ <string name="invite_error_sending">Ett fel uppstod när inbjudan skulle skickas!</string>
+ <string name="invite_error_invalid_usernames_multiple">Kan inte skicka: Det finns ogiltiga användarnamn eller e-postadresser</string>
+ <string name="invite_error_invalid_usernames_one">Kan inte skicka: Ett användarnamn eller en e-postadress är ogiltig</string>
+ <string name="invite_error_no_usernames">Ange minst ett användarnamn</string>
+ <string name="invite_message_info">(Valfritt) Du kan lägga till ett anpassat meddelande med upp till 500 tecken som kommer att ingå i inbjudan till användare.</string>
+ <string name="invite_message_remaining_other">%d tecken återstår</string>
+ <string name="invite_message_remaining_one">1 tecken återstår</string>
+ <string name="invite_message_remaining_zero">0 tecken återstår</string>
+ <string name="invite_invalid_email">E-postadressen "%s" är ogiltig</string>
+ <string name="invite_message_title">Anpassat meddelande</string>
+ <string name="invite_already_a_member">Det finns redan en medlem med användarnamnet "%s"</string>
+ <string name="invite_username_not_found">Det gick inte att hitta en användare med användarnamnet "%s"</string>
+ <string name="invite">Bjud in</string>
+ <string name="invite_names_title">Användarnamn eller e‑postmeddelanden</string>
+ <string name="signup_succeed_signin_failed">Ditt konto har skapats men ett fel uppstod medan du loggades\n in. Försök att logga in med ditt nya användarnamn och lösenord.</string>
+ <string name="send_link">Skicka länk</string>
+ <string name="my_site_header_external">Externt</string>
+ <string name="invite_people">Bjud in personer</string>
+ <string name="label_clear_search_history">Rensa sökhistoriken</string>
+ <string name="dlg_confirm_clear_search_history">Rensa sökhistoriken?</string>
+ <string name="reader_empty_posts_in_search_description">Inga inlägg hittade för %s på ditt språk</string>
+ <string name="reader_label_post_search_running">Söker...</string>
+ <string name="reader_label_related_posts">Relaterad läsning</string>
+ <string name="reader_empty_posts_in_search_title">Hittade inga inlägg</string>
+ <string name="reader_label_post_search_explainer">Sök alla offentliga WordPress.com-bloggar</string>
+ <string name="reader_hint_post_search">Sök på WordPress.com</string>
+ <string name="reader_title_related_post_detail">Relaterade inlägg</string>
+ <string name="reader_title_search_results">Sök efter%s</string>
+ <string name="preview_screen_links_disabled">Länkar har avaktiverats på förhandsvisningsskärmen</string>
+ <string name="draft_explainer">Det här inlägget är ett utkast som inte har publicerats</string>
+ <string name="send">Skicka</string>
+ <string name="user_remove_confirmation_message">Om du tar bort %1$s kommer användaren inte längre kunna besöka webbplatsen, men eventuellt innehåll som skapats av %1$s kommer att finnas kvar.\n\nVill du fortfarande ta bort denna användare?</string>
+ <string name="person_removed">%1$s borttagen</string>
+ <string name="person_remove_confirmation_title">Ta bort %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">Webbplatserna i denna lista har inte publicerat några inlägg på senare tid</string>
+ <string name="people">Människor</string>
+ <string name="edit_user">Redigera användare</string>
+ <string name="role">Roll</string>
+ <string name="error_remove_user">Kunde inte ta bort användare</string>
+ <string name="error_fetch_viewers_list">Kunde inte hämta webbplatsens läsare</string>
+ <string name="error_update_role">Kunde inte uppdatera användarroll</string>
+ <string name="gravatar_camera_and_media_permission_required">Behörigheter krävs för att välja eller ta ett foto</string>
+ <string name="error_updating_gravatar">Det gick inte att uppdatera din Gravatar</string>
+ <string name="error_locating_image">Det gick inte att hitta den beskurna bilden</string>
+ <string name="error_refreshing_gravatar">Det gick inte att ladda om din Gravatar</string>
+ <string name="gravatar_tip">Nyhet! Tryck på din Gravatar för att ändra den!</string>
+ <string name="error_cropping_image">Det gick inte att beskära bilden</string>
+ <string name="launch_your_email_app">Starta din e‑postapp</string>
+ <string name="checking_email">Kontrollerar e-post</string>
+ <string name="not_on_wordpress_com">Inte på WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">Inte tillgänglig för närvarande. Ange ditt lösenord</string>
+ <string name="check_your_email">Kolla din e-post</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Få en länk skickad till din e-post för att omedelbart logga in</string>
+ <string name="logging_in">Loggar in</string>
+ <string name="enter_your_password_instead">Ange ditt lösenord istället</string>
+ <string name="web_address_dialog_hint">Visas offentligt när du kommenterar.</string>
+ <string name="jetpack_not_connected_message">Jetpack-tillägget är installerat men inte anslutet till WordPress.com. Vill du ansluta Jetpack?</string>
+ <string name="username_email">E-post eller användarnamn</string>
+ <string name="jetpack_not_connected">Jetpack-tillägget är inte anslutet</string>
+ <string name="new_editor_reflection_error">Den visuella redigeraren är inte kompatibel med din enhet. Den\n inaktiverades automatiskt.</string>
+ <string name="stats_insights_latest_post_no_title">(ingen rubrik)</string>
+ <string name="capture_or_pick_photo">Ta eller välj ett foto</string>
+ <string name="plans_post_purchase_text_themes">Du har nu obegränsad tillgång till Premium-teman. Förhandsgranska valfritt tema på din webbplats för att komma igång.</string>
+ <string name="plans_post_purchase_button_themes">Visa teman</string>
+ <string name="plans_post_purchase_title_themes">Hitta ett perfekt premium-tema</string>
+ <string name="plans_post_purchase_button_video">Skapa nytt inlägg</string>
+ <string name="plans_post_purchase_text_video">Du kan ladda upp och spara videoklipp på din webbplats med VideoPress och ditt utökade utrymme för media.</string>
+ <string name="plans_post_purchase_title_video">Ge inlägg mer liv med video</string>
+ <string name="plans_post_purchase_button_customize">Anpassa min webbplats</string>
+ <string name="plans_post_purchase_text_customize">Du har nu tillgång till anpassade typsnitt, anpassade färger, samt möjligheten att skriva anpassad CSS.</string>
+ <string name="plans_post_purchase_text_intro">Din webbplats tackar dig. Nu kan du utforska din webbplats nya funktioner och välja var du vill börja.</string>
+ <string name="plans_post_purchase_title_customize">Anpassa typsnitt &amp; färger</string>
+ <string name="plans_post_purchase_title_intro">Allt klart, snyggt jobbat!</string>
+ <string name="export_your_content_message">Dina inlägg, sidor och inställningar kommer att skickas via e-post till %s.</string>
+ <string name="plan">Paket</string>
+ <string name="plans">Paket</string>
+ <string name="plans_loading_error">Kunde inte ladda in paket</string>
+ <string name="export_your_content">Exportera ditt innehåll</string>
+ <string name="exporting_content_progress">Exporterar innehåll...</string>
+ <string name="export_email_sent">Exporte-post skickat!</string>
+ <string name="premium_upgrades_message">Du har aktiva premium-uppgraderingar på din webbplats. Vänligen avsluta dina uppgraderingar innan du tar bort din webbplats.</string>
+ <string name="show_purchases">Visa köp</string>
+ <string name="checking_purchases">Kollar köp</string>
+ <string name="premium_upgrades_title">Premium-uppgraderingar</string>
+ <string name="purchases_request_error">Något gick fel. Kunde inte hämta köp.</string>
+ <string name="delete_site_progress">Raderar webbplats...</string>
+ <string name="delete_site_summary">Denna handling kan inte ångras. När du raderar din webbplats raderas allt innehåll, alla författare, och alla domäner från din webbplats.</string>
+ <string name="delete_site_hint">Radera webbplats</string>
+ <string name="export_site_hint">Exportera din webbplats till XML-fil</string>
+ <string name="are_you_sure">Är du säker?</string>
+ <string name="export_site_summary">Om du är säker, vänligen se till att du tar dig tid att exportera ditt innehåll nu. Ditt innehåll kan inte återskapas i framtiden.</string>
+ <string name="keep_your_content">Spara ditt innehåll</string>
+ <string name="domain_removal_hint">Domänerna som inte kommer att fungera efter att du raderat din webbplats</string>
+ <string name="domain_removal_summary">Var försiktig! Om du raderar din webbplats kommer du också ta bort domän/domänerna nedan.</string>
+ <string name="primary_domain">Primärdomän</string>
+ <string name="domain_removal">Ta bort domän</string>
+ <string name="error_deleting_site_summary">Det uppstod ett fel när din webbplats skulle tas bort. Vänligen kontakta supporten för mer hjälp.</string>
+ <string name="error_deleting_site">Kunde inte ta bort webbplats</string>
+ <string name="confirm_delete_site_prompt">Vänligen skriv %1$s i fältet nedan för att godkänna. Din webbplats kommer sedan vara borta för alltid.</string>
+ <string name="site_settings_export_content_title">Exportera innehåll</string>
+ <string name="contact_support">Kontakta support</string>
+ <string name="confirm_delete_site">Godkänn webbplatsborttagning</string>
+ <string name="start_over_text">Om du vill ha en webbplats, men inte vill ha någon av inläggen eller sidorna du har nu, kan vårt supportteam ta bort dina inlägg, sidor, mediaobjekt och kommentarer åt dig.\n\nDetta låter dig behålla din webbplats och URL men ger dig möjligheten att börja om på nytt i ditt skapande. Kontakta oss för att ta bort allt ditt nuvarande innehåll.</string>
+ <string name="site_settings_start_over_hint">Börja om med webbplatsen</string>
+ <string name="let_us_help">Låt oss hjälpa</string>
+ <string name="me_btn_app_settings">Appinställningar</string>
+ <string name="start_over">Börja om</string>
+ <string name="editor_remove_failed_uploads">Ta bort misslyckade uppladdningar</string>
+ <string name="editor_toast_failed_uploads">En del mediauppladdningar har misslyckats. Du kan inte spara eller publicera\n ditt inlägg i dess nuvarande form. Vill du ta bort all media som inte gick att ladda upp?</string>
+ <string name="comments_empty_list_filtered_trashed">Inga bortkastade kommentarer</string>
+ <string name="site_settings_advanced_header">Avancerat</string>
+ <string name="comments_empty_list_filtered_pending">Inga väntande kommentarer</string>
+ <string name="comments_empty_list_filtered_approved">Inga godkända kommentarer</string>
+ <string name="button_done">Klar</string>
+ <string name="button_skip">Hoppa över</string>
+ <string name="site_timeout_error">Kunde inte ansluta till WordPress-webbplatsen eftersom servern inte svarade.</string>
+ <string name="xmlrpc_malformed_response_error">Kunde inte ansluta. WordPress-installationen svarade med ett ogiltigt XML-RPC-dokument.</string>
+ <string name="xmlrpc_missing_method_error">Kunde inte ansluta. Obligatoriska XML-RPC-metoder saknas på servern.</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Videoklipp</string>
+ <string name="alignment_center">Centrerat</string>
+ <string name="theme_free">Free</string>
+ <string name="theme_all">Alla</string>
+ <string name="theme_premium">Premium</string>
+ <string name="post_format_chat">Chatt</string>
+ <string name="post_format_gallery">Galleri</string>
+ <string name="post_format_image">Bild</string>
+ <string name="post_format_link">Länk</string>
+ <string name="post_format_quote">Citat</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="notif_events">Information om kurser och evenemang på WordPress.com (online och fysiska möten).</string>
+ <string name="post_format_aside">Notering</string>
+ <string name="post_format_audio">Ljud</string>
+ <string name="notif_surveys">Möjligheter att delta i WordPress.coms studier och undersökningar.</string>
+ <string name="notif_tips">Tips för att få ut så mycket som möjligt av WordPress.com.</string>
+ <string name="notif_community">Community</string>
+ <string name="replies_to_my_comments">Svar på mina kommentarer</string>
+ <string name="notif_suggestions">Förslag</string>
+ <string name="notif_research">Forskning</string>
+ <string name="site_achievements">Webbplats-troféer</string>
+ <string name="username_mentions">Omnämnande av användarnamn</string>
+ <string name="likes_on_my_posts">Gilla-markeringar för mina inlägg</string>
+ <string name="site_follows">Webbplatsföljare</string>
+ <string name="likes_on_my_comments">Gillar mina kommentarer</string>
+ <string name="comments_on_my_site">Kommentarer på min webbplats</string>
+ <string name="site_settings_list_editor_summary_other">%d poster</string>
+ <string name="site_settings_list_editor_summary_one">1 post</string>
+ <string name="approve_auto_if_previously_approved">Kända användares kommentarer</string>
+ <string name="approve_auto">Alla användare</string>
+ <string name="approve_manual">Inga kommentarer</string>
+ <string name="site_settings_paging_summary_other">%d kommentarer per sida</string>
+ <string name="site_settings_paging_summary_one">1 kommentar per sida</string>
+ <string name="site_settings_multiple_links_summary_other">Kräv godkännande för fler än %d länkar</string>
+ <string name="site_settings_multiple_links_summary_one">Kräv godkännande för fler än 1 länk</string>
+ <string name="site_settings_multiple_links_summary_zero">Kräv godkännande för fler än 0 länkar</string>
+ <string name="detail_approve_auto">Godkänn allas kommentarer automatiskt.</string>
+ <string name="detail_approve_auto_if_previously_approved">Godkänn automatiskt om användaren har en tidigare godkänd kommentar</string>
+ <string name="detail_approve_manual">Kräv manuellt godkännande för alla kommentarer.</string>
+ <string name="filter_trashed_posts">Borttagen</string>
+ <string name="days_quantity_one">1 dag</string>
+ <string name="days_quantity_other">%d dagar</string>
+ <string name="filter_published_posts">Publicerad</string>
+ <string name="filter_draft_posts">Utkast</string>
+ <string name="filter_scheduled_posts">Schemalagd</string>
+ <string name="pending_email_change_snackbar">Klicka på verifieringslänken i e-postmeddelandet som skickades till %1$s för att bekräfta din nya adress</string>
+ <string name="primary_site">Primär webbplats</string>
+ <string name="web_address">Webbadress</string>
+ <string name="editor_toast_uploading_please_wait">Mediefiler laddas upp för tillfället. Vänta tills de är färdiguppladdade.</string>
+ <string name="error_refresh_comments_showing_older">Kommentarer kan inte uppdateras just nu – visar äldre kommentarer</string>
+ <string name="editor_post_settings_set_featured_image">Ange utvald bild</string>
+ <string name="editor_post_settings_featured_image">Utvald bild</string>
+ <string name="new_editor_promo_desc">I WordPress-appen för Android ingår nu en ny, fantastisk, visuell\n redigerare. Prova gärna genom att skapa ett nytt inlägg.</string>
+ <string name="new_editor_promo_title">Helt ny redigerare</string>
+ <string name="new_editor_promo_button_label">Tack!</string>
+ <string name="visual_editor_enabled">Visuell redigerare aktiverad</string>
+ <string name="editor_content_placeholder">Berätta din historia här...</string>
+ <string name="editor_page_title_placeholder">Sidans rubrik</string>
+ <string name="editor_post_title_placeholder">Inläggets rubrik</string>
+ <string name="email_address">E-postadress</string>
+ <string name="preference_show_visual_editor">Visa visuell redigerare</string>
+ <string name="dlg_sure_to_delete_comments">Radera dessa kommentarer premanent?</string>
+ <string name="preference_editor">Redigerare</string>
+ <string name="dlg_sure_to_delete_comment">Radera denna kommentar premanent?</string>
+ <string name="mnu_comment_delete_permanently">Ta bort</string>
+ <string name="comment_deleted_permanently">Kommentar raderad</string>
+ <string name="mnu_comment_untrash">Återställ</string>
+ <string name="comments_empty_list_filtered_spam">Inga skräppost-kommentarer</string>
+ <string name="could_not_load_page">Kunde inte ladda sida</string>
+ <string name="comment_status_all">Alla</string>
+ <string name="interface_language">Gränssnittspråk</string>
+ <string name="off">Av</string>
+ <string name="about_the_app">Om appen</string>
+ <string name="error_post_account_settings">Kunde inte spara dina kontoinställningar</string>
+ <string name="error_post_my_profile">Kunde inte spara din profil</string>
+ <string name="error_fetch_account_settings">Kunde inte hämta dina kontoinställningar</string>
+ <string name="error_fetch_my_profile">Kunde inte hämta din profil</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, klart</string>
+ <string name="stats_widget_promo_desc">Lägg till denna widget på startskärmen för att med ett enda klick få tillgång till din statistik.</string>
+ <string name="stats_widget_promo_title">Startskärmens statuswidget</string>
+ <string name="site_settings_unknown_language_code_error">Språkkoden känns inte igen</string>
+ <string name="site_settings_threading_dialog_description">Tillåt att kommentarer staplas i trådar.</string>
+ <string name="site_settings_threading_dialog_header">Trådar upp till</string>
+ <string name="remove">Ta bort</string>
+ <string name="search">Sök</string>
+ <string name="add_category">Lägg till kategori</string>
+ <string name="disabled">Inaktiverad</string>
+ <string name="site_settings_image_original_size">Originalstorlek</string>
+ <string name="privacy_private">Din webbplats är bara synlig för dig själv och användare som du godkänner</string>
+ <string name="privacy_public_not_indexed">Din webbplats är synlig för alla, men begär att sökmotorerna inte ska indexera den</string>
+ <string name="privacy_public">Din webbplats är synlig för alla och kan indexeras av sökmotorer</string>
+ <string name="about_me_hint">Ett par ord om dig själv…</string>
+ <string name="public_display_name_hint">Om det inte anges något annat kommer visningsnamnet som standard att vara ditt användarnamn</string>
+ <string name="about_me">Om mig</string>
+ <string name="public_display_name">Offentligt visningsnamn</string>
+ <string name="my_profile">Min profil</string>
+ <string name="first_name">Förnamn</string>
+ <string name="last_name">Efternamn</string>
+ <string name="site_privacy_public_desc">Tillåt att sökmotorer indexerar denna webbplats</string>
+ <string name="site_privacy_hidden_desc">Be att sökmotorer inte indexerar denna webbplats</string>
+ <string name="site_privacy_private_desc">Jag vill att min webbplats ska vara privat och endast tillgänglig för användare som jag själv väljer</string>
+ <string name="cd_related_post_preview_image">Relaterat inlägg förhandsgranskningsbild</string>
+ <string name="error_post_remote_site_settings">Kunde inte spara webbplatsinfo</string>
+ <string name="error_fetch_remote_site_settings">Kunde inte hämta webbplatsinfo</string>
+ <string name="error_media_upload_connection">Ett anslutningsfel uppstod vid uppladdning av media</string>
+ <string name="site_settings_disconnected_toast">Frånkopplad, redigering inaktiverad.</string>
+ <string name="site_settings_unsupported_version_error">WordPress-versionen stöds ej</string>
+ <string name="site_settings_multiple_links_dialog_description">Kräv godkännande för kommentarer som innehåller fler än detta antal länkar.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Stäng automatiskt</string>
+ <string name="site_settings_close_after_dialog_description">Stäng kommentarer om inlägg automatiskt.</string>
+ <string name="site_settings_paging_dialog_description">Dela upp kommentarstrådar i flera sidor.</string>
+ <string name="site_settings_paging_dialog_header">Kommentarer per sida</string>
+ <string name="site_settings_close_after_dialog_title">Stäng kommentarer</string>
+ <string name="site_settings_blacklist_description">När en kommentar innehåller något av dessa ord i innehåll, namn, URL, e-postadress eller IP markeras den som skräppost. Du kan ange delar av ord, så "press" kommer att matcha "Wordpress".</string>
+ <string name="site_settings_hold_for_moderation_description">När en kommentar innehåller något av dessa ord i innehåll, namn, URL, e-postadress eller IP stannar den i granskningskön Du kan ange delar av ord, så "press" kommer att matcha "Wordpress".</string>
+ <string name="site_settings_list_editor_input_hint">Ange ett ord eller en fras</string>
+ <string name="site_settings_list_editor_no_items_text">Inga objekt</string>
+ <string name="site_settings_learn_more_caption">Du kan åsidosätta dessa inställningar för enskilda inlägg.</string>
+ <string name="site_settings_rp_preview3_site">i "Uppgradera"</string>
+ <string name="site_settings_rp_preview3_title">Uppgraderingsfokus: VideoPress för bröllop</string>
+ <string name="site_settings_rp_preview2_site">i "Appar"</string>
+ <string name="site_settings_rp_preview2_title">WordPress för Android får en ansiktslyftning</string>
+ <string name="site_settings_rp_preview1_site">i "Mobilt"</string>
+ <string name="site_settings_rp_preview1_title">Stor iPhone/iPad-uppdatering finns nu tillgänglig</string>
+ <string name="site_settings_rp_show_images_title">Visa bilder</string>
+ <string name="site_settings_rp_show_header_title">Visa sidhuvud</string>
+ <string name="site_settings_rp_switch_summary">Relaterade inlägg visar relevant innehåll från din webbplats nedanför dina inlägg.</string>
+ <string name="site_settings_rp_switch_title">Visa relaterade inlägg</string>
+ <string name="site_settings_delete_site_hint">Tar bort dina webbplatsdata från appen</string>
+ <string name="site_settings_blacklist_hint">Kommentarer som matchar ett filter markeras som skräppost</string>
+ <string name="site_settings_moderation_hold_hint">Kommentarer som matchar ett filter placeras i granskningskön</string>
+ <string name="site_settings_multiple_links_hint">Ignorera länkbegränsningen för kända användare</string>
+ <string name="site_settings_whitelist_hint">Kommentarens författare måste ha en tidigare godkänd kommentar</string>
+ <string name="site_settings_user_account_required_hint">Användare måste vara registrerade och inloggade för att kunna kommentera</string>
+ <string name="site_settings_identity_required_hint">Kommentarens författare måste fylla i namn och e-postadress</string>
+ <string name="site_settings_manual_approval_hint">Kommentarer måste godkännas manuellt</string>
+ <string name="site_settings_paging_hint">Visa kommentarer i stycken av en viss storlek</string>
+ <string name="site_settings_threading_hint">Tillåt staplade kommentarer till ett visst djup</string>
+ <string name="site_settings_sort_by_hint">Avgör i vilken ordning kommentarer visas</string>
+ <string name="site_settings_close_after_hint">Tillåt inte kommentarer efter den angivna tiden</string>
+ <string name="site_settings_receive_pingbacks_hint">Tillåt länknotiser från andra bloggar</string>
+ <string name="site_settings_send_pingbacks_hint">Försök meddela alla bloggar som länkats i inlägget</string>
+ <string name="site_settings_allow_comments_hint">Tillåt att läsare publicerar kommentarer</string>
+ <string name="site_settings_discussion_hint">Visa och ändra dina webbplatsers diskussionsinställningar</string>
+ <string name="site_settings_more_hint">Visa alla tillgängliga diskussionsinställningar</string>
+ <string name="site_settings_related_posts_hint">Visa eller dölj relaterade inlägg i läsaren</string>
+ <string name="site_settings_upload_and_link_image_hint">Aktivera för att alltid ladda upp bilden i fullstorlek</string>
+ <string name="site_settings_image_width_hint">Ändrar bildstorlek i inlägg till denna bredd</string>
+ <string name="site_settings_format_hint">Anger nytt inläggsformat</string>
+ <string name="site_settings_category_hint">Anger ny inläggskategori</string>
+ <string name="site_settings_location_hint">Lägg automatiskt till platsdata i dina inlägg</string>
+ <string name="site_settings_password_hint">Ändra ditt lösenord</string>
+ <string name="site_settings_username_hint">Nuvarande användarkonto</string>
+ <string name="site_settings_language_hint">Det språk som den här bloggen huvudsakligen är skrivet på</string>
+ <string name="site_settings_privacy_hint">Kontrollerar vem som kan se din webbplats</string>
+ <string name="site_settings_address_hint">Ändring av din adress stöds för närvarande inte</string>
+ <string name="site_settings_tagline_hint">En kort beskrivning eller snärtig mening som beskriver din blogg</string>
+ <string name="site_settings_title_hint">Beskriv med några få ord vad denna webbplats handlar om</string>
+ <string name="site_settings_whitelist_known_summary">Kommentarer från kända användare</string>
+ <string name="site_settings_whitelist_all_summary">Kommentarer från alla användare</string>
+ <string name="site_settings_threading_summary">%d nivåer</string>
+ <string name="site_settings_privacy_private_summary">Privat</string>
+ <string name="site_settings_privacy_hidden_summary">Dold</string>
+ <string name="site_settings_delete_site_title">Radera webbplats</string>
+ <string name="site_settings_privacy_public_summary">Offentlig</string>
+ <string name="site_settings_blacklist_title">Svartlista</string>
+ <string name="site_settings_moderation_hold_title">Vänta på granskning</string>
+ <string name="site_settings_multiple_links_title">Länkar i kommentarer</string>
+ <string name="site_settings_whitelist_title">Godkänn automatiskt</string>
+ <string name="site_settings_threading_title">Trådning</string>
+ <string name="site_settings_paging_title">Paginering</string>
+ <string name="site_settings_sort_by_title">Sortera efter</string>
+ <string name="site_settings_account_required_title">Användare måste vara inloggade</string>
+ <string name="site_settings_identity_required_title">Måste inkludera namn och e-postadress</string>
+ <string name="site_settings_receive_pingbacks_title">Ta emot pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">Skicka pingbacks</string>
+ <string name="site_settings_allow_comments_title">Tillåt kommentarer</string>
+ <string name="site_settings_default_format_title">Standardformat</string>
+ <string name="site_settings_default_category_title">Standardkategori</string>
+ <string name="site_settings_location_title">Aktivera plats</string>
+ <string name="site_settings_address_title">Adress</string>
+ <string name="site_settings_title_title">Webbplatsens titel</string>
+ <string name="site_settings_tagline_title">Slogan</string>
+ <string name="site_settings_this_device_header">Denna enhet</string>
+ <string name="site_settings_discussion_new_posts_header">Standard för nya inlägg</string>
+ <string name="site_settings_account_header">Konto</string>
+ <string name="site_settings_writing_header">Skriva</string>
+ <string name="newest_first">Nyast först</string>
+ <string name="site_settings_general_header">Allmänt</string>
+ <string name="discussion">Diskussion</string>
+ <string name="privacy">Integritet</string>
+ <string name="related_posts">Relaterade inlägg</string>
+ <string name="comments">Kommentarer</string>
+ <string name="close_after">Stäng efter</string>
+ <string name="oldest_first">Äldst först</string>
+ <string name="media_error_no_permission_upload">Du har inte tillåtelse att ladda upp media till webbplatsen</string>
+ <string name="never">Aldrig</string>
+ <string name="unknown">Okänd</string>
+ <string name="reader_err_get_post_not_found">Detta inlägg finns inte längre</string>
+ <string name="reader_err_get_post_not_authorized">Du har inte behörighet att se detta inlägg</string>
+ <string name="reader_err_get_post_generic">Kunde inte hämta detta inlägg</string>
+ <string name="blog_name_no_spaced_allowed">En webbadress kan inte innehålla mellanslag</string>
+ <string name="invalid_username_no_spaces">Ett användarnamn kan inte innehålla mellanslag</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">De webbplatser du följer har inte publicerat något nyligen</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Inga nya inlägg</string>
+ <string name="media_details_copy_url_toast">URL kopierad till urklipp</string>
+ <string name="edit_media">Redigera media</string>
+ <string name="media_details_copy_url">Kopiera URL</string>
+ <string name="media_details_label_date_uploaded">Uppladdad</string>
+ <string name="media_details_label_date_added">Lade till</string>
+ <string name="selected_theme">Valt tema</string>
+ <string name="could_not_load_theme">Kunde inte ladda tema</string>
+ <string name="theme_activation_error">Något gick fel. Kunde inte aktivera tema</string>
+ <string name="theme_by_author_prompt_append"> av %1$s</string>
+ <string name="theme_prompt">Tack för att du väljer %1$s</string>
+ <string name="theme_try_and_customize">Testa &amp; anpassa</string>
+ <string name="theme_view">Visa</string>
+ <string name="theme_details">Information</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">KLART</string>
+ <string name="theme_manage_site">HANTERA WEBBPLATS</string>
+ <string name="title_activity_theme_support">Teman</string>
+ <string name="theme_activate">Aktivera</string>
+ <string name="date_range_start_date">Startdatum</string>
+ <string name="date_range_end_date">Slutdatum</string>
+ <string name="current_theme">Nuvarande tema</string>
+ <string name="customize">Anpassa</string>
+ <string name="details">Information</string>
+ <string name="support">Support</string>
+ <string name="active">Aktiv</string>
+ <string name="stats_referrers_spam_generic_error">Något gick fel under processen. Skräppoststatus ändrades inte.</string>
+ <string name="stats_referrers_marking_not_spam">Markerar som inte skräppost</string>
+ <string name="stats_referrers_unspam">Inte skräppost</string>
+ <string name="stats_referrers_marking_spam">Markerar som skräppost</string>
+ <string name="theme_auth_error_authenticate">Kunde inte hämta teman: kunde inte autentisera användare</string>
+ <string name="post_published">Inlägg publicerat</string>
+ <string name="page_published">Sida publicerad</string>
+ <string name="post_updated">Inlägg uppdaterat</string>
+ <string name="page_updated">Sida uppdaterad</string>
+ <string name="stats_referrers_spam">Skräppost</string>
+ <string name="theme_no_search_result_found">Tyvärr hittades inga teman.</string>
+ <string name="media_file_name">Filnamn: %s</string>
+ <string name="media_uploaded_on">Laddades upp den: %s</string>
+ <string name="media_dimensions">Storlek: %s</string>
+ <string name="upload_queued">Köad</string>
+ <string name="media_file_type">Filtyp: %s</string>
+ <string name="reader_label_gap_marker">Ladda fler inlägg</string>
+ <string name="notifications_no_search_results">Inga webbplatser matchade "%s"</string>
+ <string name="search_sites">Sök webbplatser</string>
+ <string name="notifications_empty_view_reader">Visa Läsare</string>
+ <string name="unread">Olästa</string>
+ <string name="notifications_empty_action_followers_likes">Se till att synas: kommentera inlägg som du läst.</string>
+ <string name="notifications_empty_action_comments">Gå med i en diskussion: kommentera inlägg från bloggar som du följer.</string>
+ <string name="notifications_empty_action_unread">Starta upp diskussionen igen: skriv ett nytt inlägg.</string>
+ <string name="notifications_empty_action_all">Aktivera dig! Kommentera inlägg från bloggar som du följer.</string>
+ <string name="notifications_empty_likes">Inga nya gillar att visa ... ännu.</string>
+ <string name="notifications_empty_followers">Inga nya följare att visa ... ännu.</string>
+ <string name="notifications_empty_comments">Inga nya kommentarer … ännu.</string>
+ <string name="notifications_empty_unread">Du har läst allt!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Gå till statistik i appen och försök att lägga till denna widget senare</string>
+ <string name="stats_widget_error_readd_widget">Ta bort denna widget och lägg till den igen</string>
+ <string name="stats_widget_error_no_visible_blog">Statistik kunde inte visas utan en synlig blogg</string>
+ <string name="stats_widget_error_no_permissions">Ditt konto hos WordPress.com kan inte visa statistik för denna blogg</string>
+ <string name="stats_widget_error_no_account">Logga in på WordPress</string>
+ <string name="stats_widget_error_generic">Statistik kunde inte laddas</string>
+ <string name="stats_widget_loading_data">Laddar data…</string>
+ <string name="stats_widget_name_for_blog">Dagens statistik för %1$s</string>
+ <string name="stats_widget_name">WordPress dagens status</string>
+ <string name="add_location_permission_required">Behörighet krävs för att lägga till plats</string>
+ <string name="add_media_permission_required">Behörighet krävs för att lägga till media</string>
+ <string name="access_media_permission_required">Behörighet krävs för åtkomst till media</string>
+ <string name="stats_enable_rest_api_in_jetpack">Aktivera JSON API-modulen i Jetpack för att visa din statistik.</string>
+ <string name="error_open_list_from_notification">Detta inlägg eller sida har publicerats på en annan webbplats</string>
+ <string name="reader_short_comment_count_multi">%s kommentarer</string>
+ <string name="reader_short_comment_count_one">1 kommentar</string>
+ <string name="reader_label_submit_comment">SKICKA</string>
+ <string name="reader_hint_comment_on_post">Svara inlägget...</string>
+ <string name="reader_discover_visit_blog">Besök %s</string>
+ <string name="reader_discover_attribution_blog">Först publicerat på %s</string>
+ <string name="reader_discover_attribution_author">Först publicerat av %s</string>
+ <string name="reader_discover_attribution_author_and_blog">Först publicerat av %1$s på %2$s</string>
+ <string name="reader_short_like_count_multi">%s gillar</string>
+ <string name="reader_short_like_count_one">1 gillar</string>
+ <string name="reader_label_follow_count">%,d följare</string>
+ <string name="reader_short_like_count_none">Gilla</string>
+ <string name="reader_menu_tags">Redigera etiketter och bloggar</string>
+ <string name="reader_title_post_detail">Läsare-inlägg</string>
+ <string name="local_draft_explainer">Det här inlägget är ett lokalt utkast som inte har publicerats</string>
+ <string name="local_changes_explainer">Det här inlägget har lokala ändringar som inte har publicerats</string>
+ <string name="notifications_push_summary">Inställningar för notiser som visas på din enhet.</string>
+ <string name="notifications_email_summary">Inställningar för notiser som skickas till den e-post som är knutet till ditt konto.</string>
+ <string name="notifications_tab_summary">Inställningar för notiser som visas under fliken Notiser.</string>
+ <string name="notifications_disabled">App-notiser har inaktiverats. Tryck här för att aktivera dem i Inställningar.</string>
+ <string name="notification_types">Notistyper</string>
+ <string name="error_loading_notifications">Det gick inte att läsa in notisinställningar</string>
+ <string name="replies_to_your_comments">Svar på dina kommentarer</string>
+ <string name="comment_likes">Gillar för kommentarer</string>
+ <string name="app_notifications">App-notiser</string>
+ <string name="notifications_tab">Fliken Notiser</string>
+ <string name="email">E-post</string>
+ <string name="notifications_comments_other_blogs">Kommentarer på andra webbplatser</string>
+ <string name="notifications_wpcom_updates">WordPress.com-nyheter</string>
+ <string name="notifications_other">Andra</string>
+ <string name="notifications_account_emails">E-post från WordPress.com</string>
+ <string name="notifications_account_emails_summary">Vi skickar dig alltid viktiga e-postmeddelanden om ditt konto, men du kan även få värdefullt extramaterial.</string>
+ <string name="notifications_sights_and_sounds">Bilder och ljud</string>
+ <string name="your_sites">Dina webbplatser</string>
+ <string name="stats_insights_latest_post_trend">Det har gått %1$s sedan %2$s publicerades. Så här har det gått för inlägget hittills…</string>
+ <string name="stats_insights_latest_post_summary">Sammanfattning av senaste inlägg</string>
+ <string name="button_revert">Återställ</string>
+ <string name="days_ago">%d dagar sedan</string>
+ <string name="yesterday">I går</string>
+ <string name="connectionbar_no_connection">Ingen anslutning</string>
+ <string name="page_trashed">Sidan placerad i papperskorgen</string>
+ <string name="post_deleted">Inlägg raderat</string>
+ <string name="post_trashed">Inlägget placerat i papperskorgen</string>
+ <string name="stats_no_activity_this_period">Ingen aktivitet denna period</string>
+ <string name="trashed">Borttagen</string>
+ <string name="button_back">Tillbaka</string>
+ <string name="page_deleted">Sidan borttagen</string>
+ <string name="button_stats">Statistik</string>
+ <string name="button_trash">Ta bort</string>
+ <string name="button_preview">Förhandsgranska</string>
+ <string name="button_view">Visa</string>
+ <string name="button_edit">Redigera</string>
+ <string name="button_publish">Publicera</string>
+ <string name="my_site_no_sites_view_subtitle">Vill du lägga till en?</string>
+ <string name="my_site_no_sites_view_title">Du har inga WordPress-webbplatser än.</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Du har inte behörighet för åtkomst till denna blogg</string>
+ <string name="reader_toast_err_follow_blog_not_found">Denna blogg kunde inte hittas</string>
+ <string name="undo">Ångra</string>
+ <string name="tabbar_accessibility_label_my_site">Min sida</string>
+ <string name="tabbar_accessibility_label_me">Jag</string>
+ <string name="passcodelock_prompt_message">Ange din PIN</string>
+ <string name="editor_toast_changes_saved">Ändringar sparades</string>
+ <string name="push_auth_expired">Begäran har löpt ut. Logga in på WordPress.com för att försöka igen.</string>
+ <string name="stats_insights_best_ever">Flest visningar någonsin</string>
+ <string name="ignore">Ignorera </string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% av visningar</string>
+ <string name="stats_insights_most_popular_hour">Mest populära timme</string>
+ <string name="stats_insights_most_popular_day">Mest populära dag</string>
+ <string name="stats_insights_popular">Mest populära dag och timme</string>
+ <string name="stats_insights_today">Dagens statistik </string>
+ <string name="stats_insights_all_time">Alla inlägg, visningar och besökare</string>
+ <string name="stats_insights">Insikter</string>
+ <string name="stats_sign_in_jetpack_different_com_account">Om du vill visa din statistik, logga in på det WordPress.com användarkonto som du använde för att ansluta till Jetpack.</string>
+ <string name="stats_other_recent_stats_moved_label">Letar du efter din övriga senaste statistik? Vi har flyttat den till Insikter-sidan.</string>
+ <string name="me_disconnect_from_wordpress_com">Koppla från WordPress.com </string>
+ <string name="me_connect_to_wordpress_com">Anslut till WordPress.com</string>
+ <string name="me_btn_login_logout">Logga in/Logga ut</string>
+ <string name="account_settings">Kontoinställningar</string>
+ <string name="me_btn_support">Hjälp &amp; support</string>
+ <string name="site_picker_cant_hide_current_site">"%s" kunde inte döljas eftersom den är den nuvarande webbplatsen</string>
+ <string name="site_picker_create_dotcom">Skapa en webbplats på WordPress.com</string>
+ <string name="site_picker_add_site">Lägg till webbplats</string>
+ <string name="site_picker_add_self_hosted">Lägg till webbplats på egen server</string>
+ <string name="site_picker_edit_visibility">Visa/dölj webbplatser</string>
+ <string name="my_site_btn_view_admin">Visa admin</string>
+ <string name="my_site_btn_view_site">Visa webbplats</string>
+ <string name="site_picker_title">Välj webbplats</string>
+ <string name="my_site_btn_switch_site">Byt webbplats</string>
+ <string name="my_site_btn_blog_posts">Blogginlägg</string>
+ <string name="my_site_btn_site_settings">Inställningar </string>
+ <string name="my_site_header_look_and_feel">Utseende och känsla</string>
+ <string name="my_site_header_publish">Publicera</string>
+ <string name="my_site_header_configuration">Konfiguration</string>
+ <string name="reader_label_new_posts_subtitle">Knacka för att visa dem</string>
+ <string name="notifications_account_required">Logga in på WordPress.com för att få notifikationer </string>
+ <string name="stats_unknown_author">Okänd författare</string>
+ <string name="image_added">Bild tillagd</string>
+ <string name="signout">Koppla från</string>
+ <string name="deselect_all">Avmarkera alla </string>
+ <string name="show">Visa</string>
+ <string name="hide">Dölj</string>
+ <string name="select_all">Välj alla</string>
+ <string name="sign_out_wpcom_confirm">Att koppla bort ditt konto kommer att innebära att alla @%s’s WordPress.com data från denna enhet, inklusive lokala utkast och lokala förändringar kommer försvinna.</string>
+ <string name="select_from_new_picker">Flerval med den nya väljaren</string>
+ <string name="stats_generic_error">Nödvändig statistik kunde inte laddas in. </string>
+ <string name="no_device_videos">Inga videor</string>
+ <string name="no_blog_images">Inga bilder</string>
+ <string name="no_blog_videos">Inga videor</string>
+ <string name="no_device_images">Inga bilder</string>
+ <string name="error_loading_blog_images">Kunde inte hämta bilder </string>
+ <string name="error_loading_blog_videos">Kunde inte hämta bilder </string>
+ <string name="error_loading_images">Det gick inte att ladda in bilderna</string>
+ <string name="error_loading_videos">Det gick inte att ladda in videor</string>
+ <string name="loading_blog_images">Hämtar bilder</string>
+ <string name="loading_blog_videos">Hämtar videor</string>
+ <string name="no_media_sources">Kunde inte hämta media </string>
+ <string name="loading_videos">Laddar videos </string>
+ <string name="loading_images">Laddar bilder</string>
+ <string name="no_media">Ingen media</string>
+ <string name="device">Enhet</string>
+ <string name="language">Språk</string>
+ <string name="add_to_post">Lägg till i inlägg</string>
+ <string name="media_picker_title">Välj media</string>
+ <string name="take_photo">Ta ett foto</string>
+ <string name="take_video">Spela in video</string>
+ <string name="tab_title_device_images">Bilder på enhet</string>
+ <string name="tab_title_device_videos">Videoklipp på enhet</string>
+ <string name="tab_title_site_images">Bilder på webbplats</string>
+ <string name="tab_title_site_videos">Videoklipp på webbplats</string>
+ <string name="media_details_label_file_name">Filnamn</string>
+ <string name="media_details_label_file_type">Filtyp</string>
+ <string name="error_publish_no_network">Kunde inte publicera eftersom det into fanns någon nätanslutning. Sparat som utkast.</string>
+ <string name="editor_toast_invalid_path">Ogiltig sökväg till fil</string>
+ <string name="verification_code">Verifieringskod</string>
+ <string name="invalid_verification_code">Ogiltig verifieringskod</string>
+ <string name="verify">Verifiera</string>
+ <string name="two_step_footer_label">Skriv koden från din Authenticator-app.</string>
+ <string name="two_step_footer_button">Skicka kod via SMS</string>
+ <string name="two_step_sms_sent">Kolla dina SMS för att se verifieringskoden.</string>
+ <string name="sign_in_jetpack">Logga in på ditt WordPress.com-konto för att ansluta till Jetpack.</string>
+ <string name="auth_required">Logga in igen för att fortsätta.</string>
+ <string name="reader_empty_posts_request_failed">Kunde inte hämta inlägg</string>
+ <string name="publisher">Publicerat av:</string>
+ <string name="error_notification_open">Kunde inte öppna notis</string>
+ <string name="stats_followers_total_email_paged">Visar %1$d - %2$d av %3$s följare via e-post</string>
+ <string name="stats_search_terms_unknown_search_terms">Okända söktermer</string>
+ <string name="stats_followers_total_wpcom_paged">Visar %1$d - %2$d av %3$s följare på WordPress.com</string>
+ <string name="stats_empty_search_terms_desc">Lär dig mer om din söktrafik genom att granska orden dina besökare sökte efter för att hitta din webbplats.</string>
+ <string name="stats_empty_search_terms">Inga söktermer</string>
+ <string name="stats_entry_search_terms">Sökterm</string>
+ <string name="stats_view_authors">Författare</string>
+ <string name="stats_view_search_terms">Söktermer</string>
+ <string name="comments_fetching">Hämtar kommentarer...</string>
+ <string name="pages_fetching">Hämtar sidor...</string>
+ <string name="toast_err_post_uploading">Kunde inte öppna inlägg eftersom det laddas upp</string>
+ <string name="posts_fetching">Hämtar inlägg...</string>
+ <string name="media_fetching">Hämtar media...</string>
+ <string name="post_uploading">Laddar upp</string>
+ <string name="stats_total">Totalt</string>
+ <string name="stats_overall">Övergripande</string>
+ <string name="stats_period">Period</string>
+ <string name="logs_copied_to_clipboard">App-loggar har kopierats till urklipp</string>
+ <string name="reader_label_new_posts">Nya inlägg</string>
+ <string name="reader_empty_posts_in_blog">Denna blogg är tom</string>
+ <string name="stats_average_per_day">Medel per dag</string>
+ <string name="stats_recent_weeks">Senaste veckorna</string>
+ <string name="error_copy_to_clipboard">Ett fel uppstod när text skulle kopieras till urklipp</string>
+ <string name="reader_page_recommended_blogs">Webbplatser du kanske gillar</string>
+ <string name="stats_months_and_years">Månader och år</string>
+ <string name="themes_fetching">Hämtar teman...</string>
+ <string name="stats_for">Statistik för %s</string>
+ <string name="stats_other_recent_stats_label">Annan ny statistik</string>
+ <string name="stats_view_all">Visa alla</string>
+ <string name="stats_view">Visa</string>
+ <string name="stats_followers_months">%1$d månader</string>
+ <string name="stats_followers_a_year">Ett år</string>
+ <string name="stats_followers_years">%1$d år</string>
+ <string name="stats_followers_a_month">En månad</string>
+ <string name="stats_followers_minutes">%1$d minuter</string>
+ <string name="stats_followers_an_hour_ago">en timme sedan</string>
+ <string name="stats_followers_hours">%1$d timmar</string>
+ <string name="stats_followers_a_day">En dag</string>
+ <string name="stats_followers_days">%1$d dagar</string>
+ <string name="stats_followers_a_minute_ago">en minut sedan</string>
+ <string name="stats_followers_seconds_ago">sekunder sedan</string>
+ <string name="stats_followers_total_email">Totalt antal följare via e-post: %1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">E-post</string>
+ <string name="stats_followers_total_wpcom">Totalt antal följare på WordPress.com: %1$s</string>
+ <string name="stats_comments_total_comments_followers">Totalt antal inlägg med följare av kommentarer: %1$s</string>
+ <string name="stats_comments_by_authors">Efter författare</string>
+ <string name="stats_comments_by_posts_and_pages">Efter inlägg &amp; sidor</string>
+ <string name="stats_empty_followers_desc">Håll koll på ditt totala antal följare och hur länge var och en har följt din webbplats.</string>
+ <string name="stats_empty_followers">Inga följare</string>
+ <string name="stats_empty_publicize_desc">Håll koll på dina följare från andra sociala nätverk genom att använda Offentliggör.</string>
+ <string name="stats_empty_publicize">Inga följare via Publicize</string>
+ <string name="stats_empty_video">Inga videoklipp spelade</string>
+ <string name="stats_empty_video_desc">Om du har laddat upp videoklipp med VideoPress kan du se hur många gånger de har spelats upp.</string>
+ <string name="stats_empty_comments_desc">Om du tillåter kommentarer på din webbplats kan du se vilka dina främsta debattörer är och upptäcka vilket innehåll som skapar de bästa diskussionerna. Baserat på de senaste 1 000 kommentarerna.</string>
+ <string name="stats_empty_tags_and_categories_desc">Få en överblick över de mest populära ämnena på din webbplats baserat på dina bästa inlägg och sidor från den senaste veckan.</string>
+ <string name="stats_empty_top_authors_desc">Se hur många visningar varje författares inlägg eller sidor har och se i detalj vilket innehåll som är mest populärt.</string>
+ <string name="stats_empty_tags_and_categories">Inga taggade inlägg eller sidor visade</string>
+ <string name="stats_empty_clicks_desc">När ditt innehåll har länkar till andra webbplatser kan du se vilka dina besökare klickar på mest.</string>
+ <string name="stats_empty_referrers_desc">Lär dig mer om din webbplats synlighet genom att se vilka webbplatser och sökmotorer som skickar dig mest besökare.</string>
+ <string name="stats_empty_clicks_title">Inga klick</string>
+ <string name="stats_empty_referrers_title">Inga hänvisningar</string>
+ <string name="stats_empty_top_posts_title">Inga inlägg eller sidor visade</string>
+ <string name="stats_empty_top_posts_desc">Upptäck vilket innehåll som har visats flest gånger och se detaljerad information över hur inlägg och sidor attraherar besökare över en längre tid.</string>
+ <string name="stats_totals_followers">Sedan</string>
+ <string name="stats_empty_geoviews">Inga länder</string>
+ <string name="stats_empty_geoviews_desc">Utforska listan för att se vilka länder och regioner som besöker din webbplats mest.</string>
+ <string name="stats_entry_video_plays">Videoklipp</string>
+ <string name="stats_entry_top_commenter">Författare</string>
+ <string name="stats_entry_publicize">Tjänst</string>
+ <string name="stats_entry_followers">Följare</string>
+ <string name="stats_totals_publicize">Följare</string>
+ <string name="stats_entry_clicks_link">Länk</string>
+ <string name="stats_view_top_posts_and_pages">Inlägg &amp; sidor</string>
+ <string name="stats_view_videos">Videoklipp</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Följare</string>
+ <string name="stats_view_countries">Länder</string>
+ <string name="stats_likes">Gillar</string>
+ <string name="stats_pagination_label">Sida %1$s av %2$s</string>
+ <string name="stats_timeframe_years">År</string>
+ <string name="stats_views">Visningar</string>
+ <string name="stats_visitors">Besökare</string>
+ <string name="ssl_certificate_details">Detaljer</string>
+ <string name="delete_sure_post">Radera detta inlägg</string>
+ <string name="delete_sure">Radera detta utkast</string>
+ <string name="delete_sure_page">Radera denna sida</string>
+ <string name="confirm_delete_multi_media">Radera valda poster?</string>
+ <string name="confirm_delete_media">Radera vald post?</string>
+ <string name="cab_selected">%d valda</string>
+ <string name="media_gallery_date_range">Visa media från %1$s till %2$s</string>
+ <string name="sure_to_remove_account">Ta bort denna webbplats?</string>
+ <string name="reader_empty_followed_blogs_title">Du följer inga webbplatser än</string>
+ <string name="reader_empty_posts_liked">Du har inte gillat några inlägg</string>
+ <string name="faq_button">Vanliga frågor</string>
+ <string name="browse_our_faq_button">Bläddra bland vanliga frågor</string>
+ <string name="nux_help_description">Besök hjälpsektionen för att få svar på vanliga frågor eller besök forumet för att ställa nya frågor</string>
+ <string name="agree_terms_of_service">Genom att skapa ett konto godkänner du dom fascinerande %1$sAnvändarvillkoren%2$s</string>
+ <string name="create_new_blog_wpcom">Skapa en blogg på WordPress.com</string>
+ <string name="new_blog_wpcom_created">Blogg på WordPress.com skapad!</string>
+ <string name="reader_empty_comments">Inga kommentarer än</string>
+ <string name="reader_empty_posts_in_tag">Inga inlägg med denna etikett</string>
+ <string name="reader_label_comment_count_multi">%,d kommentarer</string>
+ <string name="reader_label_view_original">Visa originalartikel</string>
+ <string name="reader_label_like">Gilla</string>
+ <string name="reader_label_liked_by">Gillat av</string>
+ <string name="reader_label_comment_count_single">En kommentar</string>
+ <string name="reader_label_comments_closed">Kommentarer inaktiverade</string>
+ <string name="reader_label_comments_on">Kommentarer till</string>
+ <string name="reader_title_photo_viewer">%1$d av %2$d</string>
+ <string name="error_publish_empty_post">Det går inte att publicera ett tomt inlägg</string>
+ <string name="error_refresh_unauthorized_posts">Du saknar behörighet för att visa eller redigera inlägg</string>
+ <string name="error_refresh_unauthorized_pages">Du saknar behörighet för att visa eller redigera sidor</string>
+ <string name="error_refresh_unauthorized_comments">Du saknar behörighet för att visa eller redigera kommentarer</string>
+ <string name="older_month">Äldre än en månad</string>
+ <string name="more">Mer</string>
+ <string name="older_two_days">Äldre än 2 dagar</string>
+ <string name="older_last_week">Äldre än en vecka</string>
+ <string name="stats_no_blog">Statistiken för efterfrågad blogg kunde inte laddas</string>
+ <string name="select_a_blog">Välj en WordPress-webbplats</string>
+ <string name="sending_content">Laddar upp %s innehåll</string>
+ <string name="uploading_total">Laddar upp %1$d av %2$d</string>
+ <string name="mnu_comment_liked">Gillat</string>
+ <string name="comment">Kommentar</string>
+ <string name="comment_trashed">Kommentar förkastad</string>
+ <string name="posts_empty_list">Inga inlägg än. Varför inte skapa ett?</string>
+ <string name="comment_reply_to_user">Svar till %s</string>
+ <string name="pages_empty_list">Inga sidor än. Varför inte skapa en?</string>
+ <string name="media_empty_list_custom_date">Ingen media för detta tidsintervall</string>
+ <string name="posting_post">Postar "%s"</string>
+ <string name="signing_out">Loggar ut...</string>
+ <string name="reader_toast_err_generic">Kunde inte genomföra denna åtgärd</string>
+ <string name="reader_toast_err_block_blog">Kunde inte blockera denna blogg</string>
+ <string name="reader_toast_blog_blocked">Inlägg från denna blogg kommer inte längre att visas</string>
+ <string name="reader_menu_block_blog">Blockera denna blogg</string>
+ <string name="contact_us">Kontakta oss</string>
+ <string name="hs__conversation_detail_error">Beskriv problemet du upplever</string>
+ <string name="hs__new_conversation_header">Supportchatt</string>
+ <string name="hs__conversation_header">Supportchatt</string>
+ <string name="hs__username_blank_error">Ange ett giltigt namn</string>
+ <string name="hs__invalid_email_error">Ange en giltig e-postadress</string>
+ <string name="add_location">Lägg till plats</string>
+ <string name="current_location">Nuvarande plats</string>
+ <string name="search_location">Sök</string>
+ <string name="edit_location">Redigera</string>
+ <string name="search_current_location">Hitta</string>
+ <string name="preference_send_usage_stats">Skicka statistik</string>
+ <string name="preference_send_usage_stats_summary">Skicka automatiskt användarstatistik för att hjälpa oss förbättra WordPress för Android</string>
+ <string name="update_verb">Uppdatera</string>
+ <string name="schedule_verb">Tidinställ</string>
+ <string name="reader_title_blog_preview">Läsare-blogg</string>
+ <string name="reader_title_tag_preview">Läsare-etikett</string>
+ <string name="reader_title_subs">Taggar &amp; bloggar</string>
+ <string name="reader_page_followed_tags">Följda taggar</string>
+ <string name="reader_page_followed_blogs">Följda webbplatser</string>
+ <string name="reader_hint_add_tag_or_url">Ange en URL eller tagg att följa</string>
+ <string name="reader_label_followed_blog">Bloggen följs</string>
+ <string name="reader_label_tag_preview">Inlägg taggade %s</string>
+ <string name="reader_toast_err_get_blog_info">Kunde inte visa denna blogg</string>
+ <string name="reader_toast_err_already_follow_blog">Du följer redan denna blogg</string>
+ <string name="reader_toast_err_follow_blog">Kunde inte följa denna blogg</string>
+ <string name="reader_toast_err_unfollow_blog">Kunde inte sluta följa denna blogg</string>
+ <string name="reader_empty_recommended_blogs">Inga rekommenderade bloggar</string>
+ <string name="saving">Sparar...</string>
+ <string name="media_empty_list">Ingen media</string>
+ <string name="ptr_tip_message">Tips: Dra ned för att uppdatera</string>
+ <string name="help">Hjälp</string>
+ <string name="forgot_password">Glömt ditt lösenord?</string>
+ <string name="forums">Forum</string>
+ <string name="help_center">Hjälpcenter</string>
+ <string name="ssl_certificate_error">Ogiltigt SSL-ceritfikat</string>
+ <string name="ssl_certificate_ask_trust">Om du vanligtvis kan nå denna webbplats utan problem kan felet betyda att någon försöker efterlikna webbplatsen och du borde inte fortsätta. Vill du lita på certifikatet ändå?</string>
+ <string name="out_of_memory">Slut på minne för enheten</string>
+ <string name="no_network_message">Inget nätverk tillgängligt</string>
+ <string name="could_not_remove_account">Det gick inte att ta bort webbplatsen</string>
+ <string name="gallery_error">Medieobjektet kunde inte hämtas</string>
+ <string name="blog_not_found">Ett fel uppstod vid anslutning till bloggen</string>
+ <string name="wait_until_upload_completes">Vänta tills uppladdningen är klar</string>
+ <string name="theme_fetch_failed">Misslyckades hämta teman</string>
+ <string name="theme_set_failed">Kunde inte ställa in tema</string>
+ <string name="theme_auth_error_message">Se till att du har behörighet att ställa in teman</string>
+ <string name="comments_empty_list">Inga kommentarer</string>
+ <string name="mnu_comment_unspam">Inte skräppost</string>
+ <string name="no_site_error">Det gick inte att ansluta till WordPress-webbplatsen</string>
+ <string name="adding_cat_failed">Misslyckades lägga till kategori</string>
+ <string name="adding_cat_success">Kategorin tillagd</string>
+ <string name="cat_name_required">Kategorinamn måste anges</string>
+ <string name="category_automatically_renamed">Kategorinamn %1$s är ogiltigt. Har ändrats till %2$s.</string>
+ <string name="no_account">Inget WordPress-konto hittat, skapa ett konto och försök igen</string>
+ <string name="sdcard_message">Ett SD-kort är nödvändigt för att ladda upp media</string>
+ <string name="stats_empty_comments">Inga kommentarer ännu</string>
+ <string name="stats_bar_graph_empty">Ingen statistik tillgänglig</string>
+ <string name="invalid_url_message">Kontrollera att URL:en är giltig</string>
+ <string name="reply_failed">Svara misslyckades</string>
+ <string name="notifications_empty_list">Inga notifieringar</string>
+ <string name="error_delete_post">Ett fel uppstod vid raderande av %s</string>
+ <string name="error_refresh_posts">Inläggen kunde inte uppdateras just nu</string>
+ <string name="error_refresh_pages">Sidorna kunde inte uppdateras just nu</string>
+ <string name="error_refresh_notifications">Notifieringar kunde inte uppdateras just nu</string>
+ <string name="error_refresh_comments">Kommentarerna kan inte uppdateras just nu</string>
+ <string name="error_refresh_stats">Statistiken kunde inte uppdateras just nu</string>
+ <string name="error_generic">Ett fel uppstod</string>
+ <string name="error_moderate_comment">Ett fel uppstod under modereringen</string>
+ <string name="error_edit_comment">Ett fel uppstod under redigeringen av kommentaren</string>
+ <string name="error_upload">Ett fel uppstod vid uppladdning av %s</string>
+ <string name="error_load_comment">Kunde inte ladda kommentaren</string>
+ <string name="error_downloading_image">Fel vid nedladdning av bild</string>
+ <string name="passcode_wrong_passcode">Fel PIN</string>
+ <string name="invalid_email_message">Din epostadress är ogilltig</string>
+ <string name="invalid_password_message">Lösenord måste innehålla minst 4 tecken</string>
+ <string name="invalid_username_too_short">Användarnamn måste vara längre än 4 tecken</string>
+ <string name="invalid_username_too_long">Användarnamn måste vara kortare än 61 tecken</string>
+ <string name="username_only_lowercase_letters_and_numbers">Användarnamn får bara innehålla små bokstäver (a-z) och siffror</string>
+ <string name="username_required">Ange ett användarnamn</string>
+ <string name="username_not_allowed">Användarnamnet är inte tillåtet</string>
+ <string name="username_must_be_at_least_four_characters">Användarnamnet måste vara minst 4 tecken</string>
+ <string name="username_contains_invalid_characters">Användarnamn får inte innehålla tecknet "_"</string>
+ <string name="username_must_include_letters">Användarnamn måste innehålla minst en bokstav (a-z)</string>
+ <string name="email_invalid">Ange en giltig epostadress</string>
+ <string name="email_not_allowed">Epostadressen är ogiltig</string>
+ <string name="username_exists">Användarnamnet används redan</string>
+ <string name="email_exists">Epostadressen används redan</string>
+ <string name="username_reserved_but_may_be_available">Användarnamnet är för tillfället reserverat men kanske blir tillgängligt om några dagar</string>
+ <string name="blog_name_required">Ange webbplatsadress</string>
+ <string name="blog_name_not_allowed">Webbplatsadressen är inte tillåten</string>
+ <string name="blog_name_must_be_at_least_four_characters">Webbplatsadressen måste vara minst 4 tecken</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Webbplatsadressen måste vara kortare än 64 tecken</string>
+ <string name="blog_name_contains_invalid_characters">Webbplatsadressen får inte innehålla tecknet "_"</string>
+ <string name="blog_name_cant_be_used">Du kan inte använda den webbplatsadressen</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Webbplatsadressen kan bara innehålla små bokstäver (a-z) och siffror</string>
+ <string name="blog_name_exists">Webbplatsen finns redan</string>
+ <string name="blog_name_reserved">Webbplatsen är reserverad</string>
+ <string name="blog_name_reserved_but_may_be_available">Webbplatsen är reserverad men kan bli tillgänglig om ett par dagar</string>
+ <string name="username_or_password_incorrect">Användarnamnet eller lösenordet som angavs är felaktigt</string>
+ <string name="nux_cannot_log_in">Vi kan inte logga in dig</string>
+ <string name="xmlrpc_error">Kunde inte ansluta. Skriv in hela adressen till xmlrpc.php-filen på din webbplats och försök igen.</string>
+ <string name="select_categories">Välj kategorier</string>
+ <string name="account_details">Kontodetaljer</string>
+ <string name="edit_post">Redigera inlägg</string>
+ <string name="add_comment">Lägg till kommentar</string>
+ <string name="connection_error">Kommunikationsfel</string>
+ <string name="cancel_edit">Avbryt redigering</string>
+ <string name="scaled_image_error">Skriv en giltig bredd för skalning</string>
+ <string name="post_not_found">Ett fel uppstod vid inläsning av inlägget. Uppdatera dina inlägg och försök igen.</string>
+ <string name="learn_more">Läs mer</string>
+ <string name="media_gallery_settings_title">Galleriinställningar</string>
+ <string name="media_gallery_image_order">Bildordning</string>
+ <string name="media_gallery_num_columns">Antal kolumner</string>
+ <string name="media_gallery_type_thumbnail_grid">Rutnät av miniatyrer</string>
+ <string name="media_gallery_edit">Redigera galleri</string>
+ <string name="media_error_no_permission">Du har inte behörighet att visa mediebiblioteket</string>
+ <string name="cannot_delete_multi_media_items">En del medieobjekt kunde inte tas bort just nu. Försök igen senare.</string>
+ <string name="themes_live_preview">Live förhandsgranskning</string>
+ <string name="theme_current_theme">Nuvarande tema</string>
+ <string name="theme_premium_theme">Premium-tema</string>
+ <string name="link_enter_url_text">Länktext (valfritt)</string>
+ <string name="create_a_link">Skapa länk</string>
+ <string name="page_settings">Sidinställningar</string>
+ <string name="local_draft">Lokalt utkast</string>
+ <string name="upload_failed">Uppladdning misslyckades</string>
+ <string name="horizontal_alignment">Vågrät justering</string>
+ <string name="file_not_found">Kunde inte hitta mediafilen att ladda upp. Har den tagits bort eller flyttats?</string>
+ <string name="post_settings">Inläggsinställningar</string>
+ <string name="delete_post">Radera inlägg</string>
+ <string name="delete_page">Radera sida</string>
+ <string name="comment_status_approved">Godkänd</string>
+ <string name="comment_status_unapproved">Väntande</string>
+ <string name="comment_status_spam">Skräppost</string>
+ <string name="comment_status_trash">Kastad</string>
+ <string name="edit_comment">Redigera kommentar</string>
+ <string name="mnu_comment_approve">Godkänn</string>
+ <string name="mnu_comment_unapprove">Godkänn ej</string>
+ <string name="mnu_comment_spam">Skräppost</string>
+ <string name="mnu_comment_trash">Skräp</string>
+ <string name="dlg_approving_comments">Godkänner</string>
+ <string name="dlg_unapproving_comments">Förkastar</string>
+ <string name="dlg_spamming_comments">Markera som skräppost</string>
+ <string name="dlg_trashing_comments">Skicka till papperskorg</string>
+ <string name="dlg_confirm_trash_comments">Kasta?</string>
+ <string name="trash_yes">Kasta</string>
+ <string name="trash_no">Kasta inte</string>
+ <string name="trash">Kasta bort</string>
+ <string name="author_name">Författarnamn</string>
+ <string name="author_email">Författarepostadress</string>
+ <string name="author_url">Författar-URL</string>
+ <string name="hint_comment_content">Kommentar</string>
+ <string name="saving_changes">Spara ändringarna</string>
+ <string name="sure_to_cancel_edit_comment">Avbryt redigeringen av denna kommentar?</string>
+ <string name="content_required">Kommentar krävs</string>
+ <string name="toast_comment_unedited">Kommentaren har inte ändrats</string>
+ <string name="remove_account">Ta bort webbplatsen</string>
+ <string name="blog_removed_successfully">Webbplatsen har tagits bort</string>
+ <string name="delete_draft">Radera utkast</string>
+ <string name="preview_page">Förhandsgranska sida</string>
+ <string name="preview_post">Förhandsgranska inlägg</string>
+ <string name="comment_added">Kommentaren tillagd</string>
+ <string name="post_not_published">Inläggets status är inte publicerat</string>
+ <string name="page_not_published">Sidans status är inte publicerat</string>
+ <string name="view_in_browser">Visa i webbläsare</string>
+ <string name="add_new_category">Lägg till ny kategori</string>
+ <string name="category_name">Kategorinamn</string>
+ <string name="category_slug">Kategorins permalänk (valfritt)</string>
+ <string name="category_desc">Kategoribeskrivning (valfri)</string>
+ <string name="category_parent">Kategorins förälder (valfritt):</string>
+ <string name="share_action_post">Nytt inlägg</string>
+ <string name="share_action_media">Mediabibliotek</string>
+ <string name="file_error_create">Kunde inte skapa temporär mediafil för uppladdning. Se till att det finns tillräckligt med utrymme på din enhet.</string>
+ <string name="location_not_found">Okänd plats</string>
+ <string name="open_source_licenses">Öppen källkodslicens</string>
+ <string name="invalid_site_url_message">Kontrollera att webbplatsens URL är giltig</string>
+ <string name="pending_review">Avvaktar granskning</string>
+ <string name="http_credentials">HTTP-autentiseringsuppgifter (valfritt)</string>
+ <string name="http_authorization_required">Autentisering krävs</string>
+ <string name="post_format">Inläggsformat</string>
+ <string name="notifications_empty_all">Inga notiser … ännu.</string>
+ <string name="new_post">Nytt inlägg</string>
+ <string name="new_media">Ny media</string>
+ <string name="view_site">Visa webbplats</string>
+ <string name="privacy_policy">Integritetspolicy</string>
+ <string name="local_changes">Lokala ändringar</string>
+ <string name="image_settings">Bildinställningar</string>
+ <string name="add_account_blog_url">Bloggadress</string>
+ <string name="wordpress_blog">WordPress-blogg</string>
+ <string name="error_blog_hidden">Den här bloggen är dold och kunde inte laddas. Aktivera den igen i inställningarna och försök igen.</string>
+ <string name="fatal_db_error">Ett fel uppstod vid skapandet av app-databasen. Försök återinstallera appen.</string>
+ <string name="jetpack_message_not_admin">Jetpack tillägget krävs för statistik. Kontakta administratören.</string>
+ <string name="reader_title_applog">Programlogg</string>
+ <string name="reader_share_link">Dela länk</string>
+ <string name="reader_toast_err_add_tag">Kunde inte lägga till etiketten</string>
+ <string name="reader_toast_err_remove_tag">Kunde inte ta bort etiketten</string>
+ <string name="required_field">Obligatoriskt fält</string>
+ <string name="email_hint">Epostadress</string>
+ <string name="site_address">Adress till din WordPress-installation (URL)</string>
+ <string name="email_cant_be_used_to_signup">Du kan inte använda den e-postadressen för att registrera dig. Vi har problem med att de blockerar viss av våra e-post. Använd en annan e-postleverantör.</string>
+ <string name="email_reserved">E-postadressen har redan använts. Kolla efter ett aktiveringsmail i din inbox. Om du inte aktiverar kan du försöka igen om några dagar.</string>
+ <string name="blog_name_must_include_letters">Webbplatsadressen måste innehålla minst 1 bokstav (a-z)</string>
+ <string name="blog_name_invalid">Ogiltig webbplatsadress</string>
+ <string name="blog_title_invalid">Ogiltig webbplatstitel</string>
+ <string name="deleting_page">Tar bort sida</string>
+ <string name="deleting_post">Tar bort inlägg</string>
+ <string name="share_url_post">Dela inlägg</string>
+ <string name="share_url_page">Dela sida</string>
+ <string name="share_link">Dela länk</string>
+ <string name="creating_your_account">Skapar ditt konto</string>
+ <string name="creating_your_site">Skapar din webbplats</string>
+ <string name="reader_empty_posts_in_tag_updating">Hämtar inlägg...</string>
+ <string name="error_refresh_media">Ett fel uppstod när mediabiblioteket skulle uppdateras. Försök igen senare.</string>
+ <string name="reader_likes_you_and_multi">Du och %,d andra gillar detta</string>
+ <string name="reader_likes_multi">%,d gillar detta</string>
+ <string name="reader_toast_err_get_comment">Kunde inte hämta denna kommentar</string>
+ <string name="reader_label_reply">Svara</string>
+ <string name="video">Video</string>
+ <string name="download">Laddar ner media</string>
+ <string name="comment_spammed">Kommentar markerad som skräppost</string>
+ <string name="cant_share_no_visible_blog">Du kan inte dela på WordPress utan att ha en synlig blogg</string>
+ <string name="select_time">Välj tid</string>
+ <string name="reader_likes_you_and_one">Du och en annan gillar detta</string>
+ <string name="reader_empty_followed_blogs_description">Inget att oroa sig för, peka på ikonen uppe till höger för att börja utforska!</string>
+ <string name="select_date">Välj datum</string>
+ <string name="pick_photo">Välj bild</string>
+ <string name="account_two_step_auth_enabled">Detta konto har två-stegsautentisering påslaget. Gå till dina säkerhetsinställningar på WordPress.com och skapa ett unikt lösenord för appen.</string>
+ <string name="pick_video">Välj video</string>
+ <string name="reader_toast_err_get_post">Kunde inte hämta detta inlägg</string>
+ <string name="validating_user_data">Validerar användarinformation</string>
+ <string name="validating_site_data">Validerar webbplats</string>
+ <string name="password_invalid">Du måste använda ett säkrare lösenord. Se till att det består av minst 7 tecken och blanda versaler, gemener, siffror, och specialtecken.</string>
+ <string name="nux_tap_continue">Fortsätt</string>
+ <string name="nux_welcome_create_account">Skapa konto</string>
+ <string name="signing_in">Loggar in...</string>
+ <string name="nux_add_selfhosted_blog">Lägg till egen installation</string>
+ <string name="nux_oops_not_selfhosted_blog">Logga in på WordPress.com</string>
+ <string name="media_add_popup_title">Lägg till i mediebiblioteket</string>
+ <string name="media_add_new_media_gallery">Skapa galleri</string>
+ <string name="empty_list_default">Listan är tom</string>
+ <string name="select_from_media_library">Välj från mediebiblioteket</string>
+ <string name="jetpack_message">Jetpack-tillägget behövs för att visa statistik. Vill du installera Jetpack?</string>
+ <string name="jetpack_not_found">Jetpack-tillägget kunde inte hittas</string>
+ <string name="reader_untitled_post">(Ingen titel)</string>
+ <string name="reader_share_subject">Delad från %s</string>
+ <string name="reader_btn_share">Dela</string>
+ <string name="reader_btn_follow">Följ</string>
+ <string name="reader_btn_unfollow">Följer</string>
+ <string name="reader_hint_comment_on_comment">Svara kommentaren...</string>
+ <string name="reader_label_added_tag">Lade till %s</string>
+ <string name="reader_label_removed_tag">Tog bort %s</string>
+ <string name="reader_likes_one">En person gillar detta</string>
+ <string name="reader_likes_only_you">Du gillar detta</string>
+ <string name="reader_toast_err_comment_failed">Kunde inte kommentera</string>
+ <string name="reader_toast_err_tag_exists">Du följer redan detta ämne</string>
+ <string name="reader_toast_err_tag_invalid">Ogiltigt ämne</string>
+ <string name="reader_toast_err_share_intent">Kunde inte dela</string>
+ <string name="reader_toast_err_view_image">Kunde inte visa bild</string>
+ <string name="reader_toast_err_url_intent">Kunde inte öppna %s</string>
+ <string name="reader_empty_followed_tags">Du följer inga ämnen</string>
+ <string name="create_account_wpcom">Skapa ett konto på WordPress.com</string>
+ <string name="button_next">Nästa</string>
+ <string name="connecting_wpcom">Ansluter till WordPress.com</string>
+ <string name="username_invalid">Ogiltigt användarnamn</string>
+ <string name="limit_reached">Gräns nådd. Du kan försöka igen om 1 minut. Att försöka igen innan kommer bara öka tiden du måste vänta innan förbudet är upphävt. Vänligen kontakta support om du tror detta är ett fel.</string>
+ <string name="nux_tutorial_get_started_title">Kom igång!</string>
+ <string name="themes">Teman</string>
+ <string name="all">Alla</string>
+ <string name="images">Bilder</string>
+ <string name="unattached">Obundna</string>
+ <string name="custom_date">Anpassat datum</string>
+ <string name="media_add_popup_capture_photo">Ta foto</string>
+ <string name="media_add_popup_capture_video">Spela in video</string>
+ <string name="media_gallery_image_order_random">Slumpmässig</string>
+ <string name="media_gallery_image_order_reverse">Omvänd</string>
+ <string name="media_gallery_type">Stil</string>
+ <string name="media_gallery_type_squares">Kvadrater</string>
+ <string name="media_gallery_type_tiled">Mosaik</string>
+ <string name="media_gallery_type_circles">Cirklar</string>
+ <string name="media_gallery_type_slideshow">Bildspel</string>
+ <string name="media_edit_title_text">Rubrik</string>
+ <string name="media_edit_caption_text">Bildtext</string>
+ <string name="media_edit_description_text">Beskrivning</string>
+ <string name="media_edit_title_hint">Skriv en rubrik här</string>
+ <string name="media_edit_caption_hint">Skriv en bildtext här</string>
+ <string name="media_edit_description_hint">Skriv en beskrivning här</string>
+ <string name="media_edit_success">Uppdaterad</string>
+ <string name="media_edit_failure">Kunde inte uppdatera</string>
+ <string name="themes_details_label">Detaljer</string>
+ <string name="themes_features_label">Funktioner</string>
+ <string name="theme_activate_button">Aktivera</string>
+ <string name="theme_activating_button">Aktiverar</string>
+ <string name="theme_set_success">Temat aktiverat!</string>
+ <string name="theme_auth_error_title">Kunde inte hämta teman</string>
+ <string name="post_excerpt">Utdrag</string>
+ <string name="share_action_title">Lägg till...</string>
+ <string name="share_action">Dela</string>
+ <string name="stats">Statistik</string>
+ <string name="stats_view_visitors_and_views">Besökare och visningar</string>
+ <string name="stats_view_clicks">Klick</string>
+ <string name="stats_view_tags_and_categories">Etiketter och kategorier</string>
+ <string name="stats_view_referrers">Hänvisningar</string>
+ <string name="stats_timeframe_today">I dag</string>
+ <string name="stats_timeframe_yesterday">I går</string>
+ <string name="stats_timeframe_days">Dagar</string>
+ <string name="stats_timeframe_weeks">Veckor</string>
+ <string name="stats_timeframe_months">Månader</string>
+ <string name="stats_entry_country">Land</string>
+ <string name="stats_entry_posts_and_pages">Rubrik</string>
+ <string name="stats_entry_tags_and_categories">Ämne</string>
+ <string name="stats_entry_authors">Författare</string>
+ <string name="stats_entry_referrers">Hänvisning</string>
+ <string name="stats_totals_views">Visningar</string>
+ <string name="stats_totals_clicks">Klick</string>
+ <string name="stats_totals_plays">Spelningar</string>
+ <string name="passcode_manage">Ändra PIN-kodlås</string>
+ <string name="passcode_enter_passcode">Skriv din PIN-kod</string>
+ <string name="passcode_enter_old_passcode">Skriv din gamla PIN-kod</string>
+ <string name="passcode_re_enter_passcode">Skriv din PIN-kod igen</string>
+ <string name="passcode_change_passcode">Ändra PIN-kod</string>
+ <string name="passcode_set">PIN-kod inställd</string>
+ <string name="passcode_preference_title">PIN-kodlås</string>
+ <string name="passcode_turn_off">Stäng av PIN-kodlås</string>
+ <string name="passcode_turn_on">Slå på PIN-kodlås</string>
+ <string name="upload">Ladda upp</string>
+ <string name="discard">Kasta bort</string>
+ <string name="sign_in">Logga in</string>
+ <string name="notifications">Notifieringar</string>
+ <string name="note_reply_successful">Svar publicerat</string>
+ <string name="follows">Följer</string>
+ <string name="new_notifications">%d nya notifieringar</string>
+ <string name="more_notifications">och %d mer.</string>
+ <string name="loading">Laddar...</string>
+ <string name="httpuser">HTTP-användarnamn</string>
+ <string name="httppassword">HTTP-lösenord</string>
+ <string name="error_media_upload">Ett fel uppstod vid uppladdning av media</string>
+ <string name="post_content">Innehåll (knacka för att lägga till text och media)</string>
+ <string name="publish_date">Publicera</string>
+ <string name="content_description_add_media">Lägg till media</string>
+ <string name="incorrect_credentials">Felaktigt användarnamn eller lösenord.</string>
+ <string name="password">Lösenord</string>
+ <string name="username">Användarnamn</string>
+ <string name="reader">Läsare</string>
+ <string name="featured">Använd som utvald bild</string>
+ <string name="featured_in_post">Inkludera bild i inlägget</string>
+ <string name="no_network_title">Inget nätverk tillgängligt</string>
+ <string name="pages">Sidor</string>
+ <string name="caption">Bildtext (valfritt)</string>
+ <string name="width">Bredd</string>
+ <string name="posts">Inlägg</string>
+ <string name="anonymous">Anonym</string>
+ <string name="page">Sida</string>
+ <string name="post">Inlägg</string>
+ <string name="blogusername">användarnamn</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Ladda upp och länka till skalad bild</string>
+ <string name="scaled_image">Välj bredd för bild</string>
+ <string name="scheduled">Tidsinställt</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">Laddar upp...</string>
+ <string name="version">Version</string>
+ <string name="tos">Allmänna villkor</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">Förvald bildbredd</string>
+ <string name="image_alignment">Justering</string>
+ <string name="refresh">Uppdatera</string>
+ <string name="untitled">Utan titel</string>
+ <string name="edit">Redigera</string>
+ <string name="post_id">Inlägg</string>
+ <string name="page_id">Sida</string>
+ <string name="post_password">Lösenord (valfritt)</string>
+ <string name="immediately">Omedelbart</string>
+ <string name="quickpress_add_alert_title">Ange genvägsnamn</string>
+ <string name="today">Idag</string>
+ <string name="settings">Inställningar</string>
+ <string name="share_url">Dela URL</string>
+ <string name="quickpress_window_title">Välj blogg för QuickPress-genväg</string>
+ <string name="quickpress_add_error">Genvägsnamn kan inte vara tomt</string>
+ <string name="publish_post">Publicera</string>
+ <string name="draft">Utkast</string>
+ <string name="post_private">Privat</string>
+ <string name="upload_full_size_image">Ladda upp och länka till originalbild</string>
+ <string name="title">Titel</string>
+ <string name="tags_separate_with_commas">Etiketter (separera etiketter med kommatecken)</string>
+ <string name="categories">Kategorier</string>
+ <string name="dlg_deleting_comments">Raderar kommentarer</string>
+ <string name="notification_blink">Blinka notifieringslampa</string>
+ <string name="notification_sound">Notifieringsljud</string>
+ <string name="notification_vibrate">Vibrera</string>
+ <string name="status">Status</string>
+ <string name="location">Plats</string>
+ <string name="sdcard_title">SD-kort krävs</string>
+ <string name="select_video">Välj en video från galleriet</string>
+ <string name="media">Media</string>
+ <string name="delete">Radera</string>
+ <string name="none">Ingen</string>
+ <string name="blogs">Bloggar</string>
+ <string name="select_photo">Välj en bild från galleri</string>
+ <string name="error">Fel</string>
+ <string name="cancel">Avbryt</string>
+ <string name="save">Spara</string>
+ <string name="add">Lägg till</string>
+ <string name="category_refresh_error">Fel vid uppdatering av kategorier</string>
+ <string name="preview">Förhandsgranska</string>
+ <string name="on">om</string>
+ <string name="reply">Svara</string>
+ <string name="notification_settings">Notisinställningar</string>
+ <string name="yes">Ja</string>
+ <string name="no">Nej</string>
+</resources>
diff --git a/WordPress/src/main/res/values-sw600dp-land/dimens.xml b/WordPress/src/main/res/values-sw600dp-land/dimens.xml
new file mode 100644
index 000000000..510e9befa
--- /dev/null
+++ b/WordPress/src/main/res/values-sw600dp-land/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+ <dimen name="notifications_list_margin">192dp</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-sw600dp-land/integers.xml b/WordPress/src/main/res/values-sw600dp-land/integers.xml
new file mode 100644
index 000000000..22d4b2250
--- /dev/null
+++ b/WordPress/src/main/res/values-sw600dp-land/integers.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="media_grid_num_columns">4</integer>
+</resources>
diff --git a/WordPress/src/main/res/values-sw600dp/dimens.xml b/WordPress/src/main/res/values-sw600dp/dimens.xml
new file mode 100644
index 000000000..ab399d69d
--- /dev/null
+++ b/WordPress/src/main/res/values-sw600dp/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="default_dialog_width">480dp</dimen>
+
+ <dimen name="reader_featured_image_height">@dimen/reader_featured_image_height_tablet</dimen>
+ <dimen name="postlist_featured_image_height">@dimen/postlist_featured_image_height_tablet</dimen>
+ <dimen name="notifications_list_margin">48dp</dimen>
+ <dimen name="fab_margin">@dimen/fab_margin_tablet</dimen>
+ <dimen name="menu_item_margin">@dimen/menu_item_margin_tablet</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-sw600dp/integers.xml b/WordPress/src/main/res/values-sw600dp/integers.xml
new file mode 100644
index 000000000..a5b3ed1b0
--- /dev/null
+++ b/WordPress/src/main/res/values-sw600dp/integers.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="media_grid_num_columns">3</integer>
+ <integer name="media_editor_save_button_visibility">@integer/visible</integer>
+ <integer name="isSW600DP">1</integer>
+ <integer name="smallest_width_dp">600</integer>
+</resources>
diff --git a/WordPress/src/main/res/values-sw600dp/styles.xml b/WordPress/src/main/res/values-sw600dp/styles.xml
new file mode 100644
index 000000000..7061ed70a
--- /dev/null
+++ b/WordPress/src/main/res/values-sw600dp/styles.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="WordPress.BorderedBackground">
+ <item name="android:layout_marginLeft">@dimen/notifications_list_margin</item>
+ <item name="android:layout_marginRight">@dimen/notifications_list_margin</item>
+ <item name="android:background">@drawable/calypso_bordered_bg</item>
+ <item name="android:paddingLeft">1dp</item>
+ <item name="android:paddingRight">1dp</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values-sw720dp/dimens.xml b/WordPress/src/main/res/values-sw720dp/dimens.xml
new file mode 100644
index 000000000..cdfbe9004
--- /dev/null
+++ b/WordPress/src/main/res/values-sw720dp/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="settings_padding">200dp</dimen>
+ <dimen name="format_bar_height">50dp</dimen>
+ <dimen name="theme_details_dialog_min_width">650dp</dimen>
+ <dimen name="theme_details_dialog_height">500dp</dimen>
+ <dimen name="reader_featured_image_height">@dimen/reader_featured_image_height_tablet_large</dimen>
+ <dimen name="stats_barchart_height">200dp</dimen>
+ <dimen name="postlist_featured_image_height">@dimen/postlist_featured_image_height_tablet_large</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-sw720dp/integers.xml b/WordPress/src/main/res/values-sw720dp/integers.xml
new file mode 100644
index 000000000..770709c15
--- /dev/null
+++ b/WordPress/src/main/res/values-sw720dp/integers.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="themes_grid_num_columns">3</integer>
+ <integer name="smallest_width_dp">720</integer>
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values-sw720dp/styles.xml b/WordPress/src/main/res/values-sw720dp/styles.xml
new file mode 100644
index 000000000..f6d196af3
--- /dev/null
+++ b/WordPress/src/main/res/values-sw720dp/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="WordPressTitleAppearance" parent="@android:style/TextAppearance">
+ <item name="android:singleLine">true</item>
+ <item name="android:shadowColor">#BB000000</item>
+ <item name="android:shadowRadius">2.75</item>
+ <item name="android:textColor">#FFF6F6F6</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="WordPressSectionHeader" parent="@android:style/Widget.TextView">
+ <item name="android:textSize">16dp</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:shadowColor">#FFFFFFFF</item>
+ <item name="android:shadowDx">0</item>
+ <item name="android:shadowDy">2</item>
+ <item name="android:shadowRadius">1</item>
+ <item name="android:layout_marginTop">1px</item>
+ <item name="android:layout_marginRight">1px</item>
+ <item name="android:layout_marginLeft">1px</item>
+ <item name="android:paddingLeft">6dp</item>
+ <item name="android:paddingRight">4dp</item>
+ <item name="android:paddingBottom">6dp</item>
+ <item name="android:paddingTop">2dp</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values-th/strings.xml b/WordPress/src/main/res/values-th/strings.xml
new file mode 100644
index 000000000..57fbeb192
--- /dev/null
+++ b/WordPress/src/main/res/values-th/strings.xml
@@ -0,0 +1,833 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="image_thumbnail">รูปย่อ</string>
+ <string name="remove">ลบออก</string>
+ <string name="search">ค้นหา</string>
+ <string name="add_category">เพิ่มหมวดหมู่</string>
+ <string name="disabled">ปิดการใช้งาน</string>
+ <string name="site_settings_image_original_size">ขนาดต้นฉบับ</string>
+ <string name="about_me_hint">คำอธิบายสั้น ๆ เกี่ยวกับคุณ....</string>
+ <string name="about_me">เกี่ยวกับฉัน</string>
+ <string name="public_display_name">ชื่อที่ใช้แสดงในสาธารณะ</string>
+ <string name="my_profile">ประวัติของฉัน</string>
+ <string name="first_name">ชื่อ</string>
+ <string name="last_name">นามสกุล</string>
+ <string name="site_privacy_public_desc">อนุญาตให้เซิร์ชเอ็นจิ้นเก็บหน้าเว็บนี้</string>
+ <string name="site_settings_close_after_dialog_title">ปิดการแสดงความเห็น</string>
+ <string name="site_settings_list_editor_input_hint">ใส่คำหรือวลี</string>
+ <string name="site_settings_list_editor_no_items_text">ไม่มีรายการ</string>
+ <string name="site_settings_rp_preview1_site">ใน "มือถือ"</string>
+ <string name="site_settings_rp_show_images_title">แสดงรูปภาพ</string>
+ <string name="site_settings_rp_show_header_title">แสดงส่วนหัว</string>
+ <string name="site_settings_whitelist_hint">ผู้เขียนความเห็นต้องมีความเห็นก่อนหน้าที่ได้รับอนุมัติแล้ว</string>
+ <string name="site_settings_user_account_required_hint">ผู้ใช้ต้องลงทะเบียนและเข้าสู่ระบบเพื่อจะเขียนความเห็น</string>
+ <string name="site_settings_identity_required_hint">ผู้เขียนความเห็นต้องใส่ชื่อและอีเมล</string>
+ <string name="site_settings_manual_approval_hint">ความเห็นต้องถูกอนุมัติด้วยมือ</string>
+ <string name="site_settings_sort_by_hint">กำหนดลำดับความเห็นที่จะแสดง</string>
+ <string name="site_settings_close_after_hint">ไม่อนุญาตให้แสดงความเห็นหลังจากเวลาที่ระบุ</string>
+ <string name="site_settings_receive_pingbacks_hint">อนุญาตการเตือนลิงก์จากบล็อกอื่น ๆ</string>
+ <string name="site_settings_allow_comments_hint">อนุญาตให้ผู้อ่านเขียนความเห็นได้</string>
+ <string name="site_settings_discussion_hint">ดูและเปลี่ยนการตั้งค่าการสนทนาเว็บของคุณ</string>
+ <string name="site_settings_more_hint">ดูการตั้งค่าการสนทนาทั้งหมดที่มีอยู่</string>
+ <string name="site_settings_related_posts_hint">แสดงหรือซ่อนเรื่องในตัวอ่าน</string>
+ <string name="site_settings_upload_and_link_image_hint">เปิดการใช้งานอัปโหลดรูปภาพแบบเต็มขนาดเสมอ</string>
+ <string name="site_settings_image_width_hint">เปลี่ยนขนาดรูปภาพในเรื่องมาที่ขนาดความกว้างนี้</string>
+ <string name="site_settings_format_hint">ตั้งค่ารูปแบบเรื่องใหม่</string>
+ <string name="site_settings_category_hint">ตั้งค่าหมวดหมู่เรื่องใหม่</string>
+ <string name="site_settings_password_hint">เปลี่ยนรหัสผ่านของคุณ</string>
+ <string name="site_settings_username_hint">บัญชีผู้ใช้ปัจจุบัน</string>
+ <string name="site_settings_privacy_hint">กำหนดว่าใครสามารถเห็นเว็บของคุณได้</string>
+ <string name="site_settings_whitelist_known_summary">ความเห็นจากผู้ใช้ที่รู้จัก</string>
+ <string name="site_settings_whitelist_all_summary">ความเห็นจากผู้ใช้ทั้งหมด</string>
+ <string name="site_settings_threading_summary">%d ระดับ</string>
+ <string name="site_settings_privacy_private_summary">ส่วนตัว</string>
+ <string name="site_settings_privacy_hidden_summary">ซ่อน</string>
+ <string name="site_settings_delete_site_title">ลบเว็บ</string>
+ <string name="site_settings_privacy_public_summary">สาธารณะ</string>
+ <string name="site_settings_blacklist_title">บัญชีดำ</string>
+ <string name="site_settings_moderation_hold_title">กดค้างเพื่อจัดการ</string>
+ <string name="site_settings_multiple_links_title">ลิงก์ในความเห็น</string>
+ <string name="site_settings_whitelist_title">อนุมัติอัตโนมัติ</string>
+ <string name="site_settings_sort_by_title">เรียงตาม</string>
+ <string name="site_settings_account_required_title">ผู้ใช้ต้องเข้าสู่ระบบ</string>
+ <string name="site_settings_identity_required_title">ต้องประกอบด้วยชื่อและอีเมล</string>
+ <string name="site_settings_receive_pingbacks_title">รับ Pingbacks</string>
+ <string name="site_settings_send_pingbacks_title">ส่ง Pingbacks</string>
+ <string name="site_settings_allow_comments_title">อนุมัติความเห็น</string>
+ <string name="site_settings_default_format_title">รูปแบบค่าเริ่มต้น</string>
+ <string name="site_settings_default_category_title">หมวดหมู่ค่าเริ่มต้น</string>
+ <string name="site_settings_location_title">เปิดใช้งานบอกที่อยู่</string>
+ <string name="site_settings_address_title">ที่อยู่</string>
+ <string name="site_settings_title_title">หัวข้อเว็บ</string>
+ <string name="site_settings_tagline_title">คำอธิบาย</string>
+ <string name="site_settings_this_device_header">อุปกรณ์นี้</string>
+ <string name="site_settings_discussion_new_posts_header">ค่าเริ่มต้นสำหรับเรื่องใหม่</string>
+ <string name="site_settings_account_header">บัญชี</string>
+ <string name="site_settings_writing_header">การเขียน</string>
+ <string name="newest_first">ใหม่สุดก่อน</string>
+ <string name="site_settings_general_header">ทั่วไป</string>
+ <string name="related_posts">เรื่องที่เกี่ยวข้อง</string>
+ <string name="comments">ความเห็น</string>
+ <string name="discussion">สนทนา</string>
+ <string name="privacy">ส่วนตัว</string>
+ <string name="close_after">ปิดหลังจาก</string>
+ <string name="oldest_first">เก่าสุดก่อน</string>
+ <string name="media_error_no_permission_upload">คุณไม่ได้รับอนุญาตให้อัปโหลดไฟล์สื่อมาที่เว็บนี้</string>
+ <string name="never">ไม่เคย</string>
+ <string name="unknown">ไม่รู้จัก</string>
+ <string name="reader_err_get_post_not_found">ไม่มีเรื่องนี้อยู่อีกแล้ว</string>
+ <string name="reader_err_get_post_not_authorized">คุณไม่มีสิทธิที่จะดูเรื่องนี้</string>
+ <string name="reader_err_get_post_generic">ไม่สามารถดึงข้อมูลเรื่องนี้</string>
+ <string name="blog_name_no_spaced_allowed">ที่อยู่เว็บไม่สามารถมีที่ว่างได้</string>
+ <string name="invalid_username_no_spaces">ชื่อผู้ใช้ไม่สามารถมีที่ว่างได้</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">เว็บที่คุณติดตามยังไม่มีเรื่องใหม่เมื่อเร็ว ๆ มานี้</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">ไม่มีเรื่องใหม่</string>
+ <string name="media_details_copy_url_toast">คัดลอก URL ไปยังคลิปบอร์ด</string>
+ <string name="edit_media">แก้ไขไฟล์สื่อ</string>
+ <string name="media_details_copy_url">คัดลอก URL</string>
+ <string name="media_details_label_date_uploaded">อัปโหลดแล้ว</string>
+ <string name="media_details_label_date_added">เพิ่มแล้ว</string>
+ <string name="selected_theme">ธีมที่เลือก</string>
+ <string name="could_not_load_theme">ไม่สามารถโหลดธีม</string>
+ <string name="theme_activation_error">มีบางสิ่งผิดปกติ ไม่สามารถเปิดใช้ธีม</string>
+ <string name="theme_by_author_prompt_append">โดย %1$s</string>
+ <string name="theme_prompt">ขอบคุณที่เลือกใช้ %1$s</string>
+ <string name="theme_try_and_customize">ทดลองและปรับแต่ง</string>
+ <string name="theme_view">ดู</string>
+ <string name="theme_details">รายละเอียด</string>
+ <string name="theme_support">สนับสนุน</string>
+ <string name="theme_done">เสร็จแล้ว</string>
+ <string name="theme_manage_site">จัดการเว็บไซต์</string>
+ <string name="title_activity_theme_support">ธีม</string>
+ <string name="theme_activate">เปิดใช้งาน</string>
+ <string name="date_range_start_date">วันที่เริ่มต้น</string>
+ <string name="date_range_end_date">วันที่สิ้นสุด</string>
+ <string name="current_theme">ธีมปัจจุบัน</string>
+ <string name="customize">ปรับแต่ง</string>
+ <string name="details">รายละเอียด</string>
+ <string name="support">สนับสนุน</string>
+ <string name="active">ใช้งานได้</string>
+ <string name="stats_referrers_spam_generic_error">มีบางสิ่งผิดปกติในระหว่างกระทำการ ดังนั้นสถานะสแปมจึงไม่เปลี่ยน</string>
+ <string name="stats_referrers_marking_not_spam">บันทึกว่าไม่ใช่สแปม</string>
+ <string name="stats_referrers_unspam">ไม่ใช่สแปม</string>
+ <string name="stats_referrers_marking_spam">บันทึกเป็นสแปม</string>
+ <string name="post_published">เผยแพร่เรื่องแล้ว</string>
+ <string name="page_published">เผยแพร่หน้าแล้ว</string>
+ <string name="post_updated">อัปเดตเรื่องแล้ว</string>
+ <string name="page_updated">อัปเดตหน้าแล้ว</string>
+ <string name="stats_referrers_spam">สแปม</string>
+ <string name="theme_auth_error_authenticate">ล้มเหลวในการดึงธีม: ผู้ใช้ไม่มีสิทธินี้</string>
+ <string name="theme_no_search_result_found">ขอโทษครับ ไม่พบธีม</string>
+ <string name="media_dimensions">ขนาดมิติ: %s</string>
+ <string name="media_uploaded_on">อัปโหลดแล้วบน: %s</string>
+ <string name="media_file_name">ชื่อไฟล์: %s</string>
+ <string name="upload_queued">เข้าคิวแล้ว</string>
+ <string name="media_file_type">ประเภทไฟล์: %s</string>
+ <string name="reader_label_gap_marker">โหลดเรื่องเพิ่มเติม</string>
+ <string name="notifications_no_search_results">ไม่มีเว็บใดตรงกับ \'%s\'</string>
+ <string name="search_sites">ค้นหาเว็บ</string>
+ <string name="notifications_empty_view_reader">ดูการอ่าน</string>
+ <string name="unread">ยังไม่ได้อ่าน</string>
+ <string name="notifications_empty_action_followers_likes">ได้การเตือน: ความเห็นบนเรื่องที่คุณอ่าน</string>
+ <string name="notifications_empty_action_comments">เข้าร่วมการสนทนา: ความเห็นบนเรื่องจากบล็อกที่คุณติดตาม</string>
+ <string name="notifications_empty_action_unread">จุดประกายให้การสนทนา: เขียนเรื่องใหม่</string>
+ <string name="notifications_empty_action_all">ได้การเตือนเสมอ ความเห็นบนเรื่องจากบล็อกที่คุณติดตาม</string>
+ <string name="notifications_empty_likes">ยังไม่มีการชื่นชอบใหม่ให้แสดง</string>
+ <string name="notifications_empty_followers">ยังไม่มีผู้ติดตามใหม่ให้รายงาน</string>
+ <string name="notifications_empty_comments">ยังไม่มีความเห็นใหม่</string>
+ <string name="notifications_empty_unread">คุณตามทันทั้งหมดแล้ว</string>
+ <string name="stats_widget_error_jetpack_no_blogid">โปรดเข้าดูสถิติในแอพ และลองเพิ่มวิดเจ็ตภายหลัง</string>
+ <string name="stats_widget_error_readd_widget">โปรดลบวิดเจ๊ตออกและลองเพิ่มใหม่อีกครั้ง</string>
+ <string name="stats_widget_error_no_visible_blog">สถิติไม่สามารถทำงานโดยไม่มีบล็อกที่มองเห็นได้</string>
+ <string name="stats_widget_error_no_permissions">บัญชี WordPress.com ของคุณไม่สามารถเข้าถึงสถิติของบล็อกนี้</string>
+ <string name="stats_widget_error_no_account">โปรดเข้าสู่ระบบเวิร์ดเพรส</string>
+ <string name="stats_widget_error_generic">ไม่สามารถโหลดสถิติ</string>
+ <string name="stats_widget_loading_data">กำลังโหลดข้อมูล...</string>
+ <string name="stats_widget_name_for_blog">สถิติวันนี้สำหรับ %1$s</string>
+ <string name="stats_widget_name">สถิติเวิร์ดเพรสวันนี้</string>
+ <string name="add_location_permission_required">ต้องได้การอนุญาตก่อนจะเพิ่มที่อยู่</string>
+ <string name="add_media_permission_required">ต้องได้การอนุญาตก่อนจะเพิ่มไฟล์สื่อ</string>
+ <string name="access_media_permission_required">ต้องได้การอนุญาตก่อนจะเข้าถึงไฟล์สื่อ</string>
+ <string name="stats_enable_rest_api_in_jetpack">เพื่อดูสถิติของคุณ ให้เปิดใช้งาน JSON API module ใน Jetpack</string>
+ <string name="error_open_list_from_notification">เรื่องหรือหน้านี้เผยแพร่แล้วบนเว็บอื่น</string>
+ <string name="reader_short_comment_count_multi">%s ความคิดเห็น</string>
+ <string name="reader_short_comment_count_one">1 ความคิดเห็น</string>
+ <string name="reader_label_submit_comment">ส่ง</string>
+ <string name="reader_hint_comment_on_post">ตอบกลับไปที่เรื่อง....</string>
+ <string name="reader_discover_visit_blog">เยี่ยมชม %s</string>
+ <string name="reader_discover_attribution_blog">เริ่มต้นเขียนบน %s</string>
+ <string name="reader_discover_attribution_author">เริ่มต้นเขียนโดย %s</string>
+ <string name="reader_discover_attribution_author_and_blog">เริ่มต้นเขียนโดย %1$s บน %2$s</string>
+ <string name="reader_short_like_count_multi">%s ไลค์</string>
+ <string name="reader_short_like_count_one">1 ไลค์</string>
+ <string name="reader_label_follow_count">%,d ผู้ติดตาม</string>
+ <string name="reader_short_like_count_none">ไลค์</string>
+ <string name="reader_menu_tags">แก้ไขป้ายกำกับและบล็อก</string>
+ <string name="reader_title_post_detail">การอ่านเรื่อง</string>
+ <string name="local_draft_explainer">เรื่องนี้เป็นฉบับร่างบนอุปกรณ์ที่ยังไม่ได้เผยแพร่</string>
+ <string name="local_changes_explainer">เรื่องนี้มีการเปลี่ยนแปลงบนอุปกรณ์ที่ยังไม่ได้เผยแพร่</string>
+ <string name="notifications_push_summary">การตั้งค่าสำหรับการเตือนที่ปรากฎบนอุปกรณ์ของคุณ</string>
+ <string name="notifications_email_summary">การตั้งค่าสำหรับการเตือนที่ส่งไปยังอีเมล์ที่ผูกกับบัญชีของคุณ</string>
+ <string name="notifications_tab_summary">การตั้งค่าสำหรับการเตือนที่ปรากฎในแทบคำเตือน</string>
+ <string name="notifications_disabled">ปิดการเตือนแอพแล้ว แตะที่นี่เพื่อเปิดใช้พวกมันในการตั้งค่า</string>
+ <string name="notification_types">ประเภทการเตือน</string>
+ <string name="error_loading_notifications">ไม่สามารถโหลดการตั้งค่าการเตือน</string>
+ <string name="replies_to_your_comments">ตอบกลับไปที่ความเห็นของคุณ</string>
+ <string name="comment_likes">ความเห็นเช่น</string>
+ <string name="app_notifications">การเตือนแอพ</string>
+ <string name="notifications_tab">แทบการเตือน</string>
+ <string name="email">อีเมล</string>
+ <string name="notifications_comments_other_blogs">ความเห็นบนเว็บอื่น</string>
+ <string name="notifications_wpcom_updates">การอัปเดตของ WordPress.com</string>
+ <string name="notifications_other">อื่น ๆ</string>
+ <string name="notifications_account_emails">อีเมลจาก WordPress.com</string>
+ <string name="notifications_account_emails_summary">เรามักจะส่งอีเมล์ที่สำคัญไปยังอีเมล์ของคุณเสมอ แต่คุณสามารถได้อีเมล์ที่มีประโยชน์เพิ่มได้เหมือนกัน</string>
+ <string name="your_sites">เว็บของคุณ</string>
+ <string name="notifications_sights_and_sounds">แบบมองเห็นและเสียง</string>
+ <string name="stats_insights_latest_post_trend">มันเป็นเวลา %1$s ตั้งแต่ %2$s ได้เผยแพร่ นี่เป็นวิธีที่เรื่องจะถูกแสดงจนถึงตอนนี้…</string>
+ <string name="stats_insights_latest_post_summary">สรุปเรื่องล่าสุด</string>
+ <string name="button_revert">คืนกลับ</string>
+ <string name="days_ago">%d วันที่แล้ว</string>
+ <string name="yesterday">เมื่อวานนี้</string>
+ <string name="connectionbar_no_connection">ไม่มีการเชื่อมต่อ</string>
+ <string name="stats_no_activity_this_period">ไม่มีกิจกรรมในช่วงเวลานี้</string>
+ <string name="trashed">เปลี่ยนเป็นขยะแล้ว</string>
+ <string name="button_back">กลับไป</string>
+ <string name="page_deleted">ลบหน้าแล้ว</string>
+ <string name="button_stats">สถิติ</string>
+ <string name="button_trash">ขยะ</string>
+ <string name="button_preview">ดูก่อน</string>
+ <string name="button_view">ดู</string>
+ <string name="button_edit">แก้ไข</string>
+ <string name="button_publish">เผยแพร่</string>
+ <string name="post_deleted">ลบหน้าแล้ว</string>
+ <string name="post_trashed">เรื่องที่ส่งไปถังขยะ</string>
+ <string name="page_trashed">หน้าที่ส่งไปถังขยะ</string>
+ <string name="my_site_no_sites_view_subtitle">คุณต้องการจะเพิ่มซักหนึ่งอย่าง</string>
+ <string name="my_site_no_sites_view_title">คุณยังไม่มีเว็บเวิร์ดเพรส</string>
+ <string name="my_site_no_sites_view_drake">ภาพประกอบ</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">คุณไม่มีสิทธิเข้าถึงบล็อกนี้</string>
+ <string name="reader_toast_err_follow_blog_not_found">ไม่สามารถค้นหาบล็อกนี้</string>
+ <string name="undo">ย้อนกลับ</string>
+ <string name="tabbar_accessibility_label_my_site">เว็บของฉัน</string>
+ <string name="tabbar_accessibility_label_me">ฉัน</string>
+ <string name="passcodelock_prompt_message">ใส่ PIN ของคุณ</string>
+ <string name="editor_toast_changes_saved">บันทึกการเปลี่ยนแปลงแล้ว</string>
+ <string name="push_auth_expired">การร้องขอหมดอายุแล้ว ลงทะเบียนเข้าสู่ WordPress.com เพื่อลองอีกครั้ง</string>
+ <string name="stats_insights_best_ever">การดูที่ดีที่สุด</string>
+ <string name="ignore">ไม่สนใจ</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% ของการดู</string>
+ <string name="stats_insights_most_popular_hour">ชั่วโมงยอดนิยมที่สุด</string>
+ <string name="stats_insights_most_popular_day">วันยอดนิยมที่สุด</string>
+ <string name="stats_insights_popular">วันและชั่วโมงยอดนิยมที่สุด</string>
+ <string name="stats_insights_today">สถิติวันนี้</string>
+ <string name="stats_insights_all_time">เรื่อง การดูและผู้เยี่ยมชมของเวลาทั้งหมด</string>
+ <string name="stats_insights">แบบละเอียด</string>
+ <string name="stats_sign_in_jetpack_different_com_account">เพื่อดูสถิติของคุณ ลงทะเบียนเข้าสู่บัญชี WordPress.com ที่คุณเคยเชื่อมต่อ Jetpack</string>
+ <string name="stats_other_recent_stats_moved_label">กำลังหาสถิติล่าสุดอื่น ๆ ของคุณ? เราได้ย้ายมันไปยังหน้ารายละเอียด (insight page)</string>
+ <string name="me_disconnect_from_wordpress_com">ตัดการติดต่อกับ WordPress.com</string>
+ <string name="me_btn_login_logout">เข้าสู่ระบบ/ออกจากระบบ</string>
+ <string name="me_connect_to_wordpress_com">เชื่อมต่อกับ WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">ไม่ได้ซ่อน "%s" เพราะว่ามันเป็นเว็บปัจจุบัน</string>
+ <string name="me_btn_support">ช่วยเหลือและสนับสนุน</string>
+ <string name="site_picker_create_dotcom">สร้างเว็บ WordPress.com</string>
+ <string name="site_picker_edit_visibility">แสดง/ซ่อน เว็บไซต์</string>
+ <string name="site_picker_add_self_hosted">เพิ่มเว็บไซต์บนโฮสท์ติดตั้งเอง</string>
+ <string name="site_picker_add_site">เพิ่มเว็บไซต์</string>
+ <string name="my_site_btn_switch_site">สลับเว็บไซต์</string>
+ <string name="site_picker_title">เลือกเว็บไซต์</string>
+ <string name="my_site_btn_view_site">ดูเว็บไซต์</string>
+ <string name="my_site_btn_view_admin">ดูผู้ควบคุม</string>
+ <string name="my_site_header_publish">เผยแพร่</string>
+ <string name="my_site_header_look_and_feel">หน้าตาและสัมผัส (Look and Feel)</string>
+ <string name="my_site_btn_site_settings">ตั้งค่า</string>
+ <string name="my_site_btn_blog_posts">เรื่องบล็อก</string>
+ <string name="reader_label_new_posts_subtitle">แตะเพื่อแสดงมัน</string>
+ <string name="my_site_header_configuration">การปรับแต่ง</string>
+ <string name="notifications_account_required">ลงทะเบียนเข้าสู่ WordPress.com สำหรับการเตือน</string>
+ <string name="stats_unknown_author">ผู้เขียนนิรนาม</string>
+ <string name="signout">ตัดการเชื่อมต่อ</string>
+ <string name="image_added">รูปที่เพิ่มแล้ว</string>
+ <string name="sign_out_wpcom_confirm">ตัดการเชื่อมต่อบัญชีของคุณจะลบข้อมูบทั้งหมดของ @%s’s WordPress.com ออกจากอุปกรณ์นี้ รวมถึงฉบับร่างและการเปลี่ยนแปลงในเครื่องด้วย</string>
+ <string name="select_all">เลือกทั้งหมด</string>
+ <string name="hide">ซ่อน</string>
+ <string name="show">แสดง</string>
+ <string name="deselect_all">ไม่เลือกทั้งหมด</string>
+ <string name="select_from_new_picker">การเลือกหลายสิ่งด้วยตัวเลือกแบบใหม่</string>
+ <string name="no_blog_videos">ไม่มีวีดีโอ</string>
+ <string name="no_media_sources">ไม่สามารถดึงข้อมูลไฟล์สื่อ</string>
+ <string name="loading_blog_videos">กำลังดึงข้อมูลวีดีโอ</string>
+ <string name="loading_blog_images">กำลังดึงข้อมูลรูปภาพ</string>
+ <string name="error_loading_videos">ผิดพลาดในการโหลดวีดีโอ</string>
+ <string name="error_loading_images">ผิดพลาดในการโหลดรูปภาพ</string>
+ <string name="error_loading_blog_images">ไม่สามารถดึงข้อมูลรูปภาพ</string>
+ <string name="error_loading_blog_videos">ไม่สามารถดึงข้อมูลวีดีโอ</string>
+ <string name="no_device_images">ไม่มีรูปภาพ</string>
+ <string name="no_blog_images">ไม่มีรูปภาพ</string>
+ <string name="no_device_videos">ไม่มีวีดีโอ</string>
+ <string name="stats_generic_error">สถิติที่ต้องการไม่สามารถแสดงผลได้</string>
+ <string name="no_media">ไม่มีไฟล์สื่อ</string>
+ <string name="loading_images">กำลังโหลดรูปภาพ</string>
+ <string name="loading_videos">กำลังโหลดวีดีโอ</string>
+ <string name="invalid_verification_code">โค้ดยืนยันไม่ถูกต้อง</string>
+ <string name="verification_code">โค้ดยืนยัน</string>
+ <string name="editor_toast_invalid_path">ที่อยู่ไฟล์ไม่ถูกต้อง</string>
+ <string name="error_publish_no_network">ไม่สามารถเผยแพร่ในขณะที่ไม่มีการเชื่อมต่อ บันทึกเป็นฉบับร่าง</string>
+ <string name="two_step_footer_label">ใส่โค้ดจากแอพยืนยันตัวตนของคุณ</string>
+ <string name="verify">ยืนยัน</string>
+ <string name="auth_required">เข้าสู่ระบบอีกครั้งเพื่อทำต่อ</string>
+ <string name="sign_in_jetpack">เข้าสู่ระบบบัญชี WordPress.com ของคุณเพื่อเชื่อมต่อกับ Jetpack</string>
+ <string name="two_step_sms_sent">ตรวจสอบข้อความของคุณสำหรับโค้ดยืนยัน</string>
+ <string name="tab_title_site_videos">วีดีโอเว็บไซต์</string>
+ <string name="two_step_footer_button">ส่งโค้ดผ่านทางข้อความ</string>
+ <string name="tab_title_site_images">รูปภาพเว็บไซต์</string>
+ <string name="tab_title_device_images">รูปภาพอุปกรณ์</string>
+ <string name="tab_title_device_videos">วีดีโออุปกรณ์</string>
+ <string name="take_video">ถ่ายวีดีโอ</string>
+ <string name="take_photo">ถ่ายรูป</string>
+ <string name="media_picker_title">เลือกไฟล์สื่อ</string>
+ <string name="add_to_post">เพิ่มไปยังเรื่อง</string>
+ <string name="device">อุปกรณ์</string>
+ <string name="language">ภาษา</string>
+ <string name="media_details_label_file_type">ประเภทไฟล์</string>
+ <string name="media_details_label_file_name">ชื่อไฟล์</string>
+ <string name="stats_empty_search_terms_desc">เรียนรู้เพิ่มเติมเกี่ยวกับการหาคนเข้าเว็บของคุณโดยดูที่เงื่อนไขที่ผู้เยี่ยมชมค้นหาสำหรับเว็บไซต์ของคุณ</string>
+ <string name="stats_followers_total_wpcom_paged">กำลังแสดง %1$d - %2$d จาก %3$s ผู้ติดตาม WordPress.com</string>
+ <string name="media_fetching">กำลังรับข้อมูลไฟล์สื่อ...</string>
+ <string name="toast_err_post_uploading">ไม่สามารถเปิดเรื่องขณะที่มันกำลังอัปโหลดได้</string>
+ <string name="posts_fetching">กำลังรับข้อมูลเรื่อง...</string>
+ <string name="pages_fetching">กำลังรับข้อมูลหน้า...</string>
+ <string name="comments_fetching">กำลังรับข้อมูลความเห็น...</string>
+ <string name="stats_view_search_terms">เงื่อนไขการค้นหา</string>
+ <string name="stats_view_authors">ผู้เขียน</string>
+ <string name="stats_entry_search_terms">เงื่อนไขการค้นหา</string>
+ <string name="stats_empty_search_terms">ไม่มีบันทึกเงื่อนไขการค้นหา</string>
+ <string name="stats_followers_total_email_paged">กำลังแสดง %1$d - %2$d จาก %3$s ผู้ติดตามอีเมล์</string>
+ <string name="stats_search_terms_unknown_search_terms">เงื่อนไขการค้นหาที่ไม่รู้จัก</string>
+ <string name="reader_empty_posts_request_failed">ไม่สามารถดึงเรื่องได้</string>
+ <string name="publisher">ผู้เผยแพร่:</string>
+ <string name="error_notification_open">ไม่สามารถเปิดคำเตือนได้</string>
+ <string name="logs_copied_to_clipboard">ปูมแอพพลิเคชั่นถูกคัดลอกลงคลิปบอร์ด</string>
+ <string name="stats_months_and_years">เดือนและปี</string>
+ <string name="error_copy_to_clipboard">มีความผิดพลาดเกิดขึ้นขณะกำลังคัดลอกข้อความไปยังคลิปบอร์ด</string>
+ <string name="stats_recent_weeks">สัปดาห์ที่ผ่านมา</string>
+ <string name="stats_average_per_day">เฉลี่ยต่อวัน</string>
+ <string name="reader_empty_posts_in_blog">บล็อกนี้ว่างเปล่า</string>
+ <string name="reader_label_new_posts">เรื่องใหม่</string>
+ <string name="stats_period">ช่วงเวลา</string>
+ <string name="stats_overall">ทั้งหมด</string>
+ <string name="stats_total">ทั้งหมด</string>
+ <string name="post_uploading">กำลังอัปโหลด</string>
+ <string name="reader_page_recommended_blogs">เว็บที่คุณอาจจะชอบ</string>
+ <string name="stats_comments_total_comments_followers">เรื่องทั้งหมดที่มีผู้ติดตามความเห็น: %1$s</string>
+ <string name="stats_empty_publicize_desc">ติดตามจำนวนผู้ติดตามจากบริการโซเชียลเน็ตเวิร์คโดยใช้การเผยแพร่</string>
+ <string name="stats_empty_followers_desc">ติดตามจำนวนผู้ติดตามทั้งหมดของคุณ และดูว่าแต่ละคนใช้เวลานานเท่าไหร่บนเว็บของคุณ</string>
+ <string name="stats_empty_geoviews_desc">ดูรายการเพื่อดูว่าประเทศไหนและทวีปไหนที่มีคนเข้าชมเว็บคุณมากที่สุด</string>
+ <string name="stats_empty_tags_and_categories_desc">ดูภาพรวมของเรื่องที่ได้รับความนิยมมากที่สุดบนเว็บไซต์ของคุณ และผลตอบรับจากเรื่องยอดนิยมของคุณจากสัปดาห์ที่ผ่านมา</string>
+ <string name="stats_empty_top_posts_desc">ค้นหาว่าบทความไหนของคุณที่มีการดูมากที่สุด และตรวจสอบว่าแต่ละเรื่องและหน้าถูกแสดงเท่าไหร่ตามเวลา</string>
+ <string name="stats_empty_top_authors_desc">ติดตามการดูเรื่องของแต่ละผู้เขียน และเจาะลึกดูบทความยอดนิยมของแต่ละผู้เขียนได้</string>
+ <string name="stats_empty_video_desc">ถ้าคุณอัปโหลดไฟล์วีดีโอโดยใช้ VideoPress คุณสามารถดูได้ว่าไฟล์พวกนี้ถูกดูจำนวนเท่าไหร่</string>
+ <string name="stats_empty_comments_desc">ถ้าคุณอนุญาตการแสดงความเห็นบนเว็บไซต์ของคุณ ติดตามผู้ให้ความเห็นสูงสุดและค้นหาว่าบทความไหนที่ทำให้เกิดการสนทนาสูงสุด อ้างอิงจาก 1,000 ความเห็นล่าสุด</string>
+ <string name="stats_empty_publicize">ไม่มีบันทึกการเผยแพร่ผู้ติดตาม</string>
+ <string name="stats_empty_video">ไม่มีการเล่นไฟล์วีดีโอ</string>
+ <string name="stats_view_publicize">การเผยแพร่</string>
+ <string name="stats_other_recent_stats_label">สถิติที่ผ่านมาอื่น ๆ</string>
+ <string name="stats_for">สถิติสำหรับ %s</string>
+ <string name="themes_fetching">กำลังดึงข้อมูลธีม</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_total_email">ผู้ติดตามอีเมล์ทั้งหมด: %1$s</string>
+ <string name="stats_followers_seconds_ago">วินาทีที่แล้ว</string>
+ <string name="stats_followers_a_minute_ago">นาทีที่แล้ว</string>
+ <string name="stats_followers_days">%1$d วัน</string>
+ <string name="stats_followers_a_day">วัน</string>
+ <string name="stats_followers_hours">%1$d ชั่วโมง</string>
+ <string name="stats_followers_an_hour_ago">ชั่วโมงที่แล้ว</string>
+ <string name="stats_followers_minutes">%1$d นาที</string>
+ <string name="stats_followers_a_month">เดือน</string>
+ <string name="stats_followers_years">%1$d ปี</string>
+ <string name="stats_followers_a_year">ปี</string>
+ <string name="stats_followers_months">%1$d เดือน</string>
+ <string name="stats_view">การดู</string>
+ <string name="stats_view_all">ดูทั้งหมด</string>
+ <string name="stats_empty_followers">ไม่มีผู้ติดตาม</string>
+ <string name="stats_comments_by_posts_and_pages">โดยเรื่องและหน้า</string>
+ <string name="stats_comments_by_authors">โดยผู้เขียน</string>
+ <string name="stats_followers_total_wpcom">ผู้ติดตาม WordPress.com ทั้งหมด: %1$s</string>
+ <string name="stats_followers_email_selector">อีเมล์</string>
+ <string name="stats_empty_geoviews">ไม่มีบันทึกประเทศ</string>
+ <string name="stats_empty_referrers_desc">เรียนรู้เพิ่มเติมเกี่ยวกับความสามารถในการเห็นของเว็บของคุณโดยดูที่เว็บไซต์และเสิร์ชเอ็นจิน</string>
+ <string name="stats_totals_publicize">ผู้ติดตาม</string>
+ <string name="stats_entry_followers">ผู้ติดตาม</string>
+ <string name="stats_entry_publicize">บริการ</string>
+ <string name="stats_entry_top_commenter">ผู้เขียน</string>
+ <string name="stats_totals_followers">ตั้งแต่</string>
+ <string name="stats_entry_video_plays">วีดีโอ</string>
+ <string name="stats_empty_top_posts_title">ไม่มีการดูเรื่องหรือหน้า</string>
+ <string name="stats_empty_referrers_title">ไม่มีบันทึกการอ้างถึง</string>
+ <string name="stats_empty_clicks_title">ไม่มีการบันทึกการกด</string>
+ <string name="stats_empty_clicks_desc">เมื่อบทความของคุณรวมลิงก์ไปยังเว็บอื่น คุณจะเห็นว่าลิงก์ไหนที่ผู้เยี่ยมชมของคุณกดมากที่สุด</string>
+ <string name="stats_empty_tags_and_categories">ไม่มีการดูป้ายกำกับเรื่องหรือป้ายกำกับหน้า</string>
+ <string name="stats_visitors">ผู้เยี่ยมชม</string>
+ <string name="stats_views">การดู</string>
+ <string name="stats_timeframe_years">ปี</string>
+ <string name="stats_pagination_label">หน้า %1$s จาก %2$s</string>
+ <string name="stats_likes">ชื่นชอบ</string>
+ <string name="stats_view_countries">ประเทศ</string>
+ <string name="stats_view_followers">ผู้ติดตาม</string>
+ <string name="stats_view_videos">วีดีโอ</string>
+ <string name="stats_view_top_posts_and_pages">เรื่องและหน้า</string>
+ <string name="stats_entry_clicks_link">ลิงก์</string>
+ <string name="ssl_certificate_details">รายละเอียด</string>
+ <string name="sure_to_remove_account">ลบเว็บนี้?</string>
+ <string name="delete_sure_post">ลบเรื่องนี้</string>
+ <string name="delete_sure">ลบฉบับร่างนี้</string>
+ <string name="delete_sure_page">ลบหน้านี้</string>
+ <string name="cab_selected">%d ถูกเลือก</string>
+ <string name="media_gallery_date_range">กำลังแสดงไฟล์สื่อจาก %1$s ถึง %2$s</string>
+ <string name="confirm_delete_multi_media">ลบสิ่งที่ถูกเลือกหรือไม่?</string>
+ <string name="confirm_delete_media">ลบสิ่งที่ถูกเลือกหรือไม่?</string>
+ <string name="comment_reply_to_user">ตอบกลับไปที่ %s</string>
+ <string name="pages_empty_list">ยังไม่มีหน้า ทำไมไม่สร้างสักหนึ่งหน้า?</string>
+ <string name="media_empty_list_custom_date">ไม่มีไฟล์สื่อในช่วงเวลานี้</string>
+ <string name="posting_post">กำลังส่งเรื่อง "%s"</string>
+ <string name="signing_out">กำลังออกจากระบบ...</string>
+ <string name="older_two_days">เก่ากว่า 2 วัน</string>
+ <string name="older_last_week">เก่ากว่าหนึ่งอาทิตย์</string>
+ <string name="stats_no_blog">ไม่สามารถโหลดสถิติได้สำหรับบล็อกที่ต้องการ</string>
+ <string name="select_a_blog">เลือกเว็บไซต์เวิร์ดเพรส</string>
+ <string name="sending_content">กำลังอัปโหลดเนื้อหา %s</string>
+ <string name="uploading_total">กำลังอัปโหลด %1$d จาก %2$d</string>
+ <string name="mnu_comment_liked">ชื่นชอบแล้ว</string>
+ <string name="comment">ความเห็น</string>
+ <string name="comment_trashed">ความเห็นขยะ</string>
+ <string name="posts_empty_list">ยังไม่มีเรื่อง ทำไมไม่สร้างสักหนึ่งเรื่อง?</string>
+ <string name="reader_label_comment_count_single">หนึ่งความเห็น</string>
+ <string name="reader_label_comments_closed">ปิดการแสดงความเห็น</string>
+ <string name="reader_label_comments_on">ความเห็นบน</string>
+ <string name="reader_title_photo_viewer">%1$d จาก %2$d</string>
+ <string name="error_publish_empty_post">ไม่สามารถเผยแพร่เรื่องที่ไม่มีเนื้อหา</string>
+ <string name="error_refresh_unauthorized_posts">คุณไม่มีสิทธิ์ดูหรือแก้ไขเรื่อง</string>
+ <string name="error_refresh_unauthorized_pages">คุณไม่มีสิทธิ์ดูหรือแก้ไขหน้า</string>
+ <string name="error_refresh_unauthorized_comments">คุณไม่มีสิทธิ์ดูหรือแก้ไขความเห็น</string>
+ <string name="older_month">เก่ากว่าหนึ่งเดือน</string>
+ <string name="more">กว่า</string>
+ <string name="reader_empty_posts_liked">คุณยังไม่ได้ชื่นชอบเรื่องใด ๆ</string>
+ <string name="faq_button">คำถามที่พบบ่อย</string>
+ <string name="browse_our_faq_button">เรียกดูคำถามที่พบบ่อยของเรา</string>
+ <string name="nux_help_description">เยี่ยมชมศูนย์กลางช่วยเหลือเพื่อหาคำตอบของคำถามทั่ว ๆ ไป หรือเยี่ยมชมฟอรั่มเพื่อตั้งคำถามใหม่</string>
+ <string name="agree_terms_of_service">โดยการสร้างบัญชี คุณได้ตกลงยอมรับ %1$sเงื่อนไขการบริการ%2$s ที่น่าตื่นเต้นของเรา</string>
+ <string name="create_new_blog_wpcom">สร้างบล็อก WordPress.com</string>
+ <string name="new_blog_wpcom_created">สร้างบล็อก WordPress.com แล้ว</string>
+ <string name="reader_empty_comments">ยังไม่มีความเห็น</string>
+ <string name="reader_empty_posts_in_tag">ไม่มีเรื่องกับป้ายกำกับนี้</string>
+ <string name="reader_label_comment_count_multi">%,d ความเห็น</string>
+ <string name="reader_label_view_original">ดูบทความเริ่มต้น</string>
+ <string name="reader_label_like">ชื่นชอบ</string>
+ <string name="reader_empty_followed_blogs_title">คุณยังไม่ได้ติดตามเว็บใด ๆ</string>
+ <string name="reader_toast_err_generic">ไม่สามารถทำการกระทำนี้ได้</string>
+ <string name="reader_toast_err_block_blog">ไม่สามารถปิดกั้นบล็อกนี้</string>
+ <string name="reader_toast_blog_blocked">เรื่องจากบล็อกนี้จะไม่แสดงอีกต่อไป</string>
+ <string name="reader_menu_block_blog">ปิดกั้นบล็อกนี้</string>
+ <string name="contact_us">ติดต่อเรา</string>
+ <string name="hs__conversation_detail_error">อธิบายปัญหาที่คุณเห็น</string>
+ <string name="hs__new_conversation_header">การคุยช่วยเหลือ</string>
+ <string name="hs__conversation_header">การคุยช่วยเหลือ</string>
+ <string name="hs__username_blank_error">ใส่ชื่อที่ใช้งานได้</string>
+ <string name="hs__invalid_email_error">ใส่ที่อยู่อีเมลที่ใช้งานได้</string>
+ <string name="add_location">เพิ่มสถานที่</string>
+ <string name="current_location">สถานที่ปัจจุบัน</string>
+ <string name="search_location">ค้นหา</string>
+ <string name="edit_location">แก้ไข</string>
+ <string name="search_current_location">ค้นหาสถานที่</string>
+ <string name="preference_send_usage_stats">ส่งสถิติ</string>
+ <string name="preference_send_usage_stats_summary">ส่งข้อมูลการใช้งานอัตโนมัติเพื่อช่วยเราในการปรับปรุงเวิร์ดเพรสสำหรับแอนดรอยด์</string>
+ <string name="update_verb">อัปเดต</string>
+ <string name="schedule_verb">ตารางงาน</string>
+ <string name="reader_title_subs">ป้ายกำกับและบล็อก</string>
+ <string name="reader_page_followed_tags">ป้ายกำกับที่ติดตามแล้ว</string>
+ <string name="reader_empty_recommended_blogs">ไม่มีบล็อกแนะนำ</string>
+ <string name="reader_label_followed_blog">ติดตามบล็อกแล้ว</string>
+ <string name="reader_label_tag_preview">ติดป้ายกำกับ %s เรื่องแล้ว</string>
+ <string name="reader_toast_err_get_blog_info">ไม่สามารถแสดงบล็อกนี้</string>
+ <string name="reader_toast_err_already_follow_blog">คุณได้ติดตามบล็อกนี้แล้ว</string>
+ <string name="reader_toast_err_follow_blog">ไม่สามารถติดตามบล็อกนี้</string>
+ <string name="reader_toast_err_unfollow_blog">ไม่สามารถเลิกติดตามบล็อกนี้</string>
+ <string name="reader_title_blog_preview">การอ่านบล็อก</string>
+ <string name="reader_title_tag_preview">การอ่านป้ายกำกับ</string>
+ <string name="reader_page_followed_blogs">เว็บที่ติดตามแล้ว</string>
+ <string name="reader_hint_add_tag_or_url">ใส่ URL หรือ ป้ายกำกับเพื่อติดตาม</string>
+ <string name="ptr_tip_message">เคล็ดลับ: ดึงลงล่างเพื่อรีเฟรช</string>
+ <string name="saving">กำลังบันทึก</string>
+ <string name="media_empty_list">ไม่มีไฟล์สื่อ</string>
+ <string name="help">ช่วยเหลือ</string>
+ <string name="forgot_password">จำรหัสผ่านของคุณไม่ได้?</string>
+ <string name="forums">ฟอรั่ม</string>
+ <string name="help_center">ศูนย์กลางความช่วยเหลือ</string>
+ <string name="ssl_certificate_error">SSL certificate ใช้งานไม่ได้</string>
+ <string name="ssl_certificate_ask_trust">ถ้าคุณสามารถเชื่อมต่อกับเว็บนี้บ่อย ๆ โดยไม่มีปัญหา ความผิดพลาดนี้อาจหมายถึงมีบางคนพยายามเปลี่ยนแปลงเว็บทำให้คุณไม่สามารถดำเนินการต่อได้ คุณต้องการที่จะเชื่อใจการรับรองนี้หรือไม่?</string>
+ <string name="could_not_remove_account">ไม่สามารถลบเว็บ</string>
+ <string name="nux_cannot_log_in">เราไม่สามารถนำคุณเข้าสู่ระบบ</string>
+ <string name="username_exists">ผู้ใช้ชื่อนั้นมีอยู่แล้ว</string>
+ <string name="out_of_memory">อุปกรณ์ใช้ความจำหมดแล้ว</string>
+ <string name="mnu_comment_unspam">ไม่ใช่สแปม</string>
+ <string name="no_site_error">ไม่สามารถเชื่อมต่อกับเว็บเวิร์ดเพรส</string>
+ <string name="adding_cat_failed">การเพิ่มหมวดหมู่ล้มเหลว</string>
+ <string name="adding_cat_success">เพิ่มหมวดหมู่สำเร็จแล้ว</string>
+ <string name="cat_name_required">ต้องใส่ชื่อหมวดหมู่</string>
+ <string name="category_automatically_renamed">ชื่อหมวดหมู่ %1$s ใช้งานไม่ได้ มันถูกเปลี่ยนชื่อเป็น %2$s</string>
+ <string name="no_account">ไม่พบบัญชีเวิร์ดเพรส ลองใส่ซักหนึ่งบัญชีแล้วลองใหม่อีกครั้ง</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">ที่อยู่เว็บต้องสั้นกว่า 64 ตัวอักษร</string>
+ <string name="blog_name_contains_invalid_characters">ที่อยู่เว็บต้องไม่มีสัญลักษณ์ “_”</string>
+ <string name="blog_name_cant_be_used">คุณอาจจะไม่ใช้ที่อยู่เว็บนั้น</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">ที่อยู่เว็บต้องประกอบด้วยตัวอักษรเล็ก (a-z) และตัวเลข</string>
+ <string name="blog_name_exists">เว็บนั้นมีอยู่แล้ว</string>
+ <string name="blog_name_reserved">เว็บนั้นถูกสงวนไว้</string>
+ <string name="username_only_lowercase_letters_and_numbers">ชื่อผู้ใช้สามารถประกอบไปด้วยตัวอักษรเล็ก (a-z) และตัวเลข</string>
+ <string name="username_required">ใส่ชื่อผู้ใช้</string>
+ <string name="username_not_allowed">ชื่อผู้ใช้ไม่อนุญาต</string>
+ <string name="username_must_be_at_least_four_characters">ชื่อผู้ใช้ต้องมีอย่างน้อย 4 ตัวอักษร</string>
+ <string name="username_contains_invalid_characters">ชื่อผู้ใช้ต้องไม่มีตัวอักษร “_”</string>
+ <string name="username_must_include_letters">ชื่อผู้ใช้ต้องมีอย่างน้อย 1 ตัวอักษร (a-z)</string>
+ <string name="username_reserved_but_may_be_available">ชื่อผู้ใช้นั้นถูกสงวนไว้ในเวลานี้ แต่อาจจะว่างภายในสองสามวันข้างหน้า</string>
+ <string name="blog_name_required">ใส่ที่อยู่เว็บ</string>
+ <string name="blog_name_not_allowed">ไม่อนุญาตที่อยู่เว็บนั้น</string>
+ <string name="blog_name_must_be_at_least_four_characters">ที่อยู่เว็บต้องมีอย่างน้อย 4 ตัวอักษร</string>
+ <string name="blog_name_reserved_but_may_be_available">เว็บนั้นถูกสงวนไว้ในเวลานี้ แต่อาจจะว่างภายในสองสามวันข้างหน้า </string>
+ <string name="username_or_password_incorrect">คุณกรอกชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง</string>
+ <string name="passcode_wrong_passcode">PIN ไม่ถูกต้อง</string>
+ <string name="invalid_password_message">รหัสผ่านต้องมีอย่างน้อย 4 ตัวอักษร</string>
+ <string name="invalid_username_too_short">ชื่อผู้ใช้ต้องยาวกว่า 4 ตัวอักษร</string>
+ <string name="invalid_username_too_long">ชื่อผู้ใช้ต้องสั้นกว่า 61 ตัวอักษร</string>
+ <string name="error_delete_post">มีความผิดพลาดเกิดขึ้นขณะกำลังลบ %s</string>
+ <string name="theme_auth_error_message">โปรดแน่ใจว่าคุณมีสิทธิการตั้งค่า themes</string>
+ <string name="comments_empty_list">ไม่มีความเห็น</string>
+ <string name="stats_empty_comments">ยังไม่มีความเห็น</string>
+ <string name="stats_bar_graph_empty">สถิติยังไม่พร้อมให้ดู</string>
+ <string name="reply_failed">การตอบกลับล้มเหลว</string>
+ <string name="notifications_empty_list">ไม่มีการเตือน</string>
+ <string name="gallery_error">ไม่สามารถรับไฟล์สื่อ</string>
+ <string name="blog_not_found">มีความผิดพลาดเกิดขึ้นเมื่อทำการเข้าบล็อกนี้</string>
+ <string name="wait_until_upload_completes">รอจนกระทั่งการอัปโหลดเสร็จสิ้น</string>
+ <string name="theme_set_failed">ล้มเหลวในการตั้งค่า theme</string>
+ <string name="error_refresh_stats">ไม่สามารถโหลดใหม่สถิติในตอนนี้</string>
+ <string name="error_generic">มีความผิดพลาดเกิดขึ้น</string>
+ <string name="error_moderate_comment">มีความผิดพลาดเกิดขึ้นขณะทำการตรวจสอบความเห็น</string>
+ <string name="error_edit_comment">มีความผิดพลาดเกิดขึ้นขณะแก้ไขความเห็น</string>
+ <string name="error_upload">มีความผิดพลาดเกิดขึ้นขณะกำลังอัปโหลด %s</string>
+ <string name="error_load_comment">ไม่สามารถโหลดความเห็น</string>
+ <string name="error_downloading_image">การดาวน์โหลดรูปภาพผิดพลาด</string>
+ <string name="no_network_message">ไม่มีเครือข่ายที่ใช้งานได้ตอนนี้</string>
+ <string name="theme_fetch_failed">ล้มเหลวในการดึง themes</string>
+ <string name="sdcard_message">ต้องมี SD card เพื่อใช้ในการอัปโหลดไฟล์สื่อ</string>
+ <string name="error_refresh_posts">ไม่สามารถโหลดใหม่เรื่องในตอนนี้</string>
+ <string name="error_refresh_pages">ไม่สามารถโหลดใหม่หน้าในตอนนี้</string>
+ <string name="error_refresh_notifications">ไม่สามารถโหลดใหม่คำเตือนในตอนนี้</string>
+ <string name="error_refresh_comments">ไม่สามารถโหลดใหม่ความเห็นในตอนนี้</string>
+ <string name="invalid_email_message">ที่อยู่อีเมลของคุณไม่ถูกต้อง</string>
+ <string name="email_invalid">ใส่ที่อยู่อีเมลที่ใช้งานได้</string>
+ <string name="email_not_allowed">ที่อยู่อีเมลนั้นไม่อนุญาต</string>
+ <string name="email_exists">ที่อยู่อีเมลนั้นถูกใช้งานแล้ว</string>
+ <string name="blog_name_must_include_letters">ที่อยู่เว็บต้องมีตัวอักษรอย่างน้อย 1 ตัว (a-z)</string>
+ <string name="blog_name_invalid">ที่อยู่เว็บไม่ถูกต้อง</string>
+ <string name="blog_title_invalid">หัวข้อเว็บไม่ถูกต้อง</string>
+ <string name="reader_share_link">แบ่งปันลิงก์</string>
+ <string name="reader_toast_err_add_tag">ไม่สามารถเพิ่มป้ายกำกับนี้</string>
+ <string name="reader_toast_err_remove_tag">ไม่สามารถลบป้ายกำกับนี้</string>
+ <string name="required_field">ช่องที่ต้องการ</string>
+ <string name="new_media">ไฟล์สื่อใหม่</string>
+ <string name="view_site">ดูเว็บไซต์</string>
+ <string name="privacy_policy">นโยบายความเป็นส่วนตัว</string>
+ <string name="image_settings">การตั้งค่ารูปภาพ</string>
+ <string name="add_account_blog_url">ที่อยู่บล็อก</string>
+ <string name="wordpress_blog">บล็อกเวิร์ดเพรส</string>
+ <string name="error_blog_hidden">บล็อกนี้ถูกซ่อนและไม่สามารถโหลดได้ ให้เปิดใช้งานมันอีกครั้งในการตั้งค่าและลองดูอีกครั้ง</string>
+ <string name="fatal_db_error">มีความผิดพลาดเกิดขึ้นระหว่างการสร้างฐานข้อมูลแอพ โปรดลองติดตั้งแอพใหม่อีกครั้ง</string>
+ <string name="jetpack_message_not_admin">ต้องการปลั๊กอิน Jetpack สำหรับการใช้งานสถิติ ติดต่อผู้ควบคุมเว็บ</string>
+ <string name="pending_review">รอคอยการตรวจสอบ</string>
+ <string name="post_format">รูปแบบเรื่อง</string>
+ <string name="new_post">เรื่องใหม่</string>
+ <string name="category_parent">หมวดหมู่แม่ (ตัวเลือก):</string>
+ <string name="share_action_post">เรื่องใหม่</string>
+ <string name="share_action_media">คลังไฟล์สื่อ</string>
+ <string name="delete_draft">ลบฉบับร่าง</string>
+ <string name="preview_page">ดูก่อนหน้า</string>
+ <string name="preview_post">ดูก่อนเรื่อง</string>
+ <string name="comment_added">เพิ่มความเห็นสำเร็จแล้ว</string>
+ <string name="post_not_published">สถานะเรื่องยังไม่เผยแพร่</string>
+ <string name="page_not_published">สถานะหน้ายังไม่เผยแพร่</string>
+ <string name="view_in_browser">ดูในเบราว์เซอร์</string>
+ <string name="add_new_category">เพิ่มหมวดหมู่ใหม่</string>
+ <string name="category_name">ชื่อหมวดหมู่</string>
+ <string name="dlg_confirm_trash_comments">ต้องการส่งไปถังขยะ?</string>
+ <string name="trash_yes">ถังขยะ</string>
+ <string name="trash_no">เอาออกจากถังขยะ</string>
+ <string name="trash">ถังขยะ</string>
+ <string name="author_name">ชื่อผู้เขียน</string>
+ <string name="author_url">URL ผู้เขียน</string>
+ <string name="hint_comment_content">ความเห็น</string>
+ <string name="saving_changes">บันทึกการเปลี่ยนแปลง</string>
+ <string name="sure_to_cancel_edit_comment">ต้องการยกเลิกการแก้ไขความเห็นนี้?</string>
+ <string name="content_required">ต้องการความเห็น</string>
+ <string name="toast_comment_unedited">ยังไม่เปลี่ยนความเห็น</string>
+ <string name="delete_post">ลบเรื่อง</string>
+ <string name="delete_page">ลบหน้า</string>
+ <string name="comment_status_approved">อนุมัติแล้ว</string>
+ <string name="comment_status_unapproved">รอการตรวจสอบ</string>
+ <string name="comment_status_spam">สแปม</string>
+ <string name="edit_comment">แก้ไขความเห็น</string>
+ <string name="mnu_comment_approve">อนุมัติ</string>
+ <string name="mnu_comment_unapprove">ไม่อนุมัติ</string>
+ <string name="mnu_comment_spam">สแปม</string>
+ <string name="mnu_comment_trash">ถังขยะ</string>
+ <string name="comment_status_trash">ถังขยะ</string>
+ <string name="dlg_approving_comments">กำลังอนุมัติ</string>
+ <string name="dlg_spamming_comments">กำลังบันทึกเป็นสแปม</string>
+ <string name="dlg_trashing_comments">กำลังส่งไปถังขยะ</string>
+ <string name="edit_post">แก้ไขเรื่อง</string>
+ <string name="add_comment">เพิ่มความเห็น</string>
+ <string name="connection_error">การเชื่อมต่อผิดพลาด</string>
+ <string name="cancel_edit">ยกเลิกการแก้ไข</string>
+ <string name="site_address">ที่อยู่เว็บแบบติดตั้งบนโฮสท์เองของคุณ (URL)</string>
+ <string name="file_not_found">ไม่สามารถหาไฟล์สื่อสำหรับการอัปโหลด มันถูกลบหรือย้ายไปแล้วหรือไม่?</string>
+ <string name="post_settings">การตั้งค่าเรื่อง</string>
+ <string name="blog_removed_successfully">ลบเว็บสำเร็จแล้ว</string>
+ <string name="remove_account">ลบเว็บ</string>
+ <string name="themes_live_preview">ดูก่อนแบบสด</string>
+ <string name="theme_current_theme">theme ปัจจุบัน</string>
+ <string name="theme_premium_theme">theme พรีเมี่ยม</string>
+ <string name="link_enter_url_text">ลิงก์ข้อความ (ทางเลือก)</string>
+ <string name="create_a_link">สร้างลิงก์</string>
+ <string name="page_settings">การตั้งค่าหน้า</string>
+ <string name="local_draft">ฉบับร่างที่บันทึกในเครื่อง</string>
+ <string name="upload_failed">การอัปโหลดล้มเหลว</string>
+ <string name="select_categories">เลือกหมวดหมู่</string>
+ <string name="account_details">รายละเอียดบัญชี</string>
+ <string name="xmlrpc_error">ไม่สามารถเชื่อมต่อได้ โปรดใส่ที่อยู่ของ xmlrpc.php บนเว็บของคุณแล้วลองใหม่อีกครั้ง</string>
+ <string name="scaled_image_error">ใส่ค่าความกว้างที่ใช้งานได้</string>
+ <string name="post_not_found">มีความผิดพลาดเกิดขึ้นเมื่อทำการโหลดเรื่อง รีเฟรชเรื่องของคุณแล้วลองใหม่อีกครั้ง</string>
+ <string name="learn_more">เรียนเพิ่มเติม</string>
+ <string name="media_gallery_settings_title">ตั้งค่าคลังรูป</string>
+ <string name="media_gallery_image_order">การเรียงรูป</string>
+ <string name="media_gallery_num_columns">จำนวนคอลัมน์</string>
+ <string name="media_gallery_edit">แก้ไขคลังรูป</string>
+ <string name="media_error_no_permission">คุณไม่มีสิทธิที่จะดูคลังไฟล์รูป</string>
+ <string name="cannot_delete_multi_media_items">ไม่สามารถลบไฟล์สื่อบางไฟล์ในตอนนี้ โปรดลองใหม่ภายหลัง</string>
+ <string name="location_not_found">ไม่ทราบที่อยู่</string>
+ <string name="open_source_licenses">ลิขสิทธิ์โอเพ่นซอร์ส</string>
+ <string name="dlg_unapproving_comments">ไม่อนุมัติ</string>
+ <string name="category_slug">หมวดหมู่ slug (ตัวเลือก)</string>
+ <string name="category_desc">คำอธิบายหมวดหมู่ (ตัวเลือก)</string>
+ <string name="file_error_create">ไม่สามารถสร้างไฟล์ชั่วคราวสำหรับการอัปโหลดไฟล์สื่อ โปรดตรวจสอบว่าอุปกรณ์ของคุณมีพื้นที่ว่างเพียงพอสำหรับการอัปโหลด</string>
+ <string name="local_changes">การเปลี่ยนแปลงบนอุปกรณ์</string>
+ <string name="media_gallery_type_thumbnail_grid">ตารางรูปขนาดย่อ</string>
+ <string name="horizontal_alignment">การเรียงในแนวนอน</string>
+ <string name="http_credentials">HTTP credentials (ตัวเลือก)</string>
+ <string name="http_authorization_required">ต้องการการอนุญาติ</string>
+ <string name="reader_title_applog">log แอพพลิเคชั่น</string>
+ <string name="author_email">อีเมลผู้เขียน</string>
+ <string name="email_hint">ที่อยู่อีเมล</string>
+ <string name="email_cant_be_used_to_signup">คุณไม่สามารถใช้อีเมลนั้นในการลงทะเบียนได้ เรากำลังมีปัญหาเรื่องการที่ผู้ให้บริการปิดกั้นอีเมลบางส่วนของเรา กรุณาใช้ผู้ให้บริการอีเมลรายอื่น</string>
+ <string name="email_reserved">มีการใช้อีเมลนั้นแล้ว โปรดตรวจสอบกล่องเมลเข้าของคุณสำหรับเมลเปิดการใช้งาน ถ้าคุณไม่เปิดใช้งาน คุณสามารถลองอีกครั้งในอีกสองสามวันภายหลัง</string>
+ <string name="notifications_empty_all">ยังไม่มีคำเตือน</string>
+ <string name="deleting_page">กำลังลบหน้า</string>
+ <string name="deleting_post">กำลังลบเรื่อง</string>
+ <string name="share_url_post">แบ่งปันเรื่อง</string>
+ <string name="share_url_page">แบ่งปันหน้า</string>
+ <string name="share_link">แบ่งปันลิงก์</string>
+ <string name="creating_your_account">กำลังสร้างบัญชีของคุณ</string>
+ <string name="creating_your_site">กำลังสร้างเว็บของคุณ</string>
+ <string name="error_refresh_media">บางสิ่งผิดปกติในระหว่างโหลดใหม่คลังไฟล์สื่อ โปรดลองใหม่ภายหลัง</string>
+ <string name="reader_empty_posts_in_tag_updating">กำลังดึงข้อมูลเรื่อง...</string>
+ <string name="comment_spammed">ความเห็นนี้ถูกบันทึกเป็นสแปม</string>
+ <string name="cant_share_no_visible_blog">คุณสามารถแบ่งปันไปยังเวิร์ดเพรสได้ถ้าไม่มีบล็อกที่มองเห็นได้</string>
+ <string name="reader_likes_multi">%,d คนชอบสิ่งนี้</string>
+ <string name="reader_likes_you_and_multi">คุณและ %,d คนชอบสิ่งนี้</string>
+ <string name="reader_toast_err_get_comment">ไม่สามารถดึงข้อมูลความเห็นนี้</string>
+ <string name="reader_label_reply">ตอบกลับ</string>
+ <string name="video">วีดีโอ</string>
+ <string name="download">กำลังดาวน์โหลดไฟล์สื่อ</string>
+ <string name="pick_photo">เลือกรูปภาพ</string>
+ <string name="pick_video">เลือกวีดีโอ</string>
+ <string name="reader_toast_err_get_post">ไม่สามารถดึงข้อมูลเรื่องนี้</string>
+ <string name="select_time">เลือกเวลา</string>
+ <string name="reader_likes_you_and_one">คุณและอีกหนึ่งคนชอบสิ่งนี้</string>
+ <string name="select_date">เลือกวันที่</string>
+ <string name="account_two_step_auth_enabled">บัญชีนี้มีสองขั้นตอนในการเปิดใช้การยืนยัน เยี่ยมชมการตั้งค่าความปลอดภัยของคุณบน WordPress.com และสร้างรหัสผ่านเฉพาะแอพพลิเคชั่น</string>
+ <string name="validating_user_data">ยืนยันข้อมูลผู้ใช้</string>
+ <string name="validating_site_data">ยืนยันข้อมูลเว็บไซต์</string>
+ <string name="reader_empty_followed_blogs_description">ไม่ต้องเป็นห่วง เพียงแตะที่ไอคอนที่ด้านบนขวาเพื่อเริ่มการค้นหา</string>
+ <string name="nux_oops_not_selfhosted_blog">เข้าสู่ระบบของ WordPress.com</string>
+ <string name="password_invalid">คุณต้องการรหัสผ่านที่ปลอดภัยมากขึ้น โปรดแน่ใจว่าได้ใช้ตัวอักษร 7 ตัวหรือมากกว่า และผสมตัวอักษรตัวใหญ่ ตัวเล็ก ตัวเลข และสัญลักษณ์พิเศษในรหัสผ่าน</string>
+ <string name="nux_tap_continue">ดำเนินการต่อ</string>
+ <string name="nux_welcome_create_account">สร้างบัญชี</string>
+ <string name="nux_add_selfhosted_blog">เพิ่มเว็บที่ติดตั้งบนโฮสท์ของคุณ</string>
+ <string name="signing_in">กำลังเข้าสู่ระบบ...</string>
+ <string name="nux_tutorial_get_started_title">เริ่มใช้งาน</string>
+ <string name="username_invalid">ชื่อผู้ใช้ไม่ถูกต้อง</string>
+ <string name="connecting_wpcom">เชื่อมต่อไปยัง WordPress.com</string>
+ <string name="reader_toast_err_view_image">ไม่สามารถดูรูปได้</string>
+ <string name="reader_toast_err_url_intent">ไม่สามารถเปิด %s</string>
+ <string name="reader_empty_followed_tags">คุณยังไม่ได้ติดตามป้ายกำกับใด ๆ</string>
+ <string name="create_account_wpcom">สร้างบัญชีบน WordPress.com</string>
+ <string name="reader_likes_one">หนึ่งคนชอบสิ่งนี้</string>
+ <string name="reader_likes_only_you">คุณชอบสิ่งนี้</string>
+ <string name="reader_toast_err_comment_failed">ไม่สามารถเขียนความเห็นของคุณ</string>
+ <string name="reader_toast_err_tag_exists">คุณติดตามป้ายกำกับนี้แล้ว</string>
+ <string name="reader_toast_err_tag_invalid">นี่ไม่ใช่ป้ายกำกับที่ใช้งานได้</string>
+ <string name="reader_toast_err_share_intent">ไม่สามารถแบ่งปันได้</string>
+ <string name="reader_label_added_tag">เพิ่ม %s แล้ว</string>
+ <string name="reader_label_removed_tag">ลบ %s แล้ว</string>
+ <string name="reader_btn_share">แบ่งปัน</string>
+ <string name="reader_btn_follow">ติดตาม</string>
+ <string name="reader_btn_unfollow">กำลังติดตาม</string>
+ <string name="jetpack_message">การใช้งานสถิติต้องใช้ปลั๊กอิน Jetpack คุณต้องการติดตั้ง Jetpack หรือไม่</string>
+ <string name="jetpack_not_found">ไม่พบปลั๊กอิน Jetpack</string>
+ <string name="reader_untitled_post">(ไม่มีหัวข้อ)</string>
+ <string name="reader_share_subject">แบ่งปันจาก %s แล้ว</string>
+ <string name="media_add_popup_title">เพิ่มไปยังคลังสื่อ</string>
+ <string name="media_add_new_media_gallery">สร้างคลังรูปภาพ</string>
+ <string name="empty_list_default">รายชื่อนี้ว่างเปล่า</string>
+ <string name="select_from_media_library">เลือกจากคลังสื่อ</string>
+ <string name="limit_reached">มาถึงขีดจำกัดแล้ว คุณสามารถลองใหม่อีกหนึ่งนาทีภายหลัง การลองใหม่ก่อนหน้าหนึ่งนาทีจะเพิ่มเวลาที่คุณต้องรอก่อนพ้นการแบนให้ยาวไปอีก ถ้าคุณคิดว่าสิ่งนี้เป็นความผิดพลาด โปรดติดต่อฝ่ายช่วยเหลือ</string>
+ <string name="reader_hint_comment_on_comment">ตอบกลับไปที่ความเห็น...</string>
+ <string name="themes">Themes</string>
+ <string name="all">ทั้งหมด</string>
+ <string name="images">รูปภาพ</string>
+ <string name="unattached">ยังไม่ได้แนบ</string>
+ <string name="custom_date">วันที่ปรับแต่ง</string>
+ <string name="stats_entry_country">ประเทศ</string>
+ <string name="stats_entry_posts_and_pages">หัวข้อ</string>
+ <string name="stats_entry_tags_and_categories">กระทู้</string>
+ <string name="stats_entry_authors">ผู้เขียน</string>
+ <string name="stats_totals_views">ดู</string>
+ <string name="stats_totals_clicks">จำนวนที่กด</string>
+ <string name="stats_totals_plays">จำนวนที่เล่น</string>
+ <string name="stats_view_referrers">ผู้อ้างอิง</string>
+ <string name="stats_timeframe_today">วันนี้</string>
+ <string name="stats_timeframe_yesterday">เมื่อวาน</string>
+ <string name="stats_timeframe_days">วัน</string>
+ <string name="stats_timeframe_weeks">สัปดาห์</string>
+ <string name="stats_timeframe_months">เดือน</string>
+ <string name="share_action_title">เพิ่มไปที่...</string>
+ <string name="share_action">แบ่งปัน</string>
+ <string name="stats">สถิติ</string>
+ <string name="stats_view_visitors_and_views">ผู้เยี่ยมชมและการดู</string>
+ <string name="stats_view_clicks">การกด</string>
+ <string name="stats_view_tags_and_categories">ป้ายกำกับและหมวดหมู่</string>
+ <string name="themes_features_label">ลักษณะพิเศษ</string>
+ <string name="theme_activate_button">เปิดใช้</string>
+ <string name="theme_activating_button">การเปิดใช้</string>
+ <string name="theme_set_success">ตั้งค่า theme สำเร็จแล้ว</string>
+ <string name="media_edit_title_text">หัวข้อ</string>
+ <string name="media_edit_caption_text">คำบรรยาย</string>
+ <string name="media_edit_description_text">คำอธิบาย</string>
+ <string name="media_edit_title_hint">ใส่หัวข้อที่นี่</string>
+ <string name="media_edit_caption_hint">ใส่คำบรรยายที่นี่</string>
+ <string name="media_edit_description_hint">ใส่คำอธิบายที่นี่</string>
+ <string name="media_edit_success">อัปเดตแล้ว</string>
+ <string name="media_edit_failure">ล้มเหลวในการอัปเดต</string>
+ <string name="themes_details_label">รายละเอียด</string>
+ <string name="media_gallery_type_tiled">เรียงแบบกระเบื้อง</string>
+ <string name="media_gallery_type_circles">เรียงแบบวงกลม</string>
+ <string name="media_gallery_type_slideshow">สไลด์โชว์</string>
+ <string name="media_add_popup_capture_photo">จับภาพรูปภาพ</string>
+ <string name="media_add_popup_capture_video">จับภาพวีดีโอ</string>
+ <string name="media_gallery_image_order_random">สุ่ม</string>
+ <string name="media_gallery_image_order_reverse">ย้อนกลับ</string>
+ <string name="media_gallery_type">ประเภท</string>
+ <string name="media_gallery_type_squares">เรียงแบบสี่เหลี่ยม</string>
+ <string name="passcode_manage">จัดการ PIN lock</string>
+ <string name="passcode_enter_passcode">ใส่ PIN ของคุณ</string>
+ <string name="passcode_enter_old_passcode">ใส่ PIN เ่ก่าของคุณ</string>
+ <string name="passcode_re_enter_passcode">ใส่ PIN ของคุณอีกครั้ง</string>
+ <string name="passcode_change_passcode">เปลี่ยน PIN</string>
+ <string name="passcode_set">การตั้งค่า PIN</string>
+ <string name="theme_auth_error_title">ล้มเหลวในการรับ theme </string>
+ <string name="post_excerpt">คำย่อเรื่อง</string>
+ <string name="stats_entry_referrers">ลิงก์อ้างอิง</string>
+ <string name="passcode_preference_title">PIN ล็อค</string>
+ <string name="passcode_turn_off">ปิดการใช้ PIN ล็อค</string>
+ <string name="passcode_turn_on">เปิดการใช้ PIN ล็อค</string>
+ <string name="upload">อัปโหลด</string>
+ <string name="discard">เอาออก</string>
+ <string name="sign_in">เข้าสู่ระบบ</string>
+ <string name="notifications">การเตือน</string>
+ <string name="note_reply_successful">เผยแพร่การตอบกลับแล้ว</string>
+ <string name="new_notifications">%d คำเตือนใหม่</string>
+ <string name="more_notifications">และ %d เพิ่มเติม</string>
+ <string name="follows">ติดตาม</string>
+ <string name="loading">กำลังโหลดข้อมูล...</string>
+ <string name="httpuser">ชื่อผู้ใช้ HTTP</string>
+ <string name="httppassword">รหัสผ่าน HTTP</string>
+ <string name="error_media_upload">มีความผิดพลาดเกิดขึ้นขณะทำการอัปโหลดไฟล์สื่อ</string>
+ <string name="post_content">เนื้อหา (แตะเพื่อเพิ่มข้อความและไฟล์สื่อ)</string>
+ <string name="publish_date">เผยแพร่</string>
+ <string name="content_description_add_media">เพิ่มไฟล์สื่อ</string>
+ <string name="incorrect_credentials">ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง</string>
+ <string name="password">รหัสผ่าน</string>
+ <string name="username">ชื่อผู้ใช้</string>
+ <string name="reader">ผู้อ่าน</string>
+ <string name="pages">หน้า</string>
+ <string name="caption">ป้ายชื่อ (ทางเลือก)</string>
+ <string name="width">ความกว้าง</string>
+ <string name="posts">เรื่อง</string>
+ <string name="anonymous">นิรนาม</string>
+ <string name="page">หน้า</string>
+ <string name="post">เรื่อง</string>
+ <string name="featured">ใช้เป็นรูปเด่น</string>
+ <string name="featured_in_post">รวมรูปภาพในเนื้อหาเรื่อง</string>
+ <string name="no_network_title">ไม่มีเครือข่ายใช้งานได้</string>
+ <string name="ok">ตกลง</string>
+ <string name="blogusername">บล็อกชื่อผู้ใช้</string>
+ <string name="upload_scaled_image">อัปโหลดและลิงก์ไปยังภาพที่ปรับขนาด</string>
+ <string name="scaled_image">ปรับขนาดความกว้างรูปภาพแล้ว</string>
+ <string name="scheduled">กำหนดตารางเวลาแล้ว</string>
+ <string name="link_enter_url">URL</string>
+ <string name="version">รุ่น</string>
+ <string name="tos">เงื่อนไขบริการ</string>
+ <string name="app_title">เวิร์ดเพรสสำหรับแอนดรอยด์</string>
+ <string name="max_thumbnail_px_width">ความกว้างรูปค่าเริ่มต้น</string>
+ <string name="image_alignment">จัดหน้า</string>
+ <string name="refresh">รีเฟรช</string>
+ <string name="untitled">ไม่มีหัวข้อ</string>
+ <string name="edit">แก้ไข</string>
+ <string name="post_id">เลขที่เรื่อง</string>
+ <string name="page_id">เลขที่หน้า</string>
+ <string name="post_password">รหัสผ่านเรื่อง (ทางเลือก)</string>
+ <string name="immediately">ทันที</string>
+ <string name="quickpress_add_alert_title">ตั้งชื่อทางลัด</string>
+ <string name="today">วันนี้</string>
+ <string name="settings">ตั้งค่า</string>
+ <string name="share_url">แบ่งปัน URL</string>
+ <string name="quickpress_window_title">เลือกบล็อกจากทางลัด QuickPress</string>
+ <string name="quickpress_add_error">ชื่อปุ่มลัดไม่สามารถเว้นว่างได้</string>
+ <string name="publish_post">เผยแพร่</string>
+ <string name="draft">ฉบับร่าง</string>
+ <string name="post_private">ส่วนตัว</string>
+ <string name="upload_full_size_image">อัปโหลดและลิงก์ไปที่รูปฉบับเต็ม</string>
+ <string name="title">หัวข้อ</string>
+ <string name="categories">หมวดหมู่</string>
+ <string name="tags_separate_with_commas">ป้ายกำกับ (แยกป้ายกำกับด้วย commas)</string>
+ <string name="notification_vibrate">สั่น</string>
+ <string name="notification_blink">เปิดไฟกระพริบการแจ้งเตือน</string>
+ <string name="status">สถานะ</string>
+ <string name="location">สถานที่</string>
+ <string name="sdcard_title">ต้องการ SD Card</string>
+ <string name="select_video">เลือกวีดีโอจาก gallery</string>
+ <string name="media">ไฟล์สื่อ</string>
+ <string name="delete">ลบ</string>
+ <string name="none">ไม่มี</string>
+ <string name="blogs">บล็อก</string>
+ <string name="select_photo">เลือกรูปจาก gallery</string>
+ <string name="error">ผิดพลาด</string>
+ <string name="yes">ใช่</string>
+ <string name="no">ไม่ใช่</string>
+ <string name="cancel">ยกเลิก</string>
+ <string name="save">บันทึก</string>
+ <string name="add">เพิ่ม</string>
+ <string name="on">บน</string>
+ <string name="reply">ตอบกลับ</string>
+ <string name="category_refresh_error">โหลดหมวดหมู่อีกครั้งผิดพลาด</string>
+ <string name="preview">ดูก่อน</string>
+ <string name="notification_settings">การตั้งค่าการเตือน</string>
+</resources>
diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml
new file mode 100644
index 000000000..110e0013f
--- /dev/null
+++ b/WordPress/src/main/res/values-tr/strings.xml
@@ -0,0 +1,1146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_editor">Editör</string>
+ <string name="role_admin">Yönetici</string>
+ <string name="role_author">Yazar</string>
+ <string name="role_follower">Takipçi</string>
+ <string name="role_contributor">Katılımcı</string>
+ <string name="role_viewer">Görüntüleyici</string>
+ <string name="error_post_my_profile_no_connection">Bağlantı yok, profiliniz kaydedilemedi</string>
+ <string name="alignment_none">Hiçbiri</string>
+ <string name="alignment_left">Sol</string>
+ <string name="alignment_right">Sağ</string>
+ <string name="site_settings_list_editor_action_mode_title">%1$d seçildi</string>
+ <string name="error_fetch_users_list">Site kullanıcıları getirilemedi</string>
+ <string name="plans_manage">Planınızı yönetin:\nWordPress.com/plans</string>
+ <string name="people_empty_list_filtered_viewers">Henüz hiç izleyiciniz yok.</string>
+ <string name="title_follower">Takipçi</string>
+ <string name="title_email_follower">E-posta takipçisi</string>
+ <string name="people_fetching">Kullanıcılar getiriliyor...</string>
+ <string name="people_empty_list_filtered_email_followers">Henüz hiç e-posta takipçiniz yok.</string>
+ <string name="people_empty_list_filtered_followers">Henüz hiç takipçiniz yok.</string>
+ <string name="people_empty_list_filtered_users">Henüz hiç kullanıcınız yok.</string>
+ <string name="people_dropdown_item_email_followers">E-posta takipçileri</string>
+ <string name="people_dropdown_item_viewers">İzleyiciler</string>
+ <string name="people_dropdown_item_followers">Takipçiler</string>
+ <string name="people_dropdown_item_team">Takım</string>
+ <string name="invite_message_usernames_limit">10 taneye kadar e-posta adresi ve/veya WordPress.com kullanıcısı davet edin. Kullanıcı adına ihtiyacı olanlara, nasol oluşturacaklarına dair bilgilendirme gönderilecek.</string>
+ <string name="viewer_remove_confirmation_message">Bu izleyiciyi kaldırırsanız artık sitenizi ziyaret edemeyecek.\n\nHala bu kullanıcıyı kaldırmak istiyor musunuz?</string>
+ <string name="follower_remove_confirmation_message">Eğer kaldırılırsa, bu takipçi site hakkındaki bildirimleri almayı kesecek, tabi tekrar takibe başlayana kadar.\n\nHala bu takipçiyi kaldırmak istiyor musunuz?</string>
+ <string name="follower_subscribed_since">%1$s tarihinden beri</string>
+ <string name="reader_label_view_gallery">Galeriyi görüntüle</string>
+ <string name="error_remove_follower">Takipçi kaldırılamıyor</string>
+ <string name="error_remove_viewer">İzleyici kaldırılamıyor</string>
+ <string name="error_fetch_email_followers_list">Sitenin e-posta takipçileri getirilemedi</string>
+ <string name="error_fetch_followers_list">Site takipçileri getirilemedi</string>
+ <string name="editor_failed_uploads_switch_html">Bazı ortam yüklemeleri başarısız oldu. HTML moduna geçebilirsiniz\n Tüm başarısız yüklemeleri kaldırılıp devam edilsin mi?</string>
+ <string name="format_bar_description_html">HTML modu</string>
+ <string name="visual_editor">Görsel düzenleyici</string>
+ <string name="image_thumbnail">Görsel küçük resm</string>
+ <string name="format_bar_description_ul">Sırasız Liste</string>
+ <string name="format_bar_description_ol">Sıralı Liste</string>
+ <string name="format_bar_description_more">Daha fazla ekle</string>
+ <string name="format_bar_description_media">Ortam ekle</string>
+ <string name="format_bar_description_strike">Üzeri çizili</string>
+ <string name="format_bar_description_quote">Alıntı</string>
+ <string name="format_bar_description_link">Bağlantı ekle</string>
+ <string name="format_bar_description_italic">Eğik</string>
+ <string name="format_bar_description_underline">Altı çizgili</string>
+ <string name="image_settings_save_toast">Değişiklikler kaydedildi</string>
+ <string name="image_caption">Altyazı</string>
+ <string name="image_alt_text">Alternatif metin</string>
+ <string name="image_link_to">Bağlantı</string>
+ <string name="image_width">Genişlik</string>
+ <string name="format_bar_description_bold">Kalın</string>
+ <string name="image_settings_dismiss_dialog_title">Kaydedilmemiş değişiklikler atılsın mı?</string>
+ <string name="stop_upload_dialog_title">Yükleme durdurulsun mu?</string>
+ <string name="stop_upload_button">Yüklemeyi durdur</string>
+ <string name="alert_error_adding_media">Ortam eklenirken bir hata oluştu</string>
+ <string name="alert_action_while_uploading">Şu anda medya yüklüyorsunuz. Lütfen bu işlem tamamlanıncaya kadar bekleyin.</string>
+ <string name="alert_insert_image_html_mode">Ortam HTML modunda doğrudan ekleyemiyor. Lütfen tekrar görsel moduna geçin.</string>
+ <string name="uploading_gallery_placeholder">Galeri yükleniyor...</string>
+ <string name="invite_error_some_failed">Davet gönderildi fakat hata(lar) oluştu!</string>
+ <string name="invite_sent">Davet başarıyla gönderildi</string>
+ <string name="tap_to_try_again">Tekrar denemek için dokunun!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_error_sending">Davet gönderilmeye çalışılırken bir hata oluştu!</string>
+ <string name="invite_error_invalid_usernames_multiple">Gönderemiyor: Geçersiz kullanıcı adları veya e-postalar</string>
+ <string name="invite_error_invalid_usernames_one">Gönderemiyor: bir kullanıcı adı veya e-posta geçersiz</string>
+ <string name="invite_error_no_usernames">Lütfen en az bir kullanıcı adı ekleyin</string>
+ <string name="invite_message_info">(İsteğe bağlı) Kullanıcıya/Kullanıcılara göndereceğiniz davete 500 karaktere kadar özel bir mesaj ekleyebilirsiniz.</string>
+ <string name="invite_message_remaining_other">%d karakter kaldı</string>
+ <string name="invite_message_remaining_one">1 karakter kaldı</string>
+ <string name="invite_message_remaining_zero">0 karakter kaldı</string>
+ <string name="invite_invalid_email">\'%s\' e-posta adresi geçersiz</string>
+ <string name="invite_message_title">Özel Mesaj</string>
+ <string name="invite_already_a_member">\'%s\' kullanıcı adı ile zaten bir üye mevcut</string>
+ <string name="invite_username_not_found">\'%s\' kullanıcı adı için kullanıcı bulunamadı</string>
+ <string name="invite">Davet Et</string>
+ <string name="invite_names_title">Kullanıcı adları ya da e-postalar</string>
+ <string name="signup_succeed_signin_failed">Hesabınız oluşturuldu fakat oturumunuzu açarken bir problem oluştu\n Yeni oluşturulan hesabınıza kullanıcı adı ve parolanız ile giriş yapmayı deneyin.</string>
+ <string name="send_link">Bağlantı gönder</string>
+ <string name="my_site_header_external">Harici</string>
+ <string name="invite_people">İnsanları davet et</string>
+ <string name="label_clear_search_history">Arama geçmişini temizle</string>
+ <string name="dlg_confirm_clear_search_history">Arama geçmişini temizle?</string>
+ <string name="reader_empty_posts_in_search_description">Dilinizde %s için hiç yazı bulunamadı</string>
+ <string name="reader_label_post_search_running">Aranıyor...</string>
+ <string name="reader_label_related_posts">İlişkili okuma</string>
+ <string name="reader_empty_posts_in_search_title">Yazı bulunamadı</string>
+ <string name="reader_label_post_search_explainer">Tüm halka açık WordPress.com bloglarında ara</string>
+ <string name="reader_hint_post_search">WordPress.com üzerinde ara</string>
+ <string name="reader_title_related_post_detail">İlişkili yazı</string>
+ <string name="reader_title_search_results">%s için ara</string>
+ <string name="preview_screen_links_disabled">Önizleme ekranında bağlantılar etkisiz durumdadır</string>
+ <string name="draft_explainer">Bu yazı henüz yayınlanmamış bir taslak</string>
+ <string name="send">Gönder</string>
+ <string name="user_remove_confirmation_message">Eğer %1$s kullanıcısını kaldırırsanız, o kullanıcı bu siteye artık erişemez fakat %1$s kullanıcı tarafından oluşturulmuş içerik sitede kalır.\n\nHala bu kullanıcıyı kaldırmak istiyor musunuz?</string>
+ <string name="person_removed">\@%1$s başarıyla kaldırıldı</string>
+ <string name="person_remove_confirmation_title">%1$s kullanıcısını kaldır</string>
+ <string name="reader_empty_posts_in_custom_list">Bu listedeki siteler yakın zamanda herhangi bir gönderide bulunmadı</string>
+ <string name="people">İnsanlar</string>
+ <string name="edit_user">Kullanıcı düzenle</string>
+ <string name="role">Rol</string>
+ <string name="error_remove_user">Kullanıcı kaldırılamıyor</string>
+ <string name="error_update_role">Kullanıcı rolü güncellenemiyor</string>
+ <string name="error_fetch_viewers_list">Site izleyicileri getirilemedi</string>
+ <string name="gravatar_camera_and_media_permission_required">Bir fotoğraf seçmek ya da yakalamak için izinler gereklidir</string>
+ <string name="error_updating_gravatar">Gravatar\'ınızı güncellerken hata oluştu</string>
+ <string name="error_locating_image">Kırpılmış görseli konumlandırırken hata oluştu</string>
+ <string name="error_refreshing_gravatar">Gravatar\'ınız yenilenirken hata oluştu</string>
+ <string name="gravatar_tip">Yeni! Değiştirmek için Gravatar\'ınıza dokunun!</string>
+ <string name="error_cropping_image">Görsel kırpılırken hata oluştu</string>
+ <string name="launch_your_email_app">E-posta uygulamanızı başlatın</string>
+ <string name="checking_email">E-posta kontrol ediliyor</string>
+ <string name="not_on_wordpress_com">WordPress.com\'da değil mi?</string>
+ <string name="magic_link_unavailable_error_message">Şu an uygun değil. Lütfen parolanızı girin</string>
+ <string name="check_your_email">E-postanızı kontrol edin</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Anında giriş için e-postanıza gönderilen bağlantıyı edinin</string>
+ <string name="logging_in">Giriş yapılıyor</string>
+ <string name="enter_your_password_instead">Bunun yerine parola gir</string>
+ <string name="web_address_dialog_hint">Yorum yaptığınızda herkes tarafından görülebilir.</string>
+ <string name="jetpack_not_connected_message">Jetpack eklentisi yüklenmiş fakat WordPress.com\'a bağlanmamış. Jetpack\'i bağlamak ister misiniz?</string>
+ <string name="username_email">E-posta ya da kullanıcı adı</string>
+ <string name="jetpack_not_connected">Jetpack eklentisi bağlanmamış</string>
+ <string name="new_editor_reflection_error">Görsel düzenleyici cihazınız ile uyumsuz. Otomatik\n olarak etkisizleştirildi.</string>
+ <string name="stats_insights_latest_post_no_title">(başlıksız)</string>
+ <string name="capture_or_pick_photo">Fotoğraf seç ya da yakala</string>
+ <string name="plans_post_purchase_text_themes">Artık premium temalara sınırsız erişiminiz var. Başlamak için istediğiniz temayla sitenizde önizleme yapın.</string>
+ <string name="plans_post_purchase_button_themes">Temalara göz at</string>
+ <string name="plans_post_purchase_title_themes">Mükemmel Premium temayı bulun</string>
+ <string name="plans_post_purchase_button_video">Yeni yazıya başla</string>
+ <string name="plans_post_purchase_text_video">VideoPress ve genişletilmiş ortam dosyası alanı ile videolarınızı sitenize yükleyebilir ve barındırabilirsiniz.</string>
+ <string name="plans_post_purchase_title_video">Video ile yazılarınızı canlandırın</string>
+ <string name="plans_post_purchase_button_customize">Sitemi özelleştir</string>
+ <string name="plans_post_purchase_text_customize">Artık özel yazı tipleri, özel renkler ve özel CSS düzenleme becerilerine sahipsiniz.</string>
+ <string name="plans_post_purchase_text_intro">Siteniz sevinçten taklalar atıyor! Şimdi gidip sitenizde yeni ne özellikler var inceleyin ve nereden başlayacağınıza karar verin.</string>
+ <string name="plans_post_purchase_title_customize">Yazı tiplerini ve renkleri özelleştir</string>
+ <string name="plans_post_purchase_title_intro">Hepsi sizin, istediğiniz kadar!</string>
+ <string name="export_your_content_message">Yazılarınız, sayfalarınız ve ayarlarınız size %s adresine e-posta ile gönderilecek.</string>
+ <string name="plan">Plan</string>
+ <string name="plans">Planlar</string>
+ <string name="plans_loading_error">Planlar yüklenemiyor</string>
+ <string name="export_your_content">İçeriğinizi dışarı aktarın</string>
+ <string name="exporting_content_progress">İçerik dışarı aktarılıyor...</string>
+ <string name="export_email_sent">Dışarı aktarma e-postası gönderildi!</string>
+ <string name="premium_upgrades_message">Sitenizde etkin premium güncellemeleri var. Lütfen sitenizi silmeden önce güncellemeleri iptal edin.</string>
+ <string name="show_purchases">Satın almaları göster</string>
+ <string name="checking_purchases">Satın almalar kontrol ediliyor</string>
+ <string name="premium_upgrades_title">Premium güncellemeleri</string>
+ <string name="purchases_request_error">Bir şeyler ters gitti. Satın almalar için istek yapılamadı.</string>
+ <string name="delete_site_progress">Site siliniyor...</string>
+ <string name="delete_site_summary">Bu eylem geri alınamaz. Siteniz silindiğinde tüm içerik, katılımcılar ve alan adları silinecektir.</string>
+ <string name="delete_site_hint">Siteyi sil</string>
+ <string name="export_site_hint">İçeriğinizi XML dosyası ile dışarı aktarın</string>
+ <string name="are_you_sure">Emin misiniz?</string>
+ <string name="export_site_summary">Eminseniz lütfen biraz vakit ayırıp içeriğinizi dışarı aktarın. Aktarmazsanız ilerde geri getirmenin yolu olmayacak.</string>
+ <string name="keep_your_content">İçeriğinizi koruyun</string>
+ <string name="domain_removal_hint">Sitenizi kaldırdığınızda şu alan adları artık çalışmayacak</string>
+ <string name="domain_removal_summary">Dikkatli olun! Sitenizi silmek demek aşağıda listelenen alan adlarını da silmek anlamına geliyor.</string>
+ <string name="primary_domain">Birincil alan adı</string>
+ <string name="domain_removal">Alan adı kaldırma</string>
+ <string name="error_deleting_site_summary">Sitenizi silerken bir hata oluştu. Lütfen daha fazla destek için destek birimine başvurun</string>
+ <string name="error_deleting_site">Site silinirken hata</string>
+ <string name="confirm_delete_site_prompt">Lütfen aşağıdaki alana %1$s yazarak doğrulayın. Sonrasında siteniz sonsuza dek yok olacak.</string>
+ <string name="site_settings_export_content_title">İçeriği dışarı aktar</string>
+ <string name="contact_support">Destek ile iletişim kur</string>
+ <string name="confirm_delete_site">Site silmeyi onayla</string>
+ <string name="start_over_text">Eğer bir site istiyor fakat içindeki yazı ve sayfaları istemiyorsanız, destek ekibimiz sizin için yazıları, sayfaları, ortam dosyalarını ve yorumları silebilir\n\nBu sitenizi ve alan adınızı etkin tutar ve size içeriğinizi oluşturabilmeniz için yeni bir başlangıç imkanı verir. Mevcut içeriğinizin temizlenmesi için bize ulaşmanız yeterli.</string>
+ <string name="site_settings_start_over_hint">Sitenize baştan başlayın</string>
+ <string name="let_us_help">Yardım edelim</string>
+ <string name="me_btn_app_settings">Uygulama ayarları</string>
+ <string name="start_over">Baştan başla</string>
+ <string name="editor_remove_failed_uploads">Başarısız yüklemeleri kaldır</string>
+ <string name="editor_toast_failed_uploads">Bazı ortam dosyası yüklemeleri başarısız oldu. Bu durumda yazınızı\n kaydedemez ya da yayımlayamazsınız. Başarısız ortam dosyaları kaldırılsın mı?</string>
+ <string name="comments_empty_list_filtered_trashed">Çöpte yorum yok</string>
+ <string name="site_settings_advanced_header">Gelişmiş</string>
+ <string name="comments_empty_list_filtered_pending">Bekleyen yorum yok</string>
+ <string name="comments_empty_list_filtered_approved">Onaylı yorum yok</string>
+ <string name="button_done">Bitti</string>
+ <string name="button_skip">Atla</string>
+ <string name="site_timeout_error">Zaman aşımı yüzünden WordPress sitesine ulaşılamadı.</string>
+ <string name="xmlrpc_malformed_response_error">Bağlanılamadı. WordPress kurulumu geçersiz bir XML-RPC belgesi döndü.</string>
+ <string name="xmlrpc_missing_method_error">Bağlanılamadı. Sunucuda gerekli XML-RPC işlevleri eksik.</string>
+ <string name="post_format_status">Durum</string>
+ <string name="post_format_video">Video</string>
+ <string name="theme_free">Ücretsiz</string>
+ <string name="theme_all">Tümü</string>
+ <string name="theme_premium">Premium</string>
+ <string name="alignment_center">Ortala</string>
+ <string name="post_format_chat">Sohbet</string>
+ <string name="post_format_gallery">Galeri</string>
+ <string name="post_format_image">Görsel</string>
+ <string name="post_format_link">Bağlantı</string>
+ <string name="post_format_quote">Alıntı</string>
+ <string name="post_format_standard">Standart</string>
+ <string name="notif_events">WordPress.com kursları ve etkinlikleri hakkında bilgi (çevrimiçi ve bire bir).</string>
+ <string name="post_format_aside">Yan sütun</string>
+ <string name="post_format_audio">Ses</string>
+ <string name="notif_surveys">WordPress.com araştırma ve anketlerine katkıda bulunma olanakları.</string>
+ <string name="notif_tips">WordPress.com\'dan en fazlasını almak için ipuçları.</string>
+ <string name="notif_community">Topluluk</string>
+ <string name="replies_to_my_comments">Yorumlarıma cevaplar</string>
+ <string name="notif_suggestions">Öneriler</string>
+ <string name="notif_research">Araştırma</string>
+ <string name="site_achievements">Site başarıları</string>
+ <string name="username_mentions">Kullanıcı adı bahsedenleri</string>
+ <string name="likes_on_my_posts">Yazılarımdaki beğenmeler</string>
+ <string name="site_follows">Site takipleri</string>
+ <string name="likes_on_my_comments">Yorumlarımdaki beğenmeler</string>
+ <string name="comments_on_my_site">Sitemdeki yorumlar</string>
+ <string name="site_settings_list_editor_summary_other">%d öğe</string>
+ <string name="site_settings_list_editor_summary_one">1 öğe</string>
+ <string name="approve_auto_if_previously_approved">Bilinen kullanıcıların yorumları</string>
+ <string name="approve_auto">Tüm kullanıcılar</string>
+ <string name="approve_manual">Yorum yapılmamış</string>
+ <string name="site_settings_paging_summary_other">Sayfa başına %d yorum</string>
+ <string name="site_settings_paging_summary_one">Sayfa başına 1 yorum</string>
+ <string name="site_settings_multiple_links_summary_other">%d bağlantıdan fazlası onay gerektirir</string>
+ <string name="site_settings_multiple_links_summary_one">1 bağlantıdan fazlası onay gerektirir</string>
+ <string name="site_settings_multiple_links_summary_zero">0 bağlantıdan fazlası onay gerektirir</string>
+ <string name="detail_approve_auto">Herkesin yorumlarını otomatik olarak onayla.</string>
+ <string name="detail_approve_auto_if_previously_approved">Kullanıcının daha önceden onaylanmış bir yorumu varsa otomatik olarak onayla</string>
+ <string name="detail_approve_manual">Herkesin yorumları için el ile onaylama ister.</string>
+ <string name="filter_trashed_posts">Çöpe taşındı</string>
+ <string name="days_quantity_one">1 gün</string>
+ <string name="days_quantity_other">%d gün</string>
+ <string name="filter_published_posts">Yayımlandı</string>
+ <string name="filter_draft_posts">Taslaklar</string>
+ <string name="filter_scheduled_posts">Takvime Eklendi</string>
+ <string name="pending_email_change_snackbar">Yeni adresinizin onaylanması için %1$s adresine gönderilen e-postadaki doğrulama bağlantısına tıklayın.</string>
+ <string name="primary_site">Birincil site</string>
+ <string name="web_address">Web Adresi</string>
+ <string name="editor_toast_uploading_please_wait">Şu anda medya yüklüyorsunuz. Lütfen bu işlem tamamlanıncaya kadar bekleyin.</string>
+ <string name="error_refresh_comments_showing_older">Yorumlar şu anda yenilenemiyor, eski yorumlar gösteriliyor</string>
+ <string name="editor_post_settings_set_featured_image">Öne çıkarılan görsel ayarla</string>
+ <string name="editor_post_settings_featured_image">Öne Çıkan Görüntü</string>
+ <string name="new_editor_promo_desc">Android için WordPress uygulaması artık yeni ve güzel bir görsel düzenleyici\n içeriyor. Yeni bir gönderi oluşturarak deneyin.</string>
+ <string name="new_editor_promo_title">Yepyeni bir düzenleyici</string>
+ <string name="new_editor_promo_button_label">Harika, teşekkürler!</string>
+ <string name="visual_editor_enabled">Görsel Düzenleyici etkinleştirildi</string>
+ <string name="editor_content_placeholder">Hikayenizi burada paylaşın...</string>
+ <string name="editor_page_title_placeholder">Sayfa Başlığı</string>
+ <string name="editor_post_title_placeholder">Yazı Başlığı</string>
+ <string name="email_address">E-posta adresi</string>
+ <string name="preference_show_visual_editor">Görsel düzenleyiciyi göster</string>
+ <string name="dlg_sure_to_delete_comments">Bu yorumlar kalıcı olarak silinsin mi?</string>
+ <string name="preference_editor">Düzenleyici</string>
+ <string name="dlg_sure_to_delete_comment">Bu yorum kalıcı olarak silinsin mi?</string>
+ <string name="mnu_comment_delete_permanently">Sil</string>
+ <string name="comment_deleted_permanently">Yorum silindi</string>
+ <string name="mnu_comment_untrash">Geri yükle</string>
+ <string name="comments_empty_list_filtered_spam">İstenmeyen yorum yok</string>
+ <string name="could_not_load_page">Sayfa yüklenemiyor</string>
+ <string name="comment_status_all">Tümü</string>
+ <string name="interface_language">Arayüz Dili</string>
+ <string name="off">Kapalı</string>
+ <string name="about_the_app">Uygulama hakkında</string>
+ <string name="error_post_account_settings">Hesap ayarlarınız kaydedilemedi</string>
+ <string name="error_post_my_profile">Profiliniz kaydedilemedi</string>
+ <string name="error_fetch_account_settings">Hesap ayarlarınız alınamadı</string>
+ <string name="error_fetch_my_profile">Profiliniz alınamadı</string>
+ <string name="stats_widget_promo_ok_btn_label">Tamam, anladım</string>
+ <string name="stats_widget_promo_desc">İstatistiklerinize tek tıkla erişmek için widget’ı ana ekranınıza ekleyin.</string>
+ <string name="stats_widget_promo_title">Ana Ekran İstatistik Widget’ı</string>
+ <string name="site_settings_unknown_language_code_error">Dil kodu tanınmadı</string>
+ <string name="site_settings_threading_dialog_description">Yorumların zincirler halinde kademelenmesine izin ver</string>
+ <string name="site_settings_threading_dialog_header">Zincirdeki maksimum yorum sayısı:</string>
+ <string name="add_category">Kategori ekle</string>
+ <string name="remove">Kaldır</string>
+ <string name="search">Ara</string>
+ <string name="disabled">Devre dışı</string>
+ <string name="site_settings_image_original_size">Orijinal Boyut</string>
+ <string name="privacy_private">Siteniz sadece siz ve onay verdiğiniz kullanıcılar tarafından görüntülenebilir</string>
+ <string name="privacy_public_not_indexed">Siteniz herkes tarafından görüntülenebilir, ancak arama motorlarından kendisini indekslememelerini ister</string>
+ <string name="privacy_public">Siteniz herkes tarafından görülebilir ve arama motorları tarafından indekslenebilir</string>
+ <string name="about_me_hint">Hakkınızda birkaç kelime…</string>
+ <string name="public_display_name_hint">Ayarlanmazsa, görünen ad varsayılan olarak kullanıcı adınız şeklinde belirlenir</string>
+ <string name="about_me">Hakkımda</string>
+ <string name="public_display_name">Herkese açık görünen ad</string>
+ <string name="my_profile">Profilim</string>
+ <string name="first_name">Ad</string>
+ <string name="last_name">Soyadı</string>
+ <string name="site_privacy_public_desc">Arama motorlarının bu siteyi endekslemesine izin ver</string>
+ <string name="site_privacy_hidden_desc">Arama motorlarının bu siteyi endekslemesini önle</string>
+ <string name="site_privacy_private_desc">Sitemin sadece seçtiğim kullanıcılara görünecek şekilde özel olmasını istiyorum</string>
+ <string name="cd_related_post_preview_image">İlgili yazı önizleme görseli</string>
+ <string name="error_post_remote_site_settings">Site bilgileri kaydedilemedi</string>
+ <string name="error_fetch_remote_site_settings">Site bilgileri alınamadı</string>
+ <string name="error_media_upload_connection">Medya yüklenirken bağlantı hatası oluştu</string>
+ <string name="site_settings_disconnected_toast">Bağlantı kesik, düzenleme devre dışı.</string>
+ <string name="site_settings_unsupported_version_error">Desteklenmeyen WordPress sürümü</string>
+ <string name="site_settings_multiple_links_dialog_description">Belirtilenden fazla sayıda bağlantı içeren yorumlar için onay iste.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Otomatik olarak kapat</string>
+ <string name="site_settings_close_after_dialog_description">Makalelerdeki yorumları otomatik kapat.</string>
+ <string name="site_settings_paging_dialog_description">Yorum zincirlerini birden çok sayfaya böl.</string>
+ <string name="site_settings_paging_dialog_header">Sayfa başına yorum</string>
+ <string name="site_settings_close_after_dialog_title">Yorumları kapat</string>
+ <string name="site_settings_blacklist_description">Bir yorum içeriğinde, adında, URL’sinde, e-posta ya da IP’sinde bu sözcüklerden biri bulunuyorsa bu yorum, istenmeyen olarak işaretlenecektir. Sözcük parçaları girebilirsiniz; örneğin "press" ifadesi "WordPress" ile eşleşecektir.</string>
+ <string name="site_settings_hold_for_moderation_description">Bir yorumun içeriğinde, adında, URL\'sinde, e-posta ya da IP’sinde bu sözcüklerden biri bulunuyorsa bu yorum, denetim kuyruğuna alınır. Sözcük parçaları girebilirsiniz; örneğin "press" ifadesi "WordPress" ile eşleşecektir.</string>
+ <string name="site_settings_list_editor_input_hint">Bir sözcük veya sözcük grubu girin</string>
+ <string name="site_settings_list_editor_no_items_text">Öge yok</string>
+ <string name="site_settings_learn_more_caption">Bu ayarları yazı özelinde geçersiz kılabilirsiniz.</string>
+ <string name="site_settings_rp_preview3_site">"Yükseltme" içinde</string>
+ <string name="site_settings_rp_preview3_title">Yükseltme Odağı: Düğünler İçin VideoPress</string>
+ <string name="site_settings_rp_preview2_site">"Uygulamalar" içinde</string>
+ <string name="site_settings_rp_preview2_title">Android için WordPress Uygulaması Büyük Bir Görsel Gelişim Yaşadı</string>
+ <string name="site_settings_rp_preview1_site">"Mobil" içinde</string>
+ <string name="site_settings_rp_preview1_title">Büyük iPhone/iPad Güncellemesi İndirilmeye Hazır</string>
+ <string name="site_settings_rp_show_images_title">Görselleri Göster</string>
+ <string name="site_settings_rp_show_header_title">Başlığı Göster</string>
+ <string name="site_settings_rp_switch_summary">Benzer Yazılar, yazılarınızın altında sitenizden ilgili içerikler gösterir.</string>
+ <string name="site_settings_rp_switch_title">Benzer Yazıları Göster</string>
+ <string name="site_settings_delete_site_hint">Sitenizin verilerini uygulamadan kaldırır</string>
+ <string name="site_settings_blacklist_hint">Bir filtre ile eşleşen yorumlar istenmeyen olarak işaretlenir</string>
+ <string name="site_settings_moderation_hold_hint">Bir filtre ile eşleşen yorumlar denetim kuyruğuna alınır</string>
+ <string name="site_settings_multiple_links_hint">Bilinen kullanıcılar için bağlantı sayısı sınırını yoksayar</string>
+ <string name="site_settings_whitelist_hint">Yorum yazarının önceden onaylanmış bir yorumu olmalıdır</string>
+ <string name="site_settings_user_account_required_hint">Kullanıcılar yorum yapmak için kaydolmalı ve oturum açmalılardır</string>
+ <string name="site_settings_identity_required_hint">Yorum yazarı ad ve e-posta alanlarını doldurmalıdır</string>
+ <string name="site_settings_manual_approval_hint">Yorumlar el ile onaylanmalıdır</string>
+ <string name="site_settings_paging_hint">Yorumları belirlenen bir boyuttaki öbekler halinde görüntüle</string>
+ <string name="site_settings_threading_hint">Belli bir seviyeye kadar iç içe yorumlara izin ver</string>
+ <string name="site_settings_sort_by_hint">Yorumların görüntülenme sırasını belirler</string>
+ <string name="site_settings_close_after_hint">Belirtilen zamandan sonra yorumlara izin verme</string>
+ <string name="site_settings_receive_pingbacks_hint">Diğer bloglardan bağlantı bildirimlerine izin ver</string>
+ <string name="site_settings_send_pingbacks_hint">Makalede bağlantı verilen blogları haberdar etmeyi dene</string>
+ <string name="site_settings_allow_comments_hint">Okuyucuların yorum göndermesine izin ver</string>
+ <string name="site_settings_discussion_hint">Sitenizin tartışma ayarlarını görüntüleyin ve değiştirin</string>
+ <string name="site_settings_more_hint">Tüm kullanılabilir Tartışma ayarlarını görüntüle</string>
+ <string name="site_settings_related_posts_hint">Benzer yazıları okuyucuda göster veya sakla</string>
+ <string name="site_settings_upload_and_link_image_hint">Görseli her zaman tam boyutta yüklemeyi etkinleştir</string>
+ <string name="site_settings_image_width_hint">Yazılardaki görüntülerin boyutunu bu genişliğe değiştirir</string>
+ <string name="site_settings_format_hint">Yeni yazı biçimi ayarlar</string>
+ <string name="site_settings_category_hint">Yeni yazı kategorisi ayarlar</string>
+ <string name="site_settings_location_hint">Yazılarınıza otomatik olarak konum verileri ekleyin</string>
+ <string name="site_settings_password_hint">Parolanızı değiştirin</string>
+ <string name="site_settings_username_hint">Geçerli kullanıcı hesabı</string>
+ <string name="site_settings_language_hint">Bu blogun yazıldığı birincil dil</string>
+ <string name="site_settings_privacy_hint">Sitenizi kimlerin görebileceğini kontrol eder</string>
+ <string name="site_settings_address_hint">Adresinizi değiştirme şu anda desteklenmiyor</string>
+ <string name="site_settings_tagline_hint">Blogunuzu anlatan kısa bir açıklama ya da akılda kalıcı bir ifade</string>
+ <string name="site_settings_title_hint">Birkaç kelimeyle bu sitenin ne ile ilgili olduğunu anlatın</string>
+ <string name="site_settings_whitelist_known_summary">Bilinen kullanıcılardan yorumlar</string>
+ <string name="site_settings_whitelist_all_summary">Tüm kullanıcılardan yorumlar</string>
+ <string name="site_settings_threading_summary">%d seviye</string>
+ <string name="site_settings_privacy_private_summary">Özel</string>
+ <string name="site_settings_privacy_hidden_summary">Gizli</string>
+ <string name="site_settings_delete_site_title">Siteyi Sil</string>
+ <string name="site_settings_privacy_public_summary">Herkese Açık</string>
+ <string name="site_settings_blacklist_title">Kara liste</string>
+ <string name="site_settings_moderation_hold_title">Denetleme için tut</string>
+ <string name="site_settings_multiple_links_title">Yorumlardaki bağlantılar</string>
+ <string name="site_settings_whitelist_title">Otomatik olarak onayla</string>
+ <string name="site_settings_threading_title">Yorum zinciri ayarı</string>
+ <string name="site_settings_paging_title">Sayfa ayarı</string>
+ <string name="site_settings_sort_by_title">Sırala</string>
+ <string name="site_settings_account_required_title">Kullanıcılar oturum açmış olmalıdır</string>
+ <string name="site_settings_identity_required_title">Ad ve e-posta adresi içermelidir</string>
+ <string name="site_settings_receive_pingbacks_title">Geri Bildirim Al</string>
+ <string name="site_settings_send_pingbacks_title">Geri Bildirim Gönder</string>
+ <string name="site_settings_allow_comments_title">Yorumlara İzin Ver</string>
+ <string name="site_settings_default_format_title">Varsayılan Biçim</string>
+ <string name="site_settings_default_category_title">Varsayılan Kategori</string>
+ <string name="site_settings_location_title">Konumu Etkinleştir</string>
+ <string name="site_settings_address_title">Adres</string>
+ <string name="site_settings_title_title">Site Başlığı</string>
+ <string name="site_settings_tagline_title">Etiket Satırı</string>
+ <string name="site_settings_this_device_header">Bu cihaz</string>
+ <string name="site_settings_discussion_new_posts_header">Yeni yazılar için varsayılanlar</string>
+ <string name="site_settings_account_header">Hesap</string>
+ <string name="site_settings_writing_header">Yazma</string>
+ <string name="newest_first">İlk en yeni</string>
+ <string name="site_settings_general_header">Genel</string>
+ <string name="discussion">Tartışma</string>
+ <string name="privacy">Gizlilik</string>
+ <string name="related_posts">İlgili Yazılar</string>
+ <string name="comments">Yorumlar</string>
+ <string name="close_after">Şu süreden sonra kapat:</string>
+ <string name="oldest_first">İlk en eski</string>
+ <string name="media_error_no_permission_upload">Siteye medya yükleme izniniz yok</string>
+ <string name="never">Hiçbir zaman</string>
+ <string name="unknown">Bilinmeyen</string>
+ <string name="reader_err_get_post_not_found">Bu yazı artık mevcut değil</string>
+ <string name="reader_err_get_post_not_authorized">Bu yazıyı görüntülemeye yetkili değilsiniz</string>
+ <string name="reader_err_get_post_generic">Bu yazı getirilemiyor</string>
+ <string name="blog_name_no_spaced_allowed">Site adresi boşluk karakteri içeremez</string>
+ <string name="invalid_username_no_spaces">Kullanıcı adı boşluk karakteri içeremez</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">Takip ettiğiniz siteler yakın tarihte herhangi bir gönderi yapmadı</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">Yeni yazı yok</string>
+ <string name="media_details_copy_url_toast">Adres panoya kopyalandı</string>
+ <string name="edit_media">Ortamı düzenle</string>
+ <string name="media_details_copy_url">Adresi kopyala</string>
+ <string name="media_details_label_date_uploaded">Yüklendi</string>
+ <string name="media_details_label_date_added">Eklendi</string>
+ <string name="selected_theme">Seçili tema</string>
+ <string name="could_not_load_theme">Tema yüklenemiyor</string>
+ <string name="theme_activation_error">Bir şeyler ters gitti. Tema etkinleştirilemiyor</string>
+ <string name="theme_by_author_prompt_append">%1$s tarafından</string>
+ <string name="theme_prompt">%1$s terchiniz için teşekkürler</string>
+ <string name="theme_try_and_customize">Deneyin ve Özelleştirin</string>
+ <string name="theme_view">Görünüm</string>
+ <string name="theme_details">Detaylar</string>
+ <string name="theme_support">Destek</string>
+ <string name="theme_done">TAMAMLANDI</string>
+ <string name="theme_manage_site">SİTEYİ YÖNET</string>
+ <string name="title_activity_theme_support">Temalar</string>
+ <string name="theme_activate">Etkinleştir</string>
+ <string name="date_range_start_date">Başlangıç tarihi</string>
+ <string name="date_range_end_date">Bitiş tarihi</string>
+ <string name="current_theme">Mevcut tema</string>
+ <string name="customize">Özelleştir</string>
+ <string name="details">Detaylar</string>
+ <string name="support">Destek</string>
+ <string name="active">Etkinleştir</string>
+ <string name="stats_referrers_spam_generic_error">İşlem sırasında bir şeyler ters gitti. İstenmeyen durumu değiştirilemedi.</string>
+ <string name="stats_referrers_marking_not_spam">İstenmeyen işareti kaldırıldı</string>
+ <string name="stats_referrers_unspam">İstenmeyen değil</string>
+ <string name="stats_referrers_marking_spam">İstenmeyen olarak işaretleniyor</string>
+ <string name="theme_auth_error_authenticate">Temalar getirilemedi: kullanıcı doğrulaması başarısız</string>
+ <string name="post_published">Yazı yayımlandı</string>
+ <string name="page_published">Sayfa yayımlandı</string>
+ <string name="post_updated">Yazı güncellendi</string>
+ <string name="page_updated">Sayfa güncellendi</string>
+ <string name="stats_referrers_spam">İstenmeyen</string>
+ <string name="theme_no_search_result_found">Üzgünüz, tema bulunamadı.</string>
+ <string name="media_file_name">Dosya adı: %s</string>
+ <string name="media_uploaded_on">Yükleme tarihi: %s</string>
+ <string name="media_dimensions">Boyutlar: %s</string>
+ <string name="upload_queued">Sırada</string>
+ <string name="media_file_type">Dosya tipi: %s</string>
+ <string name="reader_label_gap_marker">Daha fazla yazı yükle</string>
+ <string name="notifications_no_search_results">\'%s\' hiçbir siteyle eşleşmedi</string>
+ <string name="search_sites">Sitelerde ara</string>
+ <string name="unread">Okunmadı</string>
+ <string name="notifications_empty_view_reader">Okuyucuyu görüntüle</string>
+ <string name="notifications_empty_action_followers_likes">Ortaya çıkın: okuduğunuz yazılara yorum yapın.</string>
+ <string name="notifications_empty_action_comments">Konuşmalara katılın: takip ettiğiniz bloglarda yorum yapın.</string>
+ <string name="notifications_empty_action_unread">Tartışmaları alevlendirin: yeni bir yazı yazın.</string>
+ <string name="notifications_empty_action_all">Etkin olun! Takip ettiğiniz bloglarda yazılara yorum yapın.</string>
+ <string name="notifications_empty_likes">Gösterilecek beğeni yok...henüz.</string>
+ <string name="notifications_empty_followers">Raporlanacak takipçi yok...henüz.</string>
+ <string name="notifications_empty_comments">Yeni yorum yok...henüz.</string>
+ <string name="notifications_empty_unread">Her şey tamam!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Lütfen önce uygulamada istatistiklere ulaşın, sonra bileşeni tekrar eklemeyi deneyin</string>
+ <string name="stats_widget_error_readd_widget">Lütfen bileşeni kaldırıp tekrar ekleyin</string>
+ <string name="stats_widget_error_no_visible_blog">Görünür bir blog olmadan istatistiklere erişilemez</string>
+ <string name="stats_widget_error_no_permissions">WordPress.com hesabınız bu blogun istatistiklerine erişemiyor</string>
+ <string name="stats_widget_error_no_account">Lütfen WordPress oturumu açın</string>
+ <string name="stats_widget_error_generic">İstatistikler yüklenemedi</string>
+ <string name="stats_widget_loading_data">Veri yükleniyor...</string>
+ <string name="stats_widget_name_for_blog">%1$s için bugünün istatistikleri</string>
+ <string name="stats_widget_name">Bugünkü WordPress istatistikleri</string>
+ <string name="add_location_permission_required">Konum eklemek için izin gerekiyor</string>
+ <string name="add_media_permission_required">Medya eklemek için izin gerekiyor</string>
+ <string name="access_media_permission_required">Medyaya erişmek için izin gerekiyor</string>
+ <string name="stats_enable_rest_api_in_jetpack">İstatistiklerinizi görmek için Jetpack içindeki JSON API modülünü etkinleştirin.</string>
+ <string name="error_open_list_from_notification"> Bu yazı ya da sayfa başka bir sitede yayınlanmış</string>
+ <string name="reader_short_comment_count_multi">%s Yorum</string>
+ <string name="reader_short_comment_count_one">1 Yorum</string>
+ <string name="reader_label_submit_comment">GÖNDER</string>
+ <string name="reader_hint_comment_on_post">Yazıyı cevapla...</string>
+ <string name="reader_discover_visit_blog">Ziyaret et %s</string>
+ <string name="reader_discover_attribution_blog">Orijinal olarak %s adresinde yayınlandı</string>
+ <string name="reader_discover_attribution_author">Orijinal olarak %s tarafından yayınlandı</string>
+ <string name="reader_discover_attribution_author_and_blog">Orijinal olarak %1$s tarafından %2$s adresinde yayınlandı</string>
+ <string name="reader_short_like_count_multi">%s Beğeni</string>
+ <string name="reader_short_like_count_one">1 Beğeni</string>
+ <string name="reader_short_like_count_none">Beğen</string>
+ <string name="reader_label_follow_count">%,d takipçi</string>
+ <string name="reader_menu_tags">Etiketleri ve blogları düzenle</string>
+ <string name="reader_title_post_detail">Okuyucu Gönderisi</string>
+ <string name="local_draft_explainer">Bu gönderi yayımlanmamış bir yerel taslaktır</string>
+ <string name="local_changes_explainer">Bu gönderide yayımlanmamış yerel değişiklikler var</string>
+ <string name="notifications_push_summary">Cihazınızda görünen bildirimlerin ayarları.</string>
+ <string name="notifications_email_summary">Hesabınıza bağlı e-posta adresine gönderilen bildirimlerin ayarları.</string>
+ <string name="notifications_tab_summary">Bildirimler sekmesinde görünen bildirimlerin ayarları.</string>
+ <string name="notifications_disabled">Uygulama bildirimleri devre dışı bırakıldı. Ayarlar\'a giderek etkinleştirmek için buraya dokunun.</string>
+ <string name="notification_types">Bildirim tipleri</string>
+ <string name="error_loading_notifications">Bildirim ayarları yüklenemedi</string>
+ <string name="replies_to_your_comments">Yorumlarınızı cevaplar</string>
+ <string name="comment_likes">Yorum beğenileri</string>
+ <string name="email">E-posta</string>
+ <string name="notifications_tab">Bildirimler sekmesi</string>
+ <string name="app_notifications">Uygulama bildirimleri</string>
+ <string name="notifications_comments_other_blogs">Diğer sitelerdeki yorumlar</string>
+ <string name="notifications_other">Diğer</string>
+ <string name="notifications_wpcom_updates">WordPress.com güncellemeleri</string>
+ <string name="notifications_account_emails">WordPress.com tarafından gönderilen e-posta</string>
+ <string name="notifications_account_emails_summary">Hesabınızla ilgili önemli e-postaları her zaman göndereceğiz, ayrıca bazı yardımcı içerikler de alabilirsiniz/</string>
+ <string name="your_sites">Siteleriniz</string>
+ <string name="notifications_sights_and_sounds">Görüntüler ve Sesler</string>
+ <string name="stats_insights_latest_post_trend">%2$s yayımlandığından beri %1$s geçti. İşte yazının bugüne kadarki performansı...</string>
+ <string name="stats_insights_latest_post_summary">Son yazılar özeti</string>
+ <string name="button_revert">Geri Çevir</string>
+ <string name="yesterday">Dün</string>
+ <string name="days_ago">%d gün önce</string>
+ <string name="connectionbar_no_connection">Bağlantı yok</string>
+ <string name="button_back">Geri</string>
+ <string name="page_deleted">Sayfa silindi</string>
+ <string name="button_stats">İstatistikler</string>
+ <string name="button_preview">Ön izleme</string>
+ <string name="button_view">Görüntüle</string>
+ <string name="button_edit">Düzenle</string>
+ <string name="button_publish">Yayımla</string>
+ <string name="button_trash">Çöpe taşı</string>
+ <string name="post_deleted">Yazı silindi</string>
+ <string name="trashed">Çöpe taşındı</string>
+ <string name="page_trashed">Sayfa çöpe taşındı</string>
+ <string name="post_trashed">Yazı çöpe taşındı</string>
+ <string name="stats_no_activity_this_period">Bu dönemde aktivite yok</string>
+ <string name="my_site_no_sites_view_subtitle">Bir tane eklemek ister misiniz?</string>
+ <string name="my_site_no_sites_view_title">Henüz bir WordPress siteniz yok.</string>
+ <string name="my_site_no_sites_view_drake">İlüstrasyon</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">Bu bloga erişim için yetkiniz yok</string>
+ <string name="reader_toast_err_follow_blog_not_found">Blog bulunamadı</string>
+ <string name="undo">Geri al</string>
+ <string name="tabbar_accessibility_label_my_site">Sitem</string>
+ <string name="tabbar_accessibility_label_me">Ben</string>
+ <string name="editor_toast_changes_saved">Değişiklikler kaydedildi</string>
+ <string name="passcodelock_prompt_message">PIN kodunuzu girin</string>
+ <string name="push_auth_expired">İstek süresi doldu. WordPress.com sitesinde giriş yaparak tekrar deneyin.</string>
+ <string name="ignore">Yoksay</string>
+ <string name="stats_insights_best_ever">Şimdiye Kadarki En İyi Görüntülemeler</string>
+ <string name="stats_insights_most_popular_percent_views">Görüntülemelerde %1$d%%</string>
+ <string name="stats_insights_most_popular_hour">En popüler saat</string>
+ <string name="stats_insights_most_popular_day">En popüler gün</string>
+ <string name="stats_insights_today">Bugünün İstatistikleri</string>
+ <string name="stats_insights_popular">En popüler gün ve saat</string>
+ <string name="stats_insights_all_time">Tüm zamanlardaki gönderiler, görüntülemeler ve ziyaretçiler</string>
+ <string name="stats_insights">Tek Bakışta</string>
+ <string name="stats_sign_in_jetpack_different_com_account">İstatistiklerinizi görüntülemek için, Jetpack\'e bağlanmak için kullandığınız WordPress.com hesabına giriş yapın.</string>
+ <string name="stats_other_recent_stats_moved_label">Diğer Son İstatistiklerinizi mi arıyorsunuz? Bu bilgileri Tek Bakışta sayfasına taşıdık.</string>
+ <string name="me_disconnect_from_wordpress_com">WordPress.com Bağlantısını Kes</string>
+ <string name="me_btn_login_logout">Giriş/Çıkış</string>
+ <string name="me_connect_to_wordpress_com">WordPress.com\'a bağlan</string>
+ <string name="site_picker_cant_hide_current_site">"%s" şu anki site olduğundan gizlenmedi</string>
+ <string name="me_btn_support">Yardım &amp; Destek</string>
+ <string name="account_settings">Hesap ayarları</string>
+ <string name="site_picker_create_dotcom">WordPress.com site oluştur</string>
+ <string name="site_picker_add_self_hosted">Kullanıcı tarafından barındırılan site ekle</string>
+ <string name="site_picker_edit_visibility">Siteleri göster/gizle</string>
+ <string name="site_picker_add_site">Site ekle</string>
+ <string name="my_site_btn_view_admin">Yöneticiyi Görüntüle</string>
+ <string name="my_site_btn_view_site">Siteyi Göster</string>
+ <string name="my_site_btn_switch_site">Site Değiştir</string>
+ <string name="site_picker_title">Site seç</string>
+ <string name="my_site_header_look_and_feel">Genel Görünüm</string>
+ <string name="my_site_btn_blog_posts">Blog Yazıları</string>
+ <string name="my_site_btn_site_settings">Ayarlar</string>
+ <string name="my_site_header_publish">Yayımla</string>
+ <string name="reader_label_new_posts_subtitle">Göstermek için dokunun</string>
+ <string name="my_site_header_configuration">Yapılandırma</string>
+ <string name="notifications_account_required">Bildirimler için WordPress.com\'a giriş yap</string>
+ <string name="stats_unknown_author">Bilinmeyen Yazar</string>
+ <string name="signout">Bağlantıyı Kes</string>
+ <string name="image_added">Resim eklendi</string>
+ <string name="select_all">Tümünü seç</string>
+ <string name="sign_out_wpcom_confirm">Hesabınızın bağlantısını kestiğinizde yerel taslak ve değişiklikler dahil olmak üzere @%s kullanıcısına ait tüm WordPress.com verileri bu cihazdan kaldırılır.</string>
+ <string name="deselect_all">Tüm seçimleri kaldır</string>
+ <string name="show">Göster</string>
+ <string name="hide">Gizle</string>
+ <string name="select_from_new_picker">Yeni seçici ile birden çok öge seç</string>
+ <string name="no_media_sources">Ortam çekilemedi</string>
+ <string name="loading_blog_videos">Videolar çekiliyor</string>
+ <string name="error_loading_images">Görseller yüklenirken hata oluştu</string>
+ <string name="error_loading_videos">Videolar yüklenirken hata oluştu</string>
+ <string name="loading_blog_images">Görseller çekiliyor</string>
+ <string name="error_loading_blog_videos">Videolar çekilemiyor</string>
+ <string name="error_loading_blog_images">Görseller çekilemiyor</string>
+ <string name="no_device_videos">Video yok</string>
+ <string name="no_blog_images">Görsel yok</string>
+ <string name="no_blog_videos">Video yok</string>
+ <string name="no_device_images">Görsel yok</string>
+ <string name="stats_generic_error">Gerekli istatistikler yüklenemiyor</string>
+ <string name="no_media">Ortam yok</string>
+ <string name="loading_videos">Videolar yükleniyor</string>
+ <string name="loading_images">Görseller yükleniyor</string>
+ <string name="auth_required">Devam etmek için giriş yapın.</string>
+ <string name="sign_in_jetpack">JetPack bağlantısı için WordPress.com hesabınıza giriş yapın.</string>
+ <string name="two_step_sms_sent">Metin mesajlarınızı doğrulama kodu için kontrol edin.</string>
+ <string name="two_step_footer_button">Kodu metin mesaj olarak gönder</string>
+ <string name="two_step_footer_label">Doğrulama uygulamanızdaki kodu girin.</string>
+ <string name="verify">Doğrula</string>
+ <string name="invalid_verification_code">Geçersiz doğrulama kodu</string>
+ <string name="verification_code">Doğrulama kodu</string>
+ <string name="editor_toast_invalid_path">Geçersiz dosya yolu</string>
+ <string name="tab_title_site_images">Sitedeki görseller</string>
+ <string name="tab_title_site_videos">Sitedeki videolar</string>
+ <string name="tab_title_device_videos">Cihazdaki videolar</string>
+ <string name="tab_title_device_images">Cihazdaki görseller</string>
+ <string name="take_video">Video çek</string>
+ <string name="take_photo">Fotoğraf çek</string>
+ <string name="media_picker_title">Ortam seç</string>
+ <string name="add_to_post">Yazıya ekle</string>
+ <string name="language">Dil</string>
+ <string name="device">Cihaz</string>
+ <string name="error_publish_no_network">Bağlantı yokken yayımlanamıyor. Taslak olarak kaydedildi.</string>
+ <string name="media_details_label_file_type">Dosya tipi</string>
+ <string name="media_details_label_file_name">Dosya adı</string>
+ <string name="stats_empty_search_terms">Hiç arama terimi kaydedilmedi</string>
+ <string name="stats_entry_search_terms">Arama Terimi</string>
+ <string name="stats_view_authors">Yazarlar</string>
+ <string name="stats_view_search_terms">Arama Terimleri</string>
+ <string name="comments_fetching">Yorumlar alınıyor...</string>
+ <string name="pages_fetching">Sayfalar alınıyor…</string>
+ <string name="toast_err_post_uploading">Gönderi yüklenirken açılamıyor</string>
+ <string name="posts_fetching">Gönderiler alınıyor...</string>
+ <string name="media_fetching">Medya alınıyor…</string>
+ <string name="reader_empty_posts_request_failed">Gönderiler geri alınamıyor</string>
+ <string name="publisher">Yayımlayan:</string>
+ <string name="error_notification_open">Bildirim açılamadı</string>
+ <string name="stats_followers_total_email_paged">%1$d - %2$d / %3$s E-Posta Takipçisi gösteriliyor</string>
+ <string name="stats_search_terms_unknown_search_terms">Bilinmeyen Arama Terimleri</string>
+ <string name="stats_followers_total_wpcom_paged">%1$d - %2$d / %3$s WordPress.com Takipçisi gösteriliyor</string>
+ <string name="stats_empty_search_terms_desc">Ziyaretçilerin sitenizi bulmak için aradığı terimleri inceleyerek arama trafiğiniz hakkında daha fazla bilgi sahibi olun.</string>
+ <string name="post_uploading">Yükleniyor</string>
+ <string name="reader_label_new_posts">Yeni gönderiler</string>
+ <string name="stats_recent_weeks">Son Haftalar</string>
+ <string name="stats_months_and_years">Aylar ve Yıllar</string>
+ <string name="stats_overall">Genel</string>
+ <string name="stats_average_per_day">Gün Başına Ortalama</string>
+ <string name="reader_empty_posts_in_blog">Bu blog boş</string>
+ <string name="error_copy_to_clipboard">Metin panoya kopyalanırken bir hata oluştu</string>
+ <string name="stats_period">Dönem</string>
+ <string name="logs_copied_to_clipboard">Uygulama günlükleri panoya kopyalandı</string>
+ <string name="stats_total">Toplam</string>
+ <string name="reader_page_recommended_blogs">Hoşunuza gidebilecek siteler</string>
+ <string name="stats_comments_total_comments_followers">Yorum takipçili toplam gönderi sayısı: %1$s</string>
+ <string name="stats_timeframe_years">Yıl</string>
+ <string name="stats_views">Görüntülemeler</string>
+ <string name="stats_pagination_label">Sayfa %1$s / %2$s</string>
+ <string name="stats_likes">Beğenilenler</string>
+ <string name="stats_view_publicize">Duyuru</string>
+ <string name="stats_view_top_posts_and_pages">Yazılar ve Sayfalar</string>
+ <string name="stats_entry_publicize">Hizmet</string>
+ <string name="stats_empty_geoviews_desc">Sitenize en çok hangi ülke ve bölgelerden trafik olduğunu görmek için listeyi inceleyin.</string>
+ <string name="stats_empty_geoviews">Hiç ülke kaydedilmedi</string>
+ <string name="stats_totals_followers">Şu tarihten başlayarak:</string>
+ <string name="stats_empty_top_posts_desc">En çok görüntülenen içeriğinizin hangisi olduğunu keşfedin ve tek tek yazılarla sayfaların zaman içindeki başarısını kontrol edin.</string>
+ <string name="stats_empty_top_posts_title">Hiç yazı veya sayfa okunmadı</string>
+ <string name="stats_empty_referrers_title">Hiç yönlendiren kaynak kaydedilmedi</string>
+ <string name="stats_empty_clicks_title">Hiç tıklama kaydedilmedi</string>
+ <string name="stats_empty_referrers_desc">Size en çok trafik yönlendiren web sitelerini ve arama motorlarını inceleyerek sitenizin görünürlüğü hakkında daha fazla bilgi edinin</string>
+ <string name="stats_empty_clicks_desc">İçeriğiniz diğer sitelere bağlantılar verdiğinde ziyaretçilerinizin en çok hangilerini tıkladığını göreceksiniz.</string>
+ <string name="stats_empty_tags_and_categories">Hiç etiketli yazı ya da sayfa görüntülenmedi</string>
+ <string name="stats_empty_top_authors_desc">Katkıda bulunanların yazılarının ne kadar görüntülendiğini izleyin ve her yazarın en popüler içeriğini keşfetmek için ayrıntılı olarak inceleyin.</string>
+ <string name="stats_empty_tags_and_categories_desc">Geçen hafta en çok okunan yazılarınızda görüldüğü üzere sitenizdeki en popüler konulardan haberdar olun.</string>
+ <string name="stats_empty_comments_desc">Sitenizde yorumlara izin veriyorsanız, en son 1000 yorum temel alındığında yazılarınıza en çok yorum yapanları izleyin ve hangi içeriklerin en canlı tartışmalara yol açtığını keşfedin.</string>
+ <string name="stats_empty_video_desc">VideoPress\'i kullanarak video yüklediyseniz videolarınızın kaç kez izlendiğini öğrenin.</string>
+ <string name="stats_empty_video">Hiç video oynatılmadı</string>
+ <string name="stats_empty_publicize">Hiç duyuru takipçisi kaydedilmedi</string>
+ <string name="stats_empty_publicize_desc">Duyuru özelliğini kullanarak çeşitli sosyal ağ hizmetlerinden takipçilerinizi izleyin.</string>
+ <string name="stats_empty_followers_desc">Genel takipçi sayınızı ve her birinin sitenizi ne kadar süredir takip ettiğini izleyin.</string>
+ <string name="stats_empty_followers">Takipçi yok</string>
+ <string name="stats_comments_by_authors">Yazarlara Göre</string>
+ <string name="stats_comments_by_posts_and_pages">Gönderilere &amp; Sayfalara Göre</string>
+ <string name="stats_for">%s ile ilgili istatistikler</string>
+ <string name="stats_view_all">Tümünü görüntüle</string>
+ <string name="stats_view">Görüntüleme</string>
+ <string name="stats_followers_total_email">Toplam E-Posta Takipçileri: %1$s</string>
+ <string name="themes_fetching">Temalar alınıyor…</string>
+ <string name="stats_followers_total_wpcom">Toplam WordPress.com Takipçisi: %1$s</string>
+ <string name="stats_followers_email_selector">E-posta</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_seconds_ago">saniye önce</string>
+ <string name="stats_followers_minutes">%1$d dakika</string>
+ <string name="stats_followers_a_day">Bir gün</string>
+ <string name="stats_followers_days">%1$d gün</string>
+ <string name="stats_followers_a_minute_ago">bir dakika önce</string>
+ <string name="stats_followers_a_month">Bir ay</string>
+ <string name="stats_followers_years">%1$d yıl</string>
+ <string name="stats_followers_a_year">Bir yıl</string>
+ <string name="stats_other_recent_stats_label">Diğer Son İstatistikler</string>
+ <string name="stats_followers_hours">%1$d saat</string>
+ <string name="stats_followers_months">%1$d ay</string>
+ <string name="stats_followers_an_hour_ago">bir saat önce</string>
+ <string name="stats_entry_top_commenter">Yazar</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_view_countries">Ülkeler</string>
+ <string name="stats_view_followers">Takipçiler</string>
+ <string name="stats_view_videos">Videolar</string>
+ <string name="stats_entry_clicks_link">Bağlantı</string>
+ <string name="stats_entry_followers">Takipçi</string>
+ <string name="stats_visitors">Ziyaretçiler</string>
+ <string name="stats_totals_publicize">Takipçiler</string>
+ <string name="ssl_certificate_details">Detaylar</string>
+ <string name="sure_to_remove_account">Bu site kaldırılsın mı?</string>
+ <string name="delete_sure_post">Bu yazıyı sil</string>
+ <string name="delete_sure">Bu taslağı sil</string>
+ <string name="delete_sure_page">Bu sayfayı sil</string>
+ <string name="confirm_delete_multi_media">Seçili ögeler silinsin mi?</string>
+ <string name="confirm_delete_media">Seçili öge silinsin mi?</string>
+ <string name="cab_selected">%d seçili</string>
+ <string name="media_gallery_date_range">%1$s ve %2$s arası ortam dosyaları gösteriliyor</string>
+ <string name="comment_reply_to_user">Şunu cevapla: %s</string>
+ <string name="reader_empty_posts_liked">Henüz hiçbir yazıyı beğenmediniz</string>
+ <string name="faq_button">SSS</string>
+ <string name="browse_our_faq_button">SSS sayfamıza göz atın</string>
+ <string name="nux_help_description">Sık sorulan sorulara yanıt bulmak için yardım merkezini, yeni sorular sormak için forumları ziyaret edin</string>
+ <string name="agree_terms_of_service">Bir hesap açarak %1$sHizmet Koşulları%2$s\'nı kabul etmiş oluyorsunuz.</string>
+ <string name="create_new_blog_wpcom">WordPress.com blogu oluştur</string>
+ <string name="new_blog_wpcom_created">WordPress.com blogu oluşturuldu!</string>
+ <string name="reader_empty_comments">Henüz yorum yapılmadı</string>
+ <string name="reader_empty_posts_in_tag">Bu etiketi içeren yazı yok</string>
+ <string name="reader_label_comment_count_multi">%,d yorum</string>
+ <string name="reader_label_view_original">Orijinal makaleyi oku</string>
+ <string name="reader_label_like">Beğen</string>
+ <string name="reader_label_comment_count_single">Bir yorum</string>
+ <string name="reader_label_comments_closed">Yorumlar kapandı</string>
+ <string name="reader_label_comments_on">Yorum yapılan konu:</string>
+ <string name="reader_title_photo_viewer">%1$d / %2$d</string>
+ <string name="error_refresh_unauthorized_posts">Yazıları görüntüleme veya düzenleme izniniz yok</string>
+ <string name="error_refresh_unauthorized_pages">Sayfaları görüntüleme veya düzenleme izniniz yok</string>
+ <string name="error_refresh_unauthorized_comments">Yorumları görüntüleme veya düzenleme izniniz yok</string>
+ <string name="older_month">Bir aydan eski</string>
+ <string name="more">Daha Fazla</string>
+ <string name="older_two_days">2 günden eski</string>
+ <string name="older_last_week">1 haftadan eski</string>
+ <string name="stats_no_blog">İstenen blog için istatistikler yüklenemiyor</string>
+ <string name="select_a_blog">Bir WordPress sitesi seçin</string>
+ <string name="sending_content">%s içeriği yükleniyor</string>
+ <string name="uploading_total">%1$d / %2$d yükleniyor</string>
+ <string name="mnu_comment_liked">Beğenildi</string>
+ <string name="comment">Yorum Yap</string>
+ <string name="comment_trashed">Yorum iptal edildi</string>
+ <string name="posts_empty_list">Henüz yazı yok. Bir yazı oluşturmaya ne dersiniz?</string>
+ <string name="pages_empty_list">Henüz sayfa yok. Bir sayfa oluşturmaya ne dersiniz?</string>
+ <string name="media_empty_list_custom_date">Bu zaman aralığında medya yok</string>
+ <string name="posting_post">Yazılan: "%s"</string>
+ <string name="signing_out">Çıkış yapılıyor…</string>
+ <string name="error_publish_empty_post">Boş bir yazı yayımlanamaz</string>
+ <string name="reader_empty_followed_blogs_title">Henüz herhangi bir site takip etmiyorsunuz.</string>
+ <string name="reader_label_liked_by">Beğenenler</string>
+ <string name="reader_toast_err_block_blog">Bu blog engellenemiyor</string>
+ <string name="reader_toast_blog_blocked">Bu blogdan yazılar artık gösterilmeyecek</string>
+ <string name="reader_toast_err_generic">Bu eylemi gerçekleştirilemiyor</string>
+ <string name="reader_menu_block_blog">Bu blogu engelleyin</string>
+ <string name="contact_us">İletişim kurun</string>
+ <string name="hs__conversation_detail_error">Karşılaştığınız problemi açıklayın</string>
+ <string name="hs__new_conversation_header">Destek sohbeti</string>
+ <string name="hs__conversation_header">Destek sohbeti</string>
+ <string name="hs__username_blank_error">Geçerli bir isim girin</string>
+ <string name="hs__invalid_email_error">Geçerli bir e-posta adresi girin</string>
+ <string name="add_location">Konum Ekle</string>
+ <string name="current_location">Şimdiki Konumunuz</string>
+ <string name="search_location">Arama</string>
+ <string name="edit_location">Düzenle</string>
+ <string name="search_current_location">Yerleştir</string>
+ <string name="preference_send_usage_stats">Kullanım istatistiklerini gönder</string>
+ <string name="preference_send_usage_stats_summary">Android için WordPress\'i geliştirmemize yardımcı olmak için bize otomatik olarak kullanım istatistiklerini gönderin. </string>
+ <string name="update_verb">Güncelle</string>
+ <string name="schedule_verb">Zamanla</string>
+ <string name="reader_title_subs">Etiketler ve Bloglar</string>
+ <string name="reader_page_followed_tags">Takip edilen etiketler</string>
+ <string name="reader_label_followed_blog">Blog takip edildi</string>
+ <string name="reader_label_tag_preview">%s etiketli gönderiler</string>
+ <string name="reader_toast_err_get_blog_info">Bu blog gösterilemiyor</string>
+ <string name="reader_toast_err_already_follow_blog">Bu blogu zaten takip ediyorsunuz</string>
+ <string name="reader_toast_err_follow_blog">Bu blog takip edilemiyor</string>
+ <string name="reader_toast_err_unfollow_blog">Bu blogun takibi bırakılamıyor</string>
+ <string name="reader_empty_recommended_blogs">Önerilen blog yok</string>
+ <string name="reader_title_blog_preview">Okuyucu Bloğu</string>
+ <string name="reader_title_tag_preview">Okuyucu Etiketi</string>
+ <string name="reader_page_followed_blogs">Takip edilen siteler</string>
+ <string name="reader_hint_add_tag_or_url">Takip etmek için etiket veya adres girin</string>
+ <string name="saving">Kaydediliyor</string>
+ <string name="media_empty_list">Medya bulunamadı</string>
+ <string name="ptr_tip_message">Püf nokta: Yenilemek için aşağı çekin</string>
+ <string name="help">Yardım</string>
+ <string name="forums">Forumlar</string>
+ <string name="help_center">Yardım merkezi</string>
+ <string name="forgot_password">Parolanızı mı kaybettiniz?</string>
+ <string name="ssl_certificate_error">Geçersiz SSL setifikası</string>
+ <string name="ssl_certificate_ask_trust">Eğer bu siteye genelde sorunsuz şekilde bağlanabiliyorsanız bu hata birilerinin siteyi taklit ettiği anlamına gelebilir. Yine de sertifikaya güvenmek ister misiniz?</string>
+ <string name="wait_until_upload_completes">Yükleme tamamlanana kadar bekleyin</string>
+ <string name="theme_set_failed">Tema ayarlanamadı</string>
+ <string name="theme_auth_error_message">Tema ayarlama yetkisine sahip olduğunuzdan emin olun</string>
+ <string name="comments_empty_list">Yorum yok</string>
+ <string name="mnu_comment_unspam">Gereksiz Değil</string>
+ <string name="no_site_error">WordPress sitesine bağlanamadı</string>
+ <string name="adding_cat_failed">Kategori ekleme başarısız</string>
+ <string name="adding_cat_success">Kategori başarı ile eklendi</string>
+ <string name="no_account">WordPress hesabı bulunamadı, bir hesap ekleyip tekrar deneyin</string>
+ <string name="sdcard_message">Medya yüklemek için takılı SD kart gereklidir</string>
+ <string name="stats_empty_comments">Henüz yorum yok</string>
+ <string name="stats_bar_graph_empty">Erişilebilir istatistikler yok</string>
+ <string name="reply_failed">Cevaplama başarısız</string>
+ <string name="notifications_empty_list">Bildirim yok</string>
+ <string name="error_delete_post">%s silinirken hata ile karşılaşıldı</string>
+ <string name="error_generic">Bir hata oluştu</string>
+ <string name="error_moderate_comment">Düzenlenirken hata oluştu</string>
+ <string name="error_upload">%s yüklenirken hata oluştu</string>
+ <string name="error_load_comment">Yorum yüklenemedi</string>
+ <string name="error_downloading_image">Resim indirme hatası</string>
+ <string name="passcode_wrong_passcode">Yanlış PIN</string>
+ <string name="invalid_password_message">Şifreler en az 4 karakter içermelidir</string>
+ <string name="invalid_username_too_short">Kullanıcı adı 4 karakterden uzun olmalıdır</string>
+ <string name="invalid_username_too_long">Kullanıcı adı 61 karakterden kısa olmalıdır</string>
+ <string name="username_required">Bir kullanıcı adı gir</string>
+ <string name="username_must_be_at_least_four_characters">Kullanıcı adı en az 4 karakter olmalıdır</string>
+ <string name="username_exists">Bu kullanıcı adı zaten kullanılıyor</string>
+ <string name="blog_name_required">Site adresi gir</string>
+ <string name="blog_name_not_allowed">Bu site adresine izin verilmiyor</string>
+ <string name="blog_name_must_be_at_least_four_characters">Site adresi en az 4 karakter olmalıdır</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">Site adresi 64 karakterden kısa olmalıdır</string>
+ <string name="blog_name_cant_be_used">Bu site adresini kullanamazsınız</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Site adresi sadece küçük harfler (a-z) ve numaralar içerebilir</string>
+ <string name="blog_name_exists">Bu size zaten kullanılıyor</string>
+ <string name="username_or_password_incorrect">Yanlış kullanıcı adı veya şifre girdiniz</string>
+ <string name="nux_cannot_log_in">Girişinizi yapamıyoruz</string>
+ <string name="gallery_error">Ortam dosyası geri getirilemedi</string>
+ <string name="could_not_remove_account">Site kaldırılamadı</string>
+ <string name="out_of_memory">Cihaz hafızası doldu</string>
+ <string name="no_network_message">Uygun durumda şebeke yok</string>
+ <string name="blog_not_found">Bu blog\'a erişilirken hata ile karşılaşıldı</string>
+ <string name="theme_fetch_failed">Temalar çekilemedi</string>
+ <string name="cat_name_required">Kategori ismi gereklidir</string>
+ <string name="category_automatically_renamed">Kategori ismi %1$s geçersiz. İsim %2$s olarak değiştirildi</string>
+ <string name="error_refresh_posts">Gönderi şuan yenilenemiyor</string>
+ <string name="error_refresh_pages">Sayfa şuan yenilenemiyor</string>
+ <string name="error_refresh_notifications">Bildirimler şuan yenilenemedi</string>
+ <string name="error_refresh_comments">Yorumlar şuan yenilenemedi</string>
+ <string name="error_refresh_stats">İstatistikler şuan yenilenemedi</string>
+ <string name="error_edit_comment">Yorum düzeltilirken hata oluştu</string>
+ <string name="invalid_email_message">Geçersiz Email adresi</string>
+ <string name="username_only_lowercase_letters_and_numbers">Kullanıcı adı sadece küçük harfler(a-z) ve numaralar içerebilir</string>
+ <string name="username_not_allowed">Kullanıcı adı izin verilmiyor</string>
+ <string name="username_contains_invalid_characters">Kullanıcı adı "_" karakterini içeremez</string>
+ <string name="username_must_include_letters">Kullanıcı adı en az 1 harf(a-z) içermelidir</string>
+ <string name="email_invalid">Geçerli bir Email adresi girin</string>
+ <string name="email_not_allowed">Bu Email adresine izin verilmiyor</string>
+ <string name="email_exists">Bu Email adresi zaten kullanılmış</string>
+ <string name="username_reserved_but_may_be_available">Bu kullanıcı adı rezerve edilmiş fakat birkaç gün içinde erişilebilinir</string>
+ <string name="blog_name_contains_invalid_characters">Site adresi "_" karakteri içeremez</string>
+ <string name="blog_name_reserved">Bu site rezerve edilmiş</string>
+ <string name="blog_name_reserved_but_may_be_available">Bu site zaten rezerve edilmiş fakat birkaç gün içerisine erişilebilinir</string>
+ <string name="invalid_url_message">Girdiğiniz adresin geçerli olduğunu kontrol edin</string>
+ <string name="select_categories">Kategorileri seçin</string>
+ <string name="account_details">Hesap detayları</string>
+ <string name="add_comment">Yorum ekle</string>
+ <string name="connection_error">Bağlantı hatası</string>
+ <string name="cancel_edit">Düzenlemekten vazgeç</string>
+ <string name="scaled_image_error">Geçerli ölçekte genişlik değeri girin</string>
+ <string name="learn_more">Daha fazla bilgi</string>
+ <string name="media_gallery_settings_title">Galeri ayarları</string>
+ <string name="media_gallery_num_columns">Sütun sayısı</string>
+ <string name="media_gallery_type_thumbnail_grid">Küçük resim</string>
+ <string name="media_gallery_edit">Galeriyi düzenle</string>
+ <string name="media_error_no_permission">Medya kütüphanesini görüntüleme yetkiniz yok</string>
+ <string name="themes_live_preview">Canlı önizleme</string>
+ <string name="theme_current_theme">Geçerli tema</string>
+ <string name="theme_premium_theme">Premium tema</string>
+ <string name="page_settings">Sayfa ayarları</string>
+ <string name="local_draft">Yerel taslak</string>
+ <string name="upload_failed">Yükleme başarısız</string>
+ <string name="horizontal_alignment">Yatay hizalama</string>
+ <string name="delete_page">Sayfayı sil</string>
+ <string name="comment_status_unapproved">Bekliyor</string>
+ <string name="comment_status_spam">Gereksiz</string>
+ <string name="comment_status_trash">Çöp</string>
+ <string name="edit_comment">Yorumu düzenle</string>
+ <string name="mnu_comment_approve">Onayla</string>
+ <string name="mnu_comment_unapprove">Reddet</string>
+ <string name="media_gallery_image_order">İmaj sırası</string>
+ <string name="mnu_comment_spam">Gereksiz</string>
+ <string name="mnu_comment_trash">Çöp</string>
+ <string name="dlg_approving_comments">Onaylanıyor</string>
+ <string name="dlg_unapproving_comments">Reddediliyor</string>
+ <string name="dlg_spamming_comments">Gereksiz olarak işaretleniyor</string>
+ <string name="dlg_trashing_comments">Çöpe gönderiliyor</string>
+ <string name="trash_yes">Çöp</string>
+ <string name="trash">Çöp</string>
+ <string name="author_name">Yazar adı</string>
+ <string name="comment_added">Yorum başarıyla eklendi</string>
+ <string name="add_new_category">Yeni kategori ekle</string>
+ <string name="category_name">Kategori adı</string>
+ <string name="category_desc">Kategori açıklaması (opsiyonel)</string>
+ <string name="category_parent">Üst kategori (opsiyonel)</string>
+ <string name="share_action_media">Medya kütüphanesi</string>
+ <string name="pending_review">İnceleme bekliyor</string>
+ <string name="http_authorization_required">Yetkilendirme gerekli</string>
+ <string name="new_media">Yeni medya</string>
+ <string name="view_site">Siteyi görüntüle</string>
+ <string name="reader_share_link">Bağlantıyı paylaş</string>
+ <string name="reader_toast_err_add_tag">Bu etiket eklenemiyor</string>
+ <string name="reader_toast_err_remove_tag">Bu etiket kaldırılamıyor</string>
+ <string name="required_field">Gerekli alan</string>
+ <string name="blog_title_invalid">Geçersiz site başlığı</string>
+ <string name="category_slug">Kategori kısa ismi (opsiyonel)</string>
+ <string name="hint_comment_content">Yorum</string>
+ <string name="saving_changes">Değişiklikler kaydediliyor</string>
+ <string name="sure_to_cancel_edit_comment">Bu yorumun düzenlenmesi iptal edilsin mi?</string>
+ <string name="content_required">Yorum gerekli</string>
+ <string name="toast_comment_unedited">Yorum değiştirilmedi</string>
+ <string name="delete_draft">Taslağı Sil</string>
+ <string name="preview_page">Sayfayı önizle</string>
+ <string name="image_settings">Görsel ayarları</string>
+ <string name="add_account_blog_url">Blog adresi</string>
+ <string name="fatal_db_error">Uygulama veritabanı oluşturulurken hata meydana geldi. Uygulamayı tekrar yüklemeyi deneyin.</string>
+ <string name="jetpack_message_not_admin">İstatistikler için Jetpack eklentisi gerekli. Site yöneticisi ile iletişim kurun.</string>
+ <string name="view_in_browser">Tarayıcıda görüntüle</string>
+ <string name="location_not_found">Bilinmeyen konum</string>
+ <string name="open_source_licenses">Açık kaynak lisanslar</string>
+ <string name="reader_title_applog">Uygulama günlüğü</string>
+ <string name="email_cant_be_used_to_signup">Bu e-posta adresini kayıt olmak için kullanamazsınız. Bazı e-postalarımızın engellenmesiyle ilgili problem yaşıyoruz. Farklı bir e-posta sağlayıcısı kullanın.</string>
+ <string name="trash_no">Çöp değil</string>
+ <string name="dlg_confirm_trash_comments">Çöpe gönderilsin mi?</string>
+ <string name="http_credentials">HTTP bilgileri (seçime bağlı)</string>
+ <string name="privacy_policy">Gizlilik politikası</string>
+ <string name="local_changes">Yerel değişiklikler</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="error_blog_hidden">Bu blog gizli ve yüklenemedi. Ayarlardan yeniden etkinleştirin ve yeniden deneyin.</string>
+ <string name="blog_removed_successfully">Site başarıyla kaldırıldı</string>
+ <string name="remove_account">Siteyi kaldır</string>
+ <string name="xmlrpc_error">Bağlanılamadı. Sitenizdeki xmlrpc.php\'nin tam yolunu girip tekrar deneyin</string>
+ <string name="edit_post">Gönderiyi düzenle</string>
+ <string name="post_not_found">Gönderi yüklenirken hata oluştu. Gönderileri yenileyin ve tekrar deneyin</string>
+ <string name="cannot_delete_multi_media_items">Bazı medyalar silinemedi. Daha sonra tekrar deneyin</string>
+ <string name="link_enter_url_text">Link yazısı (opsiyonel)</string>
+ <string name="create_a_link">Link oluştur</string>
+ <string name="file_not_found">Upload edilecek medya dosyası bulunamadı. Silindi veya taşındı mı?</string>
+ <string name="post_settings">Yazı ayarları</string>
+ <string name="delete_post">Yazıyı sil</string>
+ <string name="comment_status_approved">Onaylanan</string>
+ <string name="author_email">Yazar emaili</string>
+ <string name="author_url">Yazar adresi</string>
+ <string name="preview_post">Yazıyı önizle</string>
+ <string name="share_action_post">Yeni yazı</string>
+ <string name="file_error_create">Geçici medya dosyası oluşturulamıyor. Cihazınızda yeterli boş alan olduğundan emin olun.</string>
+ <string name="post_format">Yazı formatı</string>
+ <string name="new_post">Yeni yazı</string>
+ <string name="email_hint">Email adresi</string>
+ <string name="site_address">Kendi sunucunuzda barınan blog adresiniz</string>
+ <string name="email_reserved">Bu email adresi zaten kullanılmış. Gelen kutunuzu aktivasyon emaili için kontrol edin. Eğer aktivasyon yapmazsanız birkaç gün içinde tekrar deneyebilirsiniz.</string>
+ <string name="blog_name_must_include_letters">Site adresi en az bir tane harf (a-z) içermelidir</string>
+ <string name="blog_name_invalid">Geçersiz ste adresi</string>
+ <string name="post_not_published">Yazı durumu yayımlanmadı</string>
+ <string name="page_not_published">Sayfa durumu yayımlanmadı</string>
+ <string name="notifications_empty_all">Bildirim yok...henüz.</string>
+ <string name="invalid_site_url_message">Girdiğiniz site URL\'sinin geçerli olduğunu kontrol edin</string>
+ <string name="share_url_page">Sayfayı paylaş</string>
+ <string name="share_link">Bağlantıyı paylaş</string>
+ <string name="deleting_page">Sayfa siliniyor</string>
+ <string name="deleting_post">Yazı siliniyor</string>
+ <string name="share_url_post">Yazıyı paylaş</string>
+ <string name="creating_your_account">Hesabınız oluşturuluyor</string>
+ <string name="creating_your_site">Siteniz oluşturuluyor</string>
+ <string name="reader_empty_posts_in_tag_updating">Yazılar getiriliyor...</string>
+ <string name="error_refresh_media">Medya kütüphanesi yenilenirken bir şeyler ters gitti. Daha sonra tekrar deneyin.</string>
+ <string name="reader_toast_err_get_comment">Bu yoruma erişilemiyor</string>
+ <string name="reader_label_reply">Yanıtla</string>
+ <string name="video">Video</string>
+ <string name="download">Medya indiriliyor</string>
+ <string name="reader_likes_multi">%,d kişi bunu beğendi</string>
+ <string name="reader_likes_you_and_multi">Siz ve %,d kişi bunu beğendi</string>
+ <string name="cant_share_no_visible_blog">Görünürbir bloğunuz olmadan WordPress ile paylaşamazsınız</string>
+ <string name="comment_spammed">Yorum istenmeyen olarak işaretlendi</string>
+ <string name="select_time">Zaman seçin</string>
+ <string name="select_date">Tarih seçin</string>
+ <string name="pick_photo">Fotoğraf seçin</string>
+ <string name="account_two_step_auth_enabled">Bu hesap iki basamaklı kimlik denetimine sahip. WordPress.com üzerindeki güvenlik ayarlarınızı ziyaret edin ve uygulamaya özel şifre oluşturun.</string>
+ <string name="pick_video">Video seçin</string>
+ <string name="validating_user_data">Kullanıcı verisi doğrulanıyor</string>
+ <string name="validating_site_data">Site verisi doğrulanıyor</string>
+ <string name="reader_likes_you_and_one">Siz ve bir başkası daha bunu değendi</string>
+ <string name="reader_toast_err_get_post">Bu yazıya erişilemiyor</string>
+ <string name="reader_empty_followed_blogs_description">Kaygılanmayın, keşfetmeye başlamak için sağ üstteki simgeye dokunmanız yeterli!</string>
+ <string name="nux_oops_not_selfhosted_blog">WordPress.com\'a giriş yap</string>
+ <string name="nux_tap_continue">Devam</string>
+ <string name="nux_welcome_create_account">Hesap oluştur</string>
+ <string name="nux_add_selfhosted_blog">Kendi sunucumda bulunan siteyi ekle</string>
+ <string name="password_invalid">Daha güvenli bir paraloya ihtiyacınız var. 7 veya daha fazla karakter kullandığınızdan emin olun, büyük ve küçük harfleri, numaraları ve özel karakterleri harmanlayın.</string>
+ <string name="signing_in">Giriş yapılıyor...</string>
+ <string name="media_add_popup_title">Medya kütüphanesine ekle</string>
+ <string name="media_add_new_media_gallery">Galeri oluştur</string>
+ <string name="empty_list_default">Bu liste boş</string>
+ <string name="select_from_media_library">Medya kütüphanesinden seç</string>
+ <string name="jetpack_message">Bu istatistikler için Jetpack eklentisi gerekli. Jetpack\'i kurmak ister misiniz?</string>
+ <string name="jetpack_not_found">Jetpack eklentisi bulunamadı</string>
+ <string name="reader_untitled_post">(Başlıksız)</string>
+ <string name="reader_share_subject">%s den paylaşıldı</string>
+ <string name="reader_btn_share">Paylaş</string>
+ <string name="reader_toast_err_tag_exists">Bu etiketi zaten takip ediyorsunuz</string>
+ <string name="reader_toast_err_tag_invalid">Bu geçerli bir etiket değil</string>
+ <string name="reader_toast_err_share_intent">Paylaşılamadı</string>
+ <string name="reader_toast_err_url_intent">%s açılamadı</string>
+ <string name="connecting_wpcom">WordPress.com\'a bağlanıyor</string>
+ <string name="username_invalid">Geçersiz kullanıcı adı</string>
+ <string name="limit_reached">Limite ulaşıldı. 1 dakika içinde tekrar deneyebilirsiniz. Bundan önceki denemeler sadece engellemenin kalkması için beklemeniz gereken zamanı arttıracaktır.Eğer bunun bir hata olduğunu düşünüyorsanız destek ile iletişime geçin.</string>
+ <string name="nux_tutorial_get_started_title">Başlayın!</string>
+ <string name="reader_likes_one">Bir kişi bunu beğendi</string>
+ <string name="reader_likes_only_you">Bunu beğendin</string>
+ <string name="reader_btn_follow">Takip et</string>
+ <string name="reader_btn_unfollow">Takipte</string>
+ <string name="reader_toast_err_comment_failed">Yorumunuz gönderilemedi</string>
+ <string name="reader_label_added_tag">%s eklendi</string>
+ <string name="reader_label_removed_tag">%s kaldırıldı</string>
+ <string name="reader_toast_err_view_image">Imaj görüntülenemedi</string>
+ <string name="reader_empty_followed_tags">Herhangi bir etiketi takip etmiyorsunuz</string>
+ <string name="create_account_wpcom">WordPress.com\'da bir hesap oluştur</string>
+ <string name="reader_hint_comment_on_comment">Yorumu yanıtla...</string>
+ <string name="button_next">Sonraki</string>
+ <string name="themes">Temalar</string>
+ <string name="media_edit_title_text">Başlık</string>
+ <string name="media_edit_caption_text">Manşet</string>
+ <string name="media_edit_description_text">Açıklama</string>
+ <string name="media_edit_title_hint">Buraya bir başlık girin</string>
+ <string name="media_edit_caption_hint">Buraya bir manşet girin</string>
+ <string name="media_edit_description_hint">Buraya bir açıklama girin</string>
+ <string name="media_edit_success">Güncellendi</string>
+ <string name="media_edit_failure">Güncelleme hatası</string>
+ <string name="themes_details_label">Detaylar</string>
+ <string name="themes_features_label">Özellikler</string>
+ <string name="theme_set_success">Tema başarıyla ayarlandı!</string>
+ <string name="media_add_popup_capture_photo">Fotoğraf çek</string>
+ <string name="media_add_popup_capture_video">Video çek</string>
+ <string name="unattached">Bağlantısız</string>
+ <string name="media_gallery_image_order_random">Rastgele</string>
+ <string name="media_gallery_image_order_reverse">Tersten</string>
+ <string name="post_excerpt">Özet</string>
+ <string name="share_action">Paylaş</string>
+ <string name="stats">İstatistikler</string>
+ <string name="stats_view_clicks">Tıklamalar</string>
+ <string name="stats_timeframe_today">Bugün</string>
+ <string name="stats_timeframe_yesterday">Dün</string>
+ <string name="stats_timeframe_days">Günler</string>
+ <string name="stats_timeframe_weeks">Haftalar</string>
+ <string name="stats_timeframe_months">Aylar</string>
+ <string name="stats_entry_country">Ülke</string>
+ <string name="stats_entry_posts_and_pages">Başlık</string>
+ <string name="stats_entry_tags_and_categories">Konu</string>
+ <string name="stats_entry_authors">Yazar</string>
+ <string name="stats_totals_views">Görüntülemeler</string>
+ <string name="stats_totals_clicks">Tıklamalar</string>
+ <string name="stats_totals_plays">Çalınanlar</string>
+ <string name="passcode_manage">PIN kilidini yönet</string>
+ <string name="passcode_change_passcode">PIN değiştir</string>
+ <string name="passcode_set">PIN ayarla</string>
+ <string name="passcode_preference_title">PIN kilidi</string>
+ <string name="passcode_turn_off">PIN kilidini kapat</string>
+ <string name="passcode_turn_on">PIN kilidini aç</string>
+ <string name="media_gallery_type_tiled">Döşeme</string>
+ <string name="media_gallery_type_slideshow">Slayt gösterisi</string>
+ <string name="media_gallery_type">Tip</string>
+ <string name="stats_view_tags_and_categories">Etiketler ve Kategoriler</string>
+ <string name="all">Tamamı</string>
+ <string name="images">Resimler</string>
+ <string name="custom_date">Özel tarih</string>
+ <string name="media_gallery_type_squares">Kare</string>
+ <string name="media_gallery_type_circles">Çember</string>
+ <string name="theme_activate_button">Aktifleştir</string>
+ <string name="theme_activating_button">Aktifleştiriliyor</string>
+ <string name="theme_auth_error_title">Temalar alınamadı</string>
+ <string name="share_action_title">Ekle ...</string>
+ <string name="stats_view_visitors_and_views">Ziyaretçiler ve görüntülemeler</string>
+ <string name="stats_view_referrers">Gönderenler</string>
+ <string name="stats_entry_referrers">Gönderen</string>
+ <string name="passcode_enter_passcode">PIN girin</string>
+ <string name="passcode_enter_old_passcode">Eski PINi girin</string>
+ <string name="passcode_re_enter_passcode">PINi tekrar girin</string>
+ <string name="upload">Yükle</string>
+ <string name="discard">Vazgeç</string>
+ <string name="notifications">Bildirimler</string>
+ <string name="new_notifications">%d yeni bildirim</string>
+ <string name="more_notifications">ve %d daha.</string>
+ <string name="sign_in">Oturum aç</string>
+ <string name="note_reply_successful">Yanıt yayımlandı</string>
+ <string name="follows">Takipçiler</string>
+ <string name="loading">Yükleniyor...</string>
+ <string name="httpuser">HTTP Kullanici Adi</string>
+ <string name="httppassword">HTTP Sifre</string>
+ <string name="error_media_upload">Ortam yüklenirken bir hata oluştu</string>
+ <string name="post_content">İçerik (metin ve ortam eklemek için dokunun)</string>
+ <string name="content_description_add_media">Ortam Dosyasi Ekle</string>
+ <string name="publish_date">Yayımla</string>
+ <string name="incorrect_credentials">Geçersiz kullanıcı adı veya parola.</string>
+ <string name="password">Sifre</string>
+ <string name="username">Kullanici Adi</string>
+ <string name="reader">Okuyucu</string>
+ <string name="featured">Öne çıkan görsel olarak kullan</string>
+ <string name="pages">Sayfalar</string>
+ <string name="anonymous">Anonim</string>
+ <string name="page">Sayfa</string>
+ <string name="no_network_title">Kullanılabilir ağ yok</string>
+ <string name="caption">Başlık (seçime bağlı)</string>
+ <string name="width">Genişlik</string>
+ <string name="featured_in_post">Görseli yazı içeriğine ekle</string>
+ <string name="posts">Yazılar</string>
+ <string name="post">Yazı</string>
+ <string name="ok">Tamam</string>
+ <string name="blogusername">blogkullanıcıadı</string>
+ <string name="scaled_image">Oranlanmış görsel genişliği</string>
+ <string name="upload_scaled_image">Boyutlandırılmış görseli yükle ve bağlantı ver</string>
+ <string name="scheduled">Zamanlandı</string>
+ <string name="link_enter_url">Adres</string>
+ <string name="uploading">Yükleniyor...</string>
+ <string name="version">Sürüm</string>
+ <string name="app_title">Android için WordPress</string>
+ <string name="tos">Hizmet Şartları</string>
+ <string name="max_thumbnail_px_width">Varsayılan Görsel Genişliği</string>
+ <string name="image_alignment">Konumlandırma</string>
+ <string name="refresh">Yenile</string>
+ <string name="untitled">Başlıksız</string>
+ <string name="edit">Düzenle</string>
+ <string name="page_id">Sayfa</string>
+ <string name="post_id">Yazı</string>
+ <string name="immediately">Hemen</string>
+ <string name="post_password">Parola (seçime bağlı)</string>
+ <string name="quickpress_add_alert_title">Kısayol ismi belirle</string>
+ <string name="today">Bugün</string>
+ <string name="settings">Ayarlar</string>
+ <string name="share_url">Adresi paylaş</string>
+ <string name="quickpress_window_title">QuickPress kısayolu için blog seçin</string>
+ <string name="quickpress_add_error">Kısayol ismi boş olamaz</string>
+ <string name="draft">Taslak</string>
+ <string name="post_private">Özel</string>
+ <string name="publish_post">Yayımla</string>
+ <string name="upload_full_size_image">Yükle ve tam resme bağlantı ver</string>
+ <string name="categories">Kategoriler</string>
+ <string name="tags_separate_with_commas">Etiketler (etiketleri virgül ile ayırın)</string>
+ <string name="title">Başlık</string>
+ <string name="dlg_deleting_comments">Yorumlar siliniyor</string>
+ <string name="notification_vibrate">Titre</string>
+ <string name="notification_blink">Bilgilendirme ışığını parlat</string>
+ <string name="notification_sound">Bilgilendirme sesi</string>
+ <string name="status">Durum</string>
+ <string name="location">Konum</string>
+ <string name="sdcard_title">SD kart gerekli</string>
+ <string name="select_video">Galeriden bir video seçin</string>
+ <string name="media">Ortam</string>
+ <string name="delete">Sil</string>
+ <string name="none">Hiçbiri</string>
+ <string name="blogs">Bloglar</string>
+ <string name="select_photo">Galeriden bir fotoğraf seç</string>
+ <string name="error">Hata</string>
+ <string name="cancel">İptal</string>
+ <string name="save">Kaydet</string>
+ <string name="add">Ekle</string>
+ <string name="preview">Önizleme</string>
+ <string name="on">üstünde</string>
+ <string name="reply">Cevapla</string>
+ <string name="yes">Evet</string>
+ <string name="no">Hayır</string>
+ <string name="category_refresh_error">Kategori tazeleme hatası</string>
+ <string name="notification_settings">Bildirim Ayarları</string>
+</resources>
diff --git a/WordPress/src/main/res/values-uz/strings.xml b/WordPress/src/main/res/values-uz/strings.xml
new file mode 100644
index 000000000..4299b1baf
--- /dev/null
+++ b/WordPress/src/main/res/values-uz/strings.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="loading_videos">Videolarni yuklash</string>
+ <string name="loading_images">Rasmlarni yuklash</string>
+ <string name="device">Qurilma</string>
+ <string name="creating_your_account">Akkauntingiz yaratilmoqda</string>
+ <string name="creating_your_site">Saytingiz yaratilmoqda</string>
+ <string name="reader_empty_posts_in_tag_updating">Postlar yuklanmoqda...</string>
+ <string name="error_refresh_media">Media kutubxonani yangilashda xatolik yuz berdi. Iltimos, keyinroq qayta harakat qilib ko‘ring.</string>
+ <string name="reader_likes_you_and_multi">Siz hamda %,d kishi buni yoqtiradi</string>
+ <string name="reader_likes_multi">%,d kishi buni yoqtiradi</string>
+ <string name="reader_toast_err_get_comment">Bu mulohazani yuklab bo‘lmadi</string>
+ <string name="reader_label_reply">Javob</string>
+ <string name="video">Video</string>
+ <string name="download">Media saqlanmoqda</string>
+ <string name="comment_spammed">Mulohaza spam sifatida belgilandi</string>
+ <string name="cant_share_no_visible_blog">Ochiq blogsiz WordPress bilan bo‘lisha olmaysiz</string>
+ <string name="select_time">Vaqt tanlang</string>
+ <string name="reader_likes_you_and_one">Siz va yana bir kishi buni yoqtiradi</string>
+ <string name="select_date">Sana tanlang</string>
+ <string name="pick_photo">Rasm tanlang</string>
+ <string name="pick_video">Video tanlang</string>
+ <string name="nux_tap_continue">Davom</string>
+ <string name="nux_welcome_create_account">Akkaunt ochish</string>
+ <string name="media_add_new_media_gallery">Yangi galereya</string>
+ <string name="reader_untitled_post">(Nomsiz)</string>
+ <string name="jetpack_not_found">Jetpack plagini topilmadi</string>
+ <string name="media_add_popup_title">Media kutubxonasiga qo‘sh</string>
+ <string name="empty_list_default">Ushbu ro‘yxat bo‘sh</string>
+ <string name="select_from_media_library">Media kutubxonadan tanla</string>
+ <string name="jetpack_message">Statistikalar uchun JetPack plagini lozim. JetPack plaginini o‘rnatasizmi?</string>
+ <string name="reader_btn_share">Bo‘lish</string>
+ <string name="reader_btn_follow">Kuzatish</string>
+ <string name="reader_btn_unfollow">Kuzatmoqda</string>
+ <string name="reader_toast_err_comment_failed">Mulohazangiz kiritilmadi</string>
+ <string name="reader_toast_err_tag_exists">Allaqachon bu tegni kuzatishni boshlagansiz</string>
+ <string name="reader_toast_err_tag_invalid">Bu yaroqli teg emas</string>
+ <string name="reader_toast_err_share_intent">Bo‘lishib bo‘lmadi</string>
+ <string name="reader_toast_err_view_image">Rasm ko‘rinmayapti</string>
+ <string name="reader_toast_err_url_intent">%s ochilmayapti</string>
+ <string name="reader_share_subject">%s orqali baham ko‘rilgan</string>
+ <string name="reader_label_added_tag">%s qo‘shildi</string>
+ <string name="reader_label_removed_tag">%s o‘chirildi</string>
+ <string name="reader_likes_one">Bir kishi yoqtirgan</string>
+ <string name="reader_likes_only_you">Siz yoqtirgansiz</string>
+ <string name="reader_empty_followed_tags">Birorta ham teg kuzatmayapsiz</string>
+ <string name="create_account_wpcom">Wordpress.com da akkaunt oching</string>
+ <string name="connecting_wpcom">Wordpress.com ga bog‘lanmoqda</string>
+ <string name="username_invalid">Yaroqsiz foydalanuvchi nomi</string>
+ <string name="limit_reached">Chegaraga yetib keldik. Bir minutdan keyin harakat qilib ko‘rishingiz mumkin. Bu vaqtdan avval harakatga o‘tishingiz kutish vaqtining yanada uzayishiga olib keladi. Agar bu biror tushunmovchilik natijasi deb o‘ylasangiz, yordam xizmatiga murojaat qiling.</string>
+ <string name="all">Hammasi</string>
+ <string name="images">Rasmlar</string>
+ <string name="unattached">Biriktirilmagan</string>
+ <string name="media_gallery_type_circles">Doiralar</string>
+ <string name="themes">Shablonlar</string>
+ <string name="media_gallery_image_order_random">Tasodifiy</string>
+ <string name="media_gallery_image_order_reverse">Teskari</string>
+ <string name="media_gallery_type">Tur</string>
+ <string name="media_gallery_type_squares">Kvadrat</string>
+ <string name="media_gallery_type_tiled">Bir qator</string>
+ <string name="media_edit_title_text">Sarlavha</string>
+ <string name="media_edit_caption_text">Titr</string>
+ <string name="media_edit_description_text">Izoh</string>
+ <string name="media_edit_success">Yangilandi</string>
+ <string name="media_edit_failure">Yangilanmadi</string>
+ <string name="themes_details_label">Batafsil</string>
+ <string name="themes_features_label">Xususiyatlari</string>
+ <string name="theme_activate_button">Faollashtir</string>
+ <string name="theme_activating_button">Faollashtirilmoqda</string>
+ <string name="theme_set_success">Shablon muvaffaqiyatli faollashtirildi!</string>
+ <string name="theme_auth_error_title">Shablonlar yuklanmadi</string>
+ <string name="post_excerpt">Sitata</string>
+ <string name="share_action_title">Qo‘sh...</string>
+ <string name="share_action">Bo‘lish</string>
+ <string name="stats">Statistika</string>
+ <string name="stats_view_visitors_and_views">Kirivchular va ko‘rishlar</string>
+ <string name="stats_view_clicks">Bosishlar</string>
+ <string name="stats_view_tags_and_categories">Teg va bo‘limlar</string>
+ <string name="stats_timeframe_today">Bugun</string>
+ <string name="stats_timeframe_yesterday">Kecha</string>
+ <string name="stats_timeframe_days">Kunlar</string>
+ <string name="stats_timeframe_weeks">Xaftalar</string>
+ <string name="stats_timeframe_months">Oylar</string>
+ <string name="stats_entry_country">Mamlakat</string>
+ <string name="stats_entry_posts_and_pages">Sarlavha</string>
+ <string name="stats_entry_tags_and_categories">Mavzu</string>
+ <string name="stats_entry_authors">Muallif</string>
+ <string name="stats_totals_views">Ko‘rishlar</string>
+ <string name="stats_totals_clicks">Bosishlar</string>
+ <string name="stats_totals_plays">Ko‘rishlar</string>
+ <string name="custom_date">Ixtiyoriy sana</string>
+ <string name="media_add_popup_capture_photo">Sur‘atga oling</string>
+ <string name="media_add_popup_capture_video">Videoga oling</string>
+ <string name="stats_view_referrers">Referrerlar</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="passcode_manage">PIN kod moslamalari</string>
+ <string name="passcode_enter_passcode">PIN kod kiriting</string>
+ <string name="passcode_enter_old_passcode">Eski PIN kodni kiriting</string>
+ <string name="passcode_re_enter_passcode">PIN kodni qayta kiriting</string>
+ <string name="passcode_change_passcode">PIN kodni o‘zgartirish</string>
+ <string name="passcode_set">PIN kod o‘zgartirildi</string>
+ <string name="passcode_preference_title">PIN qulf</string>
+ <string name="passcode_turn_off">PIN qulfni bekor qilish</string>
+ <string name="passcode_turn_on">PIN qulfdan foydalanish</string>
+ <string name="media_gallery_type_slideshow">Slaydshou</string>
+ <string name="media_edit_title_hint">Sarlavha kiriting</string>
+ <string name="media_edit_caption_hint">Titr kiriting</string>
+ <string name="media_edit_description_hint">Izoh kiriting</string>
+ <string name="upload">Yuklash</string>
+ <string name="discard">Bekor qilish</string>
+ <string name="notifications">Xabarlar</string>
+ <string name="note_reply_successful">Javob chop qilindi</string>
+ <string name="new_notifications">%d ta yangi xabarlar</string>
+ <string name="more_notifications">va yana %d ta.</string>
+ <string name="sign_in">Kirish</string>
+ <string name="loading">Yuklanmoqda...</string>
+ <string name="httppassword">HTTP parol</string>
+ <string name="httpuser">HTTP foydalanuvchi nomi</string>
+ <string name="error_media_upload">Media yuklash jarayonida xatolik yuz berdi.</string>
+ <string name="publish_date">Chop etish</string>
+ <string name="content_description_add_media">Media qo‘shish</string>
+ <string name="post_content">Kontent (matn va media qo‘shish uchun bosing)</string>
+ <string name="incorrect_credentials">Foydalanuvchi nomi yoki parol xato.</string>
+ <string name="password">Parol</string>
+ <string name="username">Foydalanuvchi nomi</string>
+ <string name="reader">Rider</string>
+ <string name="page">Sahifa</string>
+ <string name="anonymous">Anonim</string>
+ <string name="featured">Yuz rasm sifatida foydalanish</string>
+ <string name="featured_in_post">Postga rasm qo‘shish</string>
+ <string name="no_network_title">Netvork mavjud emas</string>
+ <string name="pages">Sahifalar</string>
+ <string name="width">En</string>
+ <string name="posts">Postlar</string>
+ <string name="post">Post</string>
+ <string name="caption">Titr (ixtiyoriy)</string>
+ <string name="blogusername">blogusername</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">Rasm yuklab, hajmi moslashtirilgan (kichiklashtirilgan) nusxasiga havola berish</string>
+ <string name="scaled_image">Yangi hajm uchun en qiymati</string>
+ <string name="scheduled">Jadvalga kiritildi</string>
+ <string name="link_enter_url">URL</string>
+ <string name="version">Versiya</string>
+ <string name="tos">Xizmat shartlari</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="image_alignment">Tekislash</string>
+ <string name="refresh">Yangila</string>
+ <string name="untitled">Nomsiz</string>
+ <string name="edit">Tahrir qil</string>
+ <string name="post_id">Post</string>
+ <string name="page_id">Sahifa</string>
+ <string name="post_password">Parol (ixtiyoriy)</string>
+ <string name="immediately">Darrov</string>
+ <string name="quickpress_add_alert_title">Tez o‘tish nomi</string>
+ <string name="settings">Moslamalar</string>
+ <string name="quickpress_add_error">Tez o‘tar nomi bo‘sh bo‘lishi mumkin emas</string>
+ <string name="quickpress_window_title">QuickPress tez o‘tari uchun blog tanlang</string>
+ <string name="post_private">Maxfiy</string>
+ <string name="publish_post">Chop qil</string>
+ <string name="draft">Qoralama</string>
+ <string name="upload_full_size_image">To‘liq hajmda rasm yuklash</string>
+ <string name="tags_separate_with_commas">Teglar (vergul bilan ajrating)</string>
+ <string name="title">Sarlavha</string>
+ <string name="categories">Bo‘limlar</string>
+ <string name="notification_vibrate">Vibratsiya</string>
+ <string name="notification_blink">Yonib-o‘chuvchi xabar</string>
+ <string name="status">Status</string>
+ <string name="location">Manzil</string>
+ <string name="select_video">Galereyadan video tanla</string>
+ <string name="sdcard_title">SD karta kerak</string>
+ <string name="media">Media</string>
+ <string name="delete">O‘chir</string>
+ <string name="none">Hech bir</string>
+ <string name="blogs">Bloglar</string>
+ <string name="select_photo">Galereyadan rasm tanlash</string>
+ <string name="no">Yo‘q</string>
+ <string name="yes">Ha</string>
+ <string name="reply">Javob</string>
+ <string name="cancel">Bekor qilish</string>
+ <string name="add">Qo‘shish</string>
+ <string name="preview">Oldindan ko‘rish</string>
+ <string name="category_refresh_error">Bo‘limlarni yangilashda xatolik</string>
+ <string name="error">Xato</string>
+ <string name="save">Saqla</string>
+ <string name="on">da</string>
+</resources>
diff --git a/WordPress/src/main/res/values-v16/styles.xml b/WordPress/src/main/res/values-v16/styles.xml
new file mode 100644
index 000000000..e1f64aa05
--- /dev/null
+++ b/WordPress/src/main/res/values-v16/styles.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="ThemeDetailsHeader">
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+</resources>
diff --git a/WordPress/src/main/res/values-w400dp/dimens.xml b/WordPress/src/main/res/values-w400dp/dimens.xml
new file mode 100644
index 000000000..e10a0ee5a
--- /dev/null
+++ b/WordPress/src/main/res/values-w400dp/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="tabstrip_icon_spacing">@dimen/tabstrip_icon_spacing_medium</dimen>
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values-w600dp/dimens.xml b/WordPress/src/main/res/values-w600dp/dimens.xml
new file mode 100644
index 000000000..fbd9032d2
--- /dev/null
+++ b/WordPress/src/main/res/values-w600dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <dimen name="content_margin">@dimen/content_margin_tablet</dimen>
+ <dimen name="reader_card_margin">@dimen/reader_card_margin_tablet</dimen>
+ <dimen name="reader_detail_margin">@dimen/reader_detail_margin_tablet</dimen>
+ <dimen name="notifications_content_margin">@dimen/content_margin_tablet</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-w720dp/dimens.xml b/WordPress/src/main/res/values-w720dp/dimens.xml
new file mode 100644
index 000000000..ed5223165
--- /dev/null
+++ b/WordPress/src/main/res/values-w720dp/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="content_margin">@dimen/content_margin_tablet_big</dimen>
+ <dimen name="reader_card_margin">@dimen/reader_card_margin_tablet_big</dimen>
+ <dimen name="reader_detail_margin">@dimen/reader_detail_margin_tablet_big</dimen>
+ <dimen name="reader_webview_width">680dp</dimen>
+ <dimen name="notifications_content_margin">@dimen/content_margin_tablet_big</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values-zh b/WordPress/src/main/res/values-zh
new file mode 120000
index 000000000..46af1b036
--- /dev/null
+++ b/WordPress/src/main/res/values-zh
@@ -0,0 +1 @@
+values-zh-rCN \ No newline at end of file
diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 000000000..5b92da2c1
--- /dev/null
+++ b/WordPress/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,1136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">管理员</string>
+ <string name="role_editor">编辑者</string>
+ <string name="role_author">作者</string>
+ <string name="role_contributor">贡献者</string>
+ <string name="role_follower">粉丝</string>
+ <string name="role_viewer">查看者</string>
+ <string name="error_post_my_profile_no_connection">网络未连接,无法保存您的个人资料</string>
+ <string name="alignment_none">无</string>
+ <string name="alignment_left">左</string>
+ <string name="alignment_right">右</string>
+ <string name="site_settings_list_editor_action_mode_title">已选择 %1$d</string>
+ <string name="error_fetch_users_list">无法检索站点用户</string>
+ <string name="plans_manage">在 WordPress.com/plans 上\n管理您的套餐</string>
+ <string name="people_empty_list_filtered_viewers">您目前没有任何查看者。</string>
+ <string name="people_fetching">正在获取用户…</string>
+ <string name="title_follower">粉丝</string>
+ <string name="title_email_follower">电子邮件粉丝</string>
+ <string name="people_empty_list_filtered_email_followers">您目前没有任何电子邮件粉丝。</string>
+ <string name="people_empty_list_filtered_followers">您目前没有任何粉丝。</string>
+ <string name="people_empty_list_filtered_users">您目前没有任何用户。</string>
+ <string name="people_dropdown_item_email_followers">电子邮件粉丝</string>
+ <string name="people_dropdown_item_viewers">查看者</string>
+ <string name="people_dropdown_item_followers">粉丝</string>
+ <string name="people_dropdown_item_team">团队</string>
+ <string name="invite_message_usernames_limit">最多可邀请 10 个电子邮件地址和/或 WordPress.com 用户名。没有用户名的人将收到有关如何创建用户名的说明。</string>
+ <string name="viewer_remove_confirmation_message">如果删除该查看者,他或她将无法访问此站点。\n\n是否仍要删除该查看者?</string>
+ <string name="follower_remove_confirmation_message">如果删除该粉丝,则此人将不会再收到关于此站点的通知,除非其重新关注此站点。\n\n是否仍要删除该粉丝?</string>
+ <string name="follower_subscribed_since">从 %1$s开始</string>
+ <string name="reader_label_view_gallery">查看图库</string>
+ <string name="error_remove_follower">无法删除粉丝</string>
+ <string name="error_remove_viewer">无法删除查看者</string>
+ <string name="error_fetch_email_followers_list">无法检索站点电子邮件粉丝</string>
+ <string name="error_fetch_followers_list">无法检索站点粉丝</string>
+ <string name="editor_failed_uploads_switch_html">部分媒体上传失败。在此状态下,您无法切换\n到 HTML 模式。删除所有失败的上传并继续?</string>
+ <string name="format_bar_description_html">HTML 模式</string>
+ <string name="visual_editor">可视化编辑器</string>
+ <string name="image_thumbnail">图片缩略图</string>
+ <string name="format_bar_description_ul">无序列表</string>
+ <string name="format_bar_description_ol">有序列表</string>
+ <string name="format_bar_description_more">插入更多</string>
+ <string name="format_bar_description_media">插入媒体</string>
+ <string name="format_bar_description_strike">删除线</string>
+ <string name="format_bar_description_quote">阻止引用</string>
+ <string name="format_bar_description_link">插入链接</string>
+ <string name="format_bar_description_italic">斜体</string>
+ <string name="format_bar_description_underline">下划线</string>
+ <string name="image_settings_save_toast">更改已保存</string>
+ <string name="image_caption">标题</string>
+ <string name="image_alt_text">替代文本</string>
+ <string name="image_link_to">链接到</string>
+ <string name="image_width">宽度</string>
+ <string name="format_bar_description_bold">粗体</string>
+ <string name="image_settings_dismiss_dialog_title">放弃未保存的更改?</string>
+ <string name="stop_upload_dialog_title">停止上传?</string>
+ <string name="stop_upload_button">停止上传</string>
+ <string name="alert_error_adding_media">插入媒体时出错</string>
+ <string name="alert_action_while_uploading">您目前正在上传媒体。请等待,直到此任务完成。</string>
+ <string name="alert_insert_image_html_mode">无法在 HTML 模式中直接插入媒体。请切换回可视模式。</string>
+ <string name="uploading_gallery_placeholder">正在上传图库…</string>
+ <string name="invite_error_for_username">%1$s:%2$s</string>
+ <string name="invite_sent">已成功发送邀请</string>
+ <string name="tap_to_try_again">轻点即可重试!</string>
+ <string name="invite_message_info">(可选)您最多可以在用户邀请函中输入 500 个字符的自定义消息。</string>
+ <string name="invite_message_remaining_other">剩余 %d 个字符</string>
+ <string name="invite_message_remaining_one">剩余 1 个字符</string>
+ <string name="invite_message_remaining_zero">剩余 0 个字符</string>
+ <string name="invite_invalid_email">电子邮件地址“%s”无效</string>
+ <string name="invite_message_title">自定义消息</string>
+ <string name="invite_already_a_member">已存在用户名为“%s”的成员</string>
+ <string name="invite_username_not_found">未找到用户名为“%s”的用户</string>
+ <string name="invite">邀请</string>
+ <string name="invite_names_title">用户名或电子邮件地址</string>
+ <string name="signup_succeed_signin_failed">您的帐户已创建,但在您登录时出错\n。请尝试使用您新创建的用户名和密码登录。</string>
+ <string name="send_link">发送链接</string>
+ <string name="my_site_header_external">外部</string>
+ <string name="invite_people">邀请用户</string>
+ <string name="label_clear_search_history">清除搜索历史记录</string>
+ <string name="dlg_confirm_clear_search_history">清除搜索历史记录?</string>
+ <string name="reader_empty_posts_in_search_description">未找到与您的语言对应的符合“%s”搜索条件的文章</string>
+ <string name="reader_label_post_search_running">正在搜索…</string>
+ <string name="reader_label_related_posts">相关阅读</string>
+ <string name="reader_empty_posts_in_search_title">未找到文章</string>
+ <string name="reader_label_post_search_explainer">搜索所有公开的 WordPress.com 博客</string>
+ <string name="reader_hint_post_search">搜索 WordPress.com</string>
+ <string name="reader_title_related_post_detail">相关文章</string>
+ <string name="reader_title_search_results">搜索 %s</string>
+ <string name="preview_screen_links_disabled">预览屏幕禁用链接</string>
+ <string name="draft_explainer">此文章为尚未发布的草稿</string>
+ <string name="send">发送</string>
+ <string name="person_remove_confirmation_title">删除 %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">此列表中的站点最近未发布任何内容</string>
+ <string name="people">人员</string>
+ <string name="edit_user">编辑用户</string>
+ <string name="role">角色</string>
+ <string name="error_remove_user">无法删除用户</string>
+ <string name="error_update_role">无法更新用户角色</string>
+ <string name="gravatar_camera_and_media_permission_required">要选择或拍摄照片,请获取权限</string>
+ <string name="error_updating_gravatar">更新 Gravatar 时出错</string>
+ <string name="error_locating_image">查找已裁剪的图像时出错</string>
+ <string name="error_refreshing_gravatar">重新加载 Gravatar 时出错</string>
+ <string name="gravatar_tip">新功能!轻点 Gravatar 即可进行更改!</string>
+ <string name="error_cropping_image">裁剪图像时出错</string>
+ <string name="launch_your_email_app">启动电子邮件应用程序</string>
+ <string name="checking_email">检查电子邮件地址</string>
+ <string name="not_on_wordpress_com">尚未登录 WordPress.com?</string>
+ <string name="magic_link_unavailable_error_message">目前不可用。请输入密码</string>
+ <string name="check_your_email">检查电子邮件</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">获取电子邮件中收到的链接,立即登录</string>
+ <string name="logging_in">正在登录</string>
+ <string name="enter_your_password_instead">改为输入密码</string>
+ <string name="web_address_dialog_hint">公开显示您的评论。</string>
+ <string name="jetpack_not_connected_message">已安装 Jetpack 插件,但未连接至 WordPress.com。要连接 Jetpack 吗?</string>
+ <string name="username_email">电子邮件或用户名</string>
+ <string name="jetpack_not_connected">未连接 Jetpack 插件</string>
+ <string name="new_editor_reflection_error">可视化编辑器与您的设备不兼容。已\n自动停用。</string>
+ <string name="stats_insights_latest_post_no_title">(无标题)</string>
+ <string name="capture_or_pick_photo">拍摄或选择照片</string>
+ <string name="plans_post_purchase_text_themes">您现在获得了对高级套餐主题的无限制访问权限。预览站点上的任意主题即可开始使用。</string>
+ <string name="plans_post_purchase_button_themes">浏览主题</string>
+ <string name="plans_post_purchase_title_themes">查找出色的高级套餐主题</string>
+ <string name="plans_post_purchase_button_video">撰写新文章</string>
+ <string name="plans_post_purchase_text_video">您可以通过 VideoPress 和扩展媒体存储向站点上传视频和在站点上托管视频。</string>
+ <string name="plans_post_purchase_title_video">通过视频让文章生动起来</string>
+ <string name="plans_post_purchase_button_customize">自定义我的站点</string>
+ <string name="plans_post_purchase_text_customize">您现在可以访问自定义字体、自定义颜色和自定义 CSS 编辑功能。</string>
+ <string name="plans_post_purchase_text_intro">您的站点正在实现华丽变身!现在探索站点的新功能,根据需要选择合适的切入点。</string>
+ <string name="plans_post_purchase_title_customize">自定义字体和颜色</string>
+ <string name="plans_post_purchase_title_intro">这些功能都是您的了,尽情使用吧!</string>
+ <string name="export_your_content_message">系统会将您的文章、页面和设置发送到以下电子邮件地址:%s。</string>
+ <string name="plan">套餐</string>
+ <string name="plans">套餐</string>
+ <string name="plans_loading_error">无法加载套餐</string>
+ <string name="export_your_content">导出您的内容</string>
+ <string name="exporting_content_progress">正在导出内容…</string>
+ <string name="export_email_sent">导出已发送的电子邮件!</string>
+ <string name="premium_upgrades_message">您的站点上存在处于活动状态的高级套餐升级。删除站点前,请先取消升级。</string>
+ <string name="show_purchases">显示购买交易</string>
+ <string name="checking_purchases">查看购买交易</string>
+ <string name="premium_upgrades_title">高级套餐升级</string>
+ <string name="purchases_request_error">出现错误。无法请求购买交易。</string>
+ <string name="delete_site_progress">正在删除站点…</string>
+ <string name="delete_site_summary">此操作不可撤销。删除站点会同时删除此站点中的全部内容、贡献者以及域。</string>
+ <string name="delete_site_hint">删除站点</string>
+ <string name="export_site_hint">将站点导出到 XML 文件中</string>
+ <string name="are_you_sure">确定吗?</string>
+ <string name="export_site_summary">如果确定,请务必花点时间立即导出您的内容。这些内容一经删除便无法恢复。</string>
+ <string name="keep_your_content">保留您的内容</string>
+ <string name="domain_removal_hint">删除您的站点后无法工作的域</string>
+ <string name="domain_removal_summary">注意!删除您的站点也会删除下列域。</string>
+ <string name="primary_domain">主域</string>
+ <string name="domain_removal">删除域</string>
+ <string name="error_deleting_site_summary">删除您的站点时出现错误。请联系支持人员,获取更多帮助</string>
+ <string name="error_deleting_site">删除站点时出错</string>
+ <string name="confirm_delete_site_prompt">请在下列字段中输入 %1$s,进行确认。之后,您的站点将不复存在。</string>
+ <string name="site_settings_export_content_title">导出内容</string>
+ <string name="contact_support">联系支持人员</string>
+ <string name="confirm_delete_site">确认删除站点</string>
+ <string name="start_over_text">如果您想留下某个站点,但不想保留其中现有的任何文章和页面,我们的支持团队可以为您删除相关文章、页面、媒体以及评论。\n\n这不会影响您站点和 URL 的有效性,还能让您重新开始创建内容。联系我们即可清除当前所有内容。</string>
+ <string name="site_settings_start_over_hint">从头开始构建您的站点</string>
+ <string name="let_us_help">让我们助您一臂之力</string>
+ <string name="me_btn_app_settings">应用程序设置</string>
+ <string name="start_over">从头开始</string>
+ <string name="editor_remove_failed_uploads">删除上传失败的内容</string>
+ <string name="editor_toast_failed_uploads">部分媒体上传失败。您无法保存或发布\n此状态下的文章。是否删除所有上传失败的媒体?</string>
+ <string name="comments_empty_list_filtered_trashed">无已放入垃圾桶的评论</string>
+ <string name="site_settings_advanced_header">高级</string>
+ <string name="comments_empty_list_filtered_pending">无待审评论</string>
+ <string name="comments_empty_list_filtered_approved">无已批准的评论</string>
+ <string name="button_done">完成</string>
+ <string name="button_skip">跳过</string>
+ <string name="site_timeout_error">出现超时错误,无法连接到 WordPress 站点。</string>
+ <string name="xmlrpc_malformed_response_error">无法连接。WordPress 安装响应并返回了无效的 XML-RPC 文档。</string>
+ <string name="xmlrpc_missing_method_error">无法连接。服务器上没有所需的 XML-RPC 方法。</string>
+ <string name="post_format_status">状态</string>
+ <string name="post_format_video">视频</string>
+ <string name="theme_free">免费</string>
+ <string name="theme_all">全部</string>
+ <string name="theme_premium">付费</string>
+ <string name="post_format_chat">聊天</string>
+ <string name="post_format_gallery">图库</string>
+ <string name="post_format_image">图像</string>
+ <string name="post_format_link">链接</string>
+ <string name="post_format_quote">引用</string>
+ <string name="post_format_standard">标准</string>
+ <string name="notif_events">有关 WordPress.com 课程和活动(在线和现场)的信息。</string>
+ <string name="post_format_aside">旁白</string>
+ <string name="post_format_audio">音频</string>
+ <string name="notif_surveys">参加 WordPress.com 研究和调查的机会。</string>
+ <string name="notif_tips">有关畅享 WordPress.com 的提示。</string>
+ <string name="notif_community">社区</string>
+ <string name="replies_to_my_comments">我的评论收到的回复</string>
+ <string name="notif_suggestions">建议</string>
+ <string name="notif_research">研究</string>
+ <string name="site_achievements">站点成就</string>
+ <string name="username_mentions">用户名提到的次数</string>
+ <string name="likes_on_my_posts">我的文章收到的赞</string>
+ <string name="site_follows">站点关注人数</string>
+ <string name="likes_on_my_comments">我的评论收到的赞</string>
+ <string name="comments_on_my_site">我的站点上的评论</string>
+ <string name="site_settings_list_editor_summary_other">%d 个条目</string>
+ <string name="site_settings_list_editor_summary_one">1 个条目</string>
+ <string name="approve_auto_if_previously_approved">已知用户的评论</string>
+ <string name="approve_auto">所有用户</string>
+ <string name="approve_manual">没有评论</string>
+ <string name="site_settings_paging_summary_other">每页 %d 条评论</string>
+ <string name="site_settings_paging_summary_one">每页 1 条评论</string>
+ <string name="site_settings_multiple_links_summary_other">超过 %d 个链接需要审核</string>
+ <string name="site_settings_multiple_links_summary_one">超过 1 个链接需要审核</string>
+ <string name="site_settings_multiple_links_summary_zero">超过 0 个链接需要审核</string>
+ <string name="detail_approve_auto">自动批准每个人的评论。</string>
+ <string name="detail_approve_auto_if_previously_approved">如果用户已有获得批准的评论,则自动批准该用户的其他评论</string>
+ <string name="detail_approve_manual">每个人的评论都需要进行人工审核。</string>
+ <string name="filter_trashed_posts">已放入回收站</string>
+ <string name="days_quantity_one">1 天</string>
+ <string name="days_quantity_other">%d 天</string>
+ <string name="filter_published_posts">已发布</string>
+ <string name="filter_draft_posts">草稿</string>
+ <string name="filter_scheduled_posts">预发布</string>
+ <string name="pending_email_change_snackbar">单击发送给 %1$s 的电子邮件中的验证链接以确认您的新地址</string>
+ <string name="primary_site">主站点</string>
+ <string name="web_address">网页地址</string>
+ <string name="editor_toast_uploading_please_wait">您目前正在上传媒体。请等待,直到此任务完成。</string>
+ <string name="error_refresh_comments_showing_older">目前无法刷新评论 – 显示的是旧评论</string>
+ <string name="editor_post_settings_set_featured_image">设置推荐图像</string>
+ <string name="editor_post_settings_featured_image">推荐图像</string>
+ <string name="new_editor_promo_desc">适用于 Android 的 WordPress 应用程序现在包含美观的全新可视化\n编辑器。请创建一篇新文章试试看。</string>
+ <string name="new_editor_promo_title">全新的编辑器</string>
+ <string name="new_editor_promo_button_label">太棒了,谢谢!</string>
+ <string name="visual_editor_enabled">已启用可视化编辑器</string>
+ <string name="editor_content_placeholder">在此处分享您的故事…</string>
+ <string name="editor_page_title_placeholder">页面标题</string>
+ <string name="editor_post_title_placeholder">文章标题</string>
+ <string name="email_address">电子邮件地址</string>
+ <string name="preference_show_visual_editor">显示可视化编辑器</string>
+ <string name="dlg_sure_to_delete_comments">是否永久删除这些评论?</string>
+ <string name="preference_editor">编辑器</string>
+ <string name="dlg_sure_to_delete_comment">是否永久删除该评论?</string>
+ <string name="mnu_comment_delete_permanently">删除</string>
+ <string name="comment_deleted_permanently">已删除评论</string>
+ <string name="mnu_comment_untrash">恢复</string>
+ <string name="comments_empty_list_filtered_spam">无垃圾评论</string>
+ <string name="could_not_load_page">无法加载页面</string>
+ <string name="comment_status_all">全部</string>
+ <string name="interface_language">界面语言</string>
+ <string name="off">关闭</string>
+ <string name="about_the_app">关于该应用程序</string>
+ <string name="error_post_account_settings">无法保存您的帐户设置</string>
+ <string name="error_post_my_profile">无法保存您的个人资料</string>
+ <string name="error_fetch_account_settings">无法检索您的帐户设置</string>
+ <string name="error_fetch_my_profile">无法检索您的个人资料</string>
+ <string name="stats_widget_promo_ok_btn_label">好的,知道了</string>
+ <string name="stats_widget_promo_desc">在主屏幕中添加小组件,即可一键访问统计信息。</string>
+ <string name="stats_widget_promo_title">主屏幕统计信息小组件</string>
+ <string name="site_settings_unknown_language_code_error">无法识别语言代码</string>
+ <string name="site_settings_threading_dialog_description">允许在线程中嵌入评论。</string>
+ <string name="site_settings_threading_dialog_header">线程级数上限</string>
+ <string name="remove">移除</string>
+ <string name="search">搜索</string>
+ <string name="add_category">添加分类目录</string>
+ <string name="disabled">禁用</string>
+ <string name="site_settings_image_original_size">原始大小</string>
+ <string name="privacy_private">您的站点仅对您以及您审核通过的用户可见</string>
+ <string name="privacy_public_not_indexed">您的站点对所有人可见,但要求搜索引擎不将其编入索引</string>
+ <string name="privacy_public">您的站点对所有人可见,并且可以被搜索引擎编入索引</string>
+ <string name="about_me_hint">有关您的一些字词…</string>
+ <string name="public_display_name_hint">显示名称(如果未设置)将默认为您的用户名</string>
+ <string name="about_me">关于我</string>
+ <string name="public_display_name">公开显示名称</string>
+ <string name="my_profile">我的个人资料</string>
+ <string name="first_name">名字</string>
+ <string name="last_name">姓氏</string>
+ <string name="site_privacy_public_desc">允许搜索引擎将此站点编入索引</string>
+ <string name="site_privacy_hidden_desc">不鼓励搜索引擎将此站点编入索引</string>
+ <string name="site_privacy_private_desc">我希望我的站点保持私密,仅对我选择的用户可见</string>
+ <string name="cd_related_post_preview_image">相关文章预览图片</string>
+ <string name="error_post_remote_site_settings">无法保存站点信息</string>
+ <string name="error_fetch_remote_site_settings">无法检索站点信息</string>
+ <string name="error_media_upload_connection">上传媒体文件时出现连接错误</string>
+ <string name="site_settings_disconnected_toast">已断开连接,编辑已被禁用。</string>
+ <string name="site_settings_unsupported_version_error">不受支持的 WordPress 版本</string>
+ <string name="site_settings_multiple_links_dialog_description">所含链接数超过此数量的评论需要进行审核。</string>
+ <string name="site_settings_close_after_dialog_switch_text">自动关闭</string>
+ <string name="site_settings_close_after_dialog_description">自动关闭文章评论。</string>
+ <string name="site_settings_paging_dialog_description">将评论线程划分到多个页面。</string>
+ <string name="site_settings_paging_dialog_header">每个页面上的评论</string>
+ <string name="site_settings_close_after_dialog_title">关闭评论</string>
+ <string name="site_settings_blacklist_description">如果评论的内容、名称、URL、电子邮件或 IP 中包含其中任意字词,则评论会被标记为垃圾内容。您可输入部分字词,这样,输入“press”将匹配出“WordPress”。</string>
+ <string name="site_settings_hold_for_moderation_description">如果评论的内容、名称、URL、电子邮件或 IP 中包含其中任意字词,则评论会保留在审核队列中。您可输入部分字词,这样,输入“press”将匹配出“WordPress”。</string>
+ <string name="site_settings_list_editor_input_hint">输入一个单词或短语</string>
+ <string name="site_settings_list_editor_no_items_text">无菜单项</string>
+ <string name="site_settings_learn_more_caption">您可以为各篇文章覆盖这些设置。</string>
+ <string name="site_settings_rp_preview3_site">在“升级”中</string>
+ <string name="site_settings_rp_preview3_title">升级重点:婚礼 VideoPress</string>
+ <string name="site_settings_rp_preview2_site">在“应用程序”中</string>
+ <string name="site_settings_rp_preview2_title">Android 版 WordPress 应用程序外观有大变动</string>
+ <string name="site_settings_rp_preview1_site">在“手机”中</string>
+ <string name="site_settings_rp_preview1_title">iPhone/iPad 现在有大更新</string>
+ <string name="site_settings_rp_show_images_title">显示图片</string>
+ <string name="site_settings_rp_show_header_title">显示标题</string>
+ <string name="site_settings_rp_switch_summary">“相关文章”在您的文章下方显示您站点中的相关内容。</string>
+ <string name="site_settings_rp_switch_title">显示相关文章</string>
+ <string name="site_settings_delete_site_hint">从应用程序中删除站点数据</string>
+ <string name="site_settings_blacklist_hint">将与过滤器匹配的评论标记为垃圾内容</string>
+ <string name="site_settings_moderation_hold_hint">将与过滤器匹配的评论置于审核队列</string>
+ <string name="site_settings_multiple_links_hint">忽略来自已知用户的链接限制</string>
+ <string name="site_settings_whitelist_hint">评论作者必须已有审核通过的评论</string>
+ <string name="site_settings_user_account_required_hint">用户必须已注册并登录才能评论</string>
+ <string name="site_settings_identity_required_hint">评论作者必须填写名称和电子邮件</string>
+ <string name="site_settings_manual_approval_hint">评论必须经过人工审核</string>
+ <string name="site_settings_paging_hint">以指定大小的数据块显示评论</string>
+ <string name="site_settings_threading_hint">允许特定深度的嵌套评论</string>
+ <string name="site_settings_sort_by_hint">确定评论的显示顺序</string>
+ <string name="site_settings_close_after_hint">不允许在指定时间后评论</string>
+ <string name="site_settings_receive_pingbacks_hint">允许来自其他博客的链接通知</string>
+ <string name="site_settings_send_pingbacks_hint">尝试通知从文章链接到的任何博客</string>
+ <string name="site_settings_allow_comments_hint">允许阅读者发布评论</string>
+ <string name="site_settings_discussion_hint">查看并更改您的站点讨论设置</string>
+ <string name="site_settings_more_hint">查看所有可用的讨论设置</string>
+ <string name="site_settings_related_posts_hint">在阅读器中显示或隐藏相关文章</string>
+ <string name="site_settings_upload_and_link_image_hint">启用以始终上传完整大小的图片</string>
+ <string name="site_settings_image_width_hint">将文章中的图片大小调整至此宽度</string>
+ <string name="site_settings_format_hint">设置新的文章格式</string>
+ <string name="site_settings_category_hint">设置新的文章类别</string>
+ <string name="site_settings_location_hint">自动为文章添加位置数据</string>
+ <string name="site_settings_password_hint">更改您的密码</string>
+ <string name="site_settings_username_hint">当前用户帐户</string>
+ <string name="site_settings_language_hint">此博客的主要撰写语言</string>
+ <string name="site_settings_privacy_hint">控制可查看站点的人员</string>
+ <string name="site_settings_address_hint">目前不支持更改地址</string>
+ <string name="site_settings_tagline_hint">介绍您的博客的简短说明或夺人眼球的短语</string>
+ <string name="site_settings_title_hint">以简洁的话语介绍此站点的内容</string>
+ <string name="site_settings_whitelist_known_summary">来自已知用户的评论</string>
+ <string name="site_settings_whitelist_all_summary">来自所有用户的评论</string>
+ <string name="site_settings_threading_summary">%d个级别</string>
+ <string name="site_settings_privacy_private_summary">私密</string>
+ <string name="site_settings_privacy_hidden_summary">已隐藏</string>
+ <string name="site_settings_delete_site_title">删除站点</string>
+ <string name="site_settings_privacy_public_summary">公开</string>
+ <string name="site_settings_blacklist_title">黑名单</string>
+ <string name="site_settings_moderation_hold_title">保持审核状态</string>
+ <string name="site_settings_multiple_links_title">评论中的链接</string>
+ <string name="site_settings_whitelist_title">自动审核</string>
+ <string name="site_settings_threading_title">线程</string>
+ <string name="site_settings_paging_title">分页</string>
+ <string name="site_settings_sort_by_title">排序方式</string>
+ <string name="site_settings_account_required_title">用户必须注册</string>
+ <string name="site_settings_identity_required_title">必须包括名称和电子邮件</string>
+ <string name="site_settings_receive_pingbacks_title">接收 Pingback</string>
+ <string name="site_settings_send_pingbacks_title">发送 Pingback</string>
+ <string name="site_settings_allow_comments_title">允许评论</string>
+ <string name="site_settings_default_format_title">默认格式</string>
+ <string name="site_settings_default_category_title">默认类别</string>
+ <string name="site_settings_location_title">启用位置</string>
+ <string name="site_settings_address_title">地址</string>
+ <string name="site_settings_title_title">站点标题</string>
+ <string name="site_settings_tagline_title">标语</string>
+ <string name="site_settings_this_device_header">此设备</string>
+ <string name="site_settings_discussion_new_posts_header">默认用于新文章</string>
+ <string name="site_settings_account_header">帐户</string>
+ <string name="site_settings_writing_header">撰写</string>
+ <string name="newest_first">最新评论在先</string>
+ <string name="site_settings_general_header">常规</string>
+ <string name="discussion">讨论</string>
+ <string name="privacy">隐私</string>
+ <string name="related_posts">相关文章</string>
+ <string name="comments">评论</string>
+ <string name="close_after">在指定天数后关闭</string>
+ <string name="oldest_first">最早评论在先</string>
+ <string name="media_error_no_permission_upload">您无权向此站点上传媒体文件</string>
+ <string name="never">从不</string>
+ <string name="unknown">未知</string>
+ <string name="reader_err_get_post_not_found">此文章不再存在</string>
+ <string name="reader_err_get_post_not_authorized">您不具备查看此文章的权限</string>
+ <string name="reader_err_get_post_generic">无法检索这篇文章</string>
+ <string name="blog_name_no_spaced_allowed">站点地址中不能包含空格</string>
+ <string name="invalid_username_no_spaces">用户名中不能包含空格</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">您关注的站点近期未发布任何内容</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">无近期文章</string>
+ <string name="media_details_copy_url_toast">已将 URL 复制到剪贴板</string>
+ <string name="edit_media">编辑媒体</string>
+ <string name="media_details_copy_url">复制 URL</string>
+ <string name="media_details_label_date_uploaded">已上传</string>
+ <string name="media_details_label_date_added">已添加</string>
+ <string name="selected_theme">选择的主题</string>
+ <string name="could_not_load_theme">无法加载主题</string>
+ <string name="theme_activation_error">出现错误。无法激活主题</string>
+ <string name="theme_by_author_prompt_append">作者:%1$s</string>
+ <string name="theme_prompt">感谢您选择 %1$s</string>
+ <string name="theme_try_and_customize">尝试与定制</string>
+ <string name="theme_view">查看</string>
+ <string name="theme_details">详细信息</string>
+ <string name="theme_support">支持</string>
+ <string name="theme_done">完成</string>
+ <string name="theme_manage_site">管理站点</string>
+ <string name="title_activity_theme_support">主题</string>
+ <string name="theme_activate">激活</string>
+ <string name="date_range_start_date">开始日期</string>
+ <string name="date_range_end_date">结束日期</string>
+ <string name="current_theme">当前主题</string>
+ <string name="customize">定制</string>
+ <string name="details">详细信息</string>
+ <string name="support">支持</string>
+ <string name="active">已激活</string>
+ <string name="stats_referrers_spam_generic_error">操作过程中出现错误。未更改垃圾评论的状态。</string>
+ <string name="stats_referrers_marking_not_spam">正在标记为非垃圾评论</string>
+ <string name="stats_referrers_unspam">非垃圾评论</string>
+ <string name="stats_referrers_marking_spam">标记为垃圾评论</string>
+ <string name="theme_auth_error_authenticate">未能获取主题:验证用户身份失败</string>
+ <string name="post_published">文章已发布</string>
+ <string name="page_published">页面已发布</string>
+ <string name="post_updated">文章已更新</string>
+ <string name="page_updated">页面已更新</string>
+ <string name="stats_referrers_spam">垃圾评论</string>
+ <string name="theme_no_search_result_found">抱歉,找不到主题。</string>
+ <string name="media_file_name">文件名:%s</string>
+ <string name="media_uploaded_on">上传时间:%s</string>
+ <string name="media_dimensions">尺寸:%s</string>
+ <string name="upload_queued">在排队</string>
+ <string name="media_file_type">文件类型:%s</string>
+ <string name="reader_label_gap_marker">加载更多文章</string>
+ <string name="notifications_no_search_results">没有符合“%s”条件的站点</string>
+ <string name="search_sites">搜索站点</string>
+ <string name="notifications_empty_view_reader">查看阅读器</string>
+ <string name="unread">未读</string>
+ <string name="notifications_empty_action_followers_likes">受到注意:评论您已读过的文章。</string>
+ <string name="notifications_empty_action_comments">加入会话:评论您关注的博客中的文章。</string>
+ <string name="notifications_empty_action_unread">重新发起会话:撰写新文章。</string>
+ <string name="notifications_empty_action_all">活跃起来吧!评论您关注的博客中的文章。</string>
+ <string name="notifications_empty_likes">尚无新的赞可显示…。</string>
+ <string name="notifications_empty_followers">尚无新的关注者可报告…。</string>
+ <string name="notifications_empty_comments">尚无新评论…。</string>
+ <string name="notifications_empty_unread">您已看完所有通知!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">请访问应用程序中的“统计”,并于稍后尝试添加该小组件</string>
+ <string name="stats_widget_error_readd_widget">请删除该小组件,然后重新添加</string>
+ <string name="stats_widget_error_no_visible_blog">若没有可见的博客,则无法访问“统计”</string>
+ <string name="stats_widget_error_no_permissions">您的 WordPress.com 帐户无法访问此博客上的“统计”</string>
+ <string name="stats_widget_error_no_account">请登录 WordPress</string>
+ <string name="stats_widget_error_generic">无法加载统计信息</string>
+ <string name="stats_widget_loading_data">正在加载数据…</string>
+ <string name="stats_widget_name_for_blog">%1$s 的今日统计</string>
+ <string name="stats_widget_name">WordPress 今日统计</string>
+ <string name="add_location_permission_required">添加位置所需的权限。</string>
+ <string name="add_media_permission_required">添加媒体所需的权限</string>
+ <string name="access_media_permission_required">访问媒体所需的权限</string>
+ <string name="stats_enable_rest_api_in_jetpack">如需查看您的状态,请启用Jetpack的JSON API模块。</string>
+ <string name="error_open_list_from_notification">此文章或页面已在其它站点发布</string>
+ <string name="reader_short_comment_count_multi">%s 评论</string>
+ <string name="reader_short_comment_count_one">1评论</string>
+ <string name="reader_label_submit_comment">发送</string>
+ <string name="reader_hint_comment_on_post">回复文章...</string>
+ <string name="reader_discover_visit_blog">访问 %s</string>
+ <string name="reader_discover_attribution_blog">最初发布在 %s 上</string>
+ <string name="reader_discover_attribution_author">最初由%s发布</string>
+ <string name="reader_discover_attribution_author_and_blog">最初由%1$s发布在 %2$s 上</string>
+ <string name="reader_short_like_count_multi">%s 个赞</string>
+ <string name="reader_short_like_count_one">1 个赞</string>
+ <string name="reader_label_follow_count">%,d 个粉丝</string>
+ <string name="reader_short_like_count_none">顶</string>
+ <string name="reader_menu_tags">编辑标签和博客</string>
+ <string name="reader_title_post_detail">读者文章</string>
+ <string name="local_draft_explainer">此文章为尚未发布的本地草稿</string>
+ <string name="local_changes_explainer">此文章存在尚未发布的本地更改</string>
+ <string name="notifications_push_summary">显示在设备上的通知的设置。</string>
+ <string name="notifications_email_summary">发送到与您的帐户相关联的电子邮件中的通知的设置。</string>
+ <string name="notifications_tab_summary">显示在“通知”选项卡中的通知的设置。</string>
+ <string name="notifications_disabled">应用程序通知已禁用。轻点此处以在“设置”中将其启用。</string>
+ <string name="notification_types">通知类型</string>
+ <string name="error_loading_notifications">无法加载通知设置</string>
+ <string name="replies_to_your_comments">评论回复</string>
+ <string name="comment_likes">评论赞数</string>
+ <string name="app_notifications">应用程序通知</string>
+ <string name="notifications_tab">“通知”选项卡</string>
+ <string name="email">电子邮件</string>
+ <string name="notifications_comments_other_blogs">其他站点上的评论</string>
+ <string name="notifications_wpcom_updates">WordPress.com 更新</string>
+ <string name="notifications_other">其他</string>
+ <string name="notifications_account_emails">来自 WordPress.com 的电子邮件</string>
+ <string name="notifications_account_emails_summary">我们会将与您的帐户相关的重要电子邮件都发送给您,但是您还可以获得一些其他的有用信息。</string>
+ <string name="notifications_sights_and_sounds">场景和声音</string>
+ <string name="your_sites">您的站点</string>
+ <string name="stats_insights_latest_post_trend">距 %2$s 发布已有 %1$s。以下是该文章到目前为止的效果…</string>
+ <string name="stats_insights_latest_post_summary">最新的文章摘要</string>
+ <string name="button_revert">恢复</string>
+ <string name="days_ago">%d 天前</string>
+ <string name="yesterday">昨天</string>
+ <string name="connectionbar_no_connection">无连接</string>
+ <string name="page_trashed">页面已放入回收站</string>
+ <string name="post_deleted">已删除文章</string>
+ <string name="post_trashed">文章已放入回收站</string>
+ <string name="stats_no_activity_this_period">这段时间内没有任何活动</string>
+ <string name="trashed">已放入回收站</string>
+ <string name="button_back">返回</string>
+ <string name="page_deleted">已删除页面</string>
+ <string name="button_stats">统计信息</string>
+ <string name="button_trash">放入回收站</string>
+ <string name="button_preview">预览</string>
+ <string name="button_view">查看</string>
+ <string name="button_edit">编辑</string>
+ <string name="button_publish">发布</string>
+ <string name="my_site_no_sites_view_subtitle">想要添加一个吗?</string>
+ <string name="my_site_no_sites_view_title">您目前没有任何 WordPress 站点。</string>
+ <string name="my_site_no_sites_view_drake">插图</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">您无权访问此博客。</string>
+ <string name="reader_toast_err_follow_blog_not_found">找不到此博客</string>
+ <string name="undo">撤消</string>
+ <string name="tabbar_accessibility_label_my_site">我的站点</string>
+ <string name="tabbar_accessibility_label_me">我</string>
+ <string name="passcodelock_prompt_message">输入 PIN</string>
+ <string name="editor_toast_changes_saved">更改已保存</string>
+ <string name="push_auth_expired">请求已过期。请登录到 WordPress.com 并重试。</string>
+ <string name="stats_insights_best_ever">最受欢迎的文章</string>
+ <string name="ignore">忽略</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% 的阅读次数</string>
+ <string name="stats_insights_most_popular_hour">人气最高的时刻</string>
+ <string name="stats_insights_most_popular_day">人气最高的日期</string>
+ <string name="stats_insights_popular">人气最高的日期和时刻</string>
+ <string name="stats_insights_today">今日统计</string>
+ <string name="stats_insights_all_time">全时段的文章篇数、阅读次数和访客人数</string>
+ <string name="stats_insights">见解</string>
+ <string name="stats_sign_in_jetpack_different_com_account">要查看您的统计数据,请登录关联 Jetpack 时使用的 WordPress.com 帐户。</string>
+ <string name="stats_other_recent_stats_moved_label">要查找您的“其他最近统计数据”?我们已将其迁移至“见解”页面。</string>
+ <string name="me_disconnect_from_wordpress_com">从 WordPress.com 断开连接</string>
+ <string name="me_connect_to_wordpress_com">连接至 WordPress.com</string>
+ <string name="me_btn_login_logout">登录/注销</string>
+ <string name="account_settings">帐户设置</string>
+ <string name="me_btn_support">帮助与支持</string>
+ <string name="site_picker_cant_hide_current_site">“%s”未隐藏,因为它是当前站点</string>
+ <string name="site_picker_create_dotcom">创建 WordPress.com 站点</string>
+ <string name="site_picker_add_site">添加站点</string>
+ <string name="site_picker_add_self_hosted">添加自托管站点</string>
+ <string name="site_picker_edit_visibility">显示/隐藏站点</string>
+ <string name="my_site_btn_view_admin">查看管理员</string>
+ <string name="my_site_btn_view_site">查看站点</string>
+ <string name="site_picker_title">选择站点</string>
+ <string name="my_site_btn_switch_site">转换站点</string>
+ <string name="my_site_btn_blog_posts">博客文章</string>
+ <string name="my_site_btn_site_settings">设置</string>
+ <string name="my_site_header_look_and_feel">外观</string>
+ <string name="my_site_header_publish">发布</string>
+ <string name="my_site_header_configuration">配置</string>
+ <string name="reader_label_new_posts_subtitle">轻点以显示文章</string>
+ <string name="notifications_account_required">登录 WordPress.com,以查看通知</string>
+ <string name="stats_unknown_author">未知作者</string>
+ <string name="image_added">图片已添加</string>
+ <string name="signout">断开连接</string>
+ <string name="deselect_all">全部不选</string>
+ <string name="show">显示</string>
+ <string name="hide">隐藏</string>
+ <string name="select_all">选择全部</string>
+ <string name="sign_out_wpcom_confirm">断开帐户连接将删除此设备上所有的 @%s WordPress.com 数据,其中包括本地草稿和本地更改。</string>
+ <string name="select_from_new_picker">使用全新选择器进行多选</string>
+ <string name="stats_generic_error">无法加载所需统计</string>
+ <string name="no_device_videos">无任何视频</string>
+ <string name="no_blog_images">无任何图片</string>
+ <string name="no_blog_videos">无任何视频</string>
+ <string name="no_device_images">无任何图片</string>
+ <string name="error_loading_blog_images">无法获取图片</string>
+ <string name="error_loading_blog_videos">无法获取视频</string>
+ <string name="error_loading_images">加载图片时出错</string>
+ <string name="error_loading_videos">加载视频时出错</string>
+ <string name="loading_blog_images">正在获取图片</string>
+ <string name="loading_blog_videos">正在获取视频</string>
+ <string name="no_media_sources">无法获取媒体</string>
+ <string name="loading_videos">正在加载视频</string>
+ <string name="loading_images">正在加载图片</string>
+ <string name="no_media">无任何媒体</string>
+ <string name="device">设备</string>
+ <string name="language">语言</string>
+ <string name="add_to_post">添加到文章</string>
+ <string name="media_picker_title">选择媒体</string>
+ <string name="take_photo">拍照</string>
+ <string name="take_video">拍摄视频</string>
+ <string name="tab_title_device_images">设备图片</string>
+ <string name="tab_title_device_videos">设备视频</string>
+ <string name="tab_title_site_images">站点图片</string>
+ <string name="tab_title_site_videos">站点视频</string>
+ <string name="media_details_label_file_name">文件名</string>
+ <string name="media_details_label_file_type">文件类型</string>
+ <string name="error_publish_no_network">未连接网络时无法发布。保存为草稿。</string>
+ <string name="editor_toast_invalid_path">文件路径无效</string>
+ <string name="verification_code">验证码</string>
+ <string name="invalid_verification_code">验证码无效</string>
+ <string name="verify">验证</string>
+ <string name="two_step_footer_label">输入您验证器应用程中的代码。</string>
+ <string name="two_step_footer_button">通过短信发送代码</string>
+ <string name="two_step_sms_sent">查收短信查看验证码。</string>
+ <string name="sign_in_jetpack">登录 WordPress.com 帐户来连接 Jetpack。</string>
+ <string name="auth_required">再次登录以继续操作。</string>
+ <string name="reader_empty_posts_request_failed">无法检索文章</string>
+ <string name="publisher">发布者:</string>
+ <string name="error_notification_open">无法打开通知</string>
+ <string name="stats_followers_total_email_paged">正在显示 %1$d - %2$d 位(共 %3$s 位)电子邮件关注者</string>
+ <string name="stats_search_terms_unknown_search_terms">未知搜索字词</string>
+ <string name="stats_followers_total_wpcom_paged">正在显示 %1$d - %2$d 位(共 %3$s 位)WordPress.com 关注者</string>
+ <string name="stats_empty_search_terms_desc">要了解更多有关搜索流量的信息,请查看您的访客用于搜索您站点的字词。</string>
+ <string name="stats_empty_search_terms">未记录任何搜索字词</string>
+ <string name="stats_entry_search_terms">搜索字词</string>
+ <string name="stats_view_authors">作者</string>
+ <string name="stats_view_search_terms">搜索字词</string>
+ <string name="comments_fetching">正在提取评论…</string>
+ <string name="pages_fetching">正在获取页面…</string>
+ <string name="toast_err_post_uploading">无法在上传文章的同时打开文章</string>
+ <string name="posts_fetching">正在获取文章…</string>
+ <string name="media_fetching">正在获取媒体…</string>
+ <string name="post_uploading">正在上传</string>
+ <string name="stats_total">总计</string>
+ <string name="stats_overall">总体</string>
+ <string name="stats_period">时段</string>
+ <string name="logs_copied_to_clipboard">已将应用程序日志复制到剪贴板</string>
+ <string name="reader_label_new_posts">新文章</string>
+ <string name="reader_empty_posts_in_blog">此博客没有任何内容</string>
+ <string name="stats_average_per_day">每日平均</string>
+ <string name="stats_recent_weeks">最近几个星期</string>
+ <string name="error_copy_to_clipboard">复制文本到剪贴板时出错</string>
+ <string name="reader_page_recommended_blogs">您可能会喜欢的站点</string>
+ <string name="stats_months_and_years">月和年</string>
+ <string name="themes_fetching">正在获取主题…</string>
+ <string name="stats_for">“%s”的统计</string>
+ <string name="stats_other_recent_stats_label">其他最新统计资料</string>
+ <string name="stats_view_all">查看全部</string>
+ <string name="stats_view">查看</string>
+ <string name="stats_followers_months">%1$d 个月</string>
+ <string name="stats_followers_a_year">一年</string>
+ <string name="stats_followers_years">%1$d 年</string>
+ <string name="stats_followers_a_month">一个月</string>
+ <string name="stats_followers_minutes">%1$d 分钟</string>
+ <string name="stats_followers_an_hour_ago">一小时以前</string>
+ <string name="stats_followers_hours">%1$d 小时</string>
+ <string name="stats_followers_a_day">一天</string>
+ <string name="stats_followers_days">%1$d 天</string>
+ <string name="stats_followers_a_minute_ago">一分钟以前</string>
+ <string name="stats_followers_seconds_ago">秒以前</string>
+ <string name="stats_followers_total_email">电子邮件粉丝总数:%1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">电子邮件</string>
+ <string name="stats_followers_total_wpcom">WordPress.com 粉丝总数:%1$s</string>
+ <string name="stats_comments_total_comments_followers">拥有评论粉丝的文章总数:%1$s</string>
+ <string name="stats_comments_by_authors">按作者</string>
+ <string name="stats_comments_by_posts_and_pages">按文章和页面</string>
+ <string name="stats_empty_followers_desc">随时了解粉丝总数,以及每个粉丝关注您的网站的时长。</string>
+ <string name="stats_empty_followers">没有粉丝</string>
+ <string name="stats_empty_publicize_desc">借助 Publicize,随时了解各个社交网络服务的粉丝动态。</string>
+ <string name="stats_empty_publicize">未记录任何 Publicize 粉丝</string>
+ <string name="stats_empty_video">未播放任何视频</string>
+ <string name="stats_empty_video_desc">请在使用 VideoPress 上传完视频后,查看其观看次数。</string>
+ <string name="stats_empty_comments_desc">如果您的站点允许评论,您可以根据最新的 1000 条评论,随时了解最活跃的评论者,并找出引发最活跃对话的内容。</string>
+ <string name="stats_empty_tags_and_categories_desc">概述您的站点上最热门的主题(如自上周起您的热门文章所示)。</string>
+ <string name="stats_empty_top_authors_desc">跟踪各贡献者文章的查看次数,并进一步探索在各个作者创建的内容中哪些内容人气最高。</string>
+ <string name="stats_empty_tags_and_categories">未查看任何带标签的文章或页面</string>
+ <string name="stats_empty_clicks_desc">当您的内容中包含指向其他网站的链接时,您将会看到访客最多的链接。</string>
+ <string name="stats_empty_referrers_desc">查看为您发送最多流量的网站和搜索引擎,了解更多关于您的站点可见性的信息</string>
+ <string name="stats_empty_clicks_title">没有点击记录</string>
+ <string name="stats_empty_referrers_title">未记录任何推荐来源</string>
+ <string name="stats_empty_top_posts_title">未查看任何文章或页面</string>
+ <string name="stats_empty_top_posts_desc">发现您最常查看的内容,然后查看随着时间的推移个人发布的文章和页面的情况如何。</string>
+ <string name="stats_totals_followers">自从</string>
+ <string name="stats_empty_geoviews">未记录任何国家/地区</string>
+ <string name="stats_empty_geoviews_desc">查看列表,看看您的网站上哪个国家和地区产生的流量最多。</string>
+ <string name="stats_entry_video_plays">视频</string>
+ <string name="stats_entry_top_commenter">作者</string>
+ <string name="stats_entry_publicize">服务</string>
+ <string name="stats_entry_followers">粉丝</string>
+ <string name="stats_totals_publicize">关注者</string>
+ <string name="stats_entry_clicks_link">链接</string>
+ <string name="stats_view_top_posts_and_pages">文章和页面</string>
+ <string name="stats_view_videos">视频</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">关注者</string>
+ <string name="stats_view_countries">国家/地区</string>
+ <string name="stats_likes">赞</string>
+ <string name="stats_pagination_label">第 %1$s 页,共 %2$s 页</string>
+ <string name="stats_timeframe_years">年</string>
+ <string name="stats_views">浏览量</string>
+ <string name="stats_visitors">访客</string>
+ <string name="ssl_certificate_details">详细信息</string>
+ <string name="delete_sure_post">删除此文章</string>
+ <string name="delete_sure">删除此草稿</string>
+ <string name="delete_sure_page">删除此页面</string>
+ <string name="confirm_delete_multi_media">是否删除所选项目?</string>
+ <string name="confirm_delete_media">是否删除所选项目?</string>
+ <string name="cab_selected">已选 %d 个</string>
+ <string name="media_gallery_date_range">显示从 %1$s 到 %2$s 的媒体</string>
+ <string name="sure_to_remove_account">要删除此站点吗?</string>
+ <string name="reader_empty_followed_blogs_title">您尚未关注任何站点</string>
+ <string name="reader_empty_posts_liked">您尚未给任何文章点赞</string>
+ <string name="faq_button">常见问题解答</string>
+ <string name="browse_our_faq_button">浏览我们的常见问题解答</string>
+ <string name="nux_help_description">访问帮助中心,以获取常见问题的答案;或访问论坛,以提出新问题。</string>
+ <string name="agree_terms_of_service">创建帐户,即表示您同意清晰易懂的%1$s服务条款%2$s</string>
+ <string name="create_new_blog_wpcom">创建 WordPress.com 博客</string>
+ <string name="new_blog_wpcom_created">WordPress.com 博客已创建!</string>
+ <string name="reader_empty_comments">尚无评论</string>
+ <string name="reader_empty_posts_in_tag">没有包含此标签的文章</string>
+ <string name="reader_label_comment_count_multi">%,d 条评论</string>
+ <string name="reader_label_view_original">查看原文</string>
+ <string name="reader_label_like">赞</string>
+ <string name="reader_label_comment_count_single">1 条评论</string>
+ <string name="reader_label_comments_closed">评论功能被关闭</string>
+ <string name="reader_label_comments_on">评论于</string>
+ <string name="reader_title_photo_viewer">%1$d/%2$d</string>
+ <string name="error_publish_empty_post">发布的文章不能为空</string>
+ <string name="error_refresh_unauthorized_posts">您不具备查看或编辑文章的权限</string>
+ <string name="error_refresh_unauthorized_pages">您不具备查看或编辑页面的权限</string>
+ <string name="error_refresh_unauthorized_comments">您不具备查看或编辑评论的权限</string>
+ <string name="older_month">超过一个月</string>
+ <string name="more">更多</string>
+ <string name="older_two_days">超过 2 天</string>
+ <string name="older_last_week">超过 1 周</string>
+ <string name="stats_no_blog">无法加载所需博客的统计信息</string>
+ <string name="select_a_blog">选择一个 WordPress 站点</string>
+ <string name="sending_content">正在上传“%s”内容</string>
+ <string name="uploading_total">正在上传 %1$d/%2$d</string>
+ <string name="mnu_comment_liked">赞</string>
+ <string name="comment">评论</string>
+ <string name="comment_trashed">评论已移到回收站</string>
+ <string name="posts_empty_list">尚无文章。为何不创建一个?</string>
+ <string name="comment_reply_to_user">回复 %s</string>
+ <string name="pages_empty_list">还没有页面。为何不创建一个?</string>
+ <string name="media_empty_list_custom_date">此事件段内没有媒体</string>
+ <string name="posting_post">正在发布“%s”</string>
+ <string name="signing_out">正在注销…</string>
+ <string name="reader_toast_err_generic">无法执行此操作</string>
+ <string name="reader_toast_err_block_blog">无法阻止此博客</string>
+ <string name="reader_toast_blog_blocked">此博客中的文章不会再显示</string>
+ <string name="reader_menu_block_blog">阻止此博客</string>
+ <string name="contact_us">联系我们</string>
+ <string name="hs__conversation_detail_error">描述您所遇到的问题</string>
+ <string name="hs__new_conversation_header">支持聊天</string>
+ <string name="hs__conversation_header">支持聊天</string>
+ <string name="hs__username_blank_error">请输入有效名称</string>
+ <string name="hs__invalid_email_error">请输入有效电子邮件地址</string>
+ <string name="add_location">添加位置</string>
+ <string name="current_location">当前位置</string>
+ <string name="search_location">搜索</string>
+ <string name="edit_location">编辑</string>
+ <string name="search_current_location">定位</string>
+ <string name="preference_send_usage_stats">发送统计数据</string>
+ <string name="preference_send_usage_stats_summary">自动发送使用统计数据以帮助我们改进 Android 版 WordPress</string>
+ <string name="update_verb">更新</string>
+ <string name="schedule_verb">计划</string>
+ <string name="reader_title_blog_preview">读者博客</string>
+ <string name="reader_title_tag_preview">读者标签</string>
+ <string name="reader_title_subs">标签和博文</string>
+ <string name="reader_page_followed_tags">关注的标签</string>
+ <string name="reader_page_followed_blogs">关注的站点</string>
+ <string name="reader_hint_add_tag_or_url">输入要关注的 URL 或标签</string>
+ <string name="reader_label_followed_blog">关注的博客</string>
+ <string name="reader_label_tag_preview">标记为“%s”的博文</string>
+ <string name="reader_toast_err_get_blog_info">无法显示此博客</string>
+ <string name="reader_toast_err_already_follow_blog">您已经关注此博客</string>
+ <string name="reader_toast_err_follow_blog">无法关注此博客</string>
+ <string name="reader_toast_err_unfollow_blog">无法取消关注此博客</string>
+ <string name="reader_empty_recommended_blogs">没有推荐的博客</string>
+ <string name="saving">正在保存...</string>
+ <string name="media_empty_list">没有媒体</string>
+ <string name="ptr_tip_message">注意:下拉以刷新</string>
+ <string name="help">帮助</string>
+ <string name="forgot_password">忘记密码?</string>
+ <string name="forums">论坛</string>
+ <string name="help_center">帮助中心</string>
+ <string name="ssl_certificate_error">无效的SSL证书</string>
+ <string name="ssl_certificate_ask_trust">如果你平时连接到这个网站没有任何问题,这个错误可能意味着有人试图冒充该网站,你不应该继续下去。你想仍然信任这个证书?</string>
+ <string name="out_of_memory">设备内存已满</string>
+ <string name="no_network_message">没用可用的网络</string>
+ <string name="could_not_remove_account">无法删除站点</string>
+ <string name="gallery_error">抱歉,无法从图库检索该媒体对象</string>
+ <string name="blog_not_found">抱歉,尝试访问该博客时发生错误。</string>
+ <string name="wait_until_upload_completes">等待上传完成</string>
+ <string name="theme_fetch_failed">未能获取主题</string>
+ <string name="theme_set_failed">未能设置主题</string>
+ <string name="theme_auth_error_message">确保您有权限设置主题</string>
+ <string name="comments_empty_list">没有评论</string>
+ <string name="mnu_comment_unspam">不是垃圾评论</string>
+ <string name="no_site_error">无法连接到 WordPress 网站</string>
+ <string name="adding_cat_failed">添加分类失败</string>
+ <string name="adding_cat_success">分类添加成功</string>
+ <string name="cat_name_required">“分类名称”必填</string>
+ <string name="category_automatically_renamed">"分类名称 %1$s 不可用. 它已经被重命名为 %2$s."</string>
+ <string name="no_account">未找到 WordPress 账号,请添加一个账号后再试</string>
+ <string name="sdcard_message">上传媒体需要已装载的 SD 卡</string>
+ <string name="stats_empty_comments">现在没有评论</string>
+ <string name="stats_bar_graph_empty">还没有评论</string>
+ <string name="invalid_url_message">请检查输入的 URL 是否有效</string>
+ <string name="reply_failed">回复失败</string>
+ <string name="notifications_empty_list">没有通知</string>
+ <string name="error_delete_post">在删除 %s 时发生了一个错误</string>
+ <string name="error_refresh_posts">现在不能刷新文章</string>
+ <string name="error_refresh_pages">现在不能刷新页面</string>
+ <string name="error_refresh_notifications">现在不能刷新通知</string>
+ <string name="error_refresh_comments">现在不能刷新评论</string>
+ <string name="error_refresh_stats">现在不能刷新统计</string>
+ <string name="error_generic">发生错误</string>
+ <string name="error_moderate_comment">调节时出现了一个错误</string>
+ <string name="error_edit_comment">在编辑评论时出现了一个错误</string>
+ <string name="error_upload">在上传 %s 时发生错误</string>
+ <string name="error_load_comment">无法加载评论</string>
+ <string name="error_downloading_image">下载图像错误</string>
+ <string name="passcode_wrong_passcode">PIN错误</string>
+ <string name="invalid_email_message">你的邮件地址不可用</string>
+ <string name="invalid_password_message">密码至少4个字符</string>
+ <string name="invalid_username_too_short">用户名必须大于4个字符</string>
+ <string name="invalid_username_too_long">用户名必须小于61个字符</string>
+ <string name="username_only_lowercase_letters_and_numbers">用户名只能包含小写字母(a-z) 和数字</string>
+ <string name="username_required">输入用户名</string>
+ <string name="username_not_allowed">用户名不允许</string>
+ <string name="username_must_be_at_least_four_characters">用户名至少包括4个字符</string>
+ <string name="username_contains_invalid_characters">用户名不能包含字符“_”</string>
+ <string name="username_must_include_letters">用户名需要至少包含一个字母(a-z)</string>
+ <string name="email_invalid">请输入一个可用的电子邮件地址</string>
+ <string name="email_not_allowed">该电子邮件地址是不允许的</string>
+ <string name="username_exists">该用户名已存在</string>
+ <string name="email_exists">该邮件地址已被使用</string>
+ <string name="username_reserved_but_may_be_available">这个用户名现在不可用</string>
+ <string name="blog_name_required">输入一个网址</string>
+ <string name="blog_name_not_allowed">该网址是不允许的</string>
+ <string name="blog_name_must_be_at_least_four_characters">网址最少4个字符</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">网址最多64子字符</string>
+ <string name="blog_name_contains_invalid_characters">网址不能包含字符“_”</string>
+ <string name="blog_name_cant_be_used">你不能使用这个网址</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">网址只能包含小写字母(a-z)和数字</string>
+ <string name="blog_name_exists">这个网站已经存在</string>
+ <string name="blog_name_reserved">这个网址是保留的</string>
+ <string name="blog_name_reserved_but_may_be_available">这个网址目前是保留的但可能在几天后可用</string>
+ <string name="username_or_password_incorrect">你输入的用户名或密码错误</string>
+ <string name="nux_cannot_log_in">无法登陆</string>
+ <string name="xmlrpc_error">无法连接。请输入您网站上 xmlrpc.php 的完整路径再试。</string>
+ <string name="select_categories">选择分类</string>
+ <string name="account_details">帐户详细资料</string>
+ <string name="edit_post">编辑文章</string>
+ <string name="add_comment">添加评论</string>
+ <string name="connection_error">连接错误</string>
+ <string name="cancel_edit">取消编辑</string>
+ <string name="scaled_image_error">请输入有效的缩放宽度</string>
+ <string name="post_not_found">抱歉,尝试加载文章时出现错误。请刷新文章列表再试.</string>
+ <string name="learn_more">了解更多</string>
+ <string name="media_gallery_settings_title">相册设置</string>
+ <string name="media_gallery_image_order">图像顺序</string>
+ <string name="media_gallery_num_columns">列数</string>
+ <string name="media_gallery_type_thumbnail_grid">缩略图</string>
+ <string name="media_gallery_edit">编辑相册</string>
+ <string name="media_error_no_permission">你没有权限查看媒体库</string>
+ <string name="cannot_delete_multi_media_items">一些媒体文件现在不能删除.请稍后再试.</string>
+ <string name="themes_live_preview">实时预览</string>
+ <string name="theme_current_theme">当前主题</string>
+ <string name="theme_premium_theme">高级主题</string>
+ <string name="link_enter_url_text">链接文字 (可选)</string>
+ <string name="create_a_link">创建一个链接</string>
+ <string name="page_settings">页面设置</string>
+ <string name="local_draft">本地草稿</string>
+ <string name="upload_failed">上传失败</string>
+ <string name="horizontal_alignment">水平对齐</string>
+ <string name="file_not_found">无法找到要上传的媒体文件。是不是已经删除或移动到其他地方了?</string>
+ <string name="post_settings">博文设置</string>
+ <string name="delete_post">删除文章</string>
+ <string name="delete_page">删除页面</string>
+ <string name="comment_status_approved">通过</string>
+ <string name="comment_status_unapproved">待审</string>
+ <string name="comment_status_spam">垃圾</string>
+ <string name="comment_status_trash">删除</string>
+ <string name="edit_comment">编辑评论</string>
+ <string name="mnu_comment_approve">通过</string>
+ <string name="mnu_comment_unapprove">不通过</string>
+ <string name="mnu_comment_spam">垃圾</string>
+ <string name="mnu_comment_trash">删除</string>
+ <string name="dlg_approving_comments">批准</string>
+ <string name="dlg_unapproving_comments">不批准</string>
+ <string name="dlg_spamming_comments">正在标记为垃圾评论</string>
+ <string name="dlg_trashing_comments">正在删除</string>
+ <string name="dlg_confirm_trash_comments">确定删除?</string>
+ <string name="trash_yes">删除</string>
+ <string name="trash_no">不要删除</string>
+ <string name="trash">删除</string>
+ <string name="author_name">作者姓名</string>
+ <string name="author_email">作者邮箱</string>
+ <string name="author_url">作者网址</string>
+ <string name="hint_comment_content">评论</string>
+ <string name="saving_changes">正在保存更改</string>
+ <string name="sure_to_cancel_edit_comment">是否确定要取消编辑该评论?</string>
+ <string name="content_required">评论正文为必填项。</string>
+ <string name="toast_comment_unedited">评论没有改变</string>
+ <string name="remove_account">删除站点</string>
+ <string name="blog_removed_successfully">已成功删除站点</string>
+ <string name="delete_draft">删除草稿</string>
+ <string name="preview_page">预览页面</string>
+ <string name="preview_post">预览文章</string>
+ <string name="comment_added">评论添加成功</string>
+ <string name="post_not_published">文章尚未发布</string>
+ <string name="page_not_published">页面尚未发布</string>
+ <string name="view_in_browser">在浏览器中查看</string>
+ <string name="add_new_category">添加新分类</string>
+ <string name="category_name">分类名称</string>
+ <string name="category_slug">分类代号 (可选)</string>
+ <string name="category_desc">分类描述 (可选)</string>
+ <string name="category_parent">上级分类 (可选)</string>
+ <string name="share_action_post">新文章</string>
+ <string name="share_action_media">媒体库</string>
+ <string name="file_error_create">无法创建上传媒体所需的临时文件。请确保设备有足够的空闲空间.</string>
+ <string name="location_not_found">未知位置</string>
+ <string name="open_source_licenses">开源许可协议</string>
+ <string name="invalid_site_url_message">请检查输入的站点 URL 是否有效</string>
+ <string name="pending_review">尚待审阅</string>
+ <string name="http_credentials">HTTP 登录凭据 (可选)</string>
+ <string name="http_authorization_required">需要验证</string>
+ <string name="post_format">文章形式</string>
+ <string name="notifications_empty_all">尚无通知…。</string>
+ <string name="new_post">新文章</string>
+ <string name="new_media">新媒体</string>
+ <string name="view_site">查看站点</string>
+ <string name="privacy_policy">隐私政策</string>
+ <string name="local_changes">本地修改</string>
+ <string name="image_settings">图像设置</string>
+ <string name="add_account_blog_url">博客地址</string>
+ <string name="wordpress_blog">WordPress 博客</string>
+ <string name="error_blog_hidden">这个博客是隐藏的,无法加载。在设置中重新启用它并再次尝试。</string>
+ <string name="fatal_db_error">创建程序数据库时出错,请重新安装程序.</string>
+ <string name="jetpack_message_not_admin">统计需要安装Jetpack插件,请联系网站管理员.</string>
+ <string name="reader_title_applog">程序日志</string>
+ <string name="reader_share_link">分享链接</string>
+ <string name="reader_toast_err_add_tag">不能增加这个标签</string>
+ <string name="reader_toast_err_remove_tag">不能删除这个标签</string>
+ <string name="required_field">必填字段</string>
+ <string name="email_hint">邮件地址</string>
+ <string name="site_address">你的博客地址</string>
+ <string name="email_cant_be_used_to_signup">你不能使用该电子邮件地址来注册。它们会阻止我们的电子邮件。请使用其他电子邮件提供商。</string>
+ <string name="email_reserved">该电子邮件地址已经被使用。检查你的收件箱里的激活邮件。如果你没有激活可以在几天后再次尝试。</string>
+ <string name="blog_name_must_include_letters">网址必须至少有1个字母(a-z)</string>
+ <string name="blog_name_invalid">无效的网址</string>
+ <string name="blog_title_invalid">无效的网站标题</string>
+ <string name="deleting_page">正在删除页面</string>
+ <string name="deleting_post">正在删除日志</string>
+ <string name="share_url_post">分享日志</string>
+ <string name="share_url_page">分享页面</string>
+ <string name="share_link">分享链接</string>
+ <string name="creating_your_account">正在创建您的账户</string>
+ <string name="creating_your_site">正在创建您的网站</string>
+ <string name="reader_empty_posts_in_tag_updating">正在获取文章…</string>
+ <string name="error_refresh_media">刷新媒体库时出错,请稍后再试.</string>
+ <string name="reader_likes_you_and_multi">你和 %,d 人喜欢</string>
+ <string name="reader_likes_multi">%,d 人喜欢</string>
+ <string name="reader_toast_err_get_comment">无法检索此评论</string>
+ <string name="reader_label_reply">回复</string>
+ <string name="video">视频</string>
+ <string name="download">下载媒体文件</string>
+ <string name="comment_spammed">评论被标记为垃圾评论</string>
+ <string name="cant_share_no_visible_blog">没有一个博客,你不能分享到WordPress</string>
+ <string name="select_time">选择时间</string>
+ <string name="reader_likes_you_and_one">你和另一个人喜欢</string>
+ <string name="reader_empty_followed_blogs_description">别担心,只需轻点右上角的图标即可开始浏览!</string>
+ <string name="select_date">选择日期</string>
+ <string name="pick_photo">选择图片</string>
+ <string name="account_two_step_auth_enabled">此帐户已启用两步认证。访问您WordPress.com上的安全设置生成一个特定的密码。</string>
+ <string name="pick_video">选择视频</string>
+ <string name="reader_toast_err_get_post">无法检索这篇文章</string>
+ <string name="validating_user_data">验证用户数据</string>
+ <string name="validating_site_data">验证站点数据</string>
+ <string name="password_invalid">你需要一个更安全的密码。确定使用7个或更多字符,混合大、小写字母、数字或特殊字符.</string>
+ <string name="nux_tap_continue">继续</string>
+ <string name="nux_welcome_create_account">创建账户</string>
+ <string name="signing_in">正在登录…</string>
+ <string name="nux_add_selfhosted_blog">添加一个自己的站点</string>
+ <string name="nux_oops_not_selfhosted_blog">登录到 WordPress.com</string>
+ <string name="media_add_popup_title">添加到媒体库</string>
+ <string name="media_add_new_media_gallery">创建相册</string>
+ <string name="empty_list_default">这个列表是空的</string>
+ <string name="select_from_media_library">从媒体库选择</string>
+ <string name="jetpack_message">统计需要Jetpack插件,您希望安装Jetpack吗?</string>
+ <string name="jetpack_not_found">没有找到Jetpack插件</string>
+ <string name="reader_untitled_post">(未命名)</string>
+ <string name="reader_share_subject">从 %s 共享</string>
+ <string name="reader_btn_share">共享</string>
+ <string name="reader_btn_follow">订阅</string>
+ <string name="reader_btn_unfollow">关注中</string>
+ <string name="reader_hint_comment_on_comment">回复评论</string>
+ <string name="reader_label_added_tag">已添加 %s</string>
+ <string name="reader_label_removed_tag">已移除 %s</string>
+ <string name="reader_likes_one">1 人喜欢</string>
+ <string name="reader_likes_only_you">已喜欢</string>
+ <string name="reader_toast_err_comment_failed">无法发表评论</string>
+ <string name="reader_toast_err_tag_exists">你已经订阅了这个标签</string>
+ <string name="reader_toast_err_tag_invalid">这不是一个合法的标签</string>
+ <string name="reader_toast_err_share_intent">无法共享</string>
+ <string name="reader_toast_err_view_image">无法查看图像</string>
+ <string name="reader_toast_err_url_intent">无法打开 %s</string>
+ <string name="reader_empty_followed_tags">您没有关注任何标签</string>
+ <string name="create_account_wpcom">在 WordPress.com 上创建帐户</string>
+ <string name="button_next">下一步</string>
+ <string name="connecting_wpcom">连接到 WordPress.com</string>
+ <string name="username_invalid">无效的用户名</string>
+ <string name="limit_reached">达到限制.你可以在1分钟后尝试.在这之前尝试只会增加你所需等待的时间 . 如果你觉得这是个错误,联系支持.</string>
+ <string name="nux_tutorial_get_started_title">开始!</string>
+ <string name="themes">主题</string>
+ <string name="all">所有</string>
+ <string name="images">图像</string>
+ <string name="unattached">独立的</string>
+ <string name="custom_date">自定义日期</string>
+ <string name="media_add_popup_capture_photo">捕获照片</string>
+ <string name="media_add_popup_capture_video">捕获视频</string>
+ <string name="media_gallery_image_order_random">随机</string>
+ <string name="media_gallery_image_order_reverse">反向</string>
+ <string name="media_gallery_type">类型</string>
+ <string name="media_gallery_type_squares">方格</string>
+ <string name="media_gallery_type_tiled">磁贴</string>
+ <string name="media_gallery_type_circles">环状</string>
+ <string name="media_gallery_type_slideshow">幻灯片</string>
+ <string name="media_edit_title_text">标题</string>
+ <string name="media_edit_caption_text">标题</string>
+ <string name="media_edit_description_text">描述</string>
+ <string name="media_edit_title_hint">在这里输入标题</string>
+ <string name="media_edit_caption_hint">在这里输入标题</string>
+ <string name="media_edit_description_hint">在这里输入描述</string>
+ <string name="media_edit_success">已更新</string>
+ <string name="media_edit_failure">更新失败</string>
+ <string name="themes_details_label">详细</string>
+ <string name="themes_features_label">特征</string>
+ <string name="theme_activate_button">激活</string>
+ <string name="theme_activating_button">正在激活</string>
+ <string name="theme_set_success">主题设置成功!</string>
+ <string name="theme_auth_error_title">未能获取主题</string>
+ <string name="post_excerpt">摘要</string>
+ <string name="share_action_title">添加到...</string>
+ <string name="share_action">分享</string>
+ <string name="stats">统计</string>
+ <string name="stats_view_visitors_and_views">访问量和浏览量</string>
+ <string name="stats_view_clicks">点击</string>
+ <string name="stats_view_tags_and_categories">标签和类别</string>
+ <string name="stats_view_referrers">推荐人</string>
+ <string name="stats_timeframe_today">今天</string>
+ <string name="stats_timeframe_yesterday">昨天</string>
+ <string name="stats_timeframe_days">天</string>
+ <string name="stats_timeframe_weeks">周</string>
+ <string name="stats_timeframe_months">月</string>
+ <string name="stats_entry_country">国家</string>
+ <string name="stats_entry_posts_and_pages">标题</string>
+ <string name="stats_entry_tags_and_categories">话题</string>
+ <string name="stats_entry_authors">作者</string>
+ <string name="stats_entry_referrers">推荐人</string>
+ <string name="stats_totals_views">浏览次数</string>
+ <string name="stats_totals_clicks">点击数</string>
+ <string name="stats_totals_plays">播放数</string>
+ <string name="passcode_manage">管理PIN锁</string>
+ <string name="passcode_enter_passcode">输入新PIN</string>
+ <string name="passcode_enter_old_passcode">输入旧的PIN</string>
+ <string name="passcode_re_enter_passcode">再次输入你的PIN</string>
+ <string name="passcode_change_passcode">更改PIN</string>
+ <string name="passcode_set">PIN设置</string>
+ <string name="passcode_preference_title">PIN锁</string>
+ <string name="passcode_turn_off">关闭PIN锁</string>
+ <string name="passcode_turn_on">打开PIN锁</string>
+ <string name="upload">上传</string>
+ <string name="discard">舍弃</string>
+ <string name="sign_in">登录</string>
+ <string name="notifications">通知</string>
+ <string name="note_reply_successful">回复已发表</string>
+ <string name="follows">关注</string>
+ <string name="new_notifications">%d 条新通知</string>
+ <string name="more_notifications">以及其他 %d 人。</string>
+ <string name="loading">正在加载…</string>
+ <string name="httpuser">HTTP 用户名</string>
+ <string name="httppassword">HTTP 密码</string>
+ <string name="error_media_upload">上传媒体时出错</string>
+ <string name="post_content">内容 (轻按可添加文字与媒体)</string>
+ <string name="publish_date">发布</string>
+ <string name="content_description_add_media">添加媒体</string>
+ <string name="incorrect_credentials">用户名或密码不正确。</string>
+ <string name="password">密码</string>
+ <string name="username">用户名</string>
+ <string name="reader">阅读器</string>
+ <string name="featured">用作精选图像</string>
+ <string name="featured_in_post">在博文内容中包含图像</string>
+ <string name="no_network_title">无网络可用</string>
+ <string name="pages">页面</string>
+ <string name="caption">说明 (可选)</string>
+ <string name="width">宽度</string>
+ <string name="posts">博文</string>
+ <string name="anonymous">匿名</string>
+ <string name="page">页面</string>
+ <string name="post">博文</string>
+ <string name="blogusername">博客用户名</string>
+ <string name="ok">确定</string>
+ <string name="upload_scaled_image">上传并创建缩略图链接</string>
+ <string name="scaled_image">图像缩放宽度</string>
+ <string name="scheduled">定时</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">正在上传…</string>
+ <string name="version">版本</string>
+ <string name="tos">服务条款</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">默认图片宽度</string>
+ <string name="image_alignment">对齐</string>
+ <string name="refresh">刷新</string>
+ <string name="untitled">无标题</string>
+ <string name="edit">编辑</string>
+ <string name="post_id">文章</string>
+ <string name="page_id">页面</string>
+ <string name="post_password">密码(可选)</string>
+ <string name="immediately">立即</string>
+ <string name="quickpress_add_alert_title">设置快捷方式名称</string>
+ <string name="today">今天</string>
+ <string name="settings">设置</string>
+ <string name="share_url">分享网址</string>
+ <string name="quickpress_window_title">选择用于 QuickPress 快捷方式的博客</string>
+ <string name="quickpress_add_error">快捷方式名称不能为空</string>
+ <string name="publish_post">发布</string>
+ <string name="draft">草稿</string>
+ <string name="post_private">私密</string>
+ <string name="upload_full_size_image">上传并链接到完整图像</string>
+ <string name="title">标题</string>
+ <string name="tags_separate_with_commas">标签(多个标签请以英文逗号“,”隔开)</string>
+ <string name="categories">分类</string>
+ <string name="dlg_deleting_comments">删除评论</string>
+ <string name="notification_blink">闪烁通知灯</string>
+ <string name="notification_sound">通知声音</string>
+ <string name="notification_vibrate">振动</string>
+ <string name="status">状态</string>
+ <string name="location">位置</string>
+ <string name="sdcard_title">需要 SD 卡</string>
+ <string name="select_video">从媒体库选择视频</string>
+ <string name="media">媒体</string>
+ <string name="delete">删除</string>
+ <string name="none">无</string>
+ <string name="blogs">博客</string>
+ <string name="select_photo">从媒体库选择照片</string>
+ <string name="error">错误</string>
+ <string name="cancel">取消</string>
+ <string name="save">保存</string>
+ <string name="add">添加</string>
+ <string name="category_refresh_error">分类刷新错误</string>
+ <string name="preview">预览</string>
+ <string name="on">于</string>
+ <string name="reply">回复</string>
+ <string name="notification_settings">通知设置</string>
+ <string name="yes">是</string>
+ <string name="no">否</string>
+</resources>
diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 000000000..7f823de4b
--- /dev/null
+++ b/WordPress/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,1122 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">管理員</string>
+ <string name="role_editor">編輯者</string>
+ <string name="role_author">作者</string>
+ <string name="role_contributor">撰寫人員</string>
+ <string name="role_follower">關注者</string>
+ <string name="role_viewer">瀏覽者</string>
+ <string name="error_post_my_profile_no_connection">無法連線,故無法儲存你的個人檔案</string>
+ <string name="alignment_none">無</string>
+ <string name="alignment_left">左</string>
+ <string name="alignment_right">右</string>
+ <string name="site_settings_list_editor_action_mode_title">已選取 %1$d</string>
+ <string name="viewer_remove_confirmation_message">如果移除這位瀏覽者,對方將無法造訪此網站。\n\n仍要移除這位瀏覽者嗎?</string>
+ <string name="follower_remove_confirmation_message">移除後,這位關注者如果沒有重新關注,就會停止收到此網站的通知。\n\n仍要移除這位關注者嗎?</string>
+ <string name="follower_subscribed_since">自 %1$s 開始</string>
+ <string name="reader_label_view_gallery">檢視圖庫</string>
+ <string name="error_remove_follower">無法移除關注者</string>
+ <string name="error_remove_viewer">無法移除瀏覽者</string>
+ <string name="error_fetch_email_followers_list">無法擷取網站電子郵件關注者</string>
+ <string name="error_fetch_followers_list">無法擷取網站關注者</string>
+ <string name="editor_failed_uploads_switch_html">部分媒體上傳失敗。在此狀態下,你無法\n切換至 HTML 模式。要移除所有上傳失敗的項目並繼續嗎?</string>
+ <string name="format_bar_description_html">HTML 模式</string>
+ <string name="visual_editor">視覺化編輯器</string>
+ <string name="image_thumbnail">縮圖</string>
+ <string name="format_bar_description_ul">未排序清單</string>
+ <string name="format_bar_description_ol">已排序清單</string>
+ <string name="format_bar_description_more">插入更多</string>
+ <string name="format_bar_description_media">插入媒體</string>
+ <string name="format_bar_description_strike">刪除線</string>
+ <string name="format_bar_description_quote">封鎖引文</string>
+ <string name="format_bar_description_link">插入連結</string>
+ <string name="format_bar_description_italic">斜體</string>
+ <string name="format_bar_description_underline">底線</string>
+ <string name="image_settings_save_toast">變更已儲存</string>
+ <string name="image_caption">說明</string>
+ <string name="image_alt_text">替代文字</string>
+ <string name="image_link_to">連結至</string>
+ <string name="image_width">寬度</string>
+ <string name="format_bar_description_bold">粗體</string>
+ <string name="image_settings_dismiss_dialog_title">要捨棄未儲存的變更嗎?</string>
+ <string name="stop_upload_dialog_title">停止上傳?</string>
+ <string name="stop_upload_button">停止上傳</string>
+ <string name="alert_error_adding_media">插入媒體時發生錯誤</string>
+ <string name="alert_action_while_uploading">你目前正在上傳媒體。請等待此項作業完成。</string>
+ <string name="alert_insert_image_html_mode">無法直接在 HTML 模式中插入媒體。請切換回視覺化模式。</string>
+ <string name="uploading_gallery_placeholder">正在上傳圖庫...</string>
+ <string name="invite_sent">已成功送出邀請</string>
+ <string name="tap_to_try_again">點選以重試!</string>
+ <string name="invite_error_for_username">%1$s:%2$s</string>
+ <string name="invite_message_info">(選用) 你最多可以輸入 500 個字元的自訂訊息,附在傳送給使用者的邀請中。</string>
+ <string name="invite_message_remaining_other">剩下 %d 個字元</string>
+ <string name="invite_message_remaining_one">剩下 1 個字元</string>
+ <string name="invite_message_remaining_zero">剩下 0 個字元</string>
+ <string name="invite_invalid_email">電子郵件地址「%s」無效</string>
+ <string name="invite_message_title">自訂訊息</string>
+ <string name="invite_already_a_member">已有成員使用「%s」作為使用者名稱</string>
+ <string name="invite_username_not_found">找不到使用者名稱為「%s」的使用者</string>
+ <string name="invite">邀請</string>
+ <string name="invite_names_title">使用者名稱或電子郵件</string>
+ <string name="signup_succeed_signin_failed">系統已為你建立帳號,但在將你登入時發生錯誤\n。請嘗試使用新建立的使用者名稱與密碼登入。</string>
+ <string name="send_link">傳送連結</string>
+ <string name="my_site_header_external">外部</string>
+ <string name="invite_people">邀請他人</string>
+ <string name="label_clear_search_history">清除搜尋記錄</string>
+ <string name="dlg_confirm_clear_search_history">清除搜尋記錄?</string>
+ <string name="reader_empty_posts_in_search_description">無法以你的語言找到與「%s」相關的文章</string>
+ <string name="reader_label_post_search_running">搜尋中…</string>
+ <string name="reader_label_related_posts">相關選讀內容</string>
+ <string name="reader_empty_posts_in_search_title">找不到文章</string>
+ <string name="reader_label_post_search_explainer">搜尋所有公開的 WordPress.com 網誌</string>
+ <string name="reader_hint_post_search">搜尋 WordPress.com</string>
+ <string name="reader_title_related_post_detail">相關文章</string>
+ <string name="reader_title_search_results">搜尋「%s」</string>
+ <string name="preview_screen_links_disabled">預覽畫面上的連結已停用</string>
+ <string name="draft_explainer">這篇文章只是草稿,尚未發表</string>
+ <string name="send">傳送</string>
+ <string name="person_remove_confirmation_title">移除 %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">此清單中的網站最近未張貼任何文章</string>
+ <string name="people">使用者</string>
+ <string name="edit_user">編輯使用者</string>
+ <string name="role">角色</string>
+ <string name="error_remove_user">無法移除使用者</string>
+ <string name="error_update_role">無法更新使用者角色</string>
+ <string name="gravatar_camera_and_media_permission_required">需要權限才能選取或拍攝照片</string>
+ <string name="error_updating_gravatar">更新你的 Gravatar 時發生錯誤</string>
+ <string name="error_locating_image">尋找裁切的圖片時發生錯誤</string>
+ <string name="error_refreshing_gravatar">重新載入你的 Gravatar 時發生錯誤</string>
+ <string name="gravatar_tip">新功能!點選 Gravatar 可加以變更!</string>
+ <string name="error_cropping_image">裁切圖片時發生錯誤</string>
+ <string name="launch_your_email_app">啟動你的電子郵件應用程式</string>
+ <string name="checking_email">正在檢查電子郵件</string>
+ <string name="not_on_wordpress_com">不在 WordPress.com 上嗎?</string>
+ <string name="magic_link_unavailable_error_message">目前無法使用。請輸入你的密碼</string>
+ <string name="check_your_email">檢查你的電子郵件</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">取得傳送至你電子郵件的連結以立即登入</string>
+ <string name="logging_in">正在登入</string>
+ <string name="enter_your_password_instead">改為輸入你的密碼</string>
+ <string name="web_address_dialog_hint">當你留言時公開顯示。</string>
+ <string name="jetpack_not_connected_message">已安裝 Jetpack 外掛程式,但未連結至 WordPress.com。是否要連結 Jetpack?</string>
+ <string name="username_email">電子郵件或使用者名稱</string>
+ <string name="jetpack_not_connected">Jetpack 外掛程式未連結</string>
+ <string name="new_editor_reflection_error">視覺化編輯器與你的裝置不相容,已\n自動停用。</string>
+ <string name="stats_insights_latest_post_no_title">(無標題)</string>
+ <string name="capture_or_pick_photo">拍攝或選取照片</string>
+ <string name="plans_post_purchase_text_themes">你現在可以無限使用進階版佈景主題。在你的網站上預覽任一佈景主題以開始使用。</string>
+ <string name="plans_post_purchase_button_themes">瀏覽佈景主題</string>
+ <string name="plans_post_purchase_title_themes">尋找最適合的進階版佈景主題</string>
+ <string name="plans_post_purchase_button_video">開始撰寫新文章</string>
+ <string name="plans_post_purchase_text_video">你可以使用 VideoPress 和擴充的媒體儲存空間上傳並託管影片。</string>
+ <string name="plans_post_purchase_title_video">使用影片讓文章生動有趣</string>
+ <string name="plans_post_purchase_button_customize">自訂我的網站</string>
+ <string name="plans_post_purchase_text_customize">你現在可以存取自訂字型、自訂顏色及自訂 CSS 編輯功能。</string>
+ <string name="plans_post_purchase_text_intro">你的網站簡直令人樂不可支!現在,你可以探索自己網站的新功能,並自行選擇要從哪裡開始。</string>
+ <string name="plans_post_purchase_title_customize">自訂字型和顏色</string>
+ <string name="plans_post_purchase_title_intro">交給你了,加油!</string>
+ <string name="export_your_content_message">我們會將你的文章、頁面和設定寄到你的電子郵件地址:%s.</string>
+ <string name="plan">方案</string>
+ <string name="plans">方案</string>
+ <string name="plans_loading_error">無法載入方案</string>
+ <string name="export_your_content">匯出你的內容</string>
+ <string name="exporting_content_progress">匯出內容...</string>
+ <string name="export_email_sent">匯出電子郵件已傳送!</string>
+ <string name="premium_upgrades_message">你的網站已啟用進階版升級服務。刪除網站前,請先取消升級。</string>
+ <string name="show_purchases">顯示購買項目</string>
+ <string name="checking_purchases">檢查購買項目</string>
+ <string name="premium_upgrades_title">進階版升級</string>
+ <string name="purchases_request_error">發生了點錯誤。無法要求購買項目。</string>
+ <string name="delete_site_progress">正在刪除網站...</string>
+ <string name="delete_site_summary">此動作無法復原。刪除網站將一併移除網站上的所有內容、參與者及網域。</string>
+ <string name="delete_site_hint">刪除網站</string>
+ <string name="export_site_hint">將網站匯出為 XML 檔案</string>
+ <string name="are_you_sure">你確定?</string>
+ <string name="export_site_summary">如果確定,現在請務必花點時間匯出你的內容。日後將無法還原內容。</string>
+ <string name="keep_your_content">保留你的內容</string>
+ <string name="domain_removal_hint">移除網站後便無法再運作的網域</string>
+ <string name="domain_removal_summary">注意!刪除網站會一併移除下方列出的網域。</string>
+ <string name="primary_domain">主要網域</string>
+ <string name="domain_removal">移除網域</string>
+ <string name="error_deleting_site_summary">刪除網站時發生錯誤。請聯絡支援團隊,尋求進一步協助</string>
+ <string name="error_deleting_site">刪除網站時發生錯誤</string>
+ <string name="confirm_delete_site_prompt">請在下方欄位中輸入 %1$s 以確認。系統將永久刪除你的網站。</string>
+ <string name="site_settings_export_content_title">匯出內容</string>
+ <string name="contact_support">聯絡支援團隊</string>
+ <string name="confirm_delete_site">確認刪除網站</string>
+ <string name="start_over_text">如果你想保留網站,但不想保留現有的文章和頁面,我們的支援團隊可以為你刪除網站上的文章、頁面、媒體及留言。\n\n這能讓你保留網站和 URL,但重新開始建立內容。若要清除目前的內容,請與我們聯絡。</string>
+ <string name="site_settings_start_over_hint">重新開始設計你的網站</string>
+ <string name="let_us_help">我們很樂意協助</string>
+ <string name="me_btn_app_settings">應用程式設定</string>
+ <string name="start_over">重新開始</string>
+ <string name="editor_remove_failed_uploads">移除失敗的上傳項目</string>
+ <string name="editor_toast_failed_uploads">部分媒體上傳失敗。在此狀態中,你無法儲存或發表\n自己的文章。是否要移除所有失敗的媒體?</string>
+ <string name="comments_empty_list_filtered_trashed">沒有已移至垃圾桶的留言</string>
+ <string name="site_settings_advanced_header">進階</string>
+ <string name="comments_empty_list_filtered_pending">沒有待審核的留言</string>
+ <string name="comments_empty_list_filtered_approved">沒有已核准的留言</string>
+ <string name="button_done">完成</string>
+ <string name="button_skip">略過</string>
+ <string name="site_timeout_error">由於發生逾時錯誤,導致無法連結到 WordPress 網站。</string>
+ <string name="xmlrpc_malformed_response_error">無法連結。WordPress 安裝以無效的 XML-RPC 文件回應。</string>
+ <string name="xmlrpc_missing_method_error">無法連結。伺服器缺少必要的 XML-RPC 方式。</string>
+ <string name="post_format_status">狀態</string>
+ <string name="post_format_video">影片</string>
+ <string name="theme_free">免費</string>
+ <string name="theme_all">全部</string>
+ <string name="theme_premium">進階版</string>
+ <string name="post_format_chat">聊天</string>
+ <string name="post_format_gallery">圖庫</string>
+ <string name="post_format_image">圖片</string>
+ <string name="post_format_link">連結</string>
+ <string name="post_format_quote">引文</string>
+ <string name="post_format_standard">標準版</string>
+ <string name="notif_events">WordPress.com 課程與活動相關資訊 (線上和專人洽詢)。</string>
+ <string name="post_format_aside">旁白</string>
+ <string name="post_format_audio">音訊</string>
+ <string name="notif_surveys">參與 WordPress.com 研究與調查的機會。</string>
+ <string name="notif_tips">讓 WordPress.com 發揮最大功效的秘訣。</string>
+ <string name="notif_community">社群</string>
+ <string name="replies_to_my_comments">留言的回覆</string>
+ <string name="notif_suggestions">建議</string>
+ <string name="notif_research">研究</string>
+ <string name="site_achievements">網站成就</string>
+ <string name="username_mentions">使用者名稱標記</string>
+ <string name="likes_on_my_posts">文章按讚數</string>
+ <string name="site_follows">網站關注數</string>
+ <string name="likes_on_my_comments">留言按讚數</string>
+ <string name="comments_on_my_site">網站留言</string>
+ <string name="site_settings_list_editor_summary_other">%d 個項目</string>
+ <string name="site_settings_list_editor_summary_one">1 個項目</string>
+ <string name="approve_auto_if_previously_approved">已知使用者的留言</string>
+ <string name="approve_auto">所有使用者</string>
+ <string name="approve_manual">無留言</string>
+ <string name="site_settings_paging_summary_other">每頁 %d 則留言</string>
+ <string name="site_settings_paging_summary_one">每頁 1 則留言</string>
+ <string name="site_settings_multiple_links_summary_other">需要審核超過 %d 個連結</string>
+ <string name="site_settings_multiple_links_summary_one">需要審核超過 1 個連結</string>
+ <string name="site_settings_multiple_links_summary_zero">需要審核超過 0 個連結</string>
+ <string name="detail_approve_auto">自動核准所有人的留言。</string>
+ <string name="detail_approve_auto_if_previously_approved">如果使用者有已審核的留言,則自動核准</string>
+ <string name="detail_approve_manual">需要人工審核所有人的留言。</string>
+ <string name="filter_trashed_posts">已移至垃圾桶</string>
+ <string name="days_quantity_one">1 天</string>
+ <string name="days_quantity_other">%d 天</string>
+ <string name="filter_published_posts">已發表</string>
+ <string name="filter_draft_posts">草稿</string>
+ <string name="filter_scheduled_posts">已排程</string>
+ <string name="pending_email_change_snackbar">按一下電子郵件 (收件者:%1$s) 中的驗證連結以確認您的新地址</string>
+ <string name="primary_site">主要網站</string>
+ <string name="web_address">網站位址</string>
+ <string name="editor_toast_uploading_please_wait">你目前正在上傳媒體。請等待此作業完成。</string>
+ <string name="error_refresh_comments_showing_older">目前無法重新整理留言 – 顯示較舊的留言</string>
+ <string name="editor_post_settings_set_featured_image">設定特色圖片</string>
+ <string name="editor_post_settings_featured_image">特色圖片</string>
+ <string name="new_editor_promo_desc">Android 版 WordPress 應用程式現已包含美觀的全新視覺化\n編輯器。建立新的文章,即可試用編輯器。</string>
+ <string name="new_editor_promo_title">全新編輯器</string>
+ <string name="new_editor_promo_button_label">太棒了,謝謝!</string>
+ <string name="visual_editor_enabled">視覺化編輯器已啟用</string>
+ <string name="editor_content_placeholder">在此分享你的故事...</string>
+ <string name="editor_page_title_placeholder">頁面標題</string>
+ <string name="editor_post_title_placeholder">文章標題</string>
+ <string name="email_address">電子郵件地址</string>
+ <string name="preference_show_visual_editor">顯示視覺化編輯器</string>
+ <string name="dlg_sure_to_delete_comments">永久刪除這些留言?</string>
+ <string name="preference_editor">編輯者</string>
+ <string name="dlg_sure_to_delete_comment">永久刪除此回應?</string>
+ <string name="mnu_comment_delete_permanently">刪除</string>
+ <string name="comment_deleted_permanently">留言已刪除</string>
+ <string name="mnu_comment_untrash">還原</string>
+ <string name="comments_empty_list_filtered_spam">無垃圾留言</string>
+ <string name="could_not_load_page">無法載入頁面</string>
+ <string name="comment_status_all">全部</string>
+ <string name="interface_language">介面語言</string>
+ <string name="off">關閉</string>
+ <string name="about_the_app">關於此應用程式</string>
+ <string name="error_post_account_settings">無法儲存你的帳號設定</string>
+ <string name="error_post_my_profile">無法儲存你的個人檔案</string>
+ <string name="error_fetch_account_settings">無法擷取你的帳號設定</string>
+ <string name="error_fetch_my_profile">無法擷取你的個人檔案</string>
+ <string name="stats_widget_promo_ok_btn_label">好,知道了</string>
+ <string name="stats_widget_promo_desc">將小工具新增至主畫面,輕鬆按一下即可存取統計資料。</string>
+ <string name="stats_widget_promo_title">主畫面統計資料小工具</string>
+ <string name="site_settings_unknown_language_code_error">無法辨識語言代碼</string>
+ <string name="site_settings_threading_dialog_description">允許留言以階層式嵌入留言串中。</string>
+ <string name="site_settings_threading_dialog_header">留言串最多</string>
+ <string name="remove">移除</string>
+ <string name="search">搜尋</string>
+ <string name="add_category">新增類別</string>
+ <string name="disabled">已停用</string>
+ <string name="site_settings_image_original_size">原始尺寸</string>
+ <string name="privacy_private">只有你和經核准的使用者可看見你的網站</string>
+ <string name="privacy_public_not_indexed">所有人都能看見你的網站,但網站會要求搜尋引擎不要加入索引</string>
+ <string name="privacy_public">所有人都能看見你的網站,且搜尋引擎可能會將網站加入索引</string>
+ <string name="about_me_hint">簡單介紹一下自己…</string>
+ <string name="public_display_name_hint">如果未設定,顯示名稱會預設為你的使用者名稱</string>
+ <string name="about_me">關於我</string>
+ <string name="public_display_name">公開顯示名稱</string>
+ <string name="my_profile">我的個人檔案</string>
+ <string name="first_name">名</string>
+ <string name="last_name">姓</string>
+ <string name="site_privacy_public_desc">允許搜尋引擎將這個網站加入索引</string>
+ <string name="site_privacy_hidden_desc">阻擋搜尋引擎將這個網站加入索引</string>
+ <string name="site_privacy_private_desc">我想要將我的網站設為私人網站,只有我選擇的使用者可看見</string>
+ <string name="cd_related_post_preview_image">相關文章預覽圖片</string>
+ <string name="error_post_remote_site_settings">無法儲存網站資訊</string>
+ <string name="error_fetch_remote_site_settings">無法擷取網站資訊</string>
+ <string name="error_media_upload_connection">上傳媒體時發生連線錯誤</string>
+ <string name="site_settings_disconnected_toast">已中斷連線,編輯功能已停用。</string>
+ <string name="site_settings_unsupported_version_error">不支援的 WordPress 版本</string>
+ <string name="site_settings_multiple_links_dialog_description">如果留言包含的連結超過此數量,留言就必須經過審核。</string>
+ <string name="site_settings_close_after_dialog_switch_text">自動關閉</string>
+ <string name="site_settings_close_after_dialog_description">自動關閉文章的留言功能。</string>
+ <string name="site_settings_paging_dialog_description">將留言串分為數頁。</string>
+ <string name="site_settings_paging_dialog_header">每頁留言數</string>
+ <string name="site_settings_close_after_dialog_title">關閉留言功能</string>
+ <string name="site_settings_blacklist_description">如果留言的內容、名稱、URL、電子郵件或 IP 位址中含有以下文字,該留言將標記為垃圾留言。你也可以輸入字詞的一部分,例如「press」會與「WordPress」相符。</string>
+ <string name="site_settings_hold_for_moderation_description">如果留言的內容、名稱、URL、電子郵件或 IP 位址中含有以下文字,就會被送進審核清單。你也可以輸入字詞的一部分,例如「press」會與「WordPress」相符。</string>
+ <string name="site_settings_list_editor_input_hint">輸入字詞或片語</string>
+ <string name="site_settings_list_editor_no_items_text">無項目</string>
+ <string name="site_settings_learn_more_caption">你可以針對不同的文章覆寫這些設定。</string>
+ <string name="site_settings_rp_preview3_site">在「升級」中</string>
+ <string name="site_settings_rp_preview3_title">升級重點:婚禮適用的 VideoPress</string>
+ <string name="site_settings_rp_preview2_site">在「應用程式」中</string>
+ <string name="site_settings_rp_preview2_title">Android 專用的 WordPress 應用程式已全面翻新</string>
+ <string name="site_settings_rp_preview1_site">在「行動」中</string>
+ <string name="site_settings_rp_preview1_title">有重大的 iPhone/iPad 更新可供使用</string>
+ <string name="site_settings_rp_show_images_title">顯示圖片</string>
+ <string name="site_settings_rp_show_header_title">顯示頁首</string>
+ <string name="site_settings_rp_switch_summary">相關文章會在你的文章下方顯示網站上的相關內容。</string>
+ <string name="site_settings_rp_switch_title">顯示相關文章</string>
+ <string name="site_settings_delete_site_hint">從應用程式移除你的網站資料</string>
+ <string name="site_settings_blacklist_hint">與篩選條件相符的留言將標示為垃圾留言</string>
+ <string name="site_settings_moderation_hold_hint">與篩選條件相符的留言將被送進審核清單</string>
+ <string name="site_settings_multiple_links_hint">忽略已知使用者的連結限制</string>
+ <string name="site_settings_whitelist_hint">先前有經核准的留言才可以發表留言</string>
+ <string name="site_settings_user_account_required_hint">已註冊且登入的使用者才可以發表留言</string>
+ <string name="site_settings_identity_required_hint">輸入名稱和電子郵件後才可以發表留言</string>
+ <string name="site_settings_manual_approval_hint">留言必須經人工核准</string>
+ <string name="site_settings_paging_hint">按照指定的區塊大小顯示留言</string>
+ <string name="site_settings_threading_hint">允許留言達到一定高度後以巢狀顯示</string>
+ <string name="site_settings_sort_by_hint">決定留言顯示順序</string>
+ <string name="site_settings_close_after_hint">不允許在指定的時間後發表留言</string>
+ <string name="site_settings_receive_pingbacks_hint">允許其他網誌在建立連結時傳送通知</string>
+ <string name="site_settings_send_pingbacks_hint">嘗試通知在文章中建立連結的所有網誌</string>
+ <string name="site_settings_allow_comments_hint">允許讀者發表留言</string>
+ <string name="site_settings_discussion_hint">檢視及變更你的網站討論設定</string>
+ <string name="site_settings_more_hint">檢視所有可變更的討論設定</string>
+ <string name="site_settings_related_posts_hint">在讀取器中顯示或隱藏相關文章</string>
+ <string name="site_settings_upload_and_link_image_hint">允許一律上傳完整大小的圖片</string>
+ <string name="site_settings_image_width_hint">將文章中的圖片調整為符合以下寬度</string>
+ <string name="site_settings_format_hint">設定新的文章格式</string>
+ <string name="site_settings_category_hint">設定新的文章類別</string>
+ <string name="site_settings_location_hint">自動將位置資料加到文章中</string>
+ <string name="site_settings_password_hint">變更你的密碼</string>
+ <string name="site_settings_username_hint">目前的使用者帳號</string>
+ <string name="site_settings_language_hint">此網誌主要使用的語言</string>
+ <string name="site_settings_privacy_hint">控制網站存取權限</string>
+ <string name="site_settings_address_hint">目前不支援變更網址</string>
+ <string name="site_settings_tagline_hint">使用簡短的說明或好記的字詞組合來描述你的網誌</string>
+ <string name="site_settings_title_hint">以簡單幾個字說明此網站主題</string>
+ <string name="site_settings_whitelist_known_summary">已知使用者的留言</string>
+ <string name="site_settings_whitelist_all_summary">所有使用者的留言</string>
+ <string name="site_settings_threading_summary">%d 層</string>
+ <string name="site_settings_privacy_private_summary">私密</string>
+ <string name="site_settings_privacy_hidden_summary">隱藏</string>
+ <string name="site_settings_delete_site_title">刪除網站</string>
+ <string name="site_settings_privacy_public_summary">公開</string>
+ <string name="site_settings_blacklist_title">黑名單</string>
+ <string name="site_settings_moderation_hold_title">等待審核</string>
+ <string name="site_settings_multiple_links_title">留言中的連結</string>
+ <string name="site_settings_whitelist_title">自動核准</string>
+ <string name="site_settings_threading_title">階層顯示</string>
+ <string name="site_settings_paging_title">分頁</string>
+ <string name="site_settings_sort_by_title">排序依據</string>
+ <string name="site_settings_account_required_title">使用者必須登入</string>
+ <string name="site_settings_identity_required_title">必須包含名稱和電子郵件</string>
+ <string name="site_settings_receive_pingbacks_title">接收引用通知</string>
+ <string name="site_settings_send_pingbacks_title">傳送引用通知</string>
+ <string name="site_settings_allow_comments_title">允許留言</string>
+ <string name="site_settings_default_format_title">預設格式</string>
+ <string name="site_settings_default_category_title">預設類別</string>
+ <string name="site_settings_location_title">啟用定位</string>
+ <string name="site_settings_address_title">位址</string>
+ <string name="site_settings_title_title">網站標題</string>
+ <string name="site_settings_tagline_title">標語</string>
+ <string name="site_settings_this_device_header">此裝置</string>
+ <string name="site_settings_discussion_new_posts_header">新文章預設設定</string>
+ <string name="site_settings_account_header">帳號</string>
+ <string name="site_settings_writing_header">撰寫</string>
+ <string name="newest_first">最新的在前</string>
+ <string name="site_settings_general_header">一般</string>
+ <string name="discussion">討論</string>
+ <string name="privacy">隱私</string>
+ <string name="related_posts">相關文章</string>
+ <string name="comments">留言</string>
+ <string name="close_after">以下時間後關閉</string>
+ <string name="oldest_first">最舊的在前</string>
+ <string name="media_error_no_permission_upload">你沒有權限,無法上傳媒體檔案至網站</string>
+ <string name="never">從未</string>
+ <string name="unknown">不明</string>
+ <string name="reader_err_get_post_not_found">此文章已不存在</string>
+ <string name="reader_err_get_post_not_authorized">你未獲授權無法檢視此文章</string>
+ <string name="reader_err_get_post_generic">無法擷取此文章</string>
+ <string name="blog_name_no_spaced_allowed">網站位址不可包含空格</string>
+ <string name="invalid_username_no_spaces">使用者名稱不可包含空格</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">您所關注的網站最近未發表任何文章</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">沒有近期文章</string>
+ <string name="media_details_copy_url_toast">URL 已複製到剪貼簿</string>
+ <string name="edit_media">編輯媒體</string>
+ <string name="media_details_copy_url">複製 URL</string>
+ <string name="media_details_label_date_uploaded">已上傳</string>
+ <string name="media_details_label_date_added">已新增</string>
+ <string name="selected_theme">選取的佈景主題</string>
+ <string name="could_not_load_theme">無法載入佈景主題</string>
+ <string name="theme_activation_error">發生了點錯誤。無法啟用佈景主題</string>
+ <string name="theme_by_author_prompt_append">作者:%1$s</string>
+ <string name="theme_prompt">感謝你選擇 %1$s</string>
+ <string name="theme_try_and_customize">試用與自訂</string>
+ <string name="theme_view">檢視</string>
+ <string name="theme_details">詳細資料</string>
+ <string name="theme_support">支援</string>
+ <string name="theme_done">完成</string>
+ <string name="theme_manage_site">管理網站</string>
+ <string name="title_activity_theme_support">佈景主題</string>
+ <string name="theme_activate">啟用</string>
+ <string name="date_range_start_date">開始日期</string>
+ <string name="date_range_end_date">結束日期</string>
+ <string name="current_theme">目前佈景主題</string>
+ <string name="customize">自訂</string>
+ <string name="details">詳細資料</string>
+ <string name="support">支援</string>
+ <string name="active">已啟用</string>
+ <string name="stats_referrers_spam_generic_error">操作期間發生錯誤。垃圾留言狀態並未變更。</string>
+ <string name="stats_referrers_marking_not_spam">標記為非垃圾留言</string>
+ <string name="stats_referrers_unspam">非垃圾留言</string>
+ <string name="stats_referrers_marking_spam">標記為垃圾留言</string>
+ <string name="theme_auth_error_authenticate">佈景主題擷取失敗:使用者授權失敗</string>
+ <string name="post_published">文章已發表</string>
+ <string name="page_published">頁面已發表</string>
+ <string name="post_updated">文章已更新</string>
+ <string name="page_updated">頁面已更新</string>
+ <string name="stats_referrers_spam">垃圾郵件</string>
+ <string name="theme_no_search_result_found">很抱歉,找不到佈景主題。</string>
+ <string name="media_file_name">檔案名稱:%s</string>
+ <string name="media_uploaded_on">上傳時間:%s</string>
+ <string name="media_dimensions">尺寸:%s</string>
+ <string name="upload_queued">已排進佇列</string>
+ <string name="media_file_type">檔案類型:%s</string>
+ <string name="reader_label_gap_marker">載入更多文章</string>
+ <string name="notifications_no_search_results">沒有網站符合「%s」</string>
+ <string name="search_sites">搜尋網站</string>
+ <string name="notifications_empty_view_reader">檢視讀取器</string>
+ <string name="unread">未讀</string>
+ <string name="notifications_empty_action_followers_likes">取得通知:在已閱讀的文章上留言。</string>
+ <string name="notifications_empty_action_comments">加入討論:在關注的網誌文章上留言。</string>
+ <string name="notifications_empty_action_unread">再度展開討論:撰寫新文章。</string>
+ <string name="notifications_empty_action_all">動起來吧!立即在你關注的網誌文章上留言。</string>
+ <string name="notifications_empty_likes">尚未有新的讚。</string>
+ <string name="notifications_empty_followers">尚未有新的關注者。</string>
+ <string name="notifications_empty_comments">尚未有新的留言。</string>
+ <string name="notifications_empty_unread">你已追上最新進度!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">請先存取應用程式中的統計資料,稍後再試著新增小工具</string>
+ <string name="stats_widget_error_readd_widget">請移除小工具,然後再新增一次</string>
+ <string name="stats_widget_error_no_visible_blog">在未開放網誌的情況下無法存取統計資料</string>
+ <string name="stats_widget_error_no_permissions">你的 WordPress.com 帳號無法存取此網誌的統計資料</string>
+ <string name="stats_widget_error_no_account">請登入 WordPress</string>
+ <string name="stats_widget_error_generic">無法載入統計資料</string>
+ <string name="stats_widget_loading_data">正在載入資料…</string>
+ <string name="stats_widget_name_for_blog">%1$s 的本日統計資料</string>
+ <string name="stats_widget_name">WordPress 本日統計</string>
+ <string name="add_location_permission_required">需要擁有權限才能新增位置</string>
+ <string name="add_media_permission_required">需要擁有權限才能新增媒體</string>
+ <string name="access_media_permission_required">需要擁有權限才能存取媒體</string>
+ <string name="stats_enable_rest_api_in_jetpack">若要查看統計資料,請啟用 Jetpack 的 JSON API 模組。</string>
+ <string name="error_open_list_from_notification">此文章或頁面已發表於其他網站</string>
+ <string name="reader_short_comment_count_multi">%s評論</string>
+ <string name="reader_short_comment_count_one">1評論</string>
+ <string name="reader_label_submit_comment">發送</string>
+ <string name="reader_hint_comment_on_post">回复文章...</string>
+ <string name="reader_discover_visit_blog">造訪 %s</string>
+ <string name="reader_discover_attribution_blog">原始發表位置:%s</string>
+ <string name="reader_discover_attribution_author">原始作者:%s</string>
+ <string name="reader_discover_attribution_author_and_blog">原始作者:%1$s,發表位置:%2$s</string>
+ <string name="reader_short_like_count_multi">%s 個讚</string>
+ <string name="reader_short_like_count_one">1 個讚</string>
+ <string name="reader_label_follow_count">%,d 個關注者</string>
+ <string name="reader_short_like_count_none">讚</string>
+ <string name="reader_menu_tags">編輯標籤和網誌</string>
+ <string name="reader_title_post_detail">讀者文章</string>
+ <string name="local_draft_explainer">這篇文章是儲存在本機上的草稿,尚未發表</string>
+ <string name="local_changes_explainer">這篇文章的本機存檔經過變更,變更內容尚未發表</string>
+ <string name="notifications_push_summary">在裝置上顯示通知的相關設定。</string>
+ <string name="notifications_email_summary">將通知傳送至帳號所繫結之電子郵件地址的相關設定。</string>
+ <string name="notifications_tab_summary">在通知索引標籤中顯示通知的相關設定。</string>
+ <string name="notifications_disabled">應用程式通知已停用。點選這裡,即可在「設定」中啟用通知。</string>
+ <string name="notification_types">通知類型</string>
+ <string name="error_loading_notifications">無法載入通知設定</string>
+ <string name="replies_to_your_comments">留言的回覆</string>
+ <string name="comment_likes">按讚的留言</string>
+ <string name="app_notifications">應用程式通知</string>
+ <string name="notifications_tab">通知索引標籤</string>
+ <string name="email">電子郵件</string>
+ <string name="notifications_comments_other_blogs">其他網站上的留言</string>
+ <string name="notifications_other">其他</string>
+ <string name="notifications_wpcom_updates">WordPress.com 消息更新</string>
+ <string name="notifications_account_emails">WordPress.com 寄送的電子郵件</string>
+ <string name="notifications_account_emails_summary">我們會隨時傳送與帳號資訊相關的重要電子郵件,除此之外,你也能獲得實用的額外內容。</string>
+ <string name="your_sites">你的網站</string>
+ <string name="notifications_sights_and_sounds">提示與音效</string>
+ <string name="stats_insights_latest_post_trend">自從 %2$s 發表以來已經過了 %1$s。文章截至目前為止獲得的迴響如下…</string>
+ <string name="stats_insights_latest_post_summary">最新文章摘要</string>
+ <string name="button_revert">還原</string>
+ <string name="days_ago">%d 天前</string>
+ <string name="yesterday">昨天</string>
+ <string name="connectionbar_no_connection">沒有連線</string>
+ <string name="page_trashed">頁面已移至垃圾桶</string>
+ <string name="post_deleted">文章已刪除</string>
+ <string name="post_trashed">文章已移至垃圾桶</string>
+ <string name="stats_no_activity_this_period">此期間沒有任何活動</string>
+ <string name="trashed">已移至垃圾桶</string>
+ <string name="button_back">返回</string>
+ <string name="page_deleted">頁面已刪除</string>
+ <string name="button_stats">統計</string>
+ <string name="button_trash">垃圾桶</string>
+ <string name="button_preview">預覽</string>
+ <string name="button_view">檢視</string>
+ <string name="button_edit">編輯</string>
+ <string name="button_publish">發表</string>
+ <string name="my_site_no_sites_view_subtitle">你希望加入一個嗎?</string>
+ <string name="my_site_no_sites_view_title">你還沒有任何 WordPress 網站。</string>
+ <string name="my_site_no_sites_view_drake">圖示</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">你沒有存取此網誌的權限</string>
+ <string name="reader_toast_err_follow_blog_not_found">無法找到這個部落格</string>
+ <string name="undo">復原</string>
+ <string name="tabbar_accessibility_label_my_site">我的網站</string>
+ <string name="tabbar_accessibility_label_me">我</string>
+ <string name="passcodelock_prompt_message">輸入 PIN 碼</string>
+ <string name="editor_toast_changes_saved">變更已儲存</string>
+ <string name="push_auth_expired">要求已到期。登入 WordPress.com 再試一次。</string>
+ <string name="stats_insights_best_ever">最佳的瀏覽次數</string>
+ <string name="ignore">忽略</string>
+ <string name="stats_insights_most_popular_percent_views">瀏覽次數:%1$d%%</string>
+ <string name="stats_insights_most_popular_hour">最熱門的時段</string>
+ <string name="stats_insights_most_popular_day">最熱門的日子</string>
+ <string name="stats_insights_popular">最熱門的日子及時段</string>
+ <string name="stats_insights_today">本日統計</string>
+ <string name="stats_insights_all_time">有史以來的文章數、瀏覽次數與訪客數</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">若要檢視統計資料,請使用連線 Jetpack 的帳號登入 WordPress.com。</string>
+ <string name="stats_other_recent_stats_moved_label">正在尋找其他近期統計資料嗎?我們已將統計資料移至「Insights」頁面。</string>
+ <string name="me_disconnect_from_wordpress_com">中斷與 WordPress.com 的連線</string>
+ <string name="me_btn_login_logout">登入/登出</string>
+ <string name="me_connect_to_wordpress_com">連線 WordPress.com</string>
+ <string name="site_picker_cant_hide_current_site">由於「%s」是目前的網站,因此並未隱藏</string>
+ <string name="me_btn_support">說明與客戶服務</string>
+ <string name="account_settings">帳號設定</string>
+ <string name="site_picker_create_dotcom">建立 WordPress.com 網站</string>
+ <string name="site_picker_edit_visibility">顯示/隱藏網站</string>
+ <string name="site_picker_add_self_hosted">新增自助託管的網站</string>
+ <string name="site_picker_add_site">新增網站</string>
+ <string name="my_site_btn_switch_site">切換網站</string>
+ <string name="site_picker_title">選擇網站</string>
+ <string name="my_site_btn_view_site">檢視網站</string>
+ <string name="my_site_btn_view_admin">檢視管理員</string>
+ <string name="my_site_header_publish">發表</string>
+ <string name="my_site_header_look_and_feel">外觀和風格</string>
+ <string name="my_site_btn_site_settings">設定</string>
+ <string name="my_site_btn_blog_posts">網誌文章</string>
+ <string name="reader_label_new_posts_subtitle">點選以顯示</string>
+ <string name="my_site_header_configuration">組態</string>
+ <string name="notifications_account_required">登入 WordPress.com 以接收通知</string>
+ <string name="stats_unknown_author">不明作者</string>
+ <string name="signout">中斷連結</string>
+ <string name="image_added">已新增圖片</string>
+ <string name="select_all">全部選取</string>
+ <string name="sign_out_wpcom_confirm">中斷帳號連結將會移除此裝置上 @%s 的所有 WordPress.com 資料,包括本機草稿及變更項目。</string>
+ <string name="show">顯示</string>
+ <string name="hide">隱藏</string>
+ <string name="deselect_all">取消全選</string>
+ <string name="select_from_new_picker">使用全新挑選器一次選取多個項目</string>
+ <string name="loading_blog_videos">正在擷取影片</string>
+ <string name="no_media_sources">無法擷取媒體</string>
+ <string name="loading_blog_images">正在擷取圖片</string>
+ <string name="error_loading_videos">載入影片時發生錯誤</string>
+ <string name="error_loading_images">載入圖片時發生錯誤</string>
+ <string name="error_loading_blog_videos">無法擷取影片</string>
+ <string name="no_device_images">沒有圖片</string>
+ <string name="error_loading_blog_images">無法擷取圖片</string>
+ <string name="no_blog_images">沒有圖片</string>
+ <string name="no_blog_videos">沒有影片</string>
+ <string name="no_device_videos">沒有影片</string>
+ <string name="stats_generic_error">無法載入必要的統計資料</string>
+ <string name="loading_videos">正在載入影片</string>
+ <string name="loading_images">正在載入圖片</string>
+ <string name="no_media">沒有媒體</string>
+ <string name="device">裝置</string>
+ <string name="language">語言</string>
+ <string name="add_to_post">新增至文章</string>
+ <string name="media_picker_title">選取媒體</string>
+ <string name="take_photo">拍攝照片</string>
+ <string name="take_video">拍攝影片</string>
+ <string name="tab_title_device_images">裝置圖片</string>
+ <string name="tab_title_device_videos">裝置影片</string>
+ <string name="tab_title_site_images">網站圖片</string>
+ <string name="tab_title_site_videos">網站影片</string>
+ <string name="error_publish_no_network">未連線時無法發表。已儲存為草稿。</string>
+ <string name="editor_toast_invalid_path">檔案路徑無效</string>
+ <string name="verification_code">驗證碼</string>
+ <string name="invalid_verification_code">驗證碼無效</string>
+ <string name="verify">驗證</string>
+ <string name="two_step_footer_label">輸入驗證器應用程式上的驗證碼。</string>
+ <string name="two_step_footer_button">透過簡訊傳送驗證碼</string>
+ <string name="two_step_sms_sent">請查看簡訊是否收到驗證碼。</string>
+ <string name="sign_in_jetpack">登入 WordPress.com 帳號以連結至 Jetpack。</string>
+ <string name="auth_required">再次登入以繼續操作。</string>
+ <string name="media_details_label_file_name">檔案名稱</string>
+ <string name="media_details_label_file_type">檔案類型</string>
+ <string name="media_fetching">正在擷取媒體…</string>
+ <string name="posts_fetching">正在擷取文章…</string>
+ <string name="toast_err_post_uploading">上傳文章時無法加以開啟</string>
+ <string name="pages_fetching">正在擷取頁面…</string>
+ <string name="comments_fetching">正在擷取留言…</string>
+ <string name="stats_view_search_terms">搜尋字詞</string>
+ <string name="stats_view_authors">作者</string>
+ <string name="stats_entry_search_terms">搜尋字詞</string>
+ <string name="stats_empty_search_terms">沒有任何搜尋字詞的記錄</string>
+ <string name="stats_empty_search_terms_desc">查看訪客以哪些搜尋字詞找到你的網站,進一步瞭解搜尋流量的相關資訊。</string>
+ <string name="stats_followers_total_wpcom_paged">顯示 %1$d - %2$d 個 WordPress.com 關注者 (共 %3$s 個)</string>
+ <string name="stats_search_terms_unknown_search_terms">不明搜尋字詞</string>
+ <string name="stats_followers_total_email_paged">顯示 %1$d - %2$d 個電子郵件關注者 (共 %3$s 個)</string>
+ <string name="publisher">發佈者:</string>
+ <string name="error_notification_open">無法開啟通知</string>
+ <string name="reader_empty_posts_request_failed">無法擷取文章</string>
+ <string name="stats_months_and_years">月份與年份</string>
+ <string name="stats_average_per_day">每天平均</string>
+ <string name="stats_recent_weeks">最近幾星期</string>
+ <string name="error_copy_to_clipboard">將文字複製到剪貼簿時發生錯誤</string>
+ <string name="reader_label_new_posts">新文章</string>
+ <string name="reader_empty_posts_in_blog">這個部落格沒有內容</string>
+ <string name="stats_period">期間</string>
+ <string name="logs_copied_to_clipboard">應用程式記錄已複製到剪貼簿</string>
+ <string name="stats_total">總計</string>
+ <string name="stats_overall">整體</string>
+ <string name="post_uploading">正在上傳</string>
+ <string name="reader_page_recommended_blogs">你或許也會喜歡以下網站:</string>
+ <string name="stats_comments_total_comments_followers">含有留言關注者的文章總數:%1$s</string>
+ <string name="stats_visitors">訪客</string>
+ <string name="stats_timeframe_years">年</string>
+ <string name="stats_views">點閱數</string>
+ <string name="stats_pagination_label">第 %1$s 頁,共 %2$s 頁</string>
+ <string name="stats_view_countries">國家</string>
+ <string name="stats_likes">按讚數</string>
+ <string name="stats_view_publicize">宣傳</string>
+ <string name="stats_view_followers">關注者</string>
+ <string name="stats_view_videos">影片</string>
+ <string name="stats_entry_publicize">服務</string>
+ <string name="stats_entry_followers">關注者</string>
+ <string name="stats_totals_publicize">關注者</string>
+ <string name="stats_entry_clicks_link">連結</string>
+ <string name="stats_view_top_posts_and_pages">文章與頁面</string>
+ <string name="stats_entry_video_plays">影片</string>
+ <string name="stats_entry_top_commenter">作者</string>
+ <string name="stats_empty_geoviews">尚無任何國家記錄</string>
+ <string name="stats_empty_geoviews_desc">探索清單以查看哪些國家及地區為你的網站帶來最多流量。</string>
+ <string name="stats_totals_followers">自從</string>
+ <string name="stats_empty_referrers_desc">查看傳送最多流量的網站及搜尋引擎,深入瞭解你的網站可見度</string>
+ <string name="stats_empty_clicks_title">尚無任何點擊記錄</string>
+ <string name="stats_empty_referrers_title">尚無任何來源連結記錄</string>
+ <string name="stats_empty_top_posts_title">尚未有人點閱文章或頁面</string>
+ <string name="stats_empty_top_posts_desc">查看你最多人瀏覽的內容,並瞭解個別文章和頁面在一段時間內的點閱率。</string>
+ <string name="stats_empty_tags_and_categories">尚無任何人點閱加上標籤的文章或頁面</string>
+ <string name="stats_empty_clicks_desc">如果你的內容含有其他網站連結,可以查看訪客最常點閱哪些連結。</string>
+ <string name="stats_empty_top_authors_desc">追蹤各個參與者的文章瀏覽次數,仔細觀察每位作者最熱門的內容。</string>
+ <string name="stats_empty_tags_and_categories_desc">瞭解你網站上最熱門的主題,例如上星期以來的熱門文章中所反映的主題。</string>
+ <string name="stats_empty_publicize">尚無任何 Publicize 關注者的記錄</string>
+ <string name="stats_empty_video">尚無影片播放記錄</string>
+ <string name="stats_empty_video_desc">如果你已使用 VideoPress 上傳影片,你可以查看影片的觀看次數。</string>
+ <string name="stats_empty_comments_desc">如果你允許他人在網站上留言,可以依據 1000 則最新的留言,追蹤主要留言者,並查看哪些內容引起最熱烈的討論。</string>
+ <string name="stats_empty_followers">無關注者</string>
+ <string name="stats_empty_publicize_desc">使用 Publicize 追蹤來自不同社交網路服務的關注者。</string>
+ <string name="stats_comments_by_posts_and_pages">依文章與頁面</string>
+ <string name="stats_empty_followers_desc">追蹤關注者總人數,以及這些關注者分別已關注你的網站多久時間。</string>
+ <string name="stats_comments_by_authors">依作者</string>
+ <string name="stats_followers_email_selector">電子郵件</string>
+ <string name="stats_followers_total_wpcom">WordPress.com 關注者總數:%1$s</string>
+ <string name="stats_followers_seconds_ago">幾秒鐘前</string>
+ <string name="stats_followers_total_email">電子郵件關注者總數:%1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_days">%1$d 天</string>
+ <string name="stats_followers_a_minute_ago">一分鐘前</string>
+ <string name="stats_followers_hours">%1$d 個小時</string>
+ <string name="stats_followers_a_day">一天</string>
+ <string name="stats_followers_a_month">一個月</string>
+ <string name="stats_followers_minutes">%1$d 分鐘</string>
+ <string name="stats_followers_an_hour_ago">一小時前</string>
+ <string name="stats_view">檢視</string>
+ <string name="stats_followers_months">%1$d 個月</string>
+ <string name="stats_followers_a_year">一年</string>
+ <string name="stats_followers_years">%1$d 年</string>
+ <string name="stats_view_all">檢視全部</string>
+ <string name="stats_for">%s 的統計資料</string>
+ <string name="stats_other_recent_stats_label">其他近期統計資料</string>
+ <string name="themes_fetching">正在擷取佈景主題...</string>
+ <string name="ssl_certificate_details">詳細資料</string>
+ <string name="sure_to_remove_account">移除此網站?</string>
+ <string name="delete_sure_post">刪除此文章</string>
+ <string name="delete_sure">刪除此草稿</string>
+ <string name="delete_sure_page">刪除此頁面</string>
+ <string name="confirm_delete_multi_media">要刪除選取的項目嗎?</string>
+ <string name="confirm_delete_media">要刪除選取的項目嗎?</string>
+ <string name="cab_selected">已選取:%d</string>
+ <string name="media_gallery_date_range">顯示自 %1$s 到 %2$s 的媒體</string>
+ <string name="reader_empty_posts_liked">你未對任何文章按讚</string>
+ <string name="faq_button">常見問題集</string>
+ <string name="browse_our_faq_button">瀏覽常見問題集</string>
+ <string name="nux_help_description">造訪說明中心以取得常見問題的解答,或前往論壇提出新問題</string>
+ <string name="agree_terms_of_service">建立帳號即表示你同意超棒的%1$s服務條款%2$s</string>
+ <string name="create_new_blog_wpcom">建立 WordPress.com 網誌</string>
+ <string name="new_blog_wpcom_created">WordPress.com 網誌已建立!</string>
+ <string name="reader_empty_comments">尚未有留言</string>
+ <string name="reader_empty_posts_in_tag">沒有任何含有此標籤的文章</string>
+ <string name="reader_label_comment_count_multi">%,d 則留言</string>
+ <string name="reader_label_view_original">查看原始文章</string>
+ <string name="reader_label_like">按讚</string>
+ <string name="reader_label_comment_count_single">一則留言</string>
+ <string name="reader_label_comments_closed">留言已關閉</string>
+ <string name="reader_label_comments_on">以下內容的留言:</string>
+ <string name="reader_title_photo_viewer">第 %1$d 個,共 %2$d 個</string>
+ <string name="error_publish_empty_post">無法發表空白的文章</string>
+ <string name="error_refresh_unauthorized_posts">你沒有查看或編輯文章的權限</string>
+ <string name="error_refresh_unauthorized_pages">你沒有查看或編輯頁面的權限</string>
+ <string name="error_refresh_unauthorized_comments">你沒有查看或編輯留言的權限</string>
+ <string name="older_month">超過一個月</string>
+ <string name="more">更多</string>
+ <string name="older_two_days">超過 2 天</string>
+ <string name="older_last_week">超過一週</string>
+ <string name="stats_no_blog">無法為所需網誌載入統計</string>
+ <string name="select_a_blog">選取 WordPress 網站</string>
+ <string name="sending_content">正在上傳 %s 內容</string>
+ <string name="uploading_total">正在上傳第 %1$d 個,共 %2$d 個</string>
+ <string name="mnu_comment_liked">已按讚</string>
+ <string name="comment">留言</string>
+ <string name="comment_trashed">留言已移至垃圾桶</string>
+ <string name="posts_empty_list">尚無文章。建立文章?</string>
+ <string name="comment_reply_to_user">回覆 %s</string>
+ <string name="pages_empty_list">尚無任何頁面。建立頁面?</string>
+ <string name="media_empty_list_custom_date">此時間間隔內沒有媒體</string>
+ <string name="posting_post">正在發表「%s」</string>
+ <string name="signing_out">正在登出…</string>
+ <string name="reader_empty_followed_blogs_title">你尚未關注任何網站</string>
+ <string name="reader_toast_err_generic">無法執行此動作</string>
+ <string name="reader_toast_err_block_blog">無法封鎖此網誌</string>
+ <string name="reader_toast_blog_blocked">將不再顯示此網誌的文章</string>
+ <string name="reader_menu_block_blog">封鎖此網誌</string>
+ <string name="contact_us">聯絡我們</string>
+ <string name="hs__conversation_detail_error">描述你遇到的問題</string>
+ <string name="hs__new_conversation_header">支援交談</string>
+ <string name="hs__conversation_header">支援交談</string>
+ <string name="hs__username_blank_error">輸入有效的名稱</string>
+ <string name="hs__invalid_email_error">輸入有效的電子郵件地址</string>
+ <string name="add_location">新增位置</string>
+ <string name="current_location">目前的位置</string>
+ <string name="search_location">搜尋</string>
+ <string name="edit_location">編輯</string>
+ <string name="search_current_location">尋找</string>
+ <string name="preference_send_usage_stats">傳送統計資料</string>
+ <string name="preference_send_usage_stats_summary">自動傳送使用統計資料,協助我們改善 Android 版 WordPress</string>
+ <string name="update_verb">更新</string>
+ <string name="schedule_verb">排程</string>
+ <string name="reader_title_subs">標籤和網誌</string>
+ <string name="reader_page_followed_tags">關注的標籤</string>
+ <string name="reader_label_followed_blog">網誌已關注</string>
+ <string name="reader_label_tag_preview">已下標籤的文章 %s</string>
+ <string name="reader_toast_err_get_blog_info">無法顯示此網誌</string>
+ <string name="reader_toast_err_already_follow_blog">你已經在關注此網誌</string>
+ <string name="reader_toast_err_follow_blog">無法關注此網誌</string>
+ <string name="reader_toast_err_unfollow_blog">無法取消關注此網誌</string>
+ <string name="reader_empty_recommended_blogs">沒有推薦的網誌</string>
+ <string name="reader_title_blog_preview">讀者網誌</string>
+ <string name="reader_title_tag_preview">讀者標籤</string>
+ <string name="reader_page_followed_blogs">關注的網站</string>
+ <string name="reader_hint_add_tag_or_url">輸入要關注的 URL 或標籤</string>
+ <string name="saving">儲存中…</string>
+ <string name="media_empty_list">沒有媒體</string>
+ <string name="ptr_tip_message">秘訣:下拉即可重新整理</string>
+ <string name="help">說明</string>
+ <string name="forgot_password">忘記密碼?</string>
+ <string name="forums">論壇</string>
+ <string name="help_center">說明中心</string>
+ <string name="ssl_certificate_error">無效的 SSL 憑證</string>
+ <string name="ssl_certificate_ask_trust">如果你通常能夠順利連線至此網站,則此錯誤可能代表有人正嘗試冒充該網站,因此請勿繼續操作。是否仍然要信任憑證?</string>
+ <string name="could_not_remove_account">無法移除網站</string>
+ <string name="out_of_memory">裝置記憶體不足</string>
+ <string name="no_network_message">沒有可用的網路</string>
+ <string name="gallery_error">無法擷取媒體項目</string>
+ <string name="blog_not_found">存取此網誌時發生錯誤</string>
+ <string name="wait_until_upload_completes">請等候上傳完成</string>
+ <string name="theme_fetch_failed">無法擷取佈景主題</string>
+ <string name="theme_set_failed">無法設定佈景主題</string>
+ <string name="theme_auth_error_message">確認你有設定佈景主題的權限</string>
+ <string name="comments_empty_list">無回應</string>
+ <string name="mnu_comment_unspam">非垃圾</string>
+ <string name="no_site_error">無法連線至 WordPress 網站</string>
+ <string name="adding_cat_failed">新增類別失敗</string>
+ <string name="adding_cat_success">類別已成功新增</string>
+ <string name="cat_name_required">類別名稱欄位為必填</string>
+ <string name="category_automatically_renamed">類別名稱 %1$s 無效。已將它重新命名為 %2$s。</string>
+ <string name="no_account">未找到 WordPress 帳號,請新增帳號然後重試一次</string>
+ <string name="sdcard_message">需要掛接 SD 卡以上傳媒體</string>
+ <string name="stats_empty_comments">尚無回應</string>
+ <string name="stats_bar_graph_empty">沒有可用的統計</string>
+ <string name="reply_failed">回覆失敗</string>
+ <string name="notifications_empty_list">沒有通知</string>
+ <string name="error_delete_post">刪除 %s 時發生錯誤</string>
+ <string name="error_refresh_posts">目前無法重新整理文章</string>
+ <string name="error_refresh_pages">目前無法重新整理頁面</string>
+ <string name="error_refresh_notifications">目前無法重新整理通知</string>
+ <string name="error_refresh_comments">目前無法重新整理回應</string>
+ <string name="error_refresh_stats">目前無法重新整理統計</string>
+ <string name="error_generic">發生錯誤</string>
+ <string name="error_moderate_comment">審核時發生錯誤</string>
+ <string name="error_edit_comment">編輯回應時發生錯誤</string>
+ <string name="error_upload">上傳 %s 時發生錯誤</string>
+ <string name="error_load_comment">無法載入回應</string>
+ <string name="error_downloading_image">下載圖片時發生錯誤</string>
+ <string name="passcode_wrong_passcode">錯誤的 PIN</string>
+ <string name="invalid_email_message">你的電子郵件地址無效</string>
+ <string name="invalid_password_message">密碼必須包含至少 4 個字元</string>
+ <string name="invalid_username_too_short">使用者名稱長度必須超過 4 個字元</string>
+ <string name="invalid_username_too_long">使用者名稱長度必須小於 61 個字元</string>
+ <string name="username_only_lowercase_letters_and_numbers">使用者名稱只能包含小寫字母 (a-z) 和數字</string>
+ <string name="username_required">輸入使用者名稱</string>
+ <string name="username_not_allowed">不允許使用該使用者名稱</string>
+ <string name="username_must_be_at_least_four_characters">使用者名稱必須至少為 4 個字元</string>
+ <string name="username_contains_invalid_characters">使用者名稱不能包含字元「_」</string>
+ <string name="username_must_include_letters">使用者名稱必須至少有 1 個字母 (a-z)</string>
+ <string name="email_invalid">輸入有效的電子郵件地址</string>
+ <string name="email_not_allowed">不允許使用該電子郵件地址</string>
+ <string name="username_exists">該使用者名稱已存在</string>
+ <string name="email_exists">該電子郵件地址已使用</string>
+ <string name="username_reserved_but_may_be_available">該使用者名稱目前已被預定,但可能幾天後便可供使用</string>
+ <string name="blog_name_required">輸入網站位址</string>
+ <string name="blog_name_not_allowed">不允許使用該網站位址</string>
+ <string name="blog_name_must_be_at_least_four_characters">網站位址至少必須為 4 個字元</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">網站位址長度必須小於 64 個字元</string>
+ <string name="blog_name_contains_invalid_characters">網站位址不能包含字元「_」</string>
+ <string name="blog_name_cant_be_used">你不可使用該網站位址</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">網站位址只能包含小寫字母 (a-z) 和數字</string>
+ <string name="blog_name_exists">該網站已存在</string>
+ <string name="blog_name_reserved">該網站已被預定</string>
+ <string name="blog_name_reserved_but_may_be_available">該網站目前已被預定,但可能幾天後便可供使用</string>
+ <string name="username_or_password_incorrect">你輸入的使用者名稱或密碼不正確</string>
+ <string name="nux_cannot_log_in">我們無法將你登入</string>
+ <string name="invalid_url_message">檢查輸入的 URL 是否有效</string>
+ <string name="blog_removed_successfully">已成功移除網站</string>
+ <string name="remove_account">移除網站</string>
+ <string name="xmlrpc_error">無法連線。在你的網站上輸入 xmlrpc.php 的完整路徑,然後再試一次。</string>
+ <string name="select_categories">選取類別</string>
+ <string name="account_details">帳號詳細資料</string>
+ <string name="edit_post">編輯文章</string>
+ <string name="add_comment">新增回應</string>
+ <string name="connection_error">連線錯誤</string>
+ <string name="cancel_edit">取消編輯</string>
+ <string name="scaled_image_error">輸入有效縮放的寬度值</string>
+ <string name="post_not_found">載入文章時發生錯誤。重新整理你的文章,然後再試一次。</string>
+ <string name="learn_more">瞭解更多</string>
+ <string name="media_gallery_settings_title">藝廊設定</string>
+ <string name="media_gallery_image_order">圖片順序</string>
+ <string name="media_gallery_num_columns">欄位數</string>
+ <string name="media_gallery_type_thumbnail_grid">縮圖格線</string>
+ <string name="media_gallery_edit">編輯藝廊</string>
+ <string name="media_error_no_permission">你沒有檢視媒體庫的權限</string>
+ <string name="cannot_delete_multi_media_items">目前無法刪除部分媒體。請稍後再試一次。</string>
+ <string name="themes_live_preview">即時預覽</string>
+ <string name="theme_current_theme">目前佈景主題</string>
+ <string name="theme_premium_theme">進階佈景主題</string>
+ <string name="link_enter_url_text">連結文字 (選填)</string>
+ <string name="create_a_link">建立連結</string>
+ <string name="page_settings">頁面設定</string>
+ <string name="local_draft">本機草稿</string>
+ <string name="upload_failed">上傳失敗</string>
+ <string name="horizontal_alignment">水平對齊</string>
+ <string name="file_not_found">找不到可上傳的媒體檔案。該檔案是否已刪除或移走?</string>
+ <string name="post_settings">文章設定</string>
+ <string name="delete_post">刪除文章</string>
+ <string name="delete_page">刪除頁面</string>
+ <string name="comment_status_approved">已核准</string>
+ <string name="comment_status_unapproved">審核中</string>
+ <string name="comment_status_spam">垃圾</string>
+ <string name="comment_status_trash">已移至垃圾桶</string>
+ <string name="edit_comment">編輯回應</string>
+ <string name="mnu_comment_approve">核准</string>
+ <string name="mnu_comment_unapprove">駁回</string>
+ <string name="mnu_comment_spam">垃圾</string>
+ <string name="mnu_comment_trash">垃圾桶</string>
+ <string name="dlg_approving_comments">核准中</string>
+ <string name="dlg_unapproving_comments">駁回中</string>
+ <string name="dlg_spamming_comments">標記為垃圾</string>
+ <string name="dlg_trashing_comments">正在移至垃圾桶</string>
+ <string name="dlg_confirm_trash_comments">移至垃圾桶?</string>
+ <string name="trash_yes">垃圾桶</string>
+ <string name="trash_no">不要移至垃圾桶</string>
+ <string name="trash">垃圾桶</string>
+ <string name="author_name">作者名稱</string>
+ <string name="author_email">作者電子郵件</string>
+ <string name="author_url">作者網址</string>
+ <string name="hint_comment_content">回應</string>
+ <string name="saving_changes">正在儲存變更</string>
+ <string name="sure_to_cancel_edit_comment">取消編輯此回應?</string>
+ <string name="content_required">必須發表回應</string>
+ <string name="toast_comment_unedited">回應尚未變更</string>
+ <string name="delete_draft">刪除草稿</string>
+ <string name="preview_page">預覽頁面</string>
+ <string name="preview_post">預覽文章</string>
+ <string name="comment_added">回應已成功新增</string>
+ <string name="post_not_published">文章狀態未發佈</string>
+ <string name="page_not_published">頁面狀態未發佈</string>
+ <string name="view_in_browser">在瀏覽器中檢視</string>
+ <string name="add_new_category">新增類別</string>
+ <string name="category_name">類別名稱</string>
+ <string name="category_slug">類別文章網址代稱 (選填)</string>
+ <string name="category_desc">類別描述 (選填)</string>
+ <string name="category_parent">類別上層 (選填)</string>
+ <string name="share_action_post">新文章</string>
+ <string name="share_action_media">媒體庫</string>
+ <string name="file_error_create">無法建立暫存檔進行媒體上傳。確認裝置上有足夠的可用空間。</string>
+ <string name="location_not_found">未知的位置</string>
+ <string name="open_source_licenses">開啟來源授權</string>
+ <string name="pending_review">待審中</string>
+ <string name="http_credentials">HTTP 憑證 (選填)</string>
+ <string name="http_authorization_required">需要授權</string>
+ <string name="post_format">文章格式</string>
+ <string name="new_post">新文章</string>
+ <string name="new_media">新媒體</string>
+ <string name="view_site">檢視網站</string>
+ <string name="privacy_policy">隱私權政策</string>
+ <string name="local_changes">本機變更</string>
+ <string name="image_settings">圖片設定</string>
+ <string name="add_account_blog_url">網誌位址</string>
+ <string name="wordpress_blog">WordPress 網誌</string>
+ <string name="error_blog_hidden">此網誌已隱藏,無法載入。請在設定中啟用網誌,然後再試一次。</string>
+ <string name="fatal_db_error">建立應用程式資料庫時發生錯誤。請嘗試重新安裝應用程式。</string>
+ <string name="jetpack_message_not_admin">統計需要 Jetpack 外掛程式。請聯絡網站管理員。</string>
+ <string name="reader_title_applog">應用程式記錄檔</string>
+ <string name="reader_share_link">分享連結</string>
+ <string name="reader_toast_err_add_tag">無法新增此標籤</string>
+ <string name="reader_toast_err_remove_tag">無法移除此標籤</string>
+ <string name="required_field">必填欄位</string>
+ <string name="email_hint">電子郵件地址</string>
+ <string name="site_address">你自助託管的位址 (網址)</string>
+ <string name="email_cant_be_used_to_signup">你無法使用該電子郵件地址來註冊。我們無法使用這些電子郵件地址,因為它們會阻擋我們的某些郵件。請使用其他電子郵件供應商。</string>
+ <string name="email_reserved">已有人使用該電子郵件地址。請查看你的收件匣是否有啟用郵件。如果你現在不啟用,可以幾天後再試一次。</string>
+ <string name="blog_name_must_include_letters">網站位址必須至少有 1 個字母 (a-z)</string>
+ <string name="blog_name_invalid">無效的網站位址</string>
+ <string name="blog_title_invalid">無效的網站標題</string>
+ <string name="notifications_empty_all">尚未有任何通知。</string>
+ <string name="invalid_site_url_message">檢查輸入的網站 URL 是否有效</string>
+ <string name="deleting_page">正在刪除頁面</string>
+ <string name="deleting_post">正在刪除文章</string>
+ <string name="share_url_post">分享文章</string>
+ <string name="share_url_page">分享頁面</string>
+ <string name="share_link">分享連結</string>
+ <string name="creating_your_account">建立你的帳號</string>
+ <string name="creating_your_site">建立你的網站</string>
+ <string name="reader_empty_posts_in_tag_updating">正在擷取文章…</string>
+ <string name="error_refresh_media">重新整理媒體櫃時發生某種錯誤。請稍後再試一次。</string>
+ <string name="reader_likes_you_and_multi">你和其他 %,d 個人都說這個讚</string>
+ <string name="reader_likes_multi">%,d 人說這個讚</string>
+ <string name="reader_toast_err_get_comment">無法擷取此回應</string>
+ <string name="reader_label_reply">回覆</string>
+ <string name="video">影片</string>
+ <string name="download">正在下載媒體</string>
+ <string name="cant_share_no_visible_blog">你無法在沒有可見網誌的情形下分享至 WordPress</string>
+ <string name="comment_spammed">已將留言標記為垃圾</string>
+ <string name="select_time">選取時間</string>
+ <string name="reader_likes_you_and_one">你和其他 1 人都說這個讚</string>
+ <string name="select_date">選取日期</string>
+ <string name="pick_photo">選取照片</string>
+ <string name="account_two_step_auth_enabled">此帳號已啟用兩步驟驗證。瀏覽你的 WordPress.com 安全性設定,並產生一組應用程式專用密碼。</string>
+ <string name="pick_video">選取影片</string>
+ <string name="reader_toast_err_get_post">無法擷取此文章</string>
+ <string name="validating_user_data">正在驗證使用者資料</string>
+ <string name="validating_site_data">正在驗證網站資料</string>
+ <string name="reader_empty_followed_blogs_description">不過別擔心,只要點選右上方的圖示即可開始探索!</string>
+ <string name="password_invalid">你需要一組更安全的密碼。請務必使用 7 個或更多字元,並混合使用大小寫字母、數字或特殊字元。</string>
+ <string name="nux_tap_continue">繼續</string>
+ <string name="nux_welcome_create_account">建立帳號</string>
+ <string name="nux_add_selfhosted_blog">新增自助託管的網站</string>
+ <string name="nux_oops_not_selfhosted_blog">登入 WordPress.com</string>
+ <string name="signing_in">正在登入…</string>
+ <string name="media_add_popup_title">增加媒體庫</string>
+ <string name="media_add_new_media_gallery">創建藝廊</string>
+ <string name="empty_list_default">清單是空的</string>
+ <string name="select_from_media_library">從媒體庫選擇</string>
+ <string name="jetpack_not_found">找不到 Jetpack</string>
+ <string name="jetpack_message">Jetpack 外掛需要統計資訊。你想要安裝 Jetpack 嗎?</string>
+ <string name="reader_untitled_post">(未命名)</string>
+ <string name="reader_share_subject">從 %s 分享</string>
+ <string name="reader_btn_unfollow">追蹤</string>
+ <string name="reader_label_added_tag">新增 %s</string>
+ <string name="reader_label_removed_tag">移除 %s</string>
+ <string name="reader_btn_share">分享</string>
+ <string name="reader_btn_follow">追蹤</string>
+ <string name="reader_toast_err_url_intent">無法開啟 %s</string>
+ <string name="connecting_wpcom">連結至 WordPress.com</string>
+ <string name="username_invalid">無效的使用者名稱</string>
+ <string name="reader_likes_one">1 人說這個讚</string>
+ <string name="reader_likes_only_you">你說這個讚</string>
+ <string name="reader_toast_err_comment_failed">無法張貼你的回應</string>
+ <string name="reader_toast_err_tag_exists">你已經在關注此標籤</string>
+ <string name="reader_toast_err_tag_invalid">這並非有效的標籤</string>
+ <string name="reader_toast_err_share_intent">無法分享</string>
+ <string name="reader_toast_err_view_image">無法檢視圖片</string>
+ <string name="limit_reached">已達限制。你可以在 1 分鐘後再試一次。如果在 1 分鐘不到之前重試,只會延長你必須等候繼續進行的時間。如果你覺得這是錯誤,請聯絡支援團隊。</string>
+ <string name="nux_tutorial_get_started_title">開始使用!</string>
+ <string name="reader_empty_followed_tags">你未關注任何標籤</string>
+ <string name="create_account_wpcom">在 WordPress.com 上建立帳號</string>
+ <string name="reader_hint_comment_on_comment">回應評論</string>
+ <string name="button_next">下一步</string>
+ <string name="themes">佈景主題</string>
+ <string name="all">全部</string>
+ <string name="images">圖片</string>
+ <string name="unattached">未附加</string>
+ <string name="custom_date">自訂日期</string>
+ <string name="media_add_popup_capture_photo">拍照</string>
+ <string name="media_add_popup_capture_video">錄影</string>
+ <string name="media_gallery_type_slideshow">幻燈片</string>
+ <string name="media_gallery_image_order_random">隨機</string>
+ <string name="media_edit_description_text">說明</string>
+ <string name="media_edit_title_hint">在這裡輸入標題</string>
+ <string name="theme_set_success">成功設定佈景主題</string>
+ <string name="theme_auth_error_title">抓取佈景主題失敗</string>
+ <string name="media_gallery_image_order_reverse">反轉順序</string>
+ <string name="media_gallery_type">型態</string>
+ <string name="media_edit_title_text">標題</string>
+ <string name="media_edit_caption_text">副標題</string>
+ <string name="media_edit_caption_hint">在這裡輸入副標題</string>
+ <string name="share_action_title">加入到</string>
+ <string name="media_gallery_type_squares">方塊</string>
+ <string name="media_gallery_type_tiled">磁磚</string>
+ <string name="media_gallery_type_circles">圓形</string>
+ <string name="media_edit_description_hint">請在這輸入簡述</string>
+ <string name="media_edit_success">更新</string>
+ <string name="media_edit_failure">更新失敗</string>
+ <string name="themes_details_label">細節</string>
+ <string name="stats_view_tags_and_categories">標籤與分類</string>
+ <string name="themes_features_label">功能</string>
+ <string name="theme_activate_button">啟用</string>
+ <string name="theme_activating_button">啟用中</string>
+ <string name="post_excerpt">文章摘要</string>
+ <string name="share_action">分享</string>
+ <string name="stats">統計</string>
+ <string name="stats_view_visitors_and_views">訪客和點閱數</string>
+ <string name="stats_view_clicks">點擊率</string>
+ <string name="stats_view_referrers">來源網址</string>
+ <string name="stats_timeframe_today">今天</string>
+ <string name="stats_timeframe_yesterday">昨天</string>
+ <string name="stats_timeframe_days">天</string>
+ <string name="stats_timeframe_weeks">週</string>
+ <string name="stats_timeframe_months">月</string>
+ <string name="stats_entry_country">國家</string>
+ <string name="stats_entry_posts_and_pages">標題</string>
+ <string name="stats_entry_tags_and_categories">主題</string>
+ <string name="stats_entry_authors">作者</string>
+ <string name="stats_entry_referrers">來源網址</string>
+ <string name="stats_totals_views">點閱數</string>
+ <string name="stats_totals_clicks">點擊率</string>
+ <string name="stats_totals_plays">播放次數</string>
+ <string name="passcode_manage">管理 PIN 鎖定</string>
+ <string name="passcode_enter_passcode">輸入你的 PIN</string>
+ <string name="passcode_enter_old_passcode">輸入你的舊 PIN</string>
+ <string name="passcode_re_enter_passcode">重新輸入你的 PIN</string>
+ <string name="passcode_change_passcode">變更 PIN</string>
+ <string name="passcode_set">PIN 設定</string>
+ <string name="passcode_preference_title">PIN 鎖定</string>
+ <string name="passcode_turn_off">關閉 PIN 鎖定</string>
+ <string name="passcode_turn_on">開啟 PIN 鎖定</string>
+ <string name="upload">上傳</string>
+ <string name="discard">放棄</string>
+ <string name="sign_in">登入</string>
+ <string name="notifications">通知</string>
+ <string name="note_reply_successful">回覆已發佈</string>
+ <string name="new_notifications">%d 個新通知</string>
+ <string name="more_notifications">還有 %d 個。</string>
+ <string name="follows">關注者</string>
+ <string name="loading">載入中...</string>
+ <string name="httppassword">HTTP 密碼</string>
+ <string name="httpuser">HTTP 使用者帳號</string>
+ <string name="error_media_upload">上傳媒體時發生錯誤</string>
+ <string name="publish_date">發佈</string>
+ <string name="content_description_add_media">新增媒體</string>
+ <string name="post_content">內容(輕按以增加文字或媒體)</string>
+ <string name="incorrect_credentials">錯誤的使用者帳號或密碼。</string>
+ <string name="password">密碼</string>
+ <string name="username">使用者帳號</string>
+ <string name="reader">閱讀器</string>
+ <string name="pages">頁面</string>
+ <string name="caption">說明(可選)</string>
+ <string name="width">寬度</string>
+ <string name="posts">文章</string>
+ <string name="anonymous">匿名</string>
+ <string name="page">頁面</string>
+ <string name="post">文章</string>
+ <string name="featured">選為精選圖片</string>
+ <string name="featured_in_post">文章內容包含圖片</string>
+ <string name="no_network_title">沒有網路連線</string>
+ <string name="ok">OK</string>
+ <string name="blogusername">網誌使用者帳號</string>
+ <string name="upload_scaled_image">上傳並建立縮圖連結</string>
+ <string name="scaled_image">圖片縮放寬度</string>
+ <string name="scheduled">定時</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">上傳中…</string>
+ <string name="version">版本</string>
+ <string name="tos">服務條款</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">預設圖片寬度</string>
+ <string name="image_alignment">對齊</string>
+ <string name="refresh">更新</string>
+ <string name="untitled">無標題</string>
+ <string name="edit">編輯</string>
+ <string name="post_id">文章</string>
+ <string name="page_id">頁面</string>
+ <string name="post_password">密碼(非必要)</string>
+ <string name="immediately">立即</string>
+ <string name="quickpress_add_alert_title">設定捷徑名稱</string>
+ <string name="today">今天</string>
+ <string name="settings">設定</string>
+ <string name="share_url">分享網址</string>
+ <string name="quickpress_add_error">捷徑名稱不能為空</string>
+ <string name="quickpress_window_title">選擇用於 QuickPress 捷徑的網誌</string>
+ <string name="publish_post">發佈</string>
+ <string name="draft">草稿</string>
+ <string name="post_private">私密</string>
+ <string name="upload_full_size_image">上傳並連結到完整圖片</string>
+ <string name="title">標題</string>
+ <string name="tags_separate_with_commas">標籤(多個標籤請以英文逗號“,”隔開)</string>
+ <string name="categories">分類</string>
+ <string name="dlg_deleting_comments">刪除留言</string>
+ <string name="notification_blink">閃爍通知燈</string>
+ <string name="notification_vibrate">振動</string>
+ <string name="notification_sound">通知音效</string>
+ <string name="status">狀態</string>
+ <string name="location">位置</string>
+ <string name="sdcard_title">需要 SD 卡</string>
+ <string name="select_video">從媒體庫選擇影片</string>
+ <string name="media">媒體</string>
+ <string name="delete">刪除</string>
+ <string name="none">無</string>
+ <string name="blogs">部落格</string>
+ <string name="select_photo">從媒體庫選擇照片</string>
+ <string name="error">錯誤</string>
+ <string name="cancel">取消</string>
+ <string name="save">儲存</string>
+ <string name="add">新增</string>
+ <string name="category_refresh_error">分類更新錯誤</string>
+ <string name="on">於</string>
+ <string name="reply">回覆</string>
+ <string name="yes">是</string>
+ <string name="no">否</string>
+ <string name="preview">預覽</string>
+ <string name="notification_settings">通知設定</string>
+</resources>
diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 000000000..daea17395
--- /dev/null
+++ b/WordPress/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,1122 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="role_admin">管理員</string>
+ <string name="role_editor">編輯者</string>
+ <string name="role_author">作者</string>
+ <string name="role_contributor">撰寫人員</string>
+ <string name="role_follower">關注者</string>
+ <string name="role_viewer">瀏覽者</string>
+ <string name="error_post_my_profile_no_connection">無法連線,故無法儲存你的個人檔案</string>
+ <string name="alignment_none">無</string>
+ <string name="alignment_left">左</string>
+ <string name="alignment_right">右</string>
+ <string name="site_settings_list_editor_action_mode_title">已選取 %1$d</string>
+ <string name="viewer_remove_confirmation_message">如果移除這位瀏覽者,對方將無法造訪此網站。\n\n仍要移除這位瀏覽者嗎?</string>
+ <string name="follower_remove_confirmation_message">移除後,這位關注者如果沒有重新關注,就會停止收到此網站的通知。\n\n仍要移除這位關注者嗎?</string>
+ <string name="follower_subscribed_since">自 %1$s 開始</string>
+ <string name="reader_label_view_gallery">檢視圖庫</string>
+ <string name="error_remove_follower">無法移除關注者</string>
+ <string name="error_remove_viewer">無法移除瀏覽者</string>
+ <string name="error_fetch_email_followers_list">無法擷取網站電子郵件關注者</string>
+ <string name="error_fetch_followers_list">無法擷取網站關注者</string>
+ <string name="editor_failed_uploads_switch_html">部分媒體上傳失敗。在此狀態下,你無法\n切換至 HTML 模式。要移除所有上傳失敗的項目並繼續嗎?</string>
+ <string name="format_bar_description_html">HTML 模式</string>
+ <string name="visual_editor">視覺化編輯器</string>
+ <string name="image_thumbnail">縮圖</string>
+ <string name="format_bar_description_ul">未排序清單</string>
+ <string name="format_bar_description_ol">已排序清單</string>
+ <string name="format_bar_description_more">插入更多</string>
+ <string name="format_bar_description_media">插入媒體</string>
+ <string name="format_bar_description_strike">刪除線</string>
+ <string name="format_bar_description_quote">封鎖引文</string>
+ <string name="format_bar_description_link">插入連結</string>
+ <string name="format_bar_description_italic">斜體</string>
+ <string name="format_bar_description_underline">底線</string>
+ <string name="image_settings_save_toast">變更已儲存</string>
+ <string name="image_caption">說明</string>
+ <string name="image_alt_text">替代文字</string>
+ <string name="image_link_to">連結至</string>
+ <string name="image_width">寬度</string>
+ <string name="format_bar_description_bold">粗體</string>
+ <string name="image_settings_dismiss_dialog_title">要捨棄未儲存的變更嗎?</string>
+ <string name="stop_upload_dialog_title">停止上傳?</string>
+ <string name="stop_upload_button">停止上傳</string>
+ <string name="alert_error_adding_media">插入媒體時發生錯誤</string>
+ <string name="alert_action_while_uploading">你目前正在上傳媒體。請等待此項作業完成。</string>
+ <string name="alert_insert_image_html_mode">無法直接在 HTML 模式中插入媒體。請切換回視覺化模式。</string>
+ <string name="uploading_gallery_placeholder">正在上傳圖庫...</string>
+ <string name="invite_error_for_username">%1$s:%2$s</string>
+ <string name="invite_sent">已成功送出邀請</string>
+ <string name="tap_to_try_again">點選以重試!</string>
+ <string name="invite_message_info">(選用) 你最多可以輸入 500 個字元的自訂訊息,附在傳送給使用者的邀請中。</string>
+ <string name="invite_message_remaining_other">剩下 %d 個字元</string>
+ <string name="invite_message_remaining_one">剩下 1 個字元</string>
+ <string name="invite_message_remaining_zero">剩下 0 個字元</string>
+ <string name="invite_invalid_email">電子郵件地址「%s」無效</string>
+ <string name="invite_message_title">自訂訊息</string>
+ <string name="invite_already_a_member">已有成員使用「%s」作為使用者名稱</string>
+ <string name="invite_username_not_found">找不到使用者名稱為「%s」的使用者</string>
+ <string name="invite">邀請</string>
+ <string name="invite_names_title">使用者名稱或電子郵件</string>
+ <string name="signup_succeed_signin_failed">系統已為你建立帳號,但在將你登入時發生錯誤\n。請嘗試使用新建立的使用者名稱與密碼登入。</string>
+ <string name="send_link">傳送連結</string>
+ <string name="my_site_header_external">外部</string>
+ <string name="invite_people">邀請他人</string>
+ <string name="label_clear_search_history">清除搜尋記錄</string>
+ <string name="dlg_confirm_clear_search_history">清除搜尋記錄?</string>
+ <string name="reader_empty_posts_in_search_description">無法以你的語言找到與「%s」相關的文章</string>
+ <string name="reader_label_post_search_running">搜尋中…</string>
+ <string name="reader_label_related_posts">相關選讀內容</string>
+ <string name="reader_empty_posts_in_search_title">找不到文章</string>
+ <string name="reader_label_post_search_explainer">搜尋所有公開的 WordPress.com 網誌</string>
+ <string name="reader_hint_post_search">搜尋 WordPress.com</string>
+ <string name="reader_title_related_post_detail">相關文章</string>
+ <string name="reader_title_search_results">搜尋「%s」</string>
+ <string name="preview_screen_links_disabled">預覽畫面上的連結已停用</string>
+ <string name="draft_explainer">這篇文章只是草稿,尚未發表</string>
+ <string name="send">傳送</string>
+ <string name="person_remove_confirmation_title">移除 %1$s</string>
+ <string name="reader_empty_posts_in_custom_list">此清單中的網站最近未張貼任何文章</string>
+ <string name="people">使用者</string>
+ <string name="edit_user">編輯使用者</string>
+ <string name="role">角色</string>
+ <string name="error_remove_user">無法移除使用者</string>
+ <string name="error_update_role">無法更新使用者角色</string>
+ <string name="gravatar_camera_and_media_permission_required">需要權限才能選取或拍攝照片</string>
+ <string name="error_updating_gravatar">更新你的 Gravatar 時發生錯誤</string>
+ <string name="error_locating_image">尋找裁切的圖片時發生錯誤</string>
+ <string name="error_refreshing_gravatar">重新載入你的 Gravatar 時發生錯誤</string>
+ <string name="gravatar_tip">新功能!點選 Gravatar 可加以變更!</string>
+ <string name="error_cropping_image">裁切圖片時發生錯誤</string>
+ <string name="launch_your_email_app">啟動你的電子郵件應用程式</string>
+ <string name="checking_email">正在檢查電子郵件</string>
+ <string name="not_on_wordpress_com">不在 WordPress.com 上嗎?</string>
+ <string name="magic_link_unavailable_error_message">目前無法使用。請輸入你的密碼</string>
+ <string name="check_your_email">檢查你的電子郵件</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">取得傳送至你電子郵件的連結以立即登入</string>
+ <string name="logging_in">正在登入</string>
+ <string name="enter_your_password_instead">改為輸入你的密碼</string>
+ <string name="web_address_dialog_hint">當你留言時公開顯示。</string>
+ <string name="jetpack_not_connected_message">已安裝 Jetpack 外掛程式,但未連結至 WordPress.com。是否要連結 Jetpack?</string>
+ <string name="username_email">電子郵件或使用者名稱</string>
+ <string name="jetpack_not_connected">Jetpack 外掛程式未連結</string>
+ <string name="new_editor_reflection_error">視覺化編輯器與你的裝置不相容,已\n自動停用。</string>
+ <string name="stats_insights_latest_post_no_title">(無標題)</string>
+ <string name="capture_or_pick_photo">拍攝或選取照片</string>
+ <string name="plans_post_purchase_text_themes">你現在可以無限使用進階版佈景主題。在你的網站上預覽任一佈景主題以開始使用。</string>
+ <string name="plans_post_purchase_button_themes">瀏覽佈景主題</string>
+ <string name="plans_post_purchase_title_themes">尋找最適合的進階版佈景主題</string>
+ <string name="plans_post_purchase_button_video">開始撰寫新文章</string>
+ <string name="plans_post_purchase_text_video">你可以使用 VideoPress 和擴充的媒體儲存空間上傳並託管影片。</string>
+ <string name="plans_post_purchase_title_video">使用影片讓文章生動有趣</string>
+ <string name="plans_post_purchase_button_customize">自訂我的網站</string>
+ <string name="plans_post_purchase_text_customize">你現在可以存取自訂字型、自訂顏色及自訂 CSS 編輯功能。</string>
+ <string name="plans_post_purchase_text_intro">你的網站簡直令人樂不可支!現在,你可以探索自己網站的新功能,並自行選擇要從哪裡開始。</string>
+ <string name="plans_post_purchase_title_customize">自訂字型和顏色</string>
+ <string name="plans_post_purchase_title_intro">交給你了,加油!</string>
+ <string name="export_your_content_message">我們會將你的文章、頁面和設定寄到你的電子郵件地址:%s.</string>
+ <string name="plan">方案</string>
+ <string name="plans">方案</string>
+ <string name="plans_loading_error">無法載入方案</string>
+ <string name="export_your_content">匯出你的內容</string>
+ <string name="exporting_content_progress">匯出內容...</string>
+ <string name="export_email_sent">匯出電子郵件已傳送!</string>
+ <string name="premium_upgrades_message">你的網站已啟用進階版升級服務。刪除網站前,請先取消升級。</string>
+ <string name="show_purchases">顯示購買項目</string>
+ <string name="checking_purchases">檢查購買項目</string>
+ <string name="premium_upgrades_title">進階版升級</string>
+ <string name="purchases_request_error">發生了點錯誤。無法要求購買項目。</string>
+ <string name="delete_site_progress">正在刪除網站...</string>
+ <string name="delete_site_summary">此動作無法復原。刪除網站將一併移除網站上的所有內容、參與者及網域。</string>
+ <string name="delete_site_hint">刪除網站</string>
+ <string name="export_site_hint">將網站匯出為 XML 檔案</string>
+ <string name="are_you_sure">你確定?</string>
+ <string name="export_site_summary">如果確定,現在請務必花點時間匯出你的內容。日後將無法還原內容。</string>
+ <string name="keep_your_content">保留你的內容</string>
+ <string name="domain_removal_hint">移除網站後便無法再運作的網域</string>
+ <string name="domain_removal_summary">注意!刪除網站會一併移除下方列出的網域。</string>
+ <string name="primary_domain">主要網域</string>
+ <string name="domain_removal">移除網域</string>
+ <string name="error_deleting_site_summary">刪除網站時發生錯誤。請聯絡支援團隊,尋求進一步協助</string>
+ <string name="error_deleting_site">刪除網站時發生錯誤</string>
+ <string name="confirm_delete_site_prompt">請在下方欄位中輸入 %1$s 以確認。系統將永久刪除你的網站。</string>
+ <string name="site_settings_export_content_title">匯出內容</string>
+ <string name="contact_support">聯絡支援團隊</string>
+ <string name="confirm_delete_site">確認刪除網站</string>
+ <string name="start_over_text">如果你想保留網站,但不想保留現有的文章和頁面,我們的支援團隊可以為你刪除網站上的文章、頁面、媒體及留言。\n\n這能讓你保留網站和 URL,但重新開始建立內容。若要清除目前的內容,請與我們聯絡。</string>
+ <string name="site_settings_start_over_hint">重新開始設計你的網站</string>
+ <string name="let_us_help">我們很樂意協助</string>
+ <string name="me_btn_app_settings">應用程式設定</string>
+ <string name="start_over">重新開始</string>
+ <string name="editor_remove_failed_uploads">移除失敗的上傳項目</string>
+ <string name="editor_toast_failed_uploads">部分媒體上傳失敗。在此狀態中,你無法儲存或發表\n自己的文章。是否要移除所有失敗的媒體?</string>
+ <string name="comments_empty_list_filtered_trashed">沒有已移至垃圾桶的留言</string>
+ <string name="site_settings_advanced_header">進階</string>
+ <string name="comments_empty_list_filtered_pending">沒有待審核的留言</string>
+ <string name="comments_empty_list_filtered_approved">沒有已核准的留言</string>
+ <string name="button_done">完成</string>
+ <string name="button_skip">略過</string>
+ <string name="site_timeout_error">由於發生逾時錯誤,導致無法連結到 WordPress 網站。</string>
+ <string name="xmlrpc_malformed_response_error">無法連結。WordPress 安裝以無效的 XML-RPC 文件回應。</string>
+ <string name="xmlrpc_missing_method_error">無法連結。伺服器缺少必要的 XML-RPC 方式。</string>
+ <string name="post_format_status">狀態</string>
+ <string name="post_format_video">影片</string>
+ <string name="theme_free">免費</string>
+ <string name="theme_all">全部</string>
+ <string name="theme_premium">進階版</string>
+ <string name="post_format_chat">聊天</string>
+ <string name="post_format_gallery">圖庫</string>
+ <string name="post_format_image">圖片</string>
+ <string name="post_format_link">連結</string>
+ <string name="post_format_quote">引文</string>
+ <string name="post_format_standard">標準版</string>
+ <string name="notif_events">WordPress.com 課程與活動相關資訊 (線上和專人洽詢)。</string>
+ <string name="post_format_aside">旁白</string>
+ <string name="post_format_audio">音訊</string>
+ <string name="notif_surveys">參與 WordPress.com 研究與調查的機會。</string>
+ <string name="notif_tips">讓 WordPress.com 發揮最大功效的秘訣。</string>
+ <string name="notif_community">社群</string>
+ <string name="replies_to_my_comments">留言的回覆</string>
+ <string name="notif_suggestions">建議</string>
+ <string name="notif_research">研究</string>
+ <string name="site_achievements">網站成就</string>
+ <string name="username_mentions">使用者名稱標記</string>
+ <string name="likes_on_my_posts">文章按讚數</string>
+ <string name="site_follows">網站關注數</string>
+ <string name="likes_on_my_comments">留言按讚數</string>
+ <string name="comments_on_my_site">網站留言</string>
+ <string name="site_settings_list_editor_summary_other">%d 個項目</string>
+ <string name="site_settings_list_editor_summary_one">1 個項目</string>
+ <string name="approve_auto_if_previously_approved">已知使用者的留言</string>
+ <string name="approve_auto">所有使用者</string>
+ <string name="approve_manual">無留言</string>
+ <string name="site_settings_paging_summary_other">每頁 %d 則留言</string>
+ <string name="site_settings_paging_summary_one">每頁 1 則留言</string>
+ <string name="site_settings_multiple_links_summary_other">需要審核超過 %d 個連結</string>
+ <string name="site_settings_multiple_links_summary_one">需要審核超過 1 個連結</string>
+ <string name="site_settings_multiple_links_summary_zero">需要審核超過 0 個連結</string>
+ <string name="detail_approve_auto">自動核准所有人的留言。</string>
+ <string name="detail_approve_auto_if_previously_approved">如果使用者有已審核的留言,則自動核准</string>
+ <string name="detail_approve_manual">需要人工審核所有人的留言。</string>
+ <string name="filter_trashed_posts">已移至垃圾桶</string>
+ <string name="days_quantity_one">1 天</string>
+ <string name="days_quantity_other">%d 天</string>
+ <string name="filter_published_posts">已發表</string>
+ <string name="filter_draft_posts">草稿</string>
+ <string name="filter_scheduled_posts">已排程</string>
+ <string name="pending_email_change_snackbar">按一下電子郵件 (收件者:%1$s) 中的驗證連結以確認您的新地址</string>
+ <string name="primary_site">主要網站</string>
+ <string name="web_address">網站位址</string>
+ <string name="editor_toast_uploading_please_wait">你目前正在上傳媒體。請等待此作業完成。</string>
+ <string name="error_refresh_comments_showing_older">目前無法重新整理留言 – 顯示較舊的留言</string>
+ <string name="editor_post_settings_set_featured_image">設定特色圖片</string>
+ <string name="editor_post_settings_featured_image">特色圖片</string>
+ <string name="new_editor_promo_desc">Android 版 WordPress 應用程式現已包含美觀的全新視覺化\n編輯器。建立新的文章,即可試用編輯器。</string>
+ <string name="new_editor_promo_title">全新編輯器</string>
+ <string name="new_editor_promo_button_label">太棒了,謝謝!</string>
+ <string name="visual_editor_enabled">視覺化編輯器已啟用</string>
+ <string name="editor_content_placeholder">在此分享你的故事...</string>
+ <string name="editor_page_title_placeholder">頁面標題</string>
+ <string name="editor_post_title_placeholder">文章標題</string>
+ <string name="email_address">電子郵件地址</string>
+ <string name="preference_show_visual_editor">顯示視覺化編輯器</string>
+ <string name="dlg_sure_to_delete_comments">永久刪除這些留言?</string>
+ <string name="preference_editor">編輯者</string>
+ <string name="dlg_sure_to_delete_comment">永久刪除此回應?</string>
+ <string name="mnu_comment_delete_permanently">刪除</string>
+ <string name="comment_deleted_permanently">留言已刪除</string>
+ <string name="mnu_comment_untrash">還原</string>
+ <string name="comments_empty_list_filtered_spam">無垃圾留言</string>
+ <string name="could_not_load_page">無法載入頁面</string>
+ <string name="comment_status_all">全部</string>
+ <string name="interface_language">介面語言</string>
+ <string name="off">關閉</string>
+ <string name="about_the_app">關於此應用程式</string>
+ <string name="error_post_account_settings">無法儲存你的帳號設定</string>
+ <string name="error_post_my_profile">無法儲存你的個人檔案</string>
+ <string name="error_fetch_account_settings">無法擷取你的帳號設定</string>
+ <string name="error_fetch_my_profile">無法擷取你的個人檔案</string>
+ <string name="stats_widget_promo_ok_btn_label">好,知道了</string>
+ <string name="stats_widget_promo_desc">將小工具新增至主畫面,輕鬆按一下即可存取統計資料。</string>
+ <string name="stats_widget_promo_title">主畫面統計資料小工具</string>
+ <string name="site_settings_unknown_language_code_error">無法辨識語言代碼</string>
+ <string name="site_settings_threading_dialog_description">允許留言以階層式嵌入留言串中。</string>
+ <string name="site_settings_threading_dialog_header">留言串最多</string>
+ <string name="remove">移除</string>
+ <string name="search">搜尋</string>
+ <string name="add_category">新增類別</string>
+ <string name="disabled">已停用</string>
+ <string name="site_settings_image_original_size">原始尺寸</string>
+ <string name="privacy_private">只有你和經核准的使用者可看見你的網站</string>
+ <string name="privacy_public_not_indexed">所有人都能看見你的網站,但網站會要求搜尋引擎不要加入索引</string>
+ <string name="privacy_public">所有人都能看見你的網站,且搜尋引擎可能會將網站加入索引</string>
+ <string name="about_me_hint">簡單介紹一下自己…</string>
+ <string name="public_display_name_hint">如果未設定,顯示名稱會預設為你的使用者名稱</string>
+ <string name="about_me">關於我</string>
+ <string name="public_display_name">公開顯示名稱</string>
+ <string name="my_profile">我的個人檔案</string>
+ <string name="first_name">名</string>
+ <string name="last_name">姓</string>
+ <string name="site_privacy_public_desc">允許搜尋引擎將這個網站加入索引</string>
+ <string name="site_privacy_hidden_desc">阻擋搜尋引擎將這個網站加入索引</string>
+ <string name="site_privacy_private_desc">我想要將我的網站設為私人網站,只有我選擇的使用者可看見</string>
+ <string name="cd_related_post_preview_image">相關文章預覽圖片</string>
+ <string name="error_post_remote_site_settings">無法儲存網站資訊</string>
+ <string name="error_fetch_remote_site_settings">無法擷取網站資訊</string>
+ <string name="error_media_upload_connection">上傳媒體時發生連線錯誤</string>
+ <string name="site_settings_disconnected_toast">已中斷連線,編輯功能已停用。</string>
+ <string name="site_settings_unsupported_version_error">不支援的 WordPress 版本</string>
+ <string name="site_settings_multiple_links_dialog_description">如果留言包含的連結超過此數量,留言就必須經過審核。</string>
+ <string name="site_settings_close_after_dialog_switch_text">自動關閉</string>
+ <string name="site_settings_close_after_dialog_description">自動關閉文章的留言功能。</string>
+ <string name="site_settings_paging_dialog_description">將留言串分為數頁。</string>
+ <string name="site_settings_paging_dialog_header">每頁留言數</string>
+ <string name="site_settings_close_after_dialog_title">關閉留言功能</string>
+ <string name="site_settings_blacklist_description">如果留言的內容、名稱、URL、電子郵件或 IP 位址中含有以下文字,該留言將標記為垃圾留言。你也可以輸入字詞的一部分,例如「press」會與「WordPress」相符。</string>
+ <string name="site_settings_hold_for_moderation_description">如果留言的內容、名稱、URL、電子郵件或 IP 位址中含有以下文字,就會被送進審核清單。你也可以輸入字詞的一部分,例如「press」會與「WordPress」相符。</string>
+ <string name="site_settings_list_editor_input_hint">輸入字詞或片語</string>
+ <string name="site_settings_list_editor_no_items_text">無項目</string>
+ <string name="site_settings_learn_more_caption">你可以針對不同的文章覆寫這些設定。</string>
+ <string name="site_settings_rp_preview3_site">在「升級」中</string>
+ <string name="site_settings_rp_preview3_title">升級重點:婚禮適用的 VideoPress</string>
+ <string name="site_settings_rp_preview2_site">在「應用程式」中</string>
+ <string name="site_settings_rp_preview2_title">Android 專用的 WordPress 應用程式已全面翻新</string>
+ <string name="site_settings_rp_preview1_site">在「行動」中</string>
+ <string name="site_settings_rp_preview1_title">有重大的 iPhone/iPad 更新可供使用</string>
+ <string name="site_settings_rp_show_images_title">顯示圖片</string>
+ <string name="site_settings_rp_show_header_title">顯示頁首</string>
+ <string name="site_settings_rp_switch_summary">相關文章會在你的文章下方顯示網站上的相關內容。</string>
+ <string name="site_settings_rp_switch_title">顯示相關文章</string>
+ <string name="site_settings_delete_site_hint">從應用程式移除你的網站資料</string>
+ <string name="site_settings_blacklist_hint">與篩選條件相符的留言將標示為垃圾留言</string>
+ <string name="site_settings_moderation_hold_hint">與篩選條件相符的留言將被送進審核清單</string>
+ <string name="site_settings_multiple_links_hint">忽略已知使用者的連結限制</string>
+ <string name="site_settings_whitelist_hint">先前有經核准的留言才可以發表留言</string>
+ <string name="site_settings_user_account_required_hint">已註冊且登入的使用者才可以發表留言</string>
+ <string name="site_settings_identity_required_hint">輸入名稱和電子郵件後才可以發表留言</string>
+ <string name="site_settings_manual_approval_hint">留言必須經人工核准</string>
+ <string name="site_settings_paging_hint">按照指定的區塊大小顯示留言</string>
+ <string name="site_settings_threading_hint">允許留言達到一定高度後以巢狀顯示</string>
+ <string name="site_settings_sort_by_hint">決定留言顯示順序</string>
+ <string name="site_settings_close_after_hint">不允許在指定的時間後發表留言</string>
+ <string name="site_settings_receive_pingbacks_hint">允許其他網誌在建立連結時傳送通知</string>
+ <string name="site_settings_send_pingbacks_hint">嘗試通知在文章中建立連結的所有網誌</string>
+ <string name="site_settings_allow_comments_hint">允許讀者發表留言</string>
+ <string name="site_settings_discussion_hint">檢視及變更你的網站討論設定</string>
+ <string name="site_settings_more_hint">檢視所有可變更的討論設定</string>
+ <string name="site_settings_related_posts_hint">在讀取器中顯示或隱藏相關文章</string>
+ <string name="site_settings_upload_and_link_image_hint">允許一律上傳完整大小的圖片</string>
+ <string name="site_settings_image_width_hint">將文章中的圖片調整為符合以下寬度</string>
+ <string name="site_settings_format_hint">設定新的文章格式</string>
+ <string name="site_settings_category_hint">設定新的文章類別</string>
+ <string name="site_settings_location_hint">自動將位置資料加到文章中</string>
+ <string name="site_settings_password_hint">變更你的密碼</string>
+ <string name="site_settings_username_hint">目前的使用者帳號</string>
+ <string name="site_settings_language_hint">此網誌主要使用的語言</string>
+ <string name="site_settings_privacy_hint">控制網站存取權限</string>
+ <string name="site_settings_address_hint">目前不支援變更網址</string>
+ <string name="site_settings_tagline_hint">使用簡短的說明或好記的字詞組合來描述你的網誌</string>
+ <string name="site_settings_title_hint">以簡單幾個字說明此網站主題</string>
+ <string name="site_settings_whitelist_known_summary">已知使用者的留言</string>
+ <string name="site_settings_whitelist_all_summary">所有使用者的留言</string>
+ <string name="site_settings_threading_summary">%d 層</string>
+ <string name="site_settings_privacy_private_summary">私密</string>
+ <string name="site_settings_privacy_hidden_summary">隱藏</string>
+ <string name="site_settings_delete_site_title">刪除網站</string>
+ <string name="site_settings_privacy_public_summary">公開</string>
+ <string name="site_settings_blacklist_title">黑名單</string>
+ <string name="site_settings_moderation_hold_title">等待審核</string>
+ <string name="site_settings_multiple_links_title">留言中的連結</string>
+ <string name="site_settings_whitelist_title">自動核准</string>
+ <string name="site_settings_threading_title">階層顯示</string>
+ <string name="site_settings_paging_title">分頁</string>
+ <string name="site_settings_sort_by_title">排序依據</string>
+ <string name="site_settings_account_required_title">使用者必須登入</string>
+ <string name="site_settings_identity_required_title">必須包含名稱和電子郵件</string>
+ <string name="site_settings_receive_pingbacks_title">接收引用通知</string>
+ <string name="site_settings_send_pingbacks_title">傳送引用通知</string>
+ <string name="site_settings_allow_comments_title">允許留言</string>
+ <string name="site_settings_default_format_title">預設格式</string>
+ <string name="site_settings_default_category_title">預設類別</string>
+ <string name="site_settings_location_title">啟用定位</string>
+ <string name="site_settings_address_title">位址</string>
+ <string name="site_settings_title_title">網站標題</string>
+ <string name="site_settings_tagline_title">標語</string>
+ <string name="site_settings_this_device_header">此裝置</string>
+ <string name="site_settings_discussion_new_posts_header">新文章預設設定</string>
+ <string name="site_settings_account_header">帳號</string>
+ <string name="site_settings_writing_header">撰寫</string>
+ <string name="newest_first">最新的在前</string>
+ <string name="site_settings_general_header">一般</string>
+ <string name="discussion">討論</string>
+ <string name="privacy">隱私</string>
+ <string name="related_posts">相關文章</string>
+ <string name="comments">留言</string>
+ <string name="close_after">以下時間後關閉</string>
+ <string name="oldest_first">最舊的在前</string>
+ <string name="media_error_no_permission_upload">你沒有權限,無法上傳媒體檔案至網站</string>
+ <string name="never">從未</string>
+ <string name="unknown">不明</string>
+ <string name="reader_err_get_post_not_found">此文章已不存在</string>
+ <string name="reader_err_get_post_not_authorized">你未獲授權無法檢視此文章</string>
+ <string name="reader_err_get_post_generic">無法擷取此文章</string>
+ <string name="blog_name_no_spaced_allowed">網站位址不可包含空格</string>
+ <string name="invalid_username_no_spaces">使用者名稱不可包含空格</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">您所關注的網站最近未發表任何文章</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">沒有近期文章</string>
+ <string name="media_details_copy_url_toast">URL 已複製到剪貼簿</string>
+ <string name="edit_media">編輯媒體</string>
+ <string name="media_details_copy_url">複製 URL</string>
+ <string name="media_details_label_date_uploaded">已上傳</string>
+ <string name="media_details_label_date_added">已新增</string>
+ <string name="selected_theme">選取的佈景主題</string>
+ <string name="could_not_load_theme">無法載入佈景主題</string>
+ <string name="theme_activation_error">發生了點錯誤。無法啟用佈景主題</string>
+ <string name="theme_by_author_prompt_append">作者:%1$s</string>
+ <string name="theme_prompt">感謝你選擇 %1$s</string>
+ <string name="theme_try_and_customize">試用與自訂</string>
+ <string name="theme_view">檢視</string>
+ <string name="theme_details">詳細資料</string>
+ <string name="theme_support">支援</string>
+ <string name="theme_done">完成</string>
+ <string name="theme_manage_site">管理網站</string>
+ <string name="title_activity_theme_support">佈景主題</string>
+ <string name="theme_activate">啟用</string>
+ <string name="date_range_start_date">開始日期</string>
+ <string name="date_range_end_date">結束日期</string>
+ <string name="current_theme">目前佈景主題</string>
+ <string name="customize">自訂</string>
+ <string name="details">詳細資料</string>
+ <string name="support">支援</string>
+ <string name="active">已啟用</string>
+ <string name="stats_referrers_spam_generic_error">操作期間發生錯誤。垃圾留言狀態並未變更。</string>
+ <string name="stats_referrers_marking_not_spam">標記為非垃圾留言</string>
+ <string name="stats_referrers_unspam">非垃圾留言</string>
+ <string name="stats_referrers_marking_spam">標記為垃圾留言</string>
+ <string name="theme_auth_error_authenticate">佈景主題擷取失敗:使用者授權失敗</string>
+ <string name="post_published">文章已發表</string>
+ <string name="page_published">頁面已發表</string>
+ <string name="post_updated">文章已更新</string>
+ <string name="page_updated">頁面已更新</string>
+ <string name="stats_referrers_spam">垃圾郵件</string>
+ <string name="theme_no_search_result_found">很抱歉,找不到佈景主題。</string>
+ <string name="media_file_name">檔案名稱:%s</string>
+ <string name="media_uploaded_on">上傳時間:%s</string>
+ <string name="media_dimensions">尺寸:%s</string>
+ <string name="upload_queued">已排進佇列</string>
+ <string name="media_file_type">檔案類型:%s</string>
+ <string name="reader_label_gap_marker">載入更多文章</string>
+ <string name="notifications_no_search_results">沒有網站符合「%s」</string>
+ <string name="search_sites">搜尋網站</string>
+ <string name="notifications_empty_view_reader">檢視讀取器</string>
+ <string name="unread">未讀</string>
+ <string name="notifications_empty_action_followers_likes">取得通知:在已閱讀的文章上留言。</string>
+ <string name="notifications_empty_action_comments">加入討論:在關注的網誌文章上留言。</string>
+ <string name="notifications_empty_action_unread">再度展開討論:撰寫新文章。</string>
+ <string name="notifications_empty_action_all">動起來吧!立即在你關注的網誌文章上留言。</string>
+ <string name="notifications_empty_likes">尚未有新的讚。</string>
+ <string name="notifications_empty_followers">尚未有新的關注者。</string>
+ <string name="notifications_empty_comments">尚未有新的留言。</string>
+ <string name="notifications_empty_unread">你已追上最新進度!</string>
+ <string name="stats_widget_error_jetpack_no_blogid">請先存取應用程式中的統計資料,稍後再試著新增小工具</string>
+ <string name="stats_widget_error_readd_widget">請移除小工具,然後再新增一次</string>
+ <string name="stats_widget_error_no_visible_blog">在未開放網誌的情況下無法存取統計資料</string>
+ <string name="stats_widget_error_no_permissions">你的 WordPress.com 帳號無法存取此網誌的統計資料</string>
+ <string name="stats_widget_error_no_account">請登入 WordPress</string>
+ <string name="stats_widget_error_generic">無法載入統計資料</string>
+ <string name="stats_widget_loading_data">正在載入資料…</string>
+ <string name="stats_widget_name_for_blog">%1$s 的本日統計資料</string>
+ <string name="stats_widget_name">WordPress 本日統計</string>
+ <string name="add_location_permission_required">需要擁有權限才能新增位置</string>
+ <string name="add_media_permission_required">需要擁有權限才能新增媒體</string>
+ <string name="access_media_permission_required">需要擁有權限才能存取媒體</string>
+ <string name="stats_enable_rest_api_in_jetpack">若要查看統計資料,請啟用 Jetpack 的 JSON API 模組。</string>
+ <string name="error_open_list_from_notification">此文章或頁面已發表於其他網站</string>
+ <string name="reader_short_comment_count_multi">%s評論</string>
+ <string name="reader_short_comment_count_one">1評論</string>
+ <string name="reader_label_submit_comment">發送</string>
+ <string name="reader_hint_comment_on_post">回复文章...</string>
+ <string name="reader_discover_visit_blog">造訪 %s</string>
+ <string name="reader_discover_attribution_blog">原始發表位置:%s</string>
+ <string name="reader_discover_attribution_author">原始作者:%s</string>
+ <string name="reader_discover_attribution_author_and_blog">原始作者:%1$s,發表位置:%2$s</string>
+ <string name="reader_short_like_count_multi">%s 個讚</string>
+ <string name="reader_short_like_count_one">1 個讚</string>
+ <string name="reader_label_follow_count">%,d 個關注者</string>
+ <string name="reader_short_like_count_none">讚</string>
+ <string name="reader_menu_tags">編輯標籤和網誌</string>
+ <string name="reader_title_post_detail">讀者文章</string>
+ <string name="local_draft_explainer">這篇文章是儲存在本機上的草稿,尚未發表</string>
+ <string name="local_changes_explainer">這篇文章的本機存檔經過變更,變更內容尚未發表</string>
+ <string name="notifications_push_summary">在裝置上顯示通知的相關設定。</string>
+ <string name="notifications_email_summary">將通知傳送至帳號所繫結之電子郵件地址的相關設定。</string>
+ <string name="notifications_tab_summary">在通知索引標籤中顯示通知的相關設定。</string>
+ <string name="notifications_disabled">應用程式通知已停用。點選這裡,即可在「設定」中啟用通知。</string>
+ <string name="notification_types">通知類型</string>
+ <string name="error_loading_notifications">無法載入通知設定</string>
+ <string name="replies_to_your_comments">留言的回覆</string>
+ <string name="comment_likes">按讚的留言</string>
+ <string name="app_notifications">應用程式通知</string>
+ <string name="notifications_tab">通知索引標籤</string>
+ <string name="email">電子郵件</string>
+ <string name="notifications_comments_other_blogs">其他網站上的留言</string>
+ <string name="notifications_wpcom_updates">WordPress.com 消息更新</string>
+ <string name="notifications_other">其他</string>
+ <string name="notifications_account_emails">WordPress.com 寄送的電子郵件</string>
+ <string name="notifications_account_emails_summary">我們會隨時傳送與帳號資訊相關的重要電子郵件,除此之外,你也能獲得實用的額外內容。</string>
+ <string name="notifications_sights_and_sounds">提示與音效</string>
+ <string name="your_sites">你的網站</string>
+ <string name="stats_insights_latest_post_trend">自從 %2$s 發表以來已經過了 %1$s。文章截至目前為止獲得的迴響如下…</string>
+ <string name="stats_insights_latest_post_summary">最新文章摘要</string>
+ <string name="button_revert">還原</string>
+ <string name="days_ago">%d 天前</string>
+ <string name="yesterday">昨天</string>
+ <string name="connectionbar_no_connection">沒有連線</string>
+ <string name="page_trashed">頁面已移至垃圾桶</string>
+ <string name="post_deleted">文章已刪除</string>
+ <string name="post_trashed">文章已移至垃圾桶</string>
+ <string name="stats_no_activity_this_period">此期間沒有任何活動</string>
+ <string name="trashed">已移至垃圾桶</string>
+ <string name="button_back">返回</string>
+ <string name="page_deleted">頁面已刪除</string>
+ <string name="button_stats">統計</string>
+ <string name="button_trash">垃圾桶</string>
+ <string name="button_preview">預覽</string>
+ <string name="button_view">檢視</string>
+ <string name="button_edit">編輯</string>
+ <string name="button_publish">發表</string>
+ <string name="my_site_no_sites_view_subtitle">你希望加入一個嗎?</string>
+ <string name="my_site_no_sites_view_title">你還沒有任何 WordPress 網站。</string>
+ <string name="my_site_no_sites_view_drake">圖示</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">你沒有存取此網誌的權限</string>
+ <string name="reader_toast_err_follow_blog_not_found">無法找到這個部落格</string>
+ <string name="undo">復原</string>
+ <string name="tabbar_accessibility_label_my_site">我的網站</string>
+ <string name="tabbar_accessibility_label_me">我</string>
+ <string name="passcodelock_prompt_message">輸入 PIN 碼</string>
+ <string name="editor_toast_changes_saved">變更已儲存</string>
+ <string name="push_auth_expired">要求已到期。登入 WordPress.com 再試一次。</string>
+ <string name="stats_insights_best_ever">最佳的瀏覽次數</string>
+ <string name="ignore">忽略</string>
+ <string name="stats_insights_most_popular_percent_views">瀏覽次數:%1$d%%</string>
+ <string name="stats_insights_most_popular_hour">最熱門的時段</string>
+ <string name="stats_insights_most_popular_day">最熱門的日子</string>
+ <string name="stats_insights_popular">最熱門的日子及時段</string>
+ <string name="stats_insights_today">本日統計</string>
+ <string name="stats_insights_all_time">有史以來的文章數、瀏覽次數與訪客數</string>
+ <string name="stats_insights">Insights</string>
+ <string name="stats_sign_in_jetpack_different_com_account">若要檢視統計資料,請使用連線 Jetpack 的帳號登入 WordPress.com。</string>
+ <string name="stats_other_recent_stats_moved_label">正在尋找其他近期統計資料嗎?我們已將統計資料移至「Insights」頁面。</string>
+ <string name="me_disconnect_from_wordpress_com">中斷與 WordPress.com 的連線</string>
+ <string name="me_connect_to_wordpress_com">連線 WordPress.com</string>
+ <string name="me_btn_login_logout">登入/登出</string>
+ <string name="account_settings">帳號設定</string>
+ <string name="me_btn_support">說明與客戶服務</string>
+ <string name="site_picker_cant_hide_current_site">由於「%s」是目前的網站,因此並未隱藏</string>
+ <string name="site_picker_create_dotcom">建立 WordPress.com 網站</string>
+ <string name="site_picker_add_site">新增網站</string>
+ <string name="site_picker_add_self_hosted">新增自助託管的網站</string>
+ <string name="site_picker_edit_visibility">顯示/隱藏網站</string>
+ <string name="my_site_btn_view_admin">檢視管理員</string>
+ <string name="my_site_btn_view_site">檢視網站</string>
+ <string name="site_picker_title">選擇網站</string>
+ <string name="my_site_btn_switch_site">切換網站</string>
+ <string name="my_site_btn_blog_posts">網誌文章</string>
+ <string name="my_site_btn_site_settings">設定</string>
+ <string name="my_site_header_look_and_feel">外觀和風格</string>
+ <string name="my_site_header_publish">發表</string>
+ <string name="my_site_header_configuration">組態</string>
+ <string name="reader_label_new_posts_subtitle">點選以顯示</string>
+ <string name="notifications_account_required">登入 WordPress.com 以接收通知</string>
+ <string name="stats_unknown_author">不明作者</string>
+ <string name="image_added">已新增圖片</string>
+ <string name="signout">中斷連結</string>
+ <string name="deselect_all">取消全選</string>
+ <string name="show">顯示</string>
+ <string name="hide">隱藏</string>
+ <string name="select_all">全部選取</string>
+ <string name="sign_out_wpcom_confirm">中斷帳號連結將會移除此裝置上 @%s 的所有 WordPress.com 資料,包括本機草稿及變更項目。</string>
+ <string name="select_from_new_picker">使用全新挑選器一次選取多個項目</string>
+ <string name="stats_generic_error">無法載入必要的統計資料</string>
+ <string name="no_device_videos">沒有影片</string>
+ <string name="no_blog_images">沒有圖片</string>
+ <string name="no_blog_videos">沒有影片</string>
+ <string name="no_device_images">沒有圖片</string>
+ <string name="error_loading_blog_images">無法擷取圖片</string>
+ <string name="error_loading_blog_videos">無法擷取影片</string>
+ <string name="error_loading_images">載入圖片時發生錯誤</string>
+ <string name="error_loading_videos">載入影片時發生錯誤</string>
+ <string name="loading_blog_images">正在擷取圖片</string>
+ <string name="loading_blog_videos">正在擷取影片</string>
+ <string name="no_media_sources">無法擷取媒體</string>
+ <string name="loading_videos">正在載入影片</string>
+ <string name="loading_images">正在載入圖片</string>
+ <string name="no_media">沒有媒體</string>
+ <string name="device">裝置</string>
+ <string name="language">語言</string>
+ <string name="add_to_post">新增至文章</string>
+ <string name="media_picker_title">選取媒體</string>
+ <string name="take_photo">拍攝照片</string>
+ <string name="take_video">拍攝影片</string>
+ <string name="tab_title_device_images">裝置圖片</string>
+ <string name="tab_title_device_videos">裝置影片</string>
+ <string name="tab_title_site_images">網站圖片</string>
+ <string name="tab_title_site_videos">網站影片</string>
+ <string name="media_details_label_file_name">檔案名稱</string>
+ <string name="media_details_label_file_type">檔案類型</string>
+ <string name="error_publish_no_network">未連線時無法發表。已儲存為草稿。</string>
+ <string name="editor_toast_invalid_path">檔案路徑無效</string>
+ <string name="verification_code">驗證碼</string>
+ <string name="invalid_verification_code">驗證碼無效</string>
+ <string name="verify">驗證</string>
+ <string name="two_step_footer_label">輸入驗證器應用程式上的驗證碼。</string>
+ <string name="two_step_footer_button">透過簡訊傳送驗證碼</string>
+ <string name="two_step_sms_sent">請查看簡訊是否收到驗證碼。</string>
+ <string name="sign_in_jetpack">登入 WordPress.com 帳號以連結至 Jetpack。</string>
+ <string name="auth_required">再次登入以繼續操作。</string>
+ <string name="reader_empty_posts_request_failed">無法擷取文章</string>
+ <string name="publisher">發佈者:</string>
+ <string name="error_notification_open">無法開啟通知</string>
+ <string name="stats_followers_total_email_paged">顯示 %1$d - %2$d 個電子郵件關注者 (共 %3$s 個)</string>
+ <string name="stats_search_terms_unknown_search_terms">不明搜尋字詞</string>
+ <string name="stats_followers_total_wpcom_paged">顯示 %1$d - %2$d 個 WordPress.com 關注者 (共 %3$s 個)</string>
+ <string name="stats_empty_search_terms_desc">查看訪客以哪些搜尋字詞找到你的網站,進一步瞭解搜尋流量的相關資訊。</string>
+ <string name="stats_empty_search_terms">沒有任何搜尋字詞的記錄</string>
+ <string name="stats_entry_search_terms">搜尋字詞</string>
+ <string name="stats_view_authors">作者</string>
+ <string name="stats_view_search_terms">搜尋字詞</string>
+ <string name="comments_fetching">正在擷取留言…</string>
+ <string name="pages_fetching">正在擷取頁面…</string>
+ <string name="toast_err_post_uploading">上傳文章時無法加以開啟</string>
+ <string name="posts_fetching">正在擷取文章…</string>
+ <string name="media_fetching">正在擷取媒體…</string>
+ <string name="post_uploading">正在上傳</string>
+ <string name="stats_total">總計</string>
+ <string name="stats_overall">整體</string>
+ <string name="stats_period">期間</string>
+ <string name="logs_copied_to_clipboard">應用程式記錄已複製到剪貼簿</string>
+ <string name="reader_label_new_posts">新文章</string>
+ <string name="reader_empty_posts_in_blog">這個部落格沒有內容</string>
+ <string name="stats_average_per_day">每天平均</string>
+ <string name="stats_recent_weeks">最近幾星期</string>
+ <string name="error_copy_to_clipboard">將文字複製到剪貼簿時發生錯誤</string>
+ <string name="reader_page_recommended_blogs">你或許也會喜歡以下網站:</string>
+ <string name="stats_months_and_years">月份與年份</string>
+ <string name="themes_fetching">正在擷取佈景主題...</string>
+ <string name="stats_for">%s 的統計資料</string>
+ <string name="stats_other_recent_stats_label">其他近期統計資料</string>
+ <string name="stats_view_all">檢視全部</string>
+ <string name="stats_view">檢視</string>
+ <string name="stats_followers_months">%1$d 個月</string>
+ <string name="stats_followers_a_year">一年</string>
+ <string name="stats_followers_years">%1$d 年</string>
+ <string name="stats_followers_a_month">一個月</string>
+ <string name="stats_followers_minutes">%1$d 分鐘</string>
+ <string name="stats_followers_an_hour_ago">一小時前</string>
+ <string name="stats_followers_hours">%1$d 個小時</string>
+ <string name="stats_followers_a_day">一天</string>
+ <string name="stats_followers_days">%1$d 天</string>
+ <string name="stats_followers_a_minute_ago">一分鐘前</string>
+ <string name="stats_followers_seconds_ago">幾秒鐘前</string>
+ <string name="stats_followers_total_email">電子郵件關注者總數:%1$s</string>
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">電子郵件</string>
+ <string name="stats_followers_total_wpcom">WordPress.com 關注者總數:%1$s</string>
+ <string name="stats_comments_total_comments_followers">含有留言關注者的文章總數:%1$s</string>
+ <string name="stats_comments_by_authors">依作者</string>
+ <string name="stats_comments_by_posts_and_pages">依文章與頁面</string>
+ <string name="stats_empty_followers_desc">追蹤關注者總人數,以及這些關注者分別已關注你的網站多久時間。</string>
+ <string name="stats_empty_followers">無關注者</string>
+ <string name="stats_empty_publicize_desc">使用 Publicize 追蹤來自不同社交網路服務的關注者。</string>
+ <string name="stats_empty_publicize">尚無任何 Publicize 關注者的記錄</string>
+ <string name="stats_empty_video">尚無影片播放記錄</string>
+ <string name="stats_empty_video_desc">如果你已使用 VideoPress 上傳影片,你可以查看影片的觀看次數。</string>
+ <string name="stats_empty_comments_desc">如果你允許他人在網站上留言,可以依據 1000 則最新的留言,追蹤主要留言者,並查看哪些內容引起最熱烈的討論。</string>
+ <string name="stats_empty_tags_and_categories_desc">瞭解你網站上最熱門的主題,例如上星期以來的熱門文章中所反映的主題。</string>
+ <string name="stats_empty_top_authors_desc">追蹤各個參與者的文章瀏覽次數,仔細觀察每位作者最熱門的內容。</string>
+ <string name="stats_empty_tags_and_categories">尚無任何人點閱加上標籤的文章或頁面</string>
+ <string name="stats_empty_clicks_desc">如果你的內容含有其他網站連結,可以查看訪客最常點閱哪些連結。</string>
+ <string name="stats_empty_referrers_desc">查看傳送最多流量的網站及搜尋引擎,深入瞭解你的網站可見度</string>
+ <string name="stats_empty_clicks_title">尚無任何點擊記錄</string>
+ <string name="stats_empty_referrers_title">尚無任何來源連結記錄</string>
+ <string name="stats_empty_top_posts_title">尚未有人點閱文章或頁面</string>
+ <string name="stats_empty_top_posts_desc">查看你最多人瀏覽的內容,並瞭解個別文章和頁面在一段時間內的點閱率。</string>
+ <string name="stats_totals_followers">自從</string>
+ <string name="stats_empty_geoviews">尚無任何國家記錄</string>
+ <string name="stats_empty_geoviews_desc">探索清單以查看哪些國家及地區為你的網站帶來最多流量。</string>
+ <string name="stats_entry_video_plays">影片</string>
+ <string name="stats_entry_top_commenter">作者</string>
+ <string name="stats_entry_publicize">服務</string>
+ <string name="stats_entry_followers">關注者</string>
+ <string name="stats_totals_publicize">關注者</string>
+ <string name="stats_entry_clicks_link">連結</string>
+ <string name="stats_view_top_posts_and_pages">文章與頁面</string>
+ <string name="stats_view_videos">影片</string>
+ <string name="stats_view_publicize">宣傳</string>
+ <string name="stats_view_followers">關注者</string>
+ <string name="stats_view_countries">國家</string>
+ <string name="stats_likes">按讚數</string>
+ <string name="stats_pagination_label">第 %1$s 頁,共 %2$s 頁</string>
+ <string name="stats_timeframe_years">年</string>
+ <string name="stats_views">點閱數</string>
+ <string name="stats_visitors">訪客</string>
+ <string name="ssl_certificate_details">詳細資料</string>
+ <string name="delete_sure_post">刪除此文章</string>
+ <string name="delete_sure">刪除此草稿</string>
+ <string name="delete_sure_page">刪除此頁面</string>
+ <string name="confirm_delete_multi_media">要刪除選取的項目嗎?</string>
+ <string name="confirm_delete_media">要刪除選取的項目嗎?</string>
+ <string name="cab_selected">已選取:%d</string>
+ <string name="media_gallery_date_range">顯示自 %1$s 到 %2$s 的媒體</string>
+ <string name="sure_to_remove_account">移除此網站?</string>
+ <string name="reader_empty_followed_blogs_title">你尚未關注任何網站</string>
+ <string name="reader_empty_posts_liked">你未對任何文章按讚</string>
+ <string name="faq_button">常見問題集</string>
+ <string name="browse_our_faq_button">瀏覽常見問題集</string>
+ <string name="nux_help_description">造訪說明中心以取得常見問題的解答,或前往論壇提出新問題</string>
+ <string name="agree_terms_of_service">建立帳號即表示你同意超棒的%1$s服務條款%2$s</string>
+ <string name="create_new_blog_wpcom">建立 WordPress.com 網誌</string>
+ <string name="new_blog_wpcom_created">WordPress.com 網誌已建立!</string>
+ <string name="reader_empty_comments">尚未有留言</string>
+ <string name="reader_empty_posts_in_tag">沒有任何含有此標籤的文章</string>
+ <string name="reader_label_comment_count_multi">%,d 則留言</string>
+ <string name="reader_label_view_original">查看原始文章</string>
+ <string name="reader_label_like">按讚</string>
+ <string name="reader_label_comment_count_single">一則留言</string>
+ <string name="reader_label_comments_closed">留言已關閉</string>
+ <string name="reader_label_comments_on">以下內容的留言:</string>
+ <string name="reader_title_photo_viewer">第 %1$d 個,共 %2$d 個</string>
+ <string name="error_publish_empty_post">無法發表空白的文章</string>
+ <string name="error_refresh_unauthorized_posts">你沒有查看或編輯文章的權限</string>
+ <string name="error_refresh_unauthorized_pages">你沒有查看或編輯頁面的權限</string>
+ <string name="error_refresh_unauthorized_comments">你沒有查看或編輯留言的權限</string>
+ <string name="older_month">超過一個月</string>
+ <string name="more">更多</string>
+ <string name="older_two_days">超過 2 天</string>
+ <string name="older_last_week">超過一週</string>
+ <string name="stats_no_blog">無法為所需網誌載入統計</string>
+ <string name="select_a_blog">選取 WordPress 網站</string>
+ <string name="sending_content">正在上傳 %s 內容</string>
+ <string name="uploading_total">正在上傳第 %1$d 個,共 %2$d 個</string>
+ <string name="mnu_comment_liked">已按讚</string>
+ <string name="comment">留言</string>
+ <string name="comment_trashed">留言已移至垃圾桶</string>
+ <string name="posts_empty_list">尚無文章。建立文章?</string>
+ <string name="comment_reply_to_user">回覆 %s</string>
+ <string name="pages_empty_list">尚無任何頁面。建立頁面?</string>
+ <string name="media_empty_list_custom_date">此時間間隔內沒有媒體</string>
+ <string name="posting_post">正在發表「%s」</string>
+ <string name="signing_out">正在登出…</string>
+ <string name="reader_toast_err_generic">無法執行此動作</string>
+ <string name="reader_toast_err_block_blog">無法封鎖此網誌</string>
+ <string name="reader_toast_blog_blocked">將不再顯示此網誌的文章</string>
+ <string name="reader_menu_block_blog">封鎖此網誌</string>
+ <string name="contact_us">聯絡我們</string>
+ <string name="hs__conversation_detail_error">描述你遇到的問題</string>
+ <string name="hs__new_conversation_header">支援交談</string>
+ <string name="hs__conversation_header">支援交談</string>
+ <string name="hs__username_blank_error">輸入有效的名稱</string>
+ <string name="hs__invalid_email_error">輸入有效的電子郵件地址</string>
+ <string name="add_location">新增位置</string>
+ <string name="current_location">目前的位置</string>
+ <string name="search_location">搜尋</string>
+ <string name="edit_location">編輯</string>
+ <string name="search_current_location">尋找</string>
+ <string name="preference_send_usage_stats">傳送統計資料</string>
+ <string name="preference_send_usage_stats_summary">自動傳送使用統計資料,協助我們改善 Android 版 WordPress</string>
+ <string name="update_verb">更新</string>
+ <string name="schedule_verb">排程</string>
+ <string name="reader_title_blog_preview">讀者網誌</string>
+ <string name="reader_title_tag_preview">讀者標籤</string>
+ <string name="reader_title_subs">標籤和網誌</string>
+ <string name="reader_page_followed_tags">關注的標籤</string>
+ <string name="reader_page_followed_blogs">關注的網站</string>
+ <string name="reader_hint_add_tag_or_url">輸入要關注的 URL 或標籤</string>
+ <string name="reader_label_followed_blog">網誌已關注</string>
+ <string name="reader_label_tag_preview">已下標籤的文章 %s</string>
+ <string name="reader_toast_err_get_blog_info">無法顯示此網誌</string>
+ <string name="reader_toast_err_already_follow_blog">你已經在關注此網誌</string>
+ <string name="reader_toast_err_follow_blog">無法關注此網誌</string>
+ <string name="reader_toast_err_unfollow_blog">無法取消關注此網誌</string>
+ <string name="reader_empty_recommended_blogs">沒有推薦的網誌</string>
+ <string name="saving">儲存中…</string>
+ <string name="media_empty_list">沒有媒體</string>
+ <string name="ptr_tip_message">秘訣:下拉即可重新整理</string>
+ <string name="help">說明</string>
+ <string name="forgot_password">忘記密碼?</string>
+ <string name="forums">論壇</string>
+ <string name="help_center">說明中心</string>
+ <string name="ssl_certificate_error">無效的 SSL 憑證</string>
+ <string name="ssl_certificate_ask_trust">如果你通常能夠順利連線至此網站,則此錯誤可能代表有人正嘗試冒充該網站,因此請勿繼續操作。是否仍然要信任憑證?</string>
+ <string name="out_of_memory">裝置記憶體不足</string>
+ <string name="no_network_message">沒有可用的網路</string>
+ <string name="could_not_remove_account">無法移除網站</string>
+ <string name="gallery_error">無法擷取媒體項目</string>
+ <string name="blog_not_found">存取此網誌時發生錯誤</string>
+ <string name="wait_until_upload_completes">請等候上傳完成</string>
+ <string name="theme_fetch_failed">無法擷取佈景主題</string>
+ <string name="theme_set_failed">無法設定佈景主題</string>
+ <string name="theme_auth_error_message">確認你有設定佈景主題的權限</string>
+ <string name="comments_empty_list">無回應</string>
+ <string name="mnu_comment_unspam">非垃圾</string>
+ <string name="no_site_error">無法連線至 WordPress 網站</string>
+ <string name="adding_cat_failed">新增類別失敗</string>
+ <string name="adding_cat_success">類別已成功新增</string>
+ <string name="cat_name_required">類別名稱欄位為必填</string>
+ <string name="category_automatically_renamed">類別名稱 %1$s 無效。已將它重新命名為 %2$s。</string>
+ <string name="no_account">未找到 WordPress 帳號,請新增帳號然後重試一次</string>
+ <string name="sdcard_message">需要掛接 SD 卡以上傳媒體</string>
+ <string name="stats_empty_comments">尚無回應</string>
+ <string name="stats_bar_graph_empty">沒有可用的統計</string>
+ <string name="invalid_url_message">檢查輸入的 URL 是否有效</string>
+ <string name="reply_failed">回覆失敗</string>
+ <string name="notifications_empty_list">沒有通知</string>
+ <string name="error_delete_post">刪除 %s 時發生錯誤</string>
+ <string name="error_refresh_posts">目前無法重新整理文章</string>
+ <string name="error_refresh_pages">目前無法重新整理頁面</string>
+ <string name="error_refresh_notifications">目前無法重新整理通知</string>
+ <string name="error_refresh_comments">目前無法重新整理回應</string>
+ <string name="error_refresh_stats">目前無法重新整理統計</string>
+ <string name="error_generic">發生錯誤</string>
+ <string name="error_moderate_comment">審核時發生錯誤</string>
+ <string name="error_edit_comment">編輯回應時發生錯誤</string>
+ <string name="error_upload">上傳 %s 時發生錯誤</string>
+ <string name="error_load_comment">無法載入回應</string>
+ <string name="error_downloading_image">下載圖片時發生錯誤</string>
+ <string name="passcode_wrong_passcode">錯誤的 PIN</string>
+ <string name="invalid_email_message">你的電子郵件地址無效</string>
+ <string name="invalid_password_message">密碼必須包含至少 4 個字元</string>
+ <string name="invalid_username_too_short">使用者名稱長度必須超過 4 個字元</string>
+ <string name="invalid_username_too_long">使用者名稱長度必須小於 61 個字元</string>
+ <string name="username_only_lowercase_letters_and_numbers">使用者名稱只能包含小寫字母 (a-z) 和數字</string>
+ <string name="username_required">輸入使用者名稱</string>
+ <string name="username_not_allowed">不允許使用該使用者名稱</string>
+ <string name="username_must_be_at_least_four_characters">使用者名稱必須至少為 4 個字元</string>
+ <string name="username_contains_invalid_characters">使用者名稱不能包含字元「_」</string>
+ <string name="username_must_include_letters">使用者名稱必須至少有 1 個字母 (a-z)</string>
+ <string name="email_invalid">輸入有效的電子郵件地址</string>
+ <string name="email_not_allowed">不允許使用該電子郵件地址</string>
+ <string name="username_exists">該使用者名稱已存在</string>
+ <string name="email_exists">該電子郵件地址已使用</string>
+ <string name="username_reserved_but_may_be_available">該使用者名稱目前已被預定,但可能幾天後便可供使用</string>
+ <string name="blog_name_required">輸入網站位址</string>
+ <string name="blog_name_not_allowed">不允許使用該網站位址</string>
+ <string name="blog_name_must_be_at_least_four_characters">網站位址至少必須為 4 個字元</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">網站位址長度必須小於 64 個字元</string>
+ <string name="blog_name_contains_invalid_characters">網站位址不能包含字元「_」</string>
+ <string name="blog_name_cant_be_used">你不可使用該網站位址</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">網站位址只能包含小寫字母 (a-z) 和數字</string>
+ <string name="blog_name_exists">該網站已存在</string>
+ <string name="blog_name_reserved">該網站已被預定</string>
+ <string name="blog_name_reserved_but_may_be_available">該網站目前已被預定,但可能幾天後便可供使用</string>
+ <string name="username_or_password_incorrect">你輸入的使用者名稱或密碼不正確</string>
+ <string name="nux_cannot_log_in">我們無法將你登入</string>
+ <string name="xmlrpc_error">無法連線。在你的網站上輸入 xmlrpc.php 的完整路徑,然後再試一次。</string>
+ <string name="select_categories">選取類別</string>
+ <string name="account_details">帳號詳細資料</string>
+ <string name="edit_post">編輯文章</string>
+ <string name="add_comment">新增回應</string>
+ <string name="connection_error">連線錯誤</string>
+ <string name="cancel_edit">取消編輯</string>
+ <string name="scaled_image_error">輸入有效縮放的寬度值</string>
+ <string name="post_not_found">載入文章時發生錯誤。重新整理你的文章,然後再試一次。</string>
+ <string name="learn_more">瞭解更多</string>
+ <string name="media_gallery_settings_title">藝廊設定</string>
+ <string name="media_gallery_image_order">圖片順序</string>
+ <string name="media_gallery_num_columns">欄位數</string>
+ <string name="media_gallery_type_thumbnail_grid">縮圖格線</string>
+ <string name="media_gallery_edit">編輯藝廊</string>
+ <string name="media_error_no_permission">你沒有檢視媒體庫的權限</string>
+ <string name="cannot_delete_multi_media_items">目前無法刪除部分媒體。請稍後再試一次。</string>
+ <string name="themes_live_preview">即時預覽</string>
+ <string name="theme_current_theme">目前佈景主題</string>
+ <string name="theme_premium_theme">進階佈景主題</string>
+ <string name="link_enter_url_text">連結文字 (選填)</string>
+ <string name="create_a_link">建立連結</string>
+ <string name="page_settings">頁面設定</string>
+ <string name="local_draft">本機草稿</string>
+ <string name="upload_failed">上傳失敗</string>
+ <string name="horizontal_alignment">水平對齊</string>
+ <string name="file_not_found">找不到可上傳的媒體檔案。該檔案是否已刪除或移走?</string>
+ <string name="post_settings">文章設定</string>
+ <string name="delete_post">刪除文章</string>
+ <string name="delete_page">刪除頁面</string>
+ <string name="comment_status_approved">已核准</string>
+ <string name="comment_status_unapproved">審核中</string>
+ <string name="comment_status_spam">垃圾</string>
+ <string name="comment_status_trash">已移至垃圾桶</string>
+ <string name="edit_comment">編輯回應</string>
+ <string name="mnu_comment_approve">核准</string>
+ <string name="mnu_comment_unapprove">駁回</string>
+ <string name="mnu_comment_spam">垃圾</string>
+ <string name="mnu_comment_trash">垃圾桶</string>
+ <string name="dlg_approving_comments">核准中</string>
+ <string name="dlg_unapproving_comments">駁回中</string>
+ <string name="dlg_spamming_comments">標記為垃圾</string>
+ <string name="dlg_trashing_comments">正在移至垃圾桶</string>
+ <string name="dlg_confirm_trash_comments">移至垃圾桶?</string>
+ <string name="trash_yes">垃圾桶</string>
+ <string name="trash_no">不要移至垃圾桶</string>
+ <string name="trash">垃圾桶</string>
+ <string name="author_name">作者名稱</string>
+ <string name="author_email">作者電子郵件</string>
+ <string name="author_url">作者網址</string>
+ <string name="hint_comment_content">回應</string>
+ <string name="saving_changes">正在儲存變更</string>
+ <string name="sure_to_cancel_edit_comment">取消編輯此回應?</string>
+ <string name="content_required">必須發表回應</string>
+ <string name="toast_comment_unedited">回應尚未變更</string>
+ <string name="remove_account">移除網站</string>
+ <string name="blog_removed_successfully">已成功移除網站</string>
+ <string name="delete_draft">刪除草稿</string>
+ <string name="preview_page">預覽頁面</string>
+ <string name="preview_post">預覽文章</string>
+ <string name="comment_added">回應已成功新增</string>
+ <string name="post_not_published">文章狀態未發佈</string>
+ <string name="page_not_published">頁面狀態未發佈</string>
+ <string name="view_in_browser">在瀏覽器中檢視</string>
+ <string name="add_new_category">新增類別</string>
+ <string name="category_name">類別名稱</string>
+ <string name="category_slug">類別文章網址代稱 (選填)</string>
+ <string name="category_desc">類別描述 (選填)</string>
+ <string name="category_parent">類別上層 (選填)</string>
+ <string name="share_action_post">新文章</string>
+ <string name="share_action_media">媒體庫</string>
+ <string name="file_error_create">無法建立暫存檔進行媒體上傳。確認裝置上有足夠的可用空間。</string>
+ <string name="location_not_found">未知的位置</string>
+ <string name="open_source_licenses">開啟來源授權</string>
+ <string name="invalid_site_url_message">檢查輸入的網站 URL 是否有效</string>
+ <string name="pending_review">待審中</string>
+ <string name="http_credentials">HTTP 憑證 (選填)</string>
+ <string name="http_authorization_required">需要授權</string>
+ <string name="post_format">文章格式</string>
+ <string name="notifications_empty_all">尚未有任何通知。</string>
+ <string name="new_post">新文章</string>
+ <string name="new_media">新媒體</string>
+ <string name="view_site">檢視網站</string>
+ <string name="privacy_policy">隱私權政策</string>
+ <string name="local_changes">本機變更</string>
+ <string name="image_settings">圖片設定</string>
+ <string name="add_account_blog_url">網誌位址</string>
+ <string name="wordpress_blog">WordPress 網誌</string>
+ <string name="error_blog_hidden">此網誌已隱藏,無法載入。請在設定中啟用網誌,然後再試一次。</string>
+ <string name="fatal_db_error">建立應用程式資料庫時發生錯誤。請嘗試重新安裝應用程式。</string>
+ <string name="jetpack_message_not_admin">統計需要 Jetpack 外掛程式。請聯絡網站管理員。</string>
+ <string name="reader_title_applog">應用程式記錄檔</string>
+ <string name="reader_share_link">分享連結</string>
+ <string name="reader_toast_err_add_tag">無法新增此標籤</string>
+ <string name="reader_toast_err_remove_tag">無法移除此標籤</string>
+ <string name="required_field">必填欄位</string>
+ <string name="email_hint">電子郵件地址</string>
+ <string name="site_address">你自助託管的位址 (網址)</string>
+ <string name="email_cant_be_used_to_signup">你無法使用該電子郵件地址來註冊。我們無法使用這些電子郵件地址,因為它們會阻擋我們的某些郵件。請使用其他電子郵件供應商。</string>
+ <string name="email_reserved">已有人使用該電子郵件地址。請查看你的收件匣是否有啟用郵件。如果你現在不啟用,可以幾天後再試一次。</string>
+ <string name="blog_name_must_include_letters">網站位址必須至少有 1 個字母 (a-z)</string>
+ <string name="blog_name_invalid">無效的網站位址</string>
+ <string name="blog_title_invalid">無效的網站標題</string>
+ <string name="deleting_page">正在刪除頁面</string>
+ <string name="deleting_post">正在刪除文章</string>
+ <string name="share_url_post">分享文章</string>
+ <string name="share_url_page">分享頁面</string>
+ <string name="share_link">分享連結</string>
+ <string name="creating_your_account">建立你的帳號</string>
+ <string name="creating_your_site">建立你的網站</string>
+ <string name="reader_empty_posts_in_tag_updating">正在擷取文章…</string>
+ <string name="error_refresh_media">重新整理媒體櫃時發生某種錯誤。請稍後再試一次。</string>
+ <string name="reader_likes_you_and_multi">你和其他 %,d 個人都說這個讚</string>
+ <string name="reader_likes_multi">%,d 人說這個讚</string>
+ <string name="reader_toast_err_get_comment">無法擷取此回應</string>
+ <string name="reader_label_reply">回覆</string>
+ <string name="video">影片</string>
+ <string name="download">正在下載媒體</string>
+ <string name="comment_spammed">已將留言標記為垃圾</string>
+ <string name="cant_share_no_visible_blog">你無法在沒有可見網誌的情形下分享至 WordPress</string>
+ <string name="select_time">選取時間</string>
+ <string name="reader_likes_you_and_one">你和其他 1 人都說這個讚</string>
+ <string name="reader_empty_followed_blogs_description">不過別擔心,只要點選右上方的圖示即可開始探索!</string>
+ <string name="select_date">選取日期</string>
+ <string name="pick_photo">選取照片</string>
+ <string name="account_two_step_auth_enabled">此帳號已啟用兩步驟驗證。瀏覽你的 WordPress.com 安全性設定,並產生一組應用程式專用密碼。</string>
+ <string name="pick_video">選取影片</string>
+ <string name="reader_toast_err_get_post">無法擷取此文章</string>
+ <string name="validating_user_data">正在驗證使用者資料</string>
+ <string name="validating_site_data">正在驗證網站資料</string>
+ <string name="password_invalid">你需要一組更安全的密碼。請務必使用 7 個或更多字元,並混合使用大小寫字母、數字或特殊字元。</string>
+ <string name="nux_tap_continue">繼續</string>
+ <string name="nux_welcome_create_account">建立帳號</string>
+ <string name="signing_in">正在登入…</string>
+ <string name="nux_add_selfhosted_blog">新增自助託管的網站</string>
+ <string name="nux_oops_not_selfhosted_blog">登入 WordPress.com</string>
+ <string name="media_add_popup_title">增加媒體庫</string>
+ <string name="media_add_new_media_gallery">創建藝廊</string>
+ <string name="empty_list_default">清單是空的</string>
+ <string name="select_from_media_library">從媒體庫選擇</string>
+ <string name="jetpack_message">Jetpack 外掛需要統計資訊。你想要安裝 Jetpack 嗎?</string>
+ <string name="jetpack_not_found">找不到 Jetpack</string>
+ <string name="reader_untitled_post">(未命名)</string>
+ <string name="reader_share_subject">從 %s 分享</string>
+ <string name="reader_btn_share">分享</string>
+ <string name="reader_btn_follow">追蹤</string>
+ <string name="reader_btn_unfollow">追蹤</string>
+ <string name="reader_hint_comment_on_comment">回應評論</string>
+ <string name="reader_label_added_tag">新增 %s</string>
+ <string name="reader_label_removed_tag">移除 %s</string>
+ <string name="reader_likes_one">1 人說這個讚</string>
+ <string name="reader_likes_only_you">你說這個讚</string>
+ <string name="reader_toast_err_comment_failed">無法張貼你的回應</string>
+ <string name="reader_toast_err_tag_exists">你已經在關注此標籤</string>
+ <string name="reader_toast_err_tag_invalid">這並非有效的標籤</string>
+ <string name="reader_toast_err_share_intent">無法分享</string>
+ <string name="reader_toast_err_view_image">無法檢視圖片</string>
+ <string name="reader_toast_err_url_intent">無法開啟 %s</string>
+ <string name="reader_empty_followed_tags">你未關注任何標籤</string>
+ <string name="create_account_wpcom">在 WordPress.com 上建立帳號</string>
+ <string name="button_next">下一步</string>
+ <string name="connecting_wpcom">連結至 WordPress.com</string>
+ <string name="username_invalid">無效的使用者名稱</string>
+ <string name="limit_reached">已達限制。你可以在 1 分鐘後再試一次。如果在 1 分鐘不到之前重試,只會延長你必須等候繼續進行的時間。如果你覺得這是錯誤,請聯絡支援團隊。</string>
+ <string name="nux_tutorial_get_started_title">開始使用!</string>
+ <string name="themes">佈景主題</string>
+ <string name="all">全部</string>
+ <string name="images">圖片</string>
+ <string name="unattached">未附加</string>
+ <string name="custom_date">自訂日期</string>
+ <string name="media_add_popup_capture_photo">拍照</string>
+ <string name="media_add_popup_capture_video">錄影</string>
+ <string name="media_gallery_image_order_random">隨機</string>
+ <string name="media_gallery_image_order_reverse">反轉順序</string>
+ <string name="media_gallery_type">型態</string>
+ <string name="media_gallery_type_squares">方塊</string>
+ <string name="media_gallery_type_tiled">磁磚</string>
+ <string name="media_gallery_type_circles">圓形</string>
+ <string name="media_gallery_type_slideshow">幻燈片</string>
+ <string name="media_edit_title_text">標題</string>
+ <string name="media_edit_caption_text">副標題</string>
+ <string name="media_edit_description_text">說明</string>
+ <string name="media_edit_title_hint">在這裡輸入標題</string>
+ <string name="media_edit_caption_hint">在這裡輸入副標題</string>
+ <string name="media_edit_description_hint">請在這輸入簡述</string>
+ <string name="media_edit_success">更新</string>
+ <string name="media_edit_failure">更新失敗</string>
+ <string name="themes_details_label">細節</string>
+ <string name="themes_features_label">功能</string>
+ <string name="theme_activate_button">啟用</string>
+ <string name="theme_activating_button">啟用中</string>
+ <string name="theme_set_success">成功設定佈景主題</string>
+ <string name="theme_auth_error_title">抓取佈景主題失敗</string>
+ <string name="post_excerpt">文章摘要</string>
+ <string name="share_action_title">加入到</string>
+ <string name="share_action">分享</string>
+ <string name="stats">統計</string>
+ <string name="stats_view_visitors_and_views">訪客和點閱數</string>
+ <string name="stats_view_clicks">點擊率</string>
+ <string name="stats_view_tags_and_categories">標籤與分類</string>
+ <string name="stats_view_referrers">來源網址</string>
+ <string name="stats_timeframe_today">今天</string>
+ <string name="stats_timeframe_yesterday">昨天</string>
+ <string name="stats_timeframe_days">天</string>
+ <string name="stats_timeframe_weeks">週</string>
+ <string name="stats_timeframe_months">月</string>
+ <string name="stats_entry_country">國家</string>
+ <string name="stats_entry_posts_and_pages">標題</string>
+ <string name="stats_entry_tags_and_categories">主題</string>
+ <string name="stats_entry_authors">作者</string>
+ <string name="stats_entry_referrers">來源網址</string>
+ <string name="stats_totals_views">點閱數</string>
+ <string name="stats_totals_clicks">點擊率</string>
+ <string name="stats_totals_plays">播放次數</string>
+ <string name="passcode_manage">管理 PIN 鎖定</string>
+ <string name="passcode_enter_passcode">輸入你的 PIN</string>
+ <string name="passcode_enter_old_passcode">輸入你的舊 PIN</string>
+ <string name="passcode_re_enter_passcode">重新輸入你的 PIN</string>
+ <string name="passcode_change_passcode">變更 PIN</string>
+ <string name="passcode_set">PIN 設定</string>
+ <string name="passcode_preference_title">PIN 鎖定</string>
+ <string name="passcode_turn_off">關閉 PIN 鎖定</string>
+ <string name="passcode_turn_on">開啟 PIN 鎖定</string>
+ <string name="upload">上傳</string>
+ <string name="discard">放棄</string>
+ <string name="sign_in">登入</string>
+ <string name="notifications">通知</string>
+ <string name="note_reply_successful">回覆已發佈</string>
+ <string name="follows">關注者</string>
+ <string name="new_notifications">%d 個新通知</string>
+ <string name="more_notifications">還有 %d 個。</string>
+ <string name="loading">載入中...</string>
+ <string name="httpuser">HTTP 使用者帳號</string>
+ <string name="httppassword">HTTP 密碼</string>
+ <string name="error_media_upload">上傳媒體時發生錯誤</string>
+ <string name="post_content">內容(輕按以增加文字或媒體)</string>
+ <string name="publish_date">發佈</string>
+ <string name="content_description_add_media">新增媒體</string>
+ <string name="incorrect_credentials">錯誤的使用者帳號或密碼。</string>
+ <string name="password">密碼</string>
+ <string name="username">使用者帳號</string>
+ <string name="reader">閱讀器</string>
+ <string name="featured">選為精選圖片</string>
+ <string name="featured_in_post">文章內容包含圖片</string>
+ <string name="no_network_title">沒有網路連線</string>
+ <string name="pages">頁面</string>
+ <string name="caption">說明(可選)</string>
+ <string name="width">寬度</string>
+ <string name="posts">文章</string>
+ <string name="anonymous">匿名</string>
+ <string name="page">頁面</string>
+ <string name="post">文章</string>
+ <string name="blogusername">網誌使用者帳號</string>
+ <string name="ok">OK</string>
+ <string name="upload_scaled_image">上傳並建立縮圖連結</string>
+ <string name="scaled_image">圖片縮放寬度</string>
+ <string name="scheduled">定時</string>
+ <string name="link_enter_url">URL</string>
+ <string name="uploading">上傳中…</string>
+ <string name="version">版本</string>
+ <string name="tos">服務條款</string>
+ <string name="app_title">WordPress for Android</string>
+ <string name="max_thumbnail_px_width">預設圖片寬度</string>
+ <string name="image_alignment">對齊</string>
+ <string name="refresh">更新</string>
+ <string name="untitled">無標題</string>
+ <string name="edit">編輯</string>
+ <string name="post_id">文章</string>
+ <string name="page_id">頁面</string>
+ <string name="post_password">密碼(非必要)</string>
+ <string name="immediately">立即</string>
+ <string name="quickpress_add_alert_title">設定捷徑名稱</string>
+ <string name="today">今天</string>
+ <string name="settings">設定</string>
+ <string name="share_url">分享網址</string>
+ <string name="quickpress_window_title">選擇用於 QuickPress 捷徑的網誌</string>
+ <string name="quickpress_add_error">捷徑名稱不能為空</string>
+ <string name="publish_post">發佈</string>
+ <string name="draft">草稿</string>
+ <string name="post_private">私密</string>
+ <string name="upload_full_size_image">上傳並連結到完整圖片</string>
+ <string name="title">標題</string>
+ <string name="tags_separate_with_commas">標籤(多個標籤請以英文逗號“,”隔開)</string>
+ <string name="categories">分類</string>
+ <string name="dlg_deleting_comments">刪除留言</string>
+ <string name="notification_blink">閃爍通知燈</string>
+ <string name="notification_sound">通知音效</string>
+ <string name="notification_vibrate">振動</string>
+ <string name="status">狀態</string>
+ <string name="location">位置</string>
+ <string name="sdcard_title">需要 SD 卡</string>
+ <string name="select_video">從媒體庫選擇影片</string>
+ <string name="media">媒體</string>
+ <string name="delete">刪除</string>
+ <string name="none">無</string>
+ <string name="blogs">部落格</string>
+ <string name="select_photo">從媒體庫選擇照片</string>
+ <string name="error">錯誤</string>
+ <string name="cancel">取消</string>
+ <string name="save">儲存</string>
+ <string name="add">新增</string>
+ <string name="category_refresh_error">分類更新錯誤</string>
+ <string name="preview">預覽</string>
+ <string name="on">於</string>
+ <string name="reply">回覆</string>
+ <string name="notification_settings">通知設定</string>
+ <string name="yes">是</string>
+ <string name="no">否</string>
+</resources>
diff --git a/WordPress/src/main/res/values/attrs.xml b/WordPress/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..6aa3afb9f
--- /dev/null
+++ b/WordPress/src/main/res/values/attrs.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <attr name="longClickHint" format="string" />
+
+ <!-- SummaryEditTextPreference attributes -->
+ <declare-styleable name="SummaryEditTextPreference">
+ <attr name="summaryLines" format="integer" />
+ <attr name="maxSummaryLines" format="integer" />
+ <attr name="longClickHint" />
+ </declare-styleable>
+
+ <!-- DetailListPreference attributes -->
+ <declare-styleable name="DetailListPreference">
+ <attr name="dialogTitle" format="string" />
+ <attr name="dialogHelpUrl" format="string" />
+ <attr name="entryDetails" format="string" />
+ <attr name="longClickHint" />
+ </declare-styleable>
+
+ <declare-styleable name="WPStartOverPreference">
+ <attr name="preficon" format="reference" />
+ <attr name="buttonText" format="string" />
+ <attr name="buttonTextColor" format="reference|color" />
+ <attr name="buttonTextAllCaps" format="boolean" />
+ </declare-styleable>
+
+ <declare-styleable name="FlowLayout">
+ <attr name="horizontalSpacing" format="dimension" />
+ <attr name="verticalSpacing" format="dimension" />
+ </declare-styleable>
+ <declare-styleable name="FlowLayout_LayoutParams">
+ <attr name="layout_horizontalSpacing" format="dimension" />
+ <attr name="layout_verticalSpacing" format="dimension" />
+ </declare-styleable>
+
+ <!--
+ WPTextView attributes
+ -->
+ <declare-styleable name="WPTextView">
+ <attr name="wpFontFamily" format="enum">
+ <enum name="defaultNormal" value="0" />
+ <enum name="defaultLight" value="1" />
+ <enum name="merriweather" value="2" />
+ </attr>
+ <attr name="fixWidowWords" format="boolean"/>
+ </declare-styleable>
+
+ <!--
+ WPLinearLayoutSizeBound attributes
+ -->
+ <declare-styleable name="WPLinearLayoutSizeBound">
+ <attr name="maxWidth" format="dimension" />
+ <attr name="maxHeight" format="dimension" />
+ </declare-styleable>
+
+ <!--
+ ReaderIconCountView attributes
+ -->
+ <declare-styleable name="ReaderIconCountView">
+ <attr name="readerIcon" format="enum">
+ <enum name="like" value="0" />
+ <enum name="comment" value="1" />
+ </attr>
+ </declare-styleable>
+
+ <!--
+ PostListButton attributes
+ -->
+ <declare-styleable name="wpPostListButton">
+ <attr name="wpPostButtonType" format="enum">
+ <enum name="none" value="0" />
+ <enum name="edit" value="1" />
+ <enum name="view" value="2" />
+ <enum name="preview" value="3" />
+ <enum name="stats" value="4" />
+ <enum name="trash" value="5" />
+ <enum name="delete" value="6" />
+ <enum name="publish" value="7" />
+ <enum name="more" value="8" />
+ <enum name="back" value="9" />
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="wpBoundedWidth">
+ <attr name="bounded_width" format="dimension" />
+ </declare-styleable>
+
+</resources>
diff --git a/WordPress/src/main/res/values/available_languages.xml b/WordPress/src/main/res/values/available_languages.xml
new file mode 100644
index 000000000..cc7be32a9
--- /dev/null
+++ b/WordPress/src/main/res/values/available_languages.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Warning: Auto-generated file, don't edit it.-->
+<resources>
+<string-array name="available_languages" translatable="false">
+
+<item>
+en_US
+</item>
+
+<item>
+az
+</item>
+
+<item>
+de
+</item>
+
+<item>
+el
+</item>
+
+<item>
+es
+</item>
+
+<item>
+es_CL
+</item>
+
+<item>
+fr
+</item>
+
+<item>
+gd
+</item>
+
+<item>
+hi
+</item>
+
+<item>
+hu
+</item>
+
+<item>
+id
+</item>
+
+<item>
+it
+</item>
+
+<item>
+ja
+</item>
+
+<item>
+ko
+</item>
+
+<item>
+nb
+</item>
+
+<item>
+nl
+</item>
+
+<item>
+pl
+</item>
+
+<item>
+ru
+</item>
+
+<item>
+sv
+</item>
+
+<item>
+th
+</item>
+
+<item>
+uz
+</item>
+
+<item>
+zh_CN
+</item>
+
+<item>
+zh_TW
+</item>
+
+<item>
+zh_HK
+</item>
+
+<item>
+en_GB
+</item>
+
+<item>
+tr
+</item>
+
+<item>
+eu
+</item>
+
+<item>
+he
+</item>
+
+<item>
+pt_BR
+</item>
+
+<item>
+ar
+</item>
+
+<item>
+ro
+</item>
+
+<item>
+mk
+</item>
+
+<item>
+en_AU
+</item>
+
+<item>
+sr
+</item>
+
+<item>
+sk
+</item>
+
+<item>
+cy
+</item>
+
+<item>
+da
+</item>
+
+<item>
+bg
+</item>
+
+<item>
+sq
+</item>
+
+<item>
+hr
+</item>
+
+<item>
+cs
+</item>
+
+<item>
+en_CA
+</item>
+
+<item>
+ms
+</item>
+
+<item>
+es_VE
+</item>
+
+<item>
+gl
+</item>
+
+</string-array>
+</resources>
+
diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml
new file mode 100644
index 000000000..e1d10b54d
--- /dev/null
+++ b/WordPress/src/main/res/values/colors.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- material colors -->
+ <!--
+ colorPrimary app branding color for the app bar
+ colorPrimaryDark status bar and contextual app bars
+ colorControlNormal (unused here) controls in their normal state (spinner arrow, scrollbars, unchecked checkboxes)
+ colorControlActivated controls in their activated/checked state (search highlight, checked checkboxes)
+ colorControlHighlight control highlights (ripples, list selectors)
+ -->
+ <color name="color_primary">@color/blue_wordpress</color>
+ <color name="color_primary_dark">@color/status_bar_tint</color>
+ <color name="color_accent">@color/orange_jazzy</color>
+ <color name="color_control_activated">@color/blue_light</color>
+ <color name="color_control_highlight">@color/grey_lighten_20</color>
+
+ <color name="fab_normal">@color/color_accent</color>
+ <color name="fab_pressed">@color/orange_fire</color>
+ <color name="list_selected">@color/semi_transparent_blue_light</color>
+
+ <!-- Default Colors -->
+ <color name="translucent">#80000000</color>
+ <color name="transparent">#00000000</color>
+ <color name="light_gray">@color/grey_lighten_30</color>
+ <color name="gray">@color/grey_lighten_20</color>
+ <color name="white">#FFFFFF</color>
+ <!-- note that black should NOT be used for text - use grey_dark instead -->
+ <color name="black">#000000</color>
+ <color name="default_background">@color/grey_lighten_30</color>
+
+ <!-- ==================================== -->
+ <!-- WordPress.com Colors -->
+ <!-- wordpress.com/design-handbook/colors -->
+ <!-- Blues -->
+ <color name="blue_wordpress">#0087be</color>
+ <color name="blue_light">#78dcfa</color>
+ <color name="blue_medium">#00aadc</color>
+ <color name="blue_dark">#005082</color>
+
+ <!-- Primary Greys -->
+ <color name="grey">#87a6bc</color>
+ <color name="grey_light">#f3f6f8</color>
+ <color name="grey_dark">#2e4453</color>
+ <color name="grey_disabled">@color/grey_darken_10</color>
+
+ <!-- Secondary Greys -->
+ <color name="grey_lighten_30">#e9eff3</color>
+ <color name="grey_lighten_20">#c8d7e1</color>
+ <color name="grey_lighten_10">#a8bece</color>
+ <color name="grey_darken_10">#668eaa</color>
+ <color name="grey_darken_20">#4f748e</color>
+ <color name="grey_darken_30">#3d596d</color>
+
+ <!-- Oranges -->
+ <color name="orange_jazzy">#f0821e</color>
+ <color name="orange_fire">#d54e21</color>
+
+ <!-- Alerts -->
+ <color name="alert_yellow">#f0b849</color>
+ <color name="alert_red">#d94f4f</color>
+ <color name="alert_green">#4ab866</color>
+
+ <!-- Translucent -->
+ <color name="semi_transparent_grey_dark">#B32e4453</color>
+ <color name="semi_transparent_blue_light">#cc78dcfa</color>
+ <color name="translucent_grey_dark">#802e4453</color>
+ <color name="translucent_grey_lighten_10">#80a8bece</color>
+ <color name="translucent_grey_lighten_20">#80c8d7e1</color>
+ <color name="translucent_grey_lighten_30">#80e9eff3</color>
+ <color name="translucent_grey">#80888888</color>
+
+ <!-- END WordPress.com Colors -->
+ <!-- ======================== -->
+
+ <!-- App specific colors -->
+ <!-- We could use blue_dark for the status bar color, but to adhere to the material design spec very closely,
+ we're using a shade of blue_wordpress -->
+ <color name="status_bar_tint">#006b98</color>
+ <color name="action_mode_status_bar_tint">#ff517188</color>
+
+ <!-- Media Gallery -->
+ <color name="media_gallery_grid_label_bg">@color/semi_transparent_grey_dark</color>
+ <color name="media_gallery_grid_label">@color/white</color>
+ <color name="media_gallery_bg">@color/grey_darken_30</color>
+ <color name="media_gallery_option_selected">@color/grey_dark</color>
+ <color name="media_gallery_option_default">@color/grey_lighten_30</color>
+
+ <!-- Theme Details -->
+ <color name="theme_details_name">@color/grey_dark</color>
+ <color name="theme_details_button">@color/white</color>
+ <color name="theme_details_premium">@color/orange_jazzy</color>
+ <color name="theme_feature_text">@color/grey_dark</color>
+
+ <!-- Tab Strip -->
+ <color name="tab_background">@color/color_primary</color>
+ <color name="tab_text_normal">@color/blue_light</color>
+ <color name="tab_text_selected">@color/white</color>
+ <color name="tab_indicator">@color/white</color>
+
+ <!-- Stats -->
+ <color name="stats_text_color">@color/grey_dark</color>
+ <color name="stats_link_text_color">@color/blue_wordpress</color>
+ <color name="stats_module_content_list_header">@color/grey</color>
+ <color name="stats_blue_labels">@color/grey_darken_20</color>
+ <color name="stats_bar_graph_main_series">@color/blue_wordpress</color>
+ <color name="stats_bar_graph_main_series_highlight">@color/orange_jazzy</color>
+ <color name="stats_bar_graph_outer_highlight">#1Af0821e</color> <!-- 10% Opacity on orange_jazzy #f0821e -->
+ <color name="stats_bar_graph_secondary_series">@color/blue_dark</color>
+ <color name="stats_empty_placeholder_color">@color/translucent_grey_lighten_30</color>
+
+ <!-- Reader -->
+ <color name="reader_divider_grey">@color/grey_lighten_30</color>
+ <color name="reader_follow">@color/grey_dark</color>
+ <color name="reader_following">@color/alert_green</color>
+ <color name="reader_hyperlink">@color/blue_medium</color>
+ <color name="filtered_list_suggestions">#f8f8f8</color>
+
+ <!-- Comment Status -->
+ <color name="comment_status_unapproved">@color/orange_jazzy</color>
+ <color name="comment_status_spam">@color/alert_red</color>
+
+ <!-- Notification Status -->
+ <color name="notification_status_unapproved">@color/alert_yellow</color>
+ <color name="notification_status_unapproved_dark">#eeac31</color>
+ <color name="notification_status_unapproved_background">#fef8ee</color>
+
+ <!-- NUX -->
+ <color name="nux_grey_button">@color/grey_lighten_20</color>
+ <color name="nux_alert_bg">@color/grey_dark</color>
+ <color name="nux_background">@color/blue_wordpress</color>
+ <color name="nux_eye_icon_color_closed">@color/grey_lighten_20</color>
+ <color name="nux_eye_icon_color_open">@color/grey_lighten_10</color>
+
+ <!-- Editor -->
+ <color name="image_options_label">@color/grey_lighten_20</color>
+ <!-- copied from private support lib resource: color/design_snackbar_background_color -->
+ <color name="snackbar_background_color">#323232</color>
+
+ <!-- Misc -->
+ <color name="pressed_wordpress">@color/semi_transparent_blue_light</color>
+ <color name="background_grey">@color/grey_lighten_30</color>
+ <color name="translucent_white">#A6FFFFFF</color>
+
+ <!--Me-->
+ <color name="me_divider">#D8E3EA</color>
+
+ <color name="passcodelock_background">@color/color_primary</color>
+ <color name="passcodelock_prompt_text_color">@color/white</color>
+ <color name="passcodelock_button_text_color">@color/white</color>
+
+ <!-- Notifications -->
+ <color name="notifications_settings_divider_color">#d0d5d9</color>
+
+ <!-- Site Settings -->
+ <color name="site_settings_pref_divider_color">#dce1e6</color>
+
+ <!-- Themes -->
+ <color name="theme_price">#4FB769</color>
+ <color name="theme_active">#A9E9FC</color>
+
+ <!-- dividers -->
+ <!-- TODO: consolidate divider colors -->
+ <color name="divider_grey">#D8E3EA</color>
+
+</resources>
diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..139dd8db7
--- /dev/null
+++ b/WordPress/src/main/res/values/dimens.xml
@@ -0,0 +1,292 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="settings_padding">16dp</dimen>
+ <dimen name="category_row_height">60dp</dimen>
+ <dimen name="category_parent_spinner_row_height">40dp</dimen>
+
+ <dimen name="toolbar_height">56dp</dimen>
+ <dimen name="toolbar_subtitle_height">48dp</dimen>
+ <dimen name="toolbar_content_offset">72dp</dimen>
+
+ <dimen name="fab_margin">@dimen/fab_margin_default</dimen>
+ <dimen name="fab_margin_default">16dp</dimen>
+ <dimen name="fab_margin_tablet">24dp</dimen>
+
+ <!-- http://www.google.com/design/spec/what-is-material/elevation-shadows.html#elevation-shadows-shadows -->
+ <dimen name="appbar_elevation">4dp</dimen>
+ <dimen name="filter_subbar_elevation">@dimen/appbar_elevation</dimen>
+
+ <dimen name="card_elevation">2dp</dimen>
+ <dimen name="card_elevation_pressed">8dp</dimen>
+
+ <dimen name="message_bar_elevation">2dp</dimen>
+ <dimen name="tabs_elevation">4dp</dimen>
+
+ <dimen name="media_grid_local_image_width">160dp</dimen>
+ <dimen name="media_grid_progress_height">50dp</dimen>
+
+ <dimen name="theme_details_fragment_width">650dp</dimen>
+ <dimen name="theme_details_fragment_height">450dp</dimen>
+
+ <dimen name="theme_details_dialog_min_width">650dp</dimen>
+ <dimen name="theme_details_dialog_height">450dp</dimen>
+
+ <dimen name="action_bar_spinner_width">312dp</dimen>
+ <dimen name="action_bar_spinner_y_offset">-8dp</dimen>
+
+ <dimen name="default_dialog_width">280dp</dimen>
+
+ <dimen name="post_editor_content_side_margin">20dp</dimen>
+ <dimen name="post_detail_button_size">48dp</dimen>
+
+ <!-- featured image in post list -->
+ <dimen name="postlist_featured_image_height_normal">128dp</dimen>
+ <dimen name="postlist_featured_image_height_tablet">220dp</dimen>
+ <dimen name="postlist_featured_image_height_tablet_large">280dp</dimen>
+ <dimen name="postlist_featured_image_height">@dimen/postlist_featured_image_height_normal</dimen>
+
+ <!-- left/right margin for content such as Me fragment, My Site fragment -->
+ <dimen name="content_margin_normal">@dimen/margin_medium</dimen>
+ <dimen name="content_margin_normal_landscape">48dp</dimen>
+ <dimen name="content_margin_tablet">48dp</dimen>
+ <dimen name="content_margin_tablet_big">96dp</dimen>
+ <dimen name="content_margin">@dimen/content_margin_normal</dimen>
+
+ <!-- spacing between icons in main tab strip -->
+ <dimen name="tabstrip_icon_spacing_zero">0dp</dimen>
+ <dimen name="tabstrip_icon_spacing_medium">24dp</dimen>
+ <dimen name="tabstrip_icon_spacing">@dimen/tabstrip_icon_spacing_zero</dimen>
+
+ <!--
+ native reader dimens
+ -->
+ <dimen name="margin_none">0dp</dimen>
+ <dimen name="margin_extra_small">2dp</dimen>
+ <dimen name="margin_small">4dp</dimen>
+ <dimen name="margin_medium">8dp</dimen>
+ <dimen name="margin_large">12dp</dimen>
+ <dimen name="margin_extra_large">16dp</dimen>
+ <dimen name="margin_filter_spinner">64dp</dimen>
+
+ <item name="wp_match_parent" type="dimen">-1</item>
+ <dimen name="reader_webview_width">@dimen/wp_match_parent</dimen>
+
+ <!-- left/right margin for reader cards -->
+ <dimen name="reader_card_margin_normal">0dp</dimen>
+ <dimen name="reader_card_margin_tablet">48dp</dimen>
+ <dimen name="reader_card_margin_tablet_big">96dp</dimen>
+ <dimen name="reader_card_margin">@dimen/reader_card_margin_normal</dimen>
+
+ <!-- left/right margin for reader detail -->
+ <dimen name="reader_detail_margin_normal">24dp</dimen>
+ <dimen name="reader_detail_margin_tablet">48dp</dimen>
+ <dimen name="reader_detail_margin_tablet_big">96dp</dimen>
+ <dimen name="reader_detail_margin">@dimen/reader_detail_margin_normal</dimen>
+
+ <dimen name="reader_related_post_margin">@dimen/margin_extra_large</dimen>
+ <dimen name="reader_related_post_image_size">48dp</dimen>
+
+ <dimen name="reader_thumbnail_strip_image_size">48dp</dimen>
+
+ <!-- height of divider between cards -->
+ <dimen name="reader_card_gutters">12dp</dimen>
+
+ <!-- padding inside the card (space between card border and card content) -->
+ <dimen name="reader_card_content_padding">16dp</dimen>
+
+ <dimen name="text_sz_extra_small">10sp</dimen>
+ <dimen name="text_sz_small">12sp</dimen>
+ <dimen name="text_sz_medium">14sp</dimen>
+ <dimen name="text_sz_large">16sp</dimen>
+ <dimen name="text_sz_extra_large">20sp</dimen>
+ <dimen name="text_sz_double_extra_large">24sp</dimen>
+ <dimen name="text_sz_triple_extra_large">26sp</dimen>
+
+ <dimen name="avatar_sz_tiny">18dp</dimen>
+ <dimen name="avatar_sz_extra_small">24dp</dimen>
+ <dimen name="avatar_sz_small">32dp</dimen>
+ <dimen name="avatar_sz_medium">38dp</dimen>
+ <dimen name="avatar_sz_large">64dp</dimen>
+ <dimen name="blavatar_sz_small">32dp</dimen>
+ <dimen name="blavatar_sz">40dp</dimen>
+
+ <dimen name="reader_button_icon">24dp</dimen>
+ <dimen name="reader_follow_icon">16dp</dimen>
+ <dimen name="reader_more_icon">48dp</dimen>
+
+ <dimen name="reader_detail_tag_height">48dp</dimen>
+
+ <dimen name="reader_featured_image_height_default">220dp</dimen>
+ <dimen name="reader_featured_image_height_tablet">280dp</dimen>
+ <dimen name="reader_featured_image_height_tablet_large">340dp</dimen>
+ <dimen name="reader_featured_image_height">@dimen/reader_featured_image_height_default</dimen>
+
+ <!-- margin to use when indenting comment replies under their parents -->
+ <dimen name="reader_comment_indent_per_level">24dp</dimen>
+
+ <!-- max size for images in Reader comments -->
+ <dimen name="reader_comment_max_image_size">160dp</dimen>
+
+ <dimen name="empty_list_title_text_size">24sp</dimen>
+ <dimen name="empty_list_description_text_size">18sp</dimen>
+ <dimen name="empty_list_title_side_margin">32dp</dimen>
+ <dimen name="empty_list_title_bottom_margin">8dp</dimen>
+ <dimen name="empty_list_description_bottom_margin">8dp</dimen>
+ <dimen name="empty_list_description_side_margin">48dp</dimen>
+
+ <dimen name="nux_title_font_size">28sp</dimen>
+ <dimen name="nux_edit_field_font_size">16sp</dimen>
+ <dimen name="nux_main_button_height">48dp</dimen>
+ <dimen name="nux_secondary_button_height">36dp</dimen>
+
+ <dimen name="nux_width">600dp</dimen>
+
+ <!-- editor -->
+ <dimen name="post_settings_featured_image_height_min">150dp</dimen>
+ <dimen name="post_settings_featured_image_height_max">300dp</dimen>
+
+ <!-- notifications -->
+ <dimen name="note_icon_sz">21dp</dimen>
+ <dimen name="comment_avatar_margin_top">6dp</dimen>
+ <dimen name="notifications_max_image_size">240dp</dimen>
+ <dimen name="notifications_adjusted_font_margin">10dp</dimen>
+ <dimen name="notifications_content_margin">0dp</dimen>
+ <dimen name="notifications_avatar_sz">48dp</dimen>
+ <dimen name="notifications_text_indent_sz">22dp</dimen>
+
+ <dimen name="progress_bar_height">3dp</dimen>
+
+ <!-- DetailListPreference -->
+ <dimen name="dlp_padding_start">20dp</dimen>
+ <dimen name="dlp_padding_end">24dp</dimen>
+ <dimen name="dlp_padding_top">6dp</dimen>
+ <dimen name="dlp_padding_bottom">6dp</dimen>
+ <dimen name="dlp_radio_margin_top">4dp</dimen>
+ <dimen name="dlp_text_margin_start">4dp</dimen>
+ <dimen name="dlp_text_margin_end">4dp</dimen>
+ <dimen name="dlp_title_padding_start">24dp</dimen>
+ <dimen name="dlp_title_padding_end">24dp</dimen>
+ <dimen name="dlp_title_padding_top">24dp</dimen>
+ <dimen name="dlp_title_padding_bottom">16dp</dimen>
+ <dimen name="dlp_title_logo_padding">8dp</dimen>
+ <dimen name="dlp_title_logo_width">36dp</dimen>
+ <dimen name="dlp_title_logo_height">36dp</dimen>
+
+ <!-- stats -->
+ <dimen name="stats_button_corner_radius">2dp</dimen>
+ <dimen name="stats_barchart_height">128dp</dimen>
+ <dimen name="stats_barchart_legend_item">12dp</dimen>
+
+ <!-- stats widget-->
+ <dimen name="stats_widget_icon_size">18dp</dimen>
+ <dimen name="stats_widget_text_sz">14dp</dimen>
+ <dimen name="stats_widget_text_sz_small">10dp</dimen>
+ <dimen name="stats_widget_min_height">62dp</dimen>
+ <dimen name="stats_widget_main_container_min_size">38dp</dimen>
+ <dimen name="stats_widget_main_container_size">48dp</dimen>
+ <dimen name="stats_widget_four_cells">250dp</dimen>
+ <dimen name="stats_widget_five_cells">320dp</dimen>
+ <dimen name="stats_widget_image_layout_size">12dp</dimen>
+ <dimen name="stats_widget_image_layout_margin">3dp</dimen>
+
+ <!-- MediaPicker -->
+ <dimen name="media_padding_left">2dp</dimen>
+ <dimen name="media_padding_top">2dp</dimen>
+ <dimen name="media_padding_right">2dp</dimen>
+ <dimen name="media_padding_bottom">0dp</dimen>
+ <dimen name="media_spacing_vertical">2dp</dimen>
+ <dimen name="media_spacing_horizontal">2dp</dimen>
+ <dimen name="media_item_height">128dp</dimen>
+ <dimen name="media_item_frame_margin_left">0dp</dimen>
+ <dimen name="media_item_frame_margin_top">0dp</dimen>
+ <dimen name="media_item_frame_margin_right">0dp</dimen>
+ <dimen name="media_item_frame_margin_bottom">0dp</dimen>
+ <dimen name="media_item_frame_padding_left">8dp</dimen>
+ <dimen name="media_item_frame_padding_top">8dp</dimen>
+ <dimen name="media_item_frame_padding_right">8dp</dimen>
+ <dimen name="media_item_frame_padding_bottom">8dp</dimen>
+
+ <!-- my site -->
+ <dimen name="my_site_add_button_padding_bottom">5dp</dimen>
+ <dimen name="my_site_blog_name_margin_top">11dp</dimen>
+ <dimen name="my_site_list_row_icon_size">24dp</dimen>
+ <dimen name="my_site_list_row_padding_left">8dp</dimen>
+ <dimen name="my_site_margin_general">10dp</dimen>
+ <dimen name="my_site_switch_site_button_padding_bottom">11dp</dimen>
+ <dimen name="my_site_no_site_view_margin">24dp</dimen>
+
+ <!--me-->
+ <dimen name="me_avatar_margin_top">24dp</dimen>
+ <dimen name="me_list_margin_top">20dp</dimen>
+ <dimen name="me_list_row_icon_size">24dp</dimen>
+
+ <!--site picker-->
+ <dimen name="site_picker_blavatar_margin_left">7dp</dimen>
+ <dimen name="site_picker_blavatar_margin_right">15dp</dimen>
+ <dimen name="site_picker_container_padding_bottom">11dp</dimen>
+ <dimen name="site_picker_container_padding_top">10dp</dimen>
+
+ <dimen name="menu_item_height">48dp</dimen>
+ <dimen name="menu_item_width">168dp</dimen>
+ <dimen name="menu_item_margin_normal">16dp</dimen>
+ <dimen name="menu_item_margin_tablet">24dp</dimen>
+ <dimen name="menu_item_margin">@dimen/menu_item_margin_normal</dimen>
+
+ <!-- Site Settings -->
+ <dimen name="site_settings_divider_height">1dp</dimen>
+ <dimen name="related_posts_dialog_padding_left">24dp</dimen>
+ <dimen name="related_posts_dialog_padding_top">12dp</dimen>
+ <dimen name="related_posts_dialog_padding_right">24dp</dimen>
+ <dimen name="related_posts_dialog_padding_bottom">12dp</dimen>
+ <dimen name="text_sz_related_post_small">11sp</dimen>
+ <dimen name="list_divider_height">1dp</dimen>
+ <dimen name="list_editor_input_max_width">500dp</dimen>
+ <dimen name="start_over_padding_top">40dp</dimen>
+ <dimen name="start_over_margin">20dp</dimen>
+ <dimen name="start_over_width">300dp</dimen>
+
+ <!--empty lists-->
+ <dimen name="empty_list_button_top_margin">32dp</dimen>
+
+ <!--theme browser-->
+ <dimen name="theme_browser_default_column_width">320dp</dimen>
+ <dimen name="theme_browser_cardview_margin_large">5dp</dimen>
+ <dimen name="theme_browser_cardview_margin_small">2dp</dimen>
+ <dimen name="theme_browser_cardview_header_margin">10dp</dimen>
+ <dimen name="theme_browser_separator_thickness">1dp</dimen>
+ <dimen name="theme_browser_more_button_height">40dp</dimen>
+ <dimen name="theme_browser_more_button_width">48dp</dimen>
+ <dimen name="theme_browser_header_button_height">50dp</dimen>
+ <dimen name="theme_browser_header_button_width">100dp</dimen>
+ <dimen name="theme_browser_more_button_padding">8dp</dimen>
+ <dimen name="drake_themes_width">400dp</dimen>
+ <dimen name="start_over_text_margin">25dp</dimen>
+ <dimen name="start_over_url_margin">22dp</dimen>
+ <dimen name="start_over_preference_margin_large">15dp</dimen>
+ <dimen name="start_over_preference_margin_medium">6dp</dimen>
+ <dimen name="start_over_preference_margin_small">4dp</dimen>
+ <dimen name="start_over_preference_padding_left">30dp</dimen>
+ <dimen name="start_over_icon_padding">2dp</dimen>
+ <dimen name="start_over_text_margin_left">37dp</dimen>
+ <dimen name="start_over_icon_size">24dp</dimen>
+ <dimen name="start_over_icon_margin_right">13dp</dimen>
+ <dimen name="start_over_title_margin">19dp</dimen>
+ <dimen name="start_over_summary_margin">53dp</dimen>
+
+ <!-- plans -->
+ <dimen name="plan_indicator_size">12dp</dimen>
+ <dimen name="plan_indicator_margin">6dp</dimen>
+ <dimen name="plan_icon_size">79dp</dimen>
+
+ <!-- gravatar tooltip -->
+ <dimen name="tooltip_padding">7dp</dimen>
+ <dimen name="tooltip_text_size">16sp</dimen>
+ <dimen name="tooltip_radius">5dp</dimen>
+
+ <!--people management-->
+ <dimen name="people_avatar_sz">40dp</dimen>
+ <dimen name="people_list_row_height">72dp</dimen>
+ <dimen name="people_list_row_role_margin_top">20dp</dimen>
+ <dimen name="people_list_divider_height">1dp</dimen>
+ <dimen name="people_list_divider_left_margin">72dp</dimen>
+</resources>
diff --git a/WordPress/src/main/res/values/ids.xml b/WordPress/src/main/res/values/ids.xml
new file mode 100644
index 000000000..fead989ec
--- /dev/null
+++ b/WordPress/src/main/res/values/ids.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item type="id" name="row_post_id" />
+ <item type="id" name="drag_handle" />
+ <item type="id" name="click_remove" />
+ <item type="id" name="stats_tags" />
+ <item type="id" name="stats_geoviews" />
+ <item type="id" name="stats_comments" />
+ <item type="id" name="stats_referrers" />
+ <item type="id" name="stats_clicks" />
+ <item type="id" name="stats_search" />
+ <item type="id" name="stats_top_authors" />
+ <item type="id" name="stats_top_posts" />
+ <item type="id" name="note_block_tag_id" />
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values/integers.xml b/WordPress/src/main/res/values/integers.xml
new file mode 100644
index 000000000..9bbd395b3
--- /dev/null
+++ b/WordPress/src/main/res/values/integers.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="media_grid_num_columns">2</integer>
+ <integer name="themes_grid_num_columns">2</integer>
+ <integer name="visible">0</integer>
+ <integer name="gone">2</integer>
+ <integer name="media_editor_save_button_visibility">@integer/gone</integer>
+ <integer name="isSW600DP">0</integer>
+ <integer name="smallest_width_dp">320</integer>
+
+ <!-- max #chars user can type when adding a new topic to the reader -->
+ <integer name="max_length_reader_topic_name">96</integer>
+
+ <!-- max #chars for usernames and passwords -->
+ <integer name="max_length_username">254</integer>
+ <integer name="max_length_password">254</integer>
+
+ <!-- MediaPicker -->
+ <integer name="num_media_columns">2</integer>
+
+ <integer name="fab_animation_delay">300</integer>
+
+ <!-- Site Settings -->
+ <integer name="max_links_limit">100</integer>
+ <integer name="paging_limit">300</integer>
+ <integer name="threading_limit">10</integer>
+ <integer name="close_after_limit">365</integer>
+
+ <!-- People Management -->
+ <integer name="invite_message_char_limit">500</integer>
+
+</resources>
diff --git a/WordPress/src/main/res/values/key_strings.xml b/WordPress/src/main/res/values/key_strings.xml
new file mode 100644
index 000000000..d0540fb37
--- /dev/null
+++ b/WordPress/src/main/res/values/key_strings.xml
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- layout tags - do not translate -->
+ <string name="fragment_tag_comment_list" translatable="false">fragment_comment_list</string>
+ <string name="fragment_tag_comment_detail" translatable="false">fragment_comment_detail</string>
+ <string name="fragment_tag_reader_post_list" translatable="false">fragment_reader_post_list</string>
+ <string name="fragment_tag_reader_post_detail" translatable="false">fragment_reader_post_detail</string>
+ <string name="fragment_tag_post_preview" translatable="false">fragment_post_preview</string>
+
+ <!-- Preference keys -->
+ <string name="pref_key_account_settings_root" translatable="false">wp_pref_root</string>
+ <string name="pref_key_app_settings_root" translatable="false">wp_pref_root</string>
+ <string name="pref_key_passlock_section" translatable="false">wp_passcode_lock_category</string>
+ <string name="pref_key_passlock" translatable="false">wp_pref_passlock_enabled</string>
+ <string name="pref_key_send_usage" translatable="false">wp_pref_send_usage_stats</string>
+ <string name="pref_key_about_section" translatable="false">wp_pref_app_about_section</string>
+ <string name="pref_key_language" translatable="false">wp_pref_language</string>
+ <string name="pref_key_app_about" translatable="false">wp_pref_app_about</string>
+ <string name="pref_key_oss_licenses" translatable="false">wp_pref_open_source_licenses</string>
+ <string name="pref_key_editor" translatable="false">wp_pref_editor</string>
+ <string name="pref_key_visual_editor_enabled" translatable="false">wp_pref_visual_editor_enabled</string>
+ <string name="pref_notification_blogs" translatable="false">wp_pref_notification_blogs</string>
+ <string name="pref_notification_other_category" translatable="false">wp_pref_notification_other_category</string>
+ <string name="pref_notification_other_blogs" translatable="false">wp_pref_notification_other_blogs</string>
+ <string name="pref_notifications_enabled" translatable="false">wp_pref_notifications_enabled</string>
+ <string name="pref_notification_types" translatable="false">wp_pref_notification_types</string>
+ <string name="pref_notification_account_emails" translatable="false">wp_pref_notification_account_emails</string>
+ <string name="pref_notification_sights_sounds" translatable="false">wp_pref_notification_sights_sounds</string>
+ <string name="pref_key_notification_site_settings" translatable="false">wp_pref_notification_site_settings</string>
+ <string name="pref_key_site_screen" translatable="false">wp_pref_site_screen</string>
+ <string name="pref_key_site_general" translatable="false">wp_pref_site_general</string>
+ <string name="pref_key_site_account" translatable="false">wp_pref_site_account</string>
+ <string name="pref_key_site_writing" translatable="false">wp_pref_site_writing</string>
+ <string name="pref_key_site_title" translatable="false">wp_pref_site_title</string>
+ <string name="pref_key_site_tagline" translatable="false">wp_pref_site_tagline</string>
+ <string name="pref_key_site_address" translatable="false">wp_pref_site_address</string>
+ <string name="pref_key_site_language" translatable="false">wp_pref_site_language</string>
+ <string name="pref_key_site_visibility" translatable="false">wp_pref_site_visibility</string>
+ <string name="pref_key_site_username" translatable="false">wp_pref_site_username</string>
+ <string name="pref_key_site_password" translatable="false">wp_pref_site_password</string>
+ <string name="pref_key_site_related_posts" translatable="false">wp_pref_site_related_posts</string>
+ <string name="pref_key_site_location" translatable="false">wp_pref_site_location</string>
+ <string name="pref_key_site_category" translatable="false">wp_pref_site_default_category</string>
+ <string name="pref_key_site_format" translatable="false">wp_pref_site_default_format</string>
+ <string name="pref_key_site_this_device" translatable="false">wp_pref_site_default_this_device</string>
+ <string name="pref_key_site_image_width" translatable="false">wp_pref_site_default_image_width</string>
+ <string name="pref_key_site_upload_and_link_image" translatable="false">wp_pref_site_upload_and_link_image</string>
+ <string name="pref_key_site_discussion" translatable="false">wp_pref_site_discussion</string>
+ <string name="pref_key_site_allow_comments" translatable="false">wp_pref_site_allow_comments</string>
+ <string name="pref_key_site_allow_comments_nested" translatable="false">wp_pref_site_allow_comments_nested</string>
+ <string name="pref_key_site_send_pingbacks" translatable="false">wp_pref_site_send_pingbacks</string>
+ <string name="pref_key_site_send_pingbacks_nested" translatable="false">wp_pref_site_send_pingbacks_nested</string>
+ <string name="pref_key_site_receive_pingbacks" translatable="false">wp_pref_site_receive_pingbacks</string>
+ <string name="pref_key_site_receive_pingbacks_nested" translatable="false">wp_pref_site_receive_pingbacks_nested</string>
+ <string name="pref_key_site_more_discussion" translatable="false">wp_pref_site_more_discussion</string>
+ <string name="pref_key_site_learn_more" translatable="false">wp_pref_site_learn_more</string>
+ <string name="pref_key_site_close_after" translatable="false">wp_pref_site_close_after</string>
+ <string name="pref_key_site_sort_by" translatable="false">wp_pref_site_sort_by</string>
+ <string name="pref_key_site_threading" translatable="false">wp_pref_site_threading</string>
+ <string name="pref_key_site_paging" translatable="false">wp_pref_site_paging</string>
+ <string name="pref_key_site_manual_approval" translatable="false">wp_pref_site_manual_approval</string>
+ <string name="pref_key_site_identity_required" translatable="false">wp_pref_site_identity_required</string>
+ <string name="pref_key_site_user_account_required" translatable="false">wp_pref_site_user_account_required</string>
+ <string name="pref_key_site_whitelist" translatable="false">wp_pref_site_whitelist</string>
+ <string name="pref_key_site_multiple_links" translatable="false">wp_pref_site_multiple_links</string>
+ <string name="pref_key_site_moderation_hold" translatable="false">wp_pref_site_moderation_hold</string>
+ <string name="pref_key_site_blacklist" translatable="false">wp_pref_site_blacklist</string>
+ <string name="pref_key_site_danger" translatable="false">wp_pref_site_danger</string>
+ <string name="pref_key_site_advanced" translatable="false">wp_pref_site_advanced</string>
+ <string name="pref_key_site_delete_site_screen" translatable="false">wp_pref_site_delete_site_screen</string>
+ <string name="pref_key_site_start_over" translatable="false">wp_pref_site_start_over</string>
+ <string name="pref_key_site_export_site" translatable="false">pref_key_site_export_site</string>
+ <string name="pref_key_site_delete_site" translatable="false">wp_pref_site_delete_site</string>
+ <string name="pref_key_site_remove_site" translatable="false">wp_pref_site_remove_site</string>
+ <string name="pref_key_username" translatable="false">wp_pref_key_username</string>
+ <string name="pref_key_email" translatable="false">wp_pref_key_email</string>
+ <string name="pref_key_primary_site" translatable="false">wp_pref_key_primary_site></string>
+ <string name="pref_key_web_address" translatable="false">wp_pref_key_web_address</string>
+ <string name="pref_key_site_start_over_screen" translatable="false">wp_pref_key_site_start_over_screen</string>
+
+ <!-- Notifications -->
+ <string-array name="notifications_blog_settings_values" translatable="false">
+ <item>new_comment</item>
+ <item>comment_like</item>
+ <item>post_like</item>
+ <item>follow</item>
+ <item>achievement</item>
+ <item>mentions</item>
+ </string-array>
+
+ <string-array name="notifications_other_settings_values" translatable="false">
+ <item>comment_reply</item>
+ <item>comment_like</item>
+ </string-array>
+
+ <string-array name="notifications_wpcom_settings_values" translatable="false">
+ <item>marketing</item>
+ <item>research</item>
+ <item>community</item>
+ </string-array>
+
+ <!-- Preference Entries -->
+ <string-array name="site_settings_image_width_entries" translatable="false">
+ <item>@string/site_settings_image_original_size</item>
+ <item>100</item>
+ <item>200</item>
+ <item>300</item>
+ <item>400</item>
+ <item>500</item>
+ <item>600</item>
+ <item>700</item>
+ <item>800</item>
+ <item>900</item>
+ <item>1000</item>
+ <item>1100</item>
+ <item>1200</item>
+ <item>1300</item>
+ <item>1400</item>
+ <item>1500</item>
+ <item>1600</item>
+ <item>1700</item>
+ <item>1800</item>
+ <item>1900</item>
+ <item>2000</item>
+ </string-array>
+
+ <!-- Preference Values -->
+ <string-array name="site_settings_auto_approve_values" translatable="false">
+ <item>-1</item>
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+ <string-array name="site_settings_sort_values" translatable="false">
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+ <string-array name="site_settings_privacy_values" translatable="false">
+ <item>1</item>
+ <item>0</item>
+ <item>-1</item>
+ </string-array>
+ <string-array name="site_settings_image_width_values" translatable="false">
+ <item>@string/site_settings_image_original_size</item>
+ <item>100</item>
+ <item>200</item>
+ <item>300</item>
+ <item>400</item>
+ <item>500</item>
+ <item>600</item>
+ <item>700</item>
+ <item>800</item>
+ <item>900</item>
+ <item>1000</item>
+ <item>1100</item>
+ <item>1200</item>
+ <item>1300</item>
+ <item>1400</item>
+ <item>1500</item>
+ <item>1600</item>
+ <item>1700</item>
+ <item>1800</item>
+ <item>1900</item>
+ <item>2000</item>
+ </string-array>
+
+ <!-- Used as values to a preference in Site Settings -->
+ <string-array name="language_codes" translatable="false">
+ <item>en_US</item>
+ <item>az</item>
+ <item>de</item>
+ <item>el</item>
+ <item>es</item>
+ <item>es_CL</item>
+ <item>fr</item>
+ <item>gd</item>
+ <item>hi</item>
+ <item>hu</item>
+ <item>id</item>
+ <item>it</item>
+ <item>ja</item>
+ <item>ko</item>
+ <item>nl</item>
+ <item>pl</item>
+ <item>ru</item>
+ <item>sv</item>
+ <item>th</item>
+ <item>uz</item>
+ <item>zh_CN</item>
+ <item>zh_TW</item>
+ <item>en_GB</item>
+ <item>tr</item>
+ <item>eu</item>
+ <item>he</item>
+ <item>pt_BR</item>
+ <item>ar</item>
+ <item>ro</item>
+ <item>mk</item>
+ <item>sr</item>
+ <item>sk</item>
+ <item>cy</item>
+ <item>da</item>
+ <item>bg</item>
+ <item>sq</item>
+ <item>hr</item>
+ <item>cs</item>
+ </string-array>
+
+ <!-- Language IDs for WordPress REST call -->
+ <!-- ref https://wpcom.trac.automattic.com/browser/trunk/.config/locales.php -->
+ <string-array name="lang_ids" translatable="false">
+ <item>1</item>
+ <item>79</item>
+ <item>15</item>
+ <item>17</item>
+ <item>19</item>
+ <item>484</item>
+ <item>24</item>
+ <item>476</item>
+ <item>30</item>
+ <item>31</item>
+ <item>33</item>
+ <item>35</item>
+ <item>36</item>
+ <item>40</item>
+ <item>49</item>
+ <item>58</item>
+ <item>62</item>
+ <item>68</item>
+ <item>71</item>
+ <item>458</item>
+ <item>449</item>
+ <item>452</item>
+ <item>482</item>
+ <item>78</item>
+ <item>429</item>
+ <item>29</item>
+ <item>438</item>
+ <item>3</item>
+ <item>61</item>
+ <item>435</item>
+ <item>67</item>
+ <item>64</item>
+ <item>13</item>
+ <item>14</item>
+ <item>6</item>
+ <item>66</item>
+ <item>431</item>
+ <item>11</item>
+ </string-array>
+
+ <!-- Smart Lock for Passwords -->
+ <string name="asset_statements" translatable="false">
+ [{\"include\": \"https://wordpress.com/.well-known/assetlinks.json\"}]
+ </string>
+</resources>
diff --git a/WordPress/src/main/res/values/reader_styles.xml b/WordPress/src/main/res/values/reader_styles.xml
new file mode 100644
index 000000000..838e9a79a
--- /dev/null
+++ b/WordPress/src/main/res/values/reader_styles.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ styles used by the native reader - all margins & padding use margin_xxxx sizes
+ and all font sizes use text_sz_xxxx sizes defined in dimens.xml for consistency
+-->
+<resources>
+ <style name="ReaderPhotoViewerTheme" parent="Theme.AppCompat.NoActionBar">
+ <item name="colorAccent">@color/accent_material_dark</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowFullscreen">true</item>
+ </style>
+
+ <!-- TextViews -->
+ <style name="ReaderTextView" parent="android:Widget.TextView">
+ <item name="android:textColorLink">@color/reader_hyperlink</item>
+ </style>
+ <style name="ReaderTextView.Label" parent="ReaderTextView">
+ <item name="android:textColor">@color/grey_darken_10</item>
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ </style>
+ <style name="ReaderTextView.Label.Medium" parent="ReaderTextView.Label">
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ </style>
+ <style name="ReaderTextView.Label.Large" parent="ReaderTextView.Label">
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ </style>
+ <style name="ReaderTextView.Post.Title" parent="ReaderTextView">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:maxLines">3</item>
+ <item name="wpFontFamily">merriweather</item>
+ </style>
+ <style name="ReaderTextView.Post.Title.Detail" parent="ReaderTextView.Post.Title">
+ <item name="android:textSize">@dimen/text_sz_extra_large</item>
+ <item name="android:maxLines">5</item>
+ </style>
+ <style name="ReaderTextView.Post.Excerpt" parent="ReaderTextView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:lineSpacingMultiplier">1.1</item>
+ <item name="android:maxLines">3</item>
+ <item name="android:ellipsize">end</item>
+ <item name="wpFontFamily">merriweather</item>
+ </style>
+ <style name="ReaderTextView.Date" parent="ReaderTextView">
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:textColor">@color/grey_darken_10</item>
+ <item name="android:drawableLeft">@drawable/noticon_clock</item>
+ </style>
+ <style name="ReaderTextView.Date.Large" parent="ReaderTextView">
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textColor">@color/grey_darken_10</item>
+ <item name="android:drawableLeft">@drawable/noticon_clock</item>
+ </style>
+ <style name="ReaderTextView.Action" parent="ReaderTextView">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:textColor">@color/blue_medium</item>
+ </style>
+ <style name="ReaderTextView.EmptyList" parent="ReaderTextView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ </style>
+
+ <!-- EditTexts -->
+ <style name="ReaderEditText" parent="android:Widget.EditText">
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ <item name="android:textColorHint">@color/grey_darken_10</item>
+ </style>
+ <style name="ReaderEditText.Topic" parent="ReaderEditText">
+ <item name="android:background">@color/transparent</item>
+ <item name="android:padding">@dimen/margin_medium</item>
+ <item name="android:inputType">text</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:imeOptions">actionDone</item>
+ <item name="android:maxLength">@integer/max_length_reader_topic_name</item>
+ </style>
+
+ <!-- images -->
+ <style name="ReaderImageView" />
+ <style name="ReaderImageView.Featured" parent="ReaderImageView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/reader_featured_image_height</item>
+ <item name="android:scaleType">centerCrop</item>
+ </style>
+ <style name="ReaderImageView.Avatar" parent="ReaderImageView">
+ <item name="android:layout_width">@dimen/avatar_sz_medium</item>
+ <item name="android:layout_height">@dimen/avatar_sz_medium</item>
+ </style>
+ <style name="ReaderImageView.Avatar.Small" parent="ReaderImageView">
+ <item name="android:layout_width">@dimen/avatar_sz_small</item>
+ <item name="android:layout_height">@dimen/avatar_sz_small</item>
+ </style>
+ <style name="ReaderImageView.Avatar.ExtraSmall" parent="ReaderImageView">
+ <item name="android:layout_width">@dimen/avatar_sz_extra_small</item>
+ <item name="android:layout_height">@dimen/avatar_sz_extra_small</item>
+ </style>
+ <style name="ReaderImageView.Avatar.Tiny" parent="ReaderImageView">
+ <item name="android:layout_width">@dimen/avatar_sz_tiny</item>
+ <item name="android:layout_height">@dimen/avatar_sz_tiny</item>
+ </style>
+
+ <!-- progress bars -->
+ <style name="ReaderProgressBar" parent="@android:style/Widget.Holo.Light.ProgressBar" />
+
+ <style name="ReaderTabStripTextAppearance">
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColor">@color/tab_text_color</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:gravity">center</item>
+ </style>
+</resources>
diff --git a/WordPress/src/main/res/values/stats_styles.xml b/WordPress/src/main/res/values/stats_styles.xml
new file mode 100644
index 000000000..aa7ac234d
--- /dev/null
+++ b/WordPress/src/main/res/values/stats_styles.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="StatsWhiteBackground">
+ <item name="android:background">@drawable/stats_white_background</item>
+ </style>
+
+ <style name="StatsHeader">
+ <item name="android:textColor">@color/stats_module_content_list_header</item>
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ <item name="android:textStyle">bold</item>
+ <!-- item name="android:textAllCaps">true</item -->
+ </style>
+
+ <!-- Used in activity details -->
+ <style name="StatsInsightsValues">
+ <item name="android:textSize">@dimen/text_sz_extra_large</item>
+ <item name="android:layout_centerHorizontal">true</item>
+ <item name="android:layout_gravity">top</item>
+ <item name="android:singleLine">true</item>
+ <!-- item name="android:fontFamily">sans-serif-light</item -->
+ </style>
+
+ <style name="StatsInsightsLabel">
+ <item name="android:textSize">@dimen/text_sz_extra_small</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:layout_centerHorizontal">true</item>
+ <item name="android:layout_gravity">bottom</item>
+ <!-- item name="android:fontFamily">sans-serif-light</item -->
+ </style>
+
+ <style name="StatsViewAllButton">
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:button">@android:color/transparent</item>
+ <item name="android:minHeight">48dp</item>
+ <item name="android:paddingLeft">@dimen/margin_large</item>
+ <item name="android:paddingRight">@dimen/margin_large</item>
+ <item name="android:textColor">@color/blue_wordpress</item>
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ </style>
+
+ <style name="StatsModuleTitle">
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <!-- TextViews -->
+ <!-- style name="StatsTextView" parent="android:Widget.TextView">
+ <item name="android:textColorLink">@color/reader_hyperlink</item>
+ </style>
+
+ <style name="StatsTextView.SecondaryAction" parent="StatsTextView">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ <item name="android:minWidth">60dp</item>
+ <item name="android:gravity">center</item>
+ <item name="android:drawableLeft">@drawable/dashicon_external_black</item>
+ <item name="android:background">@drawable/reader_button_follow</item>
+ <item name="android:textColor">@color/reader_follow_text</item>
+ <item name="android:paddingLeft">@dimen/margin_small</item>
+ <item name="android:paddingTop">@dimen/margin_small</item>
+ <item name="android:paddingBottom">@dimen/margin_small</item>
+ <item name="android:paddingRight">@dimen/margin_small</item>
+ <item name="android:layout_marginRight">@dimen/margin_medium</item>
+ <item name="android:drawablePadding">0dp</item>
+ <item name="android:text">View</item>
+ </style -->
+</resources> \ No newline at end of file
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
new file mode 100644
index 000000000..31cae06aa
--- /dev/null
+++ b/WordPress/src/main/res/values/strings.xml
@@ -0,0 +1,1579 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name" translatable="false">WordPress</string>
+
+ <!-- account setup -->
+ <string name="xmlrpc_error">Couldn\'t connect. Enter the full path to xmlrpc.php on your site and try again.</string>
+ <string name="xmlrpc_missing_method_error">Couldn\'t connect. Required XML-RPC methods are missing on the server.</string>
+ <string name="xmlrpc_malformed_response_error">Couldn\'t connect. The WordPress installation responded with an invalid XML-RPC document.</string>
+ <string name="site_timeout_error">Couldn\'t connect to the WordPress site due to Timeout error.</string>
+ <string name="no_network_title">No network available</string>
+ <string name="no_network_message">There is no network available</string>
+ <string name="sign_out_wpcom_confirm">Disconnecting your account will remove all of @%s’s WordPress.com data from this device, including local drafts and local changes.</string>
+ <string name="account_two_step_auth_enabled">This account has two step authentication enabled. Visit your security settings on WordPress.com and generate an application-specific password.</string>
+
+ <!-- form labels -->
+ <string name="select_categories">Select categories</string>
+ <string name="tags_separate_with_commas">Tags (separate tags with commas)</string>
+ <string name="post_content">Content (tap to add text and media)</string>
+ <string name="max_thumbnail_px_width">Default Image Width</string>
+ <string name="password">Password</string>
+ <string name="blogs">Blogs</string>
+ <string name="account_details">Account details</string>
+ <string name="status">Status</string>
+
+ <!-- comment form labels -->
+ <string name="anonymous">Anonymous</string>
+
+ <!-- general strings -->
+ <string name="device">Device</string>
+ <string name="post">Post</string>
+ <string name="page">Page</string>
+ <string name="posts">Posts</string>
+ <string name="media">Media</string>
+ <string name="video">Video</string>
+ <string name="themes">Themes</string>
+ <string name="pages">Pages</string>
+ <string name="about_the_app">About the app</string>
+ <string name="username">Username</string>
+ <string name="cancel">Cancel</string>
+ <string name="save">Save</string>
+ <string name="add">Add</string>
+ <string name="remove">Remove</string>
+ <string name="search">Search</string>
+ <string name="show">Show</string>
+ <string name="hide">Hide</string>
+ <string name="select_all">Select all</string>
+ <string name="deselect_all">Deselect all</string>
+ <string name="notification_sound">Notification sound</string>
+ <string name="notification_vibrate">Vibrate</string>
+ <string name="notification_blink">Blink notification light</string>
+ <string name="sure_to_remove_account">Remove this site?</string>
+ <string name="yes">Yes</string>
+ <string name="no">No</string>
+ <string name="error">Error</string>
+ <string name="could_not_remove_account">Couldn\'t remove site</string>
+ <string name="edit_post">Edit post</string>
+ <string name="add_comment">Add comment</string>
+ <string name="connection_error">Connection error</string>
+ <string name="category_refresh_error">Category refresh error</string>
+ <string name="incorrect_credentials">Incorrect username or password.</string>
+ <string name="cancel_edit">Cancel edit</string>
+ <string name="upload_full_size_image">Upload and link to full image</string>
+ <string name="upload_scaled_image">Upload and link to scaled image</string>
+ <string name="scaled_image_error">Enter a valid scaled width value</string>
+ <string name="scaled_image">Scaled image width</string>
+ <string name="immediately">Immediately</string>
+ <string name="gallery_error">The media item couldn\'t be retrieved</string>
+ <string name="refresh">Refresh</string>
+ <string name="blog_not_found">An error occurred when accessing this blog</string>
+ <string name="post_not_found">An error occurred when loading the post. Refresh your posts and try again.</string>
+ <string name="sign_in">Sign in</string>
+ <string name="signing_out">Signing out…</string>
+ <string name="upload">Upload</string>
+ <string name="learn_more">Learn more</string>
+ <string name="posting_post">Posting \"%s\"</string>
+ <string name="language">Language</string>
+ <string name="interface_language">Interface Language</string>
+ <string name="signout">Disconnect</string>
+ <string name="undo">Undo</string>
+ <string name="never">Never</string>
+ <string name="unknown">Unknown</string>
+ <string name="off">Off</string>
+ <string name="could_not_load_page">Could not load page</string>
+ <string name="send">Send</string>
+
+ <string name="button_skip">Skip</string>
+ <string name="button_next">Next</string>
+ <string name="button_done">Done</string>
+
+ <!-- timestamps for posts / pages -->
+ <string name="today">Today</string>
+ <string name="yesterday">Yesterday</string>
+ <string name="days_ago">%d days ago</string>
+
+ <!-- MediaPicker -->
+ <string name="add_to_post">Add to Post</string>
+ <string name="media_picker_title">Select media</string>
+ <string name="take_photo">Take a photo</string>
+ <string name="take_video">Take a video</string>
+ <string name="tab_title_device_images">Device Images</string>
+ <string name="tab_title_device_videos">Device Videos</string>
+ <string name="tab_title_site_images">Site Images</string>
+ <string name="tab_title_site_videos">Site Videos</string>
+
+ <!-- Media Gallery Action Bar -->
+ <string name="media_add_popup_title">Add to media library</string>
+ <string name="media_add_popup_capture_photo">Capture photo</string>
+ <string name="media_add_popup_capture_video">Capture video</string>
+ <string name="media_add_new_media_gallery">Create gallery</string>
+ <string name="media_gallery_date_range" comment="Displaying media from 2014-01-13 to 2014-01-23">Displaying media from %1$s to %2$s</string>
+
+ <!-- CAB -->
+ <string name="cab_selected">%d selected</string>
+
+ <!-- Media Gallery -->
+ <string name="all">All</string>
+ <string name="images">Images</string>
+ <string name="unattached">Unattached</string>
+ <string name="custom_date" comment="one of the media page filter (others are all, images, unattached, custom date), after selecting Custom Date, you can choose a date range to filter media files">Custom Date</string>
+ <string name="media_gallery_settings_title">Gallery settings</string>
+ <string name="media_gallery_image_order">Image order</string>
+ <string name="media_gallery_image_order_random">Random</string>
+ <string name="media_gallery_image_order_reverse">Reverse</string>
+ <string name="media_gallery_num_columns">Number of columns</string>
+ <string name="media_gallery_type">Type</string>
+ <string name="media_gallery_type_thumbnail_grid">Thumbnail grid</string>
+ <string name="media_gallery_type_squares">Squares</string>
+ <string name="media_gallery_type_tiled">Tiled</string>
+ <string name="media_gallery_type_circles">Circles</string>
+ <string name="media_gallery_type_slideshow">Slideshow</string>
+ <string name="media_gallery_edit">Edit gallery</string>
+ <string name="pick_photo">Select photo</string>
+ <string name="pick_video">Select video</string>
+ <string name="capture_or_pick_photo">Capture or select photo</string>
+ <string name="reader_toast_err_get_post">Unable to retrieve this post</string>
+ <string name="media_error_no_permission">You don\'t have permission to view the media library</string>
+ <string name="media_error_no_permission_upload">You don\'t have permission to upload media to the site</string>
+ <string name="access_media_permission_required">Permissions required in order to access media</string>
+ <string name="add_media_permission_required">Permissions required in order to add media</string>
+ <string name="media_fetching">Fetching media…</string>
+ <string name="loading_videos">Loading videos</string>
+ <string name="loading_images">Loading images</string>
+ <string name="loading_blog_images">Fetching images</string>
+ <string name="loading_blog_videos">Fetching videos</string>
+ <string name="no_media">No media</string>
+ <string name="no_media_sources">Couldn\'t fetch media</string>
+ <string name="error_loading_images">Error loading images</string>
+ <string name="error_loading_videos">Error loading videos</string>
+ <string name="error_loading_blog_images">Unable to fetch images</string>
+ <string name="error_loading_blog_videos">Unable to fetch videos</string>
+ <string name="no_device_images">No images</string>
+ <string name="no_device_videos">No videos</string>
+ <string name="no_blog_images">No images</string>
+ <string name="no_blog_videos">No videos</string>
+ <string name="upload_queued">Queued</string>
+ <string name="media_file_type">File type: %s</string>
+ <string name="media_file_name">File name: %s</string>
+ <string name="media_uploaded_on">Uploaded on: %s</string>
+ <string name="media_dimensions">Dimensions: %s</string>
+
+ <!-- Upload Media -->
+ <string name="image_added">Image added</string>
+
+ <!-- Edit Media -->
+ <string name="media_edit_title_text">Title</string>
+ <string name="media_edit_caption_text">Caption</string>
+ <string name="media_edit_description_text">Description</string>
+ <string name="media_edit_title_hint">Enter a title here</string>
+ <string name="media_edit_caption_hint">Enter a caption here</string>
+ <string name="media_edit_description_hint">Enter a description here</string>
+ <string name="media_edit_success">Updated</string>
+ <string name="media_edit_failure">Failed to update</string>
+ <string name="saving">Saving…</string>
+
+ <!-- Delete Media -->
+ <string name="confirm_delete_media">Delete selected item?</string>
+ <string name="confirm_delete_multi_media">Delete selected items?</string>
+ <string name="wait_until_upload_completes">Wait until upload completes</string>
+ <string name="cannot_delete_multi_media_items">Some media can\'t be deleted at this time. Try again later.</string>
+ <string name="media_empty_list">No media</string>
+ <string name="media_empty_list_custom_date">No media in this time interval</string>
+ <string name="delete">Delete</string>
+
+ <!-- Media details -->
+ <string name="media_details_label_date_added">Added</string>
+ <string name="media_details_label_date_uploaded">Uploaded</string>
+ <string name="media_details_label_file_name">File name</string>
+ <string name="media_details_label_file_type">File type</string>
+ <string name="media_details_copy_url">Copy URL</string>
+ <string name="media_details_copy_url_toast">URL copied to clipboard</string>
+
+ <!-- tab titles -->
+ <string name="tab_comments" translatable="false">@string/comments</string>
+
+ <!-- themes -->
+ <string name="themes_live_preview">Live preview</string>
+ <string name="themes_details_label">Details</string>
+ <string name="themes_features_label">Features</string>
+ <string name="themes_fetching">Fetching themes…</string>
+
+ <string name="theme_activate_button">Activate</string>
+ <string name="theme_activating_button">Activating</string>
+ <string name="theme_fetch_failed">Failed to fetch themes</string>
+ <string name="theme_set_failed">Failed to set theme</string>
+ <string name="theme_set_success">Successfully set theme!</string>
+ <string name="theme_auth_error_title">Failed to fetch themes</string>
+ <string name="theme_auth_error_message">Ensure you have the privilege to set themes</string>
+ <string name="theme_current_theme">Current theme</string>
+ <string name="theme_premium_theme">Premium theme</string>
+ <string name="theme_no_search_result_found">Sorry, no themes found.</string>
+ <string name="theme_auth_error_authenticate">Failed to fetch themes: failed authenticate user</string>
+
+ <!-- link view -->
+ <string name="link_enter_url">URL</string>
+ <string name="link_enter_url_text">Link text (optional)</string>
+ <string name="create_a_link">Create a link</string>
+
+ <!-- page view -->
+ <string name="title">Title</string>
+ <string name="pages_empty_list">No pages yet. Why not create one?</string>
+ <string name="page_id">Page</string>
+ <string name="page_settings">Page settings</string>
+
+ <!-- posts tab -->
+ <string name="untitled">Untitled</string>
+ <string name="local_draft">Local draft</string>
+ <string name="post_uploading">Uploading</string>
+ <string name="posts_empty_list">No posts yet. Why not create one?</string>
+ <string name="empty_list_default">This list is empty</string>
+
+ <!-- buttons on post cards -->
+ <string name="button_edit">Edit</string>
+ <string name="button_publish">Publish</string>
+ <string name="button_view">View</string>
+ <string name="button_preview">Preview</string>
+ <string name="button_stats">Stats</string>
+ <string name="button_trash">Trash</string>
+ <string name="button_delete" translatable="false">@string/delete</string>
+ <string name="button_more" translatable="false">@string/more</string>
+ <string name="button_back">Back</string>
+ <string name="button_revert">Revert</string>
+
+ <!-- dropdown filter above post cards -->
+ <string name="filter_published_posts">Published</string>
+ <string name="filter_draft_posts">Drafts</string>
+ <string name="filter_scheduled_posts">Scheduled</string>
+ <string name="filter_trashed_posts">Trashed</string>
+ <string-array name="post_filters_array" translatable="false">
+ <item>@string/filter_published_posts</item>
+ <item>@string/filter_draft_posts</item>
+ <item>@string/filter_scheduled_posts</item>
+ <item>@string/filter_trashed_posts</item>
+ </string-array>
+
+ <!-- post view -->
+ <string name="post_id">Post</string>
+ <string name="upload_failed">Upload failed</string>
+ <string name="post_published">Post published</string>
+ <string name="page_published">Page published</string>
+ <string name="post_updated">Post updated</string>
+ <string name="page_updated">Page updated</string>
+ <string name="post_password">Password (optional)</string>
+ <string name="caption">Caption (optional)</string>
+ <string name="horizontal_alignment">Horizontal alignment</string>
+ <string name="width">Width</string>
+ <string name="featured">Use as featured image</string>
+ <string name="featured_in_post">Include image in post content</string>
+ <string name="out_of_memory">Device out of memory</string>
+ <string name="file_not_found">Couldn\'t find the media file for upload. Was it deleted or moved?</string>
+ <string name="post_excerpt">Excerpt</string>
+ <string name="download">Downloading media</string>
+ <string name="post_settings">Post settings</string>
+ <string name="delete_post">Delete post</string>
+ <string name="delete_page">Delete page</string>
+ <string name="share_url">Share URL</string>
+ <string name="posts_fetching">Fetching posts…</string>
+ <string name="pages_fetching">Fetching pages…</string>
+ <string name="toast_err_post_uploading">Unable to open post while it\'s uploading</string>
+
+ <!-- reload drop down -->
+ <string name="loading">Loading…</string>
+
+ <!-- comment view -->
+ <string name="on">on</string>
+ <string name="comment_status_approved">Approved</string>
+ <string name="comment_status_unapproved">Pending</string>
+ <string name="comment_status_spam">Spam</string>
+ <string name="comment_status_trash">Trashed</string>
+ <string name="comment_status_all">All</string>
+ <string name="edit_comment">Edit comment</string>
+ <string name="comments_empty_list">No comments</string>
+ <string name="comments_empty_list_filtered_approved">No Approved comments</string>
+ <string name="comments_empty_list_filtered_pending">No Pending comments</string>
+ <string name="comments_empty_list_filtered_spam">No Spam comments</string>
+ <string name="comments_empty_list_filtered_trashed">No Trashed comments</string>
+ <string name="comment_reply_to_user">Reply to %s</string>
+ <string name="comment_trashed">Comment trashed</string>
+ <string name="comment_spammed">Comment marked as spam</string>
+ <string name="comment_deleted_permanently">Comment deleted</string>
+ <string name="comment">Comment</string>
+ <string name="comments_fetching">Fetching comments…</string>
+
+ <!-- comment menu and buttons on comment detail - keep these short! -->
+ <string name="mnu_comment_approve">Approve</string>
+ <string name="mnu_comment_unapprove">Unapprove</string>
+ <string name="mnu_comment_spam">Spam</string>
+ <string name="mnu_comment_unspam">Not spam</string>
+ <string name="mnu_comment_trash">Trash</string>
+ <string name="mnu_comment_untrash">Restore</string>
+ <string name="mnu_comment_delete_permanently">Delete</string>
+ <string name="mnu_comment_liked">Liked</string>
+
+ <!-- comment dialogs - must be worded to work for moderation of single/multiple comments -->
+ <string name="dlg_approving_comments">Approving</string>
+ <string name="dlg_unapproving_comments">Unapproving</string>
+ <string name="dlg_spamming_comments">Marking as spam</string>
+ <string name="dlg_trashing_comments">Sending to trash</string>
+ <string name="dlg_deleting_comments">Deleting comments</string>
+ <string name="dlg_confirm_trash_comments">Send to trash?</string>
+ <string name="trash_yes">Trash</string>
+ <string name="trash_no">Don\'t trash</string>
+
+ <!-- comment actions -->
+ <string name="reply">Reply</string>
+ <string name="trash">Trash</string>
+ <string name="like">Like</string>
+ <string name="approve">Approve</string>
+ <string name="comment_moderated_approved">Comment approved!</string>
+
+ <!-- edit comment view -->
+ <string name="author_name">Author name</string>
+ <string name="author_email">Author email</string>
+ <string name="author_url">Author URL</string>
+ <string name="hint_comment_content">Comment</string>
+ <string name="saving_changes">Saving changes</string>
+ <string name="sure_to_cancel_edit_comment">Cancel editing this comment?</string>
+ <string name="dlg_sure_to_delete_comment">Permanently delete this comment?</string>
+ <string name="dlg_sure_to_delete_comments">Permanently delete these comments?</string>
+ <string name="content_required">Comment is required</string>
+ <string name="toast_comment_unedited">Comment hasn\'t changed</string>
+
+ <!-- context menu -->
+ <string name="remove_account">Remove site</string>
+ <string name="blog_removed_successfully">Site removed successfully</string>
+
+ <!-- draft actions -->
+ <string name="delete_draft">Delete draft</string>
+ <string name="delete_sure">Delete this draft</string>
+
+ <!-- page actions -->
+ <string name="preview_page">Preview page</string>
+ <string name="deleting_page">Deleting page</string>
+ <string name="page_deleted">Page deleted</string>
+ <string name="delete_sure_page">Delete this page</string>
+ <string name="page_trashed">Page sent to trash</string>
+
+ <!-- post actions -->
+ <string name="preview_post">Preview post</string>
+ <string name="deleting_post">Deleting post</string>
+ <string name="post_deleted">Post deleted</string>
+ <string name="post_trashed">Post sent to trash</string>
+ <string name="comment_added">Comment added successfully</string>
+ <string name="delete_sure_post">Delete this post</string>
+ <string name="share_url_post">Share post</string>
+ <string name="share_url_page">Share page</string>
+ <string name="share_link">Share link</string>
+ <string name="post_not_published">Post status isn\'t published</string>
+ <string name="page_not_published">Page status isn\'t published</string>
+ <string name="view_in_browser">View in browser</string>
+ <string name="preview">Preview</string>
+ <string name="update_verb">Update</string>
+ <string name="sending_content">Uploading %s content</string>
+ <string name="uploading_total">Uploading %1$d of %2$d</string>
+
+ <!-- new account view -->
+ <string name="signing_in">Signing in…</string>
+ <string name="no_site_error">Couldn\'t connect to the WordPress site</string>
+
+ <!-- media selection -->
+ <string name="select_photo">Select a photo from gallery</string>
+ <string name="select_video">Select a video from gallery</string>
+ <string name="select_from_media_library">Select from media library</string>
+ <string name="select_from_new_picker">Multi-select with the new picker</string>
+
+ <!-- category management -->
+ <string name="categories">Categories</string>
+ <string name="add_new_category">Add new category</string>
+ <string name="add_category">Add category</string>
+ <string name="category_name">Category name</string>
+ <string name="category_slug">Category slug (optional)</string>
+ <string name="category_desc">Category description (optional)</string>
+ <string name="category_parent">Category parent (optional):</string>
+ <string name="adding_cat_failed">Adding category failed</string>
+ <string name="adding_cat_success">Category added successfully</string>
+ <string name="cat_name_required">The category name field is required</string>
+ <string name="category_automatically_renamed">Category name %1$s isn\'t valid. It has been renamed to %2$s.</string>
+
+ <!-- action from share intents -->
+ <string name="select_a_blog">Select a WordPress site</string>
+ <string name="share_action_title">Add to …</string>
+ <string name="share_action_post">New post</string>
+ <string name="share_action_media">Media library</string>
+ <string name="share_action">Share</string>
+ <string name="cant_share_no_visible_blog">You can\'t share to WordPress without a visible blog</string>
+ <string name="no_account">No WordPress account found, add an account and try again</string>
+
+ <!-- file errors -->
+ <string name="file_error_create">Couldn\'t create temp file for media upload. Make sure there is enough free space on your device.</string>
+
+ <!-- SD Card errors -->
+ <string name="sdcard_title">SD Card Required</string>
+ <string name="sdcard_message">A mounted SD card is required to upload media</string>
+
+ <!-- location -->
+ <string name="location">Location</string>
+ <string name="location_not_found">Unknown location</string>
+ <string name="add_location">Add location</string>
+ <string name="current_location">Current location</string>
+ <string name="search_current_location">Locate</string>
+ <string name="search_location">Search</string>
+ <string name="edit_location">Edit</string>
+ <string name="add_location_permission_required">Permission required in order to add location</string>
+
+ <!-- Begin -->
+ <!-- Site Settings -->
+ <!-- -->
+
+ <!-- General -->
+ <string name="discussion">Discussion</string>
+ <string name="privacy">Privacy</string>
+ <string name="related_posts">Related Posts</string>
+ <string name="more">More</string>
+ <string name="none">None</string>
+ <string name="disabled">Disabled</string>
+ <string name="comments">Comments</string>
+ <string name="close_after">Close after</string>
+ <string name="oldest_first">Oldest first</string>
+ <string name="newest_first">Newest first</string>
+ <string name="days_quantity_one">1 day</string>
+ <string name="days_quantity_other">%d days</string>
+
+ <!-- PreferenceCategory Headers -->
+ <string name="site_settings_general_header">General</string>
+ <string name="site_settings_account_header">Account</string>
+ <string name="site_settings_writing_header">Writing</string>
+ <string name="site_settings_discussion_header" translatable="false">@string/discussion</string>
+ <string name="site_settings_discussion_new_posts_header">Defaults for new posts</string>
+ <string name="site_settings_comments_header" translatable="false">@string/comments</string>
+ <string name="site_settings_this_device_header">This device</string>
+ <string name="site_settings_advanced_header">Advanced</string>
+
+ <!-- Preference Titles -->
+ <string name="site_settings_title_title">Site Title</string>
+ <string name="site_settings_tagline_title">Tagline</string>
+ <string name="site_settings_address_title">Address</string>
+ <string name="site_settings_privacy_title" translatable="false">@string/privacy</string>
+ <string name="site_settings_language_title" translatable="false">@string/language</string>
+ <string name="site_settings_username_title" translatable="false">@string/username</string>
+ <string name="site_settings_password_title" translatable="false">@string/password</string>
+ <string name="site_settings_location_title">Enable Location</string>
+ <string name="site_settings_default_category_title">Default Category</string>
+ <string name="site_settings_default_format_title">Default Format</string>
+ <string name="site_settings_image_original_size">Original Size</string>
+ <string name="site_settings_default_image_width_title" translatable="false">@string/max_thumbnail_px_width</string>
+ <string name="site_settings_upload_and_link_image_title" translatable="false">@string/upload_full_size_image</string>
+ <string name="site_settings_related_posts_title" translatable="false">@string/related_posts</string>
+ <string name="site_settings_more_title" translatable="false">@string/more</string>
+ <string name="site_settings_allow_comments_title">Allow Comments</string>
+ <string name="site_settings_send_pingbacks_title">Send Pingbacks</string>
+ <string name="site_settings_receive_pingbacks_title">Receive Pingbacks</string>
+ <string name="site_settings_identity_required_title">Must include name and email</string>
+ <string name="site_settings_account_required_title">Users must be signed in</string>
+ <string name="site_settings_close_after_title" translatable="false">@string/close_after</string>
+ <string name="site_settings_sort_by_title">Sort by</string>
+ <string name="site_settings_threading_title">Threading</string>
+ <string name="site_settings_paging_title">Paging</string>
+ <string name="site_settings_whitelist_title">Automatically approve</string>
+ <string name="site_settings_multiple_links_title">Links in comments</string>
+ <string name="site_settings_moderation_hold_title">Hold for Moderation</string>
+ <string name="site_settings_blacklist_title">Blacklist</string>
+ <string name="site_settings_delete_site_title">Delete Site</string>
+
+ <!-- Preference Summaries -->
+ <string name="site_settings_privacy_public_summary">Public</string>
+ <string name="site_settings_privacy_hidden_summary">Hidden</string>
+ <string name="site_settings_privacy_private_summary">Private</string>
+ <string name="site_settings_threading_summary">%d levels</string>
+ <string name="site_settings_whitelist_all_summary">Comments from all users</string>
+ <string name="site_settings_whitelist_known_summary">Comments from known users</string>
+ <string name="site_settings_whitelist_none_summary" translatable="false">@string/none</string>
+
+ <string name="detail_approve_manual">Require manual approval for everyone\'s comments.</string>
+ <string name="detail_approve_auto_if_previously_approved">Automatically approve if the user has a previously approved comment</string>
+ <string name="detail_approve_auto">Automatically approve everyone\'s comments.</string>
+ <string-array name="site_settings_auto_approve_details" translatable="false">
+ <item>@string/detail_approve_manual</item>
+ <item>@string/detail_approve_auto_if_previously_approved</item>
+ <item>@string/detail_approve_auto</item>
+ </string-array>
+
+ <string name="site_settings_multiple_links_summary_zero">Require approval for more than 0 links</string>
+ <string name="site_settings_multiple_links_summary_one">Require approval for more than 1 link</string>
+ <string name="site_settings_multiple_links_summary_other">Require approval for more than %d links</string>
+ <string name="site_settings_paging_summary_one">1 comment per page</string>
+ <string name="site_settings_paging_summary_other">%d comments per page</string>
+
+ <string name="privacy_public">Your site is visible to everyone and may be indexed by search engines</string>
+ <string name="privacy_public_not_indexed">Your site is visible to everyone but asks search engines not to index it</string>
+ <string name="privacy_private">Your site is visible only to you and users you approve</string>
+ <string-array name="privacy_details" translatable="false">
+ <item>@string/privacy_public</item>
+ <item>@string/privacy_public_not_indexed</item>
+ <item>@string/privacy_private</item>
+ </string-array>
+
+ <!-- Preference Entries -->
+ <string name="approve_manual">No comments</string>
+ <string name="approve_auto_if_previously_approved">Known users\' comments</string>
+ <string name="approve_auto">All users</string>
+ <string-array name="site_settings_auto_approve_entries" translatable="false">
+ <item>@string/approve_manual</item>
+ <item>@string/approve_auto_if_previously_approved</item>
+ <item>@string/approve_auto</item>
+ </string-array>
+
+ <string-array name="site_settings_privacy_entries" translatable="false">
+ <item>@string/site_settings_privacy_public_summary</item>
+ <item>@string/site_settings_privacy_hidden_summary</item>
+ <item>@string/site_settings_privacy_private_summary</item>
+ </string-array>
+
+ <string-array name="site_settings_sort_entries" translatable="false">
+ <item>@string/oldest_first</item>
+ <item>@string/newest_first</item>
+ </string-array>
+
+ <!-- Hints (long press) -->
+ <string name="site_settings_title_hint">In a few words, explain what this site is about</string>
+ <string name="site_settings_tagline_hint">A short description or catchy phrase to describe your blog</string>
+ <string name="site_settings_address_hint">Changing your address is not currently supported</string>
+ <string name="site_settings_privacy_hint">Controls who can see your site</string>
+ <string name="site_settings_language_hint">Language this blog is primarily written in</string>
+ <string name="site_settings_username_hint">Current user account</string>
+ <string name="site_settings_password_hint">Change your password</string>
+ <string name="site_settings_location_hint">Automatically add location data to your posts</string>
+ <string name="site_settings_category_hint">Sets new post category</string>
+ <string name="site_settings_format_hint">Sets new post format</string>
+ <string name="site_settings_image_width_hint">Resizes images in posts to this width</string>
+ <string name="site_settings_upload_and_link_image_hint">Enable to always upload the fullsize image</string>
+ <string name="site_settings_related_posts_hint">Show or hide related posts in reader</string>
+ <string name="site_settings_more_hint">View all available Discussion settings</string>
+ <string name="site_settings_discussion_hint">View and change your sites discussion settings</string>
+ <string name="site_settings_allow_comments_hint">Allow readers to post comments</string>
+ <string name="site_settings_send_pingbacks_hint">Attempt to notify any blogs linked to from the article</string>
+ <string name="site_settings_receive_pingbacks_hint">Allow link notifications from other blogs</string>
+ <string name="site_settings_close_after_hint">Disallow comments after the specified time</string>
+ <string name="site_settings_sort_by_hint">Determines the order comments are displayed</string>
+ <string name="site_settings_threading_hint">Allow nested comments to a certain depth</string>
+ <string name="site_settings_paging_hint">Display comments in chunks of a specified size</string>
+ <string name="site_settings_manual_approval_hint">Comments must be manually approved</string>
+ <string name="site_settings_identity_required_hint">Comment author must fill out name and e-mail</string>
+ <string name="site_settings_user_account_required_hint">Users must be registered and logged in to comment</string>
+ <string name="site_settings_whitelist_hint">Comment author must have a previously approved comment</string>
+ <string name="site_settings_multiple_links_hint">Ignores link limit from known users</string>
+ <string name="site_settings_moderation_hold_hint">Comments that match a filter are put in the moderation queue</string>
+ <string name="site_settings_blacklist_hint">Comments that match a filter are marked as spam</string>
+ <string name="site_settings_delete_site_hint">Removes your site data from the app</string>
+
+ <!-- Related Posts -->
+ <string name="site_settings_rp_switch_title">Show Related Posts</string>
+ <string name="site_settings_rp_switch_summary">Related Posts displays relevant content from your site below your posts.</string>
+ <string name="site_settings_rp_show_header_title">Show Header</string>
+ <string name="site_settings_rp_show_images_title">Show Images</string>
+ <string name="site_settings_rp_preview_header" translatable="false">@string/related_posts</string>
+ <string name="site_settings_rp_preview1_title">Big iPhone/iPad Update Now Available</string>
+ <string name="site_settings_rp_preview1_site">in \"Mobile\"</string>
+ <string name="site_settings_rp_preview2_title">The WordPress for Android App Gets a Big Facelift</string>
+ <string name="site_settings_rp_preview2_site">in \"Apps\"</string>
+ <string name="site_settings_rp_preview3_title">Upgrade Focus: VideoPress For Weddings</string>
+ <string name="site_settings_rp_preview3_site">in \"Upgrade\"</string>
+
+ <!-- Learn More -->
+ <string name="site_settings_learn_more_header" translatable="false">@string/learn_more</string>
+ <string name="site_settings_learn_more_caption">You can override these settings for individual posts.</string>
+
+ <!-- List Editors (Blacklist, Hold for Moderation) -->
+ <string name="site_settings_list_editor_summary_one">1 item</string>
+ <string name="site_settings_list_editor_summary_other">%d items</string>
+
+ <string name="site_settings_list_editor_action_mode_title">Selected %1$d</string>
+ <string name="site_settings_list_editor_no_items_text">No items</string>
+ <string name="site_settings_list_editor_input_hint">Enter a word or phrase</string>
+ <string name="site_settings_hold_for_moderation_description">When a comment contains any of these words in its content, name, URL, e-mail, or IP, it will be held in the moderation queue. You can enter partial words, so \"press\" will match \"WordPress.\"</string>
+ <string name="site_settings_blacklist_description">When a comment contains any of these words in its content, name, URL, e-mail, or IP, it will be marked as spam. You can enter partial words, so \"press\" will match \"WordPress.\"</string>
+
+ <!-- Dialogs -->
+ <string name="site_settings_discussion_title" translatable="false">@string/discussion</string>
+ <string name="site_settings_close_after_dialog_title">Close commenting</string>
+ <string name="site_settings_paging_dialog_header">Comments per page</string>
+ <string name="site_settings_paging_dialog_description">Break comment threads into multiple pages.</string>
+ <string name="site_settings_threading_dialog_header">Thread up to</string>
+ <string name="site_settings_threading_dialog_description">Allow comments to be nested in threads.</string>
+ <string name="site_settings_close_after_dialog_header" translatable="false">@string/close_after</string>
+ <string name="site_settings_close_after_dialog_description">Automatically close comments on articles.</string>
+ <string name="site_settings_close_after_dialog_switch_text">Automatically close</string>
+ <string name="site_settings_multiple_links_dialog_description">Require approval for comments that include more than this number of links.</string>
+
+ <!-- Errors -->
+ <string name="site_settings_unsupported_version_error">Unsupported WordPress version</string>
+ <string name="site_settings_unknown_language_code_error">Language code not recognized</string>
+ <string name="site_settings_disconnected_toast">Disconnected, editing disabled.</string>
+
+ <!-- -->
+ <!-- Site Settings -->
+ <!-- End -->
+
+ <!-- preferences -->
+ <string name="open_source_licenses">Open source licenses</string>
+ <string name="preference_send_usage_stats">Send statistics</string>
+ <string name="preference_send_usage_stats_summary">Automatically send usage statistics to help us improve WordPress for Android</string>
+ <string name="preference_editor">Editor</string>
+ <string name="preference_show_visual_editor">Show visual editor</string>
+
+ <!-- stats -->
+ <string name="stats">Stats</string>
+ <string name="stats_for">Stats for %s</string>
+ <string name="stats_other_recent_stats_label">Other Recent Stats</string>
+ <string name="stats_other_recent_stats_moved_label">Looking for your Other Recent Stats? We\'ve moved them to the Insights page.</string>
+ <string name="stats_view_all">View all</string>
+ <string name="stats_view">View</string>
+ <string name="stats_pagination_label">Page %1$s of %2$s</string>
+ <string name="stats_no_activity_this_period">No activity this period</string>
+ <string name="stats_default_number_zero" translatable="false">0</string>
+
+ <!-- stats: errors -->
+ <string name="stats_no_blog">Stats couldn\'t be loaded for the required blog</string>
+ <string name="stats_generic_error">Required Stats couldn\'t be loaded</string>
+ <string name="stats_sign_in_jetpack_different_com_account">To view your stats, sign in to the WordPress.com account you used to connect Jetpack.</string>
+ <string name="stats_enable_rest_api_in_jetpack">To view your stats, enable the JSON API module in Jetpack.</string>
+
+ <!-- stats: Widget labels -->
+ <string name="stats_widget_name">WordPress Today\'s Stats</string>
+ <string name="stats_widget_name_for_blog">Today\'s Stats for %1$s</string>
+ <string name="stats_widget_loading_data">Loading data…</string>
+ <string name="stats_widget_error_generic">Stats couldn\'t be loaded</string>
+ <string name="stats_widget_error_no_account">Please login into WordPress</string>
+ <string name="stats_widget_error_no_permissions">Your WordPress.com account can\'t access Stats on this blog</string>
+ <string name="stats_widget_error_no_visible_blog">Stats couldn\'t be accessed without a visible blog</string>
+ <string name="stats_widget_error_readd_widget">Please remove the widget and re-add it again</string>
+ <string name="stats_widget_error_jetpack_no_blogid">Please access the Stats in the app, and try adding the widget later</string>
+
+ <!-- stats: Widget Promote Dialog -->
+ <string name="stats_widget_promo_title">Home Screen Stats Widget</string>
+ <string name="stats_widget_promo_desc">Add the widget to your home screen to access your Stats in one click.</string>
+ <string name="stats_widget_promo_ok_btn_label">Ok, got it</string>
+
+ <!-- stats: labels for timeframes -->
+ <string name="stats_timeframe_today">Today</string>
+ <string name="stats_timeframe_yesterday">Yesterday</string>
+ <string name="stats_timeframe_days">Days</string>
+ <string name="stats_timeframe_weeks">Weeks</string>
+ <string name="stats_timeframe_months">Months</string>
+ <string name="stats_timeframe_years">Years</string>
+
+ <string name="stats_views">Views</string>
+ <string name="stats_visitors">Visitors</string>
+ <string name="stats_likes">Likes</string>
+ <string name="stats_comments" translatable="false">@string/comments</string>
+
+ <!-- stats: labels for the views -->
+ <string name="stats_view_visitors_and_views">Visitors and Views</string>
+ <string name="stats_view_countries">Countries</string>
+ <string name="stats_view_top_posts_and_pages">Posts &amp; Pages</string>
+ <string name="stats_view_clicks">Clicks</string>
+ <string name="stats_view_tags_and_categories">Tags &amp; Categories</string>
+ <string name="stats_view_authors">Authors</string>
+ <string name="stats_view_referrers">Referrers</string>
+ <string name="stats_view_videos">Videos</string>
+ <string name="stats_view_comments" translatable="false">@string/comments</string>
+ <string name="stats_view_search_terms">Search Terms</string>
+ <string name="stats_view_publicize">Publicize</string>
+ <string name="stats_view_followers">Followers</string>
+
+ <!-- stats: label for the entries -->
+ <string name="stats_entry_country">Country</string>
+ <string name="stats_entry_posts_and_pages">Title</string>
+ <string name="stats_entry_clicks_link">Link</string>
+ <string name="stats_entry_tags_and_categories">Topic</string>
+ <string name="stats_entry_authors">Author</string>
+ <string name="stats_entry_referrers">Referrer</string>
+ <string name="stats_entry_video_plays">Video</string>
+ <string name="stats_entry_top_commenter">Author</string>
+ <string name="stats_entry_publicize">Service</string>
+ <string name="stats_entry_followers">Follower</string>
+ <string name="stats_entry_search_terms">Search Term</string>
+
+ <!-- stats: label for the totals -->
+ <string name="stats_totals_views">Views</string>
+ <string name="stats_totals_clicks">Clicks</string>
+ <string name="stats_totals_plays">Plays</string>
+ <string name="stats_totals_comments" translatable="false">@string/comments</string>
+ <string name="stats_totals_publicize">Followers</string>
+ <string name="stats_totals_followers">Since</string>
+
+ <!-- stats: empty list strings -->
+ <string name="stats_empty_geoviews">No countries recorded</string>
+ <string name="stats_empty_geoviews_desc">Explore the list to see which countries and regions generate the most traffic to your site.</string>
+ <string name="stats_empty_top_posts_title">No posts or pages viewed</string>
+ <string name="stats_empty_top_posts_desc">Discover what your most-viewed content is, and check how individual posts and pages perform over time.</string>
+ <string name="stats_empty_referrers_title">No referrers recorded</string>
+ <string name="stats_empty_referrers_desc">Learn more about your site’s visibility by looking at the websites and search engines that send the most traffic your way</string>
+ <string name="stats_empty_clicks_title">No clicks recorded</string>
+ <string name="stats_empty_clicks_desc">When your content includes links to other sites, you’ll see which ones your visitors click on the most.</string>
+ <string name="stats_empty_top_authors_desc">Track the views on each contributor\'s posts, and zoom in to discover the most popular content by each author.</string>
+ <string name="stats_empty_tags_and_categories">No tagged posts or pages viewed</string>
+ <string name="stats_empty_tags_and_categories_desc">Get an overview of the most popular topics on your site, as reflected in your top posts from the past week.</string>
+ <string name="stats_empty_video">No videos played</string>
+ <string name="stats_empty_video_desc">If you\'ve uploaded videos using VideoPress, find out how many times they’ve been watched.</string>
+ <string name="stats_empty_comments">No comments yet</string>
+ <string name="stats_empty_comments_desc">If you allow comments on your site, track your top commenters and discover what content sparks the liveliest conversations, based on the most recent 1,000 comments.</string>
+ <string name="stats_bar_graph_empty">No stats available</string>
+ <string name="stats_empty_publicize">No publicize followers recorded</string>
+ <string name="stats_empty_publicize_desc">Keep track of your followers from various social networking services using publicize.</string>
+ <string name="stats_empty_followers">No followers</string>
+ <string name="stats_empty_followers_desc">Keep track of your overall number of followers, and how long each one has been following your site.</string>
+ <string name="stats_empty_search_terms">No search terms recorded</string>
+ <string name="stats_empty_search_terms_desc">Learn more about your search traffic by looking at the terms your visitors searched for to find your site.</string>
+
+ <!-- stats: comments -->
+ <string name="stats_comments_by_authors">By Authors</string>
+ <string name="stats_comments_by_posts_and_pages">By Posts &amp; Pages</string>
+ <string name="stats_comments_total_comments_followers">Total posts with comment followers: %1$s</string>
+
+ <!-- stats: referrers -->
+ <string name="stats_referrers_spam">Spam</string>
+ <string name="stats_referrers_unspam">Not spam</string>
+ <string name="stats_referrers_marking_spam">Marking as spam</string>
+ <string name="stats_referrers_marking_not_spam">Marking as not spam</string>
+ <string name="stats_referrers_spam_generic_error">Something went wrong during the operation. The spam state wasn\'t changed.</string>
+
+ <!-- stats: followers -->
+ <string name="stats_followers_wpcom_selector">WordPress.com</string>
+ <string name="stats_followers_email_selector">Email</string>
+ <string name="stats_followers_total_wpcom">Total WordPress.com Followers: %1$s</string>
+ <string name="stats_followers_total_email">Total Email Followers: %1$s</string>
+ <string name="stats_followers_total_wpcom_paged">Showing %1$d - %2$d of %3$s WordPress.com Followers</string>
+ <string name="stats_followers_total_email_paged">Showing %1$d - %2$d of %3$s Email Followers</string>
+ <string name="stats_followers_seconds_ago">seconds ago</string>
+ <string name="stats_followers_a_minute_ago">a minute ago</string>
+ <string name="stats_followers_minutes">%1$d minutes</string>
+ <string name="stats_followers_an_hour_ago">an hour ago</string>
+ <string name="stats_followers_hours">%1$d hours</string>
+ <string name="stats_followers_a_day">A day</string>
+ <string name="stats_followers_days">%1$d days</string>
+ <string name="stats_followers_a_month">A month</string>
+ <string name="stats_followers_months">%1$d months</string>
+ <string name="stats_followers_a_year">A year</string>
+ <string name="stats_followers_years">%1$d years</string>
+
+ <!-- stats: search terms -->
+ <string name="stats_search_terms_unknown_search_terms">Unknown Search Terms</string>
+
+ <!-- stats: Authors -->
+ <string name="stats_unknown_author">Unknown Author</string>
+
+ <!-- Stats: Single post details view -->
+ <string name="stats_period">Period</string>
+ <string name="stats_total">Total</string>
+ <string name="stats_overall">Overall</string>
+ <string name="stats_months_and_years">Months and Years</string>
+ <string name="stats_average_per_day">Average per Day</string>
+ <string name="stats_recent_weeks">Recent Weeks</string>
+
+ <!-- Stats insights -->
+ <string name="stats_insights">Insights</string>
+ <string name="stats_insights_all_time">All-time posts, views, and visitors</string>
+ <string name="stats_insights_today">Today\'s Stats</string>
+ <string name="stats_insights_latest_post_no_title">(no title)</string>
+ <string name="stats_insights_latest_post_summary">Latest Post Summary</string>
+ <string name="stats_insights_latest_post_trend">It\'s been %1$s since %2$s was published. Here\'s how the post has performed so far…</string>
+ <string name="stats_insights_popular">Most popular day and hour</string>
+ <string name="stats_insights_most_popular_day">Most popular day</string>
+ <string name="stats_insights_most_popular_hour">Most popular hour</string>
+ <string name="stats_insights_most_popular_percent_views">%1$d%% of views</string>
+ <string name="stats_insights_best_ever">Best Views Ever</string>
+
+ <!-- invalid_url -->
+ <string name="invalid_site_url_message">Check that the site URL entered is valid</string>
+ <string name="invalid_url_message">Check that the URL entered is valid</string>
+
+ <!-- post status -->
+ <string name="publish_post">Publish</string>
+ <string name="pending_review">Pending review</string>
+ <string name="draft">Draft</string>
+ <string name="post_private">Private</string>
+ <string name="scheduled">Scheduled</string>
+ <string name="trashed">Trashed</string>
+
+ <!-- QuickPress -->
+ <string name="quickpress_window_title">Select blog for QuickPress shortcut</string>
+ <string name="quickpress_add_error">Shortcut name can\'t be empty</string>
+ <string name="quickpress_add_alert_title">Set shortcut name</string>
+
+ <!-- HTTP Authentication -->
+ <string name="httpuser">HTTP username</string>
+ <string name="httppassword">HTTP password</string>
+ <string name="settings">Settings</string>
+ <string name="http_credentials">HTTP credentials (optional)</string>
+ <string name="http_authorization_required">Authorization required</string>
+
+ <!-- post scheduling and password -->
+ <string name="publish_date">Publish</string>
+ <string name="post_format">Post format</string>
+ <string name="schedule_verb">Schedule</string>
+
+ <!-- post date selection -->
+ <string name="select_date">Select date</string>
+ <string name="select_time">Select time</string>
+
+ <!-- notifications -->
+ <string name="notifications">Notifications</string>
+ <string name="note_reply_successful">Reply published</string>
+ <string name="new_notifications">%d new notifications</string>
+ <string name="more_notifications">and %d more.</string>
+ <string name="reply_failed">Reply failed</string>
+ <string name="notifications_empty_list">No notifications</string>
+ <string name="notifications_empty_all">No notifications&#8230;yet.</string>
+ <string name="notifications_empty_unread">You\'re all caught up!</string>
+ <string name="notifications_empty_comments">No new comments&#8230;yet.</string>
+ <string name="notifications_empty_followers">No new followers to report&#8230;yet.</string>
+ <string name="notifications_empty_likes">No new likes to show&#8230;yet.</string>
+ <string name="notifications_empty_action_all">Get active! Comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_action_unread">Reignite the conversation: write a new post.</string>
+ <string name="notifications_empty_action_comments">Join a conversation: comment on posts from blogs you follow.</string>
+ <string name="notifications_empty_action_followers_likes">Get noticed: comment on posts you\'ve read.</string>
+ <string name="notifications_account_required">Sign in to WordPress.com for notifications</string>
+ <string name="notifications_empty_view_reader">View Reader</string>
+ <string name="older_two_days">Older than 2 days</string>
+ <string name="older_last_week">Older than a week</string>
+ <string name="older_month">Older than a month</string>
+ <string name="error_notification_open">Could not open notification</string>
+ <string name="ignore">Ignore</string>
+ <string name="push_auth_expired">The request has expired. Sign in to WordPress.com to try again.</string>
+ <string name="unread">Unread</string>
+ <string name="follows">Follows</string>
+
+ <!-- Notification Settings -->
+ <string name="notification_settings">Notification Settings</string>
+ <string name="notifications_sights_and_sounds">Sights and Sounds</string>
+ <string name="your_sites">Your Sites</string>
+ <string name="notifications_account_emails_summary">We\'ll always send important emails regarding your account, but you can get some helpful extras, too.</string>
+ <string name="notifications_account_emails">Email from WordPress.com</string>
+ <string name="notifications_wpcom_updates">WordPress.com Updates</string>
+ <string name="notifications_other">Other</string>
+ <string name="notifications_comments_other_blogs">Comments on other sites</string>
+ <string name="notifications_tab">Notifications tab</string>
+ <string name="email">Email</string>
+ <string name="email_address">Email address</string>
+ <string name="app_notifications">App notifications</string>
+ <string name="comment_likes">Comment likes</string>
+ <string name="replies_to_your_comments">Replies to your comments</string>
+ <string name="error_loading_notifications">Couldn\'t load notification settings</string>
+ <string name="notification_types">Notification Types</string>
+ <string name="notifications_disabled">App notifications have been disabled. Tap here to enable them in Settings.</string>
+ <string name="notifications_tab_summary">Settings for notifications that appear in the Notifications tab.</string>
+ <string name="notifications_email_summary">Settings for notifications that are sent to the email tied to your account.</string>
+ <string name="notifications_push_summary">Settings for notifications that appear on your device.</string>
+ <string name="search_sites">Search sites</string>
+ <string name="notifications_no_search_results">No sites matched \'%s\'</string>
+
+ <string name="comments_on_my_site">Comments on my site</string>
+ <string name="likes_on_my_comments">Likes on my comments</string>
+ <string name="likes_on_my_posts">Likes on my posts</string>
+ <string name="site_follows">Site follows</string>
+ <string name="site_achievements">Site achievements</string>
+ <string name="username_mentions">Username mentions</string>
+ <string-array name="notifications_blog_settings" translatable="false">
+ <item>@string/comments_on_my_site</item>
+ <item>@string/likes_on_my_comments</item>
+ <item>@string/likes_on_my_posts</item>
+ <item>@string/site_follows</item>
+ <item>@string/site_achievements</item>
+ <item>@string/username_mentions</item>
+ </string-array>
+
+ <string name="replies_to_my_comments">Replies to my comments</string>
+ <string-array name="notifications_other_settings" translatable="false">
+ <item>@string/replies_to_my_comments</item>
+ <item>@string/likes_on_my_comments</item>
+ </string-array>
+
+ <string name="notif_suggestions">Suggestions</string>
+ <string name="notif_research">Research</string>
+ <string name="notif_community">Community</string>
+ <string-array name="notifications_wpcom_settings" translatable="false">
+ <item>@string/notif_suggestions</item>
+ <item>@string/notif_research</item>
+ <item>@string/notif_community</item>
+ </string-array>
+
+ <string name="notif_tips">Tips for getting the most out of WordPress.com.</string>
+ <string name="notif_surveys">Opportunities to participate in WordPress.com research &amp; surveys.</string>
+ <string name="notif_events">Information on WordPress.com courses and events (online &amp; in-person).</string>
+ <string-array name="notifications_wpcom_settings_summaries" translatable="false">
+ <item>@string/notif_tips</item>
+ <item>@string/notif_surveys</item>
+ <item>@string/notif_events</item>
+ </string-array>
+
+ <!-- reader -->
+ <string name="reader">Reader</string>
+
+ <!-- editor -->
+ <string name="editor_post_title_placeholder">Post Title</string>
+ <string name="editor_page_title_placeholder">Page Title</string>
+ <string name="editor_content_placeholder">Share your story here…</string>
+ <string name="visual_editor_enabled">Visual Editor enabled</string>
+ <string name="new_editor_promo_button_label">Great, thanks!</string>
+ <string name="new_editor_promo_title">Brand new editor</string>
+ <string name="new_editor_promo_desc">The WordPress app for Android now includes a beautiful new visual
+ editor. Try it out by creating a new post.</string>
+ <string name="new_editor_reflection_error">Visual editor is not compatible with your device. It was
+ automatically disabled.</string>
+
+ <!-- editor post settings -->
+ <string name="editor_post_settings_featured_image">Featured Image</string>
+ <string name="editor_post_settings_set_featured_image">Set Featured Image</string>
+
+
+ <!-- Post Formats -->
+ <string name="post_format_aside">Aside</string>
+ <string name="post_format_audio">Audio</string>
+ <string name="post_format_chat">Chat</string>
+ <string name="post_format_gallery">Gallery</string>
+ <string name="post_format_image">Image</string>
+ <string name="post_format_link">Link</string>
+ <string name="post_format_quote">Quote</string>
+ <string name="post_format_standard">Standard</string>
+ <string name="post_format_status">Status</string>
+ <string name="post_format_video">Video</string>
+ <string-array name="post_formats_array" translatable="false">
+ <item>@string/post_format_aside</item>
+ <item>@string/post_format_audio</item>
+ <item>@string/post_format_chat</item>
+ <item>@string/post_format_gallery</item>
+ <item>@string/post_format_image</item>
+ <item>@string/post_format_link</item>
+ <item>@string/post_format_quote</item>
+ <item>@string/post_format_standard</item>
+ <item>@string/post_format_status</item>
+ <item>@string/post_format_video</item>
+ </string-array>
+
+ <!-- Menu Buttons -->
+ <string name="new_post">New post</string>
+ <string name="new_media">New media</string>
+ <string name="edit_media">Edit media</string>
+ <string name="view_site">View site</string>
+
+ <!-- Image Alignment -->
+ <string name="image_alignment">Alignment</string>
+
+ <string name="alignment_none">None</string>
+ <string name="alignment_left">Left</string>
+ <string name="alignment_center">Center</string>
+ <string name="alignment_right">Right</string>
+
+ <string-array name="alignment_key_array" translatable="false">
+ <item>none</item>
+ <item>left</item>
+ <item>center</item>
+ <item>right</item>
+ </string-array>
+
+ <string-array name="alignment_array" translatable="false">
+ <item>@string/alignment_none</item>
+ <item>@string/alignment_left</item>
+ <item>@string/alignment_center</item>
+ <item>@string/alignment_right</item>
+ </string-array>
+
+ <!-- About View -->
+ <string name="app_title">WordPress for Android</string>
+ <string name="publisher">Publisher:</string>
+ <string name="automattic_inc" translatable="false">Automattic, Inc</string>
+ <string name="automattic_url" translatable="false">automattic.com</string>
+ <string name="version">Version</string>
+ <string name="tos">Terms of Service</string>
+ <string name="privacy_policy">Privacy policy</string>
+
+ <!-- Remote Post Changes -->
+ <string name="local_changes">Local changes</string>
+
+ <!-- message on post preview explaining what local changes, local drafts and drafts are -->
+ <string name="local_changes_explainer">This post has local changes which haven\'t been published</string>
+ <string name="local_draft_explainer">This post is a local draft which hasn\'t been published</string>
+ <string name="draft_explainer">This post is a draft which hasn\'t been published</string>
+
+ <!-- message on post preview explaining links are disabled -->
+ <string name="preview_screen_links_disabled">Links are disabled on the preview screen</string>
+
+ <string name="ok">OK</string>
+ <string name="image_settings">Image settings</string>
+ <string name="add_account_blog_url">Blog address</string>
+ <string name="wordpress_blog">WordPress blog</string>
+ <string name="blogusername">blogusername</string>
+ <string name="dot_wordpress_dot_com_url" translatable="false">.wordpress.com</string>
+ <string name="wordpress_dot_com" translatable="false">wordpress.com</string>
+
+ <!-- Error Messages -->
+ <string name="error_delete_post">An error occurred while deleting the %s</string>
+ <!-- The following messages can\'t be factorized due to i18n -->
+ <string name="error_refresh_posts">Posts couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_pages">Pages couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_notifications">Notifications couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_comments">Comments couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_comments_showing_older">Comments couldn\'t be refreshed at this time - showing older comments</string>
+ <string name="error_refresh_stats">Stats couldn\'t be refreshed at this time</string>
+ <string name="error_refresh_media">Something went wrong while refreshing the media library. Try again later.</string>
+
+ <string name="error_refresh_unauthorized_comments">You don\'t have permission to view or edit comments</string>
+ <string name="error_refresh_unauthorized_pages">You don\'t have permission to view or edit pages</string>
+ <string name="error_refresh_unauthorized_posts">You don\'t have permission to view or edit posts</string>
+ <string name="error_fetch_my_profile">Couldn\'t retrieve your profile</string>
+ <string name="error_fetch_account_settings">Couldn\'t retrieve your account settings</string>
+ <string name="error_post_my_profile">Couldn\'t save your profile</string>
+ <string name="error_post_my_profile_no_connection">No connection, couldn\'t save your profile</string>
+ <string name="error_post_account_settings">Couldn\'t save your account settings</string>
+ <string name="error_generic">An error occurred</string>
+ <string name="error_moderate_comment">An error occurred while moderating</string>
+ <string name="error_edit_comment">An error occurred while editing the comment</string>
+ <string name="error_publish_empty_post">Can\'t publish an empty post</string>
+ <string name="error_publish_no_network">Can\'t publish while there is no connection. Saved as draft.</string>
+ <string name="error_upload">An error occurred while uploading the %s</string>
+ <string name="error_media_upload">An error occurred while uploading media</string>
+ <string name="error_media_upload_connection">A connection error occurred while uploading media</string>
+ <string name="error_blog_hidden">This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again.</string>
+ <string name="fatal_db_error">An error occurred while creating the app database. Try reinstalling the app.</string>
+ <string name="error_copy_to_clipboard">An error occurred while copying text to clipboard</string>
+ <string name="error_fetch_remote_site_settings">Couldn\'t retrieve site info</string>
+ <string name="error_post_remote_site_settings">Couldn\'t save site info</string>
+ <string name="error_open_list_from_notification">This post or page was published on another site</string>
+ <string name="error_fetch_users_list">Couldn\'t retrieve site users</string>
+ <string name="error_fetch_followers_list">Couldn\'t retrieve site followers</string>
+ <string name="error_fetch_email_followers_list">Couldn\'t retrieve site email followers</string>
+ <string name="error_fetch_viewers_list">Couldn\'t retrieve site viewers</string>
+ <string name="error_update_role">Couldn\'t update user role</string>
+ <string name="error_remove_user">Couldn\'t remove user</string>
+ <string name="error_remove_follower">Couldn\'t remove follower</string>
+ <string name="error_remove_viewer">Couldn\'t remove viewer</string>
+
+ <!-- Image Descriptions for Accessibility -->
+ <string name="content_description_add_media">Add media</string>
+ <string name="error_load_comment">Couldn\'t load the comment</string>
+ <string name="error_downloading_image">Error downloading image</string>
+ <string name="cd_related_post_preview_image">Related post preview image</string>
+
+ <!-- Passcode lock -->
+ <string name="passcode_manage">Manage PIN lock</string>
+ <string name="passcode_enter_passcode">Enter your PIN</string>
+ <string name="passcode_enter_old_passcode">Enter your old PIN</string>
+ <string name="passcode_re_enter_passcode">Re-enter your PIN</string>
+ <string name="passcode_change_passcode">Change PIN</string>
+ <string name="passcode_set">PIN set</string>
+ <string name="passcode_wrong_passcode">Wrong PIN</string>
+ <string name="passcode_preference_title">PIN lock</string>
+ <string name="passcode_turn_off">Turn PIN lock off</string>
+ <string name="passcode_turn_on">Turn PIN lock on</string>
+ <string name="passcodelock_prompt_message">Enter your PIN</string>
+ <string name="passcodelock_hint"></string>
+
+ <!--
+ Jetpack strings
+ -->
+ <string name="jetpack_message">The Jetpack plugin is required for stats. Do you want to install Jetpack?</string>
+ <string name="jetpack_message_not_admin">The Jetpack plugin is required for stats. Contact the site administrator.</string>
+ <string name="jetpack_not_found">Jetpack plugin not found</string>
+ <string name="jetpack_not_connected">Jetpack plugin not connected</string>
+ <string name="jetpack_not_connected_message">The Jetpack plugin is installed, but not connected to WordPress.com. Do you want to connect Jetpack?</string>
+
+ <!--
+ reader strings
+ -->
+ <!-- timespan shown for posts/comments published within the past 60 seconds -->
+ <string name="timespan_now">now</string>
+
+ <!-- title shown for untitled posts and blogs -->
+ <string name="reader_untitled_post">(Untitled)</string>
+
+ <!-- activity titles -->
+ <string name="reader_title_applog">Application log</string>
+ <string name="reader_title_blog_preview">Reader Blog</string>
+ <string name="reader_title_tag_preview">Reader Tag</string>
+ <string name="reader_title_post_detail">Reader Post</string>
+ <string name="reader_title_related_post_detail">Related Post</string>
+ <string name="reader_title_subs">Tags &amp; Blogs</string>
+ <string name="reader_title_photo_viewer">%1$d of %2$d</string>
+ <string name="reader_title_comments" translatable="false">@string/comments</string>
+ <string name="reader_title_search_results">Search for %s</string>
+
+ <!-- view pager titles -->
+ <string name="reader_page_followed_tags">Followed tags</string>
+ <string name="reader_page_followed_blogs">Followed sites</string>
+ <string name="reader_page_recommended_blogs">Sites you may like</string>
+
+ <!-- share dialog title when sharing a reader url -->
+ <string name="reader_share_link">Share link</string>
+
+ <!-- subject line when sharing a reader url -->
+ <string name="reader_share_subject">Shared from %s</string>
+
+ <!-- menu text -->
+ <string name="reader_menu_tags">Edit tags and blogs</string>
+ <string name="reader_menu_block_blog">Block this blog</string>
+
+ <!-- button text -->
+ <string name="reader_btn_share">Share</string>
+ <string name="reader_btn_follow">Follow</string>
+ <string name="reader_btn_unfollow">Following</string>
+
+ <!-- EditText hints -->
+ <string name="reader_hint_comment_on_post">Reply to post…</string>
+ <string name="reader_hint_comment_on_comment">Reply to comment…</string>
+ <string name="reader_hint_add_tag_or_url">Enter a URL or tag to follow</string>
+ <string name="reader_hint_post_search">Search WordPress.com</string>
+
+ <!-- TextView labels -->
+ <string name="reader_label_new_posts">New posts</string>
+ <string name="reader_label_new_posts_subtitle">Tap to show them</string>
+ <string name="reader_label_added_tag">Added %s</string>
+ <string name="reader_label_removed_tag">Removed %s</string>
+ <string name="reader_label_reply">Reply</string>
+ <string name="reader_label_followed_blog">Blog followed</string>
+ <string name="reader_label_tag_preview">Posts tagged %s</string>
+ <string name="reader_label_comments_on">Comments on</string>
+ <string name="reader_label_comments_closed">Comments are closed</string>
+ <string name="reader_label_comment_count_single">One comment</string>
+ <string name="reader_label_comment_count_multi">%,d comments</string>
+ <string name="reader_label_view_original">View original article</string>
+ <string name="reader_label_follow_count">%,d followers</string>
+ <string name="reader_label_submit_comment">SEND</string>
+ <string name="reader_label_gap_marker">Load more posts</string>
+ <string name="reader_label_post_search_explainer">Search all public WordPress.com blogs</string>
+ <string name="reader_label_post_search_running">Searching…</string>
+ <string name="reader_label_related_posts">Related Reading</string>
+ <string name="reader_label_view_gallery">View Gallery</string>
+ <string name="reader_label_image_count_one">1 image</string>
+ <string name="reader_label_image_count_multi">%d images</string>
+
+ <!-- like counts -->
+ <string name="reader_label_like">Like</string>
+ <string name="reader_likes_one">One person likes this</string>
+ <string name="reader_likes_multi">%,d people like this</string>
+ <string name="reader_likes_only_you">You like this</string>
+ <string name="reader_likes_you_and_one">You and one other like this</string>
+ <string name="reader_likes_you_and_multi">You and %,d others like this</string>
+ <string name="reader_label_liked_by">Liked By</string>
+
+ <string name="reader_short_like_count_none">Like</string>
+ <string name="reader_short_like_count_one">1 Like</string>
+ <string name="reader_short_like_count_multi">%s Likes</string>
+
+ <string name="reader_short_comment_count_one">1 Comment</string>
+ <string name="reader_short_comment_count_multi">%s Comments</string>
+
+ <!-- toast messages -->
+ <string name="reader_toast_err_comment_failed">Couldn\'t post your comment</string>
+ <string name="reader_toast_err_tag_exists">You already follow this tag</string>
+ <string name="reader_toast_err_tag_invalid">That isn\'t a valid tag</string>
+ <string name="reader_toast_err_add_tag">Unable to add this tag</string>
+ <string name="reader_toast_err_remove_tag">Unable to remove this tag</string>
+ <string name="reader_toast_err_share_intent">Unable to share</string>
+ <string name="reader_toast_err_view_image">Unable to view image</string>
+ <string name="reader_toast_err_url_intent">Unable to open %s</string>
+ <string name="reader_toast_err_get_comment">Unable to retrieve this comment</string>
+ <string name="reader_toast_err_get_blog_info">Unable to show this blog</string>
+ <string name="reader_toast_err_already_follow_blog">You already follow this blog</string>
+ <string name="reader_toast_err_follow_blog">Unable to follow this blog</string>
+ <string name="reader_toast_err_follow_blog_not_found">This blog could not be found</string>
+ <string name="reader_toast_err_follow_blog_not_authorized">You are not authorized to access this blog</string>
+ <string name="reader_toast_err_unfollow_blog">Unable to unfollow this blog</string>
+ <string name="reader_toast_blog_blocked">Posts from this blog will no longer be shown</string>
+ <string name="reader_toast_err_block_blog">Unable to block this blog</string>
+ <string name="reader_toast_err_generic">Unable to perform this action</string>
+
+ <!-- failure messages when retrieving a single reader post -->
+ <string name="reader_err_get_post_generic">Unable to retrieve this post</string>
+ <string name="reader_err_get_post_not_authorized">You\'re not authorized to view this post</string>
+ <string name="reader_err_get_post_not_found">This post no longer exists</string>
+
+ <!-- empty list/grid text -->
+ <string name="reader_empty_posts_no_connection" translatable="false">@string/no_network_title</string>
+ <string name="reader_empty_posts_request_failed">Unable to retrieve posts</string>
+ <string name="reader_empty_posts_in_tag">No posts with this tag</string>
+ <string name="reader_empty_posts_in_tag_updating">Fetching posts…</string>
+ <string name="reader_empty_posts_in_custom_list">The sites in this list haven\'t posted anything recently</string>
+ <string name="reader_empty_followed_tags">You don\'t follow any tags</string>
+ <string name="reader_empty_recommended_blogs">No recommended blogs</string>
+ <string name="reader_empty_followed_blogs_title">You\'re not following any sites yet</string>
+ <string name="reader_empty_followed_blogs_description">But don\'t worry, just tap the icon at the top right to start exploring!</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_title">No recent posts</string>
+ <string name="reader_empty_followed_blogs_no_recent_posts_description">The sites you follow haven\'t posted anything recently</string>
+ <string name="reader_empty_posts_liked">You haven\'t liked any posts</string>
+ <string name="reader_empty_comments">No comments yet</string>
+ <string name="reader_empty_posts_in_blog">This blog is empty</string>
+ <string name="reader_empty_posts_in_search_title">No posts found</string>
+ <string name="reader_empty_posts_in_search_description">No posts found for %s for your language</string>
+
+ <string name="dlg_confirm_clear_search_history">Clear search history?</string>
+ <string name="label_clear_search_history">Clear search history</string>
+
+ <!-- attribution line for Discover posts, ex: "Originally posted by [AuthorName] on [BlogName] -->
+ <string name="reader_discover_attribution_author_and_blog">Originally posted by %1$s on %2$s</string>
+ <string name="reader_discover_attribution_author">Originally posted by %s</string>
+ <string name="reader_discover_attribution_blog">Originally posted on %s</string>
+ <string name="reader_discover_visit_blog">Visit %s</string>
+
+ <!-- connection bar which appears on main activity when there's no connection -->
+ <string name="connectionbar_no_connection">No connection</string>
+
+ <!-- NUX strings -->
+ <string name="create_account_wpcom">Create an account on WordPress.com</string>
+ <string name="create_new_blog_wpcom">Create WordPress.com blog</string>
+ <string name="new_blog_wpcom_created">WordPress.com blog created!</string>
+ <string name="validating_user_data">Validating user data</string>
+ <string name="validating_site_data">Validating site data</string>
+ <string name="creating_your_account">Creating your account</string>
+ <string name="creating_your_site">Creating your site</string>
+ <string name="required_field">Required field</string>
+ <string name="invalid_email_message">Your email address isn\'t valid</string>
+ <string name="invalid_password_message">Password must contain at least 4 characters</string>
+ <string name="invalid_username_too_short">Username must be longer than 4 characters</string>
+ <string name="invalid_username_too_long">Username must be shorter than 61 characters</string>
+ <string name="invalid_username_no_spaces">Username can\'t contain spaces</string>
+ <string name="email_hint">Email address</string>
+ <string name="agree_terms_of_service">By creating an account you agree to the fascinating %1$sTerms of Service%2$s</string>
+ <string name="username_email">Email or username</string>
+ <string name="site_address">Your self-hosted address (URL)</string>
+ <string name="connecting_wpcom">Connecting to WordPress.com</string>
+ <string name="username_only_lowercase_letters_and_numbers">Username can only contain lowercase letters (a-z) and numbers</string>
+ <string name="username_required">Enter a username</string>
+ <string name="username_not_allowed">Username not allowed</string>
+ <string name="email_cant_be_used_to_signup">You can\'t use that email address to signup. We are having problems with them blocking some of our email. Use another email provider.</string>
+ <string name="username_must_be_at_least_four_characters">Username must be at least 4 characters</string>
+ <string name="username_contains_invalid_characters">Username may not contain the character “_”</string>
+ <string name="username_must_include_letters">Username must have a least 1 letter (a-z)</string>
+ <string name="email_invalid">Enter a valid email address</string>
+ <string name="email_not_allowed">That email address isn\'t allowed</string>
+ <string name="username_exists">That username already exists</string>
+ <string name="email_exists">That email address is already being used</string>
+ <string name="username_reserved_but_may_be_available">That username is currently reserved but may be available in a couple of days</string>
+ <string name="email_reserved">That email address has already been used. Check your inbox for an activation email. If you don\'t activate you can try again in a few days.</string>
+ <string name="blog_name_required">Enter a site address</string>
+ <string name="blog_name_not_allowed">That site address isn\'t allowed</string>
+ <string name="blog_name_no_spaced_allowed">Site address can\'t contain spaces</string>
+ <string name="blog_name_must_be_at_least_four_characters">Site address must be at least 4 characters</string>
+ <string name="blog_name_must_be_less_than_sixty_four_characters">The site address must be shorter than 64 characters</string>
+ <string name="blog_name_contains_invalid_characters">Site address may not contain the character “_”</string>
+ <string name="blog_name_cant_be_used">You may not use that site address</string>
+ <string name="blog_name_only_lowercase_letters_and_numbers">Site address can only contain lowercase letters (a-z) and numbers</string>
+ <string name="blog_name_must_include_letters">Site address must have at least 1 letter (a-z)</string>
+ <string name="blog_name_exists">That site already exists</string>
+ <string name="blog_name_reserved">That site is reserved</string>
+ <string name="blog_name_reserved_but_may_be_available">That site is currently reserved but may be available in a couple days</string>
+ <string name="password_invalid">You need a more secure password. Make sure to use 7 or more characters, mix uppercase and lowercase letters, numbers or special characters.</string>
+ <string name="blog_name_invalid">Invalid site address</string>
+ <string name="blog_title_invalid">Invalid site title</string>
+ <string name="username_invalid">Invalid username</string>
+ <string name="limit_reached">Limit reached. You can try again in 1 minute. Trying again before that will only increase the time you have to wait before the ban is lifted. If you think this is in error, contact support.</string>
+ <string name="username_or_password_incorrect">The username or password you entered is incorrect</string>
+ <string name="nux_tap_continue">Continue</string>
+ <string name="nux_cannot_log_in">We can\'t log you in</string>
+ <string name="nux_tutorial_get_started_title">Get started!</string>
+ <string name="nux_welcome_create_account">Create account</string>
+ <string name="nux_add_selfhosted_blog">Add self-hosted site</string>
+ <string name="nux_oops_not_selfhosted_blog">Sign in to WordPress.com</string>
+ <string name="ssl_certificate_details">Details</string>
+ <string name="ssl_certificate_error">Invalid SSL certificate</string>
+ <string name="ssl_certificate_ask_trust">If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn\'t continue. Would you like to trust the certificate anyway?</string>
+ <string name="ptr_tip_message">Tip: Pull down to refresh</string>
+ <string name="verification_code">Verification code</string>
+ <string name="invalid_verification_code">Invalid verification code</string>
+ <string name="verify">Verify</string>
+ <string name="two_step_footer_label">Enter the code from your authenticator app.</string>
+ <string name="two_step_footer_button">Send code via text message</string>
+ <string name="two_step_sms_sent">Check your text messages for the verification code.</string>
+ <string name="sign_in_jetpack">Sign in to your WordPress.com account to connect to Jetpack.</string>
+ <string name="auth_required">Sign in again to continue.</string>
+ <string name="signup_succeed_signin_failed">Your account has been created but an error occured while we signed you
+ in. Try to sign in with your newly created username and password.</string>
+ <string name="send_link">Send link</string>
+ <string name="get_a_link_sent_to_your_email_to_sign_in_instantly">Get a link sent to your email to sign in instantly</string>
+ <string name="logging_in">Logging in</string>
+ <string name="magic_link_unavailable_error_message">Currently unavailable. Please enter your password</string>
+ <string name="check_your_email">Check your email</string>
+ <string name="launch_your_email_app">Launch your email app</string>
+ <string name="checking_email">Checking email</string>
+ <string name="not_on_wordpress_com">Not on WordPress.com?</string>
+
+ <!-- Help view -->
+ <string name="help">Help</string>
+ <string name="forgot_password">Lost your password?</string>
+ <string name="nux_help_description">Visit the help center to get answers to common questions or visit the forums to ask new ones</string>
+ <string name="forums">Forums</string>
+ <string name="contact_us">Contact us</string>
+ <string name="help_center">Help center</string>
+ <string name="browse_our_faq_button">Browse our FAQ</string>
+ <string name="faq_button">FAQ</string>
+
+ <!--My Site-->
+ <string name="my_site_header_external">External</string>
+ <string name="my_site_header_configuration">Configuration</string>
+ <string name="my_site_header_look_and_feel">Look and Feel</string>
+ <string name="my_site_header_publish">Publish</string>
+ <string name="my_site_btn_blog_posts">Blog Posts</string>
+ <string name="my_site_btn_site_settings">Settings</string>
+ <string name="my_site_btn_comments" translatable="false">@string/comments</string>
+ <string name="my_site_btn_switch_site">Switch Site</string>
+ <string name="my_site_btn_view_admin">View Admin</string>
+ <string name="my_site_btn_view_site">View Site</string>
+ <string name="my_site_no_sites_view_drake">Illustration</string>
+ <string name="my_site_no_sites_view_title">You don\'t have any WordPress sites yet.</string>
+ <string name="my_site_no_sites_view_subtitle">Would you like to add one?</string>
+
+ <!-- site picker -->
+ <string name="site_picker_title">Choose site</string>
+ <string name="site_picker_edit_visibility">Show/hide sites</string>
+ <string name="site_picker_add_site">Add site</string>
+ <string name="site_picker_add_self_hosted">Add self-hosted site</string>
+ <string name="site_picker_create_dotcom">Create WordPress.com site</string>
+ <string name="site_picker_cant_hide_current_site">\"%s\" wasn\'t hidden because it\'s the current site</string>
+
+ <!-- Application logs view -->
+ <string name="logs_copied_to_clipboard">Application logs have been copied to the clipboard</string>
+
+ <!-- Helpshift overridden strings -->
+ <string name="hs__conversation_detail_error">Describe the problem you\'re seeing</string>
+ <string name="hs__new_conversation_header">Support chat</string>
+ <string name="hs__conversation_header">Support chat</string>
+ <string name="hs__username_blank_error">Enter a valid name</string>
+ <string name="hs__invalid_email_error">Enter a valid email address</string>
+
+ <!--Me-->
+ <string name="me_btn_app_settings">App Settings</string>
+ <string name="me_btn_support">Help &amp; Support</string>
+ <string name="me_btn_login_logout">Login/Logout</string>
+ <string name="me_connect_to_wordpress_com">Connect to WordPress.com</string>
+ <string name="me_disconnect_from_wordpress_com">Disconnect from WordPress.com</string>
+
+ <!--TabBar Accessibility Labels-->
+ <string name="tabbar_accessibility_label_my_site">My Site</string>
+ <string name="tabbar_accessibility_label_me">Me</string>
+ <string name="site_privacy_private_desc">I would like my site to be private, visible only to users I choose</string>
+ <string name="site_privacy_hidden_desc">Discourage search engines from indexing this site</string>
+ <string name="site_privacy_public_desc">Allow search engines to index this site</string>
+
+ <!-- Static URLs -->
+ <string name="privacy_settings_url" translatable="false">https://en.support.wordpress.com/privacy-settings</string>
+ <string name="language_settings_url" translatable="false">https://en.support.wordpress.com/language-settings</string>
+ <string name="role_info_url" translatable="false">https://en.support.wordpress.com/user-roles/</string>
+
+ <string name="date_range_start_date">Start Date</string>
+ <string name="date_range_end_date">End Date</string>
+
+ <!-- Special characters -->
+ <string name="bullet" translatable="false">\u2022</string>
+ <string name="previous_button" translatable="false">&lt;</string>
+ <string name="next_button" translatable="false">&gt;</string>
+ <string name="vertical_line" translatable="false">\u007C</string>
+
+ <!-- Noticons -->
+ <string name="noticon_clock" translatable="false">\uf303</string>
+ <string name="noticon_note" translatable="false">\uf814</string>
+
+ <!--Theme Browser-->
+ <string name="current_theme">Current Theme</string>
+ <string name="customize">Customize</string>
+ <string name="details">Details</string>
+ <string name="support">Support</string>
+ <string name="active">Active</string>
+
+ <string name="theme_free">Free</string>
+ <string name="theme_all">All</string>
+ <string name="theme_premium">Premium</string>
+ <string-array name="themes_filter_array" translatable="false">
+ <item>@string/theme_free</item>
+ <item>@string/theme_all</item>
+ <item>@string/theme_premium</item>
+ </string-array>
+
+ <string name="title_activity_theme_support">Themes</string>
+ <string name="theme_activate">Activate</string>
+ <string name="theme_try_and_customize">Try &amp; Customize</string>
+ <string name="theme_view">View</string>
+ <string name="theme_details">Details</string>
+ <string name="theme_support">Support</string>
+ <string name="theme_done">DONE</string>
+ <string name="theme_manage_site">MANAGE SITE</string>
+ <string name="theme_prompt">Thanks for choosing %1$s</string>
+ <string name="theme_by_author_prompt_append"> by %1$s</string>
+ <string name="theme_activation_error">Something went wrong. Could not activate theme</string>
+ <string name="selected_theme">Selected Theme</string>
+ <string name="could_not_load_theme">Could not load theme</string>
+
+ <!--People Management-->
+ <string name="people">People</string>
+ <string name="edit_user">Edit User</string>
+ <string name="role">Role</string>
+ <string name="follower_subscribed_since">Since %1$s</string>
+ <string name="person_remove_confirmation_title">Remove %1$s</string>
+ <string name="user_remove_confirmation_message">If you remove %1$s, that user will no longer be able to access this site, but any content that was created by %1$s will remain on the site.\n\nWould you still like to remove this user?</string>
+ <string name="follower_remove_confirmation_message">If removed, this follower will stop receiving notifications about this site, unless they re-follow.\n\nWould you still like to remove this follower?</string>
+ <string name="viewer_remove_confirmation_message">If you remove this viewer, he or she will not be able to visit this site.\n\nWould you still like to remove this viewer?</string>
+ <string name="person_removed">Successfully removed %1$s</string>
+ <string name="invite_people">Invite People</string>
+ <string name="invite_names_title">Usernames or Emails</string>
+ <string name="invite">Invite</string>
+ <string name="button_invite" translatable="false">@string/invite</string>
+ <string name="invite_username_not_found">%s: User not found</string>
+ <string name="invite_already_a_member">%s: Already a member</string>
+ <string name="invite_already_following">%s: Already following</string>
+ <string name="invite_user_blocked_invites">%s: User blocked invites</string>
+ <string name="invite_invalid_email">%s: Invalid email</string>
+ <string name="invite_message_title">Custom Message</string>
+ <string name="invite_message_remaining_zero">0 characters remaining</string>
+ <string name="invite_message_remaining_one">1 character remaining</string>
+ <string name="invite_message_remaining_other">%d characters remaining</string>
+ <string name="invite_message_info">(Optional) You can enter a custom message of up to 500 characters that will be included in the invitation to the user(s).</string>
+ <string name="invite_message_usernames_limit">Invite up to 10 email addresses and/or WordPress.com usernames. Those needing a username will be sent instructions on how to create one.</string>
+ <string name="invite_error_no_usernames">Please add at least one username</string>
+ <string name="invite_error_invalid_usernames_one">Cannot send: A username or email is invalid</string>
+ <string name="invite_error_invalid_usernames_multiple">Cannot send: There are invalid usernames or emails</string>
+ <string name="invite_error_sending">An error occurred while trying to send the invite!</string>
+ <string name="invite_error_some_failed">Invite sent but error(s) occurred!</string>
+ <string name="invite_error_for_username">%1$s: %2$s</string>
+ <string name="invite_sent">Invite sent successfully</string>
+
+ <string name="people_dropdown_item_team">Team</string>
+ <string name="people_dropdown_item_followers">Followers</string>
+ <string name="people_dropdown_item_email_followers">Email Followers</string>
+ <string name="people_dropdown_item_viewers">Viewers</string>
+ <string name="people_empty_list_filtered_users">You don\'t have any users yet.</string>
+ <string name="people_empty_list_filtered_followers">You don\'t have any followers yet.</string>
+ <string name="people_empty_list_filtered_email_followers">You don\'t have any email followers yet.</string>
+ <string name="people_empty_list_filtered_viewers">You don\'t have any viewers yet.</string>
+ <string name="people_fetching">Fetching users…</string>
+ <string name="title_follower">Follower</string>
+ <string name="title_email_follower">Email Follower</string>
+
+ <string name="role_admin">Administrator</string>
+ <string name="role_editor">Editor</string>
+ <string name="role_author">Author</string>
+ <string name="role_contributor">Contributor</string>
+ <string name="role_follower">Follower</string>
+ <string name="role_viewer">Viewer</string>
+
+ <!--My profile-->
+ <string name="my_profile">My Profile</string>
+ <string name="first_name">First name</string>
+ <string name="last_name">Last name</string>
+ <string name="public_display_name">Public display name</string>
+ <string name="public_display_name_hint">Display name will default to your username if it is not set</string>
+ <string name="about_me">About me</string>
+ <string name="about_me_hint">A few words about you…</string>
+ <string name="start_over">Start Over</string>
+ <string name="site_settings_start_over_hint">Start your site over</string>
+ <string name="let_us_help">Let Us Help</string>
+ <string name="start_over_text">If you want a site but don\'t want any of the posts and pages you have now, our support team can delete your posts, pages, media and comments for you.\n\nThis will keep your site and URL active, but give you a fresh start on your content creation. Just contact us to have your current content cleared out.</string>
+ <string name="contact_support">Contact support</string>
+ <string name="confirm_delete_site">Confirm Delete Site</string>
+ <string name="confirm_delete_site_prompt">Please type in %1$s in the field below to confirm. Your site will then be gone forever.</string>
+ <string name="site_settings_export_content_title">Export content</string>
+ <string name="error_deleting_site">Error deleting site</string>
+ <string name="error_deleting_site_summary">There was an error in deleting your site. Please contact support for more assistance</string>
+ <string name="primary_domain">Primary Domain</string>
+ <string name="domain_removal">Domain Removal</string>
+ <string name="domain_removal_summary">Be careful! Deleting your site will also remove your domain(s) listed below.</string>
+ <string name="domain_removal_hint">The domains that will not work once you remove your site</string>
+ <string name="keep_your_content">Keep Your Content</string>
+ <string name="export_site_summary">If you are sure, please be sure to take the time and export your content now. It can not be recovered in the future.</string>
+ <string name="export_site_hint">Export your site to an XML file</string>
+ <string name="are_you_sure">Are You Sure?</string>
+ <string name="delete_site_summary">This action can not be undone. Deleting your site will remove all content, contributors, and domains from the site.</string>
+ <string name="delete_site_hint">Delete site</string>
+ <string name="delete_site_progress">Deleting site…</string>
+ <string name="purchases_request_error">Something went wrong. Could not request purchases.</string>
+ <string name="premium_upgrades_title">Premium Upgrades</string>
+ <string name="premium_upgrades_message">You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site.</string>
+ <string name="show_purchases">Show purchases</string>
+ <string name="checking_purchases">Checking purchases</string>
+
+ <!--Account Settings-->
+ <string name="account_settings">Account Settings</string>
+ <string name="pending_email_change_snackbar">Click the verification link in the email sent to %1$s to confirm your new address</string>
+ <string name="primary_site">Primary site</string>
+ <string name="web_address">Web Address</string>
+ <string name="web_address_dialog_hint">Shown publicly when you comment.</string>
+ <string name="exporting_content_progress">Exporting content…</string>
+ <string name="export_email_sent">Export email sent!</string>
+ <string name="export_your_content">Export your content</string>
+ <string name="export_your_content_message">Your posts, pages, and settings will be emailed to you at %s.</string>
+
+ <!-- Plans -->
+ <string name="plan">Plan</string>
+ <string name="plans">Plans</string>
+ <string name="plans_loading_error">Unable to load plans</string>
+ <string name="plans_manage">Manage your plan at\nWordPress.com/plans</string>
+ <string name="enter_your_password_instead">Enter your password instead</string>
+
+ <!-- Plans business post-purchase -->
+ <string name="plans_post_purchase_title_intro">It\'s all yours, way to go!</string>
+ <string name="plans_post_purchase_text_intro">Your site is doing somersaults in excitement! Now explore your site\'s new features and choose where you\'d like to begin.</string>
+ <string name="plans_post_purchase_title_customize">Customize Fonts &amp; Colors</string>
+ <string name="plans_post_purchase_text_customize">You now have access to custom fonts, custom colors, and custom CSS editing capabilities.</string>
+ <string name="plans_post_purchase_button_customize">Customize my Site</string>
+ <string name="plans_post_purchase_title_video">Bring posts to life with video</string>
+ <string name="plans_post_purchase_text_video">You can upload and host videos on your site with VideoPress and your expanded media storage.</string>
+ <string name="plans_post_purchase_button_video">Start new post</string>
+ <string name="plans_post_purchase_title_themes">Find a perfect, Premium theme</string>
+ <string name="plans_post_purchase_text_themes">You now have unlimited access to Premium themes. Preview any theme on your site to get started.</string>
+ <string name="plans_post_purchase_button_themes">Browse Themes</string>
+
+ <!-- gravatar -->
+ <string name="gravatar_tip">New! Tap your Gravatar to change it!</string>
+ <string name="error_cropping_image">Error cropping the image</string>
+ <string name="error_locating_image">Error locating the cropped image</string>
+ <string name="error_refreshing_gravatar">Error reloading your Gravatar</string>
+ <string name="error_updating_gravatar">Error updating your Gravatar</string>
+ <string name="gravatar_camera_and_media_permission_required">Permissions required in order to select or capture a photo</string>
+
+ <!-- Editor -->
+ <string name="discard">Discard</string>
+ <string name="edit">Edit</string>
+ <string name="tap_to_try_again">Tap to try again!</string>
+ <string name="uploading">Uploading…</string>
+ <string name="uploading_gallery_placeholder">Uploading gallery…</string>
+
+ <string name="alert_insert_image_html_mode">Can\'t insert media directly in HTML mode. Please switch back to visual mode.</string>
+ <string name="alert_action_while_uploading">You are currently uploading media. Please wait until this completes.</string>
+ <string name="alert_error_adding_media">An error occurred while inserting media</string>
+
+ <string name="stop_upload_dialog_title">Stop uploading?</string>
+ <string name="stop_upload_button">Stop Upload</string>
+
+ <string name="image_settings_dismiss_dialog_title">Discard unsaved changes?</string>
+ <string name="image_settings_save_toast">Changes saved</string>
+
+ <string name="image_caption">Caption</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Link to</string>
+ <string name="image_width">Width</string>
+
+ <!-- Editor: Accessibility - format bar button descriptions -->
+ <string name="format_bar_description_bold">Bold</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Underline</string>
+ <string name="format_bar_description_strike">Strikethrough</string>
+ <string name="format_bar_description_quote">Block quote</string>
+ <string name="format_bar_description_link">Insert link</string>
+ <string name="format_bar_description_more">Insert more</string>
+ <string name="format_bar_description_media">Insert media</string>
+ <string name="format_bar_description_ul">Unordered list</string>
+ <string name="format_bar_description_ol">Ordered list</string>
+ <string name="format_bar_description_html">HTML mode</string>
+ <string name="visual_editor">Visual editor</string>
+ <string name="image_thumbnail">Image thumbnail</string>
+
+ <!-- Editor: Errors -->
+ <string name="editor_failed_uploads_switch_html">Some media uploads have failed. You can\'t switch to HTML mode
+ in this state. Remove all failed uploads and continue?</string>
+ <string name="editor_toast_invalid_path">Invalid file path</string>
+ <string name="editor_toast_changes_saved">Changes saved</string>
+ <string name="editor_toast_uploading_please_wait">You are currently uploading media. Please wait until this completes.</string>
+ <string name="editor_toast_failed_uploads">Some media uploads have failed. You can\'t save or publish
+ your post in this state. Would you like to remove all failed media?</string>
+ <string name="editor_remove_failed_uploads">Remove failed uploads</string>
+
+</resources>
diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml
new file mode 100644
index 000000000..b79c84321
--- /dev/null
+++ b/WordPress/src/main/res/values/styles.xml
@@ -0,0 +1,435 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="WordPress" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Material theme -->
+ <item name="colorPrimary">@color/color_primary</item>
+ <item name="colorPrimaryDark">@color/color_primary_dark</item>
+ <item name="colorAccent">@color/color_accent</item>
+ <item name="colorControlActivated">@color/color_control_activated</item>
+ <item name="colorControlHighlight">@color/color_control_highlight</item>
+
+ <item name="android:windowBackground">@color/grey_lighten_30</item>
+ <item name="android:actionBarItemBackground">@drawable/selectable_background_wordpress</item>
+ <item name="android:popupMenuStyle">@style/PopupMenu.WordPress</item>
+ <item name="android:dropDownListViewStyle">@style/DropDownListView.Light.WordPress</item>
+ <item name="android:actionDropDownStyle">@style/DropDownNav.WordPress</item>
+ <item name="android:actionModeCloseButtonStyle">@style/ActionButton.CloseMode.WordPress</item>
+ <item name="android:actionModeBackground">@color/color_primary_dark</item>
+ <item name="android:actionBarTabTextStyle">@style/TabTextStyle.WordPress</item>
+ <item name="android:actionBarTabBarStyle">@style/TabBarStyle.WordPress</item>
+ <item name="android:statusBarColor">@color/status_bar_tint</item>
+
+ <item name="colorButtonNormal">@color/light_gray</item>
+
+ <!-- Light.DarkActionBar specific -->
+ <item name="android:actionBarWidgetTheme">@style/Theme.WordPress.Widget</item>
+
+ <item name="windowActionModeOverlay">true</item>
+ <item name="swipeToRefreshStyle">@style/WordPress.SwipeToRefresh</item>
+ <item name="searchViewStyle">@style/WordPress.SearchViewStyle</item>
+ </style>
+
+ <!-- http://android-developers.blogspot.com/2014/10/appcompat-v21-material-design-for-pre.html -->
+ <style name="WordPress.SearchViewStyle" parent="Widget.AppCompat.SearchView.ActionBar" />
+
+ <style name="WordPress.DropDownListView.Light" parent="WordPress">
+ <item name="android:dropDownListViewStyle">@style/DropDownListView.Light.WordPress</item>
+ </style>
+
+ <!-- this style is only referenced in a Light.DarkActionBar based theme -->
+ <style name="Theme.WordPress.Widget" parent="Theme.AppCompat">
+ <item name="android:popupMenuStyle">@style/PopupMenu.WordPress</item>
+ <item name="android:dropDownListViewStyle">@style/DropDownListView.Light.WordPress</item>
+ </style>
+
+ <style name="SignInTheme" parent="Theme.AppCompat.NoActionBar">
+ <item name="colorControlActivated">@color/color_control_activated</item>
+ <item name="colorControlHighlight">@color/color_control_highlight</item>
+ <item name="android:statusBarColor">@color/status_bar_tint</item>
+ </style>
+
+ <style name="TabBarStyle.WordPress" parent="@android:style/Widget.Holo.ActionBar.TabBar">
+ <item name="android:showDividers">middle</item>
+ <item name="android:divider">@drawable/tab_divider_wordpress</item>
+ </style>
+
+ <style name="PopupMenu.WordPress" parent="android:Widget.Holo.ListPopupWindow">
+ <item name="android:popupBackground">@drawable/menu_dropdown_panel_wordpress</item>
+ </style>
+
+ <style name="DropDownListView.Dark.WordPress" parent="android:Widget.Holo.ListView.DropDown">
+ <item name="android:listSelector">@drawable/selectable_background_wordpress</item>
+ <item name="android:fadeScrollbars">false</item>
+ <item name="android:scrollbarThumbVertical">@drawable/scrollbar_transparent_white</item>
+ </style>
+
+ <style name="DropDownListView.Light.WordPress" parent="android:Widget.Holo.ListView.DropDown">
+ <item name="android:listSelector">@drawable/selectable_background_wordpress</item>
+ <item name="android:fadeScrollbars">false</item>
+ <item name="android:scrollbarThumbVertical">@drawable/scrollbar_transparent_black</item>
+ </style>
+
+ <style name="TabTextStyle.WordPress" parent="android:Widget.Holo.ActionBar.TabText">
+ <item name="android:textColor">@color/grey_dark</item>
+ </style>
+
+ <style name="DropDownNav.WordPress" parent="android:Widget.Holo.Light.Spinner">
+ <item name="android:background">@drawable/spinner_background_ab_wordpress</item>
+ <item name="android:dropDownSelector">@drawable/selectable_background_wordpress</item>
+ </style>
+
+ <style name="FilteredRecyclerViewSpinner.WordPress" parent="DropDownNav.WordPress">
+ <item name="android:background">@color/transparent</item>
+ </style>
+
+ <style name="FilteredRecyclerViewFilterTextView.WordPress" parent="android:TextAppearance.Widget.TextView">
+ <item name="android:padding">@dimen/margin_medium</item>
+ <item name="android:layout_marginLeft">@dimen/margin_extra_large</item>
+ <item name="android:paddingRight">@dimen/margin_large</item>
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ </style>
+
+ <style name="FilteredRecyclerViewToolbar" parent="Widget.AppCompat.Toolbar">
+ <item name="android:paddingLeft">@dimen/margin_filter_spinner</item>
+ <item name="android:elevation">@dimen/filter_subbar_elevation</item>
+ <item name="android:theme">@style/FilteredRecyclerViewToolbar.Theme</item>
+ </style>
+
+ <style name="FilteredRecyclerViewToolbar.Theme" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
+ <item name="android:listDivider">@null</item>
+ </style>
+
+ <style name="ProgressBar.WordPress" parent="android:Widget.Holo.ProgressBar.Horizontal">
+ <item name="android:progressDrawable">@drawable/progress_horizontal_wordpress</item>
+ </style>
+
+ <style name="ActionButton.CloseMode.WordPress" parent="android:Widget.Holo.ActionButton.CloseMode">
+ <item name="android:background">@drawable/btn_cab_done_wordpress</item>
+ </style>
+
+ <style name="WordPressSettingsSectionHeader">
+ <item name="android:layout_marginTop">@dimen/margin_large</item>
+ <item name="android:layout_marginBottom">@dimen/margin_small</item>
+ <item name="android:textColor">@color/grey</item>
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="WordPressSubHeader">
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:layout_marginTop">@dimen/margin_large</item>
+ <item name="android:textColor">@color/grey</item>
+ </style>
+
+ <style name="WordPressTitleAppearance" parent="@android:style/TextAppearance">
+ <item name="android:singleLine">true</item>
+ <item name="android:shadowColor">#BB000000</item>
+ <item name="android:shadowRadius">2.75</item>
+ <item name="android:textColor">#FFF6F6F6</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="WordPressWelcomeStyle">
+ <item name="android:layout_gravity">center</item>
+ </style>
+
+ <style name="ThemeDetailsHeader">
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="MediaGalleryText">
+ <item name="android:textColor">@color/white</item>
+ <item name="android:textSize">16sp</item>
+ </style>
+
+ <style name="MediaGalleryNumOfColumns" parent="@style/MediaGalleryText">
+ <item name="android:textColor">@color/media_grid_item_checkstate_text_selector</item>
+ <item name="android:background">@drawable/media_gallery_option_selector</item>
+ <item name="android:button">@null</item>
+ </style>
+
+ <style name="MediaGalleryTypeCheckbox">
+ <item name="android:textColor">@color/white</item>
+ <item name="android:drawablePadding">8dp</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:button">@null</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="EmptyListText">
+ <item name="android:layout_width">fill_parent</item>
+ <item name="android:layout_height">fill_parent</item>
+ <item name="android:gravity">center_horizontal|center_vertical</item>
+ <item name="android:textColor">@color/grey_darken_10</item>
+ </style>
+
+ <style name="WhiteButton" parent="Widget.AppCompat.Button">
+ <item name="colorButtonNormal">@color/white</item>
+ </style>
+
+ <!-- NUX Styles -->
+ <style name="WordPress.NUXPrimaryButton">
+ <item name="android:textColor">@color/nux_primary_button</item>
+ <item name="android:layout_height">@dimen/nux_main_button_height</item>
+ <item name="android:layout_marginLeft">4dp</item>
+ <item name="android:layout_marginRight">4dp</item>
+ <item name="android:layout_marginBottom">8dp</item>
+ <item name="android:clickable">true</item>
+ <item name="android:background">@drawable/nux_primary_button_selector</item>
+ <item name="android:stateListAnimator">@anim/raise</item>
+ </style>
+
+ <style name="WordPress.NUXFlatButton">
+ <item name="android:textColor">@drawable/nux_flat_button_text_selector</item>
+ <item name="android:background">@drawable/nux_flat_button_selector</item>
+ <item name="android:padding">8dp</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <style name="WordPress.NUXGreyButtonNoBg">
+ <item name="android:gravity">center_horizontal</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">@drawable/nux_flat_button_grey_text_selector</item>
+ </style>
+
+ <style name="WordPress.NUXEditText">
+ <item name="android:background">@color/white</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textColor">#444444</item>
+ <item name="android:textColorHint">#AAAAAA</item>
+ <item name="android:padding">12dp</item>
+ <item name="android:layout_marginLeft">40dp</item>
+ </style>
+
+ <style name="WordPress.NUXTitle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">24dp</item>
+ <item name="android:gravity">center_horizontal</item>
+ <item name="android:textColor">@color/white</item>
+ <item name="android:textSize">@dimen/nux_title_font_size</item>
+ <item name="android:lineSpacingExtra">-4sp</item>
+ </style>
+
+ <style name="WordPress.EmptyList">
+ <item name="android:textColor">@color/grey_darken_30</item>
+ <item name="android:gravity">center</item>
+ <item name="android:layout_marginLeft">16dp</item>
+ <item name="android:layout_marginRight">16dp</item>
+ </style>
+
+ <style name="WordPress.EmptyList.Title" parent="WordPress.EmptyList">
+ <item name="android:textSize">@dimen/empty_list_title_text_size</item>
+ <item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
+ </style>
+
+ <style name="WordPress.EmptyList.Description" parent="WordPress.EmptyList">
+ <item name="android:textSize">@dimen/empty_list_description_text_size</item>
+ </style>
+
+ <!--
+ moderation views on comment detail
+ -->
+ <style name="WordPress.ModerateButton">
+ <item name="android:textColor">@color/grey</item>
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:background">@drawable/moderate_button_selector</item>
+ <item name="android:paddingTop">@dimen/margin_small</item>
+ <item name="android:paddingRight">@dimen/margin_small</item>
+ <item name="android:paddingLeft">@dimen/margin_small</item>
+ <item name="android:paddingBottom">@dimen/margin_medium</item>
+ <item name="android:drawablePadding">-4dp</item>
+ <item name="android:focusable">true</item>
+ <item name="android:clickable">true</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="WordPress.BorderedBackground">
+ <item name="android:background">@color/white</item>
+ </style>
+
+ <style name="WordPress.SwipeToRefresh">
+ <item name="refreshIndicatorColor">@color/blue_medium</item>
+ </style>
+
+ <!--My Site Styles-->
+ <style name="MySiteListRowLayout">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingLeft">@dimen/my_site_list_row_padding_left</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+
+ <style name="MySiteListRowIcon">
+ <item name="android:layout_width">@dimen/my_site_list_row_icon_size</item>
+ <item name="android:layout_height">@dimen/my_site_list_row_icon_size</item>
+ <item name="android:layout_alignParentLeft">true</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:layout_marginTop">@dimen/margin_large</item>
+ <item name="android:layout_marginBottom">@dimen/margin_large</item>
+ <item name="android:layout_marginRight">@dimen/margin_extra_large</item>
+ <item name="android:gravity">center_vertical</item>
+ </style>
+
+ <style name="MySiteListRowTextView">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:layout_marginRight">@dimen/margin_medium</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ <item name="android:paddingTop">@dimen/margin_large</item>
+ <item name="android:paddingBottom">@dimen/margin_large</item>
+ <item name="android:paddingLeft">@dimen/margin_extra_large</item>
+ </style>
+
+ <style name="MySiteListRowSecondaryElement">
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:layout_marginRight">@dimen/margin_extra_large</item>
+ <item name="android:gravity">right|center_vertical</item>
+ </style>
+
+ <style name="MySiteListRowSecondaryTextView" parent="MySiteListRowSecondaryElement">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_alignParentEnd">true</item>
+ <item name="android:layout_alignParentRight">true</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:textColor">@color/grey_darken_20</item>
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ <item name="android:paddingTop">@dimen/margin_large</item>
+ <item name="android:paddingBottom">@dimen/margin_large</item>
+ <item name="android:paddingLeft">@dimen/margin_extra_large</item>
+ </style>
+
+ <style name="MySiteListRowSecondaryIcon" parent="MySiteListRowSecondaryElement">
+ <item name="android:layout_width">16dp</item>
+ <item name="android:layout_height">16dp</item>
+ </style>
+
+ <style name="MySiteListHeaderLayout">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingBottom">@dimen/my_site_margin_general</item>
+ <item name="android:paddingLeft">@dimen/my_site_list_row_padding_left</item>
+ <item name="android:paddingTop">@dimen/my_site_margin_general</item>
+ </style>
+
+ <style name="MySiteListHeaderTextView">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:textColor">@color/grey</item>
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="MySiteListSectionDividerView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:layout_marginLeft">@dimen/margin_large</item>
+ <item name="android:background">@color/grey_lighten_20</item>
+ <item name="android:layout_marginRight">@dimen/margin_extra_large</item>
+ </style>
+
+ <style name="MeListRowLayout">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:orientation">horizontal</item>
+ </style>
+
+ <style name="MeListRowIcon">
+ <item name="android:layout_width">@dimen/me_list_row_icon_size</item>
+ <item name="android:layout_height">@dimen/me_list_row_icon_size</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:layout_marginTop">@dimen/margin_large</item>
+ <item name="android:layout_marginBottom">@dimen/margin_large</item>
+ <item name="android:layout_marginLeft">@dimen/margin_medium</item>
+ <item name="android:layout_marginRight">@dimen/margin_extra_large</item>
+ </style>
+
+ <style name="MeListRowTextView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:minHeight">0dp</item>
+ <item name="android:padding">@dimen/margin_extra_large</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ </style>
+
+ <style name="MeListSectionDividerView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:background">@color/me_divider</item>
+ </style>
+
+ <style name="fab_menu_label_style">
+ <item name="android:background">@drawable/fab_menu_label_background</item>
+ <item name="android:textColor">@color/white</item>
+ </style>
+
+ <!-- Used in Notifications and Site settings to animate nested preference screens -->
+ <style name="DialogAnimations">
+ <item name="android:windowEnterAnimation">@anim/activity_slide_in_from_right</item>
+ <item name="android:windowExitAnimation">@anim/activity_slide_out_to_right</item>
+ </style>
+
+ <style name="MyProfileRow">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:orientation">vertical</item>
+ <item name="android:padding">@dimen/margin_extra_large</item>
+ </style>
+
+ <style name="MyProfileLabel">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ </style>
+
+ <style name="MyProfileText">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textColor">@color/grey_darken_10</item>
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ </style>
+
+ <style name="MyProfileDividerView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:background">@color/me_divider</item>
+ </style>
+
+ <!--People Management Styles-->
+ <style name="PersonAvatar">
+ <item name="android:layout_width">@dimen/people_avatar_sz</item>
+ <item name="android:layout_height">@dimen/people_avatar_sz</item>
+ <item name="android:layout_margin">@dimen/margin_extra_large</item>
+ </style>
+
+ <style name="PersonTextView">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:maxLines">1</item>
+ </style>
+
+ <!-- Plans Styles -->
+ <style name="PlansProgressBar" parent="@android:style/Widget.Holo.ProgressBar" />
+</resources>
diff --git a/WordPress/src/main/res/values/styles_calypso.xml b/WordPress/src/main/res/values/styles_calypso.xml
new file mode 100644
index 000000000..7b5951d23
--- /dev/null
+++ b/WordPress/src/main/res/values/styles_calypso.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Calypso (WP.com homepage) styles
+-->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <style name="CalypsoTheme" parent="@style/WordPress">
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:popupMenuStyle">@style/Calypso.PopupMenu</item>
+ <item name="android:textAppearanceSmallPopupMenu">@style/Calypso.TextAppearance</item>
+ <item name="android:textAppearanceLargePopupMenu">@style/Calypso.TextAppearance</item>
+ </style>
+
+ <style name="Calypso.SiteSettingsTheme" parent="@style/CalypsoTheme">
+ <item name="android:windowActionModeOverlay">true</item>
+ <item name="android:actionModeCloseDrawable">@drawable/ic_arrow_back_white_24dp</item>
+ <item name="android:actionModeCloseButtonStyle">@style/Calypso.ActionMode.Button</item>
+ <item name="android:actionModeStyle">@style/Calypso.ActionMode</item>
+ <item name="colorPrimaryDark">@color/grey_darken_10</item>
+
+ <item name="android:windowContentOverlay">@color/grey_lighten_30</item>
+ <item name="alertDialogTheme">@style/Calypso.AlertDialog</item>
+ <item name="dialogTheme">@style/Calypso.AlertDialog</item>
+ <item name="colorControlActivated">@color/blue_medium</item>
+ <item name="colorControlNormal">@color/grey_lighten_20</item>
+ <item name="colorAccent">@color/orange_jazzy</item>
+ <item name="android:listDivider">@drawable/preferences_divider</item>
+ <item name="android:dividerHeight">@dimen/site_settings_divider_height</item>
+ </style>
+
+ <style name="Calypso.ActionMode.Button" parent="ActionButton.CloseMode.WordPress">
+ <item name="android:background">@color/transparent</item>
+ </style>
+
+ <style name="Calypso.ActionMode.Text">
+ <item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
+ <item name="android:textColor">@color/white</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textSize">@dimen/text_sz_extra_large</item>
+ </style>
+
+ <style name="Calypso.ActionMode" parent="@style/Widget.AppCompat.ActionMode">
+ <item name="android:titleTextStyle">@style/Calypso.ActionMode.Text</item>
+ <item name="android:background">@color/grey_darken_10</item>
+ </style>
+
+ <style name="Calypso.AlertDialog" parent="@style/Theme.AppCompat.Light.Dialog.Alert">
+ <item name="windowActionBar">false</item>
+ <item name="colorPrimary">@color/white</item>
+ <item name="colorPrimaryDark">@color/grey_darken_10</item>
+ <item name="colorAccent">@color/blue_medium</item>
+ <item name="actionModeStyle">@style/Calypso.ActionMode</item>
+ <item name="android:background">@color/white</item>
+ <item name="android:listDivider">@drawable/preferences_divider</item>
+ <item name="android:dividerHeight">@dimen/site_settings_divider_height</item>
+ </style>
+
+ <style name="Calypso.TextAppearance.AlertDialog">
+ <item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
+ <item name="android:background">@color/white</item>
+ <item name="android:textSize">@dimen/text_sz_extra_large</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColor">@color/grey_dark</item>
+ <item name="android:paddingLeft">@dimen/dlp_title_padding_start</item>
+ <item name="android:paddingRight">@dimen/dlp_title_padding_end</item>
+ </style>
+
+ <style name="CalypsoTheme.NoActionBarShadow" parent="CalypsoTheme">
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+ <style name="Calypso.NoActionBar" parent="CalypsoTheme">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
+
+ <style name="Calypso.PopupMenu" parent="@android:style/Widget.Holo.Light.PopupMenu">
+ <item name="android:textAppearance">@style/Calypso.TextAppearance</item>
+ </style>
+
+ <style name="Calypso.TextAppearance" parent="@android:style/TextAppearance">
+ <item name="android:textColor">@color/grey_dark</item>
+ </style>
+
+ <style name="Calypso.Text.Date" parent="android:Widget.TextView">
+ <item name="android:textSize">@dimen/text_sz_large</item>
+ <item name="android:textColor">@color/grey_lighten_10</item>
+ </style>
+
+ <style name="Calypso.Text.Header" parent="android:Widget.TextView">
+ <item name="android:textSize">@dimen/text_sz_medium</item>
+ <item name="android:textColor">@color/grey_lighten_10</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="Calypso.FloatingActivity" parent="Calypso.NoActionBar">
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:windowFullscreen">false</item>
+ </style>
+
+ <style name="Calypso.SegmentedControl" parent="Widget.AppCompat.CompoundButton.RadioButton">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:minHeight">32dp</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:button">@null</item>
+ <item name="android:gravity">center</item>
+ <item name="android:padding">4dp</item>
+ <item name="android:textColor">@color/calypso_segmented_control_text</item>
+ <item name="android:textSize">@dimen/text_sz_small</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+</resources>
diff --git a/WordPress/src/main/res/xml/account_settings.xml b/WordPress/src/main/res/xml/account_settings.xml
new file mode 100644
index 000000000..d3893a96b
--- /dev/null
+++ b/WordPress/src/main/res/xml/account_settings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:key="@string/pref_key_account_settings_root">
+
+ <Preference
+ android:enabled="false"
+ android:key="@string/pref_key_username"
+ android:layout="@layout/preference_layout"
+ android:title="@string/username" />
+
+ <org.wordpress.android.ui.prefs.EditTextPreferenceWithValidation
+ android:key="@string/pref_key_email"
+ android:layout="@layout/preference_layout"
+ android:title="@string/email_address" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:key="@string/pref_key_primary_site"
+ android:layout="@layout/preference_layout"
+ android:title="@string/primary_site" />
+
+ <org.wordpress.android.ui.prefs.EditTextPreferenceWithValidation
+ android:key="@string/pref_key_web_address"
+ android:layout="@layout/preference_layout"
+ android:title="@string/web_address" />
+
+</PreferenceScreen>
diff --git a/WordPress/src/main/res/xml/app_settings.xml b/WordPress/src/main/res/xml/app_settings.xml
new file mode 100644
index 000000000..5367dd712
--- /dev/null
+++ b/WordPress/src/main/res/xml/app_settings.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:key="@string/pref_key_app_settings_root">
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:key="@string/pref_key_language"
+ android:layout="@layout/preference_layout"
+ android:title="@string/interface_language" />
+
+ <SwitchPreference
+ android:defaultValue="true"
+ android:key="@string/pref_key_send_usage"
+ android:layout="@layout/preference_layout"
+ android:summary="@string/preference_send_usage_stats_summary"
+ android:title="@string/preference_send_usage_stats" />
+
+ <PreferenceCategory
+ android:key="@string/pref_key_editor"
+ android:layout="@layout/preference_category"
+ android:title="@string/preference_editor">
+
+ <SwitchPreference
+ android:defaultValue="true"
+ android:key="@string/pref_key_visual_editor_enabled"
+ android:layout="@layout/preference_layout"
+ android:title="@string/preference_show_visual_editor" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="@string/pref_key_passlock_section"
+ android:layout="@layout/preference_category"
+ android:persistent="false"
+ android:title="@string/passcode_preference_title">
+
+ <SwitchPreference
+ android:key="@string/pref_key_passcode_toggle"
+ android:layout="@layout/preference_layout"
+ android:persistent="false"
+ android:title="@string/passcode_turn_on" />
+
+ <Preference
+ android:key="@string/pref_key_change_passcode"
+ android:layout="@layout/preference_layout"
+ android:persistent="false"
+ android:title="@string/passcode_change_passcode" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="@string/pref_key_about_section"
+ android:layout="@layout/preference_category"
+ android:title="@string/about_the_app">
+
+ <Preference
+ android:key="@string/pref_key_app_about"
+ android:layout="@layout/preference_layout"
+ android:title="@string/app_title" />
+
+ <Preference
+ android:key="@string/pref_key_oss_licenses"
+ android:layout="@layout/preference_layout"
+ android:title="@string/open_source_licenses" />
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/WordPress/src/main/res/xml/backup_scheme.xml b/WordPress/src/main/res/xml/backup_scheme.xml
new file mode 100644
index 000000000..f08be9004
--- /dev/null
+++ b/WordPress/src/main/res/xml/backup_scheme.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content>
+ <include domain="database" path="wordpress"/>
+ <include domain="database" path="persistentedittext.db"/>
+ <include domain="sharedpref" path="self_signed_certs_truststore.bks"/>
+ <include domain="sharedpref" path="org.wordpress.android_preferences.xml"/>
+ <include domain="sharedpref" path="simperium.xml"/>
+</full-backup-content>
diff --git a/WordPress/src/main/res/xml/notifications_settings.xml b/WordPress/src/main/res/xml/notifications_settings.xml
new file mode 100644
index 000000000..3b0ff89e9
--- /dev/null
+++ b/WordPress/src/main/res/xml/notifications_settings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:key="wp_pref_notifications_root">
+ <PreferenceCategory
+ android:key="@string/pref_notification_blogs"
+ android:title="@string/your_sites">
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="@string/pref_notification_other_category"
+ android:title="@string/notifications_other">
+ <PreferenceScreen
+ android:key="@string/pref_notification_other_blogs"
+ android:title="@string/notifications_comments_other_blogs"/>
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="@string/pref_notification_sights_sounds"
+ android:title="@string/notifications_sights_and_sounds">
+ <RingtonePreference
+ android:key="wp_pref_custom_notification_sound"
+ android:ringtoneType="notification"
+ android:showDefault="true"
+ android:title="@string/notification_sound"/>
+ <SwitchPreference
+ android:defaultValue="false"
+ android:key="wp_pref_notification_vibrate"
+ android:title="@string/notification_vibrate"/>
+ <SwitchPreference
+ android:defaultValue="false"
+ android:key="wp_pref_notification_light"
+ android:title="@string/notification_blink"/>
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/WordPress/src/main/res/xml/provider_paths.xml b/WordPress/src/main/res/xml/provider_paths.xml
new file mode 100644
index 000000000..7c82c7779
--- /dev/null
+++ b/WordPress/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ <external-path
+ name="external_files"
+ path="."/>
+</paths>
diff --git a/WordPress/src/main/res/xml/site_settings.xml b/WordPress/src/main/res/xml/site_settings.xml
new file mode 100644
index 000000000..b017142eb
--- /dev/null
+++ b/WordPress/src/main/res/xml/site_settings.xml
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:key="@string/pref_key_site_screen">
+
+ <!-- General settings -->
+ <PreferenceCategory
+ android:id="@+id/pref_site_general"
+ android:key="@string/pref_key_site_general"
+ android:title="@string/site_settings_general_header">
+
+ <org.wordpress.android.ui.prefs.SummaryEditTextPreference
+ android:id="@+id/pref_site_title"
+ android:key="@string/pref_key_site_title"
+ android:title="@string/site_settings_title_title"
+ app:summaryLines="1"
+ app:maxSummaryLines="2"
+ app:longClickHint="@string/site_settings_title_hint" />
+
+ <org.wordpress.android.ui.prefs.SummaryEditTextPreference
+ android:id="@+id/pref_site_tagline"
+ android:key="@string/pref_key_site_tagline"
+ android:title="@string/site_settings_tagline_title"
+ app:summaryLines="1"
+ app:maxSummaryLines="1"
+ app:longClickHint="@string/site_settings_tagline_hint" />
+
+ <org.wordpress.android.ui.prefs.SummaryEditTextPreference
+ android:enabled="false"
+ android:id="@+id/pref_site_address"
+ android:key="@string/pref_key_site_address"
+ android:title="@string/site_settings_address_title"
+ app:summaryLines="1"
+ app:maxSummaryLines="1"
+ app:longClickHint="@string/site_settings_address_hint" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_site_privacy"
+ android:key="@string/pref_key_site_visibility"
+ android:title="@string/site_settings_privacy_title"
+ android:entries="@array/site_settings_privacy_entries"
+ android:entryValues="@array/site_settings_privacy_values"
+ android:defaultValue="-2"
+ app:longClickHint="@string/site_settings_privacy_hint"
+ app:entryDetails="@array/privacy_details" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_site_language"
+ android:key="@string/pref_key_site_language"
+ android:title="@string/site_settings_language_title"
+ android:entries="@array/language_codes"
+ android:entryValues="@array/language_codes"
+ app:longClickHint="@string/site_settings_language_hint" />
+
+ </PreferenceCategory>
+
+ <!-- Account settings; only used with .org sites -->
+ <PreferenceCategory
+ android:id="@+id/pref_site_account"
+ android:key="@string/pref_key_site_account"
+ android:title="@string/site_settings_account_header">
+
+ <org.wordpress.android.ui.prefs.SummaryEditTextPreference
+ android:id="@+id/pref_site_username"
+ android:key="@string/pref_key_site_username"
+ android:title="@string/site_settings_username_title"
+ android:enabled="false"
+ android:maxLength="@integer/max_length_username"
+ app:summaryLines="1"
+ app:maxSummaryLines="2"
+ app:longClickHint="@string/site_settings_username_hint" />
+
+ <org.wordpress.android.ui.prefs.SummaryEditTextPreference
+ android:id="@+id/pref_site_password"
+ android:key="@string/pref_key_site_password"
+ android:title="@string/site_settings_password_title"
+ android:enabled="false"
+ android:inputType="textPassword"
+ android:maxLength="@integer/max_length_password"
+ app:summaryLines="1"
+ app:maxSummaryLines="2"
+ app:longClickHint="@string/site_settings_password_hint" />
+
+ </PreferenceCategory>
+
+ <!-- Writing Settings -->
+ <PreferenceCategory
+ android:id="@+id/pref_site_writing"
+ android:key="@string/pref_key_site_writing"
+ android:title="@string/site_settings_writing_header">
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_default_category"
+ android:key="@string/pref_key_site_category"
+ android:title="@string/site_settings_default_category_title"
+ app:longClickHint="@string/site_settings_category_hint" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_default_format"
+ android:key="@string/pref_key_site_format"
+ android:title="@string/site_settings_default_format_title"
+ android:entries="@array/post_formats_array"
+ android:entryValues="@array/post_formats_array"
+ app:longClickHint="@string/site_settings_format_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_related_posts"
+ android:key="@string/pref_key_site_related_posts"
+ android:title="@string/site_settings_related_posts_title"
+ app:longClickHint="@string/site_settings_related_posts_hint" />
+
+ </PreferenceCategory>
+
+ <!-- Discussion settings -->
+ <PreferenceCategory
+ android:id="@+id/pref_discussion_settings2"
+ android:key="@string/pref_key_site_discussion"
+ android:title="@string/site_settings_discussion_header"
+ app:longClickHint="@string/site_settings_discussion_hint">
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_allow_comments"
+ android:key="@string/pref_key_site_allow_comments"
+ android:title="@string/site_settings_allow_comments_title"
+ app:longClickHint="@string/site_settings_allow_comments_hint" />
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_send_pingbacks"
+ android:key="@string/pref_key_site_send_pingbacks"
+ android:title="@string/site_settings_send_pingbacks_title"
+ app:longClickHint="@string/site_settings_send_pingbacks_hint" />
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_receive_pingbacks"
+ android:key="@string/pref_key_site_receive_pingbacks"
+ android:title="@string/site_settings_receive_pingbacks_title"
+ app:longClickHint="@string/site_settings_receive_pingbacks_hint" />
+
+ <PreferenceScreen
+ android:id="@+id/pref_more_discussion_settings"
+ android:key="@string/pref_key_site_more_discussion"
+ android:title="@string/site_settings_more_title"
+ app:longClickHint="@string/site_settings_more_hint">
+
+ <PreferenceCategory
+ android:id="@+id/pref_site_new_posts_defaults"
+ android:title="@string/site_settings_discussion_new_posts_header">
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_allow_comments_nested"
+ android:key="@string/pref_key_site_allow_comments_nested"
+ android:title="@string/site_settings_allow_comments_title"
+ app:longClickHint="@string/site_settings_allow_comments_hint" />
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_send_pingbacks_nested"
+ android:key="@string/pref_key_site_send_pingbacks_nested"
+ android:title="@string/site_settings_send_pingbacks_title"
+ app:longClickHint="@string/site_settings_send_pingbacks_hint" />
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_receive_pingbacks_nested"
+ android:key="@string/pref_key_site_receive_pingbacks_nested"
+ android:title="@string/site_settings_receive_pingbacks_title"
+ app:longClickHint="@string/site_settings_receive_pingbacks_hint" />
+
+ <org.wordpress.android.ui.prefs.LearnMorePreference
+ android:id="@+id/pref_learn_more"
+ android:key="@string/pref_key_site_learn_more"
+ android:title="@string/site_settings_learn_more_header" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:id="@+id/pref_site_comments"
+ android:title="@string/site_settings_comments_header">
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_identity_required"
+ android:key="@string/pref_key_site_identity_required"
+ android:title="@string/site_settings_identity_required_title"
+ app:longClickHint="@string/site_settings_identity_required_hint" />
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_user_account_required"
+ android:key="@string/pref_key_site_user_account_required"
+ android:title="@string/site_settings_account_required_title"
+ app:longClickHint="@string/site_settings_user_account_required_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_close_after"
+ android:key="@string/pref_key_site_close_after"
+ android:title="@string/site_settings_close_after_title"
+ android:defaultValue="0"
+ app:longClickHint="@string/site_settings_close_after_hint" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_sort_by"
+ android:key="@string/pref_key_site_sort_by"
+ android:title="@string/site_settings_sort_by_title"
+ android:entries="@array/site_settings_sort_entries"
+ android:entryValues="@array/site_settings_sort_values"
+ android:defaultValue="0"
+ app:longClickHint="@string/site_settings_sort_by_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_threading"
+ android:key="@string/pref_key_site_threading"
+ android:title="@string/site_settings_threading_title"
+ android:defaultValue="0"
+ app:longClickHint="@string/site_settings_threading_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_paging"
+ android:key="@string/pref_key_site_paging"
+ android:title="@string/site_settings_paging_title"
+ android:defaultValue="0"
+ app:longClickHint="@string/site_settings_paging_hint" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_whitelist"
+ android:key="@string/pref_key_site_whitelist"
+ android:title="@string/site_settings_whitelist_title"
+ android:entries="@array/site_settings_auto_approve_entries"
+ android:entryValues="@array/site_settings_auto_approve_values"
+ app:entryDetails="@array/site_settings_auto_approve_details"
+ app:longClickHint="@string/site_settings_whitelist_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_multiple_links"
+ android:key="@string/pref_key_site_multiple_links"
+ android:title="@string/site_settings_multiple_links_title"
+ app:longClickHint="@string/site_settings_multiple_links_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_moderation_hold"
+ android:key="@string/pref_key_site_moderation_hold"
+ android:title="@string/site_settings_moderation_hold_title"
+ app:longClickHint="@string/site_settings_moderation_hold_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_blacklist"
+ android:key="@string/pref_key_site_blacklist"
+ android:title="@string/site_settings_blacklist_title"
+ app:longClickHint="@string/site_settings_blacklist_hint" />
+
+ </PreferenceCategory>
+
+ </PreferenceScreen>
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:id="@+id/pref_this_device"
+ android:key="@string/pref_key_site_this_device"
+ android:title="@string/site_settings_this_device_header">
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_enable_location"
+ android:key="@string/pref_key_site_location"
+ android:title="@string/site_settings_location_title"
+ app:longClickHint="@string/site_settings_location_hint" />
+
+ <org.wordpress.android.ui.prefs.DetailListPreference
+ android:id="@+id/pref_default_image_width"
+ android:key="@string/pref_key_site_image_width"
+ android:title="@string/site_settings_default_image_width_title"
+ android:entries="@array/site_settings_image_width_entries"
+ android:entryValues="@array/site_settings_image_width_values"
+ app:longClickHint="@string/site_settings_image_width_hint" />
+
+ <org.wordpress.android.ui.prefs.WPSwitchPreference
+ android:id="@+id/pref_upload_and_link_image"
+ android:key="@string/pref_key_site_upload_and_link_image"
+ android:title="@string/site_settings_upload_and_link_image_title"
+ app:longClickHint="@string/site_settings_upload_and_link_image_hint" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:id="@+id/pref_advanced"
+ android:key="@string/pref_key_site_advanced"
+ android:title="@string/site_settings_advanced_header">
+
+ <PreferenceScreen
+ android:id="@+id/pref_start_screen"
+ android:key="@string/pref_key_site_start_over_screen"
+ android:title="@string/start_over"
+ app:longClickHint="@string/site_settings_start_over_hint">
+
+ <org.wordpress.android.ui.prefs.WPStartOverPreference
+ android:id="@+id/pref_start_over"
+ android:key="@string/pref_key_site_start_over"
+ android:title="@string/let_us_help"
+ android:summary="@string/start_over_text"
+ android:widgetLayout="@layout/start_over_preference_button"
+ android:layout="@layout/start_over_preference"
+ app:preficon="@drawable/gridicons_history"
+ app:longClickHint="@string/site_settings_start_over_hint"
+ app:buttonText="@string/contact_support"
+ app:buttonTextColor="@color/grey_dark"
+ app:buttonTextAllCaps="true" />
+
+ </PreferenceScreen>
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_export_site"
+ android:key="@string/pref_key_site_export_site"
+ android:title="@string/site_settings_export_content_title"
+ app:longClickHint="@string/export_site_hint" />
+
+ <org.wordpress.android.ui.prefs.WPPreference
+ android:id="@+id/pref_delete_site"
+ android:key="@string/pref_key_site_delete_site"
+ android:title="@string/site_settings_delete_site_title"
+ app:longClickHint="@string/delete_site_hint" />
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/WordPress/src/main/res/xml/stats_widget_info.xml b/WordPress/src/main/res/xml/stats_widget_info.xml
new file mode 100644
index 000000000..5c377adf2
--- /dev/null
+++ b/WordPress/src/main/res/xml/stats_widget_info.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:initialLayout="@layout/stats_widget_layout"
+ android:minHeight="@dimen/stats_widget_min_height"
+ android:configure="org.wordpress.android.ui.stats.StatsWidgetConfigureActivity"
+ android:resizeMode="horizontal"
+ android:minWidth="@dimen/stats_widget_five_cells"
+ android:minResizeWidth="@dimen/stats_widget_four_cells"
+ android:previewImage="@drawable/stats_widget_preview"
+ android:updatePeriodMillis="3600000">
+</appwidget-provider> \ No newline at end of file
diff --git a/WordPress/src/main/res/xml/wpcom_languages.xml b/WordPress/src/main/res/xml/wpcom_languages.xml
new file mode 100644
index 000000000..ee602d9ca
--- /dev/null
+++ b/WordPress/src/main/res/xml/wpcom_languages.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<languages>
+<language id="1" code="en">en - English</language>
+<language id="2" code="af">af - Afrikaans</language>
+<language id="418" code="als">als - Alemannisch</language>
+<language id="3" code="ar">ar - العربية</language>
+<language id="419" code="arc">arc - Aramaic</language>
+<language id="4" code="as">as - Assamese</language>
+<language id="420" code="ast">ast - Asturianu</language>
+<language id="421" code="av">av - Авар</language>
+<language id="422" code="ay">ay - Aymar</language>
+<language id="423" code="ba">ba - Башҡорт</language>
+<language id="5" code="be">be - Беларуская</language>
+<language id="6" code="bg">bg - Български</language>
+<language id="7" code="bm">bm - Bamanankan</language>
+<language id="8" code="bn">bn - Bengali</language>
+<language id="9" code="bo">bo - Tibetan</language>
+<language id="424" code="br">br - Brezhoneg</language>
+<language id="10" code="ca">ca - Català</language>
+<language id="425" code="ce">ce - Нохчийн</language>
+<language id="11" code="cs">cs - Česky</language>
+<language id="12" code="csb">csb - Kaszëbsczi</language>
+<language id="426" code="cv">cv - Чӑваш</language>
+<language id="13" code="cy">cy - Cymraeg</language>
+<language id="14" code="da">da - Dansk</language>
+<language id="15" code="de">de - Deutsch</language>
+<language id="427" code="dv">dv - Divehi; Dhivehi; Maldivian</language>
+<language id="16" code="dz">dz - Dzongkha</language>
+<language id="17" code="el">el - Ελληνικά</language>
+<language id="18" code="eo">eo - Esperanto</language>
+<language id="19" code="es">es - Español</language>
+<language id="20" code="et">et - Eesti</language>
+<language id="429" code="eu">eu - Euskara</language>
+<language id="21" code="fa">fa - فارسی</language>
+<language id="22" code="fi">fi - Suomi</language>
+<language id="23" code="fo">fo - Føroyskt</language>
+<language id="24" code="fr">fr - Français</language>
+<language id="25" code="fur">fur - Furlan</language>
+<language id="26" code="fy">fy - Frysk</language>
+<language id="27" code="ga">ga - Gaeilge</language>
+<language id="430" code="gn">gn - Avañeẽ</language>
+<language id="28" code="gu">gu - Gujarati</language>
+<language id="29" code="he">he - עברית</language>
+<language id="30" code="hi">hi - Hindi</language>
+<language id="431" code="hr">hr - Hrvatski</language>
+<language id="31" code="hu">hu - Magyar</language>
+<language id="32" code="ia">ia - Interlingua</language>
+<language id="33" code="id">id - Bahasa Indonesia</language>
+<language id="432" code="ii">ii - Sichuan Yi; Nuosu</language>
+<language id="34" code="is">is - Íslenska</language>
+<language id="35" code="it">it - Italiano</language>
+<language id="36" code="ja">ja - 日本語</language>
+<language id="37" code="ka">ka - Georgian</language>
+<language id="38" code="km">km - Central Khmer</language>
+<language id="39" code="kn">kn - Kannada</language>
+<language id="40" code="ko">ko - 한국어</language>
+<language id="433" code="ks">ks - Kashmiri - (كشميري)</language>
+<language id="41" code="ku">ku - Kurdî / كوردي</language>
+<language id="434" code="kv">kv - Коми</language>
+<language id="42" code="la">la - Latina</language>
+<language id="43" code="li">li - Limburgs</language>
+<language id="44" code="lo">lo - Lao</language>
+<language id="45" code="lt">lt - Lietuvių</language>
+<language id="435" code="mk">mk - Македонски</language>
+<language id="46" code="ml">ml - Malayalam</language>
+<language id="47" code="ms">ms - Bahasa Melayu</language>
+<language id="436" code="nah">nah - Nahuatl</language>
+<language id="437" code="nap">nap - Nnapulitano</language>
+<language id="48" code="nds">nds - Plattdüütsch</language>
+<language id="49" code="nl">nl - Nederlands</language>
+<language id="50" code="nn">nn - Norsk (nynorsk)</language>
+<language id="51" code="no">no - Norsk (bokmål)</language>
+<language id="52" code="non">non - Norrǿna</language>
+<language id="53" code="nv">nv - Diné bizaad</language>
+<language id="54" code="oc">oc - Occitan</language>
+<language id="55" code="or">or - Oriya</language>
+<language id="56" code="os">os - Иронау</language>
+<language id="57" code="pa">pa - Punjabi</language>
+<language id="58" code="pl">pl - Polski</language>
+<language id="59" code="ps">ps - Pushto; Pashto</language>
+<language id="60" code="pt">pt - Português</language>
+<language id="438" code="pt-br">pt-br - Português do Brasil</language>
+<language id="439" code="qu">qu - Runa Simi</language>
+<language id="61" code="ro">ro - Română</language>
+<language id="62" code="ru">ru - Русский</language>
+<language id="63" code="sc">sc - Sardu</language>
+<language id="440" code="sd">sd - Sindhi</language>
+<language id="64" code="sk">sk - Slovenčina</language>
+<language id="65" code="sl">sl - Slovenščina</language>
+<language id="66" code="sq">sq - Shqip</language>
+<language id="67" code="sr">sr - Српски / Srpski</language>
+<language id="441" code="su">su - Basa Sunda</language>
+<language id="68" code="sv">sv - Svenska</language>
+<language id="69" code="ta">ta - Tamil</language>
+<language id="70" code="te">te - Telugu</language>
+<language id="71" code="th">th - ไทย</language>
+<language id="78" code="tr">tr - Türkçe</language>
+<language id="72" code="tt">tt - Tatarça</language>
+<language id="442" code="ty">ty - Reo Mā`ohi</language>
+<language id="443" code="udm">udm - Удмурт</language>
+<language id="444" code="ug">ug - Oyghurque</language>
+<language id="73" code="uk">uk - Українська</language>
+<language id="74" code="ur">ur - اردو</language>
+<language id="445" code="vec">vec - Vèneto</language>
+<language id="446" code="vi">vi - Tiếng Việt</language>
+<language id="75" code="wa">wa - Walon</language>
+<language id="447" code="xal">xal - Хальмг</language>
+<language id="76" code="yi">yi - ייִדיש</language>
+<language id="448" code="za">za - (Cuengh)</language>
+<language id="77" code="zh">zh - 中文</language>
+<language id="449" code="zh-cn">zh-cn - 中文(简体)</language>
+<language id="450" code="zh-hk">zh-hk - 中文(繁體)</language>
+<language id="451" code="zh-sg">zh-sg - 中文(简体)</language>
+<language id="452" code="zh-tw">zh-tw - 中文(繁體)</language>
+</languages> \ No newline at end of file
diff --git a/WordPress/src/wasabi/res/mipmap-hdpi/app_icon.png b/WordPress/src/wasabi/res/mipmap-hdpi/app_icon.png
new file mode 100644
index 000000000..9d1dd46d9
--- /dev/null
+++ b/WordPress/src/wasabi/res/mipmap-hdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/wasabi/res/mipmap-xhdpi/app_icon.png b/WordPress/src/wasabi/res/mipmap-xhdpi/app_icon.png
new file mode 100644
index 000000000..28a7ae7fc
--- /dev/null
+++ b/WordPress/src/wasabi/res/mipmap-xhdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/wasabi/res/mipmap-xxhdpi/app_icon.png b/WordPress/src/wasabi/res/mipmap-xxhdpi/app_icon.png
new file mode 100644
index 000000000..06969e035
--- /dev/null
+++ b/WordPress/src/wasabi/res/mipmap-xxhdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/wasabi/res/mipmap-xxxhdpi/app_icon.png b/WordPress/src/wasabi/res/mipmap-xxxhdpi/app_icon.png
new file mode 100644
index 000000000..40fc38428
--- /dev/null
+++ b/WordPress/src/wasabi/res/mipmap-xxxhdpi/app_icon.png
Binary files differ
diff --git a/WordPress/src/wasabi/res/values/strings.xml b/WordPress/src/wasabi/res/values/strings.xml
new file mode 100644
index 000000000..fec1b13fe
--- /dev/null
+++ b/WordPress/src/wasabi/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name" translatable="false">WordPress Beta</string>
+</resources>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..10427278e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,11 @@
+project.ext.preDexLibs = !project.hasProperty('disablePreDex')
+
+subprojects {
+ project.plugins.whenPluginAdded { plugin ->
+ if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) {
+ project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
+ } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
+ project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
+ }
+ }
+}
diff --git a/cq-configs/checkstyle/checkstyle-html.xsl b/cq-configs/checkstyle/checkstyle-html.xsl
new file mode 100644
index 000000000..e8605ed4a
--- /dev/null
+++ b/cq-configs/checkstyle/checkstyle-html.xsl
@@ -0,0 +1,177 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+ <xsl:output method="html" indent="yes"/>
+ <xsl:decimal-format decimal-separator="." grouping-separator="," />
+
+ <xsl:key name="files" match="file" use="@name" />
+
+ <!-- Checkstyle XML Style Sheet by Stephane Bailliez <sbailliez@apache.org> -->
+ <!-- Part of the Checkstyle distribution found at http://checkstyle.sourceforge.net -->
+ <!-- Usage (generates checkstyle_report.html): -->
+ <!-- <checkstyle failonviolation="false" config="${check.config}"> -->
+ <!-- <fileset dir="${src.dir}" includes="**/*.java"/> -->
+ <!-- <formatter type="xml" toFile="${doc.dir}/checkstyle_report.xml"/> -->
+ <!-- </checkstyle> -->
+ <!-- <style basedir="${doc.dir}" destdir="${doc.dir}" -->
+ <!-- includes="checkstyle_report.xml" -->
+ <!-- style="${doc.dir}/checkstyle-noframes-sorted.xsl"/> -->
+
+ <xsl:template match="checkstyle">
+ <html>
+ <head>
+ <style type="text/css">
+ .bannercell {
+ border: 0px;
+ padding: 0px;
+ }
+ body {
+ margin-left: 10;
+ margin-right: 10;
+ font:normal 80% arial,helvetica,sanserif;
+ background-color:#FFFFFF;
+ color:#000000;
+ }
+ .a td {
+ background: #efefef;
+ }
+ .b td {
+ background: #fff;
+ }
+ th, td {
+ text-align: left;
+ vertical-align: top;
+ }
+ th {
+ font-weight:bold;
+ background: #ccc;
+ color: black;
+ }
+ table, th, td {
+ font-size:100%;
+ border: none
+ }
+ table.log tr td, tr th {
+
+ }
+ h2 {
+ font-weight:bold;
+ font-size:140%;
+ margin-bottom: 5;
+ }
+ h3 {
+ font-size:100%;
+ font-weight:bold;
+ background: #525D76;
+ color: white;
+ text-decoration: none;
+ padding: 5px;
+ margin-right: 2px;
+ margin-left: 2px;
+ margin-bottom: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <a name="top"></a>
+ <!-- jakarta logo -->
+ <table border="0" cellpadding="0" cellspacing="0" width="100%">
+ <tr>
+ <td class="bannercell" rowspan="2">
+ <!--a href="http://jakarta.apache.org/">
+ <img src="http://jakarta.apache.org/images/jakarta-logo.gif" alt="http://jakarta.apache.org" align="left" border="0"/>
+ </a-->
+ </td>
+ <td class="text-align:right"><h2>CheckStyle Audit</h2></td>
+ </tr>
+ <tr>
+ <td class="text-align:right">Designed for use with <a href='http://checkstyle.sourceforge.net/'>CheckStyle</a> and <a href='http://jakarta.apache.org'>Ant</a>.</td>
+ </tr>
+ </table>
+ <hr size="1"/>
+
+ <!-- Summary part -->
+ <xsl:apply-templates select="." mode="summary"/>
+ <hr size="1" width="100%" align="left"/>
+
+ <!-- Package List part -->
+ <xsl:apply-templates select="." mode="filelist"/>
+ <hr size="1" width="100%" align="left"/>
+
+ <!-- For each package create its part -->
+ <xsl:apply-templates select="file[@name and generate-id(.) = generate-id(key('files', @name))]" />
+
+ <hr size="1" width="100%" align="left"/>
+
+
+ </body>
+ </html>
+ </xsl:template>
+
+
+
+ <xsl:template match="checkstyle" mode="filelist">
+ <h3>Files</h3>
+ <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
+ <tr>
+ <th>Name</th>
+ <th>Errors</th>
+ </tr>
+ <xsl:for-each select="file[@name and generate-id(.) = generate-id(key('files', @name))]">
+ <xsl:sort data-type="number" order="descending" select="count(key('files', @name)/error)"/>
+ <xsl:variable name="errorCount" select="count(error)"/>
+ <tr>
+ <xsl:call-template name="alternated-row"/>
+ <td><a href="#f-{@name}"><xsl:value-of select="@name"/></a></td>
+ <td><xsl:value-of select="$errorCount"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </xsl:template>
+
+
+ <xsl:template match="file">
+ <a name="f-{@name}"></a>
+ <h3>File <xsl:value-of select="@name"/></h3>
+
+ <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
+ <tr>
+ <th>Error Description</th>
+ <th>Line</th>
+ </tr>
+ <xsl:for-each select="key('files', @name)/error">
+ <xsl:sort data-type="number" order="ascending" select="@line"/>
+ <tr>
+ <xsl:call-template name="alternated-row"/>
+ <td><xsl:value-of select="@message"/></td>
+ <td><xsl:value-of select="@line"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ <a href="#top">Back to top</a>
+ </xsl:template>
+
+
+ <xsl:template match="checkstyle" mode="summary">
+ <h3>Summary</h3>
+ <xsl:variable name="fileCount" select="count(file[@name and generate-id(.) = generate-id(key('files', @name))])"/>
+ <xsl:variable name="errorCount" select="count(file/error)"/>
+ <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
+ <tr>
+ <th>Files</th>
+ <th>Errors</th>
+ </tr>
+ <tr>
+ <xsl:call-template name="alternated-row"/>
+ <td><xsl:value-of select="$fileCount"/></td>
+ <td><xsl:value-of select="$errorCount"/></td>
+ </tr>
+ </table>
+ </xsl:template>
+
+ <xsl:template name="alternated-row">
+ <xsl:attribute name="class">
+ <xsl:if test="position() mod 2 = 1">a</xsl:if>
+ <xsl:if test="position() mod 2 = 0">b</xsl:if>
+ </xsl:attribute>
+ </xsl:template>
+</xsl:stylesheet>
+
diff --git a/cq-configs/checkstyle/checkstyle.xml b/cq-configs/checkstyle/checkstyle.xml
new file mode 100644
index 000000000..2a6816e63
--- /dev/null
+++ b/cq-configs/checkstyle/checkstyle.xml
@@ -0,0 +1,252 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<module name="Checker">
+
+ <!-- Checks that a package-info.java file exists for each package. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->
+ <!--
+ <module name="JavadocPackage">
+ <property name="allowLegacy" value="true"/>
+ </module>
+ -->
+
+ <!-- Checks whether files end with a new line. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
+ <module name="NewlineAtEndOfFile"/>
+
+ <!-- Checks that property files contain the same keys. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
+ <module name="Translation"/>
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <!--
+ <module name="FileLength"/>
+ -->
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="FileTabCharacter"/>
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="RegexpSingleline">
+ <property name="format" value="\s+$"/>
+ <property name="minimum" value="0"/>
+ <property name="maximum" value="0"/>
+ <property name="message" value="Line has trailing spaces."/>
+ <property name="severity" value="error"/>
+ </module>
+
+
+ <module name="RegexpMultiline">
+ <property name="format"
+ value="\n[\t ]*\n[\t ]*\}"/>
+ <property name="message" value="Empty line not allowed before brace"/>
+ </module>
+
+ <module name="RegexpMultiline">
+ <property name="format"
+ value="\{[\t ]*\n[\t ]*\n"/>
+ <property name="message" value="Empty line not allowed after brace"/>
+ </module>
+
+ <module name="StrictDuplicateCode">
+ <property name="min" value="25"/>
+ </module>
+
+ <module name="RegexpSingleline">
+ <property name="format" value=" \/\/[^ \t]"/>
+ <property name="message" value="Line comments '//' must be followed by one whitespace"/>
+ <property name="severity" value="error"/>
+ </module>
+
+ <module name="TreeWalker">
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <!--
+ <module name="JavadocMethod">
+ <property name="scope" value="package"/>
+ <property name="allowMissingParamTags" value="true"/>
+ <property name="allowMissingThrowsTags" value="true"/>
+ <property name="allowMissingReturnTag" value="true"/>
+ <property name="allowThrowsTagsForSubclasses" value="true"/>
+ <property name="allowUndeclaredRTE" value="true"/>
+ <property name="allowMissingPropertyJavadoc" value="true"/>
+ </module>
+
+ <module name="JavadocType">
+ <property name="scope" value="package"/>
+ </module>
+ <module name="JavadocVariable">
+ <property name="scope" value="package"/>
+ </module>
+
+ <module name="JavadocStyle">
+ <property name="checkEmptyJavadoc" value="true"/>
+ </module>
+ -->
+ <!-- Checks for Naming Conventions. -->
+ <!-- See http://checkstyle.sf.net/config_naming.html -->
+ <module name="ConstantName"/>
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MemberName">
+ <property name="applyToPublic" value="true"/>
+ <property name="applyToProtected" value="false"/>
+ <property name="applyToPackage" value="false"/>
+ <property name="applyToPrivate" value="false"/>
+ <property name="format" value="^[a-zA-Z0-9]*$"/>
+ </module>
+ <module name="MemberName">
+ <property name="applyToPublic" value="false"/>
+ <property name="applyToProtected" value="true"/>
+ <property name="applyToPackage" value="true"/>
+ <property name="applyToPrivate" value="true"/>
+ <property name="format" value="^m[a-zA-Z0-9]*$"/>
+ </module>
+
+
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+ <!-- Checks for imports -->
+ <!-- See http://checkstyle.sf.net/config_import.html -->
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+
+ <module name="ImportOrder">
+ <property name="groups" value="android,com,junit,net,org,java,javax"/>
+ <property name="ordered" value="true"/>
+ <property name="separated" value="true"/>
+ <property name="option" value="above"/>
+ </module>
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <module name="LineLength">
+ <!-- what is a good max value? -->
+ <property name="max" value="120"/>
+ <!-- ignore lines like "$File: //depot/... $" -->
+ <property name="ignorePattern" value="\$File.*\$"/>
+ <property name="severity" value="info"/>
+ </module>
+ <module name="MethodLength">
+ <property name="tokens" value="METHOD_DEF"/>
+ <property name="max" value="40"/>
+ </module>
+ <module name="ParameterNumber"/>
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="EmptyForIteratorPad"/>
+ <module name="GenericWhitespace"/>
+ <module name="MethodParamPad"/>
+ <module name="NoWhitespaceAfter"/>
+ <module name="NoWhitespaceBefore"/>
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="TypecastParenPad"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround"/>
+ <module name="NoWhitespaceAfter">
+ <property name="tokens"
+ value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS, UNARY_PLUS" />
+ </module>
+
+ <!-- Modifier Checks -->
+ <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+ <module name="ModifierOrder"/>
+ <module name="RedundantModifier"/>
+
+
+ <!-- Checks for blocks. You know, those {}'s -->
+ <!-- See http://checkstyle.sf.net/config_blocks.html -->
+ <module name="AvoidNestedBlocks"/>
+ <module name="EmptyBlock">
+ <property name="option" value="text"/>
+ </module>
+ <module name="LeftCurly"/>
+ <module name="NeedBraces"/>
+ <module name="RightCurly"/>
+
+
+ <!-- Checks for common coding problems -->
+ <!-- See http://checkstyle.sf.net/config_coding.html -->
+ <!-- <module name="AvoidInlineConditionals"/> -->
+ <module name="EmptyStatement"/>
+ <module name="EqualsHashCode"/>
+ <module name="HiddenField">
+ <property name="ignoreConstructorParameter" value="true"/>
+ <property name="ignoreSetter" value="true"/>
+ <property name="severity" value="warning"/>
+ </module>
+ <module name="IllegalInstantiation"/>
+ <module name="InnerAssignment"/>
+ <!--
+ <module name="MagicNumber">
+ <property name="severity" value="warning"/>
+ </module>
+ -->
+ <module name="MissingSwitchDefault"/>
+ <!-- Problem with finding exception types... -->
+ <module name="RedundantThrows">
+ <property name="allowUnchecked" value="true"/>
+ <property name="suppressLoadErrors" value="true"/>
+ <property name="severity" value="info"/>
+ </module>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+
+ <!-- Checks for class design -->
+ <!-- See http://checkstyle.sf.net/config_design.html -->
+ <!-- <module name="DesignForExtension"/> -->
+ <module name="FinalClass"/>
+ <module name="HideUtilityClassConstructor"/>
+ <module name="InterfaceIsType"/>
+ <module name="VisibilityModifier">
+ <property name="packageAllowed" value="true"/>
+ <property name="protectedAllowed" value="true"/>
+ </module>
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="ArrayTypeStyle"/>
+ <!-- <module name="FinalParameters"/> -->
+ <module name="TodoComment">
+ <property name="format" value="FIXME"/>
+ <property name="severity" value="warning"/>
+ </module>
+ <module name="TrailingComment"/>
+
+ <module name="UpperEll"/>
+
+ <module name="FileContentsHolder"/> <!-- Required by comment suppression filters -->
+
+ <module name="OneStatementPerLine"/>
+
+ </module>
+
+ <!-- Enable suppression comments -->
+ <module name="SuppressionCommentFilter">
+ <property name="offCommentFormat" value="CHECKSTYLE IGNORE\s+(\S+)"/>
+ <property name="onCommentFormat" value="CHECKSTYLE END IGNORE\s+(\S+)"/>
+ <property name="checkFormat" value="$1"/>
+ </module>
+ <module name="SuppressWithNearbyCommentFilter">
+ <!-- Syntax is "SUPPRESS CHECKSTYLE name" -->
+ <property name="commentFormat" value="SUPPRESS CHECKSTYLE (\w+)"/>
+ <property name="checkFormat" value="$1"/>
+ <property name="influenceFormat" value="1"/>
+ </module>
+</module>
diff --git a/gradle.properties-example b/gradle.properties-example
new file mode 100644
index 000000000..5017b4b1c
--- /dev/null
+++ b/gradle.properties-example
@@ -0,0 +1,26 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# Enables new incubating mode that makes Gradle selective when configuring projects.
+# Only relevant projects are configured which results in faster builds for large multi-projects.
+# https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:configuration_on_demand
+# org.gradle.configureondemand = true
+
+# When set to true the Gradle daemon is used to run the build.
+# org.gradle.daemon = true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..3baa851b2
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..76f244240
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Sep 22 11:02:09 PDT 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..27309d923
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..832fdb607
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/keystore/debug.keystore b/keystore/debug.keystore
new file mode 100644
index 000000000..0ceb43e4d
--- /dev/null
+++ b/keystore/debug.keystore
Binary files differ
diff --git a/keystore/debug.keystore.properties b/keystore/debug.keystore.properties
new file mode 100644
index 000000000..d1b497c76
--- /dev/null
+++ b/keystore/debug.keystore.properties
@@ -0,0 +1,4 @@
+# The files and modifications provided by Facebook are for testing and evaluation purposes only. Facebook reserves all rights not expressly granted.
+key.alias=androiddebugkey
+key.store.password=android
+key.alias.password=android
diff --git a/libs/analytics/.gitignore b/libs/analytics/.gitignore
new file mode 100644
index 000000000..8babf679a
--- /dev/null
+++ b/libs/analytics/.gitignore
@@ -0,0 +1,25 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
diff --git a/libs/analytics/LICENSE b/libs/analytics/LICENSE
new file mode 100644
index 000000000..20efd1b3e
--- /dev/null
+++ b/libs/analytics/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/libs/analytics/README.md b/libs/analytics/README.md
new file mode 100644
index 000000000..0ef374cf6
--- /dev/null
+++ b/libs/analytics/README.md
@@ -0,0 +1,6 @@
+# WordPressCom-Analytics-Android
+Library for handling Analytics tracking in WordPress Android.
+
+Part of the [WordPress-Android] project.
+
+[WordPress-Android]: https://github.com/wordpress-mobile/WordPress-Android
diff --git a/libs/analytics/WordPressAnalytics/build.gradle b/libs/analytics/WordPressAnalytics/build.gradle
new file mode 100644
index 000000000..6332955f4
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/build.gradle
@@ -0,0 +1,89 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+apply plugin: 'signing'
+
+repositories {
+ jcenter()
+}
+
+dependencies {
+ compile 'com.automattic:tracks:1.1.0'
+ compile 'com.mixpanel.android:mixpanel-android:4.6.4'
+ compile 'org.wordpress:utils:1.11.0'
+}
+
+android {
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionName "1.2.0"
+ minSdkVersion 16
+ targetSdkVersion 24
+ }
+}
+
+version android.defaultConfig.versionName
+group = "org.wordpress"
+archivesBaseName = "analytics"
+
+signing {
+ required {
+ project.properties.containsKey("signing.keyId") && project.properties.containsKey("signing.secretKeyRingFile")
+ }
+ sign configurations.archives
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+ repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
+ authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword)
+ }
+
+ snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
+ authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword)
+ }
+
+ pom.project {
+ name 'WordPressCom-Analytics-Android'
+ packaging 'aar'
+ description 'Analytics lib for WordPress-Android'
+ url 'https://github.com/wordpress-mobile/WordPressCom-Analytics-Android'
+ scm {
+ connection 'scm:git:https://github.com/wordpress-mobile/WordPressCom-Analytics-Android.git'
+ developerConnection 'scm:git:https://github.com/wordpress-mobile/WordPressCom-Analytics-Android.git'
+ url 'https://github.com/wordpress-mobile/WordPressCom-Analytics-Android'
+ }
+
+ licenses {
+ license {
+ name 'The MIT License (MIT)'
+ url 'http://opensource.org/licenses/MIT'
+ }
+ }
+
+ developers {
+ developer {
+ id 'maxme'
+ name 'Maxime Biais'
+ email 'maxime@automattic.com'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/gradle.properties-example b/libs/analytics/WordPressAnalytics/gradle.properties-example
new file mode 100644
index 000000000..5281d935c
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/gradle.properties-example
@@ -0,0 +1,6 @@
+ossrhUsername=hello
+ossrhPassword=world
+
+signing.keyId=byebye
+signing.password=secret
+signing.secretKeyRingFile=/home/user/secret.gpg
diff --git a/libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml b/libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d4cf8c539
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.analytics">
+
+</manifest>
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java
new file mode 100644
index 000000000..a49629bc0
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.analytics;
+
+public class AnalyticsMetadata {
+ private boolean mIsUserConnected;
+ private boolean mIsWordPressComUser;
+ private boolean mIsJetpackUser;
+ private int mSessionCount;
+ private int mNumBlogs;
+ private String mUsername = "";
+ private String mEmail = "";
+
+ public AnalyticsMetadata() {}
+
+ public boolean isUserConnected() {
+ return mIsUserConnected;
+ }
+
+ public void setUserConnected(boolean isUserConnected) {
+ mIsUserConnected = isUserConnected;
+ }
+
+ public boolean isWordPressComUser() {
+ return mIsWordPressComUser;
+ }
+
+ public void setWordPressComUser(boolean isWordPressComUser) {
+ mIsWordPressComUser = isWordPressComUser;
+ }
+
+ public boolean isJetpackUser() {
+ return mIsJetpackUser;
+ }
+
+ public void setJetpackUser(boolean isJetpackUser) {
+ mIsJetpackUser = isJetpackUser;
+ }
+
+ public int getSessionCount() {
+ return mSessionCount;
+ }
+
+ public void setSessionCount(int sessionCount) {
+ this.mSessionCount = sessionCount;
+ }
+
+ public int getNumBlogs() {
+ return mNumBlogs;
+ }
+
+ public void setNumBlogs(int numBlogs) {
+ this.mNumBlogs = numBlogs;
+ }
+
+ public String getUsername() {
+ return mUsername;
+ }
+
+ public void setUsername(String username) {
+ this.mUsername = username;
+ }
+
+ public String getEmail() {
+ return mEmail;
+ }
+
+ public void setEmail(String email) {
+ this.mEmail = email;
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
new file mode 100644
index 000000000..c160f47a6
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
@@ -0,0 +1,271 @@
+package org.wordpress.android.analytics;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public final class AnalyticsTracker {
+ private static boolean mHasUserOptedOut;
+
+ public static final String READER_DETAIL_TYPE_KEY = "post_detail_type";
+ public static final String READER_DETAIL_TYPE_NORMAL = "normal";
+ public static final String READER_DETAIL_TYPE_BLOG_PREVIEW = "preview-blog";
+ public static final String READER_DETAIL_TYPE_TAG_PREVIEW = "preview-tag";
+
+ public enum Stat {
+ APPLICATION_OPENED,
+ APPLICATION_CLOSED,
+ APPLICATION_INSTALLED,
+ APPLICATION_UPGRADED,
+ READER_ACCESSED,
+ READER_ARTICLE_COMMENTED_ON,
+ READER_ARTICLE_LIKED,
+ READER_ARTICLE_OPENED,
+ READER_ARTICLE_UNLIKED,
+ READER_BLOG_BLOCKED,
+ READER_BLOG_FOLLOWED,
+ READER_BLOG_PREVIEWED,
+ READER_BLOG_UNFOLLOWED,
+ READER_DISCOVER_VIEWED,
+ READER_INFINITE_SCROLL,
+ READER_LIST_FOLLOWED,
+ READER_LIST_LOADED,
+ READER_LIST_PREVIEWED,
+ READER_LIST_UNFOLLOWED,
+ READER_TAG_FOLLOWED,
+ READER_TAG_LOADED,
+ READER_TAG_PREVIEWED,
+ READER_TAG_UNFOLLOWED,
+ READER_SEARCH_LOADED,
+ READER_SEARCH_PERFORMED,
+ READER_SEARCH_RESULT_TAPPED,
+ READER_RELATED_POST_CLICKED,
+ STATS_ACCESSED,
+ STATS_INSIGHTS_ACCESSED,
+ STATS_PERIOD_DAYS_ACCESSED,
+ STATS_PERIOD_WEEKS_ACCESSED,
+ STATS_PERIOD_MONTHS_ACCESSED,
+ STATS_PERIOD_YEARS_ACCESSED,
+ STATS_VIEW_ALL_ACCESSED,
+ STATS_SINGLE_POST_ACCESSED,
+ STATS_TAPPED_BAR_CHART,
+ STATS_SCROLLED_TO_BOTTOM,
+ STATS_WIDGET_ADDED,
+ STATS_WIDGET_REMOVED,
+ STATS_WIDGET_TAPPED,
+ EDITOR_CREATED_POST,
+ EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY,
+ EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY,
+ EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY,
+ EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY,
+ EDITOR_UPDATED_POST,
+ EDITOR_SCHEDULED_POST,
+ EDITOR_CLOSED,
+ EDITOR_PUBLISHED_POST,
+ EDITOR_SAVED_DRAFT,
+ EDITOR_DISCARDED_CHANGES,
+ EDITOR_EDITED_IMAGE, // Visual editor only
+ EDITOR_ENABLED_NEW_VERSION, // Visual editor only
+ EDITOR_TOGGLED_OFF, // Visual editor only
+ EDITOR_TOGGLED_ON, // Visual editor only
+ EDITOR_UPLOAD_MEDIA_FAILED, // Visual editor only
+ EDITOR_UPLOAD_MEDIA_RETRIED, // Visual editor only
+ EDITOR_TAPPED_BLOCKQUOTE,
+ EDITOR_TAPPED_BOLD,
+ EDITOR_TAPPED_HTML, // Visual editor only
+ EDITOR_TAPPED_IMAGE,
+ EDITOR_TAPPED_ITALIC,
+ EDITOR_TAPPED_LINK,
+ EDITOR_TAPPED_MORE,
+ EDITOR_TAPPED_STRIKETHROUGH,
+ EDITOR_TAPPED_UNDERLINE,
+ EDITOR_TAPPED_ORDERED_LIST, // Visual editor only
+ EDITOR_TAPPED_UNLINK, // Visual editor only
+ EDITOR_TAPPED_UNORDERED_LIST, // Visual editor only
+ ME_ACCESSED,
+ ME_GRAVATAR_TAPPED,
+ ME_GRAVATAR_TOOLTIP_TAPPED,
+ ME_GRAVATAR_PERMISSIONS_INTERRUPTED,
+ ME_GRAVATAR_PERMISSIONS_DENIED,
+ ME_GRAVATAR_PERMISSIONS_ACCEPTED,
+ ME_GRAVATAR_SHOT_NEW,
+ ME_GRAVATAR_GALLERY_PICKED,
+ ME_GRAVATAR_CROPPED,
+ ME_GRAVATAR_UPLOADED,
+ ME_GRAVATAR_UPLOAD_UNSUCCESSFUL,
+ ME_GRAVATAR_UPLOAD_EXCEPTION,
+ MY_SITE_ACCESSED,
+ NOTIFICATIONS_ACCESSED,
+ NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS,
+ NOTIFICATION_REPLIED_TO,
+ NOTIFICATION_APPROVED,
+ NOTIFICATION_UNAPPROVED,
+ NOTIFICATION_LIKED,
+ NOTIFICATION_UNLIKED,
+ NOTIFICATION_TRASHED,
+ NOTIFICATION_FLAGGED_AS_SPAM,
+ OPENED_POSTS,
+ OPENED_PAGES,
+ OPENED_COMMENTS,
+ OPENED_VIEW_SITE,
+ OPENED_VIEW_ADMIN,
+ OPENED_MEDIA_LIBRARY,
+ OPENED_BLOG_SETTINGS,
+ OPENED_ACCOUNT_SETTINGS,
+ OPENED_APP_SETTINGS,
+ OPENED_MY_PROFILE,
+ OPENED_PEOPLE_MANAGEMENT,
+ OPENED_PERSON,
+ CREATED_ACCOUNT,
+ CREATED_SITE,
+ ACCOUNT_LOGOUT,
+ SHARED_ITEM,
+ ADDED_SELF_HOSTED_SITE,
+ SIGNED_IN,
+ SIGNED_INTO_JETPACK,
+ PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN,
+ STATS_SELECTED_INSTALL_JETPACK,
+ STATS_SELECTED_CONNECT_JETPACK,
+ PUSH_NOTIFICATION_RECEIVED,
+ PUSH_NOTIFICATION_TAPPED, // Same of opened
+ SUPPORT_OPENED_HELPSHIFT_SCREEN,
+ SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE,
+ LOGIN_MAGIC_LINK_EXITED,
+ LOGIN_MAGIC_LINK_FAILED,
+ LOGIN_MAGIC_LINK_OPENED,
+ LOGIN_MAGIC_LINK_REQUESTED,
+ LOGIN_MAGIC_LINK_SUCCEEDED,
+ LOGIN_FAILED,
+ LOGIN_FAILED_TO_GUESS_XMLRPC,
+ LOGIN_INSERTED_INVALID_URL,
+ LOGIN_AUTOFILL_CREDENTIALS_FILLED,
+ LOGIN_AUTOFILL_CREDENTIALS_UPDATED,
+ PERSON_REMOVED,
+ PERSON_UPDATED,
+ PUSH_AUTHENTICATION_APPROVED,
+ PUSH_AUTHENTICATION_EXPIRED,
+ PUSH_AUTHENTICATION_FAILED,
+ PUSH_AUTHENTICATION_IGNORED,
+ NOTIFICATION_SETTINGS_LIST_OPENED,
+ NOTIFICATION_SETTINGS_STREAMS_OPENED,
+ NOTIFICATION_SETTINGS_DETAILS_OPENED,
+ THEMES_ACCESSED_THEMES_BROWSER,
+ THEMES_ACCESSED_SEARCH,
+ THEMES_CHANGED_THEME,
+ THEMES_PREVIEWED_SITE,
+ THEMES_DEMO_ACCESSED,
+ THEMES_CUSTOMIZE_ACCESSED,
+ THEMES_SUPPORT_ACCESSED,
+ THEMES_DETAILS_ACCESSED,
+ ACCOUNT_SETTINGS_LANGUAGE_CHANGED,
+ SITE_SETTINGS_ACCESSED,
+ SITE_SETTINGS_ACCESSED_MORE_SETTINGS,
+ SITE_SETTINGS_LEARN_MORE_CLICKED,
+ SITE_SETTINGS_LEARN_MORE_LOADED,
+ SITE_SETTINGS_ADDED_LIST_ITEM,
+ SITE_SETTINGS_DELETED_LIST_ITEMS,
+ SITE_SETTINGS_SAVED_REMOTELY,
+ SITE_SETTINGS_HINT_TOAST_SHOWN,
+ SITE_SETTINGS_START_OVER_ACCESSED,
+ SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED,
+ SITE_SETTINGS_EXPORT_SITE_ACCESSED,
+ SITE_SETTINGS_EXPORT_SITE_REQUESTED,
+ SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK,
+ SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR,
+ SITE_SETTINGS_DELETE_SITE_ACCESSED,
+ SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED,
+ SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN,
+ SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED,
+ SITE_SETTINGS_DELETE_SITE_REQUESTED,
+ SITE_SETTINGS_DELETE_SITE_RESPONSE_OK,
+ SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR,
+ ABTEST_START,
+ TRAIN_TRACKS_RENDER,
+ TRAIN_TRACKS_INTERACT
+ }
+
+ private static final List<Tracker> TRACKERS = new ArrayList<>();
+
+ private AnalyticsTracker() {
+ }
+
+ public static void init(Context context) {
+ loadPrefHasUserOptedOut(context);
+ }
+
+ public static void loadPrefHasUserOptedOut(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean hasUserOptedOut = !prefs.getBoolean("wp_pref_send_usage_stats", true);
+ if (hasUserOptedOut != mHasUserOptedOut) {
+ mHasUserOptedOut = hasUserOptedOut;
+ }
+ }
+
+ public static void registerTracker(Tracker tracker) {
+ if (tracker != null) {
+ TRACKERS.add(tracker);
+ }
+ }
+
+ public static void track(Stat stat) {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.track(stat);
+ }
+ }
+
+ public static void track(Stat stat, Map<String, ?> properties) {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.track(stat, properties);
+ }
+ }
+
+
+ public static void flush() {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.flush();
+ }
+ }
+
+ public static void endSession(boolean force) {
+ if (mHasUserOptedOut && !force) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.endSession();
+ }
+ }
+
+ public static void registerPushNotificationToken(String regId) {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.registerPushNotificationToken(regId);
+ }
+ }
+
+ public static void clearAllData() {
+ for (Tracker tracker : TRACKERS) {
+ tracker.clearAllData();
+ }
+ }
+
+ public static void refreshMetadata(AnalyticsMetadata metadata) {
+ for (Tracker tracker : TRACKERS) {
+ tracker.refreshMetadata(metadata);
+ }
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java
new file mode 100644
index 000000000..1e1b278f8
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java
@@ -0,0 +1,1172 @@
+package org.wordpress.android.analytics;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.mixpanel.android.mpmetrics.MixpanelAPI;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+public class AnalyticsTrackerMixpanel extends Tracker {
+ public static final String SESSION_COUNT = "sessionCount";
+
+ private MixpanelAPI mMixpanel;
+ private EnumMap<AnalyticsTracker.Stat, JSONObject> mAggregatedProperties;
+ private static final String MIXPANEL_PLATFORM = "platform";
+ private static final String MIXPANEL_SESSION_COUNT = "session_count";
+ private static final String DOTCOM_USER = "dotcom_user";
+ private static final String JETPACK_USER = "jetpack_user";
+ private static final String MIXPANEL_NUMBER_OF_BLOGS = "number_of_blogs";
+ private static final String APP_LOCALE = "app_locale";
+ private static final String MIXPANEL_ANON_ID = "mixpanel_user_anon_id";
+
+ public AnalyticsTrackerMixpanel(Context context, String token) throws IllegalArgumentException {
+ super(context);
+ mAggregatedProperties = new EnumMap<>(AnalyticsTracker.Stat.class);
+ mMixpanel = MixpanelAPI.getInstance(context, token);
+ }
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ public static void showNotification(Context context, PendingIntent intent, int notificationIcon, CharSequence title,
+ CharSequence message) {
+ final NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ final Notification.Builder builder = new Notification.Builder(context).setSmallIcon(notificationIcon)
+ .setTicker(message).setWhen(System.currentTimeMillis()).setContentTitle(title).setContentText(message)
+ .setContentIntent(intent);
+ Notification notification;
+ notification = builder.build();
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+ nm.notify(0, notification);
+ }
+
+ String getAnonIdPrefKey() {
+ return MIXPANEL_ANON_ID;
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat) {
+ track(stat, null);
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat, Map<String, ?> properties) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = instructionsForStat(stat);
+
+ if (instructions == null) {
+ return;
+ }
+
+ trackMixpanelDataForInstructions(instructions, properties);
+ }
+
+ private void trackMixpanelDataForInstructions(AnalyticsTrackerMixpanelInstructionsForStat instructions,
+ Map<String, ?> properties) {
+ if (instructions.getDisableForSelfHosted()) {
+ return;
+ }
+
+ // Just a security check we're tracking the correct user
+ if (getWordPressComUserName() == null && getAnonID() == null) {
+ this.clearAllData();
+ generateNewAnonID();
+ mMixpanel.identify(getAnonID());
+ }
+
+ trackMixpanelEventForInstructions(instructions, properties);
+ trackMixpanelPropertiesForInstructions(instructions);
+ }
+
+ private void trackMixpanelPropertiesForInstructions(AnalyticsTrackerMixpanelInstructionsForStat instructions) {
+ if (instructions.getPeoplePropertyToIncrement() != null && !instructions.getPeoplePropertyToIncrement()
+ .isEmpty()) {
+ incrementPeopleProperty(instructions.getPeoplePropertyToIncrement());
+ }
+
+ if (instructions.getSuperPropertyToIncrement() != null && !instructions.getSuperPropertyToIncrement()
+ .isEmpty()) {
+ incrementSuperProperty(instructions.getSuperPropertyToIncrement());
+ }
+
+ if (instructions.getPropertyToIncrement() != null && !instructions.getPropertyToIncrement().isEmpty()) {
+ incrementProperty(instructions.getPropertyToIncrement(), instructions.getStatToAttachProperty());
+ }
+
+ if (instructions.getSuperPropertiesToFlag() != null && instructions.getSuperPropertiesToFlag().size() > 0) {
+ for (String superPropertyToFlag : instructions.getSuperPropertiesToFlag()) {
+ flagSuperProperty(superPropertyToFlag);
+ }
+ }
+
+ if (instructions.getPeoplePropertiesToAssign() != null
+ && instructions.getPeoplePropertiesToAssign().size() > 0) {
+ for (Map.Entry<String, Object> entry: instructions.getPeoplePropertiesToAssign().entrySet()) {
+ setValueForPeopleProperty(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ private void setValueForPeopleProperty(String peopleProperty, Object value) {
+ try {
+ mMixpanel.getPeople().set(peopleProperty, value);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ private void trackMixpanelEventForInstructions(AnalyticsTrackerMixpanelInstructionsForStat instructions,
+ Map<String, ?> properties) {
+ String eventName = instructions.getMixpanelEventName();
+ if (eventName != null && !eventName.isEmpty()) {
+ JSONObject savedPropertiesForStat = propertiesForStat(instructions.getStat());
+ if (savedPropertiesForStat == null) {
+ savedPropertiesForStat = new JSONObject();
+ }
+
+ // Retrieve properties user has already passed in and combine them with the saved properties
+ if (properties != null) {
+ for (Object o : properties.entrySet()) {
+ Map.Entry pairs = (Map.Entry) o;
+ String key = (String) pairs.getKey();
+ try {
+ Object value = pairs.getValue();
+ savedPropertiesForStat.put(key, value);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+ }
+ mMixpanel.track(eventName, savedPropertiesForStat);
+ removePropertiesForStat(instructions.getStat());
+ }
+ }
+
+ @Override
+ public void registerPushNotificationToken(String regId) {
+ try {
+ mMixpanel.getPeople().setPushRegistrationId(regId);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ @Override
+ public void endSession() {
+ mAggregatedProperties.clear();
+ mMixpanel.flush();
+ }
+
+ @Override
+ public void flush() {
+ mMixpanel.flush();
+ }
+
+ @Override
+ public void refreshMetadata(AnalyticsMetadata metadata) {
+ // Register super properties
+ try {
+ JSONObject properties = new JSONObject();
+ properties.put(MIXPANEL_PLATFORM, "Android");
+ properties.put(MIXPANEL_SESSION_COUNT, metadata.getSessionCount());
+ properties.put(DOTCOM_USER, metadata.isUserConnected());
+ properties.put(JETPACK_USER, metadata.isJetpackUser());
+ properties.put(MIXPANEL_NUMBER_OF_BLOGS, metadata.getNumBlogs());
+ properties.put(APP_LOCALE, mContext.getResources().getConfiguration().locale.toString());
+ mMixpanel.registerSuperProperties(properties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+
+ if (metadata.isUserConnected() && metadata.isWordPressComUser()) {
+ setWordPressComUserName(metadata.getUsername());
+ // Re-unify the user
+ if (getAnonID() != null) {
+ mMixpanel.alias(getWordPressComUserName(), getAnonID());
+ clearAnonID();
+ } else {
+ mMixpanel.identify(metadata.getUsername());
+ }
+ } else {
+ // Not wpcom connected. Check if anonID is already present
+ setWordPressComUserName(null);
+ if (getAnonID() == null) {
+ generateNewAnonID();
+ }
+ mMixpanel.identify(getAnonID());
+ }
+
+ // Application opened and start.
+ if (metadata.isUserConnected()) {
+ try {
+ String userID = getWordPressComUserName() != null ? getWordPressComUserName() : getAnonID();
+ if (userID == null) {
+ // This should not be an option here
+ return;
+ }
+
+ mMixpanel.getPeople().identify(userID);
+ JSONObject jsonObj = new JSONObject();
+ jsonObj.put("$username", userID);
+ if (metadata.getEmail() != null) {
+ jsonObj.put("$email", metadata.getEmail());
+ }
+ jsonObj.put("$first_name", userID);
+ mMixpanel.getPeople().set(jsonObj);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+ }
+
+ @Override
+ public void clearAllData() {
+ super.clearAllData();
+ mMixpanel.clearSuperProperties();
+ try {
+ mMixpanel.getPeople().clearPushRegistrationId();
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ private AnalyticsTrackerMixpanelInstructionsForStat instructionsForStat(
+ AnalyticsTracker.Stat stat) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions;
+ switch (stat) {
+ case APPLICATION_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Opened");
+ instructions.setSuperPropertyToIncrement("Application Opened");
+ incrementSessionCount();
+ break;
+ case APPLICATION_CLOSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Closed");
+ break;
+ case APPLICATION_INSTALLED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Installed");
+ break;
+ case APPLICATION_UPGRADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Upgraded");
+ break;
+ case READER_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_reader");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_reader");
+ break;
+ case READER_ARTICLE_COMMENTED_ON:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Commented on Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_commented_on_reader_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_commented_on_article");
+ break;
+ case READER_ARTICLE_LIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Liked Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_liked_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_liked_reader_article");
+ break;
+ case READER_ARTICLE_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Opened Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_opened_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_opened_reader_article");
+ break;
+ case READER_ARTICLE_UNLIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unliked Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unliked_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_unliked_reader_article");
+ break;
+ case READER_BLOG_BLOCKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Blocked Blog");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_blocked_a_blog");
+ instructions.setCurrentDateForPeopleProperty("last_time_blocked_a_blog");
+ break;
+ case READER_BLOG_FOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Followed Site");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_followed_site");
+ instructions.setCurrentDateForPeopleProperty("last_time_followed_site");
+ break;
+ case READER_BLOG_PREVIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Blog Preview");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_viewed_blog_preview");
+ instructions.setCurrentDateForPeopleProperty("last_time_viewed_blog_preview");
+ break;
+ case READER_BLOG_UNFOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unfollowed Site");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unfollowed_site");
+ instructions.setCurrentDateForPeopleProperty("last_time_unfollowed_site");
+ break;
+ case READER_DISCOVER_VIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Discover Content Viewed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_discover_content_viewed");
+ instructions.setCurrentDateForPeopleProperty("last_time_discover_content_viewed");
+ break;
+ case READER_INFINITE_SCROLL:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Infinite Scroll");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_reader_performed_infinite_scroll");
+ instructions.setCurrentDateForPeopleProperty("last_time_performed_reader_infinite_scroll");
+ break;
+ case READER_LIST_FOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Followed List");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_followed_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_followed_list");
+ break;
+ case READER_LIST_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Loaded List");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_loaded_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_loaded_list");
+ break;
+ case READER_LIST_PREVIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - List Preview");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_viewed_list_preview");
+ instructions.setCurrentDateForPeopleProperty("last_time_viewed_list_preview");
+ break;
+ case READER_LIST_UNFOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unfollowed List");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unfollowed_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_unfollowed_list");
+ break;
+ case READER_TAG_FOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Followed Reader Tag");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_followed_reader_tag");
+ instructions.setCurrentDateForPeopleProperty("last_time_followed_reader_tag");
+ break;
+ case READER_TAG_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Loaded Tag");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_loaded_tag");
+ instructions.setCurrentDateForPeopleProperty("last_time_loaded_tag");
+ break;
+ case READER_TAG_PREVIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Tag Preview");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_viewed_tag_preview");
+ instructions.setCurrentDateForPeopleProperty("last_time_viewed_tag_preview");
+ break;
+ case READER_TAG_UNFOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unfollowed Reader Tag");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unfollowed_reader_tag");
+ instructions.setCurrentDateForPeopleProperty("last_time_unfollowed_reader_tag");
+ break;
+ case READER_SEARCH_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Loaded Search");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_search_loaded");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_search_loaded");
+ break;
+ case READER_SEARCH_PERFORMED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Performed Search");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_search_performed");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_search_performed");
+ break;
+ case READER_SEARCH_RESULT_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Tapped Search Result");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_search_result_tapped");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_search_result_tapped");
+ break;
+ case READER_RELATED_POST_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Related Post Clicked");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_related_post_clicked");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_related_post_clicked");
+ break;
+ case EDITOR_CREATED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Created Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_created_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_created_post_in_editor");
+ break;
+ case EDITOR_SAVED_DRAFT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Saved Draft");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_saved_draft");
+ instructions.setCurrentDateForPeopleProperty("last_time_saved_draft");
+ break;
+ case EDITOR_DISCARDED_CHANGES:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Discarded Changes");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_discarded_changes");
+ instructions.setCurrentDateForPeopleProperty("last_time_discarded_changes");
+ break;
+ case EDITOR_EDITED_IMAGE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Edited Image");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_edited_image");
+ instructions.setCurrentDateForPeopleProperty("last_time_edited_image");
+ break;
+ case EDITOR_ENABLED_NEW_VERSION:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Enabled New Version");
+ instructions.addSuperPropertyToFlag("enabled_new_editor");
+ break;
+ case EDITOR_TOGGLED_ON:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Toggled New Editor On");
+ instructions.setPeoplePropertyToValue("enabled_new_editor", true);
+ break;
+ case EDITOR_TOGGLED_OFF:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Toggled New Editor Off");
+ instructions.setPeoplePropertyToValue("enabled_new_editor", false);
+ break;
+ case EDITOR_UPLOAD_MEDIA_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Upload Media Failed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_upload_media_failed");
+ instructions.setCurrentDateForPeopleProperty("last_time_editor_upload_media_failed");
+ break;
+ case EDITOR_UPLOAD_MEDIA_RETRIED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Retried Uploading Media");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_retried_uploading_media");
+ instructions.setCurrentDateForPeopleProperty("last_time_editor_retried_uploading_media");
+ break;
+ case EDITOR_CLOSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Closed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_closed");
+ break;
+ case EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Photo via Local Library");
+ instructions.
+ setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_added_photo_via_local_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_photo_via_local_library_to_post");
+ break;
+ case EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Photo via WP Media Library");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_added_photo_via_wp_media_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_photo_via_wp_media_library_to_post");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Video via Local Library");
+ instructions.
+ setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_added_video_via_local_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_video_via_local_library_to_post");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Video via WP Media Library");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_added_video_via_wp_media_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_video_via_wp_media_library_to_post");
+ break;
+ case EDITOR_PUBLISHED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Published Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_published_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_published_post");
+ break;
+ case EDITOR_UPDATED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Updated Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_updated_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_updated_post");
+ break;
+ case EDITOR_SCHEDULED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Scheduled Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_scheduled_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_scheduled_post");
+ break;
+ case EDITOR_TAPPED_BLOCKQUOTE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Blockquote Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_blockquote");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_blockquote_in_editor");
+ break;
+ case EDITOR_TAPPED_BOLD:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Bold Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_bold");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_bold_in_editor");
+ break;
+ case EDITOR_TAPPED_IMAGE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Image Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_image");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_image_in_editor");
+ break;
+ case EDITOR_TAPPED_ITALIC:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Italics Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_italic");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_italic_in_editor");
+ break;
+ case EDITOR_TAPPED_LINK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Link Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_link");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_link_in_editor");
+ break;
+ case EDITOR_TAPPED_MORE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped More Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_more");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_more_in_editor");
+ break;
+ case EDITOR_TAPPED_STRIKETHROUGH:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Strikethrough Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_strikethrough");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_strikethrough_in_editor");
+ break;
+ case EDITOR_TAPPED_UNDERLINE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Underline Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_underline");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_underline_in_editor");
+ break;
+ case EDITOR_TAPPED_HTML:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped HTML Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_html");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_html_in_editor");
+ break;
+ case EDITOR_TAPPED_ORDERED_LIST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Ordered List Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_ordered_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_ordered_list_in_editor");
+ break;
+ case EDITOR_TAPPED_UNLINK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Unlink Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_unlink");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_unlink_in_editor");
+ break;
+ case EDITOR_TAPPED_UNORDERED_LIST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Unordered List Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_unordered_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_unordered_list_in_editor");
+ break;
+ case NOTIFICATIONS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_notifications");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_notifications");
+ break;
+ case NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Opened Notification Details");
+ instructions.
+ setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_opened_notification_details");
+ instructions.setCurrentDateForPeopleProperty("last_time_opened_notification_details");
+ break;
+ case NOTIFICATION_APPROVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_approved");
+ break;
+ case NOTIFICATION_UNAPPROVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_unapproved");
+ break;
+ case NOTIFICATION_REPLIED_TO:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_replied_to");
+ break;
+ case NOTIFICATION_TRASHED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_trashed");
+ break;
+ case NOTIFICATION_FLAGGED_AS_SPAM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_flagged_as_spam");
+ break;
+ case NOTIFICATION_LIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Liked Comment");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_comment_likes_from_notification");
+ break;
+ case NOTIFICATION_UNLIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Unliked Comment");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_comment_unlikes_from_notification");
+ break;
+ case OPENED_POSTS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Posts");
+ break;
+ case OPENED_PAGES:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Pages");
+ break;
+ case OPENED_COMMENTS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Comments");
+ break;
+ case OPENED_VIEW_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened View Site");
+ break;
+ case OPENED_VIEW_ADMIN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened View Admin");
+ break;
+ case OPENED_MEDIA_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Media Library");
+ break;
+ case OPENED_BLOG_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Site Settings");
+ break;
+ case OPENED_ACCOUNT_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Opened Account Settings");
+ break;
+ case OPENED_APP_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Opened App Settings");
+ break;
+ case OPENED_MY_PROFILE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Opened My Profile");
+ break;
+ case OPENED_PEOPLE_MANAGEMENT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Accessed List");
+ break;
+ case OPENED_PERSON:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Accessed Details");
+ break;
+ case CREATED_ACCOUNT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Created Account");
+ instructions.setCurrentDateForPeopleProperty("$created");
+ instructions.addSuperPropertyToFlag("created_account_on_mobile");
+ break;
+ case CREATED_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Created Site");
+ break;
+ case SHARED_ITEM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor("number_of_items_shared");
+ break;
+ case ADDED_SELF_HOSTED_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Added Self Hosted Site");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_self_hosted_site");
+ break;
+ case SIGNED_IN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Signed In");
+ break;
+ case SIGNED_INTO_JETPACK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Signed into Jetpack");
+ instructions.addSuperPropertyToFlag("jetpack_user");
+ instructions.addSuperPropertyToFlag("dotcom_user");
+ break;
+ case ACCOUNT_LOGOUT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Logged Out");
+ break;
+ case PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Signed into Jetpack from Stats Screen");
+ break;
+ case STATS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_stats");
+ break;
+ case STATS_INSIGHTS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Insights Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_insights_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_insights_screen_stats");
+ break;
+ case STATS_PERIOD_DAYS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Days Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_days_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_days_screen_stats");
+ break;
+ case STATS_PERIOD_WEEKS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Weeks Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_weeks_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_weeks_screen_stats");
+ break;
+ case STATS_PERIOD_MONTHS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Months Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_months_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_months_screen_stats");
+ break;
+ case STATS_PERIOD_YEARS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Years Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_years_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_years_screen_stats");
+ break;
+ case STATS_VIEW_ALL_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - View All Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_view_all_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_view_all_screen_stats");
+ break;
+ case STATS_SINGLE_POST_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Single Post Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_single_post_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_single_post_screen_stats");
+ break;
+ case STATS_TAPPED_BAR_CHART:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Tapped Bar Chart");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_tapped_stats_bar_chart");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_stats_bar_chart");
+ break;
+ case STATS_SCROLLED_TO_BOTTOM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Scrolled to Bottom");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_scrolled_to_bottom_of_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_scrolled_to_bottom_of_stats");
+ break;
+ case STATS_SELECTED_INSTALL_JETPACK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Selected Install Jetpack");
+ break;
+ case STATS_SELECTED_CONNECT_JETPACK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Selected Connect Jetpack");
+ break;
+ case STATS_WIDGET_ADDED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Widget Added");
+ break;
+ case STATS_WIDGET_REMOVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Widget Removed");
+ break;
+ case STATS_WIDGET_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Widget Tapped");
+ break;
+ case PUSH_NOTIFICATION_RECEIVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Notification - Received");
+ break;
+ case PUSH_NOTIFICATION_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Notification - Alert Tapped");
+ break;
+ case SUPPORT_OPENED_HELPSHIFT_SCREEN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Support - Opened Helpshift Screen");
+ instructions.addSuperPropertyToFlag("opened_helpshift_screen");
+ break;
+ case SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Support - Replied to Helpshift");
+ instructions.addSuperPropertyToFlag("support_replied_to_helpshift");
+ break;
+ case LOGIN_MAGIC_LINK_EXITED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link exited");
+ break;
+ case LOGIN_MAGIC_LINK_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link failed");
+ break;
+ case LOGIN_MAGIC_LINK_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link opened");
+ break;
+ case LOGIN_MAGIC_LINK_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link requested");
+ break;
+ case LOGIN_MAGIC_LINK_SUCCEEDED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link succeeded");
+ break;
+ case LOGIN_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Failed Login");
+ break;
+ case LOGIN_FAILED_TO_GUESS_XMLRPC:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Failed To Guess XMLRPC");
+ break;
+ case LOGIN_INSERTED_INVALID_URL:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Inserted Invalid URL");
+ break;
+ case LOGIN_AUTOFILL_CREDENTIALS_FILLED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Auto Fill Credentials Filled");
+ break;
+ case LOGIN_AUTOFILL_CREDENTIALS_UPDATED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Auto Fill Credentials Updated");
+ break;
+ case PERSON_REMOVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Removed Person");
+ break;
+ case PERSON_UPDATED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Updated Person");
+ break;
+ case PUSH_AUTHENTICATION_APPROVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Approved");
+ break;
+ case PUSH_AUTHENTICATION_EXPIRED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Expired");
+ break;
+ case PUSH_AUTHENTICATION_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Failed");
+ break;
+ case PUSH_AUTHENTICATION_IGNORED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Ignored");
+ break;
+ case NOTIFICATION_SETTINGS_LIST_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notification Settings - Accessed List");
+ break;
+ case NOTIFICATION_SETTINGS_STREAMS_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notification Settings - Accessed Stream");
+ break;
+ case NOTIFICATION_SETTINGS_DETAILS_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notification Settings - Accessed Details");
+ break;
+ case ME_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me Tab - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_me_tab");
+ break;
+ case ME_GRAVATAR_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Tapped Gravatar");
+ break;
+ case ME_GRAVATAR_TOOLTIP_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Tapped Gravatar Tooltip");
+ break;
+ case ME_GRAVATAR_PERMISSIONS_INTERRUPTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Permissions Interrupted");
+ break;
+ case ME_GRAVATAR_PERMISSIONS_DENIED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Permissions Denied");
+ break;
+ case ME_GRAVATAR_PERMISSIONS_ACCEPTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Permissions Accepted");
+ break;
+ case ME_GRAVATAR_SHOT_NEW:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Shot New Photo");
+ break;
+ case ME_GRAVATAR_GALLERY_PICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Picked From Gallery");
+ break;
+ case ME_GRAVATAR_CROPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Cropped");
+ break;
+ case ME_GRAVATAR_UPLOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Uploaded");
+ break;
+ case ME_GRAVATAR_UPLOAD_UNSUCCESSFUL:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Upload Unsuccessful");
+ break;
+ case ME_GRAVATAR_UPLOAD_EXCEPTION:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Upload Exception");
+ break;
+ case MY_SITE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("My Site - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_my_site");
+ break;
+ case THEMES_ACCESSED_THEMES_BROWSER:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Accessed Theme Browser");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_theme_browser");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_theme_browser");
+ break;
+ case THEMES_ACCESSED_SEARCH:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Accessed Theme");
+ break;
+ case THEMES_CHANGED_THEME:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Changed Theme");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_changed_theme");
+ instructions.setCurrentDateForPeopleProperty("last_time_changed_theme");
+ break;
+ case THEMES_PREVIEWED_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Previewed Theme for Site");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_previewed_a_theme");
+ instructions.setCurrentDateForPeopleProperty("last_time_previewed_a_theme");
+ break;
+ case THEMES_DEMO_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Demo Accessed");
+ break;
+ case THEMES_CUSTOMIZE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Customize Accessed");
+ break;
+ case THEMES_SUPPORT_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Support Accessed");
+ break;
+ case THEMES_DETAILS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Details Accessed");
+ break;
+ case ACCOUNT_SETTINGS_LANGUAGE_CHANGED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Account Settings - Changed Language");
+ break;
+ case SITE_SETTINGS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Site Settings Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_settings_accessed");
+ break;
+ case SITE_SETTINGS_ACCESSED_MORE_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - More Settings Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_more_settings_accessed");
+ break;
+ case SITE_SETTINGS_ADDED_LIST_ITEM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Added List Item");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_list_items_added");
+ break;
+ case SITE_SETTINGS_DELETED_LIST_ITEMS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Site Deleted List Items");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_list_items_were_deleted");
+ break;
+ case SITE_SETTINGS_HINT_TOAST_SHOWN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Preference Hint Shown");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_preference_hints_viewed");
+ break;
+ case SITE_SETTINGS_LEARN_MORE_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Learn More Clicked");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_learn_more_clicked");
+ break;
+ case SITE_SETTINGS_LEARN_MORE_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Learn More Loaded");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_learn_more_seen");
+ break;
+ case SITE_SETTINGS_SAVED_REMOTELY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Saved Remotely");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_settings_updated_remotely");
+ break;
+ case SITE_SETTINGS_START_OVER_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Start Over Accessed");
+ break;
+ case SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Start Over Contact Support Clicked");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Accessed");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Requested");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Response OK");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Response Error");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Accessed");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Purchases Requested");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Purchases Shown");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Show Purchases Clicked");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Requested");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_OK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Response OK");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Response Error");
+ break;
+ case ABTEST_START:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("AB Test - Started");
+ break;
+ case TRAIN_TRACKS_RENDER: case TRAIN_TRACKS_INTERACT:
+ // Do nothing. These events are just for Tracks.
+ instructions = null;
+ break;
+ default:
+ instructions = null;
+ break;
+ }
+ return instructions;
+ }
+
+ private void incrementPeopleProperty(String property) {
+ try {
+ mMixpanel.getPeople().increment(property, 1);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ @SuppressLint("CommitPrefEdits")
+ private void incrementSuperProperty(String property) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ int propertyCount = preferences.getInt(property, 0);
+ propertyCount++;
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putInt(property, propertyCount);
+ editor.commit();
+
+ try {
+ JSONObject superProperties = mMixpanel.getSuperProperties();
+ superProperties.put(property, propertyCount);
+ mMixpanel.registerSuperProperties(superProperties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+
+ private void flagSuperProperty(String property) {
+ try {
+ JSONObject superProperties = mMixpanel.getSuperProperties();
+ superProperties.put(property, true);
+ mMixpanel.registerSuperProperties(superProperties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+
+ private void savePropertyValueForStat(String property, Object value, AnalyticsTracker.Stat stat) {
+ JSONObject properties = mAggregatedProperties.get(stat);
+ if (properties == null) {
+ properties = new JSONObject();
+ mAggregatedProperties.put(stat, properties);
+ }
+
+ try {
+ properties.put(property, value);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+
+ private JSONObject propertiesForStat(AnalyticsTracker.Stat stat) {
+ return mAggregatedProperties.get(stat);
+ }
+
+ private void removePropertiesForStat(AnalyticsTracker.Stat stat) {
+ mAggregatedProperties.remove(stat);
+ }
+
+ private Object propertyForStat(String property, AnalyticsTracker.Stat stat) {
+ JSONObject properties = mAggregatedProperties.get(stat);
+ if (properties == null) {
+ return null;
+ }
+
+ try {
+ return properties.get(property);
+ } catch (JSONException e) {
+ // We are okay with swallowing this exception as the next line will just return a null value
+ }
+
+ return null;
+ }
+
+ private void incrementProperty(String property, AnalyticsTracker.Stat stat) {
+ Object currentValueObj = propertyForStat(property, stat);
+ int currentValue = 1;
+ if (currentValueObj != null) {
+ currentValue = Integer.valueOf(currentValueObj.toString());
+ currentValue++;
+ }
+
+ savePropertyValueForStat(property, Integer.toString(currentValue), stat);
+ }
+
+
+ public void incrementSessionCount() {
+ // Tracking session count will help us isolate users who just installed the app
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ int sessionCount = preferences.getInt(SESSION_COUNT, 0);
+ sessionCount++;
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putInt(SESSION_COUNT, sessionCount);
+ editor.apply();
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java
new file mode 100644
index 000000000..38aefa988
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java
@@ -0,0 +1,140 @@
+package org.wordpress.android.analytics;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class AnalyticsTrackerMixpanelInstructionsForStat {
+ private String mMixpanelEventName;
+ private String mSuperPropertyToIncrement;
+ private String mPeoplePropertyToIncrement;
+ private ArrayList<String> mSuperPropertiesToFlag;
+ private AnalyticsTracker.Stat mStatToAttachProperty;
+ private AnalyticsTracker.Stat mStat;
+ private String mPropertyToIncrement;
+ private boolean mDisableForSelfHosted;
+ private Map<String, Object> mPeoplePropertiesToAssign;
+
+ public AnalyticsTrackerMixpanelInstructionsForStat() {
+ mSuperPropertiesToFlag = new ArrayList<String>();
+ mPeoplePropertiesToAssign = new HashMap<String, Object>();
+ }
+
+ public static AnalyticsTrackerMixpanelInstructionsForStat mixpanelInstructionsForEventName(String eventName) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = new AnalyticsTrackerMixpanelInstructionsForStat();
+ instructions.setMixpanelEventName(eventName);
+ return instructions;
+ }
+
+ public static AnalyticsTrackerMixpanelInstructionsForStat
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(String property) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = new AnalyticsTrackerMixpanelInstructionsForStat();
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(property);
+ return instructions;
+ }
+
+ public static AnalyticsTrackerMixpanelInstructionsForStat mixpanelInstructionsWithPropertyIncrementor(
+ String property, AnalyticsTracker.Stat stat) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = new AnalyticsTrackerMixpanelInstructionsForStat();
+ instructions.setStatToAttachProperty(stat);
+ instructions.setPropertyToIncrement(property);
+ return instructions;
+ }
+
+ public String getMixpanelEventName() {
+ return mMixpanelEventName;
+ }
+
+ public void setMixpanelEventName(String mixpanelEventName) {
+ this.mMixpanelEventName = mixpanelEventName;
+ }
+
+ public String getSuperPropertyToIncrement() {
+ return mSuperPropertyToIncrement;
+ }
+
+ public void setSuperPropertyToIncrement(String superPropertyToIncrement) {
+ this.mSuperPropertyToIncrement = superPropertyToIncrement;
+ }
+
+ public String getPeoplePropertyToIncrement() {
+ return mPeoplePropertyToIncrement;
+ }
+
+ public void setPeoplePropertyToIncrement(String peoplePropertyToIncrement) {
+ this.mPeoplePropertyToIncrement = peoplePropertyToIncrement;
+ }
+
+ public void setSuperPropertyAndPeoplePropertyToIncrement(String property) {
+ setSuperPropertyToIncrement(property);
+ setPeoplePropertyToIncrement(property);
+ }
+
+ public AnalyticsTracker.Stat getStatToAttachProperty() {
+ return mStatToAttachProperty;
+ }
+
+ public void setStatToAttachProperty(AnalyticsTracker.Stat statToAttachProperty) {
+ this.mStatToAttachProperty = statToAttachProperty;
+ }
+
+ public String getPropertyToIncrement() {
+ return mPropertyToIncrement;
+ }
+
+ public void setPropertyToIncrement(String propertyToIncrement) {
+ this.mPropertyToIncrement = propertyToIncrement;
+ }
+
+ public boolean getDisableForSelfHosted() {
+ return mDisableForSelfHosted;
+ }
+
+ public void setDisableForSelfHosted(boolean disableForSelfHosted) {
+ this.mDisableForSelfHosted = disableForSelfHosted;
+ }
+
+ public AnalyticsTracker.Stat getStat() {
+ return mStat;
+ }
+
+ public void setStat(AnalyticsTracker.Stat stat) {
+ this.mStat = stat;
+ }
+
+ public ArrayList<String> getSuperPropertiesToFlag() {
+ return mSuperPropertiesToFlag;
+ }
+
+ public void addSuperPropertyToFlag(String superPropertyToFlag) {
+ if (!mSuperPropertiesToFlag.contains(superPropertyToFlag)) {
+ mSuperPropertiesToFlag.add(superPropertyToFlag);
+ }
+ }
+
+ private static final ThreadLocal<DateFormat> AnalyticsDateFormat = new ThreadLocal<DateFormat>() {
+ @Override
+ protected DateFormat initialValue() {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ format.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return format;
+ }
+ };
+
+ public void setCurrentDateForPeopleProperty(String property) {
+ setPeoplePropertyToValue(property, AnalyticsDateFormat.get().format(new Date()));
+ }
+
+ public void setPeoplePropertyToValue(String property, Object value) {
+ mPeoplePropertiesToAssign.put(property, value);
+ }
+
+ public Map<String, Object> getPeoplePropertiesToAssign() {
+ return mPeoplePropertiesToAssign;
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
new file mode 100644
index 000000000..76c181b9b
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
@@ -0,0 +1,589 @@
+package org.wordpress.android.analytics;
+
+import android.content.Context;
+
+import com.automattic.android.tracks.TracksClient;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnalyticsTrackerNosara extends Tracker {
+
+ private static final String JETPACK_USER = "jetpack_user";
+ private static final String NUMBER_OF_BLOGS = "number_of_blogs";
+ private static final String TRACKS_ANON_ID = "nosara_tracks_anon_id";
+
+ private static final String EVENTS_PREFIX = "wpandroid_";
+
+ private TracksClient mNosaraClient;
+
+ public AnalyticsTrackerNosara(Context context) throws IllegalArgumentException {
+ super(context);
+ mNosaraClient = TracksClient.getClient(context);
+ }
+
+ String getAnonIdPrefKey() {
+ return TRACKS_ANON_ID;
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat) {
+ track(stat, null);
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat, Map<String, ?> properties) {
+ if (mNosaraClient == null) {
+ return;
+ }
+
+ String eventName = getEventNameForStat(stat);
+ if (eventName == null) {
+ AppLog.w(AppLog.T.STATS, "There is NO match for the event " + stat.name() + "stat");
+ return;
+ }
+
+ Map<String, Object> predefinedEventProperties = new HashMap<String, Object>();
+ switch (stat) {
+ case EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY:
+ predefinedEventProperties.put("via", "local_library");
+ break;
+ case EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY:
+ predefinedEventProperties.put("via", "media_library");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY:
+ predefinedEventProperties.put("via", "local_library");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY:
+ predefinedEventProperties.put("via", "media_library");
+ break;
+ case EDITOR_TAPPED_BLOCKQUOTE:
+ predefinedEventProperties.put("button", "blockquote");
+ break;
+ case EDITOR_TAPPED_BOLD:
+ predefinedEventProperties.put("button", "bold");
+ break;
+ case EDITOR_TAPPED_IMAGE:
+ predefinedEventProperties.put("button", "image");
+ break;
+ case EDITOR_TAPPED_ITALIC:
+ predefinedEventProperties.put("button", "italic");
+ break;
+ case EDITOR_TAPPED_LINK:
+ predefinedEventProperties.put("button", "link");
+ break;
+ case EDITOR_TAPPED_MORE:
+ predefinedEventProperties.put("button", "more");
+ break;
+ case EDITOR_TAPPED_STRIKETHROUGH:
+ predefinedEventProperties.put("button", "strikethrough");
+ break;
+ case EDITOR_TAPPED_UNDERLINE:
+ predefinedEventProperties.put("button", "underline");
+ break;
+ case EDITOR_TAPPED_HTML:
+ predefinedEventProperties.put("button", "html");
+ break;
+ case EDITOR_TAPPED_ORDERED_LIST:
+ predefinedEventProperties.put("button", "ordered_list");
+ break;
+ case EDITOR_TAPPED_UNLINK:
+ predefinedEventProperties.put("button", "unlink");
+ break;
+ case EDITOR_TAPPED_UNORDERED_LIST:
+ predefinedEventProperties.put("button", "unordered_list");
+ break;
+ case OPENED_POSTS:
+ predefinedEventProperties.put("menu_item", "posts");
+ break;
+ case OPENED_PAGES:
+ predefinedEventProperties.put("menu_item", "pages");
+ break;
+ case OPENED_COMMENTS:
+ predefinedEventProperties.put("menu_item", "comments");
+ break;
+ case OPENED_VIEW_SITE:
+ predefinedEventProperties.put("menu_item", "view_site");
+ break;
+ case OPENED_VIEW_ADMIN:
+ predefinedEventProperties.put("menu_item", "view_admin");
+ break;
+ case OPENED_MEDIA_LIBRARY:
+ predefinedEventProperties.put("menu_item", "media_library");
+ break;
+ case OPENED_BLOG_SETTINGS:
+ predefinedEventProperties.put("menu_item", "site_settings");
+ break;
+ case STATS_PERIOD_DAYS_ACCESSED:
+ predefinedEventProperties.put("period", "days");
+ break;
+ case STATS_PERIOD_WEEKS_ACCESSED:
+ predefinedEventProperties.put("period", "weeks");
+ break;
+ case STATS_PERIOD_MONTHS_ACCESSED:
+ predefinedEventProperties.put("period", "months");
+ break;
+ case STATS_PERIOD_YEARS_ACCESSED:
+ predefinedEventProperties.put("period", "years");
+ break;
+ }
+
+ final String user;
+ final TracksClient.NosaraUserType userType;
+ if (getWordPressComUserName() != null) {
+ user = getWordPressComUserName();
+ userType = TracksClient.NosaraUserType.WPCOM;
+ } else {
+ // This is just a security checks since the anonID is already available here.
+ // refresh metadata is called on login/logout/startup and it loads/generates the anonId when necessary.
+ if (getAnonID() == null) {
+ user = generateNewAnonID();
+ } else {
+ user = getAnonID();
+ }
+ userType = TracksClient.NosaraUserType.ANON;
+ }
+
+
+ // create the merged JSON Object of properties
+ // Properties defined by the user have precedence over the default ones pre-defined at "event level"
+ final JSONObject propertiesToJSON;
+ if (properties != null && properties.size() > 0) {
+ propertiesToJSON = new JSONObject(properties);
+ for (String key : predefinedEventProperties.keySet()) {
+ try {
+ if (propertiesToJSON.has(key)) {
+ AppLog.w(AppLog.T.STATS, "The user has defined a property named: '" + key + "' that will override" +
+ "the same property pre-defined at event level. This may generate unexpected behavior!!");
+ AppLog.w(AppLog.T.STATS, "User value: " + propertiesToJSON.get(key).toString() + " - pre-defined value: " +
+ predefinedEventProperties.get(key).toString());
+ } else {
+ propertiesToJSON.put(key, predefinedEventProperties.get(key));
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Error while merging user-defined properties with pre-defined properties", e);
+ }
+ }
+ } else{
+ propertiesToJSON = new JSONObject(predefinedEventProperties);
+ }
+
+ if (propertiesToJSON.length() > 0) {
+ mNosaraClient.track(EVENTS_PREFIX + eventName, propertiesToJSON, user, userType);
+ } else {
+ mNosaraClient.track(EVENTS_PREFIX + eventName, user, userType);
+ }
+ }
+
+
+
+ @Override
+ public void endSession() {
+ this.flush();
+ }
+
+ @Override
+ public void flush() {
+ if (mNosaraClient == null) {
+ return;
+ }
+ mNosaraClient.flush();
+ }
+
+ @Override
+ public void refreshMetadata(AnalyticsMetadata metadata) {
+ if (mNosaraClient == null) {
+ return;
+ }
+
+ try {
+ JSONObject properties = new JSONObject();
+ properties.put(JETPACK_USER, metadata.isJetpackUser());
+ properties.put(NUMBER_OF_BLOGS, metadata.getNumBlogs());
+ mNosaraClient.registerUserProperties(properties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ if (metadata.isUserConnected() && metadata.isWordPressComUser()) {
+ setWordPressComUserName(metadata.getUsername());
+ // Re-unify the user
+ if (getAnonID() != null) {
+ mNosaraClient.trackAliasUser(getWordPressComUserName(), getAnonID(), TracksClient.NosaraUserType.WPCOM);
+ clearAnonID();
+ }
+ } else {
+ // Not wpcom connected. Check if anonID is already present
+ setWordPressComUserName(null);
+ if (getAnonID() == null) {
+ generateNewAnonID();
+ }
+ }
+
+
+ }
+
+
+ @Override
+ public void clearAllData() {
+ super.clearAllData();
+ if (mNosaraClient == null) {
+ return;
+ }
+ mNosaraClient.clearUserProperties();
+ }
+
+ @Override
+ public void registerPushNotificationToken(String regId) {
+ return;
+ }
+
+ public static String getEventNameForStat(AnalyticsTracker.Stat stat) {
+ switch (stat) {
+ case APPLICATION_OPENED:
+ return "application_opened";
+ case APPLICATION_CLOSED:
+ return "application_closed";
+ case APPLICATION_INSTALLED:
+ return "application_installed";
+ case APPLICATION_UPGRADED:
+ return "application_upgraded";
+ case READER_ACCESSED:
+ return "reader_accessed";
+ case READER_ARTICLE_COMMENTED_ON:
+ return "reader_article_commented_on";
+ case READER_ARTICLE_LIKED:
+ return "reader_article_liked";
+ case READER_ARTICLE_OPENED:
+ return "reader_article_opened";
+ case READER_ARTICLE_UNLIKED:
+ return "reader_article_unliked";
+ case READER_BLOG_BLOCKED:
+ return "reader_blog_blocked";
+ case READER_BLOG_FOLLOWED:
+ return "reader_site_followed";
+ case READER_BLOG_PREVIEWED:
+ return "reader_blog_previewed";
+ case READER_BLOG_UNFOLLOWED:
+ return "reader_site_unfollowed";
+ case READER_DISCOVER_VIEWED:
+ return "reader_discover_viewed";
+ case READER_INFINITE_SCROLL:
+ return "reader_infinite_scroll_performed";
+ case READER_LIST_FOLLOWED:
+ return "reader_list_followed";
+ case READER_LIST_LOADED:
+ return "reader_list_loaded";
+ case READER_LIST_PREVIEWED:
+ return "reader_list_previewed";
+ case READER_LIST_UNFOLLOWED:
+ return "reader_list_unfollowed";
+ case READER_TAG_FOLLOWED:
+ return "reader_reader_tag_followed";
+ case READER_TAG_LOADED:
+ return "reader_tag_loaded";
+ case READER_TAG_PREVIEWED:
+ return "reader_tag_previewed";
+ case READER_SEARCH_LOADED:
+ return "reader_search_loaded";
+ case READER_SEARCH_PERFORMED:
+ return "reader_search_performed";
+ case READER_SEARCH_RESULT_TAPPED:
+ return "reader_searchcard_clicked";
+ case READER_TAG_UNFOLLOWED:
+ return "reader_reader_tag_unfollowed";
+ case READER_RELATED_POST_CLICKED:
+ return "reader_related_post_clicked";
+ case EDITOR_CREATED_POST:
+ return "editor_post_created";
+ case EDITOR_SAVED_DRAFT:
+ return "editor_draft_saved";
+ case EDITOR_DISCARDED_CHANGES:
+ return "editor_discarded_changes";
+ case EDITOR_EDITED_IMAGE:
+ return "editor_image_edited";
+ case EDITOR_ENABLED_NEW_VERSION:
+ return "editor_enabled_new_version";
+ case EDITOR_TOGGLED_OFF:
+ return "editor_toggled_off";
+ case EDITOR_TOGGLED_ON:
+ return "editor_toggled_on";
+ case EDITOR_UPLOAD_MEDIA_FAILED:
+ return "editor_upload_media_failed";
+ case EDITOR_UPLOAD_MEDIA_RETRIED:
+ return "editor_upload_media_retried";
+ case EDITOR_CLOSED:
+ return "editor_closed";
+ case EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY:
+ return "editor_photo_added";
+ case EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY:
+ return "editor_photo_added";
+ case EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY:
+ return "editor_video_added";
+ case EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY:
+ return "editor_video_added";
+ case EDITOR_PUBLISHED_POST:
+ return "editor_post_published";
+ case EDITOR_UPDATED_POST:
+ return "editor_post_updated";
+ case EDITOR_SCHEDULED_POST:
+ return "editor_post_scheduled";
+ case EDITOR_TAPPED_BLOCKQUOTE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_BOLD:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_IMAGE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_ITALIC:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_LINK:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_MORE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_STRIKETHROUGH:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_UNDERLINE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_HTML:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_ORDERED_LIST:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_UNLINK:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_UNORDERED_LIST:
+ return "editor_button_tapped";
+ case NOTIFICATIONS_ACCESSED:
+ return "notifications_accessed";
+ case NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS:
+ return "notifications_notification_details_opened";
+ case NOTIFICATION_APPROVED:
+ return "notifications_approved";
+ case NOTIFICATION_UNAPPROVED:
+ return "notifications_unapproved";
+ case NOTIFICATION_REPLIED_TO:
+ return "notifications_replied_to";
+ case NOTIFICATION_TRASHED:
+ return "notifications_trashed";
+ case NOTIFICATION_FLAGGED_AS_SPAM:
+ return "notifications_flagged_as_spam";
+ case NOTIFICATION_LIKED:
+ return "notifications_comment_liked";
+ case NOTIFICATION_UNLIKED:
+ return "notifications_comment_unliked";
+ case OPENED_POSTS:
+ return "site_menu_opened";
+ case OPENED_PAGES:
+ return "site_menu_opened";
+ case OPENED_COMMENTS:
+ return "site_menu_opened";
+ case OPENED_VIEW_SITE:
+ return "site_menu_opened";
+ case OPENED_VIEW_ADMIN:
+ return "site_menu_opened";
+ case OPENED_MEDIA_LIBRARY:
+ return "site_menu_opened";
+ case OPENED_BLOG_SETTINGS:
+ return "site_menu_opened";
+ case OPENED_ACCOUNT_SETTINGS:
+ return "account_settings_opened";
+ case OPENED_APP_SETTINGS:
+ return "app_settings_opened";
+ case OPENED_MY_PROFILE:
+ return "my_profile_opened";
+ case OPENED_PEOPLE_MANAGEMENT:
+ return "people_management_list_opened";
+ case OPENED_PERSON:
+ return "people_management_details_opened";
+ case CREATED_ACCOUNT:
+ return "account_created";
+ case CREATED_SITE:
+ return "site_created";
+ case SHARED_ITEM:
+ return "item_shared";
+ case ADDED_SELF_HOSTED_SITE:
+ return "self_hosted_blog_added";
+ case SIGNED_IN:
+ return "signed_in";
+ case SIGNED_INTO_JETPACK:
+ return "signed_into_jetpack";
+ case ACCOUNT_LOGOUT:
+ return "account_logout";
+ case PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN:
+ return "stats_screen_signed_into_jetpack";
+ case STATS_ACCESSED:
+ return "stats_accessed";
+ case STATS_INSIGHTS_ACCESSED:
+ return "stats_insights_accessed";
+ case STATS_PERIOD_DAYS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_PERIOD_WEEKS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_PERIOD_MONTHS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_PERIOD_YEARS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_VIEW_ALL_ACCESSED:
+ return "stats_view_all_accessed";
+ case STATS_SINGLE_POST_ACCESSED:
+ return "stats_single_post_accessed";
+ case STATS_TAPPED_BAR_CHART:
+ return "stats_bar_chart_tapped";
+ case STATS_SCROLLED_TO_BOTTOM:
+ return "stats_scrolled_to_bottom";
+ case STATS_SELECTED_INSTALL_JETPACK:
+ return "stats_install_jetpack_selected";
+ case STATS_SELECTED_CONNECT_JETPACK:
+ return "stats_connect_jetpack_selected";
+ case STATS_WIDGET_ADDED:
+ return "stats_widget_added";
+ case STATS_WIDGET_REMOVED:
+ return "stats_widget_removed";
+ case STATS_WIDGET_TAPPED:
+ return "stats_widget_tapped";
+ case PUSH_NOTIFICATION_RECEIVED:
+ return "push_notification_received";
+ case PUSH_NOTIFICATION_TAPPED:
+ return "push_notification_alert_tapped";
+ case SUPPORT_OPENED_HELPSHIFT_SCREEN:
+ return "support_helpshift_screen_opened";
+ case SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE:
+ return "support_reply_to_support_message_sent";
+ case LOGIN_MAGIC_LINK_EXITED:
+ return "login_magic_link_exited";
+ case LOGIN_MAGIC_LINK_FAILED:
+ return "login_magic_link_failed";
+ case LOGIN_MAGIC_LINK_OPENED:
+ return "login_magic_link_opened";
+ case LOGIN_MAGIC_LINK_REQUESTED:
+ return "login_magic_link_requested";
+ case LOGIN_MAGIC_LINK_SUCCEEDED:
+ return "login_magic_link_succeeded";
+ case LOGIN_FAILED:
+ return "login_failed_to_login";
+ case LOGIN_FAILED_TO_GUESS_XMLRPC:
+ return "login_failed_to_guess_xmlrpc";
+ case LOGIN_INSERTED_INVALID_URL:
+ return "login_inserted_invalid_url";
+ case LOGIN_AUTOFILL_CREDENTIALS_FILLED:
+ return "login_autofill_credentials_filled";
+ case LOGIN_AUTOFILL_CREDENTIALS_UPDATED:
+ return "login_autofill_credentials_updated";
+ case PERSON_REMOVED:
+ return "people_management_person_removed";
+ case PERSON_UPDATED:
+ return "people_management_person_updated";
+ case PUSH_AUTHENTICATION_APPROVED:
+ return "push_authentication_approved";
+ case PUSH_AUTHENTICATION_EXPIRED:
+ return "push_authentication_expired";
+ case PUSH_AUTHENTICATION_FAILED:
+ return "push_authentication_failed";
+ case PUSH_AUTHENTICATION_IGNORED:
+ return "push_authentication_ignored";
+ case NOTIFICATION_SETTINGS_LIST_OPENED:
+ return "notification_settings_list_opened";
+ case NOTIFICATION_SETTINGS_STREAMS_OPENED:
+ return "notification_settings_streams_opened";
+ case NOTIFICATION_SETTINGS_DETAILS_OPENED:
+ return "notification_settings_details_opened";
+ case ME_ACCESSED:
+ return "me_tab_accessed";
+ case ME_GRAVATAR_TAPPED:
+ return "me_gravatar_tapped";
+ case ME_GRAVATAR_TOOLTIP_TAPPED:
+ return "me_gravatar_tooltip_tapped";
+ case ME_GRAVATAR_PERMISSIONS_INTERRUPTED:
+ return "me_gravatar_permissions_interrupted";
+ case ME_GRAVATAR_PERMISSIONS_DENIED:
+ return "me_gravatar_permissions_denied";
+ case ME_GRAVATAR_PERMISSIONS_ACCEPTED:
+ return "me_gravatar_permissions_accepted";
+ case ME_GRAVATAR_SHOT_NEW:
+ return "me_gravatar_shot_new";
+ case ME_GRAVATAR_GALLERY_PICKED:
+ return "me_gravatar_gallery_picked";
+ case ME_GRAVATAR_CROPPED:
+ return "me_gravatar_cropped";
+ case ME_GRAVATAR_UPLOADED:
+ return "me_gravatar_uploaded";
+ case ME_GRAVATAR_UPLOAD_UNSUCCESSFUL:
+ return "me_gravatar_upload_unsuccessful";
+ case ME_GRAVATAR_UPLOAD_EXCEPTION:
+ return "me_gravatar_upload_exception";
+ case MY_SITE_ACCESSED:
+ return "my_site_tab_accessed";
+ case THEMES_ACCESSED_THEMES_BROWSER:
+ return "themes_theme_browser_accessed";
+ case THEMES_ACCESSED_SEARCH:
+ return "themes_search_accessed";
+ case THEMES_CHANGED_THEME:
+ return "themes_theme_changed";
+ case THEMES_PREVIEWED_SITE:
+ return "themes_theme_for_site_previewed";
+ case THEMES_DEMO_ACCESSED:
+ return "themes_demo_accessed";
+ case THEMES_CUSTOMIZE_ACCESSED:
+ return "themes_customize_accessed";
+ case THEMES_SUPPORT_ACCESSED:
+ return "themes_support_accessed";
+ case THEMES_DETAILS_ACCESSED:
+ return "themes_details_accessed";
+ case ACCOUNT_SETTINGS_LANGUAGE_CHANGED:
+ return "account_settings_language_changed";
+ case SITE_SETTINGS_ACCESSED:
+ return "site_settings_accessed";
+ case SITE_SETTINGS_ACCESSED_MORE_SETTINGS:
+ return "site_settings_more_settings_accessed";
+ case SITE_SETTINGS_ADDED_LIST_ITEM:
+ return "site_settings_added_list_item";
+ case SITE_SETTINGS_DELETED_LIST_ITEMS:
+ return "site_settings_deleted_list_items";
+ case SITE_SETTINGS_HINT_TOAST_SHOWN:
+ return "site_settings_hint_toast_shown";
+ case SITE_SETTINGS_LEARN_MORE_CLICKED:
+ return "site_settings_learn_more_clicked";
+ case SITE_SETTINGS_LEARN_MORE_LOADED:
+ return "site_settings_learn_more_loaded";
+ case SITE_SETTINGS_SAVED_REMOTELY:
+ return "site_settings_saved_remotely";
+ case SITE_SETTINGS_START_OVER_ACCESSED:
+ return "site_settings_start_over_accessed";
+ case SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED:
+ return "site_settings_start_over_contact_support_clicked";
+ case SITE_SETTINGS_EXPORT_SITE_ACCESSED:
+ return "site_settings_export_site_accessed";
+ case SITE_SETTINGS_EXPORT_SITE_REQUESTED:
+ return "site_settings_export_site_requested";
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK:
+ return "site_settings_export_site_response_ok";
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR:
+ return "site_settings_export_site_response_error";
+ case SITE_SETTINGS_DELETE_SITE_ACCESSED:
+ return "site_settings_delete_site_accessed";
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED:
+ return "site_settings_delete_site_purchases_requested";
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN:
+ return "site_settings_delete_site_purchases_shown";
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED:
+ return "site_settings_delete_site_purchases_show_clicked";
+ case SITE_SETTINGS_DELETE_SITE_REQUESTED:
+ return "site_settings_delete_site_requested";
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_OK:
+ return "site_settings_delete_site_response_ok";
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR:
+ return "site_settings_delete_site_response_error";
+ case ABTEST_START:
+ return "abtest_start";
+ case TRAIN_TRACKS_RENDER:
+ return "traintracks_render";
+ case TRAIN_TRACKS_INTERACT:
+ return "traintracks_interact";
+ default:
+ return null;
+ }
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java
new file mode 100644
index 000000000..e1b35f125
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.analytics;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.util.AppLog;
+
+public abstract class Tracker {
+ abstract void track(Stat stat);
+ abstract void track(Stat stat, Map<String, ?> properties);
+ abstract void endSession();
+ abstract void flush();
+ abstract void refreshMetadata(AnalyticsMetadata metadata);
+ abstract void registerPushNotificationToken(String regId);
+ abstract String getAnonIdPrefKey();
+
+ private String mAnonID = null; // do not access this variable directly. Use methods.
+ private String mWpcomUserName = null;
+ Context mContext;
+
+ public Tracker(Context context) throws IllegalArgumentException {
+ if (null == context) {
+ throw new IllegalArgumentException("Tracker requires a not-null context");
+ }
+ mContext = context;
+ }
+
+ void clearAllData() {
+ // Reset the anon ID here
+ clearAnonID();
+ setWordPressComUserName(null);
+ }
+
+ void clearAnonID() {
+ mAnonID = null;
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ if (preferences.contains(getAnonIdPrefKey())) {
+ final SharedPreferences.Editor editor = preferences.edit();
+ editor.remove(getAnonIdPrefKey());
+ editor.commit();
+ }
+ }
+
+ String getAnonID() {
+ if (mAnonID == null) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mAnonID = preferences.getString(getAnonIdPrefKey(), null);
+ }
+ return mAnonID;
+ }
+
+ String generateNewAnonID() {
+ String uuid = UUID.randomUUID().toString().replace("-", "");
+ AppLog.d(AppLog.T.STATS, "New anonID generated in " + this.getClass().getSimpleName() + ": " + uuid);
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ final SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(getAnonIdPrefKey(), uuid);
+ editor.commit();
+
+ mAnonID = uuid;
+ return uuid;
+ }
+
+ String getWordPressComUserName() {
+ return mWpcomUserName;
+ }
+
+ void setWordPressComUserName(String userName) {
+ mWpcomUserName = userName;
+ }
+}
diff --git a/libs/analytics/build.gradle b/libs/analytics/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/analytics/build.gradle
diff --git a/libs/analytics/gradle/wrapper/gradle-wrapper.jar b/libs/analytics/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..0087cd3b1
--- /dev/null
+++ b/libs/analytics/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/analytics/gradle/wrapper/gradle-wrapper.properties b/libs/analytics/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..26af108cc
--- /dev/null
+++ b/libs/analytics/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 09 11:48:51 CEST 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip
diff --git a/libs/analytics/gradlew b/libs/analytics/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/analytics/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/analytics/gradlew.bat b/libs/analytics/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/analytics/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/analytics/settings.gradle b/libs/analytics/settings.gradle
new file mode 100644
index 000000000..6ad7b7de9
--- /dev/null
+++ b/libs/analytics/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressAnalytics'
diff --git a/libs/editor/.gitignore b/libs/editor/.gitignore
new file mode 100644
index 000000000..c73b54048
--- /dev/null
+++ b/libs/editor/.gitignore
@@ -0,0 +1,56 @@
+# OS X generated file
+.DS_Store
+
+# built application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+build/
+*/build/
+captures/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Eclipse project files
+.settings/
+.classpath
+.project
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Generated by gradle
+crashlytics.properties
+
+# Generated by Crashlytics
+WordPress/src/main/res/values/com_crashlytics_export_strings.xml
+
+# Silver Searcher ignore file
+.agignore
+
+# Monkey runner settings
+*.pyc
+
+# libs
+libs/utils
+
+# Node-based JS Tests
+node_modules
+npm-debug.log* \ No newline at end of file
diff --git a/libs/editor/.travis.yml b/libs/editor/.travis.yml
new file mode 100644
index 000000000..9a6a6a437
--- /dev/null
+++ b/libs/editor/.travis.yml
@@ -0,0 +1,24 @@
+language: android
+jdk: oraclejdk8
+
+android:
+ components:
+ - extra-android-m2repository
+ - extra-android-support
+ - platform-tools
+ - tools
+ - build-tools-24.0.2
+ - android-24
+
+env:
+ global:
+ - GRADLE_OPTS="-XX:MaxPermSize=4g -Xmx4g"
+ - ANDROID_SDKS=android-16
+ - ANDROID_TARGET=android-16
+
+before_install:
+ # TODO: Remove the following line when Travis' platform-tools are updated to v24+
+ - echo yes | android update sdk -a --filter platform-tools --no-ui --force
+
+script:
+ - ./gradlew build
diff --git a/libs/editor/LICENSE.md b/libs/editor/LICENSE.md
new file mode 100644
index 000000000..0671f06ac
--- /dev/null
+++ b/libs/editor/LICENSE.md
@@ -0,0 +1,264 @@
+The GNU General Public License, Version 2, June 1991 (GPLv2)
+============================================================
+
+> Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+
+Preamble
+--------
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most
+of the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom to
+distribute copies of free software (and charge for this service if you wish),
+that you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs; and that you know you can
+do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show them
+these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually
+obtain patent licenses, in effect making the program proprietary. To prevent
+this, we have made it clear that any patent must be licensed for everyone's free
+use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+
+Terms And Conditions For Copying, Distribution And Modification
+---------------------------------------------------------------
+
+**0.** This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program or
+work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or
+translated into another language. (Hereinafter, translation is included without
+limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+**1.** You may copy and distribute verbatim copies of the Program's source code
+as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the Program
+a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+**2.** You may modify your copy or copies of the Program or any portion of it,
+thus forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+* **a)** You must cause the modified files to carry prominent notices stating
+ that you changed the files and the date of any change.
+
+* **b)** You must cause any work that you distribute or publish, that in whole
+ or in part contains or is derived from the Program or any part thereof, to
+ be licensed as a whole at no charge to all third parties under the terms of
+ this License.
+
+* **c)** If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use in the
+ most ordinary way, to print or display an announcement including an
+ appropriate copyright notice and a notice that there is no warranty (or
+ else, saying that you provide a warranty) and that users may redistribute
+ the program under these conditions, and telling the user how to view a copy
+ of this License. (Exception: if the Program itself is interactive but does
+ not normally print such an announcement, your work based on the Program is
+ not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License, and
+its terms, do not apply to those sections when you distribute them as separate
+works. But when you distribute the same sections as part of a whole which is a
+work based on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the entire whole,
+and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your
+rights to work written entirely by you; rather, the intent is to exercise the
+right to control the distribution of derivative or collective works based on the
+Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+**3.** You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+* **a)** Accompany it with the complete corresponding machine-readable source
+ code, which must be distributed under the terms of Sections 1 and 2 above on
+ a medium customarily used for software interchange; or,
+
+* **b)** Accompany it with a written offer, valid for at least three years, to
+ give any third party, for a charge no more than your cost of physically
+ performing source distribution, a complete machine-readable copy of the
+ corresponding source code, to be distributed under the terms of Sections 1
+ and 2 above on a medium customarily used for software interchange; or,
+
+* **c)** Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed only for
+ noncommercial distribution and only if you received the program in object
+ code or executable form with such an offer, in accord with Subsection b
+ above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface
+definition files, plus the scripts used to control compilation and installation
+of the executable. However, as a special exception, the source code distributed
+need not include anything that is normally distributed (in either source or
+binary form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component itself
+accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source code
+from the same place counts as distribution of the source code, even though third
+parties are not compelled to copy the source along with the object code.
+
+**4.** You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so
+long as such parties remain in full compliance.
+
+**5.** You are not required to accept this License, since you have not signed
+it. However, nothing else grants you permission to modify or distribute the
+Program or its derivative works. These actions are prohibited by law if you do
+not accept this License. Therefore, by modifying or distributing the Program (or
+any work based on the Program), you indicate your acceptance of this License to
+do so, and all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+**6.** Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these terms and
+conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties to this License.
+
+**7.** If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution of
+the Program by all those who receive copies directly or indirectly through you,
+then the only way you could satisfy both it and this License would be to refrain
+entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and the
+section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices. Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose that
+choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+**8.** If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded. In such
+case, this License incorporates the limitation as if written in the body of this
+License.
+
+**9.** The Free Software Foundation may publish revised and/or new versions of
+the General Public License from time to time. Such new versions will be similar
+in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that version
+or of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of this License, you may choose any
+version ever published by the Free Software Foundation.
+
+**10.** If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of
+all derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+
+No Warranty
+-----------
+
+**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
+INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
+BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
+OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
diff --git a/libs/editor/README.md b/libs/editor/README.md
new file mode 100644
index 000000000..753794919
--- /dev/null
+++ b/libs/editor/README.md
@@ -0,0 +1,35 @@
+# WordPress-Editor-Android #
+
+[![Build Status](https://travis-ci.org/wordpress-mobile/WordPress-Editor-Android.svg?branch=develop)](https://travis-ci.org/wordpress-mobile/WordPress-Editor-Android)
+
+## Introduction ##
+
+WordPress-Editor-Android is the text editor used in the [WordPress Android app](https://github.com/wordpress-mobile/WordPress-Android) to create and edit pages & posts. In short it's a simple, straightforward way to visually edit HTML.
+
+## Testing ##
+
+This project has both unit testing and integration testing, maintained and run separately.
+
+Unit testing is done with the [Robolectric framework](http://robolectric.org/). To run unit tests simply run `gradlew testDebug`.
+
+Integration testing is done with the [Android testing framework](http://developer.android.com/tools/testing/testing_android.html). To run integration tests run `gradlew connectedAndroidTest`.
+
+Add new unit tests to `src/test/java/` and integration tests to `stc/androidTest/java/`.
+
+### JavaScript Tests ###
+
+This project also has unit tests for the JS part of the editor using [Mocha](https://mochajs.org/).
+
+To be able to run the tests, [npm](https://www.npmjs.com/) and Mocha (`npm install -g mocha`) are required.
+
+With npm and Mocha installed, from within `example/src/test/js`, run:
+
+ npm install chai
+
+And then run `mocha` inside the same folder:
+
+ cd example/src/test/js; mocha test*; cd -
+
+## LICENSE ##
+
+WordPress-Editor-Android is an Open Source project covered by the [GNU General Public License version 2](LICENSE.md).
diff --git a/libs/editor/WordPressEditor/build.gradle b/libs/editor/WordPressEditor/build.gradle
new file mode 100644
index 000000000..ccfb60927
--- /dev/null
+++ b/libs/editor/WordPressEditor/build.gradle
@@ -0,0 +1,108 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+apply plugin: 'signing'
+
+repositories {
+ jcenter()
+}
+
+android {
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionCode 13
+ versionName "1.3"
+ minSdkVersion 16
+ targetSdkVersion 24
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ // Avoid 'duplicate files during packaging of APK' errors
+ packagingOptions {
+ exclude 'LICENSE.txt'
+ exclude 'META-INF/LICENSE.txt'
+ exclude 'META-INF/LICENSE'
+ exclude 'META-INF/NOTICE'
+ exclude 'META-INF/NOTICE.txt'
+ }
+}
+
+dependencies {
+ compile 'com.android.support:appcompat-v7:24.2.1'
+ compile 'com.android.support:support-v4:24.2.1'
+ compile 'com.android.support:design:24.2.1'
+ compile 'org.wordpress:utils:1.11.0'
+}
+
+signing {
+ required {
+ project.properties.containsKey("signing.keyId") && project.properties.containsKey("signing.secretKeyRingFile")
+ }
+ sign configurations.archives
+}
+
+version android.defaultConfig.versionName
+group = "org.wordpress"
+archivesBaseName = "editor"
+
+// http://central.sonatype.org/pages/gradle.html
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+ repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
+ authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword)
+ }
+
+ snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
+ authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword)
+ }
+
+ pom.project {
+ name 'WordPress-Android-Editor'
+ packaging 'aar'
+ description 'A reusable Android rich text editor component'
+ url 'https://github.com/wordpress-mobile/WordPress-Android-Editor'
+ scm {
+ connection 'scm:git:https://github.com/wordpress-mobile/WordPress-Android-Editor.git'
+ developerConnection 'scm:git:https://github.com/wordpress-mobile/WordPress-Android-Editor.git'
+ url 'https://github.com/wordpress-mobile/WordPress-Android-Editor'
+ }
+
+ licenses {
+ license {
+ name 'The MIT License (MIT)'
+ url 'http://opensource.org/licenses/MIT'
+ }
+ }
+
+ developers {
+ developer {
+ id 'maxme'
+ name 'Maxime Biais'
+ email 'maxime@automattic.com'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/lint.xml b/libs/editor/WordPressEditor/lint.xml
new file mode 100644
index 000000000..d377d1d00
--- /dev/null
+++ b/libs/editor/WordPressEditor/lint.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+ <issue id="InvalidPackage">
+ <ignore regexp="robolectric-2.4.jar" />
+ </issue>
+
+ <issue id="MissingPrefix">
+ <ignore regexp="Unexpected namespace prefix &quot;app&quot; found for tag `Image.*`" />
+ </issue>
+</lint> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/proguard-rules.pro b/libs/editor/WordPressEditor/proguard-rules.pro
new file mode 100644
index 000000000..d8d549daa
--- /dev/null
+++ b/libs/editor/WordPressEditor/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/max/work/android-sdk-mac/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 *;
+#}
diff --git a/libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml b/libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..eeef377fb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.editor" >
+ <application>
+ <activity android:name=".MockActivity"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:exported="false" />
+ <activity android:name=".MockEditorActivity"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:exported="false"/>
+ </application>>
+</manifest>
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java
new file mode 100644
index 000000000..1babaa3db
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java
@@ -0,0 +1,41 @@
+package org.wordpress.android.editor;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Map;
+
+public class EditorFragmentForTests extends EditorFragment {
+ protected EditorWebViewAbstract mWebView;
+
+ protected boolean mInitCalled = false;
+ protected boolean mDomLoaded = false;
+ protected boolean mOnSelectionStyleChangedCalled = false;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ mWebView = (EditorWebViewAbstract) view.findViewById(R.id.webview);
+ return view;
+ }
+
+ @Override
+ protected void initJsEditor() {
+ super.initJsEditor();
+ mInitCalled = true;
+ }
+
+ @Override
+ public void onDomLoaded() {
+ super.onDomLoaded();
+ mDomLoaded = true;
+ }
+
+ @Override
+ public void onSelectionStyleChanged(final Map<String, Boolean> changeMap) {
+ super.onSelectionStyleChanged(changeMap);
+ mOnSelectionStyleChangedCalled = true;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java
new file mode 100644
index 000000000..a955f0a98
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java
@@ -0,0 +1,177 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+import android.widget.ToggleButton;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+import static org.wordpress.android.editor.TestingUtils.waitFor;
+
+public class EditorFragmentTest extends ActivityInstrumentationTestCase2<MockEditorActivity> {
+ private Activity mActivity;
+ private EditorFragmentForTests mFragment;
+
+ public EditorFragmentTest() {
+ super(MockEditorActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mActivity = getActivity();
+ mFragment = (EditorFragmentForTests) mActivity.getFragmentManager().findFragmentByTag("editorFragment");
+ }
+
+ public void testDomLoadedCallbackReceived() {
+ // initJsEditor() should have been called on setup
+ assertTrue(mFragment.mInitCalled);
+
+ waitForOnDomLoaded();
+
+ // The JS editor should have sent out a callback when the DOM loaded, triggering onDomLoaded()
+ assertTrue(mFragment.mDomLoaded);
+ }
+
+ public void testFormatBarToggledOnSelectedFieldChanged() {
+ Map<String, String> selectionArgs = new HashMap<>();
+
+ selectionArgs.put("id", "zss_field_title");
+ mFragment.onSelectionChanged(selectionArgs);
+
+ waitFor(100);
+
+ View view = mFragment.getView();
+
+ if (view == null) {
+ throw(new IllegalStateException("Fragment view is empty"));
+ }
+
+ // The formatting buttons should be disabled while the title field is selected
+ ToggleButton mediaButton = (ToggleButton) view.findViewById(R.id.format_bar_button_media);
+ ToggleButton boldButton = (ToggleButton) view.findViewById(R.id.format_bar_button_bold);
+ ToggleButton italicButton = (ToggleButton) view.findViewById(R.id.format_bar_button_italic);
+ ToggleButton quoteButton = (ToggleButton) view.findViewById(R.id.format_bar_button_quote);
+ ToggleButton ulButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ul);
+ ToggleButton olButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ol);
+ ToggleButton linkButton = (ToggleButton) view.findViewById(R.id.format_bar_button_link);
+ ToggleButton strikethroughButton = (ToggleButton) view.findViewById(R.id.format_bar_button_strikethrough);
+
+ assertFalse(mediaButton.isEnabled());
+ assertFalse(boldButton.isEnabled());
+ assertFalse(italicButton.isEnabled());
+ assertFalse(quoteButton.isEnabled());
+ assertFalse(ulButton.isEnabled());
+ assertFalse(olButton.isEnabled());
+ assertFalse(linkButton.isEnabled());
+
+ if (strikethroughButton != null) {
+ assertFalse(strikethroughButton.isEnabled());
+ }
+
+ // The HTML button should always be enabled
+ ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html);
+ assertTrue(htmlButton.isEnabled());
+
+ selectionArgs.clear();
+ selectionArgs.put("id", "zss_field_content");
+ mFragment.onSelectionChanged(selectionArgs);
+
+ waitFor(500);
+
+ // The formatting buttons should be enabled while the content field is selected
+ assertTrue(mediaButton.isEnabled());
+ assertTrue(boldButton.isEnabled());
+ assertTrue(italicButton.isEnabled());
+ assertTrue(quoteButton.isEnabled());
+ assertTrue(ulButton.isEnabled());
+ assertTrue(olButton.isEnabled());
+ assertTrue(linkButton.isEnabled());
+
+ if (strikethroughButton != null) {
+ assertTrue(strikethroughButton.isEnabled());
+ }
+
+ // The HTML button should always be enabled
+ assertTrue(htmlButton.isEnabled());
+ }
+
+ public void testHtmlModeToggleTextTransfer() throws InterruptedException {
+ waitForOnDomLoaded();
+
+ final View view = mFragment.getView();
+
+ if (view == null) {
+ throw (new IllegalStateException("Fragment view is empty"));
+ }
+
+ final ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html);
+
+ String content = mFragment.getContent().toString();
+
+ final SourceViewEditText titleText = (SourceViewEditText) view.findViewById(R.id.sourceview_title);
+ final SourceViewEditText contentText = (SourceViewEditText) view.findViewById(R.id.sourceview_content);
+
+ // -- Check that title and content text is properly loaded into the EditTexts when switching to HTML mode
+
+ final CountDownLatch uiThreadLatch1 = new CountDownLatch(1);
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ htmlButton.performClick(); // Turn on HTML mode
+
+ uiThreadLatch1.countDown();
+ }
+ });
+
+ uiThreadLatch1.await();
+
+ waitFor(500);
+
+ // The HTML mode fields should be populated with the raw HTML loaded into the WebView on load
+ // (see MockEditorActivity)
+ assertEquals("A title", titleText.getText().toString());
+ assertEquals(content, contentText.getText().toString());
+
+ // -- Check that the title and content text is updated in the WebView when switching back from HTML mode
+
+ final CountDownLatch uiThreadLatch2 = new CountDownLatch(1);
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ titleText.setText("new title");
+ contentText.setText("new <b>content</b>");
+
+ // Check that getTitle() and getContent() return latest version even in HTML mode
+ assertEquals("new title", mFragment.getTitle());
+ assertEquals("new <b>content</b>", mFragment.getContent());
+
+ htmlButton.performClick(); // Turn off HTML mode
+
+ uiThreadLatch2.countDown();
+ }
+ });
+
+ uiThreadLatch2.await();
+
+ waitFor(300); // Wait for JS to update the title/content
+
+ assertEquals("new title", mFragment.getTitle());
+ assertEquals("new <b>content</b>", mFragment.getContent());
+ }
+
+ private void waitForOnDomLoaded() {
+ long start = System.currentTimeMillis();
+ while(!mFragment.mDomLoaded) {
+ waitFor(10);
+ if (System.currentTimeMillis() - start > 5000) {
+ throw(new RuntimeException("Callback wait timed out"));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java
new file mode 100644
index 000000000..580ea2de4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.editor;
+
+import android.support.v7.app.AppCompatActivity;
+
+public class MockActivity extends AppCompatActivity {
+
+}
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java
new file mode 100644
index 000000000..ecd515154
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java
@@ -0,0 +1,102 @@
+package org.wordpress.android.editor;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.DragEvent;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorDragAndDropListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.util.ArrayList;
+
+public class MockEditorActivity extends AppCompatActivity implements EditorFragmentListener,
+ EditorDragAndDropListener {
+ public static final int LAYOUT_ID = 999;
+
+ EditorFragment mEditorFragment;
+
+ @SuppressWarnings("ResourceType")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ LinearLayout linearLayout = new LinearLayout(this);
+ linearLayout.setId(LAYOUT_ID);
+ setContentView(linearLayout);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ mEditorFragment = new EditorFragmentForTests();
+ fragmentTransaction.add(linearLayout.getId(), mEditorFragment, "editorFragment");
+ fragmentTransaction.commit();
+ }
+
+ @Override
+ public void onEditorFragmentInitialized() {
+ mEditorFragment.setTitle("A title");
+ mEditorFragment.setContent("<p>Example <strong>content</strong></p>");
+ }
+
+ @Override
+ public void onSettingsClicked() {
+
+ }
+
+ @Override
+ public void onAddMediaClicked() {
+
+ }
+
+ @Override
+ public void onMediaRetryClicked(String mediaId) {
+
+ }
+
+ @Override
+ public void onMediaUploadCancelClicked(String mediaId, boolean delete) {
+
+ }
+
+ @Override
+ public void onFeaturedImageChanged(long mediaId) {
+
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(String videoId) {
+
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ return "";
+ }
+
+ @Override
+ public void saveMediaFile(MediaFile mediaFile) {
+
+ }
+
+ @Override
+ public void onTrackableEvent(TrackableEvent event) {
+
+ }
+
+ @Override
+ public void onMediaDropped(ArrayList<Uri> mediaUri) {
+
+ }
+
+ @Override
+ public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
+
+ }
+}
+
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java
new file mode 100644
index 000000000..e51cba6ba
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.editor;
+
+import org.wordpress.android.util.AppLog;
+
+public class TestingUtils {
+
+ static public void waitFor(long milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch(InterruptedException e) {
+ AppLog.e(AppLog.T.EDITOR, "Thread interrupted");
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java
new file mode 100644
index 000000000..9989cbd65
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java
@@ -0,0 +1,128 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Build;
+import android.test.ActivityInstrumentationTestCase2;
+import android.webkit.JavascriptInterface;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for the <code>ZSSEditor</code> inside an <code>EditorWebViewAbstract</code>, with no UI.
+ */
+public class ZssEditorTest extends ActivityInstrumentationTestCase2<MockActivity> {
+ private static final String JS_CALLBACK_HANDLER = "nativeCallbackHandler";
+
+ private Instrumentation mInstrumentation;
+ private EditorWebViewAbstract mWebView;
+
+ private CountDownLatch mSetUpLatch;
+
+ private TestMethod mTestMethod;
+ private CountDownLatch mCallbackLatch;
+ private CountDownLatch mDomLoadedCallbackLatch;
+ private Set<String> mCallbackSet;
+
+ private enum TestMethod {
+ INIT
+ }
+
+ public ZssEditorTest() {
+ super(MockActivity.class);
+ }
+
+ @SuppressLint("AddJavascriptInterface")
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ Activity activity = getActivity();
+ mSetUpLatch = new CountDownLatch(1);
+ mDomLoadedCallbackLatch = new CountDownLatch(1);
+
+ mSetUpLatch.countDown();
+
+ String htmlEditor = Utils.getHtmlFromFile(activity, "android-editor.html");
+
+ if (htmlEditor != null) {
+ htmlEditor = htmlEditor.replace("%%TITLE%%", getActivity().getString(R.string.visual_editor));
+ htmlEditor = htmlEditor.replace("%%ANDROID_API_LEVEL%%", String.valueOf(Build.VERSION.SDK_INT));
+ htmlEditor = htmlEditor.replace("%%LOCALIZED_STRING_INIT%%",
+ "nativeState.localizedStringEdit = '" + getActivity().getString(R.string.edit) + "';\n" +
+ "nativeState.localizedStringUploading = '" + getActivity().getString(R.string.uploading) + "';\n" +
+ "nativeState.localizedStringUploadingGallery = '" +
+ getActivity().getString(R.string.uploading_gallery_placeholder) + "';\n");
+ }
+
+ final String finalHtmlEditor = htmlEditor;
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView = new EditorWebView(mInstrumentation.getContext(), null);
+ if (Build.VERSION.SDK_INT < 17) {
+ mWebView.setJsCallbackReceiver(new MockJsCallbackReceiver(new EditorFragmentForTests()));
+ } else {
+ mWebView.addJavascriptInterface(new MockJsCallbackReceiver(new EditorFragmentForTests()),
+ JS_CALLBACK_HANDLER);
+ }
+ mWebView.loadDataWithBaseURL("file:///android_asset/", finalHtmlEditor, "text/html", "utf-8", "");
+ mSetUpLatch.countDown();
+ }
+ });
+ }
+
+ public void testInitialization() throws InterruptedException {
+ // Wait for setUp() to finish initializing the WebView
+ mSetUpLatch.await();
+
+ // Identify this method to the MockJsCallbackReceiver
+ mTestMethod = TestMethod.INIT;
+
+ // Expecting three startup callbacks from the ZSS editor
+ mCallbackLatch = new CountDownLatch(3);
+ mCallbackSet = new HashSet<>();
+ boolean callbacksReceived = mCallbackLatch.await(5, TimeUnit.SECONDS);
+ assertTrue(callbacksReceived);
+
+ Set<String> expectedSet = new HashSet<>();
+ expectedSet.add("callback-new-field:id=zss_field_title");
+ expectedSet.add("callback-new-field:id=zss_field_content");
+ expectedSet.add("callback-dom-loaded:");
+
+ assertEquals(expectedSet, mCallbackSet);
+ }
+
+ private class MockJsCallbackReceiver extends JsCallbackReceiver {
+ public MockJsCallbackReceiver(EditorFragmentAbstract editorFragmentAbstract) {
+ super(editorFragmentAbstract);
+ }
+
+ @JavascriptInterface
+ public void executeCallback(String callbackId, String params) {
+ if (callbackId.equals("callback-dom-loaded")) {
+ // Notify test methods that the dom has loaded
+ mDomLoadedCallbackLatch.countDown();
+ }
+
+ // Handle callbacks and count down latches according to the currently running test
+ switch(mTestMethod) {
+ case INIT:
+ if (callbackId.equals("callback-dom-loaded")) {
+ mCallbackSet.add(callbackId + ":");
+ } else if (callbackId.equals("callback-new-field")) {
+ mCallbackSet.add(callbackId + ":" + params);
+ }
+ mCallbackLatch.countDown();
+ break;
+ default:
+ throw(new RuntimeException("Unknown calling method"));
+ }
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/AndroidManifest.xml b/libs/editor/WordPressEditor/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..fcade3209
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.editor" >
+ <application>
+ <activity android:name=".legacy.EditLinkActivity" />
+ </application>>
+</manifest>
diff --git a/libs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js b/libs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js
new file mode 100755
index 000000000..52dabe1e8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js
@@ -0,0 +1,3847 @@
+/*!
+ *
+ * ZSSRichTextEditor v1.0
+ * http://www.zedsaid.com
+ *
+ * Copyright 2013 Zed Said Studio
+ *
+ */
+
+// If we are using iOS or desktop
+var isUsingiOS = false;
+var isUsingAndroid = true;
+
+// THe default callback parameter separator
+var defaultCallbackSeparator = '~';
+
+const NodeName = {
+ BLOCKQUOTE: "BLOCKQUOTE",
+ PARAGRAPH: "P",
+ STRONG: "STRONG",
+ DEL: "DEL",
+ EM: "EM",
+ A: "A",
+ OL: "OL",
+ UL: "UL",
+ LI: "LI",
+ CODE: "CODE",
+ SPAN: "SPAN",
+ BR: "BR",
+ DIV: "DIV",
+ BODY: "BODY"
+};
+
+// The editor object
+var ZSSEditor = {};
+
+// These variables exist to reduce garbage (as in memory garbage) generation when typing real fast
+// in the editor.
+//
+ZSSEditor.caretArguments = ['yOffset=' + 0, 'height=' + 0];
+ZSSEditor.caretInfo = { y: 0, height: 0 };
+
+// Is this device an iPad
+ZSSEditor.isiPad;
+
+// The current selection
+ZSSEditor.currentSelection;
+
+// The current editing image
+ZSSEditor.currentEditingImage;
+
+// The current editing video
+ZSSEditor.currentEditingVideo;
+
+// The current editing link
+ZSSEditor.currentEditingLink;
+
+ZSSEditor.focusedField = null;
+
+// The objects that are enabled
+ZSSEditor.enabledItems = {};
+
+ZSSEditor.editableFields = {};
+
+ZSSEditor.lastTappedNode = null;
+
+// The default paragraph separator
+ZSSEditor.defaultParagraphSeparator = 'div';
+
+// We use a MutationObserver to catch user deletions of uploading or failed media
+// This is only officially supported on API>18; when the WebView doesn't recognize the MutationObserver,
+// we fall back to the deprecated DOMNodeRemoved event
+ZSSEditor.mutationObserver;
+
+ZSSEditor.defaultMutationObserverConfig = { attributes: false, childList: true, characterData: false };
+
+/**
+ * The initializer function that must be called onLoad
+ */
+ZSSEditor.init = function() {
+
+ rangy.init();
+
+ // Change a few CSS values if the device is an iPad
+ ZSSEditor.isiPad = (navigator.userAgent.match(/iPad/i) != null);
+ if (ZSSEditor.isiPad) {
+ $(document.body).addClass('ipad_body');
+ $('#zss_field_title').addClass('ipad_field_title');
+ $('#zss_field_content').addClass('ipad_field_content');
+ }
+
+ document.execCommand('insertBrOnReturn', false, false);
+
+ var editor = $('div.field').each(function() {
+ var editableField = new ZSSField($(this));
+ var editableFieldId = editableField.getNodeId();
+
+ ZSSEditor.editableFields[editableFieldId] = editableField;
+ ZSSEditor.callback("callback-new-field", "id=" + editableFieldId);
+ });
+
+ document.addEventListener("selectionchange", function(e) {
+ ZSSEditor.currentEditingLink = null;
+ // DRM: only do something here if the editor has focus. The reason is that when the
+ // selection changes due to the editor loosing focus, the focusout event will not be
+ // sent if we try to load a callback here.
+ //
+ if (editor.is(":focus")) {
+ ZSSEditor.selectionChangedCallback();
+ ZSSEditor.sendEnabledStyles(e);
+ var clicked = $(e.target);
+ if (!clicked.hasClass('zs_active')) {
+ $('img').removeClass('zs_active');
+ }
+ }
+ }, false);
+
+ // Attempt to instantiate a MutationObserver. This should fail for API<19, unless the OEM of the device has
+ // modified the WebView. If it fails, the editor will fall back to DOMNodeRemoved events.
+ try {
+ ZSSEditor.mutationObserver = new MutationObserver(function(mutations) {
+ ZSSEditor.onMutationObserved(mutations);} );
+ } catch(e) {
+ // no op
+ }
+
+}; //end
+
+// MARK: - Debugging logs
+
+ZSSEditor.logMainElementSizes = function() {
+ msg = 'Window [w:' + $(window).width() + '|h:' + $(window).height() + ']';
+ this.log(msg);
+
+ var msg = encodeURIComponent('Viewport [w:' + window.innerWidth + '|h:' + window.innerHeight + ']');
+ this.log(msg);
+
+ msg = encodeURIComponent('Body [w:' + $(document.body).width() + '|h:' + $(document.body).height() + ']');
+ this.log(msg);
+
+ msg = encodeURIComponent('HTML [w:' + $('html').width() + '|h:' + $('html').height() + ']');
+ this.log(msg);
+
+ msg = encodeURIComponent('Document [w:' + $(document).width() + '|h:' + $(document).height() + ']');
+ this.log(msg);
+};
+
+// MARK: - Viewport Refreshing
+
+ZSSEditor.refreshVisibleViewportSize = function() {
+ $(document.body).css('min-height', window.innerHeight + 'px');
+ $('#zss_field_content').css('min-height', (window.innerHeight - $('#zss_field_content').position().top) + 'px');
+};
+
+// MARK: - Fields
+
+ZSSEditor.focusFirstEditableField = function() {
+ $('div[contenteditable=true]:first').focus();
+};
+
+ZSSEditor.formatNewLine = function(e) {
+
+ var currentField = this.getFocusedField();
+
+ if (currentField.isMultiline()) {
+ var parentBlockQuoteNode = ZSSEditor.closerParentNodeWithName('blockquote');
+
+ if (parentBlockQuoteNode) {
+ this.formatNewLineInsideBlockquote(e);
+ } else if (!ZSSEditor.isCommandEnabled('insertOrderedList')
+ && !ZSSEditor.isCommandEnabled('insertUnorderedList')) {
+ document.execCommand('formatBlock', false, 'div');
+ }
+ } else {
+ e.preventDefault();
+ }
+};
+
+ZSSEditor.formatNewLineInsideBlockquote = function(e) {
+ this.insertBreakTagAtCaretPosition();
+ e.preventDefault();
+};
+
+ZSSEditor.getField = function(fieldId) {
+
+ var field = this.editableFields[fieldId];
+
+ return field;
+};
+
+ZSSEditor.moveCaretToCoords = function(x, y) {
+ if (document.caretRangeFromPoint) {
+ var range = document.caretRangeFromPoint(x, y);
+
+ var selection = window.getSelection();
+
+ if (range && selection.rangeCount) {
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+};
+
+ZSSEditor.getFocusedField = function() {
+ var currentField = $(this.findParentContenteditableDiv());
+ var currentFieldId;
+
+ if (currentField) {
+ currentFieldId = currentField.attr('id');
+ }
+
+ if (!currentFieldId) {
+ ZSSEditor.resetSelectionOnField('zss_field_content');
+ currentFieldId = 'zss_field_content';
+ }
+
+ return this.editableFields[currentFieldId];
+};
+
+ZSSEditor.execFunctionForResult = function(methodName) {
+ var functionArgument = "function=" + methodName;
+ var resultArgument = "result=" + window["ZSSEditor"][methodName].apply();
+ ZSSEditor.callback('callback-response-string', functionArgument + defaultCallbackSeparator + resultArgument);
+};
+
+// MARK: - Mutation observing
+
+/**
+ * @brief Register a node to be tracked for modifications
+ */
+ZSSEditor.trackNodeForMutation = function(target) {
+ if (ZSSEditor.mutationObserver != undefined) {
+ ZSSEditor.mutationObserver.observe(target[0], ZSSEditor.defaultMutationObserverConfig);
+ } else {
+ // The WebView doesn't support MutationObservers - fall back to DOMNodeRemoved events
+ target.bind("DOMNodeRemoved", function(event) { ZSSEditor.onDomNodeRemoved(event); });
+ }
+};
+
+/**
+ * @brief Called when the MutationObserver registers a mutation to a node it's listening to
+ */
+ZSSEditor.onMutationObserved = function(mutations) {
+ mutations.forEach(function(mutation) {
+ for (var i = 0; i < mutation.removedNodes.length; i++) {
+ var removedNode = mutation.removedNodes[i];
+ if (!removedNode.attributes) {
+ // Fix for https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/394
+ // If removedNode doesn't have an attributes property, it's not of type Node and we shouldn't proceed
+ continue;
+ }
+ if (ZSSEditor.isMediaContainerNode(removedNode)) {
+ // An uploading or failed container node was deleted manually - notify native
+ var mediaIdentifier = ZSSEditor.extractMediaIdentifier(removedNode);
+ ZSSEditor.sendMediaRemovedCallback(mediaIdentifier);
+ } else if (removedNode.attributes.getNamedItem("data-wpid")) {
+ // An uploading or failed image was deleted manually - remove its container and send the callback
+ var mediaIdentifier = removedNode.attributes.getNamedItem("data-wpid").value;
+ var parentRange = ZSSEditor.getParentRangeOfFocusedNode();
+ ZSSEditor.removeImage(mediaIdentifier);
+ if (parentRange != null) {
+ ZSSEditor.setRange(parentRange);
+ }
+ ZSSEditor.sendMediaRemovedCallback(mediaIdentifier);
+ } else if (removedNode.attributes.getNamedItem("data-video_wpid")) {
+ // An uploading or failed video was deleted manually - remove its container and send the callback
+ var mediaIdentifier = removedNode.attributes.getNamedItem("data-video_wpid").value;
+ var parentRange = ZSSEditor.getParentRangeOfFocusedNode();
+ ZSSEditor.removeVideo(mediaIdentifier);
+ if (parentRange != null) {
+ ZSSEditor.setRange(parentRange);
+ }
+ ZSSEditor.sendMediaRemovedCallback(mediaIdentifier);
+ } else if (mutation.target.className == "edit-container") {
+ // A media item wrapped in an edit container was deleted manually - remove its container
+ // No callback in this case since it's not an uploading or failed media item
+ var parentRange = ZSSEditor.getParentRangeOfFocusedNode();
+
+ mutation.target.remove();
+
+ if (parentRange != null) {
+ ZSSEditor.setRange(parentRange);
+ }
+
+ ZSSEditor.getFocusedField().emptyFieldIfNoContents();
+ }
+ }
+ });
+};
+
+/**
+ * @brief Called when a DOMNodeRemoved event is triggered for an element we're tracking
+ * (only used when MutationObserver is unsupported by the WebView)
+ */
+ZSSEditor.onDomNodeRemoved = function(event) {
+ if (event.target.id.length > 0) {
+ var mediaId = ZSSEditor.extractMediaIdentifier(event.target);
+ } else if (event.target.parentNode.id.length > 0) {
+ var mediaId = ZSSEditor.extractMediaIdentifier(event.target.parentNode);
+ } else {
+ return;
+ }
+ ZSSEditor.sendMediaRemovedCallback(mediaId);
+};
+
+// MARK: - Logging
+
+ZSSEditor.log = function(msg) {
+ ZSSEditor.callback('callback-log', 'msg=' + msg);
+};
+
+// MARK: - Callbacks
+
+ZSSEditor.domLoadedCallback = function() {
+
+ ZSSEditor.callback("callback-dom-loaded");
+};
+
+ZSSEditor.selectionChangedCallback = function () {
+
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+
+ ZSSEditor.callback('callback-selection-changed', joinedArguments);
+ this.callback("callback-input", joinedArguments);
+};
+
+ZSSEditor.callback = function(callbackScheme, callbackPath) {
+
+ var url = callbackScheme + ":";
+
+ if (callbackPath) {
+ url = url + callbackPath;
+ }
+
+ if (isUsingiOS) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else if (isUsingAndroid) {
+ if (nativeState.androidApiLevel < 17) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else {
+ nativeCallbackHandler.executeCallback(callbackScheme, callbackPath);
+ }
+ } else {
+ console.log(url);
+ }
+};
+
+/**
+ * @brief Executes a callback by loading it into an IFrame.
+ * @details The reason why we're using this instead of window.location is that window.location
+ * can sometimes fail silently when called multiple times in rapid succession.
+ * Found here:
+ * http://stackoverflow.com/questions/10010342/clicking-on-a-link-inside-a-webview-that-will-trigger-a-native-ios-screen-with/10080969#10080969
+ *
+ * @param url The callback URL.
+ */
+ZSSEditor.callbackThroughIFrame = function(url) {
+ var iframe = document.createElement("IFRAME");
+ iframe.setAttribute('sandbox', '');
+ iframe.setAttribute("src", url);
+
+ // IMPORTANT: the IFrame was showing up as a black box below our text. By setting its borders
+ // to be 0px transparent we make sure it's not shown at all.
+ //
+ // REF BUG: https://github.com/wordpress-mobile/WordPress-iOS-Editor/issues/318
+ //
+ iframe.style.cssText = "border: 0px transparent;";
+
+ document.documentElement.appendChild(iframe);
+ iframe.parentNode.removeChild(iframe);
+ iframe = null;
+};
+
+ZSSEditor.stylesCallback = function(stylesArray) {
+
+ var stylesString = '';
+
+ if (stylesArray.length > 0) {
+ stylesString = stylesArray.join(defaultCallbackSeparator);
+ }
+
+ ZSSEditor.callback("callback-selection-style", stylesString);
+};
+
+// MARK: - Selection
+
+ZSSEditor.backupRange = function(){
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return;
+ }
+ var range = selection.getRangeAt(0);
+
+ ZSSEditor.currentSelection =
+ {
+ "startContainer": range.startContainer,
+ "startOffset": range.startOffset,
+ "endContainer": range.endContainer,
+ "endOffset": range.endOffset
+ };
+};
+
+ZSSEditor.restoreRange = function(){
+ if (this.currentSelection) {
+ var selection = window.getSelection();
+ selection.removeAllRanges();
+
+ var range = document.createRange();
+ range.setStart(this.currentSelection.startContainer, this.currentSelection.startOffset);
+ range.setEnd(this.currentSelection.endContainer, this.currentSelection.endOffset);
+ selection.addRange(range);
+ }
+};
+
+ZSSEditor.resetSelectionOnField = function(fieldId, offset) {
+ var query = "div#" + fieldId;
+ var field = document.querySelector(query);
+
+ this.giveFocusToElement(field, offset);
+};
+
+ZSSEditor.giveFocusToElement = function(element, offset) {
+ offset = typeof offset !== 'undefined' ? offset : 0;
+
+ var range = document.createRange();
+ range.setStart(element, offset);
+ range.setEnd(element, offset);
+
+ var selection = document.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+};
+
+ZSSEditor.setFocusAfterElement = function(element) {
+ var selection = window.getSelection();
+
+ if (selection.rangeCount) {
+ var range = document.createRange();
+
+ range.setStartAfter(element);
+ range.setEndAfter(element);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+};
+
+ZSSEditor.getSelectedText = function() {
+ var selection = window.getSelection();
+ return selection.toString();
+};
+
+ZSSEditor.selectWordAroundCursor = function() {
+ var selection = window.getSelection();
+ // If there is no text selected, try to expand it to the word under the cursor
+ if (selection.rangeCount == 1) {
+ var range = selection.getRangeAt(0);
+ if (range.startOffset == range.endOffset) {
+ while (ZSSEditor.canExpandBackward(range)) {
+ range.setStart(range.startContainer, range.startOffset - 1);
+ }
+ while (ZSSEditor.canExpandForward(range)) {
+ range.setEnd(range.endContainer, range.endOffset + 1);
+ }
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+ return selection;
+};
+
+ZSSEditor.canExpandBackward = function(range) {
+ // Can't expand if focus is not a text node
+ if (!range.endContainer.nodeType == 3) {
+ return false;
+ }
+ var caretRange = range.cloneRange();
+ if (range.startOffset == 0) {
+ return false;
+ }
+ caretRange.setStart(range.startContainer, range.startOffset - 1);
+ caretRange.setEnd(range.startContainer, range.startOffset);
+ if (!caretRange.toString().match(/\w/)) {
+ return false;
+ }
+ return true;
+};
+
+ZSSEditor.canExpandForward = function(range) {
+ // Can't expand if focus is not a text node
+ if (!range.endContainer.nodeType == 3) {
+ return false;
+ }
+ var caretRange = range.cloneRange();
+ if (range.endOffset == range.endContainer.length) {
+ return false;
+ }
+ caretRange.setStart(range.endContainer, range.endOffset);
+ if (range.endOffset) {
+ caretRange.setEnd(range.endContainer, range.endOffset + 1);
+ }
+ if (!caretRange.toString().match(/\w/)) {
+ return false;
+ }
+ return true;
+};
+
+ZSSEditor.getSelectedTextToLinkify = function() {
+ ZSSEditor.selectWordAroundCursor();
+ return document.getSelection().toString();
+};
+
+ZSSEditor.getCaretArguments = function() {
+ var caretInfo = this.getYCaretInfo();
+
+ if (caretInfo == null) {
+ return null;
+ } else {
+ this.caretArguments[0] = 'yOffset=' + caretInfo.y;
+ this.caretArguments[1] = 'height=' + caretInfo.height;
+ return this.caretArguments;
+ }
+};
+
+ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments = function() {
+ var joinedArguments = ZSSEditor.getJoinedCaretArguments();
+ var idArgument = "id=" + ZSSEditor.getFocusedField().getNodeId();
+
+ joinedArguments = idArgument + defaultCallbackSeparator + joinedArguments;
+
+ return joinedArguments;
+};
+
+ZSSEditor.getJoinedCaretArguments = function() {
+
+ var caretArguments = this.getCaretArguments();
+ var joinedArguments = this.caretArguments.join(defaultCallbackSeparator);
+
+ return joinedArguments;
+};
+
+ZSSEditor.getCaretYPosition = function() {
+ var selection = window.getSelection();
+ if (selection.rangeCount == 0) {
+ return 0;
+ }
+ var range = selection.getRangeAt(0);
+ var span = document.createElement("span");
+ // Ensure span has dimensions and position by
+ // adding a zero-width space character
+ span.appendChild( document.createTextNode("\u200b") );
+ range.insertNode(span);
+ var y = span.offsetTop;
+ var spanParent = span.parentNode;
+ spanParent.removeChild(span);
+
+ // Glue any broken text nodes back together
+ spanParent.normalize();
+
+ return y;
+}
+
+ZSSEditor.getYCaretInfo = function() {
+ var selection = window.getSelection();
+ var noSelectionAvailable = selection.rangeCount == 0;
+
+ if (noSelectionAvailable) {
+ return null;
+ }
+
+ var y = 0;
+ var height = 0;
+ var range = selection.getRangeAt(0);
+ var needsToWorkAroundNewlineBug = (range.getClientRects().length == 0);
+
+ // PROBLEM: iOS seems to have problems getting the offset for some empty nodes and return
+ // 0 (zero) as the selection range top offset.
+ //
+ // WORKAROUND: To fix this problem we use a different method to obtain the Y position instead.
+ //
+ if (needsToWorkAroundNewlineBug) {
+ var closerParentNode = ZSSEditor.closerParentNode();
+ var closerDiv = ZSSEditor.findParentContenteditableDiv();
+
+ var fontSize = $(closerParentNode).css('font-size');
+ var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
+
+ y = this.getCaretYPosition();
+ height = lineHeight;
+ } else {
+ if (range.getClientRects) {
+ var rects = range.getClientRects();
+ if (rects.length > 0) {
+ // PROBLEM: some iOS versions differ in what is returned by getClientRects()
+ // Some versions return the offset from the page's top, some other return the
+ // offset from the visible viewport's top.
+ //
+ // WORKAROUND: see if the offset of the body's top is ever negative. If it is
+ // then it means that the offset we have is relative to the body's top, and we
+ // should add the scroll offset.
+ //
+ var addsScrollOffset = document.body.getClientRects()[0].top < 0;
+
+ if (addsScrollOffset) {
+ y = document.body.scrollTop;
+ }
+
+ y += rects[0].top;
+ height = rects[0].height;
+ }
+ }
+ }
+
+ this.caretInfo.y = y;
+ this.caretInfo.height = height;
+
+ return this.caretInfo;
+};
+
+// MARK: - Styles
+
+ZSSEditor.setBold = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('bold', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setItalic = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('italic', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setSubscript = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('subscript', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setSuperscript = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('superscript', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setStrikeThrough = function() {
+ ZSSEditor.selectWordAroundCursor();
+ var commandName = 'strikeThrough';
+ var isDisablingStrikeThrough = ZSSEditor.isCommandEnabled(commandName);
+
+ document.execCommand(commandName, false, null);
+
+ // DRM: WebKit has a problem disabling strikeThrough when the tag <del> is used instead of
+ // <strike>. The code below serves as a way to fix this issue.
+ //
+ var mustHandleWebKitIssue = (isDisablingStrikeThrough
+ && ZSSEditor.isCommandEnabled(commandName));
+
+ if (mustHandleWebKitIssue && window.getSelection().rangeCount > 0) {
+ var troublesomeNodeNames = ['del'];
+
+ var selection = window.getSelection();
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var container = range.commonAncestorContainer;
+ var nodeFound = false;
+ var textNode = null;
+
+ while (container && !nodeFound) {
+ nodeFound = (container
+ && container.nodeType == document.ELEMENT_NODE
+ && troublesomeNodeNames.indexOf(container.nodeName.toLowerCase()) > -1);
+
+ if (!nodeFound) {
+ container = container.parentElement;
+ }
+ }
+
+ if (container) {
+ var newObject = $(container).replaceWith(container.innerHTML);
+
+ var finalSelection = window.getSelection();
+ var finalRange = selection.getRangeAt(0).cloneRange();
+
+ finalRange.setEnd(finalRange.startContainer, finalRange.startOffset + 1);
+
+ selection.removeAllRanges();
+ selection.addRange(finalRange);
+ }
+ }
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setUnderline = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('underline', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+/**
+ * @brief Turns blockquote ON or OFF for the current selection.
+ * @details This method makes sure that the contents of the blockquotes are surrounded by the
+ * defaultParagraphSeparator tag (by default '<p>'). This ensures parity with the web
+ * editor.
+ */
+ZSSEditor.setBlockquote = function() {
+
+ var savedSelection = rangy.saveSelection();
+ var selection = document.getSelection();
+ var range = selection.getRangeAt(0).cloneRange();
+ var sendStyles = false;
+
+ // Make sure text being wrapped in blockquotes is inside paragraph tags
+ // (should be <blockquote><paragraph>contents</paragraph></blockquote>)
+ var currentHtml = ZSSEditor.focusedField.getWrappedDomNode().innerHTML;
+ if (currentHtml.search('<' + ZSSEditor.defaultParagraphSeparator) == -1) {
+ ZSSEditor.focusedField.setHTML(Util.wrapHTMLInTag(currentHtml, ZSSEditor.defaultParagraphSeparator));
+ }
+
+ var ancestorElement = this.getAncestorElementForSettingBlockquote(range);
+
+ if (ancestorElement) {
+ sendStyles = true;
+
+ var childNodes = this.getChildNodesIntersectingRange(ancestorElement, range);
+
+ // On older APIs, the rangy selection node is targeted when turning off empty blockquotes at the start of a post
+ // In that case, add the empty DIV element next to the rangy selection to the childNodes array to correctly
+ // turn the blockquote off
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/401
+ var nextChildNode = childNodes[childNodes.length-1].nextSibling;
+ if (nextChildNode && nextChildNode.nodeName == NodeName.DIV && nextChildNode.innerHTML == "") {
+ childNodes.push(nextChildNode);
+ }
+
+ if (childNodes && childNodes.length) {
+ this.toggleBlockquoteForSpecificChildNodes(ancestorElement, childNodes);
+ }
+ }
+
+ rangy.restoreSelection(savedSelection);
+
+ // When turning off an empty blockquote in an empty post, ensure there aren't any leftover empty paragraph tags
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/401
+ var currentContenteditableDiv = ZSSEditor.focusedField.getWrappedDomNode();
+ if (currentContenteditableDiv.children.length == 1 && currentContenteditableDiv.firstChild.innerHTML == "") {
+ ZSSEditor.focusedField.emptyFieldIfNoContents();
+ }
+
+ if (sendStyles) {
+ ZSSEditor.sendEnabledStyles();
+ }
+};
+
+ZSSEditor.removeFormating = function() {
+ document.execCommand('removeFormat', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setHorizontalRule = function() {
+ document.execCommand('insertHorizontalRule', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setHeading = function(heading) {
+ var formatTag = heading;
+ var formatBlock = document.queryCommandValue('formatBlock');
+
+ if (formatBlock.length > 0 && formatBlock.toLowerCase() == formatTag) {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(this.defaultParagraphSeparator));
+ } else {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(formatTag));
+ }
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setParagraph = function() {
+ var formatTag = "div";
+ var formatBlock = document.queryCommandValue('formatBlock');
+
+ if (formatBlock.length > 0 && formatBlock.toLowerCase() == formatTag) {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(this.defaultParagraphSeparator));
+ } else {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(formatTag));
+ }
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.undo = function() {
+ document.execCommand('undo', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.redo = function() {
+ document.execCommand('redo', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setOrderedList = function() {
+ document.execCommand('insertOrderedList', false, null);
+
+ // If the insertOrderedList is no longer enabled after running execCommand,
+ // we can assume the user is turning it off.
+ if (!ZSSEditor.isCommandEnabled('insertOrderedList')) {
+ ZSSEditor.completeListEditing();
+ }
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setUnorderedList = function() {
+ document.execCommand('insertUnorderedList', false, null);
+
+ // If the insertUnorderedList is no longer enabled after running execCommand,
+ // we can assume the user is turning it off.
+ if (!ZSSEditor.isCommandEnabled('insertUnorderedList')) {
+ ZSSEditor.completeListEditing();
+ }
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyCenter = function() {
+ document.execCommand('justifyCenter', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyFull = function() {
+ document.execCommand('justifyFull', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyLeft = function() {
+ document.execCommand('justifyLeft', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyRight = function() {
+ document.execCommand('justifyRight', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setIndent = function() {
+ document.execCommand('indent', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setOutdent = function() {
+ document.execCommand('outdent', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setTextColor = function(color) {
+ ZSSEditor.selectWordAroundCursor();
+ ZSSEditor.restoreRange();
+ document.execCommand("styleWithCSS", null, true);
+ document.execCommand('foreColor', false, color);
+ document.execCommand("styleWithCSS", null, false);
+ ZSSEditor.sendEnabledStyles();
+ // document.execCommand("removeFormat", false, "foreColor"); // Removes just foreColor
+};
+
+ZSSEditor.setBackgroundColor = function(color) {
+ ZSSEditor.selectWordAroundCursor();
+ ZSSEditor.restoreRange();
+ document.execCommand("styleWithCSS", null, true);
+ document.execCommand('hiliteColor', false, color);
+ document.execCommand("styleWithCSS", null, false);
+ ZSSEditor.sendEnabledStyles();
+};
+
+/**
+ * @brief Wraps given HTML in paragraph tags, appends a new line, and inserts it into the field
+ * @details This method makes sure that passed HTML is wrapped in a separate paragraph.
+ * It also appends a new opening paragraph tag and a space. This step is necessary to keep any spans or
+ * divs in the HTML from being read by the WebView as a style and applied to all future paragraphs.
+ */
+ZSSEditor.insertHTMLWrappedInParagraphTags = function(html) {
+ var space = '<br>';
+ var paragraphOpenTag = Util.buildOpeningTag(this.defaultParagraphSeparator);
+ var paragraphCloseTag = Util.buildClosingTag(this.defaultParagraphSeparator);
+
+ if (this.getFocusedField().getHTML().length == 0) {
+ html = paragraphOpenTag + html;
+ }
+
+ // Without this line, API<19 WebView will reset the caret to the start of the document, inserting the new line
+ // there instead of under the newly added media item
+ if (nativeState.androidApiLevel < 19) {
+ html = html + '&#x200b;';
+ }
+
+ // Due to the way the WebView handles divs, we need to add a new paragraph in a separate insertion - otherwise,
+ // the new paragraph will be nested within the existing paragraph.
+ this.insertHTML(html);
+
+ this.insertHTML(paragraphOpenTag + space + paragraphCloseTag);
+};
+
+ZSSEditor.insertLink = function(url, title) {
+ var html = '<a href="' + url + '">' + title + "</a>";
+
+ var parentBlockQuoteNode = ZSSEditor.closerParentNodeWithName('blockquote');
+
+ var currentRange = document.getSelection().getRangeAt(0);
+ var currentNode = currentRange.startContainer;
+ var currentNodeIsEmpty = (currentNode.innerHTML == '' || currentNode.innerHTML == '<br>');
+
+ var selectionIsAtStartOrEnd = Util.rangeIsAtStartOfParent(currentRange) || Util.rangeIsAtEndOfParent(currentRange);
+
+ if (this.getFocusedField().getHTML().length == 0
+ || (parentBlockQuoteNode && !currentNodeIsEmpty && selectionIsAtStartOrEnd)) {
+ // Wrap the link tag in paragraph tags when the post is empty, and also when inside a blockquote
+ // The latter is to fix a bug with document.execCommand('insertHTML') inside a blockquote, where the div inside
+ // the blockquote is ignored and the link tag is inserted outside it, on a new line with no wrapping div
+ // Wrapping the link in paragraph tags makes insertHTML join it to the existing div, for some reason
+ // We exclude being on an empty line inside a blockquote and when the selection isn't at the beginning or end
+ // of the line, as the fix is unnecessary in both those cases and causes paragraph formatting issues
+ html = Util.buildOpeningTag(this.defaultParagraphSeparator) + html;
+ }
+
+ this.insertHTML(html);
+};
+
+ZSSEditor.updateLink = function(url, title) {
+
+ ZSSEditor.restoreRange();
+
+ var currentLinkNode = ZSSEditor.lastTappedNode;
+
+ if (currentLinkNode) {
+ currentLinkNode.setAttribute("href", url);
+ currentLinkNode.innerHTML = title;
+ }
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.unlink = function() {
+ var savedSelection = rangy.saveSelection();
+
+ var currentLinkNode = ZSSEditor.closerParentNodeWithName('a');
+
+ if (currentLinkNode) {
+ ZSSEditor.unwrapNode(currentLinkNode);
+ }
+
+ rangy.restoreSelection(savedSelection);
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.unwrapNode = function(node) {
+ $(node).contents().unwrap();
+};
+
+ZSSEditor.quickLink = function() {
+
+ var sel = document.getSelection();
+ var link_url = "";
+ var test = new String(sel);
+ var mailregexp = new RegExp("^(.+)(\@)(.+)$", "gi");
+ if (test.search(mailregexp) == -1) {
+ checkhttplink = new RegExp("^http\:\/\/", "gi");
+ if (test.search(checkhttplink) == -1) {
+ checkanchorlink = new RegExp("^\#", "gi");
+ if (test.search(checkanchorlink) == -1) {
+ link_url = "http://" + sel;
+ } else {
+ link_url = sel;
+ }
+ } else {
+ link_url = sel;
+ }
+ } else {
+ checkmaillink = new RegExp("^mailto\:", "gi");
+ if (test.search(checkmaillink) == -1) {
+ link_url = "mailto:" + sel;
+ } else {
+ link_url = sel;
+ }
+ }
+
+ var html_code = '<a href="' + link_url + '">' + sel + '</a>';
+ ZSSEditor.insertHTML(html_code);
+};
+
+// MARK: - Blockquotes
+
+/**
+ * @brief This method toggles blockquote for the specified child nodes. This is useful since
+ * we can toggle blockquote either for some or ALL of the child nodes, depending on
+ * what we need to achieve.
+ * @details CASE 1: If the parent node is a blockquote node, the child nodes will be extracted
+ * from it leaving the remaining siblings untouched (by splitting the parent blockquote
+ * node in two if necessary).
+ * CASE 2: If the parent node is NOT a blockquote node, but the first child is, the
+ * method will make sure all child nodes that are blockquote nodes will be toggled to
+ * non-blockquote nodes.
+ * CASE 3: If both the parent node and the first node are non-blockquote nodes, this
+ * method will turn all child nodes into blockquote nodes.
+ *
+ * @param parentNode The parent node. Can be either a blockquote or non-blockquote node.
+ * Cannot be null.
+ * @param nodes The child nodes. Can be any combination of blockquote and
+ * non-blockquote nodes. Cannot be null.
+ */
+ZSSEditor.toggleBlockquoteForSpecificChildNodes = function(parentNode, nodes) {
+
+ if (nodes && nodes.length > 0) {
+ if (parentNode.nodeName == NodeName.BLOCKQUOTE) {
+ for (var counter = 0; counter < nodes.length; counter++) {
+ this.turnBlockquoteOffForNode(nodes[counter]);
+ }
+ } else {
+
+ var turnOn = (nodes[0].nodeName != NodeName.BLOCKQUOTE);
+
+ for (var counter = 0; counter < nodes.length; counter++) {
+ if (turnOn) {
+ this.turnBlockquoteOnForNode(nodes[counter]);
+ } else {
+ this.turnBlockquoteOffForNode(nodes[counter]);
+ }
+ }
+ }
+ }
+};
+
+
+/**
+ * @brief Turns blockquote off for the specified node.
+ *
+ * @param node The node to turn the blockquote off for. It can either be a blockquote
+ * node (in which case it will be removed and all child nodes extracted) or
+ * have a parent blockquote node (in which case the node will be extracted
+ * from its parent).
+ */
+ZSSEditor.turnBlockquoteOffForNode = function(node) {
+
+ if (node.nodeName == NodeName.BLOCKQUOTE) {
+ for (var i = 0; i < node.childNodes.length; i++) {
+ this.extractNodeFromAncestorNode(node.childNodes[i], node);
+ }
+ } else {
+ if (node.parentNode.nodeName == NodeName.BLOCKQUOTE) {
+ this.extractNodeFromAncestorNode(node, node.parentNode);
+ }
+ }
+};
+
+/**
+ * @brief Turns blockquote on for the specified node.
+ *
+ * @param node The node to turn blockquote on for. Will attempt to attach the newly
+ * created blockquote to sibling or uncle blockquote nodes. If the node is
+ * null or it's parent is null, this method will exit without affecting it
+ * (this can actually be caused by this method modifying the surrounding
+ * nodes, if those nodes are stored in an array - and thus are not notified
+ * of DOM hierarchy changes).
+ */
+ZSSEditor.turnBlockquoteOnForNode = function(node) {
+
+ if (!node || !node.parentNode) {
+ return;
+ }
+
+ var couldJoinBlockquotes = this.joinAdjacentSiblingsOrAncestorBlockquotes(node);
+
+ if (!couldJoinBlockquotes) {
+ var blockquote = document.createElement(NodeName.BLOCKQUOTE);
+
+ node.parentNode.insertBefore(blockquote, node);
+ blockquote.appendChild(node);
+ }
+};
+
+// MARK: - Generic media
+
+ZSSEditor.isMediaContainerNode = function(node) {
+ if (node.id === undefined) {
+ return false;
+ }
+ return (node.id.search("img_container_") == 0) || (node.id.search("video_container_") == 0);
+};
+
+ZSSEditor.extractMediaIdentifier = function(node) {
+ if (node.id.search("img_container_") == 0) {
+ return node.id.replace("img_container_", "");
+ } else if (node.id.search("video_container_") == 0) {
+ return node.id.replace("video_container_", "");
+ }
+ return "";
+};
+
+ZSSEditor.getMediaNodeWithIdentifier = function(mediaNodeIdentifier) {
+ var imageNode = ZSSEditor.getImageNodeWithIdentifier(mediaNodeIdentifier);
+ if (imageNode.length > 0) {
+ return imageNode;
+ } else {
+ return ZSSEditor.getVideoNodeWithIdentifier(mediaNodeIdentifier);
+ }
+};
+
+ZSSEditor.getMediaProgressNodeWithIdentifier = function(mediaNodeIdentifier) {
+ var imageProgressNode = ZSSEditor.getImageProgressNodeWithIdentifier(mediaNodeIdentifier);
+ if (imageProgressNode.length > 0) {
+ return imageProgressNode;
+ } else {
+ return ZSSEditor.getVideoProgressNodeWithIdentifier(mediaNodeIdentifier);
+ }
+};
+
+ZSSEditor.getMediaContainerNodeWithIdentifier = function(mediaNodeIdentifier) {
+ var imageContainerNode = ZSSEditor.getImageContainerNodeWithIdentifier(mediaNodeIdentifier);
+ if (imageContainerNode.length > 0) {
+ return imageContainerNode;
+ } else {
+ return ZSSEditor.getVideoContainerNodeWithIdentifier(mediaNodeIdentifier);
+ }
+};
+
+/**
+ * @brief Update the progress indicator for the media item identified with the value in progress.
+ *
+ * @param mediaNodeIdentifier This is a unique ID provided by the caller.
+ * @param progress A value between 0 and 1 indicating the progress on the media upload.
+ */
+ZSSEditor.setProgressOnMedia = function(mediaNodeIdentifier, progress) {
+ var mediaNode = this.getMediaNodeWithIdentifier(mediaNodeIdentifier);
+ var mediaProgressNode = this.getMediaProgressNodeWithIdentifier(mediaNodeIdentifier);
+
+ if (progress == 0) {
+ mediaNode.addClass("uploading");
+ }
+
+ // Don't allow the progress bar to move backward
+ if (mediaNode.length == 0 || mediaProgressNode.length == 0 || mediaProgressNode.attr("value") > progress) {
+ return;
+ }
+
+ // Revert to non-compatibility image container once image upload has begun. This centers the overlays on the image
+ // (instead of the screen), while still circumventing the small container bug the compat class was added to fix
+ if (progress > 0) {
+ this.getMediaContainerNodeWithIdentifier(mediaNodeIdentifier).removeClass("compat");
+ }
+
+ // Sometimes the progress bar can be stuck at 100% for a long time while further processing happens
+ // From a UX perspective, it's better to just keep the progress bars at 90% until the upload is really complete
+ // and the progress bar is removed entirely
+ if (progress > 0.9) {
+ return;
+ }
+
+ mediaProgressNode.attr("value", progress);
+};
+
+ZSSEditor.setupOptimisticProgressUpdate = function(mediaNodeIdentifier, nCall) {
+ setTimeout(ZSSEditor.sendOptimisticProgressUpdate, nCall * 100, mediaNodeIdentifier, nCall);
+};
+
+ZSSEditor.sendOptimisticProgressUpdate = function(mediaNodeIdentifier, nCall) {
+ if (nCall > 15) {
+ return;
+ }
+
+ var mediaNode = ZSSEditor.getMediaNodeWithIdentifier(mediaNodeIdentifier);
+
+ // Don't send progress updates to failed media
+ if (mediaNode.length != 0 && mediaNode[0].classList.contains("failed")) {
+ return;
+ }
+
+ ZSSEditor.setProgressOnMedia(mediaNodeIdentifier, nCall / 100);
+ ZSSEditor.setupOptimisticProgressUpdate(mediaNodeIdentifier, nCall + 1);
+};
+
+ZSSEditor.removeAllFailedMediaUploads = function() {
+ console.log("Remove all failed media");
+ var failedMediaArray = ZSSEditor.getFailedMediaIdArray();
+ for (var i = 0; i < failedMediaArray.length; i++) {
+ ZSSEditor.removeMedia(failedMediaArray[i]);
+ }
+};
+
+ZSSEditor.removeMedia = function(mediaNodeIdentifier) {
+ if (this.getImageNodeWithIdentifier(mediaNodeIdentifier).length != 0) {
+ this.removeImage(mediaNodeIdentifier);
+ } else if (this.getVideoNodeWithIdentifier(mediaNodeIdentifier).length != 0) {
+ this.removeVideo(mediaNodeIdentifier);
+ }
+};
+
+ZSSEditor.sendMediaRemovedCallback = function(mediaNodeIdentifier) {
+ var arguments = ['id=' + encodeURIComponent(mediaNodeIdentifier)];
+ var joinedArguments = arguments.join(defaultCallbackSeparator);
+ this.callback("callback-media-removed", joinedArguments);
+};
+
+/**
+ * @brief Marks all in-progress images as failed to upload
+ */
+ZSSEditor.markAllUploadingMediaAsFailed = function(message) {
+ var html = ZSSEditor.getField("zss_field_content").getHTML();
+ var tmp = document.createElement( "div" );
+ var tmpDom = $( tmp ).html( html );
+ var matches = tmpDom.find("img.uploading");
+
+ for(var i = 0; i < matches.size(); i++) {
+ if (matches[i].hasAttribute('data-wpid')) {
+ var mediaId = matches[i].getAttribute('data-wpid');
+ ZSSEditor.markImageUploadFailed(mediaId, message);
+ } else if (matches[i].hasAttribute('data-video_wpid')) {
+ var videoId = matches[i].getAttribute('data-video_wpid');
+ ZSSEditor.markVideoUploadFailed(videoId, message);
+ }
+ }
+};
+
+ZSSEditor.getFailedMediaIdArray = function() {
+ var html = ZSSEditor.getField("zss_field_content").getHTML();
+ var tmp = document.createElement( "div" );
+ var tmpDom = $( tmp ).html( html );
+ var matches = tmpDom.find("img.failed");
+
+ var mediaIdArray = [];
+
+ for (var i = 0; i < matches.size(); i++) {
+ var mediaId = null;
+ if (matches[i].hasAttribute("data-wpid")) {
+ mediaId = matches[i].getAttribute("data-wpid");
+ } else if (matches[i].hasAttribute("data-video_wpid")) {
+ mediaId = matches[i].getAttribute("data-video_wpid");
+ }
+ if (mediaId !== null) {
+ mediaIdArray.push(mediaId);
+ }
+ }
+ return mediaIdArray;
+};
+
+/**
+ * @brief Sends a callback with a list of failed images
+ */
+ZSSEditor.getFailedMedia = function() {
+ var mediaIdArray = ZSSEditor.getFailedMediaIdArray();
+ for (var i = 0; i < mediaIdArray.length; i++) {
+ // Track pre-existing failed media nodes for manual deletion events
+ ZSSEditor.trackNodeForMutation(this.getMediaContainerNodeWithIdentifier(mediaIdArray[i]));
+ }
+
+ var functionArgument = "function=getFailedMedia";
+ var joinedArguments = functionArgument + defaultCallbackSeparator + "ids=" + mediaIdArray.toString();
+ ZSSEditor.callback('callback-response-string', joinedArguments);
+};
+
+// MARK: - Images
+
+ZSSEditor.updateImage = function(url, alt) {
+
+ ZSSEditor.restoreRange();
+
+ if (ZSSEditor.currentEditingImage) {
+ var c = ZSSEditor.currentEditingImage;
+ c.attr('src', url);
+ c.attr('alt', alt);
+ }
+ ZSSEditor.sendEnabledStyles();
+
+};
+
+ZSSEditor.insertImage = function(url, remoteId, alt) {
+ var html = '<img src="' + url + '" class="wp-image-' + remoteId + ' alignnone size-full';
+ if (alt) {
+ html += '" alt="' + alt;
+ }
+ html += '"/>';
+
+ this.insertHTMLWrappedInParagraphTags(html);
+
+ this.sendEnabledStyles();
+ this.callback("callback-action-finished");
+};
+
+/**
+ * @brief Inserts a local image URL. Useful for images that need to be uploaded.
+ * @details By inserting a local image URL, we can make sure the image is shown to the user
+ * as soon as it's selected for uploading. Once the image is successfully uploaded
+ * the application should call replaceLocalImageWithRemoteImage().
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller. It exists as
+ * a mechanism to update the image node with the remote URL
+ * when replaceLocalImageWithRemoteImage() is called.
+ * @param localImageUrl The URL of the local image to display. Please keep in mind
+ * that a remote URL can be used here too, since this method
+ * does not check for that. It would be a mistake.
+ */
+ZSSEditor.insertLocalImage = function(imageNodeIdentifier, localImageUrl) {
+ var progressIdentifier = this.getImageProgressIdentifier(imageNodeIdentifier);
+ var imageContainerIdentifier = this.getImageContainerIdentifier(imageNodeIdentifier);
+
+ if (nativeState.androidApiLevel > 18) {
+ var imgContainerClass = 'img_container';
+ var progressElement = '<progress id="' + progressIdentifier + '" value=0 class="wp_media_indicator" contenteditable="false"></progress>';
+ } else {
+ // Before API 19, the WebView didn't support progress tags. Use an upload overlay instead of a progress bar
+ var imgContainerClass = 'img_container compat';
+ var progressElement = '<span class="upload-overlay" contenteditable="false">' + nativeState.localizedStringUploading
+ + '</span><span class="upload-overlay-bg"></span>';
+ }
+
+ var imgContainerStart = '<span id="' + imageContainerIdentifier + '" class="' + imgContainerClass
+ + '" contenteditable="false">';
+ var imgContainerEnd = '</span>';
+ var image = '<img data-wpid="' + imageNodeIdentifier + '" src="' + localImageUrl + '" alt="" />';
+ var html = imgContainerStart + progressElement + image + imgContainerEnd;
+
+ this.insertHTMLWrappedInParagraphTags(html);
+
+ ZSSEditor.trackNodeForMutation(this.getImageContainerNodeWithIdentifier(imageNodeIdentifier));
+
+ this.setProgressOnMedia(imageNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, imageNodeIdentifier, 1);
+ }
+
+ this.sendEnabledStyles();
+};
+
+ZSSEditor.getImageNodeWithIdentifier = function(imageNodeIdentifier) {
+ return $('img[data-wpid="' + imageNodeIdentifier+'"]');
+};
+
+ZSSEditor.getImageProgressIdentifier = function(imageNodeIdentifier) {
+ return 'progress_' + imageNodeIdentifier;
+};
+
+ZSSEditor.getImageProgressNodeWithIdentifier = function(imageNodeIdentifier) {
+ return $('#'+this.getImageProgressIdentifier(imageNodeIdentifier));
+};
+
+ZSSEditor.getImageContainerIdentifier = function(imageNodeIdentifier) {
+ return 'img_container_' + imageNodeIdentifier;
+};
+
+ZSSEditor.getImageContainerNodeWithIdentifier = function(imageNodeIdentifier) {
+ return $('#'+this.getImageContainerIdentifier(imageNodeIdentifier));
+};
+
+/**
+ * @brief Replaces a local image URL with a remote image URL. Useful for images that have
+ * just finished uploading.
+ * @details The remote image can be available after a while, when uploading images. This method
+ * allows for the remote URL to be loaded once the upload completes.
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller. It exists as
+ * a mechanism to update the image node with the remote URL
+ * when replaceLocalImageWithRemoteImage() is called.
+ * @param remoteImageUrl The URL of the remote image to display.
+ */
+ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remoteImageId, remoteImageUrl) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+
+ if (imageNode.length == 0) {
+ // even if the image is not present anymore we must do callback
+ this.markImageUploadDone(imageNodeIdentifier);
+ return;
+ }
+
+ var image = new Image;
+
+ image.onload = function () {
+ ZSSEditor.finishLocalImageSwap(image, imageNode, imageNodeIdentifier, remoteImageId)
+ image.classList.add("image-loaded");
+ console.log("Image Loaded!");
+ }
+
+ image.onerror = function () {
+ // Add a remoteUrl attribute, remoteUrl and src must be swapped before publishing.
+ image.setAttribute('remoteurl', image.src);
+ // Try to reload the image on error.
+ ZSSEditor.tryToReload(image, imageNode, imageNodeIdentifier, remoteImageId, 1);
+ }
+
+ image.src = remoteImageUrl;
+};
+
+ZSSEditor.finishLocalImageSwap = function(image, imageNode, imageNodeIdentifier, remoteImageId) {
+ imageNode.addClass("wp-image-" + remoteImageId);
+ if (image.getAttribute("remoteurl")) {
+ imageNode.attr('remoteurl', image.getAttribute("remoteurl"));
+ }
+ imageNode.attr('src', image.src);
+ // Set extra attributes and classes used by WordPress
+ imageNode.attr({'width': image.width, 'height': image.height});
+ imageNode.addClass("alignnone size-full");
+ ZSSEditor.markImageUploadDone(imageNodeIdentifier);
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+ ZSSEditor.callback("callback-input", joinedArguments);
+ image.onerror = null;
+}
+
+ZSSEditor.reloadImage = function(image, imageNode, imageNodeIdentifier, remoteImageId, nCall) {
+ if (image.classList.contains("image-loaded")) {
+ return;
+ }
+ image.onerror = ZSSEditor.tryToReload(image, imageNode, imageNodeIdentifier, remoteImageId, nCall + 1);
+ // Force reloading by updating image src
+ image.src = image.getAttribute("remoteurl") + "?retry=" + nCall;
+ console.log("Reloading image:" + nCall + " - " + image.src);
+}
+
+ZSSEditor.tryToReload = function (image, imageNode, imageNodeIdentifier, remoteImageId, nCall) {
+ if (nCall > 8) { // 7 tries: 22500 ms total
+ ZSSEditor.finishLocalImageSwap(image, imageNode, imageNodeIdentifier, remoteImageId);
+ return;
+ }
+ image.onerror = null;
+ console.log("Image not loaded");
+ // reload the image with a variable delay: 500ms, 1000ms, 1500ms, 2000ms, etc.
+ setTimeout(ZSSEditor.reloadImage, nCall * 500, image, imageNode, imageNodeIdentifier, remoteImageId, nCall);
+}
+
+/**
+ * @brief Notifies that the image upload as finished
+ *
+ * @param imageNodeIdentifier The unique image ID for the uploaded image
+ */
+ZSSEditor.markImageUploadDone = function(imageNodeIdentifier) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length == 0){
+ return;
+ }
+
+ // remove identifier attributed from image
+ imageNode.removeAttr('data-wpid');
+
+ // remove uploading style
+ imageNode.removeClass("uploading");
+
+ // Remove all extra formatting nodes for progress
+ if (imageNode.parent().attr("id") == this.getImageContainerIdentifier(imageNodeIdentifier)) {
+ // Reset id before removal to avoid triggering the manual media removal callback
+ imageNode.parent().attr("id", "");
+ imageNode.parent().replaceWith(imageNode);
+ }
+ // Wrap link around image
+ var link = $('<a>', { href: imageNode.attr("src") } );
+ imageNode.wrap(link);
+ // We invoke the sendImageReplacedCallback with a delay to avoid for
+ // it to be ignored by the webview because of the previous callback being done.
+ var thisObj = this;
+ setTimeout(function() { thisObj.sendImageReplacedCallback(imageNodeIdentifier);}, 500);
+};
+
+/**
+ * @brief Callbacks to native that the image upload as finished and the local url was replaced by the remote url
+ *
+ * @param imageNodeIdentifier The unique image ID for the uploaded image
+ */
+ZSSEditor.sendImageReplacedCallback = function( imageNodeIdentifier ) {
+ var arguments = ['id=' + encodeURIComponent( imageNodeIdentifier )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ this.callback("callback-image-replaced", joinedArguments);
+};
+
+/**
+ * @brief Marks the image as failed to upload
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller.
+ * @param message A message to show to the user, overlayed on the image
+ */
+ZSSEditor.markImageUploadFailed = function(imageNodeIdentifier, message) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length == 0){
+ return;
+ }
+
+ var sizeClass = '';
+ if ( imageNode[0].width > 480 && imageNode[0].height > 240 ) {
+ sizeClass = "largeFail";
+ } else if ( imageNode[0].width < 100 || imageNode[0].height < 100 ) {
+ sizeClass = "smallFail";
+ }
+
+ imageNode.addClass('failed');
+
+ var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier);
+ if(imageContainerNode.length != 0){
+ imageContainerNode.attr("data-failed", message);
+ imageNode.removeClass("uploading");
+ imageContainerNode.addClass('failed');
+ imageContainerNode.addClass(sizeClass);
+ }
+
+ var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier);
+ if (imageProgressNode.length != 0){
+ imageProgressNode.addClass('failed');
+ imageProgressNode.attr("value", 0);
+ }
+
+ // Delete the compatibility overlay if present
+ imageContainerNode.find("span.upload-overlay").addClass("failed");
+};
+
+/**
+ * @brief Unmarks the image as failed to upload
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.unmarkImageUploadFailed = function(imageNodeIdentifier) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length != 0){
+ imageNode.removeClass('failed');
+ }
+
+ var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier);
+ if(imageContainerNode.length != 0){
+ imageContainerNode.removeAttr("data-failed");
+ imageContainerNode.removeClass('failed');
+ }
+
+ var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier);
+ if (imageProgressNode.length != 0){
+ imageProgressNode.removeClass('failed');
+ }
+
+ // Display the compatibility overlay again if present
+ imageContainerNode.find("span.upload-overlay").removeClass("failed");
+
+ this.setProgressOnMedia(imageNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, imageNodeIdentifier, 1);
+ }
+};
+
+/**
+ * @brief Remove the image from the DOM.
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.removeImage = function(imageNodeIdentifier) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length != 0){
+ // Reset id before removal to avoid triggering the manual media removal callback
+ imageNode.attr("id","");
+ imageNode.remove();
+ }
+
+ // if image is inside options container we need to remove the container
+ var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier);
+ if (imageContainerNode.length != 0){
+ imageContainerNode.remove();
+ }
+};
+
+/**
+ * @brief Inserts a video tag using the videoURL as source and posterURL as the
+ * image to show while video is loading.
+ *
+ * @param videoURL the url of the video
+ * @param posterURL the url of an image to show while the video is loading
+ * @param videoPressID the VideoPress ID of the video, when applicable
+ *
+ */
+ZSSEditor.insertVideo = function(videoURL, posterURL, videopressID) {
+ var videoId = Date.now();
+ var html = '<video id=' + videoId + ' webkit-playsinline src="' + videoURL + '" onclick="" controls="controls" preload="metadata"';
+
+ if (posterURL != '') {
+ html += ' poster="' + posterURL + '"';
+ }
+
+ if (videopressID != '') {
+ html += ' data-wpvideopress="' + videopressID + '"';
+ }
+
+ html += '></video>';
+
+ this.insertHTMLWrappedInParagraphTags('&#x200b;' + html);
+
+ // Wrap video in edit-container node for a permanent delete button overlay
+ var videoNode = $('video[id=' + videoId + ']')[0];
+ var selectionNode = this.applyEditContainer(videoNode);
+ videoNode.removeAttribute('id');
+
+ // Remove the zero-width space node (it's not needed now that the paragraph-wrapped video is in place)
+ var zeroWidthNode = selectionNode.previousSibling;
+ if (zeroWidthNode != null && zeroWidthNode.nodeType == 3) {
+ zeroWidthNode.parentNode.removeChild(zeroWidthNode);
+ }
+
+ ZSSEditor.trackNodeForMutation($(selectionNode));
+
+ this.sendEnabledStyles();
+ this.callback("callback-action-finished");
+};
+
+/**
+ * @brief Inserts a placeholder image tag for in-progress video uploads, marked with an identifier.
+ * @details The image shown can be the video's poster if available - otherwise the default poster image is used.
+ * Using an image instead of a video placeholder is a departure from iOS, necessary because the original
+ * method caused occasional WebView freezes on Android.
+ * Once the video is successfully uploaded, the application should call replaceLocalVideoWithRemoteVideo().
+ *
+ * @param videoNodeIdentifier This is a unique ID provided by the caller. It exists as
+ * a mechanism to update the video node with the remote URL
+ * when replaceLocalVideoWithRemoteVideo() is called.
+ * @param posterURL The URL of a poster image to display while the video is being uploaded.
+ */
+ZSSEditor.insertLocalVideo = function(videoNodeIdentifier, posterURL) {
+ var progressIdentifier = this.getVideoProgressIdentifier(videoNodeIdentifier);
+ var videoContainerIdentifier = this.getVideoContainerIdentifier(videoNodeIdentifier);
+
+ if (nativeState.androidApiLevel > 18) {
+ var videoContainerClass = 'video_container';
+ var progressElement = '<progress id="' + progressIdentifier + '" value=0 class="wp_media_indicator"'
+ + 'contenteditable="false"></progress>';
+ } else {
+ // Before API 19, the WebView didn't support progress tags. Use an upload overlay instead of a progress bar
+ var videoContainerClass = 'video_container compat';
+ var progressElement = '<span class="upload-overlay" contenteditable="false">' + nativeState.localizedStringUploading
+ + '</span><span class="upload-overlay-bg"></span>';
+ }
+
+ var videoContainerStart = '<span id="' + videoContainerIdentifier + '" class="' + videoContainerClass
+ + '" contenteditable="false">';
+ var videoContainerEnd = '</span>';
+
+ if (posterURL == '') {
+ posterURL = "svg/wpposter.svg";
+ }
+
+ var image = '<img data-video_wpid="' + videoNodeIdentifier + '" src="' + posterURL + '" alt="" />';
+ var html = videoContainerStart + progressElement + image + videoContainerEnd;
+
+ this.insertHTMLWrappedInParagraphTags(html);
+
+ ZSSEditor.trackNodeForMutation(this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier));
+
+ this.setProgressOnMedia(videoNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, videoNodeIdentifier, 1);
+ }
+
+ this.sendEnabledStyles();
+};
+
+ZSSEditor.getVideoNodeWithIdentifier = function(videoNodeIdentifier) {
+ var videoNode = $('img[data-video_wpid="' + videoNodeIdentifier+'"]');
+ if (videoNode.length == 0) {
+ videoNode = $('video[data-wpid="' + videoNodeIdentifier+'"]');
+ }
+ return videoNode;
+};
+
+ZSSEditor.getVideoProgressIdentifier = function(videoNodeIdentifier) {
+ return 'progress_' + videoNodeIdentifier;
+};
+
+ZSSEditor.getVideoProgressNodeWithIdentifier = function(videoNodeIdentifier) {
+ return $('#'+this.getVideoProgressIdentifier(videoNodeIdentifier));
+};
+
+ZSSEditor.getVideoContainerIdentifier = function(videoNodeIdentifier) {
+ return 'video_container_' + videoNodeIdentifier;
+};
+
+ZSSEditor.getVideoContainerNodeWithIdentifier = function(videoNodeIdentifier) {
+ return $('#'+this.getVideoContainerIdentifier(videoNodeIdentifier));
+};
+
+/**
+ * @brief Replaces the image placeholder with a video element containing the uploaded video's attributes,
+ * and removes the upload container.
+ *
+ * @param videoNodeIdentifier The unique id of the video upload
+ * @param remoteVideoUrl The URL of the remote video to display
+ * @param remotePosterUrl The URL of the remote poster image to display
+ * @param videopressID The VideoPress ID of the video, where applicable
+ */
+ZSSEditor.replaceLocalVideoWithRemoteVideo = function(videoNodeIdentifier, remoteVideoUrl, remotePosterUrl, videopressID) {
+ var imagePlaceholderNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+
+ if (imagePlaceholderNode.length != 0) {
+ var videoNode = document.createElement("video");
+ videoNode.setAttribute('webkit-playsinline', '');
+ videoNode.setAttribute('onclick', '');
+ videoNode.setAttribute('src', remoteVideoUrl);
+ videoNode.setAttribute('controls', 'controls');
+ videoNode.setAttribute('preload', 'metadata');
+ if (videopressID != '') {
+ videoNode.setAttribute('data-wpvideopress', videopressID);
+ }
+ videoNode.setAttribute('poster', remotePosterUrl);
+
+ // Replace upload container and placeholder image with the uploaded video node
+ var containerNode = imagePlaceholderNode.parent();
+ containerNode.replaceWith(videoNode);
+ }
+
+ var selectionNode = this.applyEditContainer(videoNode);
+
+ ZSSEditor.trackNodeForMutation($(selectionNode));
+
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+ ZSSEditor.callback("callback-input", joinedArguments);
+ // We invoke the sendVideoReplacedCallback with a delay to avoid for
+ // it to be ignored by the webview because of the previous callback being done.
+ var thisObj = this;
+ setTimeout(function() { thisObj.sendVideoReplacedCallback(videoNodeIdentifier);}, 500);
+};
+
+/**
+ * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url
+ *
+ * @param videoNodeIdentifier the unique video ID for the uploaded Video
+ */
+ZSSEditor.sendVideoReplacedCallback = function( videoNodeIdentifier ) {
+ var arguments = ['id=' + encodeURIComponent( videoNodeIdentifier )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ this.callback("callback-video-replaced", joinedArguments);
+};
+
+/**
+ * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url
+ *
+ * @param videoNodeIdentifier the unique video ID for the uploaded Video
+ */
+ZSSEditor.sendVideoPressInfoRequest = function( videoPressID ) {
+ var arguments = ['id=' + encodeURIComponent( videoPressID )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ this.callback("callback-videopress-info-request", joinedArguments);
+};
+
+
+/**
+ * @brief Marks the Video as failed to upload
+ *
+ * @param VideoNodeIdentifier This is a unique ID provided by the caller.
+ * @param message A message to show to the user, overlayed on the Video
+ */
+ZSSEditor.markVideoUploadFailed = function(videoNodeIdentifier, message) {
+ var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+ if (videoNode.length == 0){
+ return;
+ }
+
+ var sizeClass = '';
+ if ( videoNode[0].width > 480 && videoNode[0].height > 240 ) {
+ sizeClass = "largeFail";
+ } else if ( videoNode[0].width < 100 || videoNode[0].height < 100 ) {
+ sizeClass = "smallFail";
+ }
+
+ videoNode.addClass('failed');
+
+ var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier);
+ if(videoContainerNode.length != 0){
+ videoContainerNode.attr("data-failed", message);
+ videoNode.removeClass("uploading");
+ videoContainerNode.addClass('failed');
+ videoContainerNode.addClass(sizeClass);
+ }
+
+ var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier);
+ if (videoProgressNode.length != 0){
+ videoProgressNode.addClass('failed');
+ videoProgressNode.attr("value", 0);
+ }
+
+ // Delete the compatibility overlay if present
+ videoContainerNode.find("span.upload-overlay").addClass("failed");
+};
+
+/**
+ * @brief Unmarks the Video as failed to upload
+ *
+ * @param VideoNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.unmarkVideoUploadFailed = function(videoNodeIdentifier) {
+ var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+ if (videoNode.length != 0){
+ videoNode.removeClass('failed');
+ }
+
+ var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier);
+ if(videoContainerNode.length != 0){
+ videoContainerNode.removeAttr("data-failed");
+ videoContainerNode.removeClass('failed');
+ }
+
+ var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier);
+ if (videoProgressNode.length != 0){
+ videoProgressNode.removeClass('failed');
+ }
+
+ // Display the compatibility overlay again if present
+ videoContainerNode.find("span.upload-overlay").removeClass("failed");
+
+ this.setProgressOnMedia(videoNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, videoNodeIdentifier, 1);
+ }
+};
+
+/**
+ * @brief Remove the Video from the DOM.
+ *
+ * @param videoNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.removeVideo = function(videoNodeIdentifier) {
+ var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+ if (videoNode.length != 0){
+ videoNode.remove();
+ }
+
+ // if Video is inside options container we need to remove the container
+ var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier);
+ if (videoContainerNode.length != 0){
+ // Reset id before removal to avoid triggering the manual media removal callback
+ videoContainerNode.attr("id","");
+ videoContainerNode.remove();
+ }
+};
+
+/**
+ * @brief Wrap the video in an edit-container with a delete button overlay.
+ */
+ZSSEditor.applyEditContainer = function(videoNode) {
+ var containerHtml = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span></span>';
+ videoNode.insertAdjacentHTML('beforebegin', containerHtml);
+
+ var selectionNode = videoNode.previousSibling;
+ selectionNode.appendChild(videoNode);
+
+ return selectionNode;
+}
+
+ZSSEditor.replaceVideoPressVideosForShortcode = function ( html) {
+ // call methods to restore any transformed content from its visual presentation to its source code.
+ var regex = /<video[^>]*data-wpvideopress="([\s\S]+?)"[^>]*>*<\/video>/g;
+ var str = html.replace( regex, ZSSEditor.removeVideoPressVisualFormattingCallback );
+
+ return str;
+}
+
+ZSSEditor.replaceVideosForShortcode = function ( html) {
+ var regex = /<video(?:(?!data-wpvideopress).)*><\/video>/g;
+ var str = html.replace( regex, ZSSEditor.removeVideoVisualFormattingCallback );
+
+ return str;
+}
+
+ZSSEditor.removeVideoContainers = function(html) {
+ var containerRegex = /<span class="edit-container" contenteditable="false">(?:<span class="delete-overlay"[^<>]*><\/span>)?(\[[^<>]*)<\/span>/g;
+ var str = html.replace(containerRegex, ZSSEditor.removeVideoContainerCallback);
+
+ return str;
+}
+
+ZSSEditor.removeVideoPressVisualFormattingCallback = function( match, content ) {
+ return "[wpvideo " + content + "]";
+}
+
+ZSSEditor.removeVideoVisualFormattingCallback = function( match, content ) {
+ var videoElement = $.parseHTML(match)[0];
+
+ // Remove editor playback attributes
+ videoElement.removeAttribute("onclick");
+ videoElement.removeAttribute("controls");
+ videoElement.removeAttribute("webkit-playsinline");
+ if (videoElement.getAttribute("preload") == "metadata") {
+ // The "metadata" setting is the WP default and is usually automatically stripped from the shortcode.
+ // If it's present, it was probably set by this editor and we should remove it. Even if it wasn't, removing it
+ // won't affect anything as it's the default setting for the preload attribute.
+ videoElement.removeAttribute("preload");
+ }
+
+ // If filetype attributes exist, the src attribute wasn't there originally and we should remove it
+ for (var i = 0; i < Formatter.videoShortcodeFormats.length; i++) {
+ var format = Formatter.videoShortcodeFormats[i];
+ if (videoElement.hasAttribute(format)) {
+ videoElement.removeAttribute("src");
+ break;
+ }
+ }
+
+ var shortcode = videoElement.outerHTML.replace(/</g, "[");
+ shortcode = shortcode.replace(/>/g, "]");
+ return shortcode;
+}
+
+ZSSEditor.removeVideoContainerCallback = function( match, content ) {
+ return content;
+}
+
+/**
+ * @brief Sets the VideoPress video URL and poster URL on a video tag.
+ * @details When switching from HTML to visual mode, wpvideo shortcodes are replaced by video tags.
+ * A request is sent using ZSSEditor.sendVideoPressInfoRequest() to obtain the video url matching each
+ * wpvideo shortcode. This function must be called to set the url for each videopress id.
+ *
+ * @param videopressID VideoPress identifier of the video.
+ * @param videoURL URL of the video file to display.
+ * @param posterURL URL of the poster image to display
+ */
+ZSSEditor.setVideoPressLinks = function(videopressID, videoURL, posterURL ) {
+ var videoNode = $('video[data-wpvideopress="' + videopressID + '"]');
+ if (videoNode.length == 0) {
+ return;
+ }
+
+ // It's safest to drop the onError now, to avoid endless calls if the video can't be loaded
+ // Even if sendVideoPressInfoRequest failed, it's still possible to request a reload by tapping the video
+ videoNode.attr('onError', '');
+
+ if (videoURL.length == 0) {
+ return;
+ }
+
+ videoNode.attr('src', videoURL);
+ videoNode.attr('controls', '');
+ videoNode.attr('poster', posterURL);
+ var thisObj = this;
+ videoNode.load();
+};
+
+/**
+ * @brief Stops all video of playing
+ *
+ */
+ZSSEditor.pauseAllVideos = function () {
+ $('video').each(function() {
+ this.pause();
+ });
+}
+
+ZSSEditor.clearCurrentEditingImage = function() {
+ ZSSEditor.currentEditingImage = null;
+};
+
+/**
+ * @brief Updates the currently selected image, replacing its markup with
+ * new markup based on the specified meta data string.
+ *
+ * @param imageMetaString A JSON string representing the updated meta data.
+ */
+ZSSEditor.updateCurrentImageMeta = function( imageMetaString ) {
+ if ( !ZSSEditor.currentEditingImage ) {
+ return;
+ }
+
+ var imageMeta = JSON.parse( imageMetaString );
+ var html = ZSSEditor.createImageFromMeta( imageMeta );
+
+ // Insert the updated html and remove the outdated node.
+ // This approach is preferred to selecting the current node via a range,
+ // and then replacing it when calling insertHTML. The insertHTML call can,
+ // in certain cases, modify the current and inserted markup depending on what
+ // elements surround the targeted node. This approach is safer.
+ var node = ZSSEditor.findImageCaptionNode( ZSSEditor.currentEditingImage );
+ var parent = node.parentNode;
+
+ node.insertAdjacentHTML( 'afterend', html );
+ // Use {node}.{parent}.removeChild() instead of {node}.remove(), since Android API<19 doesn't support Node.remove()
+ node.parentNode.removeChild(node);
+
+ ZSSEditor.currentEditingImage = null;
+
+ ZSSEditor.setFocusAfterElement(parent);
+}
+
+ZSSEditor.applyImageSelectionFormatting = function( imageNode ) {
+ var node = ZSSEditor.findImageCaptionNode( imageNode );
+
+ var overlay = '<span class="edit-overlay" contenteditable="false"><span class="edit-icon"></span>'
+ + '<span class="edit-content">' + nativeState.localizedStringEdit + '</span></span>';
+
+ var sizeClass = "";
+ if ( imageNode.width < 100 || imageNode.height < 100 ) {
+ sizeClass = " small";
+ } else {
+ overlay = '<span class="delete-overlay" contenteditable="false"></span>' + overlay;
+ }
+
+ if (document.body.style.filter == null) {
+ // CSS Filters (including blur) are not supported
+ // Use dark semi-transparent background for edit overlay instead of blur in this case
+ overlay = overlay + '<div class="edit-overlay-bg"></div>';
+ }
+
+ var html = '<span class="edit-container' + sizeClass + '">' + overlay + '</span>';
+ node.insertAdjacentHTML( 'beforebegin', html );
+ var selectionNode = node.previousSibling;
+ selectionNode.appendChild( node );
+
+ this.trackNodeForMutation($(selectionNode));
+
+ return selectionNode;
+}
+
+ZSSEditor.removeImageSelectionFormatting = function( imageNode ) {
+ var node = ZSSEditor.findImageCaptionNode( imageNode );
+ if ( !node.parentNode || node.parentNode.className.indexOf( "edit-container" ) == -1 ) {
+ return;
+ }
+
+ var parentNode = node.parentNode;
+ var container = parentNode.parentNode;
+
+ if (container != null) {
+ container.insertBefore( node, parentNode );
+ // Use {node}.{parent}.removeChild() instead of {node}.remove(), since Android API<19 doesn't support Node.remove()
+ container.removeChild(parentNode);
+ }
+}
+
+ZSSEditor.removeImageSelectionFormattingFromHTML = function( html ) {
+ var tmp = document.createElement( "div" );
+ var tmpDom = $( tmp ).html( html );
+
+ var matches = tmpDom.find( "span.edit-container img" );
+ if ( matches.length == 0 ) {
+ return html;
+ }
+
+ for ( var i = 0; i < matches.length; i++ ) {
+ ZSSEditor.removeImageSelectionFormatting( matches[i] );
+ }
+
+ return tmpDom.html();
+}
+
+ZSSEditor.removeImageRemoteUrl = function(html) {
+ var tmp = document.createElement("div");
+ var tmpDom = $(tmp).html(html);
+
+ var matches = tmpDom.find("img");
+ if (matches.length == 0) {
+ return html;
+ }
+
+ for (var i = 0; i < matches.length; i++) {
+ if (matches[i].getAttribute('remoteurl')) {
+ if (matches[i].parentNode && matches[i].parentNode.href === matches[i].src) {
+ matches[i].parentNode.href = matches[i].getAttribute('remoteurl')
+ }
+ matches[i].src = matches[i].getAttribute('remoteurl');
+ matches[i].removeAttribute('remoteurl');
+ }
+ }
+
+ return tmpDom.html();
+}
+
+/**
+ * @brief Finds all related caption nodes for the specified image node.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ */
+ZSSEditor.findImageCaptionNode = function( imageNode ) {
+ var node = imageNode;
+ if ( node.parentNode && node.parentNode.nodeName === 'A' ) {
+ node = node.parentNode;
+ }
+
+ if ( node.parentNode && node.parentNode.className.indexOf( 'wp-caption' ) != -1 ) {
+ node = node.parentNode;
+ }
+
+ if ( node.parentNode && (node.parentNode.className.indexOf( 'wp-temp' ) != -1 ) ) {
+ node = node.parentNode;
+ }
+
+ return node;
+}
+
+/**
+ * Modified from wp-includes/js/media-editor.js
+ * see `image`
+ *
+ * @brief Construct html markup for an image, and optionally a link an caption shortcode.
+ *
+ * @param props A dictionary of properties used to compose the markup. See comments in extractImageMeta.
+ *
+ * @return Returns the html mark up as a string
+ */
+ZSSEditor.createImageFromMeta = function( props ) {
+ var img = {},
+ options, classes, shortcode, html;
+
+ classes = props.classes || [];
+ if ( ! ( classes instanceof Array ) ) {
+ classes = classes.split( ' ' );
+ }
+
+ _.extend( img, _.pick( props, 'width', 'height', 'alt', 'src', 'title' ) );
+
+ // Only assign the align class to the image if we're not printing
+ // a caption, since the alignment is sent to the shortcode.
+ if ( props.align && ! props.caption ) {
+ classes.push( 'align' + props.align );
+ }
+
+ if ( props.size ) {
+ classes.push( 'size-' + props.size );
+ }
+
+ if ( props.attachment_id ) {
+ classes.push( 'wp-image-' + props.attachment_id );
+ }
+
+ img['class'] = _.compact( classes ).join(' ');
+
+ // Generate `img` tag options.
+ options = {
+ tag: 'img',
+ attrs: img,
+ single: true
+ };
+
+ // Generate the `a` element options, if they exist.
+ if ( props.linkUrl ) {
+ options = {
+ tag: 'a',
+ attrs: {
+ href: props.linkUrl
+ },
+ content: options
+ };
+
+ if ( props.linkClassName ) {
+ options.attrs.class = props.linkClassName;
+ }
+
+ if ( props.linkRel ) {
+ options.attrs.rel = props.linkRel;
+ }
+
+ if ( props.linkTargetBlank ) { // expects a boolean
+ options.attrs.target = "_blank";
+ }
+ }
+
+ html = wp.html.string( options );
+
+ // Generate the caption shortcode.
+ if ( props.caption ) {
+ shortcode = {};
+
+ if ( img.width ) {
+ shortcode.width = img.width;
+ }
+
+ if ( props.captionId ) {
+ shortcode.id = props.captionId;
+ }
+
+ if ( props.align ) {
+ shortcode.align = 'align' + props.align;
+ } else {
+ shortcode.align = 'alignnone';
+ }
+
+ if (props.captionClassName) {
+ shortcode.class = props.captionClassName;
+ }
+
+ html = wp.shortcode.string({
+ tag: 'caption',
+ attrs: shortcode,
+ content: html + props.caption
+ });
+
+ html = Formatter.applyVisualFormatting( html );
+ }
+
+ return html;
+};
+
+/**
+ * Modified from wp-includes/js/tinymce/plugins/wpeditimage/plugin.js
+ * see `extractImageData`
+ *
+ * @brief Extracts properties and meta data from an image, and optionally its link and caption.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ *
+ * @return Returns an object containing the extracted properties and meta data.
+ */
+ZSSEditor.extractImageMeta = function( imageNode ) {
+ var classes, extraClasses, metadata, captionBlock, caption, link, width, height,
+ captionClassName = [],
+ isIntRegExp = /^\d+$/;
+
+ // Default attributes. All values are strings, except linkTargetBlank
+ metadata = {
+ align: 'none', // Accepted values: center, left, right or empty string.
+ alt: '', // Image alt attribute
+ attachment_id: '', // Numeric attachment id of the image in the site's media library
+ caption: '', // The text of the caption for the image (if any)
+ captionClassName: '', // The classes for the caption shortcode (if any).
+ captionId: '', // The caption shortcode's ID attribute. The numeric value should match the value of attachment_id
+ classes: '', // The class attribute for the image. Does not include editor generated classes
+ height: '', // The image height attribute
+ linkClassName: '', // The class attribute for the link
+ linkRel: '', // The rel attribute for the link (if any)
+ linkTargetBlank: false, // true if the link should open in a new window.
+ linkUrl: '', // The href attribute of the link
+ size: 'custom', // Accepted values: custom, medium, large, thumbnail, or empty string
+ src: '', // The src attribute of the image
+ title: '', // The title attribute of the image (if any)
+ width: '', // The image width attribute
+ naturalWidth:'', // The natural width of the image.
+ naturalHeight:'' // The natural height of the image.
+ };
+
+ // populate metadata with values of matched attributes
+ metadata.src = $( imageNode ).attr( 'src' ) || '';
+ metadata.alt = $( imageNode ).attr( 'alt' ) || '';
+ metadata.title = $( imageNode ).attr( 'title' ) || '';
+ metadata.naturalWidth = imageNode.naturalWidth;
+ metadata.naturalHeight = imageNode.naturalHeight;
+
+ width = $(imageNode).attr( 'width' );
+ height = $(imageNode).attr( 'height' );
+
+ if ( ! isIntRegExp.test( width ) || parseInt( width, 10 ) < 1 ) {
+ width = imageNode.naturalWidth || imageNode.width;
+ }
+
+ if ( ! isIntRegExp.test( height ) || parseInt( height, 10 ) < 1 ) {
+ height = imageNode.naturalHeight || imageNode.height;
+ }
+
+ metadata.width = width;
+ metadata.height = height;
+
+ classes = imageNode.className.split( /\s+/ );
+ extraClasses = [];
+
+ $.each( classes, function( index, value ) {
+ if ( /^wp-image/.test( value ) ) {
+ metadata.attachment_id = parseInt( value.replace( 'wp-image-', '' ), 10 );
+ } else if ( /^align/.test( value ) ) {
+ metadata.align = value.replace( 'align', '' );
+ } else if ( /^size/.test( value ) ) {
+ metadata.size = value.replace( 'size-', '' );
+ } else {
+ extraClasses.push( value );
+ }
+ } );
+
+ metadata.classes = extraClasses.join( ' ' );
+
+ // Extract caption
+ var captionMeta = ZSSEditor.captionMetaForImage( imageNode )
+ if (captionMeta.caption != '') {
+ metadata = $.extend( metadata, captionMeta );
+ }
+
+ // Extract linkTo
+ if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' ) {
+ link = imageNode.parentNode;
+ metadata.linkClassName = link.className;
+ metadata.linkRel = $( link ).attr( 'rel' ) || '';
+ metadata.linkTargetBlank = $( link ).attr( 'target' ) === '_blank' ? true : false;
+ metadata.linkUrl = $( link ).attr( 'href' ) || '';
+ }
+
+ return metadata;
+};
+
+/**
+ * @brief Extracts the caption shortcode for an image.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ *
+ * @return Returns a shortcode match (if any) for the passed image node.
+ * See shortcode.js::next for details
+ */
+ZSSEditor.getCaptionForImage = function( imageNode ) {
+ var node = ZSSEditor.findImageCaptionNode( imageNode );
+
+ // Ensure we're working with the formatted caption
+ if ( node.className.indexOf( 'wp-temp' ) == -1 ) {
+ return;
+ }
+
+ var html = node.outerHTML;
+ html = ZSSEditor.removeVisualFormatting( html );
+
+ return wp.shortcode.next( "caption", html, 0 );
+};
+
+/**
+ * @brief Extracts meta data for the caption (if any) for the passed image node.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ *
+ * @return Returns an object containing the extracted meta data.
+ * See shortcode.js::next or details
+ */
+ZSSEditor.captionMetaForImage = function( imageNode ) {
+ var attrs,
+ meta = {
+ align: '',
+ caption: '',
+ captionClassName: '',
+ captionId: ''
+ };
+
+ var caption = ZSSEditor.getCaptionForImage( imageNode );
+ if ( !caption ) {
+ return meta;
+ }
+
+ attrs = caption.shortcode.attrs.named;
+ if ( attrs.align ) {
+ meta.align = attrs.align.replace( 'align', '' );
+ }
+ if ( attrs.class ) {
+ meta.captionClassName = attrs.class;
+ }
+ if ( attrs.id ) {
+ meta.captionId = attrs.id;
+ }
+ meta.caption = caption.shortcode.content.substr( caption.shortcode.content.lastIndexOf( ">" ) + 1 );
+
+ return meta;
+}
+
+/**
+ * @brief Removes custom visual formatting for caption shortcodes.
+ *
+ * @param html The markup to process
+ *
+ * @return The html with formatted captions restored to the original shortcode markup.
+ */
+ZSSEditor.removeCaptionFormatting = function( html ) {
+ // call methods to restore any transformed content from its visual presentation to its source code.
+ var regex = /<label class="wp-temp" data-wp-temp="caption"[^>]*>([\s\S]+?)<\/label>/g;
+
+ var str = html.replace( regex, ZSSEditor.removeCaptionFormattingCallback );
+
+ return str;
+}
+
+ZSSEditor.removeCaptionFormattingCallback = function( match, content ) {
+ // TODO: check is a visual temp node
+ var out = '';
+
+ if ( content.indexOf('<img ') === -1 ) {
+ // Broken caption. The user managed to drag the image out?
+ // Try to return the caption text as a paragraph.
+ out = content.match( /\s*<span [^>]*>([\s\S]+?)<\/span>/gi );
+
+ if ( out && out[1] ) {
+ return '<p>' + out[1] + '</p>';
+ }
+
+ return '';
+ }
+
+ out = content.replace( /\s*<span ([^>]*)>([\s\S]+?)<\/span>/gi, function( ignoreMatch, attrStr, content ) {
+ if ( ! content ) {
+ return '';
+ }
+
+ var id, classes, align, width, attrs = {};
+
+ width = attrStr.match( /data-caption-width="([0-9]*)"/ );
+ width = ( width && width[1] ) ? width[1] : '';
+ if ( width ) {
+ attrs.width = width;
+ }
+
+ id = attrStr.match( /data-caption-id="([^"]*)"/ );
+ id = ( id && id[1] ) ? id[1] : '';
+ if ( id ) {
+ attrs.id = id;
+ }
+
+ classes = attrStr.match( /data-caption-class="([^"]*)"/ );
+ classes = ( classes && classes[1] ) ? classes[1] : '';
+ if ( classes ) {
+ attrs.class = classes;
+ }
+
+ align = attrStr.match( /data-caption-align="([^"]*)"/ );
+ align = ( align && align[1] ) ? align[1] : '';
+ if ( align ) {
+ attrs.align = align;
+ }
+
+ var options = {
+ 'tag':'caption',
+ 'attrs':attrs,
+ 'type':'closed',
+ 'content':content
+ };
+
+ return wp.shortcode.string( options );
+ });
+
+ return out;
+}
+
+// MARK: - Galleries
+
+ZSSEditor.insertGallery = function( imageIds, type, columns ) {
+ var shortcode;
+ if (type) {
+ shortcode = '[gallery type="' + type + '" ids="' + imageIds + '"]';
+ } else {
+ shortcode = '[gallery columns="' + columns + '" ids="' + imageIds + '"]';
+ }
+
+ this.insertHTMLWrappedInParagraphTags(shortcode);
+}
+
+ZSSEditor.insertLocalGallery = function( placeholderId ) {
+ var container = '<span id="' + placeholderId + '" class="gallery_container">['
+ + nativeState.localizedStringUploadingGallery + ']</span>';
+ this.insertHTMLWrappedInParagraphTags(container);
+}
+
+ZSSEditor.replacePlaceholderGallery = function( placeholderId, imageIds, type, columns ) {
+ var span = 'span#' + placeholderId + '.gallery_container';
+
+ var shortcode;
+ if (type) {
+ shortcode = '[gallery type="' + type + '" ids="' + imageIds + '"]';
+ } else {
+ shortcode = '[gallery columns="' + columns + '" ids="' + imageIds + '"]';
+ }
+
+ $(span).replaceWith(shortcode);
+}
+
+// MARK: - Commands
+
+/**
+ * @brief Removes editor specific visual formatting
+ *
+ * @param html The markup to remove formatting
+ *
+ * @return Returns the string with the visual formatting removed.
+ */
+ZSSEditor.removeVisualFormatting = function( html ) {
+ var str = html;
+ str = ZSSEditor.removeImageRemoteUrl( str );
+ str = ZSSEditor.removeImageSelectionFormattingFromHTML( str );
+ str = ZSSEditor.removeCaptionFormatting( str );
+ str = ZSSEditor.replaceVideoPressVideosForShortcode( str );
+ str = ZSSEditor.replaceVideosForShortcode( str );
+ str = ZSSEditor.removeVideoContainers( str );
+
+ // More tag
+ str = str.replace(/<hr class="more-tag" wp-more-data="(.*?)">/igm, "<!--more$1-->")
+ str = str.replace(/<hr class="nextpage-tag">/igm, "<!--nextpage-->")
+ return str;
+};
+
+ZSSEditor.insertHTML = function(html) {
+ document.execCommand('insertHTML', false, html);
+ this.sendEnabledStyles();
+};
+
+ZSSEditor.insertText = function(text, reformatVisually) {
+ var focusedField = ZSSEditor.getFocusedField();
+ if (focusedField.isMultiline() && focusedField.getHTML().length == 0) {
+ // when the text field is empty, we need to add an initial paragraph tag
+ text = Util.wrapHTMLInTag(text, ZSSEditor.defaultParagraphSeparator);
+ }
+
+ if (reformatVisually) {
+ text = wp.loadText(text);
+ }
+
+ ZSSEditor.insertHTML(text);
+};
+
+ZSSEditor.isCommandEnabled = function(commandName) {
+ return document.queryCommandState(commandName);
+};
+
+ZSSEditor.sendEnabledStyles = function(e) {
+
+ var items = [];
+
+ var focusedField = this.getFocusedField();
+
+ if (!focusedField.hasNoStyle) {
+ // Find all relevant parent tags
+ var parentTags = ZSSEditor.parentTags();
+
+ if (parentTags != null) {
+ for (var i = 0; i < parentTags.length; i++) {
+ var currentNode = parentTags[i];
+
+ if (currentNode.nodeName.toLowerCase() == 'a') {
+ ZSSEditor.currentEditingLink = currentNode;
+
+ var title = encodeURIComponent(currentNode.text);
+ var href = encodeURIComponent(currentNode.href);
+
+ items.push('link-title:' + title);
+ items.push('link:' + href);
+ } else if (currentNode.nodeName == NodeName.BLOCKQUOTE) {
+ items.push('blockquote');
+ }
+ }
+ }
+
+ if (ZSSEditor.isCommandEnabled('bold')) {
+ items.push('bold');
+ }
+ if (ZSSEditor.isCommandEnabled('createLink')) {
+ items.push('createLink');
+ }
+ if (ZSSEditor.isCommandEnabled('italic')) {
+ items.push('italic');
+ }
+ if (ZSSEditor.isCommandEnabled('subscript')) {
+ items.push('subscript');
+ }
+ if (ZSSEditor.isCommandEnabled('superscript')) {
+ items.push('superscript');
+ }
+ if (ZSSEditor.isCommandEnabled('strikeThrough')) {
+ items.push('strikeThrough');
+ }
+ if (ZSSEditor.isCommandEnabled('underline')) {
+ var isUnderlined = false;
+
+ // DRM: 'underline' gets highlighted if it's inside of a link... so we need a special test
+ // in that case.
+ if (!ZSSEditor.currentEditingLink) {
+ items.push('underline');
+ }
+ }
+ if (ZSSEditor.isCommandEnabled('insertOrderedList')) {
+ items.push('orderedList');
+ }
+ if (ZSSEditor.isCommandEnabled('insertUnorderedList')) {
+ items.push('unorderedList');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyCenter')) {
+ items.push('justifyCenter');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyFull')) {
+ items.push('justifyFull');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyLeft')) {
+ items.push('justifyLeft');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyRight')) {
+ items.push('justifyRight');
+ }
+ if (ZSSEditor.isCommandEnabled('insertHorizontalRule')) {
+ items.push('horizontalRule');
+ }
+ var formatBlock = document.queryCommandValue('formatBlock');
+ if (formatBlock.length > 0) {
+ items.push(formatBlock);
+ }
+
+ // Use jQuery to figure out those that are not supported
+ if (typeof(e) != "undefined") {
+
+ // The target element
+ var t = $(e.target);
+ var nodeName = e.target.nodeName.toLowerCase();
+
+ // Background Color
+ try
+ {
+ var bgColor = t.css('backgroundColor');
+ if (bgColor && bgColor.length != 0 && bgColor != 'rgba(0, 0, 0, 0)' && bgColor != 'rgb(0, 0, 0)' && bgColor != 'transparent') {
+ items.push('backgroundColor');
+ }
+ }
+ catch(e)
+ {
+ // DRM: I had to add these stupid try-catch blocks to solve an issue with t.css throwing
+ // exceptions for no reason.
+ }
+
+ // Text Color
+ try
+ {
+ var textColor = t.css('color');
+ if (textColor && textColor.length != 0 && textColor != 'rgba(0, 0, 0, 0)' && textColor != 'rgb(0, 0, 0)' && textColor != 'transparent') {
+ items.push('textColor');
+ }
+ }
+ catch(e)
+ {
+ // DRM: I had to add these stupid try-catch blocks to solve an issue with t.css throwing
+ // exceptions for no reason.
+ }
+
+ // Image
+ if (nodeName == 'img') {
+ ZSSEditor.currentEditingImage = t;
+ items.push('image:'+t.attr('src'));
+ if (t.attr('alt') !== undefined) {
+ items.push('image-alt:'+t.attr('alt'));
+ }
+ }
+ }
+ }
+
+ ZSSEditor.stylesCallback(items);
+};
+
+ZSSEditor.storeInlineStylesAsFunctions = function() {
+ var styles = [];
+
+ if (ZSSEditor.isCommandEnabled('bold')) {
+ styles.push(ZSSEditor.setBold);
+ }
+ if (ZSSEditor.isCommandEnabled('italic')) {
+ styles.push(ZSSEditor.setItalic);
+ }
+ if (ZSSEditor.isCommandEnabled('strikeThrough')) {
+ styles.push(ZSSEditor.setStrikeThrough);
+ }
+ if (ZSSEditor.isCommandEnabled('underline')) {
+ styles.push(ZSSEditor.setUnderline);
+ }
+ if (ZSSEditor.isCommandEnabled('subscript')) {
+ styles.push(ZSSEditor.setSubscript);
+ }
+ if (ZSSEditor.isCommandEnabled('superscript')) {
+ styles.push(ZSSEditor.setSuperscript);
+ }
+
+ return styles;
+};
+
+// MARK: - Commands: High Level Editing
+
+/**
+ * @brief Inserts a br tag at the caret position.
+ */
+ZSSEditor.insertBreakTagAtCaretPosition = function() {
+ // iOS IMPORTANT: we were adding <br> tags with range.insertNode() before using
+ // this method. Unfortunately this was causing issues with the first <br> tag
+ // being completely ignored under iOS:
+ //
+ // https://bugs.webkit.org/show_bug.cgi?id=23474
+ //
+ // The following line seems to work fine under iOS, so please be careful if this
+ // needs to be changed for any reason.
+ //
+ document.execCommand("insertLineBreak");
+};
+
+// MARK: - Advanced Node Manipulation
+
+/**
+ * @brief Given the specified node, find the previous node in the DOM.
+ *
+ * @param node The node used as a starting point for the "previous" search.
+ *
+ * @returns If a previous node is found, it will be returned otherwise null;
+ */
+ZSSEditor.previousNode = function(node) {
+ if (!node) {
+ return null;
+ }
+ var previous = node.previousSibling;
+ if (previous) {
+ node = previous;
+ while (node.hasChildNodes()) {
+ node = node.lastChild;
+ }
+ return node;
+ }
+ var parent = node.parentNode;
+ if (parent && parent.hasChildNodes()) {
+ return parent;
+ }
+ return null;
+};
+
+/**
+ * @brief Ends the editing of a list (either UL or OL).
+ *
+ * @details This function finds the list node, inserts a new paragraph as a sibling to the list node
+ * then scrubs any <br> tags created as part of the insertParagraph command.
+ */
+ZSSEditor.completeListEditing = function() {
+ // Get the current selection
+ var sel = window.getSelection();
+ if (sel && sel.rangeCount > 0) {
+ var range = sel.getRangeAt(0);
+ var node = range.startContainer;
+ if (node.hasChildNodes() && range.startOffset > 0) {
+ node = node.childNodes[range.startOffset - 1];
+ }
+
+ // Walk backwards through the DOM until we find an ul or ol
+ while (node) {
+ if (node.nodeType == 1 &&
+ (node.tagName.toUpperCase() == NodeName.UL
+ || node.tagName.toUpperCase() == NodeName.OL)) {
+
+ var focusedNode = document.getSelection().getRangeAt(0).startContainer;
+
+ if (focusedNode.nodeType == 3) {
+ // If the focused node is a text node, the list item was not empty when toggled off
+ // Wrap the text in a div and attach it as a sibling to the div wrapping the list
+ var parentParagraph = focusedNode.parentNode;
+ var paragraph = document.createElement('div');
+
+ paragraph.appendChild(focusedNode);
+ parentParagraph.insertAdjacentElement('afterEnd', paragraph);
+
+ ZSSEditor.giveFocusToElement(paragraph, 1);
+ } else {
+ // Attach a new paragraph node as a sibling to the parent node
+ document.execCommand('insertParagraph', false);
+ }
+
+ // Remove any superfluous <br> tags that are created
+ ZSSEditor.scrubBRFromNode(node.parentNode);
+ break;
+ }
+ node = ZSSEditor.previousNode(node);
+ }
+ }
+}
+
+/**
+ * @brief Given the specified node, remove all instances of <br> from it and it's children.
+ *
+ * @param node The node to scrub
+ */
+ZSSEditor.scrubBRFromNode = function(node) {
+ if (!node) {
+ return;
+ }
+ $(node).contents().filter(NodeName.BR).remove();
+};
+
+/**
+ * @brief Extracts a node from a parent node, and from all nodes in between the two.
+ */
+ZSSEditor.extractNodeFromAncestorNode = function(descendant, ancestor) {
+
+ while (ancestor.contains(descendant)) {
+
+ this.extractNodeFromParent(descendant);
+ break;
+ }
+};
+
+/**
+ * @brief Extract the specified node from its direct parent node.
+ * @details If the node has siblings, before or after it, the parent node is split accordingly
+ * into two new clones of it.
+ */
+ZSSEditor.extractNodeFromParent = function(node) {
+
+ var parentNode = node.parentNode;
+ var grandParentNode = parentNode.parentNode;
+ var clonedParentForPreviousSiblings = null;
+ var clonedParentForNextSiblings = null;
+
+ if (node.previousSibling != null) {
+ var clonedParentForPreviousSiblings = parentNode.cloneNode();
+
+ while (parentNode.firstChild != node) {
+ clonedParentForPreviousSiblings.appendChild(parentNode.firstChild);
+ }
+ }
+
+ if (node.nextSibling != null) {
+ var clonedParentForNextSiblings = parentNode.cloneNode();
+
+ while (node.nextSibling != null) {
+ clonedParentForNextSiblings.appendChild(node.nextSibling);
+ }
+ }
+
+ if (clonedParentForPreviousSiblings) {
+ grandParentNode.insertBefore(clonedParentForPreviousSiblings, parentNode);
+ }
+
+ grandParentNode.insertBefore(node, parentNode);
+
+ if (clonedParentForNextSiblings) {
+ grandParentNode.insertBefore(clonedParentForNextSiblings, parentNode);
+ }
+
+ grandParentNode.removeChild(parentNode);
+};
+
+ZSSEditor.getChildNodesIntersectingRange = function(parentNode, range) {
+
+ var nodes = new Array();
+
+ if (parentNode) {
+ var currentNode = parentNode.firstChild;
+ var pushNodes = false;
+ var exit = false;
+
+ while (currentNode) {
+
+ if (range.intersectsNode(currentNode)) {
+ nodes.push(currentNode);
+ }
+
+ currentNode = currentNode.nextSibling;
+ }
+ }
+
+ return nodes;
+};
+
+/**
+ * @brief Given the specified range, find the ancestor element that will be used to set the
+ * blockquote ON or OFF.
+ *
+ * @param range The range we want to set the blockquote ON or OFF for.
+ *
+ * @returns If a parent BLOCKQUOTE element is found, it will be return. Otherwise the closest
+ * parent element will be returned.
+ */
+ZSSEditor.getAncestorElementForSettingBlockquote = function(range) {
+
+ var nodes = new Array();
+ var parentElement = range.commonAncestorContainer;
+
+ while (parentElement
+ && (parentElement.nodeType != document.ELEMENT_NODE
+ || parentElement.nodeName == NodeName.PARAGRAPH
+ || parentElement.nodeName == NodeName.STRONG
+ || parentElement.nodeName == NodeName.EM
+ || parentElement.nodeName == NodeName.DEL
+ || parentElement.nodeName == NodeName.A
+ || parentElement.nodeName == NodeName.UL
+ || parentElement.nodeName == NodeName.OL
+ || parentElement.nodeName == NodeName.LI
+ || parentElement.nodeName == NodeName.CODE
+ || parentElement.nodeName == NodeName.SPAN
+ // Include nested divs, but ignore the parent contenteditable field div
+ || (parentElement.nodeName == NodeName.DIV && parentElement.parentElement.nodeName != NodeName.BODY))) {
+ parentElement = parentElement.parentNode;
+ }
+
+ var currentElement = parentElement;
+
+ while (currentElement
+ && currentElement.nodeName != NodeName.BLOCKQUOTE) {
+ currentElement = currentElement.parentElement;
+ }
+
+ var result = currentElement ? currentElement : parentElement;
+
+ return result;
+};
+
+/**
+ * @brief Joins any adjacent blockquote siblings.
+ * @details You probably want to call joinAdjacentSiblingsOrAncestorBlockquotes() instead of
+ * this.
+ *
+ * @returns true if a sibling was joined. false otherwise.
+ */
+ZSSEditor.joinAdjacentSiblingsBlockquotes = function(node) {
+
+ var shouldJoinToPreviousSibling = this.hasPreviousSiblingWithName(node, NodeName.BLOCKQUOTE);
+ var shouldJoinToNextSibling = this.hasNextSiblingWithName(node, NodeName.BLOCKQUOTE);
+ var joinedASibling = (shouldJoinToPreviousSibling || shouldJoinToNextSibling);
+
+ var previousSibling = node.previousSibling;
+ var nextSibling = node.nextSibling;
+
+ if (shouldJoinToPreviousSibling) {
+
+ previousSibling.appendChild(node);
+
+ if (shouldJoinToNextSibling) {
+
+ while (nextSibling.firstChild) {
+ previousSibling.appendChild(nextSibling.firstChild);
+ }
+
+ nextSibling.parentNode.removeChild(nextSibling);
+ }
+ } else if (shouldJoinToNextSibling) {
+
+ nextSibling.insertBefore(node, nextSibling.firstChild);
+ }
+
+ return joinedASibling;
+};
+
+/**
+ * @brief Joins any adjacent blockquote siblings, or the blockquote siblings of any ancestor.
+ * @details When turning blockquotes back on, this method makes sure that we attach new
+ * blockquotes to exiting ones.
+ *
+ * @returns true if a sibling or ancestor sibling was joined. false otherwise.
+ */
+ZSSEditor.joinAdjacentSiblingsOrAncestorBlockquotes = function(node) {
+
+ var currentNode = node;
+ var rootNode = this.getFocusedField().getWrappedDomNode();
+ var joined = false;
+
+ while (currentNode
+ && currentNode != rootNode
+ && !joined) {
+
+ joined = this.joinAdjacentSiblingsBlockquotes(currentNode);
+ currentNode = currentNode.parentNode;
+ };
+
+ return joined;
+};
+
+/**
+ * @brief Surrounds a node's contents into another node
+ * @details When creating new nodes that should force paragraphs inside of them, this method
+ * should be called.
+ *
+ * @param node The node that will have its contents wrapped into a new node.
+ * @param wrapperNodeName The nodeName of the node that will created to wrap the contents.
+ *
+ * @returns The newly created wrapper node.
+ */
+ZSSEditor.surroundNodeContentsWithNode = function(node, wrapperNodeName) {
+
+ var range = document.createRange();
+ var wrapperNode = document.createElement(wrapperNodeName);
+
+ range.selectNodeContents(node);
+ range.surroundContents(wrapperNode);
+
+ return wrapperNode;
+};
+
+/**
+ * @brief Surrounds a node's contents with a paragraph node.
+ * @details When creating new nodes that should force paragraphs inside of them, this method
+ * should be called.
+ *
+ * @returns The paragraph node.
+ */
+ZSSEditor.surroundNodeContentsWithAParagraphNode = function(node) {
+
+ return this.surroundNodeContentsWithNode(node, this.defaultParagraphSeparator);
+};
+
+// MARK: - Sibling nodes
+
+ZSSEditor.hasNextSiblingWithName = function(node, siblingNodeName) {
+ return node.nextSibling && node.nextSibling.nodeName == siblingNodeName;
+};
+
+ZSSEditor.hasPreviousSiblingWithName = function(node, siblingNodeName) {
+ return node.previousSibling && node.previousSibling.nodeName == siblingNodeName;
+};
+
+
+// MARK: - Parent nodes & tags
+
+ZSSEditor.findParentContenteditableDiv = function() {
+ var parentNode = null;
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var referenceNode = this.closerParentNodeWithNameRelativeToNode('div', range.commonAncestorContainer);
+
+ while (referenceNode.parentNode.nodeName != NodeName.BODY) {
+ referenceNode = this.closerParentNodeWithNameRelativeToNode('div', referenceNode.parentNode);
+ }
+
+ return referenceNode;
+};
+
+ZSSEditor.closerParentNode = function() {
+
+ var parentNode = null;
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var currentNode = range.commonAncestorContainer;
+
+ while (currentNode) {
+ if (currentNode.nodeType == document.ELEMENT_NODE) {
+ parentNode = currentNode;
+
+ break;
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentNode;
+};
+
+ZSSEditor.closerParentNodeStartingAtNode = function(nodeName, startingNode) {
+
+ nodeName = nodeName.toLowerCase();
+
+ var parentNode = null;
+ var currentNode = startingNode.parentElement;
+
+ while (currentNode) {
+
+ if (currentNode.nodeName == document.body.nodeName) {
+ break;
+ }
+
+ if (currentNode.nodeName && currentNode.nodeName.toLowerCase() == nodeName
+ && currentNode.nodeType == document.ELEMENT_NODE) {
+ parentNode = currentNode;
+
+ break;
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentNode;
+};
+
+ZSSEditor.closerParentNodeWithName = function(nodeName) {
+
+ nodeName = nodeName.toLowerCase();
+
+ var parentNode = null;
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var referenceNode = range.commonAncestorContainer;
+
+ return this.closerParentNodeWithNameRelativeToNode(nodeName, referenceNode);
+};
+
+ZSSEditor.closerParentNodeWithNameRelativeToNode = function(nodeName, referenceNode) {
+
+ nodeName = nodeName.toUpperCase();
+
+ var parentNode = null;
+ var currentNode = referenceNode;
+
+ while (currentNode) {
+
+ if (currentNode.nodeName == document.body.nodeName) {
+ break;
+ }
+
+ if (currentNode.nodeName == nodeName
+ && currentNode.nodeType == document.ELEMENT_NODE) {
+ parentNode = currentNode;
+
+ break;
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentNode;
+};
+
+ZSSEditor.isCloserParentNodeABlockquote = function() {
+ return this.closerParentNode().nodeName == NodeName.BLOCKQUOTE;
+};
+
+ZSSEditor.parentTags = function() {
+
+ var parentTags = [];
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0);
+
+ var currentNode = range.commonAncestorContainer;
+ while (currentNode) {
+
+ if (currentNode.nodeName == document.body.nodeName) {
+ break;
+ }
+
+ if (currentNode.nodeType == document.ELEMENT_NODE) {
+ parentTags.push(currentNode);
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentTags;
+};
+
+// MARK: - Range handling
+
+ZSSEditor.getParentRangeOfFocusedNode = function() {
+ var selection = window.getSelection();
+ if (selection.focusNode == null) {
+ return null;
+ }
+ return selection.getRangeAt(selection.focusNode.parentNode);
+};
+
+ZSSEditor.setRange = function(range) {
+ window.getSelection().removeAllRanges();
+ window.getSelection().addRange(range);
+};
+// MARK: - ZSSField Constructor
+
+function ZSSField(wrappedObject) {
+ // When this bool is true, we are going to restrict input and certain callbacks
+ // so IME keyboards behave properly when composing.
+ this.isComposing = false;
+
+ this.multiline = false;
+ this.wrappedObject = wrappedObject;
+
+ if (this.getWrappedDomNode().hasAttribute('nostyle')) {
+ this.hasNoStyle = true;
+ }
+
+ this.useVisualFormatting = (this.wrappedObject.data("wpUseVisualFormatting") === "true")
+
+ this.bindListeners();
+};
+
+ZSSField.prototype.bindListeners = function() {
+
+ var thisObj = this;
+
+ this.wrappedObject.bind('tap', function(e) { thisObj.handleTapEvent(e); });
+ this.wrappedObject.bind('focus', function(e) { thisObj.handleFocusEvent(e); });
+ this.wrappedObject.bind('blur', function(e) { thisObj.handleBlurEvent(e); });
+ this.wrappedObject.bind('keydown', function(e) { thisObj.handleKeyDownEvent(e); });
+ this.wrappedObject.bind('input', function(e) { thisObj.handleInputEvent(e); });
+ this.wrappedObject.bind('compositionstart', function(e) { thisObj.handleCompositionStartEvent(e); });
+ this.wrappedObject.bind('compositionend', function(e) { thisObj.handleCompositionEndEvent(e); });
+
+ // Only supported on API19+
+ this.wrappedObject.on('paste', function(e) { thisObj.handlePasteEvent(e); });
+};
+
+// MARK: - Emptying the field when it should be, well... empty (HTML madness)
+
+/**
+ * @brief Sometimes HTML leaves some <br> tags or &nbsp; when the user deletes all
+ * text from a contentEditable field. This code makes sure no such 'garbage' survives.
+ * @details If the node contains child image nodes, then the content is left untouched.
+ */
+ZSSField.prototype.emptyFieldIfNoContents = function() {
+
+ var nbsp = '\xa0';
+ var text = this.wrappedObject.text().replace(nbsp, '');
+
+ if (text.length == 0 || text == '\u000A') {
+
+ var hasChildImages = (this.wrappedObject.find('img').length > 0);
+ var hasChildVideos = (this.wrappedObject.find('video').length > 0);
+ var hasUnorderedList = (this.wrappedObject.find('ul').length > 0);
+ var hasOrderedList = (this.wrappedObject.find('ol').length > 0);
+
+ if (!hasChildImages && !hasChildVideos && !hasUnorderedList && !hasOrderedList) {
+ this.wrappedObject.empty();
+ }
+ }
+};
+
+// MARK: - Handle event listeners
+
+ZSSField.prototype.handleBlurEvent = function(e) {
+ ZSSEditor.focusedField = null;
+
+ this.emptyFieldIfNoContents();
+
+ this.callback("callback-focus-out");
+};
+
+ZSSField.prototype.handleFocusEvent = function(e) {
+ ZSSEditor.focusedField = this;
+
+ this.callback("callback-focus-in");
+};
+
+ZSSField.prototype.handleKeyDownEvent = function(e) {
+
+ var wasEnterPressed = (e.keyCode == '13');
+ var isHardwareKeyboardPaste = (e.ctrlKey && e.keyCode == '86');
+
+ // Handle keyDownEvent actions that need to happen after the event has completed (and the field has been modified)
+ setTimeout(this.afterKeyDownEvent, 20, e.target.innerHTML, e);
+
+ if (this.isComposing) {
+ e.stopPropagation();
+ } else if (wasEnterPressed && !this.isMultiline()) {
+ e.preventDefault();
+ } else if (this.isMultiline()) {
+ // For hardware keyboards, don't do any paragraph handling for non-printable keyCodes
+ // https://css-tricks.com/snippets/javascript/javascript-keycodes/
+ // This avoids the filler zero-width space character from being inserted and displayed in the content field
+ // when special keys are pressed in new posts
+ var wasTabPressed = (e.keyCode == '9');
+ var intKeyCode = parseInt(e.keyCode, 10);
+ if (wasTabPressed || (intKeyCode > 13 && intKeyCode < 46) || intKeyCode == 192) {
+ return;
+ }
+
+ // The containsParagraphSeparators check is intended to work around three bugs:
+ // 1. On API19 only, paragraph wrapping the first character in a post will display a zero-width space character
+ // (from ZSSField.wrapCaretInParagraphIfNecessary)
+ // We can drop the if statement wrapping wrapCaretInParagraphIfNecessary() if we find a way to stop using
+ // zero-width space characters (e.g., autocorrect issues are fixed and we switch back to p tags)
+ //
+ // 2. On all APIs, software pasting (long press -> paste) doesn't automatically wrap the paste in paragraph
+ // tags in new posts. On API19+ this is corrected by ZSSField.handlePasteEvent(), but earlier APIs don't support
+ // it. So, instead, we allow the editor not to wrap the paste in paragraph tags and it's automatically corrected
+ // after adding a newline. But allowing wrapCaretInParagraphIfNecessary() to go through will wrap the paragraph
+ // incorrectly, so we skip it in this case.
+ //
+ // 3. On all APIs, hardware pasting (CTRL + V) doesn't automatically wrap the paste in paragraph tags in
+ // new posts. ZSSField.handlePasteEvent() won't fix the wrapping for hardware pastes if
+ // wrapCaretInParagraphIfNecessary() goes through first, so we need to skip it in that case.
+ // For API < 19, this is fixed implicitly by the 'containsParagraphSeparators' check, but for newer APIs we
+ // specifically detect hardware keyboard pastes and skip paragraph wrapping in that case
+ // case skip calling wrapCaretInParagraphIfNecessary().
+ //
+ // Previously, the check was 'if (containsParagraphSeparators)' for all API levels, but this turns out to cause
+ // an autocorrect issue for new posts on API 23+:
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/389
+ // That issue can be fixed by allowing wrapCaretInParagraphIfNecessary() to go through when adding text to an
+ // empty post on those API levels, as long as we exclude the special case of hardware keyboard pasting
+ //
+ // We're using 'nativeState.androidApiLevel>19' rather than >22 because, even though the bug only appears on
+ // 23+ at the time of writing, it's entirely possible a future System WebView or Keyboard update will introduce
+ // the bug on 21+.
+ var containsParagraphSeparators = this.getWrappedDomNode().innerHTML.search(
+ '<' + ZSSEditor.defaultParagraphSeparator) > -1;
+ if (containsParagraphSeparators || (nativeState.androidApiLevel > 19 && !isHardwareKeyboardPaste)) {
+ this.wrapCaretInParagraphIfNecessary();
+ }
+
+ if (wasEnterPressed) {
+ // Wrap the existing text in paragraph tags if necessary (this should only be needed if
+ // wrapCaretInParagraphIfNecessary() was skipped earlier)
+ var currentHtml = this.getWrappedDomNode().innerHTML;
+ if (currentHtml.search('<' + ZSSEditor.defaultParagraphSeparator) == -1) {
+ ZSSEditor.focusedField.setHTML(Util.wrapHTMLInTag(currentHtml, ZSSEditor.defaultParagraphSeparator));
+ ZSSEditor.resetSelectionOnField(this.getWrappedDomNode().id, 1);
+ }
+
+ var sel = window.getSelection();
+ if (sel.rangeCount < 1) {
+ return null;
+ }
+ var node = $(sel.anchorNode);
+ var children = $(sel.anchorNode.childNodes);
+ var parentNode = rangy.getSelection().anchorNode.parentNode;
+
+ // If enter was pressed to end a UL or OL, let's double check and handle it accordingly if so
+ if (sel.isCollapsed && node.is(NodeName.LI) && (!children.length ||
+ (children.length == 1 && children.first().is(NodeName.BR)))) {
+ e.preventDefault();
+ if (parentNode && parentNode.nodeName === NodeName.OL) {
+ ZSSEditor.setOrderedList();
+ } else if (parentNode && parentNode.nodeName === NodeName.UL) {
+ ZSSEditor.setUnorderedList();
+ }
+ // Exit blockquote when the user presses Enter inside a blockquote on a new line
+ // (main use case is to allow double Enter to exit blockquote)
+ } else if (sel.isCollapsed && sel.baseOffset == 0 && parentNode && parentNode.nodeName == 'BLOCKQUOTE') {
+ e.preventDefault();
+ ZSSEditor.setBlockquote();
+ // When pressing enter inside an image caption, clear the caption styling from the new line
+ } else if (parentNode.nodeName == NodeName.SPAN && $(parentNode).hasClass('wp-caption')) {
+ setTimeout(this.handleCaptionNewLine, 100);
+ }
+ }
+ }
+};
+
+ZSSField.prototype.handleInputEvent = function(e) {
+
+ // Skip this if we are composing on an IME keyboard
+ if (this.isComposing ) { return; }
+
+ // IMPORTANT: we want the placeholder to come up if there's no text, so we clear the field if
+ // there's no real content in it. It's important to do this here and not on keyDown or keyUp
+ // as the field could become empty because of a cut or paste operation as well as a key press.
+ // This event takes care of all cases.
+ //
+ this.emptyFieldIfNoContents();
+
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+ ZSSEditor.callback('callback-selection-changed', joinedArguments);
+ this.callback("callback-input", joinedArguments);
+};
+
+ZSSField.prototype.handleCompositionStartEvent = function(e) {
+ this.isComposing = true;
+};
+
+ZSSField.prototype.handleCompositionEndEvent = function(e) {
+ this.isComposing = false;
+};
+
+ZSSField.prototype.handleTapEvent = function(e) {
+ var targetNode = e.target;
+
+ if (targetNode) {
+
+ ZSSEditor.lastTappedNode = targetNode;
+
+ if (targetNode.nodeName.toLowerCase() == 'a') {
+ var arguments = ['url=' + encodeURIComponent(targetNode.href),
+ 'title=' + encodeURIComponent(targetNode.innerHTML)];
+ var joinedArguments = arguments.join(defaultCallbackSeparator);
+ this.callback('callback-link-tap', joinedArguments);
+ }
+
+ if (targetNode.nodeName.toLowerCase() == 'img') {
+ // If the image is uploading, or is a local image do not select it.
+ if ( targetNode.dataset.wpid || targetNode.dataset.video_wpid ) {
+ this.sendImageTappedCallback(targetNode);
+ return;
+ }
+
+ // If we're not currently editing just return. No need to apply styles
+ // or acknowledge the tap
+ if ( this.wrappedObject.attr('contenteditable') != "true" ) {
+ return;
+ }
+
+ // Is the tapped image the image we're editing?
+ if ( targetNode == ZSSEditor.currentEditingImage ) {
+ ZSSEditor.removeImageSelectionFormatting(targetNode);
+ this.sendImageTappedCallback(targetNode);
+ return;
+ }
+
+ // If there is a selected image, deselect it. A different image was tapped.
+ if ( ZSSEditor.currentEditingImage ) {
+ ZSSEditor.removeImageSelectionFormatting(ZSSEditor.currentEditingImage);
+ }
+
+ // Format and flag the image as selected.
+ ZSSEditor.currentEditingImage = targetNode;
+ var containerNode = ZSSEditor.applyImageSelectionFormatting(targetNode);
+
+ // Move the cursor to the tapped image, to prevent scrolling to the bottom of the document when the
+ // keyboard comes up. On API 19 and below does not work properly, with the image sometimes getting removed
+ // from the post instead of the edit overlay being displayed
+ if (nativeState.androidApiLevel > 19) {
+ ZSSEditor.setFocusAfterElement(containerNode);
+ }
+
+ return;
+ }
+
+ if (targetNode.className.indexOf('edit-overlay') != -1 || targetNode.className.indexOf('edit-content') != -1
+ || targetNode.className.indexOf('edit-icon') != -1) {
+ ZSSEditor.removeImageSelectionFormatting( ZSSEditor.currentEditingImage );
+
+ this.sendImageTappedCallback( ZSSEditor.currentEditingImage );
+ return;
+ }
+
+ if (targetNode.className.indexOf('upload-overlay') != -1 ||
+ targetNode.className.indexOf('upload-overlay-bg') != -1 ) {
+ // Select the image node associated with the selected upload overlay
+ var imageNode = targetNode.parentNode.getElementsByTagName('img')[0];
+
+ this.sendImageTappedCallback( imageNode );
+ return;
+ }
+
+ if (targetNode.className.indexOf('delete-overlay') != -1) {
+ var parentEditContainer = targetNode.parentElement;
+ var parentDiv = parentEditContainer.parentElement;
+
+ // If the delete button was tapped, removing the media item and its container from the document
+ if (parentEditContainer.classList.contains('edit-container')) {
+ parentEditContainer.parentElement.removeChild(parentEditContainer);
+ } else {
+ parentEditContainer.removeChild(targetNode);
+ }
+
+ this.emptyFieldIfNoContents();
+
+ ZSSEditor.currentEditingImage = null;
+ return;
+ }
+
+ if ( ZSSEditor.currentEditingImage ) {
+ ZSSEditor.removeImageSelectionFormatting( ZSSEditor.currentEditingImage );
+ ZSSEditor.currentEditingImage = null;
+ }
+
+ if (targetNode.nodeName.toLowerCase() == 'video') {
+ // If the video is uploading, or is a local image do not select it.
+ if (targetNode.dataset.wpvideopress) {
+ if (targetNode.src.length == 0 || targetNode.src == 'file:///android_asset/') {
+ // If the tapped video is a placeholder for a VideoPress video, send out an update request.
+ // This provides a way to load the video for Android API<19, where the onError property function in
+ // the placeholder video isn't being triggered, and sendVideoPressInfoRequest is never called.
+ // This is also used to manually retry loading a VideoPress video after the onError attribute has
+ // been stripped for the video tag.
+ targetNode.setAttribute("onerror", "");
+ ZSSEditor.sendVideoPressInfoRequest(targetNode.dataset.wpvideopress);
+ return;
+ }
+ }
+
+ if (targetNode.dataset.wpid) {
+ this.sendVideoTappedCallback( targetNode );
+ return;
+ }
+ }
+ }
+};
+
+ZSSField.prototype.handlePasteEvent = function(e) {
+ if (this.isMultiline() && this.getHTML().length == 0) {
+ ZSSEditor.insertHTML(Util.wrapHTMLInTag('&#x200b;', ZSSEditor.defaultParagraphSeparator));
+ }
+};
+
+/**
+ * @brief Fires after 'keydown' events, when the field contents have already been modified
+ */
+ZSSField.prototype.afterKeyDownEvent = function(beforeHTML, e) {
+ var afterHTML = e.target.innerHTML;
+ var htmlWasModified = (beforeHTML != afterHTML);
+
+ var selection = document.getSelection();
+ var range = selection.getRangeAt(0).cloneRange();
+ var focusedNode = range.startContainer;
+
+ // Correct situation where autocorrect can remove blockquotes at start of document, either when pressing enter
+ // inside a blockquote, or pressing backspace immediately after one
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/385
+ if (htmlWasModified) {
+ var blockquoteMatch = beforeHTML.match('^<blockquote><div>(.*)</div></blockquote>');
+
+ if (blockquoteMatch != null && afterHTML.match('<blockquote>') == null) {
+ // Blockquote at start of post was removed
+ var newParagraphMatch = afterHTML.match('^<div>(.*?)</div><div><br></div>');
+
+ if (newParagraphMatch != null) {
+ // The blockquote was removed in a newline operation
+ var originalText = blockquoteMatch[1];
+ var newText = newParagraphMatch[1];
+
+ if (originalText != newText) {
+ // Blockquote was removed and its inner text changed - this points to autocorrect removing the
+ // blockquote when changing the text in the previous paragraph, so we replace the blockquote
+ ZSSEditor.turnBlockquoteOnForNode(focusedNode.parentNode.firstChild);
+ }
+ } else if (afterHTML.match('^<div>(.*?)</div>') != null) {
+ // The blockquote was removed in a backspace operation
+ ZSSEditor.turnBlockquoteOnForNode(focusedNode.parentNode);
+ ZSSEditor.setFocusAfterElement(focusedNode);
+ }
+ }
+ }
+
+ var focusedNodeIsEmpty = (focusedNode.innerHTML != undefined
+ && (focusedNode.innerHTML.length == 0 || focusedNode.innerHTML == '<br>'));
+
+ // Blockquote handling
+ if (focusedNode.nodeName == NodeName.BLOCKQUOTE && focusedNodeIsEmpty) {
+ if (!htmlWasModified) {
+ // We only want to handle this if the last character inside a blockquote was just deleted - if the HTML
+ // is unchanged, it might be that afterKeyDownEvent was called too soon, and we should avoid doing anything
+ return;
+ }
+
+ // When using backspace to delete the contents of a blockquote, the div within the blockquote is deleted
+ // This makes the blockquote unable to be deleted using backspace, and also causes autocorrect issues on API19+
+ range.startContainer.innerHTML = Util.wrapHTMLInTag('<br>', ZSSEditor.defaultParagraphSeparator);
+
+ // Give focus to new div
+ var newFocusElement = focusedNode.firstChild;
+ ZSSEditor.giveFocusToElement(newFocusElement, 1);
+ } else if (focusedNode.nodeName == NodeName.DIV && focusedNode.parentNode.nodeName == NodeName.BLOCKQUOTE) {
+ if (focusedNode.parentNode.previousSibling == null && focusedNode.parentNode.childNodes.length == 1
+ && focusedNodeIsEmpty) {
+ // When a post begins with a blockquote, and there's content after that blockquote, backspacing inside that
+ // blockquote will work until the blockquote is empty. After that, backspace will have no effect
+ // This fix identifies that situation and makes the call to setBlockquote() to toggle off the blockquote
+ ZSSEditor.setBlockquote();
+ } else {
+ // Remove extraneous break tags sometimes added to blockquotes by autocorrect actions
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/385
+ var blockquoteChildNodes = focusedNode.parentNode.childNodes;
+
+ for (var i = 0; i < blockquoteChildNodes.length; i++) {
+ var childNode = blockquoteChildNodes[i];
+ if (childNode.nodeName == NodeName.BR) {
+ childNode.parentNode.removeChild(childNode);
+ }
+ }
+ }
+ }
+};
+
+ZSSField.prototype.sendImageTappedCallback = function(imageNode) {
+ var meta = JSON.stringify(ZSSEditor.extractImageMeta(imageNode));
+ var imageId = "", mediaType = "image";
+ if (imageNode.hasAttribute('data-wpid')){
+ imageId = imageNode.getAttribute('data-wpid');
+ } else if (imageNode.hasAttribute('data-video_wpid')){
+ imageId = imageNode.getAttribute('data-video_wpid');
+ mediaType = "video";
+ }
+ var arguments = ['id=' + encodeURIComponent(imageId),
+ 'url=' + encodeURIComponent(imageNode.src),
+ 'meta=' + encodeURIComponent(meta),
+ 'type=' + mediaType];
+
+ var joinedArguments = arguments.join(defaultCallbackSeparator);
+
+ var thisObj = this;
+
+ // WORKAROUND: force the event to become sort of "after-tap" through setTimeout()
+ //
+ setTimeout(function() { thisObj.callback('callback-image-tap', joinedArguments);}, 500);
+}
+
+ZSSField.prototype.sendVideoTappedCallback = function( videoNode ) {
+ var videoId = "";
+ if ( videoNode.hasAttribute( 'data-wpid' ) ){
+ videoId = videoNode.getAttribute( 'data-wpid' )
+ }
+ var arguments = ['id=' + encodeURIComponent( videoId ),
+ 'url=' + encodeURIComponent( videoNode.src )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ ZSSEditor.callback('callback-video-tap', joinedArguments);
+}
+
+// MARK: - Callback Execution
+
+ZSSField.prototype.callback = function(callbackScheme, callbackPath) {
+ var url = callbackScheme + ":";
+
+ url = url + "id=" + this.getNodeId();
+
+ if (callbackPath) {
+ url = url + defaultCallbackSeparator + callbackPath;
+ }
+
+ if (isUsingiOS) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else if (isUsingAndroid) {
+ if (nativeState.androidApiLevel < 17) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else {
+ nativeCallbackHandler.executeCallback(callbackScheme, callbackPath);
+ }
+ } else {
+ console.log(url);
+ }
+};
+
+// MARK: - Focus
+
+ZSSField.prototype.isFocused = function() {
+
+ return this.wrappedObject.is(':focus');
+};
+
+ZSSField.prototype.focus = function() {
+
+ if (!this.isFocused()) {
+ this.wrappedObject.focus();
+ }
+};
+
+ZSSField.prototype.blur = function() {
+ if (this.isFocused()) {
+ this.wrappedObject.blur();
+ }
+};
+
+// MARK: - Multiline support
+
+ZSSField.prototype.isMultiline = function() {
+ return this.multiline;
+};
+
+ZSSField.prototype.setMultiline = function(multiline) {
+ this.multiline = multiline;
+};
+
+// MARK: - NodeId
+
+ZSSField.prototype.getNodeId = function() {
+ return this.wrappedObject.attr('id');
+};
+
+// MARK: - Editing
+
+ZSSField.prototype.enableEditing = function () {
+
+ this.wrappedObject.attr('contenteditable', true);
+
+ if (!ZSSEditor.focusedField) {
+ ZSSEditor.focusFirstEditableField();
+ }
+};
+
+ZSSField.prototype.disableEditing = function () {
+ // IMPORTANT: we're blurring the field before making it non-editable since that ensures
+ // that the iOS keyboard is dismissed through an animation, as opposed to being immediately
+ // removed from the screen.
+ //
+ this.blur();
+
+ this.wrappedObject.attr('contenteditable', false);
+};
+
+// MARK: - Caret
+
+/**
+ * @brief Whenever this method is called, a check will be performed on the caret position
+ * to figure out if it needs to be wrapped in a paragraph node.
+ * @details A parent paragraph node should be added if the current parent is either the field
+ * node itself, or a blockquote node.
+ */
+ZSSField.prototype.wrapCaretInParagraphIfNecessary = function() {
+ var closerParentNode = ZSSEditor.closerParentNode();
+
+ if (closerParentNode == null) {
+ return;
+ }
+
+ var parentNodeShouldBeParagraph = (closerParentNode == this.getWrappedDomNode()
+ || closerParentNode.nodeName == NodeName.BLOCKQUOTE);
+
+ // When starting a post with a blockquote (before any text is entered), the paragraph tags inside the blockquote
+ // won't properly wrap the text once it's entered
+ // In that case, remove the paragraph tags and re-apply them, wrapping around the newly entered text
+ var fixNewPostBlockquoteBug = (closerParentNode.nodeName == NodeName.DIV
+ && closerParentNode.parentNode.nodeName == NodeName.BLOCKQUOTE
+ && closerParentNode.innerHTML.length == 0);
+
+ // On API 19 and below, identifying the situation where the blockquote bug for new posts occurs works a little
+ // differently than above, with the focused node being the parent blockquote rather than the empty div inside it.
+ // We still remove the empty div so it can be re-applied correctly to the newly entered text, but we select it
+ // differently
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/398
+ var fixNewPostBlockquoteBugOldApi = (closerParentNode.nodeName == NodeName.BLOCKQUOTE
+ && closerParentNode.parentNode.nodeName == NodeName.DIV
+ && closerParentNode.innerHTML == '<div></div>');
+
+ if (parentNodeShouldBeParagraph || fixNewPostBlockquoteBug || fixNewPostBlockquoteBugOldApi) {
+ var selection = window.getSelection();
+
+ if (selection && selection.rangeCount > 0) {
+ var range = selection.getRangeAt(0);
+
+ if (range.startContainer == range.endContainer) {
+ if (fixNewPostBlockquoteBug) {
+ closerParentNode.parentNode.removeChild(closerParentNode);
+ } else if (fixNewPostBlockquoteBugOldApi) {
+ closerParentNode.removeChild(closerParentNode.firstChild);
+ }
+
+ var storedStyles = [];
+ if (this.getWrappedDomNode().innerHTML.length == 0) {
+ // If the post is empty, store any active in-line formatting so it can be re-applied after the
+ // DOM manipulations are completed
+ // (Fix for https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/204)
+ storedStyles = ZSSEditor.storeInlineStylesAsFunctions();
+ }
+
+ var paragraph = document.createElement("div");
+ var textNode = document.createTextNode("&#x200b;");
+
+ paragraph.appendChild(textNode);
+
+ range.insertNode(paragraph);
+ range.selectNode(textNode);
+
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ // Re-apply inline styles that were cleared
+ storedStyles.map(function(styleFunction) {
+ styleFunction();
+ });
+ }
+ }
+ }
+};
+
+/**
+ * @brief Called when enter is pressed inside an image caption. Clears away the span and label tags the new line
+ * inherits from the caption styling.
+ */
+ZSSField.prototype.handleCaptionNewLine = function() {
+ var selectedNode = document.getSelection().baseNode;
+
+ var contentsNode;
+ if (selectedNode.firstChild != null) {
+ contentsNode = selectedNode.firstChild.cloneNode();
+ } else {
+ contentsNode = selectedNode.cloneNode();
+ }
+
+ var parentSpan = selectedNode.parentNode.parentNode;
+ var parentDiv = parentSpan.parentNode;
+
+ var paragraph = document.createElement("div");
+ paragraph.appendChild(contentsNode);
+
+ parentDiv.insertBefore(paragraph, parentSpan);
+ parentDiv.removeChild(parentSpan);
+
+ ZSSEditor.giveFocusToElement(contentsNode);
+};
+
+// MARK: - i18n
+
+ZSSField.prototype.isRightToLeftTextEnabled = function() {
+ var textDir = this.wrappedObject.attr('dir');
+ var isRTL = (textDir != "undefined" && textDir == 'rtl');
+ return isRTL;
+};
+
+ZSSField.prototype.enableRightToLeftText = function(isRTL) {
+ var textDirectionString = isRTL ? "rtl" : "ltr";
+ this.wrappedObject.attr('dir', textDirectionString);
+ this.wrappedObject.css('direction', textDirectionString);
+};
+
+// MARK: - HTML contents
+
+ZSSField.prototype.isEmpty = function() {
+ var html = this.getHTML();
+ var isEmpty = (html.length == 0 || html == "<br>");
+
+ return isEmpty;
+};
+
+ZSSField.prototype.getHTML = function() {
+ var html = this.wrappedObject.html();
+ if (ZSSEditor.defaultParagraphSeparator == 'div') {
+ html = Formatter.convertDivToP(html);
+ }
+ html = Formatter.visualToHtml(html);
+ html = ZSSEditor.removeVisualFormatting( html );
+ return html;
+};
+
+ZSSField.prototype.getHTMLForCallback = function() {
+ var functionArgument = "function=getHTMLForCallback";
+ var idArgument = "id=" + this.getNodeId();
+ var contentsArgument;
+
+ if (this.hasNoStyle) {
+ contentsArgument = "contents=" + this.strippedHTML();
+ } else {
+ var html;
+ if (nativeState.androidApiLevel < 17) {
+ // URI Encode HTML on API < 17 because of the use of WebViewClient.shouldOverrideUrlLoading. Data must
+ // be decoded in shouldOverrideUrlLoading.
+ html = encodeURIComponent(this.getHTML());
+ } else {
+ html = this.getHTML();
+ }
+ contentsArgument = "contents=" + html;
+ }
+ var joinedArguments = functionArgument + defaultCallbackSeparator + idArgument + defaultCallbackSeparator +
+ contentsArgument;
+ ZSSEditor.callback('callback-response-string', joinedArguments);
+};
+
+ZSSField.prototype.strippedHTML = function() {
+ return this.wrappedObject.text();
+};
+
+ZSSField.prototype.setPlainText = function(text) {
+ ZSSEditor.currentEditingImage = null;
+ this.wrappedObject.text(text);
+};
+
+ZSSField.prototype.setHTML = function(html) {
+ ZSSEditor.currentEditingImage = null;
+ var mutatedHTML = Formatter.htmlToVisual(html);
+
+ if (ZSSEditor.defaultParagraphSeparator == 'div') {
+ mutatedHTML = Formatter.convertPToDiv(mutatedHTML);
+ }
+
+ this.wrappedObject.html(mutatedHTML);
+
+ // Track video container nodes for mutation
+ var videoNodes = $('span.edit-container > video');
+ for (var i = 0; i < videoNodes.length; i++) {
+ ZSSEditor.trackNodeForMutation($(videoNodes[i].parentNode));
+ }
+};
+
+// MARK: - Placeholder
+
+ZSSField.prototype.hasPlaceholderText = function() {
+ return this.wrappedObject.attr('placeholderText') != null;
+};
+
+ZSSField.prototype.setPlaceholderText = function(placeholder) {
+ this.wrappedObject.attr('placeholderText', placeholder);
+};
+
+// MARK: - Wrapped Object
+
+ZSSField.prototype.getWrappedDomNode = function() {
+ return this.wrappedObject[0];
+};
diff --git a/libs/editor/WordPressEditor/src/main/assets/android-editor.html b/libs/editor/WordPressEditor/src/main/assets/android-editor.html
new file mode 100755
index 000000000..d70c11fac
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/android-editor.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>%%TITLE%%</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+ <script type="text/javascript">
+ var nativeState = new Object();
+ nativeState.androidApiLevel=%%ANDROID_API_LEVEL%%;
+ %%LOCALIZED_STRING_INIT%%
+ </script>
+ <script src="libs/jquery-2.1.3.min.js"></script>
+ <script src="libs/js-beautifier.js"></script>
+ <script src="libs/underscore-min.js"></script>
+ <script src="libs/shortcode.js"></script>
+ <script src="libs/jquery.mobile-events.min.js"></script>
+ <script src="libs/wpload.js"></script>
+ <script src="libs/wpsave.js"></script>
+ <script src="libs/rangy-core.js"></script>
+ <script src="libs/rangy-classapplier.js"></script>
+ <script src="libs/rangy-highlighter.js"></script>
+ <script src="libs/rangy-selectionsaverestore.js"></script>
+ <script src="libs/rangy-serializer.js"></script>
+ <script src="libs/rangy-textrange.js"></script>
+ <script src="editor-utils.js"></script>
+ <script src="editor-utils-formatter.js"></script>
+ <script src="ZSSRichTextEditor.js"></script>
+ <script>
+ // DRM: onLoad does not get called when offline, if there's remote content in the editor
+ // (such as remote images). We use the 'ready' event for this reason.
+ //
+ $(document).ready(function() {
+ ZSSEditor.init();
+ ZSSEditor.domLoadedCallback();
+ });
+ </script>
+ <link rel="stylesheet" href="editor.css">
+ <link rel="stylesheet" href="editor-android.css">
+ </head>
+ <body>
+ <div contenteditable="true" id="zss_field_title" class="field" nostyle>
+ </div>
+ <div contenteditable="false" id="separatorDiv" >
+ <hr>
+ </div>
+ <div contenteditable="true" id="zss_field_content" class="field">
+ </div>
+ </body>
+</html>
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor-android.css b/libs/editor/WordPressEditor/src/main/assets/editor-android.css
new file mode 100644
index 000000000..cd57c2424
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor-android.css
@@ -0,0 +1,95 @@
+@media screen and (min-width: 720px) and (max-width: 1279px) {
+ body {
+ padding-left:90px;
+ padding-right:90px;
+ }
+}
+
+@media screen and (min-width: 1280px){
+ body {
+ padding-left:170px;
+ padding-right:170px;
+ }
+}
+
+video::-webkit-media-controls-fullscreen-button {
+ display: none;
+}
+
+/* Duplicates paragraph tag formatting for div tags, which are needed on Android API 19+ due to autocorrect issues:
+https://bugs.chromium.org/p/chromium/issues/detail?id=599890
+*/
+div:not(.field):not(#separatorDiv) {
+ line-height: 24px;
+ margin-top: 0px;
+ margin-bottom: 24px;
+}
+
+/* --- API<19 workarounds --- */
+
+/* Used only on older APIs (API<19), which don't support CSS filter effects (specifically, blur). */
+.edit-container .edit-overlay-bg {
+ position: absolute;
+ top: -6px;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: block;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+/* Used only on older APIs (API<19), where using inline-block is buggy and sometimes displays a very small container */
+span.img_container.compat,
+span.video_container.compat {
+ display: block;
+}
+
+/* Used on API<19 to darken the image so that the 'uploading' and 'retry' overlays can still be seen when the image is
+light */
+.img_container .upload-overlay-bg,
+.video_container .upload-overlay-bg {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: block;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+/* When the upload-overlay-bg element is present (API<19), a bug is exposed where the img_container is slightly larger
+than its containing image. The upload-overlay-bg is larger as well, leaving a dark line below the image. By setting
+display:block on the image and setting a width limit we get around this issue. */
+
+.img_container .upload-overlay-bg ~ img.uploading,
+.video_container .upload-overlay-bg ~ img.uploading {
+ display:block;
+ max-width:100%;
+}
+
+.img_container .upload-overlay-bg ~ img.failed,
+.video_container .upload-overlay-bg ~ img.failed{
+ display:block;
+ max-width:100%;
+}
+
+/* Used only on older APIs (API<19) instead of a progress bar for uploading images, since the WebView at those API
+ levels does not support the progress tag */
+.img_container .upload-overlay,
+.video_container .upload-overlay{
+ position: absolute;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ width:100%;
+ z-index: 100;
+ min-width: 60px;
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight:600;
+ text-align: center;
+ text-shadow: 0px 1px 2px rgba(0,0,0,.06);
+ color: white;
+}
+
+.img_container .upload-overlay.failed,
+.video_container .upload-overlay.failed{
+ visibility: hidden;
+}
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js b/libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js
new file mode 100644
index 000000000..69b0254ca
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js
@@ -0,0 +1,158 @@
+function Formatter () {}
+
+// Video format tags supported by the [video] shortcode: https://codex.wordpress.org/Video_Shortcode
+// mp4, m4v and webm prioritized since they're supported by the stock player as of Android API 23
+Formatter.videoShortcodeFormats = ["mp4", "m4v", "webm", "ogv", "wmv", "flv"];
+
+Formatter.htmlToVisual = function(html) {
+ var mutatedHTML = wp.loadText(html);
+
+ // Perform extra transformations to properly wrap captioned images in paragraphs
+ mutatedHTML = mutatedHTML.replace(/^\[caption([^\]]*\])/igm, '<p>[caption$1');
+ mutatedHTML = mutatedHTML.replace(/([^\n>])\[caption/igm, '$1<br />\n[caption');
+ mutatedHTML = mutatedHTML.replace(/\[\/caption\]\n(?=<|$)/igm, '[/caption]</p>\n');
+ mutatedHTML = mutatedHTML.replace(/\[\/caption\]\n(?=[^<])/igm, '[/caption]<br />\n');
+
+ return Formatter.applyVisualFormatting(mutatedHTML);
+}
+
+Formatter.convertPToDiv = function(html) {
+ // Replace the paragraph tags we get from wpload with divs
+ var mutatedHTML = html.replace(/(<p(?=[>\s]))/igm, '<div').replace(/<\/p>/igm, '</div>');
+
+ // Replace break tags around media items with paragraphs
+ // The break tags appear when text and media are separated by only a line break rather than a paragraph break,
+ // which can happen when inserting media inline and switching to HTML mode and back, or by deleting line breaks
+ // in HTML mode
+ mutatedHTML = mutatedHTML.replace(/<br \/>(?=\s*(<img|<a href|<label|<video|<span class="edit-container"))/igm,
+ '</div><div>');
+ mutatedHTML = mutatedHTML.replace(/(<img [^<>]*>|<\/a>|<\/label>|<\/video>|<\/span>)<br \/>/igm,
+ function replaceBrWithDivs(match) { return match.substr(0, match.length - 6) + '</div><div>'; });
+
+ // Append paragraph-wrapped break tag under media at the end of a post
+ mutatedHTML = mutatedHTML.replace(/(<img [^<>]*>|<\/a>|<\/label>|<\/video>|<\/span>)[^<>]*<\/div>\s$/igm,
+ function replaceBrWithDivs(match) { return match + '<div><br></div>'; });
+
+ return mutatedHTML;
+}
+
+Formatter.visualToHtml = function(html) {
+ return wp.saveText(html);
+ return Formatter.removeVisualFormatting(mutatedHTML);
+}
+
+Formatter.convertDivToP = function(html) {
+ return html.replace(/(<div(?=[>\s]))/igm, '<p').replace(/<\/div>/igm, '</p>');
+}
+
+/**
+ * @brief Applies editor specific visual formatting.
+ *
+ * @param html The markup to format
+ *
+ * @return Returns the string with the visual formatting applied.
+ */
+Formatter.applyVisualFormatting = function(html) {
+ var str = wp.shortcode.replace('caption', html, Formatter.applyCaptionFormatting);
+ str = wp.shortcode.replace('wpvideo', str, Formatter.applyVideoPressFormattingCallback);
+ str = wp.shortcode.replace('video', str, Formatter.applyVideoFormattingCallback);
+
+ // More tag
+ str = str.replace(/<!--more(.*?)-->/igm, "<hr class=\"more-tag\" wp-more-data=\"$1\">")
+ str = str.replace(/<!--nextpage-->/igm, "<hr class=\"nextpage-tag\">")
+ return str;
+}
+
+/**
+ * @brief Adds visual formatting to a caption shortcodes.
+ *
+ * @param html The markup containing caption shortcodes to process.
+ *
+ * @return The html with caption shortcodes replaced with editor specific markup.
+ * See shortcode.js::next or details
+ */
+Formatter.applyCaptionFormatting = function(match) {
+ var attrs = match.attrs.named;
+ // The empty 'onclick' is important. It prevents the cursor jumping to the end
+ // of the content body when `-webkit-user-select: none` is set and the caption is tapped.
+ var out = '<label class="wp-temp" data-wp-temp="caption" onclick="">';
+ out += '<span class="wp-caption"';
+
+ if (attrs.width) {
+ out += ' style="width:' + attrs.width + 'px; max-width:100% !important;"';
+ }
+ for (var key in attrs) {
+ out += " data-caption-" + key + '="' + attrs[key] + '"';
+ }
+
+ out += '>';
+ out += match.content;
+ out += '</span>';
+ out += '</label>';
+
+ return out;
+}
+
+Formatter.applyVideoPressFormattingCallback = function(match) {
+ if (match.attrs.numeric.length == 0) {
+ return match.content;
+ }
+ var videopressID = match.attrs.numeric[0];
+ var posterSVG = '"svg/wpposter.svg"';
+ // The empty 'onclick' is important. It prevents the cursor jumping to the end
+ // of the content body when `-webkit-user-select: none` is set and the video is tapped.
+ var out = '<video data-wpvideopress="' + videopressID + '" webkit-playsinline src="" preload="metadata" poster='
+ + posterSVG +' onclick="" onerror="ZSSEditor.sendVideoPressInfoRequest(\'' + videopressID +'\');"></video>';
+
+ // Wrap video in edit-container node for a permanent delete button overlay
+ var containerStart = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>';
+ out = containerStart + out + '</span>';
+
+ return out;
+}
+
+Formatter.applyVideoFormattingCallback = function(match) {
+ // Find the tag containing the video source
+ var srcTag = "";
+
+ if (match.attrs.named['src']) {
+ srcTag = "src";
+ } else {
+ for (var i = 0; i < Formatter.videoShortcodeFormats.length; i++) {
+ var format = Formatter.videoShortcodeFormats[i];
+ if (match.attrs.named[format]) {
+ srcTag = format;
+ break;
+ }
+ }
+ }
+
+ if (srcTag.length == 0) {
+ return match.content;
+ }
+
+ var out = '<video webkit-playsinline src="' + match.attrs.named[srcTag] + '"';
+
+ // Preserve all existing tags
+ for (var item in match.attrs.named) {
+ if (item != srcTag) {
+ out += ' ' + item + '="' + match.attrs.named[item] + '"';
+ }
+ }
+
+ if (!match.attrs.named['preload']) {
+ out += ' preload="metadata"';
+ }
+
+ out += ' onclick="" controls="controls"></video>';
+
+ // Wrap video in edit-container node for a permanent delete button overlay
+ var containerStart = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>';
+ out = containerStart + out + '</span>';
+
+ return out;
+}
+
+if (typeof module !== 'undefined' && module.exports != null) {
+ exports.Formatter = Formatter;
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor-utils.js b/libs/editor/WordPressEditor/src/main/assets/editor-utils.js
new file mode 100644
index 000000000..5c211d153
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor-utils.js
@@ -0,0 +1,26 @@
+function Util () {}
+
+/* Tag building */
+
+Util.buildOpeningTag = function(tagName) {
+ return '<' + tagName + '>';
+};
+
+Util.buildClosingTag = function(tagName) {
+ return '</' + tagName + '>';
+};
+
+Util.wrapHTMLInTag = function(html, tagName) {
+ return Util.buildOpeningTag(tagName) + html + Util.buildClosingTag(tagName);
+};
+
+/* Selection */
+
+Util.rangeIsAtStartOfParent = function(range) {
+ return (range.startContainer.previousSibling == null && range.startOffset == 0);
+};
+
+Util.rangeIsAtEndOfParent = function(range) {
+ return ((range.startContainer.nextSibling == null || range.startContainer.nextSibling == "<br>")
+ && range.endOffset == range.endContainer.length);
+}; \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor.css b/libs/editor/WordPressEditor/src/main/assets/editor.css
new file mode 100644
index 000000000..4e7fa07ba
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor.css
@@ -0,0 +1,456 @@
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather Light"),
+ url('fonts/Merriweather-Light.ttf');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather-Italic"),
+ url('fonts/Merriweather-Italic.ttf');
+ font-weight: normal;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather-Bold"),
+ url('fonts/Merriweather-Bold.ttf');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather-BoldItalic"),
+ url('fonts/Merriweather-BoldItalic.ttf');
+ font-weight: bold;
+ font-style: italic;
+}
+
+* {outline: 0px solid transparent;
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+ -webkit-touch-callout: none;
+}
+
+html {
+ padding:0;
+ box-sizing: border-box;
+}
+
+html, body {
+ margin:0;
+ font-family:'Merriweather', sans-serif;
+ font-size:1em;
+ color:#2D2D2D;
+}
+
+body {
+ padding-left:15px;
+ padding-right:15px;
+ padding-top: 15px;
+ padding-bottom: 5px;
+ overflow-y: auto;
+ min-height: 100vh;
+ word-wrap: break-word;
+}
+
+p {
+ line-height: 24px;
+ margin-top: 0px;
+ margin-bottom: 24px;
+}
+
+hr {
+ border: none;
+ height: 1px;
+ color: #E9EFF3;
+ background-color: #E9EFF3;
+ width: 100%;
+}
+
+img {
+ width: auto;
+ height: auto;
+ margin: 0px 0 0px 0;
+ min-width: 30px;
+ min-height: 30px;
+ max-width: 100%;
+ opacity:1;
+}
+
+video {
+ width: auto;
+ height: auto;
+ margin: 0px 0px 0px 0px;
+ min-width: 30px;
+ min-height: 30px;
+ max-width: 100%;
+ opacity:1;
+ background:#2e4453
+}
+
+a {
+ color: #0087be;
+ text-decoration: none;
+}
+
+blockquote {
+ background: #e8f0f5;
+ padding: 10px 10px 10px 20px;
+ margin: 10px 0 10px 0;
+ border-radius: 2px;
+}
+
+img.zs_active {
+ border: 2px dashed #000;
+}
+
+img.uploading {
+ opacity:0.3;
+ -webkit-user-select: none;
+}
+
+img.failed {
+ -webkit-filter: blur(4px) grayscale(0.3);
+ margin:-1px;
+ padding:1px;
+ z-index:-1;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+span.img_container {
+ position: relative;
+ display: inline-block;
+ -webkit-user-select: none;
+}
+
+span.img_container.failed {
+ overflow: hidden;
+}
+
+span.img_container.failed::before {
+ position: absolute;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ content:"";
+ left: 0;
+ right: 0;
+ width: 48px;
+ height: 48px;
+ display: block;
+ margin: auto;
+ z-index: 100;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background: url('svg/retry-image.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+ -webkit-user-select: none;
+ pointer-events: none;
+}
+
+span.img_container.failed.largeFail::before {
+ height: 120px;
+ width: 120px;
+ border: 4px solid #FFFFFF;
+ background: url('svg/retry-image-large.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+span.img_container.failed::after {
+ position: absolute;
+ padding: 37px 0 0 0;
+ top:50%;
+ left:0%;
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight: 500;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0,0,0,.06);
+ background:clear;
+ color: white;
+ width:100%;
+ height:50%;
+ -webkit-user-select: none;
+ pointer-events: none;
+ content:attr(data-failed);
+}
+
+span.img_container.failed.largeFail::after {
+ padding: 72px 0 0 0;
+}
+
+span.img_container.failed.smallFail::after {
+ font-family: sans-serif;
+ content:attr(data-failed);
+}
+
+video.uploading {
+ opacity:0.3;
+ -webkit-user-select: none;
+}
+
+video.failed {
+ -webkit-filter: blur(4px) grayscale(0.3);
+ margin:-1px;
+ padding:1px;
+ z-index:-1;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+span.video_container {
+ position: relative;
+ display: inline-block;
+ -webkit-user-select: none;
+}
+
+span.video_container.failed {
+ overflow: hidden;
+}
+
+span.video_container.failed::before {
+ position: absolute;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ content:"";
+ left: 0;
+ right: 0;
+ width: 48px;
+ height: 48px;
+ display: block;
+ margin: auto;
+ z-index: 100;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background: url('svg/retry-image.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+ -webkit-user-select: none;
+ pointer-events: none;
+}
+
+span.video_container.failed.largeFail::before {
+ height: 120px;
+ width: 120px;
+ border: 4px solid #FFFFFF;
+ background: url('svg/retry-image-large.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+span.video_container.failed::after {
+ position: absolute;
+ padding: 37px 0 0 0;
+ top:50%;
+ left:0%;
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight: 500;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0,0,0,.06);
+ background:clear;
+ color: white;
+ width:100%;
+ height:50%;
+ -webkit-user-select: none;
+ pointer-events: none;
+ content:attr(data-failed);
+}
+
+span.video_container.failed.largeFail::after {
+ padding: 72px 0 0 0;
+}
+
+span.video_container.failed.smallFail::after {
+ font-family: sans-serif;
+ content:attr(data-failed);
+}
+
+/* Image and video editing overlay styles */
+.edit-container {
+ position: relative;
+ display: inline-block;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+.edit-container img {
+ -webkit-filter: blur(4px) grayscale(0.3);
+ margin:-1px; /*tiny margin to keep crisp edges when blurring the image*/
+ padding:1px;
+}
+
+.edit-container video {
+
+}
+
+/* default. use when images are > 100px w/h */
+.edit-container .edit-overlay {
+ position: absolute;
+ width: 100%;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ z-index: 100;
+ height:90px;
+ min-height: 90px;
+ min-width: 60px;
+}
+
+/* use when the image is < 100px w/h */
+.edit-container.small .edit-overlay {
+ height: 32px;
+ min-height: 32px;
+ min-width: 32px;
+}
+
+.edit-container .edit-icon {
+ width: 48px;
+ height: 48px;
+ display: block;
+ margin: auto;
+ z-index: 100;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background: url('svg/edit-image.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.edit-container.small .edit-icon {
+ height: 32px;
+ min-height: 32px;
+ min-width: 32px;
+ border: none;
+ border-radius: 0%;
+ background-color: initial;
+}
+
+.edit-container .delete-overlay {
+ position: absolute;
+ z-index: 100;
+ right: 0%;
+ height: 24px;
+ width: 24px;
+ min-height: 24px;
+ min-width: 24px;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background:url('svg/delete-image.svg') no-repeat center;
+ background-color: rgba(0,0,0,0.1);
+ margin-top: 12px;
+ margin-right: 12px;
+}
+
+.edit-container.small .edit-content {
+ display:none;
+}
+
+.edit-container .edit-content {
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight:500;
+ text-align: center;
+ text-shadow: 0px 1px 2px rgba(0,0,0,.06);
+ color: white;
+ -webkit-user-select: none;
+ pointer-events: none;
+ position: absolute;
+ bottom: 0;
+ width:100%;
+}
+
+progress.wp_media_indicator {
+ /* Reset the default appearance */
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ appearance: none;
+ position: absolute;
+ top:-2pt; height:2pt;
+ left:0px; width:100%;
+}
+
+progress.wp_media_indicator::-webkit-progress-bar {
+ background-color: rgba(232,240,247,1.0);
+}
+
+progress.wp_media_indicator::-webkit-progress-value {
+ background-color:rgba(0,135,190,1.0);
+ border-radius: 0 2pt 2pt 0;
+}
+
+progress.wp_media_indicator.failed::-webkit-progress-bar {
+ background-color: rgba(232,232,232,1.0);
+}
+
+progress.wp_media_indicator.failed::-webkit-progress-value {
+ background-color:rgba(135,135,135,1.0);
+ border-radius: 0 2pt 2pt 0;
+}
+
+
+div.field[contenteditable] {
+ box-sizing: border-box;
+ font-size: 16px;
+}
+
+div.field[placeholderText][contenteditable=true]:empty:before {
+ content: attr(placeholderText);
+ color: #87a6bc;
+ transition: 0.2s ease opacity;
+}
+
+div.field[placeholderText][contenteditable=true]:empty:focus:before {
+ opacity: 0.6;
+}
+
+#separatorDiv {
+ -webkit-user-select: none;
+ padding-top: 5px;
+ padding-bottom: 15px;
+}
+
+#zss_field_title, #zss_field_title p {
+ font-family:'Merriweather', serif;
+ font-weight: bold;
+ font-size: 24px;
+ line-height: 32px;
+ margin-bottom: 0px;
+}
+
+#zss_field_content {
+ font-size: 16px;
+ line-height: 24px;
+ margin-bottom: 10px;
+}
+
+.wp-temp[data-wp-temp="caption"] {
+ -webkit-user-select: none;
+}
+
+.wp-caption {
+ padding: 0px 10px 10px 0px;
+ font-size: 75%;
+ font-style: italic;
+ cursor: none;
+}
+
+.ipad_body {
+ padding-left: 80px;
+ padding-right: 80px;
+}
+
+.more-tag {
+ border: 0;
+ width: 96%;
+ height: 16px;
+ display: block;
+ background: transparent url('svg/more-2x.png') repeat-y scroll center center;
+ background-size: 1900px 20px;
+}
+
+.nextpage-tag {
+ border: 0;
+ width: 96%;
+ height: 16px;
+ display: block;
+ background: transparent url('svg/pagebreak-2x.png') repeat-y scroll center center;
+ background-size: 1900px 20px;
+}
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttf
new file mode 100755
index 000000000..2340777c8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttf
new file mode 100755
index 000000000..f67d7e588
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttf
new file mode 100755
index 000000000..583358652
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttf
new file mode 100755
index 000000000..ee7adbd44
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttf
new file mode 100755
index 000000000..28a80e487
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js b/libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js
new file mode 100644
index 000000000..35a81d683
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js
@@ -0,0 +1,821 @@
+/**
+ * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
+ *
+ * @version 1.0.3
+ * @codingstandard ftlabs-jsv2
+ * @copyright The Financial Times Limited [All Rights Reserved]
+ * @license MIT License (see LICENSE.txt)
+ */
+
+/*jslint browser:true, node:true*/
+/*global define, Event, Node*/
+
+
+/**
+ * Instantiate fast-clicking listeners on the specified layer.
+ *
+ * @constructor
+ * @param {Element} layer The layer to listen on
+ * @param {Object} options The options to override the defaults
+ */
+function FastClick(layer, options) {
+ 'use strict';
+ var oldOnClick;
+
+ options = options || {};
+
+ /**
+ * Whether a click is currently being tracked.
+ *
+ * @type boolean
+ */
+ this.trackingClick = false;
+
+
+ /**
+ * Timestamp for when click tracking started.
+ *
+ * @type number
+ */
+ this.trackingClickStart = 0;
+
+
+ /**
+ * The element being tracked for a click.
+ *
+ * @type EventTarget
+ */
+ this.targetElement = null;
+
+
+ /**
+ * X-coordinate of touch start event.
+ *
+ * @type number
+ */
+ this.touchStartX = 0;
+
+
+ /**
+ * Y-coordinate of touch start event.
+ *
+ * @type number
+ */
+ this.touchStartY = 0;
+
+
+ /**
+ * ID of the last touch, retrieved from Touch.identifier.
+ *
+ * @type number
+ */
+ this.lastTouchIdentifier = 0;
+
+
+ /**
+ * Touchmove boundary, beyond which a click will be cancelled.
+ *
+ * @type number
+ */
+ this.touchBoundary = options.touchBoundary || 10;
+
+
+ /**
+ * The FastClick layer.
+ *
+ * @type Element
+ */
+ this.layer = layer;
+
+ /**
+ * The minimum time between tap(touchstart and touchend) events
+ *
+ * @type number
+ */
+ this.tapDelay = options.tapDelay || 200;
+
+ if (FastClick.notNeeded(layer)) {
+ return;
+ }
+
+ // Some old versions of Android don't have Function.prototype.bind
+ function bind(method, context) {
+ return function() { return method.apply(context, arguments); };
+ }
+
+
+ var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
+ var context = this;
+ for (var i = 0, l = methods.length; i < l; i++) {
+ context[methods[i]] = bind(context[methods[i]], context);
+ }
+
+ // Set up event handlers as required
+ if (deviceIsAndroid) {
+ layer.addEventListener('mouseover', this.onMouse, true);
+ layer.addEventListener('mousedown', this.onMouse, true);
+ layer.addEventListener('mouseup', this.onMouse, true);
+ }
+
+ layer.addEventListener('click', this.onClick, true);
+ layer.addEventListener('touchstart', this.onTouchStart, false);
+ layer.addEventListener('touchmove', this.onTouchMove, false);
+ layer.addEventListener('touchend', this.onTouchEnd, false);
+ layer.addEventListener('touchcancel', this.onTouchCancel, false);
+
+ // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
+ // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
+ // layer when they are cancelled.
+ if (!Event.prototype.stopImmediatePropagation) {
+ layer.removeEventListener = function(type, callback, capture) {
+ var rmv = Node.prototype.removeEventListener;
+ if (type === 'click') {
+ rmv.call(layer, type, callback.hijacked || callback, capture);
+ } else {
+ rmv.call(layer, type, callback, capture);
+ }
+ };
+
+ layer.addEventListener = function(type, callback, capture) {
+ var adv = Node.prototype.addEventListener;
+ if (type === 'click') {
+ adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
+ if (!event.propagationStopped) {
+ callback(event);
+ }
+ }), capture);
+ } else {
+ adv.call(layer, type, callback, capture);
+ }
+ };
+ }
+
+ // If a handler is already declared in the element's onclick attribute, it will be fired before
+ // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
+ // adding it as listener.
+ if (typeof layer.onclick === 'function') {
+
+ // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
+ // - the old one won't work if passed to addEventListener directly.
+ oldOnClick = layer.onclick;
+ layer.addEventListener('click', function(event) {
+ oldOnClick(event);
+ }, false);
+ layer.onclick = null;
+ }
+}
+
+
+/**
+ * Android requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
+
+
+/**
+ * iOS requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
+
+
+/**
+ * iOS 4 requires an exception for select elements.
+ *
+ * @type boolean
+ */
+var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
+
+
+/**
+ * iOS 6.0(+?) requires the target element to be manually derived
+ *
+ * @type boolean
+ */
+var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
+
+/**
+ * BlackBerry requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
+
+/**
+ * Determine whether a given element requires a native click.
+ *
+ * @param {EventTarget|Element} target Target DOM element
+ * @returns {boolean} Returns true if the element needs a native click
+ */
+FastClick.prototype.needsClick = function(target) {
+ 'use strict';
+ switch (target.nodeName.toLowerCase()) {
+
+ // Don't send a synthetic click to disabled inputs (issue #62)
+ case 'button':
+ case 'select':
+ case 'textarea':
+ if (target.disabled) {
+ return true;
+ }
+
+ break;
+ case 'input':
+
+ // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
+ if ((deviceIsIOS && target.type === 'file') || target.disabled) {
+ return true;
+ }
+
+ break;
+ case 'label':
+ case 'video':
+ return true;
+ }
+
+ return (/\bneedsclick\b/).test(target.className);
+};
+
+
+/**
+ * Determine whether a given element requires a call to focus to simulate click into element.
+ *
+ * @param {EventTarget|Element} target Target DOM element
+ * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
+ */
+FastClick.prototype.needsFocus = function(target) {
+ 'use strict';
+ switch (target.nodeName.toLowerCase()) {
+ case 'textarea':
+ return true;
+ case 'select':
+ return !deviceIsAndroid;
+ case 'input':
+ switch (target.type) {
+ case 'button':
+ case 'checkbox':
+ case 'file':
+ case 'image':
+ case 'radio':
+ case 'submit':
+ return false;
+ }
+
+ // No point in attempting to focus disabled inputs
+ return !target.disabled && !target.readOnly;
+ default:
+ return (/\bneedsfocus\b/).test(target.className);
+ }
+};
+
+
+/**
+ * Send a click event to the specified element.
+ *
+ * @param {EventTarget|Element} targetElement
+ * @param {Event} event
+ */
+FastClick.prototype.sendClick = function(targetElement, event) {
+ 'use strict';
+ var clickEvent, touch;
+
+ // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
+ if (document.activeElement && document.activeElement !== targetElement) {
+ document.activeElement.blur();
+ }
+
+ touch = event.changedTouches[0];
+
+ // Synthesise a click event, with an extra attribute so it can be tracked
+ clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
+ clickEvent.forwardedTouchEvent = true;
+ targetElement.dispatchEvent(clickEvent);
+};
+
+FastClick.prototype.determineEventType = function(targetElement) {
+ 'use strict';
+
+ //Issue #159: Android Chrome Select Box does not open with a synthetic click event
+ if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
+ return 'mousedown';
+ }
+
+ return 'click';
+};
+
+
+/**
+ * @param {EventTarget|Element} targetElement
+ */
+FastClick.prototype.focus = function(targetElement) {
+ 'use strict';
+ var length;
+
+ // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
+ if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
+ length = targetElement.value.length;
+ targetElement.setSelectionRange(length, length);
+ } else {
+ targetElement.focus();
+ }
+};
+
+
+/**
+ * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
+ *
+ * @param {EventTarget|Element} targetElement
+ */
+FastClick.prototype.updateScrollParent = function(targetElement) {
+ 'use strict';
+ var scrollParent, parentElement;
+
+ scrollParent = targetElement.fastClickScrollParent;
+
+ // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
+ // target element was moved to another parent.
+ if (!scrollParent || !scrollParent.contains(targetElement)) {
+ parentElement = targetElement;
+ do {
+ if (parentElement.scrollHeight > parentElement.offsetHeight) {
+ scrollParent = parentElement;
+ targetElement.fastClickScrollParent = parentElement;
+ break;
+ }
+
+ parentElement = parentElement.parentElement;
+ } while (parentElement);
+ }
+
+ // Always update the scroll top tracker if possible.
+ if (scrollParent) {
+ scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
+ }
+};
+
+
+/**
+ * @param {EventTarget} targetElement
+ * @returns {Element|EventTarget}
+ */
+FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
+ 'use strict';
+
+ // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
+ if (eventTarget.nodeType === Node.TEXT_NODE) {
+ return eventTarget.parentNode;
+ }
+
+ return eventTarget;
+};
+
+
+/**
+ * On touch start, record the position and scroll offset.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchStart = function(event) {
+ 'use strict';
+ var targetElement, touch, selection;
+
+ // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
+ if (event.targetTouches.length > 1) {
+ return true;
+ }
+
+ targetElement = this.getTargetElementFromEventTarget(event.target);
+ touch = event.targetTouches[0];
+
+ if (deviceIsIOS) {
+
+ // Only trusted events will deselect text on iOS (issue #49)
+ selection = window.getSelection();
+ if (selection.rangeCount && !selection.isCollapsed) {
+ return true;
+ }
+
+ if (!deviceIsIOS4) {
+
+ // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
+ // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
+ // with the same identifier as the touch event that previously triggered the click that triggered the alert.
+ // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
+ // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
+ // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
+ // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
+ // random integers, it's safe to to continue if the identifier is 0 here.
+ if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
+ event.preventDefault();
+ return false;
+ }
+
+ this.lastTouchIdentifier = touch.identifier;
+
+ // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
+ // 1) the user does a fling scroll on the scrollable layer
+ // 2) the user stops the fling scroll with another tap
+ // then the event.target of the last 'touchend' event will be the element that was under the user's finger
+ // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
+ // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
+ this.updateScrollParent(targetElement);
+ }
+ }
+
+ this.trackingClick = true;
+ this.trackingClickStart = event.timeStamp;
+ this.targetElement = targetElement;
+
+ this.touchStartX = touch.pageX;
+ this.touchStartY = touch.pageY;
+
+ // Prevent phantom clicks on fast double-tap (issue #36)
+ if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
+ event.preventDefault();
+ }
+
+ return true;
+};
+
+
+/**
+ * Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.touchHasMoved = function(event) {
+ 'use strict';
+ var touch = event.changedTouches[0], boundary = this.touchBoundary;
+
+ if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
+ return true;
+ }
+
+ return false;
+};
+
+
+/**
+ * Update the last position.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchMove = function(event) {
+ 'use strict';
+ if (!this.trackingClick) {
+ return true;
+ }
+
+ // If the touch has moved, cancel the click tracking
+ if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
+ this.trackingClick = false;
+ this.targetElement = null;
+ }
+
+ return true;
+};
+
+
+/**
+ * Attempt to find the labelled control for the given label element.
+ *
+ * @param {EventTarget|HTMLLabelElement} labelElement
+ * @returns {Element|null}
+ */
+FastClick.prototype.findControl = function(labelElement) {
+ 'use strict';
+
+ // Fast path for newer browsers supporting the HTML5 control attribute
+ if (labelElement.control !== undefined) {
+ return labelElement.control;
+ }
+
+ // All browsers under test that support touch events also support the HTML5 htmlFor attribute
+ if (labelElement.htmlFor) {
+ return document.getElementById(labelElement.htmlFor);
+ }
+
+ // If no for attribute exists, attempt to retrieve the first labellable descendant element
+ // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
+ return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
+};
+
+
+/**
+ * On touch end, determine whether to send a click event at once.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchEnd = function(event) {
+ 'use strict';
+ var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
+
+ if (!this.trackingClick) {
+ return true;
+ }
+
+ // Prevent phantom clicks on fast double-tap (issue #36)
+ if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
+ this.cancelNextClick = true;
+ return true;
+ }
+
+ // Reset to prevent wrong click cancel on input (issue #156).
+ this.cancelNextClick = false;
+
+ this.lastClickTime = event.timeStamp;
+
+ trackingClickStart = this.trackingClickStart;
+ this.trackingClick = false;
+ this.trackingClickStart = 0;
+
+ // On some iOS devices, the targetElement supplied with the event is invalid if the layer
+ // is performing a transition or scroll, and has to be re-detected manually. Note that
+ // for this to function correctly, it must be called *after* the event target is checked!
+ // See issue #57; also filed as rdar://13048589 .
+ if (deviceIsIOSWithBadTarget) {
+ touch = event.changedTouches[0];
+
+ // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
+ targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
+ targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
+ }
+
+ targetTagName = targetElement.tagName.toLowerCase();
+ if (targetTagName === 'label') {
+ forElement = this.findControl(targetElement);
+ if (forElement) {
+ this.focus(targetElement);
+ if (deviceIsAndroid) {
+ return false;
+ }
+
+ targetElement = forElement;
+ }
+ } else if (this.needsFocus(targetElement)) {
+
+ // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
+ // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
+ if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
+ this.targetElement = null;
+ return false;
+ }
+
+ this.focus(targetElement);
+ this.sendClick(targetElement, event);
+
+ // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
+ // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
+ if (!deviceIsIOS || targetTagName !== 'select') {
+ this.targetElement = null;
+ event.preventDefault();
+ }
+
+ return false;
+ }
+
+ if (deviceIsIOS && !deviceIsIOS4) {
+
+ // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
+ // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
+ scrollParent = targetElement.fastClickScrollParent;
+ if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
+ return true;
+ }
+ }
+
+ // Prevent the actual click from going though - unless the target node is marked as requiring
+ // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
+ if (!this.needsClick(targetElement)) {
+ event.preventDefault();
+ this.sendClick(targetElement, event);
+ }
+
+ return false;
+};
+
+
+/**
+ * On touch cancel, stop tracking the click.
+ *
+ * @returns {void}
+ */
+FastClick.prototype.onTouchCancel = function() {
+ 'use strict';
+ this.trackingClick = false;
+ this.targetElement = null;
+};
+
+
+/**
+ * Determine mouse events which should be permitted.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onMouse = function(event) {
+ 'use strict';
+
+ // If a target element was never set (because a touch event was never fired) allow the event
+ if (!this.targetElement) {
+ return true;
+ }
+
+ if (event.forwardedTouchEvent) {
+ return true;
+ }
+
+ // Programmatically generated events targeting a specific element should be permitted
+ if (!event.cancelable) {
+ return true;
+ }
+
+ // Derive and check the target element to see whether the mouse event needs to be permitted;
+ // unless explicitly enabled, prevent non-touch click events from triggering actions,
+ // to prevent ghost/doubleclicks.
+ if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
+
+ // Prevent any user-added listeners declared on FastClick element from being fired.
+ if (event.stopImmediatePropagation) {
+ event.stopImmediatePropagation();
+ } else {
+
+ // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
+ event.propagationStopped = true;
+ }
+
+ // Cancel the event
+ event.stopPropagation();
+ event.preventDefault();
+
+ return false;
+ }
+
+ // If the mouse event is permitted, return true for the action to go through.
+ return true;
+};
+
+
+/**
+ * On actual clicks, determine whether this is a touch-generated click, a click action occurring
+ * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
+ * an actual click which should be permitted.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onClick = function(event) {
+ 'use strict';
+ var permitted;
+
+ // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
+ if (this.trackingClick) {
+ this.targetElement = null;
+ this.trackingClick = false;
+ return true;
+ }
+
+ // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
+ if (event.target.type === 'submit' && event.detail === 0) {
+ return true;
+ }
+
+ permitted = this.onMouse(event);
+
+ // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
+ if (!permitted) {
+ this.targetElement = null;
+ }
+
+ // If clicks are permitted, return true for the action to go through.
+ return permitted;
+};
+
+
+/**
+ * Remove all FastClick's event listeners.
+ *
+ * @returns {void}
+ */
+FastClick.prototype.destroy = function() {
+ 'use strict';
+ var layer = this.layer;
+
+ if (deviceIsAndroid) {
+ layer.removeEventListener('mouseover', this.onMouse, true);
+ layer.removeEventListener('mousedown', this.onMouse, true);
+ layer.removeEventListener('mouseup', this.onMouse, true);
+ }
+
+ layer.removeEventListener('click', this.onClick, true);
+ layer.removeEventListener('touchstart', this.onTouchStart, false);
+ layer.removeEventListener('touchmove', this.onTouchMove, false);
+ layer.removeEventListener('touchend', this.onTouchEnd, false);
+ layer.removeEventListener('touchcancel', this.onTouchCancel, false);
+};
+
+
+/**
+ * Check whether FastClick is needed.
+ *
+ * @param {Element} layer The layer to listen on
+ */
+FastClick.notNeeded = function(layer) {
+ 'use strict';
+ var metaViewport;
+ var chromeVersion;
+ var blackberryVersion;
+
+ // Devices that don't support touch don't need FastClick
+ if (typeof window.ontouchstart === 'undefined') {
+ return true;
+ }
+
+ // Chrome version - zero for other browsers
+ chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
+
+ if (chromeVersion) {
+
+ if (deviceIsAndroid) {
+ metaViewport = document.querySelector('meta[name=viewport]');
+
+ if (metaViewport) {
+ // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
+ if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
+ return true;
+ }
+ // Chrome 32 and above with width=device-width or less don't need FastClick
+ if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
+ return true;
+ }
+ }
+
+ // Chrome desktop doesn't need FastClick (issue #15)
+ } else {
+ return true;
+ }
+ }
+
+ if (deviceIsBlackBerry10) {
+ blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
+
+ // BlackBerry 10.3+ does not require Fastclick library.
+ // https://github.com/ftlabs/fastclick/issues/251
+ if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
+ metaViewport = document.querySelector('meta[name=viewport]');
+
+ if (metaViewport) {
+ // user-scalable=no eliminates click delay.
+ if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
+ return true;
+ }
+ // width=device-width (or less than device-width) eliminates click delay.
+ if (document.documentElement.scrollWidth <= window.outerWidth) {
+ return true;
+ }
+ }
+ }
+ }
+
+ // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
+ if (layer.style.msTouchAction === 'none') {
+ return true;
+ }
+
+ return false;
+};
+
+
+/**
+ * Factory method for creating a FastClick object
+ *
+ * @param {Element} layer The layer to listen on
+ * @param {Object} options The options to override the defaults
+ */
+FastClick.attach = function(layer, options) {
+ 'use strict';
+ return new FastClick(layer, options);
+};
+
+
+if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
+
+ // AMD. Register as an anonymous module.
+ define(function() {
+ 'use strict';
+ return FastClick;
+ });
+} else if (typeof module !== 'undefined' && module.exports) {
+ module.exports = FastClick.attach;
+ module.exports.FastClick = FastClick;
+} else {
+ window.FastClick = FastClick;
+}
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js b/libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js
new file mode 100644
index 000000000..25714ed29
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=mb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=nb(b);function qb(){}qb.prototype=d.filters=d.pseudos,d.setFilters=new qb,g=gb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?gb.error(a):z(a,i).slice(0)};function rb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)
+},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),f.removeChild(c),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec=/#.*$/,fc=/([?&])_=[^&]*/,gc=/^(.*?):[ \t]*([^\r\n]*)$/gm,hc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,ic=/^(?:GET|HEAD)$/,jc=/^\/\//,kc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,lc={},mc={},nc="*/".concat("*"),oc=a.location.href,pc=kc.exec(oc.toLowerCase())||[];function qc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function rc(a,b,c,d){var e={},f=a===mc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function sc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function tc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function uc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:oc,type:"GET",isLocal:hc.test(pc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":nc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?sc(sc(a,n.ajaxSettings),b):sc(n.ajaxSettings,a)},ajaxPrefilter:qc(lc),ajaxTransport:qc(mc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=gc.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||oc)+"").replace(ec,"").replace(jc,pc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=kc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===pc[1]&&h[2]===pc[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(pc[3]||("http:"===pc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),rc(lc,k,b,v),2===t)return v;i=n.event&&k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!ic.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=fc.test(d)?d.replace(fc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+nc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=rc(mc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=tc(k,v,f)),u=uc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var vc=/%20/g,wc=/\[\]$/,xc=/\r?\n/g,yc=/^(?:submit|button|image|reset|file)$/i,zc=/^(?:input|select|textarea|keygen)/i;function Ac(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||wc.test(a)?d(a,e):Ac(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Ac(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Ac(c,a[c],b,e);return d.join("&").replace(vc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&zc.test(this.nodeName)&&!yc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(xc,"\r\n")}}):{name:b.name,value:c.replace(xc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Bc=0,Cc={},Dc={0:200,1223:204},Ec=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Cc)Cc[a]()}),k.cors=!!Ec&&"withCredentials"in Ec,k.ajax=Ec=!!Ec,n.ajaxTransport(function(a){var b;return k.cors||Ec&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Bc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Cc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Dc[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Cc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Fc=[],Gc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Fc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Gc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Gc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Gc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Fc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Hc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Hc)return Hc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Ic=a.document.documentElement;function Jc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Jc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Ic;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Ic})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Jc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Kc=a.jQuery,Lc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Lc),b&&a.jQuery===n&&(a.jQuery=Kc),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js b/libs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js
new file mode 100755
index 000000000..b8479c6f6
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js
@@ -0,0 +1 @@
+(function(e){function d(){var e=o();if(e!==u){u=e;i.trigger("orientationchange")}}function E(t,n,r,i){var s=r.type;r.type=n;e.event.dispatch.call(t,r,i);r.type=s}e.attrFn=e.attrFn||{};var t=navigator.userAgent.toLowerCase(),n=t.indexOf("chrome")>-1&&(t.indexOf("windows")>-1||t.indexOf("macintosh")>-1||t.indexOf("linux")>-1)&&t.indexOf("mobile")<0&&t.indexOf("nexus")<0,r={tap_pixel_range:5,swipe_h_threshold:50,swipe_v_threshold:50,taphold_threshold:750,doubletap_int:500,touch_capable:"ontouchstart"in document.documentElement&&!n,orientation_support:"orientation"in window&&"onorientationchange"in window,startevent:"ontouchstart"in document.documentElement&&!n?"touchstart":"mousedown",endevent:"ontouchstart"in document.documentElement&&!n?"touchend":"mouseup",moveevent:"ontouchstart"in document.documentElement&&!n?"touchmove":"mousemove",tapevent:"ontouchstart"in document.documentElement&&!n?"tap":"click",scrollevent:"ontouchstart"in document.documentElement&&!n?"touchmove":"scroll",hold_timer:null,tap_timer:null};e.isTouchCapable=function(){return r.touch_capable};e.getStartEvent=function(){return r.startevent};e.getEndEvent=function(){return r.endevent};e.getMoveEvent=function(){return r.moveevent};e.getTapEvent=function(){return r.tapevent};e.getScrollEvent=function(){return r.scrollevent};e.each(["tapstart","tapend","tap","singletap","doubletap","taphold","swipe","swipeup","swiperight","swipedown","swipeleft","swipeend","scrollstart","scrollend","orientationchange"],function(t,n){e.fn[n]=function(e){return e?this.on(n,e):this.trigger(n)};e.attrFn[n]=true});e.event.special.tapstart={setup:function(){var t=this,n=e(t);n.on(r.startevent,function(e){n.data("callee",arguments.callee);if(e.which&&e.which!==1){return false}var i=e.originalEvent,s={position:{x:r.touch_capable?i.touches[0].screenX:e.screenX,y:r.touch_capable?i.touches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.touches[0].pageX-i.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.touches[0].pageY-i.touches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tapstart",e,s);return true})},remove:function(){e(this).off(r.startevent,e(this).data.callee)}};e.event.special.tapmove={setup:function(){var t=this,n=e(t);n.on(r.moveevent,function(e){n.data("callee",arguments.callee);var i=e.originalEvent,s={position:{x:r.touch_capable?i.touches[0].screenX:e.screenX,y:r.touch_capable?i.touches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.touches[0].pageX-i.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.touches[0].pageY-i.touches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tapmove",e,s);return true})},remove:function(){e(this).off(r.moveevent,e(this).data.callee)}};e.event.special.tapend={setup:function(){var t=this,n=e(t);n.on(r.endevent,function(e){n.data("callee",arguments.callee);var i=e.originalEvent;var s={position:{x:r.touch_capable?i.changedTouches[0].screenX:e.screenX,y:r.touch_capable?i.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.changedTouches[0].pageX-i.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.changedTouches[0].pageY-i.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tapend",e,s);return true})},remove:function(){e(this).off(r.endevent,e(this).data.callee)}};e.event.special.taphold={setup:function(){var t=this,n=e(t),i,s,o={x:0,y:0};n.on(r.startevent,function(e){if(e.which&&e.which!==1){return false}else{n.data("tapheld",false);i=e.target;var s=e.originalEvent;var u=(new Date).getTime(),a={x:r.touch_capable?s.touches[0].screenX:e.screenX,y:r.touch_capable?s.touches[0].screenY:e.screenY},f={x:r.touch_capable?s.touches[0].pageX-s.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?s.touches[0].pageY-s.touches[0].target.offsetTop:e.offsetY};o.x=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX;o.y=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;r.hold_timer=window.setTimeout(function(){var l=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX,c=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;if(e.target==i&&o.x==l&&o.y==c){n.data("tapheld",true);var h=(new Date).getTime(),p={x:r.touch_capable?s.touches[0].screenX:e.screenX,y:r.touch_capable?s.touches[0].screenY:e.screenY},d={x:r.touch_capable?s.touches[0].pageX-s.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?s.touches[0].pageY-s.touches[0].target.offsetTop:e.offsetY};duration=h-u;var v={startTime:u,endTime:h,startPosition:a,startOffset:f,endPosition:p,endOffset:d,duration:duration,target:e.target};n.data("callee1",arguments.callee);E(t,"taphold",e,v)}},r.taphold_threshold);return true}}).on(r.endevent,function(){n.data("callee2",arguments.callee);n.data("tapheld",false);window.clearTimeout(r.hold_timer)})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.doubletap={setup:function(){var t=this,n=e(t),i,s,o,u;n.on(r.startevent,function(e){if(e.which&&e.which!==1){return false}else if(!n.data("lastTouch")){n.data("doubletapped",false);i=e.target;n.data("callee1",arguments.callee);u=e.originalEvent;o={position:{x:r.touch_capable?u.touches[0].screenX:e.screenX,y:r.touch_capable?u.touches[0].screenY:e.screenY},offset:{x:r.touch_capable?u.touches[0].pageX-u.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?u.touches[0].pageY-u.touches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};return true}}).on(r.endevent,function(e){var u=(new Date).getTime();var a=n.data("lastTouch")||u+1;var f=u-a;window.clearTimeout(s);n.data("callee2",arguments.callee);if(f<r.doubletap_int&&f>0&&e.target==i&&f>100){n.data("doubletapped",true);window.clearTimeout(r.tap_timer);var l={position:{x:r.touch_capable?e.originalEvent.changedTouches[0].screenX:e.screenX,y:r.touch_capable?e.originalEvent.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?e.originalEvent.changedTouches[0].pageX-e.originalEvent.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?e.originalEvent.changedTouches[0].pageY-e.originalEvent.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};var c={firstTap:o,secondTap:l,interval:l.time-o.time};E(t,"doubletap",e,c)}else{n.data("lastTouch",u);s=window.setTimeout(function(e){window.clearTimeout(s)},r.doubletap_int,[e])}n.data("lastTouch",u)})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.singletap={setup:function(){var t=this,n=e(t),i=null,s=null,o={x:0,y:0};n.on(r.startevent,function(e){if(e.which&&e.which!==1){return false}else{s=(new Date).getTime();i=e.target;n.data("callee1",arguments.callee);o.x=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX;o.y=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;return true}}).on(r.endevent,function(e){n.data("callee2",arguments.callee);if(e.target==i){end_pos_x=e.originalEvent.changedTouches?e.originalEvent.changedTouches[0].pageX:e.pageX;end_pos_y=e.originalEvent.changedTouches?e.originalEvent.changedTouches[0].pageY:e.pageY;r.tap_timer=window.setTimeout(function(){if(!n.data("doubletapped")&&!n.data("tapheld")&&o.x==end_pos_x&&o.y==end_pos_y){var i=e.originalEvent;var u={position:{x:r.touch_capable?i.changedTouches[0].screenX:e.screenX,y:r.touch_capable?i.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.changedTouches[0].pageX-i.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.changedTouches[0].pageY-i.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};if(u.time-s<r.taphold_threshold){E(t,"singletap",e,u)}}},r.doubletap_int)}})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.tap={setup:function(){var t=this,n=e(t),i=false,s=null,o,u={x:0,y:0};n.on(r.startevent,function(e){n.data("callee1",arguments.callee);if(e.which&&e.which!==1){return false}else{i=true;u.x=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX;u.y=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;o=(new Date).getTime();s=e.target;return true}}).on(r.endevent,function(e){n.data("callee2",arguments.callee);var a=e.originalEvent.targetTouches?e.originalEvent.changedTouches[0].pageX:e.pageX,f=e.originalEvent.targetTouches?e.originalEvent.changedTouches[0].pageY:e.pageY;diff_x=u.x-a,diff_y=u.y-f;if(s==e.target&&i&&(new Date).getTime()-o<r.taphold_threshold&&(u.x==a&&u.y==f||diff_x>=-r.tap_pixel_range&&diff_x<=r.tap_pixel_range&&diff_y>=-r.tap_pixel_range&&diff_y<=r.tap_pixel_range)){var l=e.originalEvent;var c={position:{x:r.touch_capable?l.changedTouches[0].screenX:e.screenX,y:r.touch_capable?l.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?l.changedTouches[0].pageX-l.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?l.changedTouches[0].pageY-l.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tap",e,c)}})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.swipe={setup:function(){function f(t){n=e(t.target);n.data("callee1",arguments.callee);o.x=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageX:t.pageX;o.y=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageY:t.pageY;u.x=o.x;u.y=o.y;i=true;var s=t.originalEvent;a={position:{x:r.touch_capable?s.touches[0].screenX:t.screenX,y:r.touch_capable?s.touches[0].screenY:t.screenY},offset:{x:r.touch_capable?s.touches[0].pageX-s.touches[0].target.offsetLeft:t.offsetX,y:r.touch_capable?s.touches[0].pageY-s.touches[0].target.offsetTop:t.offsetY},time:(new Date).getTime(),target:t.target};var f=new Date;while(new Date-f<100){}}function l(t){n=e(t.target);n.data("callee2",arguments.callee);u.x=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageX:t.pageX;u.y=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageY:t.pageY;window.clearTimeout(r.hold_timer);var f;var l=n.data("xthreshold"),c=n.data("ythreshold"),h=typeof l!=="undefined"&&l!==false&&parseInt(l)?parseInt(l):r.swipe_h_threshold,p=typeof c!=="undefined"&&c!==false&&parseInt(c)?parseInt(c):r.swipe_v_threshold;if(o.y>u.y&&o.y-u.y>p){f="swipeup"}if(o.x<u.x&&u.x-o.x>h){f="swiperight"}if(o.y<u.y&&u.y-o.y>p){f="swipedown"}if(o.x>u.x&&o.x-u.x>h){f="swipeleft"}if(f!=undefined&&i){o.x=0;o.y=0;u.x=0;u.y=0;i=false;var d=t.originalEvent;endEvnt={position:{x:r.touch_capable?d.touches[0].screenX:t.screenX,y:r.touch_capable?d.touches[0].screenY:t.screenY},offset:{x:r.touch_capable?d.touches[0].pageX-d.touches[0].target.offsetLeft:t.offsetX,y:r.touch_capable?d.touches[0].pageY-d.touches[0].target.offsetTop:t.offsetY},time:(new Date).getTime(),target:t.target};var v=Math.abs(a.position.x-endEvnt.position.x),m=Math.abs(a.position.y-endEvnt.position.y);var g={startEvnt:a,endEvnt:endEvnt,direction:f.replace("swipe",""),xAmount:v,yAmount:m,duration:endEvnt.time-a.time};s=true;n.trigger("swipe",g).trigger(f,g)}}function c(t){n=e(t.target);var o="";n.data("callee3",arguments.callee);if(s){var u=n.data("xthreshold"),f=n.data("ythreshold"),l=typeof u!=="undefined"&&u!==false&&parseInt(u)?parseInt(u):r.swipe_h_threshold,c=typeof f!=="undefined"&&f!==false&&parseInt(f)?parseInt(f):r.swipe_v_threshold;var h=t.originalEvent;endEvnt={position:{x:r.touch_capable?h.changedTouches[0].screenX:t.screenX,y:r.touch_capable?h.changedTouches[0].screenY:t.screenY},offset:{x:r.touch_capable?h.changedTouches[0].pageX-h.changedTouches[0].target.offsetLeft:t.offsetX,y:r.touch_capable?h.changedTouches[0].pageY-h.changedTouches[0].target.offsetTop:t.offsetY},time:(new Date).getTime(),target:t.target};if(a.position.y>endEvnt.position.y&&a.position.y-endEvnt.position.y>c){o="swipeup"}if(a.position.x<endEvnt.position.x&&endEvnt.position.x-a.position.x>l){o="swiperight"}if(a.position.y<endEvnt.position.y&&endEvnt.position.y-a.position.y>c){o="swipedown"}if(a.position.x>endEvnt.position.x&&a.position.x-endEvnt.position.x>l){o="swipeleft"}var p=Math.abs(a.position.x-endEvnt.position.x),d=Math.abs(a.position.y-endEvnt.position.y);var v={startEvnt:a,endEvnt:endEvnt,direction:o.replace("swipe",""),xAmount:p,yAmount:d,duration:endEvnt.time-a.time};n.trigger("swipeend",v)}i=false;s=false}var t=this,n=e(t),i=false,s=false,o={x:0,y:0},u={x:0,y:0},a;n.on(r.startevent,f);n.on(r.moveevent,l);n.on(r.endevent,c)},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.moveevent,e(this).data.callee2).off(r.endevent,e(this).data.callee3)}};e.event.special.scrollstart={setup:function(){function o(e,n){i=n;E(t,i?"scrollstart":"scrollend",e)}var t=this,n=e(t),i,s;n.on(r.scrollevent,function(e){n.data("callee",arguments.callee);if(!i){o(e,true)}clearTimeout(s);s=setTimeout(function(){o(e,false)},50)})},remove:function(){e(this).off(r.scrollevent,e(this).data.callee)}};var i=e(window),s,o,u,a,f,l={0:true,180:true};if(r.orientation_support){var c=window.innerWidth||e(window).width(),h=window.innerHeight||e(window).height(),p=50;a=c>h&&c-h>p;f=l[window.orientation];if(a&&f||!a&&!f){l={"-90":true,90:true}}}e.event.special.orientationchange=s={setup:function(){if(r.orientation_support){return false}u=o();i.on("throttledresize",d);return true},teardown:function(){if(r.orientation_support){return false}i.off("throttledresize",d);return true},add:function(e){var t=e.handler;e.handler=function(e){e.orientation=o();return t.apply(this,arguments)}}};e.event.special.orientationchange.orientation=o=function(){var e=true,t=document.documentElement;if(r.orientation_support){e=l[window.orientation]}else{e=t&&t.clientWidth/t.clientHeight<1.1}return e?"portrait":"landscape"};e.event.special.throttledresize={setup:function(){e(this).on("resize",m)},teardown:function(){e(this).off("resize",m)}};var v=250,m=function(){b=(new Date).getTime();w=b-g;if(w>=v){g=b;e(this).trigger("throttledresize")}else{if(y){window.clearTimeout(y)}y=window.setTimeout(d,v-w)}},g=0,y,b,w;e.each({scrollend:"scrollstart",swipeup:"swipe",swiperight:"swipe",swipedown:"swipe",swipeleft:"swipe",swipeend:"swipe"},function(t,n,r){e.event.special[t]={setup:function(){e(this).on(n,e.noop)}}})})(jQuery)
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js b/libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js
new file mode 100644
index 000000000..e0227ebeb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js
@@ -0,0 +1,766 @@
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2013 Einar Lielmanis and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ JS Beautifier
+ ---------------
+
+ */
+
+function trim(s) {
+ return s.replace(/^\s+|\s+$/g, '');
+}
+
+function ltrim(s) {
+ return s.replace(/^\s+/g, '');
+}
+
+function style_html(html_source, options) {
+ //Wrapper function to invoke all the necessary constructors and deal with the output.
+
+ var multi_parser,
+ indent_inner_html,
+ indent_size,
+ indent_character,
+ wrap_line_length,
+ brace_style,
+ unformatted,
+ preserve_newlines,
+ max_preserve_newlines;
+
+ options = options || {};
+
+ // backwards compatibility to 1.3.4
+ if ((options.wrap_line_length === undefined || parseInt(options.wrap_line_length, 10) === 0) &&
+ (options.max_char === undefined || parseInt(options.max_char, 10) === 0)) {
+ options.wrap_line_length = options.max_char;
+ }
+
+ indent_inner_html = options.indent_inner_html || false;
+ indent_size = parseInt(options.indent_size || 4, 10);
+ indent_character = options.indent_char || ' ';
+ brace_style = options.brace_style || 'collapse';
+ wrap_line_length = parseInt(options.wrap_line_length, 10) === 0 ? 32786 : parseInt(options.wrap_line_length || 250, 10);
+ unformatted = options.unformatted || ['a', 'span', 'bdo', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'q', 'sub', 'sup', 'tt', 'i', 'b', 'big', 'small', 'u', 's', 'strike', 'font', 'ins', 'del', 'pre', 'address', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+ preserve_newlines = options.preserve_newlines || true;
+ max_preserve_newlines = preserve_newlines ? parseInt(options.max_preserve_newlines || 32786, 10) : 0;
+ indent_handlebars = options.indent_handlebars || false;
+
+ function Parser() {
+
+ this.pos = 0; //Parser position
+ this.token = '';
+ this.current_mode = 'CONTENT'; //reflects the current Parser mode: TAG/CONTENT
+ this.tags = { //An object to hold tags, their position, and their parent-tags, initiated with default values
+ parent: 'parent1',
+ parentcount: 1,
+ parent1: ''
+ };
+ this.tag_type = '';
+ this.token_text = this.last_token = this.last_text = this.token_type = '';
+ this.newlines = 0;
+ this.indent_content = indent_inner_html;
+
+ this.Utils = { //Uilities made available to the various functions
+ whitespace: "\n\r\t ".split(''),
+ single_token: 'br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed,?php,?,?='.split(','), //all the single tags for HTML
+ extra_liners: 'head,body,/html'.split(','), //for tags that need a line of whitespace before them
+ in_array: function(what, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (what === arr[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ this.traverse_whitespace = function() {
+ var input_char = '';
+
+ input_char = this.input.charAt(this.pos);
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ this.newlines = 0;
+ while (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ if (preserve_newlines && input_char === '\n' && this.newlines <= max_preserve_newlines) {
+ this.newlines += 1;
+ }
+
+ this.pos++;
+ input_char = this.input.charAt(this.pos);
+ }
+ return true;
+ }
+ return false;
+ };
+
+ this.get_content = function() { //function to capture regular content between tags
+
+ var input_char = '',
+ content = [],
+ space = false; //if a space is needed
+
+ while (this.input.charAt(this.pos) !== '<') {
+ if (this.pos >= this.input.length) {
+ return content.length ? content.join('') : ['', 'TK_EOF'];
+ }
+
+ if (this.traverse_whitespace()) {
+ if (content.length) {
+ space = true;
+ }
+ continue; //don't want to insert unnecessary space
+ }
+
+ if (indent_handlebars) {
+ // Handlebars parsing is complicated.
+ // {{#foo}} and {{/foo}} are formatted tags.
+ // {{something}} should get treated as content, except:
+ // {{else}} specifically behaves like {{#if}} and {{/if}}
+ var peek3 = this.input.substr(this.pos, 3);
+ if (peek3 === '{{#' || peek3 === '{{/') {
+ // These are tags and not content.
+ break;
+ } else if (this.input.substr(this.pos, 2) === '{{') {
+ if (this.get_tag(true) === '{{else}}') {
+ break;
+ }
+ }
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (space) {
+ if (this.line_char_count >= this.wrap_line_length) { //insert a line when the wrap_line_length is reached
+ this.print_newline(false, content);
+ this.print_indentation(content);
+ } else {
+ this.line_char_count++;
+ content.push(' ');
+ }
+ space = false;
+ }
+ this.line_char_count++;
+ content.push(input_char); //letter at-a-time (or string) inserted to an array
+ }
+ return content.length ? content.join('') : '';
+ };
+
+ this.get_contents_to = function(name) { //get the full content of a script or style to pass to js_beautify
+ if (this.pos === this.input.length) {
+ return ['', 'TK_EOF'];
+ }
+ var input_char = '';
+ var content = '';
+ var reg_match = new RegExp('</' + name + '\\s*>', 'igm');
+ reg_match.lastIndex = this.pos;
+ var reg_array = reg_match.exec(this.input);
+ var end_script = reg_array ? reg_array.index : this.input.length; //absolute end of script
+ if (this.pos < end_script) { //get everything in between the script tags
+ content = this.input.substring(this.pos, end_script);
+ this.pos = end_script;
+ }
+ return content;
+ };
+
+ this.record_tag = function(tag) { //function to record a tag and its parent in this.tags Object
+ if (this.tags[tag + 'count']) { //check for the existence of this tag type
+ this.tags[tag + 'count']++;
+ this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+ } else { //otherwise initialize this tag type
+ this.tags[tag + 'count'] = 1;
+ this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+ }
+ this.tags[tag + this.tags[tag + 'count'] + 'parent'] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
+ this.tags.parent = tag + this.tags[tag + 'count']; //and make this the current parent (i.e. in the case of a div 'div1')
+ };
+
+ this.retrieve_tag = function(tag) { //function to retrieve the opening tag to the corresponding closer
+ if (this.tags[tag + 'count']) { //if the openener is not in the Object we ignore it
+ var temp_parent = this.tags.parent; //check to see if it's a closable tag.
+ while (temp_parent) { //till we reach '' (the initial value);
+ if (tag + this.tags[tag + 'count'] === temp_parent) { //if this is it use it
+ break;
+ }
+ temp_parent = this.tags[temp_parent + 'parent']; //otherwise keep on climbing up the DOM Tree
+ }
+ if (temp_parent) { //if we caught something
+ this.indent_level = this.tags[tag + this.tags[tag + 'count']]; //set the indent_level accordingly
+ this.tags.parent = this.tags[temp_parent + 'parent']; //and set the current parent
+ }
+ delete this.tags[tag + this.tags[tag + 'count'] + 'parent']; //delete the closed tags parent reference...
+ delete this.tags[tag + this.tags[tag + 'count']]; //...and the tag itself
+ if (this.tags[tag + 'count'] === 1) {
+ delete this.tags[tag + 'count'];
+ } else {
+ this.tags[tag + 'count']--;
+ }
+ }
+ };
+
+ this.indent_to_tag = function(tag) {
+ // Match the indentation level to the last use of this tag, but don't remove it.
+ if (!this.tags[tag + 'count']) {
+ return;
+ }
+ var temp_parent = this.tags.parent;
+ while (temp_parent) {
+ if (tag + this.tags[tag + 'count'] === temp_parent) {
+ break;
+ }
+ temp_parent = this.tags[temp_parent + 'parent'];
+ }
+ if (temp_parent) {
+ this.indent_level = this.tags[tag + this.tags[tag + 'count']];
+ }
+ };
+
+ this.get_tag = function(peek) { //function to get a full tag and parse its type
+ var input_char = '',
+ content = [],
+ comment = '',
+ space = false,
+ tag_start, tag_end,
+ tag_start_char,
+ orig_pos = this.pos,
+ orig_line_char_count = this.line_char_count;
+
+ peek = peek !== undefined ? peek : false;
+
+ do {
+ if (this.pos >= this.input.length) {
+ if (peek) {
+ this.pos = orig_pos;
+ this.line_char_count = orig_line_char_count;
+ }
+ return content.length ? content.join('') : ['', 'TK_EOF'];
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) { //don't want to insert unnecessary space
+ space = true;
+ continue;
+ }
+
+ if (input_char === "'" || input_char === '"') {
+ input_char += this.get_unformatted(input_char);
+ space = true;
+
+ }
+
+ if (input_char === '=') { //no space before =
+ space = false;
+ }
+
+ if (content.length && content[content.length - 1] !== '=' && input_char !== '>' && space) {
+ //no space after = or before >
+ if (this.line_char_count >= this.wrap_line_length) {
+ this.print_newline(false, content);
+ this.print_indentation(content);
+ } else {
+ content.push(' ');
+ this.line_char_count++;
+ }
+ space = false;
+ }
+
+ if (indent_handlebars && tag_start_char === '<') {
+ // When inside an angle-bracket tag, put spaces around
+ // handlebars not inside of strings.
+ if ((input_char + this.input.charAt(this.pos)) === '{{') {
+ input_char += this.get_unformatted('}}');
+ if (content.length && content[content.length - 1] !== ' ' && content[content.length - 1] !== '<') {
+ input_char = ' ' + input_char;
+ }
+ space = true;
+ }
+ }
+
+ if (input_char === '<' && !tag_start_char) {
+ tag_start = this.pos - 1;
+ tag_start_char = '<';
+ }
+
+ if (indent_handlebars && !tag_start_char) {
+ if (content.length >= 2 && content[content.length - 1] === '{' && content[content.length - 2] == '{') {
+ if (input_char === '#' || input_char === '/') {
+ tag_start = this.pos - 3;
+ } else {
+ tag_start = this.pos - 2;
+ }
+ tag_start_char = '{';
+ }
+ }
+
+ this.line_char_count++;
+ content.push(input_char); //inserts character at-a-time (or string)
+
+ if (content[1] && content[1] === '!') { //if we're in a comment, do something special
+ // We treat all comments as literals, even more than preformatted tags
+ // we just look for the appropriate close tag
+ content = [this.get_comment(tag_start)];
+ break;
+ }
+
+ if (indent_handlebars && tag_start_char === '{' && content.length > 2 && content[content.length - 2] === '}' && content[content.length - 1] === '}') {
+ break;
+ }
+ } while (input_char !== '>');
+
+ var tag_complete = content.join('');
+ var tag_index;
+ var tag_offset;
+
+ if (tag_complete.indexOf(' ') !== -1) { //if there's whitespace, thats where the tag name ends
+ tag_index = tag_complete.indexOf(' ');
+ } else if (tag_complete[0] === '{') {
+ tag_index = tag_complete.indexOf('}');
+ } else { //otherwise go with the tag ending
+ tag_index = tag_complete.indexOf('>');
+ }
+ if (tag_complete[0] === '<' || !indent_handlebars) {
+ tag_offset = 1;
+ } else {
+ tag_offset = tag_complete[2] === '#' ? 3 : 2;
+ }
+ var tag_check = tag_complete.substring(tag_offset, tag_index).toLowerCase();
+ if (tag_complete.charAt(tag_complete.length - 2) === '/' ||
+ this.Utils.in_array(tag_check, this.Utils.single_token)) { //if this tag name is a single tag type (either in the list or has a closing /)
+ if (!peek) {
+ this.tag_type = 'SINGLE';
+ }
+ } else if (indent_handlebars && tag_complete[0] === '{' && tag_check === 'else') {
+ if (!peek) {
+ this.indent_to_tag('if');
+ this.tag_type = 'HANDLEBARS_ELSE';
+ this.indent_content = true;
+ this.traverse_whitespace();
+ }
+ } else if (tag_check === 'script') { //for later script handling
+ if (!peek) {
+ this.record_tag(tag_check);
+ this.tag_type = 'SCRIPT';
+ }
+ } else if (tag_check === 'style') { //for future style handling (for now it justs uses get_content)
+ if (!peek) {
+ this.record_tag(tag_check);
+ this.tag_type = 'STYLE';
+ }
+ } else if (this.is_unformatted(tag_check, unformatted)) { // do not reformat the "unformatted" tags
+ comment = this.get_unformatted('</' + tag_check + '>', tag_complete); //...delegate to get_unformatted function
+ content.push(comment);
+ // Preserve collapsed whitespace either before or after this tag.
+ if (tag_start > 0 && this.Utils.in_array(this.input.charAt(tag_start - 1), this.Utils.whitespace)) {
+ content.splice(0, 0, this.input.charAt(tag_start - 1));
+ }
+ tag_end = this.pos - 1;
+ if (this.Utils.in_array(this.input.charAt(tag_end + 1), this.Utils.whitespace)) {
+ content.push(this.input.charAt(tag_end + 1));
+ }
+ this.tag_type = 'SINGLE';
+ } else if (tag_check.charAt(0) === '!') { //peek for <! comment
+ // for comments content is already correct.
+ if (!peek) {
+ this.tag_type = 'SINGLE';
+ this.traverse_whitespace();
+ }
+ } else if (!peek) {
+ if (tag_check.charAt(0) === '/') { //this tag is a double tag so check for tag-ending
+ this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
+ this.tag_type = 'END';
+ this.traverse_whitespace();
+ } else { //otherwise it's a start-tag
+ this.record_tag(tag_check); //push it on the tag stack
+ if (tag_check.toLowerCase() !== 'html') {
+ this.indent_content = true;
+ }
+ this.tag_type = 'START';
+
+ // Allow preserving of newlines after a start tag
+ this.traverse_whitespace();
+ }
+ if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { //check if this double needs an extra line
+ this.print_newline(false, this.output);
+ if (this.output.length && this.output[this.output.length - 2] !== '\n') {
+ this.print_newline(true, this.output);
+ }
+ }
+ }
+
+ if (peek) {
+ this.pos = orig_pos;
+ this.line_char_count = orig_line_char_count;
+ }
+
+ return content.join(''); //returns fully formatted tag
+ };
+
+ this.get_comment = function(start_pos) { //function to return comment content in its entirety
+ // this is will have very poor perf, but will work for now.
+ var comment = '',
+ delimiter = '>',
+ matched = false;
+
+ this.pos = start_pos;
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ while (this.pos <= this.input.length) {
+ comment += input_char;
+
+ // only need to check for the delimiter if the last chars match
+ if (comment[comment.length - 1] === delimiter[delimiter.length - 1] &&
+ comment.indexOf(delimiter) !== -1) {
+ break;
+ }
+
+ // only need to search for custom delimiter for the first few characters
+ if (!matched && comment.length < 10) {
+ if (comment.indexOf('<![if') === 0) { //peek for <![if conditional comment
+ delimiter = '<![endif]>';
+ matched = true;
+ } else if (comment.indexOf('<![cdata[') === 0) { //if it's a <[cdata[ comment...
+ delimiter = ']]>';
+ matched = true;
+ } else if (comment.indexOf('<![') === 0) { // some other ![ comment? ...
+ delimiter = ']>';
+ matched = true;
+ } else if (comment.indexOf('<!--') === 0) { // <!-- comment ...
+ delimiter = '-->';
+ matched = true;
+ }
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+ }
+
+ return comment;
+ };
+
+ this.get_unformatted = function(delimiter, orig_tag) { //function to return unformatted content in its entirety
+
+ if (orig_tag && orig_tag.toLowerCase().indexOf(delimiter) !== -1) {
+ return '';
+ }
+ var input_char = '';
+ var content = '';
+ var min_index = 0;
+ var space = true;
+ do {
+
+ if (this.pos >= this.input.length) {
+ return content;
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ if (!space) {
+ this.line_char_count--;
+ continue;
+ }
+ if (input_char === '\n' || input_char === '\r') {
+ content += '\n';
+ /* Don't change tab indention for unformatted blocks. If using code for html editing, this will greatly affect <pre> tags if they are specified in the 'unformatted array'
+ for (var i=0; i<this.indent_level; i++) {
+ content += this.indent_string;
+ }
+ space = false; //...and make sure other indentation is erased
+ */
+ this.line_char_count = 0;
+ continue;
+ }
+ }
+ content += input_char;
+ this.line_char_count++;
+ space = true;
+
+ if (indent_handlebars && input_char === '{' && content.length && content[content.length - 2] === '{') {
+ // Handlebars expressions in strings should also be unformatted.
+ content += this.get_unformatted('}}');
+ // These expressions are opaque. Ignore delimiters found in them.
+ min_index = content.length;
+ }
+ } while (content.toLowerCase().indexOf(delimiter, min_index) === -1);
+ return content;
+ };
+
+ this.get_token = function() { //initial handler for token-retrieval
+ var token;
+
+ if (this.last_token === 'TK_TAG_SCRIPT' || this.last_token === 'TK_TAG_STYLE') { //check if we need to format javascript
+ var type = this.last_token.substr(7);
+ token = this.get_contents_to(type);
+ if (typeof token !== 'string') {
+ return token;
+ }
+ return [token, 'TK_' + type];
+ }
+ if (this.current_mode === 'CONTENT') {
+ token = this.get_content();
+ if (typeof token !== 'string') {
+ return token;
+ } else {
+ return [token, 'TK_CONTENT'];
+ }
+ }
+
+ if (this.current_mode === 'TAG') {
+ token = this.get_tag();
+ if (typeof token !== 'string') {
+ return token;
+ } else {
+ var tag_name_type = 'TK_TAG_' + this.tag_type;
+ return [token, tag_name_type];
+ }
+ }
+ };
+
+ this.get_full_indent = function(level) {
+ level = this.indent_level + level || 0;
+ if (level < 1) {
+ return '';
+ }
+
+ return Array(level + 1).join(this.indent_string);
+ };
+
+ this.is_unformatted = function(tag_check, unformatted) {
+ //is this an HTML5 block-level link?
+ if (!this.Utils.in_array(tag_check, unformatted)) {
+ return false;
+ }
+
+ if (tag_check.toLowerCase() !== 'a' || !this.Utils.in_array('a', unformatted)) {
+ return true;
+ }
+
+ //at this point we have an tag; is its first child something we want to remain
+ //unformatted?
+ var next_tag = this.get_tag(true /* peek. */ );
+
+ // test next_tag to see if it is just html tag (no external content)
+ var tag = (next_tag || "").match(/^\s*<\s*\/?([a-z]*)\s*[^>]*>\s*$/);
+
+ // if next_tag comes back but is not an isolated tag, then
+ // let's treat the 'a' tag as having content
+ // and respect the unformatted option
+ if (!tag || this.Utils.in_array(tag, unformatted)) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ this.printer = function(js_source, indent_character, indent_size, wrap_line_length, brace_style) { //handles input/output and some other printing functions
+
+ this.input = js_source || ''; //gets the input for the Parser
+ this.output = [];
+ this.indent_character = indent_character;
+ this.indent_string = '';
+ this.indent_size = indent_size;
+ this.brace_style = brace_style;
+ this.indent_level = 0;
+ this.wrap_line_length = wrap_line_length;
+ this.line_char_count = 0; //count to see if wrap_line_length was exceeded
+
+ for (var i = 0; i < this.indent_size; i++) {
+ this.indent_string += this.indent_character;
+ }
+
+ this.print_newline = function(force, arr) {
+ this.line_char_count = 0;
+ if (!arr || !arr.length) {
+ return;
+ }
+ if (force || (arr[arr.length - 1] !== '\n')) { //we might want the extra line
+ arr.push('\n');
+ }
+ };
+
+ this.print_indentation = function(arr) {
+ for (var i = 0; i < this.indent_level; i++) {
+ arr.push(this.indent_string);
+ this.line_char_count += this.indent_string.length;
+ }
+ };
+
+ this.print_token = function(text) {
+ if (text || text !== '') {
+ if (this.output.length && this.output[this.output.length - 1] === '\n') {
+ this.print_indentation(this.output);
+ text = ltrim(text);
+ }
+ }
+ this.print_token_raw(text);
+ };
+
+ this.print_token_raw = function(text) {
+ if (text && text !== '') {
+ if (text.length > 1 && text[text.length - 1] === '\n') {
+ // unformatted tags can grab newlines as their last character
+ this.output.push(text.slice(0, -1));
+ this.print_newline(false, this.output);
+ } else {
+ this.output.push(text);
+ }
+ }
+
+ for (var n = 0; n < this.newlines; n++) {
+ this.print_newline(n > 0, this.output);
+ }
+ this.newlines = 0;
+ };
+
+ this.indent = function() {
+ this.indent_level++;
+ };
+
+ this.unindent = function() {
+ if (this.indent_level > 0) {
+ this.indent_level--;
+ }
+ };
+ };
+ return this;
+ }
+
+ /*_____________________--------------------_____________________*/
+
+ multi_parser = new Parser(); //wrapping functions Parser
+ multi_parser.printer(html_source, indent_character, indent_size, wrap_line_length, brace_style); //initialize starting values
+
+ while (true) {
+ var t = multi_parser.get_token();
+ multi_parser.token_text = t[0];
+ multi_parser.token_type = t[1];
+
+ if (multi_parser.token_type === 'TK_EOF') {
+ break;
+ }
+
+ switch (multi_parser.token_type) {
+ case 'TK_TAG_START':
+ multi_parser.print_newline(false, multi_parser.output);
+ multi_parser.print_token(multi_parser.token_text);
+ if (multi_parser.indent_content) {
+ multi_parser.indent();
+ multi_parser.indent_content = false;
+ }
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_STYLE':
+ case 'TK_TAG_SCRIPT':
+ multi_parser.print_newline(false, multi_parser.output);
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_END':
+ //Print new line only if the tag has no content and has child
+ if (multi_parser.last_token === 'TK_CONTENT' && multi_parser.last_text === '') {
+ var tag_name = multi_parser.token_text.match(/\w+/)[0];
+ var tag_extracted_from_last_output = null;
+ if (multi_parser.output.length) {
+ tag_extracted_from_last_output = multi_parser.output[multi_parser.output.length - 1].match(/(?:<|{{#)\s*(\w+)/);
+ }
+ if (tag_extracted_from_last_output === null ||
+ tag_extracted_from_last_output[1] !== tag_name) {
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ }
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_SINGLE':
+ // Don't add a newline before elements that should remain unformatted.
+ var tag_check = multi_parser.token_text.match(/^\s*<([a-z]+)/i);
+ if (!tag_check || !multi_parser.Utils.in_array(tag_check[1], unformatted)) {
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_HANDLEBARS_ELSE':
+ multi_parser.print_token(multi_parser.token_text);
+ if (multi_parser.indent_content) {
+ multi_parser.indent();
+ multi_parser.indent_content = false;
+ }
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_CONTENT':
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'TAG';
+ break;
+ case 'TK_STYLE':
+ case 'TK_SCRIPT':
+ if (multi_parser.token_text !== '') {
+ multi_parser.print_newline(false, multi_parser.output);
+ var text = multi_parser.token_text,
+ _beautifier,
+ script_indent_level = 1;
+ if (multi_parser.token_type === 'TK_SCRIPT') {
+ _beautifier = typeof js_beautify === 'function' && js_beautify;
+ } else if (multi_parser.token_type === 'TK_STYLE') {
+ _beautifier = typeof css_beautify === 'function' && css_beautify;
+ }
+
+ if (options.indent_scripts === "keep") {
+ script_indent_level = 0;
+ } else if (options.indent_scripts === "separate") {
+ script_indent_level = -multi_parser.indent_level;
+ }
+
+ var indentation = multi_parser.get_full_indent(script_indent_level);
+ if (_beautifier) {
+ // call the Beautifier if avaliable
+ text = _beautifier(text.replace(/^\s*/, indentation), options);
+ } else {
+ // simply indent the string otherwise
+ var white = text.match(/^\s*/)[0];
+ var _level = white.match(/[^\n\r]*$/)[0].split(multi_parser.indent_string).length - 1;
+ var reindent = multi_parser.get_full_indent(script_indent_level - _level);
+ text = text.replace(/^\s*/, indentation)
+ .replace(/\r\n|\r|\n/g, '\n' + reindent)
+ .replace(/\s+$/, '');
+ }
+ if (text) {
+ multi_parser.print_token_raw(indentation + trim(text));
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ }
+ multi_parser.current_mode = 'TAG';
+ break;
+ }
+ multi_parser.last_token = multi_parser.token_type;
+ multi_parser.last_text = multi_parser.token_text;
+ }
+ return multi_parser.output.join('');
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js
new file mode 100755
index 000000000..211e2cd2c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js
@@ -0,0 +1,15 @@
+/**
+ * Class Applier module for Rangy.
+ * Adds, removes and toggles classes on Ranges and Selections
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("ClassApplier",["WrappedSelection"],function(e,t){function n(e,t){for(var n in e)if(e.hasOwnProperty(n)&&t(n,e[n])===!1)return!1;return!0}function s(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function r(e,t){return!!e&&new RegExp("(?:^|\\s)"+t+"(?:\\s|$)").test(e)}function o(e,t){if("object"==typeof e.classList)return e.classList.contains(t);var n="string"==typeof e.className,s=n?e.className:e.getAttribute("class");return r(s,t)}function a(e,t){if("object"==typeof e.classList)e.classList.add(t);else{var n="string"==typeof e.className,s=n?e.className:e.getAttribute("class");s?r(s,t)||(s+=" "+t):s=t,n?e.className=s:e.setAttribute("class",s)}}function l(e){var t="string"==typeof e.className;return t?e.className:e.getAttribute("class")}function u(e){return e&&e.split(/\s+/).sort().join(" ")}function f(e){return u(l(e))}function c(e,t){return f(e)==f(t)}function p(e,t){for(var n=t.split(/\s+/),r=0,i=n.length;i>r;++r)if(!o(e,s(n[r])))return!1;return!0}function d(e){var t=e.parentNode;return t&&1==t.nodeType&&!/^(textarea|style|script|select|iframe)$/i.test(t.nodeName)}function h(e,t,n,s,r){var i=e.node,o=e.offset,a=i,l=o;i==s&&o>r&&++l,i!=t||o!=n&&o!=n+1||(a=s,l+=r-n),i==t&&o>n+1&&--l,e.node=a,e.offset=l}function m(e,t,n){e.node==t&&e.offset>n&&--e.offset}function g(e,t,n,s){-1==n&&(n=t.childNodes.length);var r=e.parentNode,i=$.getNodeIndex(e);U(s,function(e){h(e,r,i,t,n)}),t.childNodes.length==n?t.appendChild(e):t.insertBefore(e,t.childNodes[n])}function N(e,t){var n=e.parentNode,s=$.getNodeIndex(e);U(t,function(e){m(e,n,s)}),$.removeNode(e)}function v(e,t,n,s,r){for(var i,o=[];i=e.firstChild;)g(i,t,n++,r),o.push(i);return s&&N(e,r),o}function y(e,t){return v(e,e.parentNode,$.getNodeIndex(e),!0,t)}function C(e,t){var n=e.cloneRange();n.selectNodeContents(t);var s=n.intersection(e),r=s?s.toString():"";return""!=r}function T(e){for(var t,n=e.getNodes([3]),s=0;(t=n[s])&&!C(e,t);)++s;for(var r=n.length-1;(t=n[r])&&!C(e,t);)--r;return n.slice(s,r+1)}function E(e,t){if(e.attributes.length!=t.attributes.length)return!1;for(var n,s,r,i=0,o=e.attributes.length;o>i;++i)if(n=e.attributes[i],r=n.name,"class"!=r){if(s=t.attributes.getNamedItem(r),null===n!=(null===s))return!1;if(n.specified!=s.specified)return!1;if(n.specified&&n.nodeValue!==s.nodeValue)return!1}return!0}function b(e,t){for(var n,s=0,r=e.attributes.length;r>s;++s)if(n=e.attributes[s].name,(!t||!B(t,n))&&e.attributes[s].specified&&"class"!=n)return!0;return!1}function A(e){var t;return e&&1==e.nodeType&&((t=e.parentNode)&&9==t.nodeType&&"on"==t.designMode||G(e)&&!G(e.parentNode))}function S(e){return(G(e)||1!=e.nodeType&&G(e.parentNode))&&!A(e)}function x(e){return e&&1==e.nodeType&&!J.test(F(e,"display"))}function R(e){if(0==e.data.length)return!0;if(K.test(e.data))return!1;var t=F(e.parentNode,"whiteSpace");switch(t){case"pre":case"pre-wrap":case"-moz-pre-wrap":return!1;case"pre-line":if(/[\r\n]/.test(e.data))return!1}return x(e.previousSibling)||x(e.nextSibling)}function P(e){var t,n,s=[];for(t=0;n=e[t++];)s.push(new z(n.startContainer,n.startOffset),new z(n.endContainer,n.endOffset));return s}function w(e,t){for(var n,s,r,i=0,o=e.length;o>i;++i)n=e[i],s=t[2*i],r=t[2*i+1],n.setStartAndEnd(s.node,s.offset,r.node,r.offset)}function O(e,t){return $.isCharacterDataNode(e)?0==t?!!e.previousSibling:t==e.length?!!e.nextSibling:!0:t>0&&t<e.childNodes.length}function I(e,n,s,r){var i,o,a=0==s;if($.isAncestorOf(n,e))return e;if($.isCharacterDataNode(n)){var l=$.getNodeIndex(n);if(0==s)s=l;else{if(s!=n.length)throw t.createError("splitNodeAt() should not be called with offset in the middle of a data node ("+s+" in "+n.data);s=l+1}n=n.parentNode}if(O(n,s)){i=n.cloneNode(!1),o=n.parentNode,i.id&&i.removeAttribute("id");for(var u,f=0;u=n.childNodes[s];)g(u,i,f++,r);return g(i,o,$.getNodeIndex(n)+1,r),n==e?i:I(e,o,$.getNodeIndex(i),r)}if(e!=n){i=n.parentNode;var c=$.getNodeIndex(n);return a||c++,I(e,i,c,r)}return e}function W(e,t){return e.namespaceURI==t.namespaceURI&&e.tagName.toLowerCase()==t.tagName.toLowerCase()&&c(e,t)&&E(e,t)&&"inline"==F(e,"display")&&"inline"==F(t,"display")}function L(e){var t=e?"nextSibling":"previousSibling";return function(n,s){var r=n.parentNode,i=n[t];if(i){if(i&&3==i.nodeType)return i}else if(s&&(i=r[t],i&&1==i.nodeType&&W(r,i))){var o=i[e?"firstChild":"lastChild"];if(o&&3==o.nodeType)return o}return null}}function M(e){this.isElementMerge=1==e.nodeType,this.textNodes=[];var t=this.isElementMerge?e.lastChild:e;t&&(this.textNodes[0]=t)}function H(e,t,r){var i,o,a,l,f=this;f.cssClass=f.className=e;var c=null,p={};if("object"==typeof t&&null!==t){for("undefined"!=typeof t.elementTagName&&(t.elementTagName=t.elementTagName.toLowerCase()),r=t.tagNames,c=t.elementProperties,p=t.elementAttributes,o=0;l=Y[o++];)t.hasOwnProperty(l)&&(f[l]=t[l]);i=t.normalize}else i=t;f.normalize="undefined"==typeof i?!0:i,f.attrExceptions=[];var d=document.createElement(f.elementTagName);f.elementProperties=f.copyPropertiesToElement(c,d,!0),n(p,function(e,t){f.attrExceptions.push(e),p[e]=""+t}),f.elementAttributes=p,f.elementSortedClassName=f.elementProperties.hasOwnProperty("className")?u(f.elementProperties.className+" "+e):e,f.applyToAnyTagName=!1;var h=typeof r;if("string"==h)"*"==r?f.applyToAnyTagName=!0:f.tagNames=s(r.toLowerCase()).split(/\s*,\s*/);else if("object"==h&&"number"==typeof r.length)for(f.tagNames=[],o=0,a=r.length;a>o;++o)"*"==r[o]?f.applyToAnyTagName=!0:f.tagNames.push(r[o].toLowerCase());else f.tagNames=[f.elementTagName]}function j(e,t,n){return new H(e,t,n)}var $=e.dom,z=$.DomPosition,B=$.arrayContains,D=e.util,U=D.forEach,V="span",k=D.isHostMethod(document,"createElementNS"),q=function(){function e(e,t,n){return t&&n?" ":""}return function(t,n){if("object"==typeof t.classList)t.classList.remove(n);else{var s="string"==typeof t.className,r=s?t.className:t.getAttribute("class");r=r.replace(new RegExp("(^|\\s)"+n+"(\\s|$)"),e),s?t.className=r:t.setAttribute("class",r)}}}(),F=$.getComputedStyleProperty,G=function(){var e=document.createElement("div");return"boolean"==typeof e.isContentEditable?function(e){return e&&1==e.nodeType&&e.isContentEditable}:function(e){return e&&1==e.nodeType&&"false"!=e.contentEditable?"true"==e.contentEditable||G(e.parentNode):!1}}(),J=/^inline(-block|-table)?$/i,K=/[^\r\n\t\f \u200B]/,Q=L(!1),X=L(!0);M.prototype={doMerge:function(e){var t=this.textNodes,n=t[0];if(t.length>1){var s,r=$.getNodeIndex(n),i=[],o=0;U(t,function(t,a){s=t.parentNode,a>0&&(s.removeChild(t),s.hasChildNodes()||$.removeNode(s),e&&U(e,function(e){e.node==t&&(e.node=n,e.offset+=o),e.node==s&&e.offset>r&&(--e.offset,e.offset==r+1&&len-1>a&&(e.node=n,e.offset=o))})),i[a]=t.data,o+=t.data.length}),n.data=i.join("")}return n.data},getLength:function(){for(var e=this.textNodes.length,t=0;e--;)t+=this.textNodes[e].length;return t},toString:function(){var e=[];return U(this.textNodes,function(t,n){e[n]="'"+t.data+"'"}),"[Merge("+e.join(",")+")]"}};var Y=["elementTagName","ignoreWhiteSpace","applyToEditableOnly","useExistingElements","removeEmptyElements","onElementCreate"],Z={};H.prototype={elementTagName:V,elementProperties:{},elementAttributes:{},ignoreWhiteSpace:!0,applyToEditableOnly:!1,useExistingElements:!0,removeEmptyElements:!0,onElementCreate:null,copyPropertiesToElement:function(e,t,n){var s,r,i,o,l,f,c={};for(var p in e)if(e.hasOwnProperty(p))if(o=e[p],l=t[p],"className"==p)a(t,o),a(t,this.className),t[p]=u(t[p]),n&&(c[p]=o);else if("style"==p){r=l,n&&(c[p]=i={});for(s in e[p])e[p].hasOwnProperty(s)&&(r[s]=o[s],n&&(i[s]=r[s]));this.attrExceptions.push(p)}else t[p]=o,n&&(c[p]=t[p],f=Z.hasOwnProperty(p)?Z[p]:p,this.attrExceptions.push(f));return n?c:""},copyAttributesToElement:function(e,t){for(var n in e)e.hasOwnProperty(n)&&!/^class(?:Name)?$/i.test(n)&&t.setAttribute(n,e[n])},appliesToElement:function(e){return B(this.tagNames,e.tagName.toLowerCase())},getEmptyElements:function(e){var t=this;return e.getNodes([1],function(e){return t.appliesToElement(e)&&!e.hasChildNodes()})},hasClass:function(e){return 1==e.nodeType&&(this.applyToAnyTagName||this.appliesToElement(e))&&o(e,this.className)},getSelfOrAncestorWithClass:function(e){for(;e;){if(this.hasClass(e))return e;e=e.parentNode}return null},isModifiable:function(e){return!this.applyToEditableOnly||S(e)},isIgnorableWhiteSpaceNode:function(e){return this.ignoreWhiteSpace&&e&&3==e.nodeType&&R(e)},postApply:function(e,t,n,s){var r,o,a=e[0],l=e[e.length-1],u=[],f=a,c=l,p=0,d=l.length;U(e,function(e){o=Q(e,!s),o?(r||(r=new M(o),u.push(r)),r.textNodes.push(e),e===a&&(f=r.textNodes[0],p=f.length),e===l&&(c=r.textNodes[0],d=r.getLength())):r=null});var h=X(l,!s);if(h&&(r||(r=new M(l),u.push(r)),r.textNodes.push(h)),u.length){for(i=0,len=u.length;len>i;++i)u[i].doMerge(n);t.setStartAndEnd(f,p,c,d)}},createContainer:function(e){var t,n=$.getDocument(e),s=k&&!$.isHtmlNamespace(e)&&(t=e.namespaceURI)?n.createElementNS(e.namespaceURI,this.elementTagName):n.createElement(this.elementTagName);return this.copyPropertiesToElement(this.elementProperties,s,!1),this.copyAttributesToElement(this.elementAttributes,s),a(s,this.className),this.onElementCreate&&this.onElementCreate(s,this),s},elementHasProperties:function(e,t){var s=this;return n(t,function(t,n){if("className"==t)return p(e,n);if("object"==typeof n){if(!s.elementHasProperties(e[t],n))return!1}else if(e[t]!==n)return!1})},elementHasAttributes:function(e,t){return n(t,function(t,n){return e.getAttribute(t)!==n?!1:void 0})},applyToTextNode:function(e){if(d(e)){var t=e.parentNode;if(1==t.childNodes.length&&this.useExistingElements&&this.appliesToElement(t)&&this.elementHasProperties(t,this.elementProperties)&&this.elementHasAttributes(t,this.elementAttributes))a(t,this.className);else{var n=e.parentNode,s=this.createContainer(n);n.insertBefore(s,e),s.appendChild(e)}}},isRemovable:function(e){return e.tagName.toLowerCase()==this.elementTagName&&f(e)==this.elementSortedClassName&&this.elementHasProperties(e,this.elementProperties)&&!b(e,this.attrExceptions)&&this.elementHasAttributes(e,this.elementAttributes)&&this.isModifiable(e)},isEmptyContainer:function(e){var t=e.childNodes.length;return 1==e.nodeType&&this.isRemovable(e)&&(0==t||1==t&&this.isEmptyContainer(e.firstChild))},removeEmptyContainers:function(e){var t=this,n=e.getNodes([1],function(e){return t.isEmptyContainer(e)}),s=[e],r=P(s);U(n,function(e){N(e,r)}),w(s,r)},undoToTextNode:function(e,t,n,s){if(!t.containsNode(n)){var r=t.cloneRange();r.selectNode(n),r.isPointInRange(t.endContainer,t.endOffset)&&(I(n,t.endContainer,t.endOffset,s),t.setEndAfter(n)),r.isPointInRange(t.startContainer,t.startOffset)&&(n=I(n,t.startContainer,t.startOffset,s))}this.isRemovable(n)?y(n,s):q(n,this.className)},splitAncestorWithClass:function(e,t,n){var s=this.getSelfOrAncestorWithClass(e);s&&I(s,e,t,n)},undoToAncestor:function(e,t){this.isRemovable(e)?y(e,t):q(e,this.className)},applyToRange:function(e,t){var n=this;t=t||[];var s=P(t||[]);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e);var r=T(e);if(r.length){U(r,function(e){n.isIgnorableWhiteSpaceNode(e)||n.getSelfOrAncestorWithClass(e)||!n.isModifiable(e)||n.applyToTextNode(e,s)});var i=r[r.length-1];e.setStartAndEnd(r[0],0,i,i.length),n.normalize&&n.postApply(r,e,s,!1),w(t,s)}var o=n.getEmptyElements(e);U(o,function(e){a(e,n.className)})},applyToRanges:function(e){for(var t=e.length;t--;)this.applyToRange(e[t],e);return e},applyToSelection:function(t){var n=e.getSelection(t);n.setRanges(this.applyToRanges(n.getAllRanges()))},undoToRange:function(e,t){var n=this;t=t||[];var s=P(t);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e,s);var r,i,o=T(e),a=o[o.length-1];if(o.length){n.splitAncestorWithClass(e.endContainer,e.endOffset,s),n.splitAncestorWithClass(e.startContainer,e.startOffset,s);for(var l=0,u=o.length;u>l;++l)r=o[l],i=n.getSelfOrAncestorWithClass(r),i&&n.isModifiable(r)&&n.undoToAncestor(i,s);e.setStartAndEnd(o[0],0,a,a.length),n.normalize&&n.postApply(o,e,s,!0),w(t,s)}var f=n.getEmptyElements(e);U(f,function(e){q(e,n.className)})},undoToRanges:function(e){for(var t=e.length;t--;)this.undoToRange(e[t],e);return e},undoToSelection:function(t){var n=e.getSelection(t),s=e.getSelection(t).getAllRanges();this.undoToRanges(s),n.setRanges(s)},isAppliedToRange:function(e){if(e.collapsed||""==e.toString())return!!this.getSelfOrAncestorWithClass(e.commonAncestorContainer);var t=e.getNodes([3]);if(t.length)for(var n,s=0;n=t[s++];)if(!this.isIgnorableWhiteSpaceNode(n)&&C(e,n)&&this.isModifiable(n)&&!this.getSelfOrAncestorWithClass(n))return!1;return!0},isAppliedToRanges:function(e){var t=e.length;if(0==t)return!1;for(;t--;)if(!this.isAppliedToRange(e[t]))return!1;return!0},isAppliedToSelection:function(t){var n=e.getSelection(t);return this.isAppliedToRanges(n.getAllRanges())},toggleRange:function(e){this.isAppliedToRange(e)?this.undoToRange(e):this.applyToRange(e)},toggleSelection:function(e){this.isAppliedToSelection(e)?this.undoToSelection(e):this.applyToSelection(e)},getElementsWithClassIntersectingRange:function(e){var t=[],n=this;return e.getNodes([3],function(e){var s=n.getSelfOrAncestorWithClass(e);s&&!B(t,s)&&t.push(s)}),t},detach:function(){}},H.util={hasClass:o,addClass:a,removeClass:q,getClass:l,hasSameClasses:c,hasAllClasses:p,replaceWithOwnChildren:y,elementsHaveSameNonClassAttributes:E,elementHasNonClassAttributes:b,splitNodeAt:I,isEditableElement:G,isEditingHost:A,isEditable:S},e.CssClassApplier=e.ClassApplier=H,e.createClassApplier=j,D.createAliasForDeprecatedMethod(e,"createCssClassApplier","createClassApplier",t)}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js
new file mode 100755
index 000000000..ea65b24d0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js
@@ -0,0 +1,11 @@
+/**
+ * Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e():t.rangy=e()}(function(){function e(e,t){var n=typeof e[t];return n==N||!(n!=C||!e[t])||"unknown"==n}function t(e,t){return!(typeof e[t]!=C||!e[t])}function n(e,t){return typeof e[t]!=E}function r(e){return function(t,n){for(var r=n.length;r--;)if(!e(t,n[r]))return!1;return!0}}function o(e){return e&&O(e,T)&&D(e,w)}function i(e){return t(e,"body")?e.body:e.getElementsByTagName("body")[0]}function a(t){typeof console!=E&&e(console,"log")&&console.log(t)}function s(e,t){b&&t?alert(e):a(e)}function c(e){I.initialized=!0,I.supported=!1,s("Rangy is not supported in this environment. Reason: "+e,I.config.alertOnFail)}function d(e){s("Rangy warning: "+e,I.config.alertOnWarn)}function f(e){return e.message||e.description||String(e)}function u(){if(b&&!I.initialized){var t,n=!1,r=!1;e(document,"createRange")&&(t=document.createRange(),O(t,y)&&D(t,S)&&(n=!0));var s=i(document);if(!s||"body"!=s.nodeName.toLowerCase())return void c("No body element found");if(s&&e(s,"createTextRange")&&(t=s.createTextRange(),o(t)&&(r=!0)),!n&&!r)return void c("Neither Range nor TextRange are available");I.initialized=!0,I.features={implementsDomRange:n,implementsTextRange:r};var d,u;for(var l in x)(d=x[l])instanceof p&&d.init(d,I);for(var h=0,g=M.length;g>h;++h)try{M[h](I)}catch(m){u="Rangy init listener threw an exception. Continuing. Detail: "+f(m),a(u)}}}function l(e,t,n){n&&(e+=" in module "+n.name),I.warn("DEPRECATED: "+e+" is deprecated. Please use "+t+" instead.")}function h(e,t,n,r){e[t]=function(){return l(t,n,r),e[n].apply(e,P.toArray(arguments))}}function g(e){e=e||window,u();for(var t=0,n=k.length;n>t;++t)k[t](e)}function p(e,t,n){this.name=e,this.dependencies=t,this.initialized=!1,this.supported=!1,this.initializer=n}function m(e,t,n){var r=new p(e,t,function(t){if(!t.initialized){t.initialized=!0;try{n(I,t),t.supported=!0}catch(r){var o="Module '"+e+"' failed to load: "+f(r);a(o),r.stack&&a(r.stack)}}});return x[e]=r,r}function R(){}function v(){}var C="object",N="function",E="undefined",S=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],y=["setStart","setStartBefore","setStartAfter","setEnd","setEndBefore","setEndAfter","collapse","selectNode","selectNodeContents","compareBoundaryPoints","deleteContents","extractContents","cloneContents","insertNode","surroundContents","cloneRange","toString","detach"],w=["boundingHeight","boundingLeft","boundingTop","boundingWidth","htmlText","text"],T=["collapse","compareEndPoints","duplicate","moveToElementText","parentElement","select","setEndPoint","getBoundingClientRect"],O=r(e),_=r(t),D=r(n),A=[].forEach?function(e,t){e.forEach(t)}:function(e,t){for(var n=0,r=e.length;r>n;++n)t(e[n],n)},x={},b=typeof window!=E&&typeof document!=E,P={isHostMethod:e,isHostObject:t,isHostProperty:n,areHostMethods:O,areHostObjects:_,areHostProperties:D,isTextRange:o,getBody:i,forEach:A},I={version:"1.3.0",initialized:!1,isBrowser:b,supported:!0,util:P,features:{},modules:x,config:{alertOnFail:!1,alertOnWarn:!1,preferTextRange:!1,autoInitialize:typeof rangyAutoInitialize==E?!0:rangyAutoInitialize}};I.fail=c,I.warn=d;var B;({}).hasOwnProperty?(P.extend=B=function(e,t,n){var r,o;for(var i in t)t.hasOwnProperty(i)&&(r=e[i],o=t[i],n&&null!==r&&"object"==typeof r&&null!==o&&"object"==typeof o&&B(r,o,!0),e[i]=o);return t.hasOwnProperty("toString")&&(e.toString=t.toString),e},P.createOptions=function(e,t){var n={};return B(n,t),e&&B(n,e),n}):c("hasOwnProperty not supported"),b||c("Rangy can only run in a browser"),function(){var e;if(b){var t=document.createElement("div");t.appendChild(document.createElement("span"));var n=[].slice;try{1==n.call(t.childNodes,0)[0].nodeType&&(e=function(e){return n.call(e,0)})}catch(r){}}e||(e=function(e){for(var t=[],n=0,r=e.length;r>n;++n)t[n]=e[n];return t}),P.toArray=e}();var H;b&&(e(document,"addEventListener")?H=function(e,t,n){e.addEventListener(t,n,!1)}:e(document,"attachEvent")?H=function(e,t,n){e.attachEvent("on"+t,n)}:c("Document does not have required addEventListener or attachEvent method"),P.addListener=H);var M=[];P.deprecationNotice=l,P.createAliasForDeprecatedMethod=h,I.init=u,I.addInitListener=function(e){I.initialized?e(I):M.push(e)};var k=[];I.addShimListener=function(e){k.push(e)},b&&(I.shim=I.createMissingNativeApi=g,h(I,"createMissingNativeApi","shim")),p.prototype={init:function(){for(var e,t,n=this.dependencies||[],r=0,o=n.length;o>r;++r){if(t=n[r],e=x[t],!(e&&e instanceof p))throw new Error("required module '"+t+"' not found");if(e.init(),!e.supported)throw new Error("required module '"+t+"' not supported")}this.initializer(this)},fail:function(e){throw this.initialized=!0,this.supported=!1,new Error(e)},warn:function(e){I.warn("Module "+this.name+": "+e)},deprecationNotice:function(e,t){I.warn("DEPRECATED: "+e+" in module "+this.name+" is deprecated. Please use "+t+" instead")},createError:function(e){return new Error("Error in Rangy "+this.name+" module: "+e)}},I.createModule=function(e){var t,n;2==arguments.length?(t=arguments[1],n=[]):(t=arguments[2],n=arguments[1]);var r=m(e,n,t);I.initialized&&I.supported&&r.init()},I.createCoreModule=function(e,t,n){m(e,t,n)},I.RangePrototype=R,I.rangePrototype=new R,I.selectionPrototype=new v,I.createCoreModule("DomUtil",[],function(e,t){function n(e){var t;return typeof e.namespaceURI==b||null===(t=e.namespaceURI)||"http://www.w3.org/1999/xhtml"==t}function r(e){var t=e.parentNode;return 1==t.nodeType?t:null}function o(e){for(var t=0;e=e.previousSibling;)++t;return t}function i(e){switch(e.nodeType){case 7:case 10:return 0;case 3:case 8:return e.length;default:return e.childNodes.length}}function a(e,t){var n,r=[];for(n=e;n;n=n.parentNode)r.push(n);for(n=t;n;n=n.parentNode)if(M(r,n))return n;return null}function s(e,t,n){for(var r=n?t:t.parentNode;r;){if(r===e)return!0;r=r.parentNode}return!1}function c(e,t){return s(e,t,!0)}function d(e,t,n){for(var r,o=n?e:e.parentNode;o;){if(r=o.parentNode,r===t)return o;o=r}return null}function f(e){var t=e.nodeType;return 3==t||4==t||8==t}function u(e){if(!e)return!1;var t=e.nodeType;return 3==t||8==t}function l(e,t){var n=t.nextSibling,r=t.parentNode;return n?r.insertBefore(e,n):r.appendChild(e),e}function h(e,t,n){var r=e.cloneNode(!1);if(r.deleteData(0,t),e.deleteData(t,e.length-t),l(r,e),n)for(var i,a=0;i=n[a++];)i.node==e&&i.offset>t?(i.node=r,i.offset-=t):i.node==e.parentNode&&i.offset>o(e)&&++i.offset;return r}function g(e){if(9==e.nodeType)return e;if(typeof e.ownerDocument!=b)return e.ownerDocument;if(typeof e.document!=b)return e.document;if(e.parentNode)return g(e.parentNode);throw t.createError("getDocument: no document found for node")}function p(e){var n=g(e);if(typeof n.defaultView!=b)return n.defaultView;if(typeof n.parentWindow!=b)return n.parentWindow;throw t.createError("Cannot get a window object for node")}function m(e){if(typeof e.contentDocument!=b)return e.contentDocument;if(typeof e.contentWindow!=b)return e.contentWindow.document;throw t.createError("getIframeDocument: No Document object found for iframe element")}function R(e){if(typeof e.contentWindow!=b)return e.contentWindow;if(typeof e.contentDocument!=b)return e.contentDocument.defaultView;throw t.createError("getIframeWindow: No Window object found for iframe element")}function v(e){return e&&P.isHostMethod(e,"setTimeout")&&P.isHostObject(e,"document")}function C(e,t,n){var r;if(e?P.isHostProperty(e,"nodeType")?r=1==e.nodeType&&"iframe"==e.tagName.toLowerCase()?m(e):g(e):v(e)&&(r=e.document):r=document,!r)throw t.createError(n+"(): Parameter must be a Window object or DOM node");return r}function N(e){for(var t;t=e.parentNode;)e=t;return e}function E(e,n,r,i){var s,c,f,u,l;if(e==r)return n===i?0:i>n?-1:1;if(s=d(r,e,!0))return n<=o(s)?-1:1;if(s=d(e,r,!0))return o(s)<i?-1:1;if(c=a(e,r),!c)throw new Error("comparePoints error: nodes have no common ancestor");if(f=e===c?c:d(e,c,!0),u=r===c?c:d(r,c,!0),f===u)throw t.createError("comparePoints got to case 4 and childA and childB are the same!");for(l=c.firstChild;l;){if(l===f)return-1;if(l===u)return 1;l=l.nextSibling}}function S(e){var t;try{return t=e.parentNode,!1}catch(n){return!0}}function y(e){if(!e)return"[No node]";if(k&&S(e))return"[Broken node]";if(f(e))return'"'+e.data+'"';if(1==e.nodeType){var t=e.id?' id="'+e.id+'"':"";return"<"+e.nodeName+t+">[index:"+o(e)+",length:"+e.childNodes.length+"]["+(e.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"}return e.nodeName}function w(e){for(var t,n=g(e).createDocumentFragment();t=e.firstChild;)n.appendChild(t);return n}function T(e,t,n){var r=I(e),o=e.createElement("div");o.contentEditable=""+!!n,t&&(o.innerHTML=t);var i=r.firstChild;return i?r.insertBefore(o,i):r.appendChild(o),o}function O(e){return e.parentNode.removeChild(e)}function _(e){this.root=e,this._next=e}function D(e){return new _(e)}function A(e,t){this.node=e,this.offset=t}function x(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}var b="undefined",P=e.util,I=P.getBody;P.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||t.fail("document missing a Node creation method"),P.isHostMethod(document,"getElementsByTagName")||t.fail("document missing getElementsByTagName method");var B=document.createElement("div");P.areHostMethods(B,["insertBefore","appendChild","cloneNode"]||!P.areHostObjects(B,["previousSibling","nextSibling","childNodes","parentNode"]))||t.fail("Incomplete Element implementation"),P.isHostProperty(B,"innerHTML")||t.fail("Element is missing innerHTML property");var H=document.createTextNode("test");P.areHostMethods(H,["splitText","deleteData","insertData","appendData","cloneNode"]||!P.areHostObjects(B,["previousSibling","nextSibling","childNodes","parentNode"])||!P.areHostProperties(H,["data"]))||t.fail("Incomplete Text Node implementation");var M=function(e,t){for(var n=e.length;n--;)if(e[n]===t)return!0;return!1},k=!1;!function(){var t=document.createElement("b");t.innerHTML="1";var n=t.firstChild;t.innerHTML="<br />",k=S(n),e.features.crashyTextNodes=k}();var L;typeof window.getComputedStyle!=b?L=function(e,t){return p(e).getComputedStyle(e,null)[t]}:typeof document.documentElement.currentStyle!=b?L=function(e,t){return e.currentStyle?e.currentStyle[t]:""}:t.fail("No means of obtaining computed style properties found"),_.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e,t,n=this._current=this._next;if(this._current)if(e=n.firstChild)this._next=e;else{for(t=null;n!==this.root&&!(t=n.nextSibling);)n=n.parentNode;this._next=t}return this._current},detach:function(){this._current=this._next=this.root=null}},A.prototype={equals:function(e){return!!e&&this.node===e.node&&this.offset==e.offset},inspect:function(){return"[DomPosition("+y(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},x.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11,INVALID_NODE_TYPE_ERR:24},x.prototype.toString=function(){return this.message},e.dom={arrayContains:M,isHtmlNamespace:n,parentElement:r,getNodeIndex:o,getNodeLength:i,getCommonAncestor:a,isAncestorOf:s,isOrIsAncestorOf:c,getClosestAncestorIn:d,isCharacterDataNode:f,isTextOrCommentNode:u,insertAfter:l,splitDataNode:h,getDocument:g,getWindow:p,getIframeWindow:R,getIframeDocument:m,getBody:I,isWindow:v,getContentDocument:C,getRootContainer:N,comparePoints:E,isBrokenNode:S,inspectNode:y,getComputedStyleProperty:L,createTestElement:T,removeNode:O,fragmentFromNodeChildren:w,createIterator:D,DomPosition:A},e.DOMException=x}),I.createCoreModule("DomRange",["DomUtil"],function(e){function t(e,t){return 3!=e.nodeType&&(F(e,t.startContainer)||F(e,t.endContainer))}function n(e){return e.document||j(e.startContainer)}function r(e){return Q(e.startContainer)}function o(e){return new M(e.parentNode,W(e))}function i(e){return new M(e.parentNode,W(e)+1)}function a(e,t,n){var r=11==e.nodeType?e.firstChild:e;return L(t)?n==t.length?B.insertAfter(e,t):t.parentNode.insertBefore(e,0==n?t:U(t,n)):n>=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[n]),r}function s(e,t,r){if(w(e),w(t),n(t)!=n(e))throw new k("WRONG_DOCUMENT_ERR");var o=z(e.startContainer,e.startOffset,t.endContainer,t.endOffset),i=z(e.endContainer,e.endOffset,t.startContainer,t.startOffset);return r?0>=o&&i>=0:0>o&&i>0}function c(e){for(var t,r,o,i=n(e.range).createDocumentFragment();r=e.next();){if(t=e.isPartiallySelectedSubtree(),r=r.cloneNode(!t),t&&(o=e.getSubtreeIterator(),r.appendChild(c(o)),o.detach()),10==r.nodeType)throw new k("HIERARCHY_REQUEST_ERR");i.appendChild(r)}return i}function d(e,t,n){var r,o;n=n||{stop:!1};for(var i,a;i=e.next();)if(e.isPartiallySelectedSubtree()){if(t(i)===!1)return void(n.stop=!0);if(a=e.getSubtreeIterator(),d(a,t,n),a.detach(),n.stop)return}else for(r=B.createIterator(i);o=r.next();)if(t(o)===!1)return void(n.stop=!0)}function f(e){for(var t;e.next();)e.isPartiallySelectedSubtree()?(t=e.getSubtreeIterator(),f(t),t.detach()):e.remove()}function u(e){for(var t,r,o=n(e.range).createDocumentFragment();t=e.next();){if(e.isPartiallySelectedSubtree()?(t=t.cloneNode(!1),r=e.getSubtreeIterator(),t.appendChild(u(r)),r.detach()):e.remove(),10==t.nodeType)throw new k("HIERARCHY_REQUEST_ERR");o.appendChild(t)}return o}function l(e,t,n){var r,o=!(!t||!t.length),i=!!n;o&&(r=new RegExp("^("+t.join("|")+")$"));var a=[];return d(new g(e,!1),function(t){if(!(o&&!r.test(t.nodeType)||i&&!n(t))){var s=e.startContainer;if(t!=s||!L(s)||e.startOffset!=s.length){var c=e.endContainer;t==c&&L(c)&&0==e.endOffset||a.push(t)}}}),a}function h(e){var t="undefined"==typeof e.getName?"Range":e.getName();return"["+t+"("+B.inspectNode(e.startContainer)+":"+e.startOffset+", "+B.inspectNode(e.endContainer)+":"+e.endOffset+")]"}function g(e,t){if(this.range=e,this.clonePartiallySelectedTextNodes=t,!e.collapsed){this.sc=e.startContainer,this.so=e.startOffset,this.ec=e.endContainer,this.eo=e.endOffset;var n=e.commonAncestorContainer;this.sc===this.ec&&L(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==n||L(this.sc)?V(this.sc,n,!0):this.sc.childNodes[this.so],this._last=this.ec!==n||L(this.ec)?V(this.ec,n,!0):this.ec.childNodes[this.eo-1])}}function p(e){return function(t,n){for(var r,o=n?t:t.parentNode;o;){if(r=o.nodeType,Y(e,r))return o;o=o.parentNode}return null}}function m(e,t){if(rt(e,t))throw new k("INVALID_NODE_TYPE_ERR")}function R(e,t){if(!Y(t,e.nodeType))throw new k("INVALID_NODE_TYPE_ERR")}function v(e,t){if(0>t||t>(L(e)?e.length:e.childNodes.length))throw new k("INDEX_SIZE_ERR")}function C(e,t){if(tt(e,!0)!==tt(t,!0))throw new k("WRONG_DOCUMENT_ERR")}function N(e){if(nt(e,!0))throw new k("NO_MODIFICATION_ALLOWED_ERR")}function E(e,t){if(!e)throw new k(t)}function S(e,t){return t<=(L(e)?e.length:e.childNodes.length)}function y(e){return!!e.startContainer&&!!e.endContainer&&!(G&&(B.isBrokenNode(e.startContainer)||B.isBrokenNode(e.endContainer)))&&Q(e.startContainer)==Q(e.endContainer)&&S(e.startContainer,e.startOffset)&&S(e.endContainer,e.endOffset)}function w(e){if(!y(e))throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: ("+e.inspect()+")")}function T(e,t){w(e);var n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset,a=n===o;L(o)&&i>0&&i<o.length&&U(o,i,t),L(n)&&r>0&&r<n.length&&(n=U(n,r,t),a?(i-=r,o=n):o==n.parentNode&&i>=W(n)&&i++,r=0),e.setStartAndEnd(n,r,o,i)}function O(e){w(e);var t=e.commonAncestorContainer.parentNode.cloneNode(!1);return t.appendChild(e.cloneContents()),t.innerHTML}function _(e){e.START_TO_START=dt,e.START_TO_END=ft,e.END_TO_END=ut,e.END_TO_START=lt,e.NODE_BEFORE=ht,e.NODE_AFTER=gt,e.NODE_BEFORE_AND_AFTER=pt,e.NODE_INSIDE=mt}function D(e){_(e),_(e.prototype)}function A(e,t){return function(){w(this);var n,r,o=this.startContainer,a=this.startOffset,s=this.commonAncestorContainer,c=new g(this,!0);o!==s&&(n=V(o,s,!0),r=i(n),o=r.node,a=r.offset),d(c,N),c.reset();var f=e(c);return c.detach(),t(this,o,a,o,a),f}}function x(n,r){function a(e,t){return function(n){R(n,Z),R(Q(n),$);var r=(e?o:i)(n);(t?s:c)(this,r.node,r.offset)}}function s(e,t,n){var o=e.endContainer,i=e.endOffset;(t!==e.startContainer||n!==e.startOffset)&&((Q(t)!=Q(o)||1==z(t,n,o,i))&&(o=t,i=n),r(e,t,n,o,i))}function c(e,t,n){var o=e.startContainer,i=e.startOffset;(t!==e.endContainer||n!==e.endOffset)&&((Q(t)!=Q(o)||-1==z(t,n,o,i))&&(o=t,i=n),r(e,o,i,t,n))}var d=function(){};d.prototype=e.rangePrototype,n.prototype=new d,H.extend(n.prototype,{setStart:function(e,t){m(e,!0),v(e,t),s(this,e,t)},setEnd:function(e,t){m(e,!0),v(e,t),c(this,e,t)},setStartAndEnd:function(){var e=arguments,t=e[0],n=e[1],o=t,i=n;switch(e.length){case 3:i=e[2];break;case 4:o=e[2],i=e[3]}r(this,t,n,o,i)},setBoundary:function(e,t,n){this["set"+(n?"Start":"End")](e,t)},setStartBefore:a(!0,!0),setStartAfter:a(!1,!0),setEndBefore:a(!0,!1),setEndAfter:a(!1,!1),collapse:function(e){w(this),e?r(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):r(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){m(e,!0),r(this,e,0,e,q(e))},selectNode:function(e){m(e,!1),R(e,Z);var t=o(e),n=i(e);r(this,t.node,t.offset,n.node,n.offset)},extractContents:A(u,r),deleteContents:A(f,r),canSurroundContents:function(){w(this),N(this.startContainer),N(this.endContainer);var e=new g(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},splitBoundaries:function(){T(this)},splitBoundariesPreservingPositions:function(e){T(this,e)},normalizeBoundaries:function(){w(this);var e,t=this.startContainer,n=this.startOffset,o=this.endContainer,i=this.endOffset,a=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(o=e,i=e.length,e.appendData(t.data),X(t))},s=function(e){var r=e.previousSibling;if(r&&r.nodeType==e.nodeType){t=e;var a=e.length;if(n=r.length,e.insertData(0,r.data),X(r),t==o)i+=n,o=t;else if(o==e.parentNode){var s=W(e);i==s?(o=e,i=a):i>s&&i--}}},c=!0;if(L(o))i==o.length?a(o):0==i&&(e=o.previousSibling,e&&e.nodeType==o.nodeType&&(i=e.length,t==o&&(c=!1),e.appendData(o.data),X(o),o=e));else{if(i>0){var d=o.childNodes[i-1];d&&L(d)&&a(d)}c=!this.collapsed}if(c){if(L(t))0==n?s(t):n==t.length&&(e=t.nextSibling,e&&e.nodeType==t.nodeType&&(o==e&&(o=t,i+=t.length),t.appendData(e.data),X(e)));else if(n<t.childNodes.length){var f=t.childNodes[n];f&&L(f)&&s(f)}}else t=o,n=i;r(this,t,n,o,i)},collapseToPoint:function(e,t){m(e,!0),v(e,t),this.setStartAndEnd(e,t)}}),D(n)}function b(e){e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset,e.commonAncestorContainer=e.collapsed?e.startContainer:B.getCommonAncestor(e.startContainer,e.endContainer)}function P(e,t,n,r,o){e.startContainer=t,e.startOffset=n,e.endContainer=r,e.endOffset=o,e.document=B.getDocument(t),b(e)}function I(e){this.startContainer=e,this.startOffset=0,this.endContainer=e,this.endOffset=0,this.document=e,b(this)}var B=e.dom,H=e.util,M=B.DomPosition,k=e.DOMException,L=B.isCharacterDataNode,W=B.getNodeIndex,F=B.isOrIsAncestorOf,j=B.getDocument,z=B.comparePoints,U=B.splitDataNode,V=B.getClosestAncestorIn,q=B.getNodeLength,Y=B.arrayContains,Q=B.getRootContainer,G=e.features.crashyTextNodes,X=B.removeNode;g.prototype={_current:null,_next:null,_first:null,_last:null,isSingleCharacterDataNode:!1,reset:function(){this._current=null,this._next=this._first},hasNext:function(){return!!this._next},next:function(){var e=this._current=this._next;return e&&(this._next=e!==this._last?e.nextSibling:null,L(e)&&this.clonePartiallySelectedTextNodes&&(e===this.ec&&(e=e.cloneNode(!0)).deleteData(this.eo,e.length-this.eo),this._current===this.sc&&(e=e.cloneNode(!0)).deleteData(0,this.so))),e},remove:function(){var e,t,n=this._current;!L(n)||n!==this.sc&&n!==this.ec?n.parentNode&&X(n):(e=n===this.sc?this.so:0,t=n===this.ec?this.eo:n.length,e!=t&&n.deleteData(e,t-e))},isPartiallySelectedSubtree:function(){var e=this._current;return t(e,this.range)},getSubtreeIterator:function(){var e;if(this.isSingleCharacterDataNode)e=this.range.cloneRange(),e.collapse(!1);else{e=new I(n(this.range));var t=this._current,r=t,o=0,i=t,a=q(t);F(t,this.sc)&&(r=this.sc,o=this.so),F(t,this.ec)&&(i=this.ec,a=this.eo),P(e,r,o,i,a)}return new g(e,this.clonePartiallySelectedTextNodes)},detach:function(){this.range=this._current=this._next=this._first=this._last=this.sc=this.so=this.ec=this.eo=null}};var Z=[1,3,4,5,7,8,10],$=[2,9,11],J=[5,6,10,12],K=[1,3,4,5,7,8,10,11],et=[1,3,4,5,7,8],tt=p([9,11]),nt=p(J),rt=p([6,10,12]),ot=document.createElement("style"),it=!1;try{ot.innerHTML="<b>x</b>",it=3==ot.firstChild.nodeType}catch(at){}e.features.htmlParsingConforms=it;var st=it?function(e){var t=this.startContainer,n=j(t);if(!t)throw new k("INVALID_STATE_ERR");var r=null;return 1==t.nodeType?r=t:L(t)&&(r=B.parentElement(t)),r=null===r||"HTML"==r.nodeName&&B.isHtmlNamespace(j(r).documentElement)&&B.isHtmlNamespace(r)?n.createElement("body"):r.cloneNode(!1),r.innerHTML=e,B.fragmentFromNodeChildren(r)}:function(e){var t=n(this),r=t.createElement("body");return r.innerHTML=e,B.fragmentFromNodeChildren(r)},ct=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],dt=0,ft=1,ut=2,lt=3,ht=0,gt=1,pt=2,mt=3;H.extend(e.rangePrototype,{compareBoundaryPoints:function(e,t){w(this),C(this.startContainer,t.startContainer);var n,r,o,i,a=e==lt||e==dt?"start":"end",s=e==ft||e==dt?"start":"end";return n=this[a+"Container"],r=this[a+"Offset"],o=t[s+"Container"],i=t[s+"Offset"],z(n,r,o,i)},insertNode:function(e){if(w(this),R(e,K),N(this.startContainer),F(e,this.startContainer))throw new k("HIERARCHY_REQUEST_ERR");var t=a(e,this.startContainer,this.startOffset);this.setStartBefore(t)},cloneContents:function(){w(this);var e,t;if(this.collapsed)return n(this).createDocumentFragment();if(this.startContainer===this.endContainer&&L(this.startContainer))return e=this.startContainer.cloneNode(!0),e.data=e.data.slice(this.startOffset,this.endOffset),t=n(this).createDocumentFragment(),t.appendChild(e),t;var r=new g(this,!0);return e=c(r),r.detach(),e},canSurroundContents:function(){w(this),N(this.startContainer),N(this.endContainer);var e=new g(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},surroundContents:function(e){if(R(e,et),!this.canSurroundContents())throw new k("INVALID_STATE_ERR");var t=this.extractContents();if(e.hasChildNodes())for(;e.lastChild;)e.removeChild(e.lastChild);a(e,this.startContainer,this.startOffset),e.appendChild(t),this.selectNode(e)},cloneRange:function(){w(this);for(var e,t=new I(n(this)),r=ct.length;r--;)e=ct[r],t[e]=this[e];return t},toString:function(){w(this);var e=this.startContainer;if(e===this.endContainer&&L(e))return 3==e.nodeType||4==e.nodeType?e.data.slice(this.startOffset,this.endOffset):"";var t=[],n=new g(this,!0);return d(n,function(e){(3==e.nodeType||4==e.nodeType)&&t.push(e.data)}),n.detach(),t.join("")},compareNode:function(e){w(this);var t=e.parentNode,n=W(e);if(!t)throw new k("NOT_FOUND_ERR");var r=this.comparePoint(t,n),o=this.comparePoint(t,n+1);return 0>r?o>0?pt:ht:o>0?gt:mt},comparePoint:function(e,t){return w(this),E(e,"HIERARCHY_REQUEST_ERR"),C(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)<0?-1:z(e,t,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:st,toHtml:function(){return O(this)},intersectsNode:function(e,t){if(w(this),Q(e)!=r(this))return!1;var n=e.parentNode,o=W(e);if(!n)return!0;var i=z(n,o,this.endContainer,this.endOffset),a=z(n,o+1,this.startContainer,this.startOffset);return t?0>=i&&a>=0:0>i&&a>0},isPointInRange:function(e,t){return w(this),E(e,"HIERARCHY_REQUEST_ERR"),C(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)>=0&&z(e,t,this.endContainer,this.endOffset)<=0},intersectsRange:function(e){return s(this,e,!1)},intersectsOrTouchesRange:function(e){return s(this,e,!0)},intersection:function(e){if(this.intersectsRange(e)){var t=z(this.startContainer,this.startOffset,e.startContainer,e.startOffset),n=z(this.endContainer,this.endOffset,e.endContainer,e.endOffset),r=this.cloneRange();return-1==t&&r.setStart(e.startContainer,e.startOffset),1==n&&r.setEnd(e.endContainer,e.endOffset),r}return null},union:function(e){if(this.intersectsOrTouchesRange(e)){var t=this.cloneRange();return-1==z(e.startContainer,e.startOffset,this.startContainer,this.startOffset)&&t.setStart(e.startContainer,e.startOffset),1==z(e.endContainer,e.endOffset,this.endContainer,this.endOffset)&&t.setEnd(e.endContainer,e.endOffset),t}throw new k("Ranges do not intersect")},containsNode:function(e,t){return t?this.intersectsNode(e,!1):this.compareNode(e)==mt},containsNodeContents:function(e){return this.comparePoint(e,0)>=0&&this.comparePoint(e,q(e))<=0},containsRange:function(e){var t=this.intersection(e);return null!==t&&e.equals(t)},containsNodeText:function(e){var t=this.cloneRange();t.selectNode(e);var n=t.getNodes([3]);if(n.length>0){t.setStart(n[0],0);var r=n.pop();return t.setEnd(r,r.length),this.containsRange(t)}return this.containsNodeContents(e)},getNodes:function(e,t){return w(this),l(this,e,t)},getDocument:function(){return n(this)},collapseBefore:function(e){this.setEndBefore(e),this.collapse(!1)},collapseAfter:function(e){this.setStartAfter(e),this.collapse(!0)},getBookmark:function(t){var r=n(this),o=e.createRange(r);t=t||B.getBody(r),o.selectNodeContents(t);var i=this.intersection(o),a=0,s=0;return i&&(o.setEnd(i.startContainer,i.startOffset),a=o.toString().length,s=a+i.toString().length),{start:a,end:s,containerNode:t}},moveToBookmark:function(e){var t=e.containerNode,n=0;this.setStart(t,0),this.collapse(!0);for(var r,o,i,a,s=[t],c=!1,d=!1;!d&&(r=s.pop());)if(3==r.nodeType)o=n+r.length,!c&&e.start>=n&&e.start<=o&&(this.setStart(r,e.start-n),c=!0),c&&e.end>=n&&e.end<=o&&(this.setEnd(r,e.end-n),d=!0),n=o;else for(a=r.childNodes,i=a.length;i--;)s.push(a[i])},getName:function(){return"DomRange"},equals:function(e){return I.rangesEqual(this,e)},isValid:function(){return y(this)},inspect:function(){return h(this)},detach:function(){}}),x(I,P),H.extend(I,{rangeProperties:ct,RangeIterator:g,copyComparisonConstants:D,createPrototypeRange:x,inspect:h,toHtml:O,getRangeDocument:n,rangesEqual:function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset}}),e.DomRange=I}),I.createCoreModule("WrappedRange",["DomRange"],function(e,t){var n,r,o=e.dom,i=e.util,a=o.DomPosition,s=e.DomRange,c=o.getBody,d=o.getContentDocument,f=o.isCharacterDataNode;if(e.features.implementsDomRange&&!function(){function r(e){for(var t,n=l.length;n--;)t=l[n],e[t]=e.nativeRange[t];e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset}function a(e,t,n,r,o){var i=e.startContainer!==t||e.startOffset!=n,a=e.endContainer!==r||e.endOffset!=o,s=!e.equals(e.nativeRange);(i||a||s)&&(e.setEnd(r,o),e.setStart(t,n))}var f,u,l=s.rangeProperties;n=function(e){if(!e)throw t.createError("WrappedRange: Range must be specified");this.nativeRange=e,r(this)},s.createPrototypeRange(n,a),f=n.prototype,f.selectNode=function(e){this.nativeRange.selectNode(e),r(this)},f.cloneContents=function(){return this.nativeRange.cloneContents()},f.surroundContents=function(e){this.nativeRange.surroundContents(e),r(this)},f.collapse=function(e){this.nativeRange.collapse(e),r(this)},f.cloneRange=function(){return new n(this.nativeRange.cloneRange())},f.refresh=function(){r(this)},f.toString=function(){return this.nativeRange.toString()};var h=document.createTextNode("test");c(document).appendChild(h);var g=document.createRange();g.setStart(h,0),g.setEnd(h,0);try{g.setStart(h,1),f.setStart=function(e,t){this.nativeRange.setStart(e,t),r(this)},f.setEnd=function(e,t){this.nativeRange.setEnd(e,t),r(this)},u=function(e){return function(t){this.nativeRange[e](t),r(this)}}}catch(p){f.setStart=function(e,t){try{this.nativeRange.setStart(e,t)}catch(n){this.nativeRange.setEnd(e,t),this.nativeRange.setStart(e,t)}r(this)},f.setEnd=function(e,t){try{this.nativeRange.setEnd(e,t)}catch(n){this.nativeRange.setStart(e,t),this.nativeRange.setEnd(e,t)}r(this)},u=function(e,t){return function(n){try{this.nativeRange[e](n)}catch(o){this.nativeRange[t](n),this.nativeRange[e](n)}r(this)}}}f.setStartBefore=u("setStartBefore","setEndBefore"),f.setStartAfter=u("setStartAfter","setEndAfter"),f.setEndBefore=u("setEndBefore","setStartBefore"),f.setEndAfter=u("setEndAfter","setStartAfter"),f.selectNodeContents=function(e){this.setStartAndEnd(e,0,o.getNodeLength(e))},g.selectNodeContents(h),g.setEnd(h,3);var m=document.createRange();m.selectNodeContents(h),m.setEnd(h,4),m.setStart(h,2),f.compareBoundaryPoints=-1==g.compareBoundaryPoints(g.START_TO_END,m)&&1==g.compareBoundaryPoints(g.END_TO_START,m)?function(e,t){return t=t.nativeRange||t,e==t.START_TO_END?e=t.END_TO_START:e==t.END_TO_START&&(e=t.START_TO_END),this.nativeRange.compareBoundaryPoints(e,t)}:function(e,t){return this.nativeRange.compareBoundaryPoints(e,t.nativeRange||t)};var R=document.createElement("div");R.innerHTML="123";var v=R.firstChild,C=c(document);C.appendChild(R),g.setStart(v,1),g.setEnd(v,2),g.deleteContents(),"13"==v.data&&(f.deleteContents=function(){this.nativeRange.deleteContents(),r(this)},f.extractContents=function(){var e=this.nativeRange.extractContents();return r(this),e}),C.removeChild(R),C=null,i.isHostMethod(g,"createContextualFragment")&&(f.createContextualFragment=function(e){return this.nativeRange.createContextualFragment(e)}),c(document).removeChild(h),f.getName=function(){return"WrappedRange"},e.WrappedRange=n,e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),e.createRange()}}(),e.features.implementsTextRange){var u=function(e){var t=e.parentElement(),n=e.duplicate();n.collapse(!0);var r=n.parentElement();n=e.duplicate(),n.collapse(!1);var i=n.parentElement(),a=r==i?r:o.getCommonAncestor(r,i);return a==t?a:o.getCommonAncestor(t,a)},l=function(e){return 0==e.compareEndPoints("StartToEnd",e)},h=function(e,t,n,r,i){var s=e.duplicate();s.collapse(n);var c=s.parentElement();if(o.isOrIsAncestorOf(t,c)||(c=t),!c.canHaveHTML){var d=new a(c.parentNode,o.getNodeIndex(c));return{boundaryPosition:d,nodeInfo:{nodeIndex:d.offset,containerElement:d.node}}}var u=o.getDocument(c).createElement("span");u.parentNode&&o.removeNode(u);for(var l,h,g,p,m,R=n?"StartToStart":"StartToEnd",v=i&&i.containerElement==c?i.nodeIndex:0,C=c.childNodes.length,N=C,E=N;;){if(E==C?c.appendChild(u):c.insertBefore(u,c.childNodes[E]),s.moveToElementText(u),l=s.compareEndPoints(R,e),0==l||v==N)break;if(-1==l){if(N==v+1)break;v=E}else N=N==v+1?v:E;E=Math.floor((v+N)/2),c.removeChild(u)}if(m=u.nextSibling,-1==l&&m&&f(m)){s.setEndPoint(n?"EndToStart":"EndToEnd",e);var S;if(/[\r\n]/.test(m.data)){var y=s.duplicate(),w=y.text.replace(/\r\n/g,"\r").length;for(S=y.moveStart("character",w);-1==(l=y.compareEndPoints("StartToEnd",y));)S++,y.moveStart("character",1)}else S=s.text.length;p=new a(m,S)}else h=(r||!n)&&u.previousSibling,g=(r||n)&&u.nextSibling,p=g&&f(g)?new a(g,0):h&&f(h)?new a(h,h.data.length):new a(c,o.getNodeIndex(u));return o.removeNode(u),{boundaryPosition:p,nodeInfo:{nodeIndex:E,containerElement:c}}},g=function(e,t){var n,r,i,a,s=e.offset,d=o.getDocument(e.node),u=c(d).createTextRange(),l=f(e.node);return l?(n=e.node,r=n.parentNode):(a=e.node.childNodes,n=s<a.length?a[s]:null,r=e.node),i=d.createElement("span"),i.innerHTML="&#feff;",n?r.insertBefore(i,n):r.appendChild(i),u.moveToElementText(i),u.collapse(!t),r.removeChild(i),l&&u[t?"moveStart":"moveEnd"]("character",s),u};r=function(e){this.textRange=e,this.refresh()},r.prototype=new s(document),r.prototype.refresh=function(){var e,t,n,r=u(this.textRange);
+l(this.textRange)?t=e=h(this.textRange,r,!0,!0).boundaryPosition:(n=h(this.textRange,r,!0,!1),e=n.boundaryPosition,t=h(this.textRange,r,!1,!1,n.nodeInfo).boundaryPosition),this.setStart(e.node,e.offset),this.setEnd(t.node,t.offset)},r.prototype.getName=function(){return"WrappedTextRange"},s.copyComparisonConstants(r);var p=function(e){if(e.collapsed)return g(new a(e.startContainer,e.startOffset),!0);var t=g(new a(e.startContainer,e.startOffset),!0),n=g(new a(e.endContainer,e.endOffset),!1),r=c(s.getRangeDocument(e)).createTextRange();return r.setEndPoint("StartToStart",t),r.setEndPoint("EndToEnd",n),r};if(r.rangeToTextRange=p,r.prototype.toTextRange=function(){return p(this)},e.WrappedTextRange=r,!e.features.implementsDomRange||e.config.preferTextRange){var m=function(e){return e("return this;")()}(Function);"undefined"==typeof m.Range&&(m.Range=r),e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),c(e).createTextRange()},e.WrappedRange=r}}e.createRange=function(n){return n=d(n,t,"createRange"),new e.WrappedRange(e.createNativeRange(n))},e.createRangyRange=function(e){return e=d(e,t,"createRangyRange"),new s(e)},i.createAliasForDeprecatedMethod(e,"createIframeRange","createRange"),i.createAliasForDeprecatedMethod(e,"createIframeRangyRange","createRangyRange"),e.addShimListener(function(t){var n=t.document;"undefined"==typeof n.createRange&&(n.createRange=function(){return e.createRange(n)}),n=t=null})}),I.createCoreModule("WrappedSelection",["DomRange","WrappedRange"],function(e,t){function n(e){return"string"==typeof e?/^backward(s)?$/i.test(e):!!e}function r(e,n){if(e){if(D.isWindow(e))return e;if(e instanceof R)return e.win;var r=D.getContentDocument(e,t,n);return D.getWindow(r)}return window}function o(e){return r(e,"getWinSelection").getSelection()}function i(e){return r(e,"getDocSelection").document.selection}function a(e){var t=!1;return e.anchorNode&&(t=1==D.comparePoints(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset)),t}function s(e,t,n){var r=n?"end":"start",o=n?"start":"end";e.anchorNode=t[r+"Container"],e.anchorOffset=t[r+"Offset"],e.focusNode=t[o+"Container"],e.focusOffset=t[o+"Offset"]}function c(e){var t=e.nativeSelection;e.anchorNode=t.anchorNode,e.anchorOffset=t.anchorOffset,e.focusNode=t.focusNode,e.focusOffset=t.focusOffset}function d(e){e.anchorNode=e.focusNode=null,e.anchorOffset=e.focusOffset=0,e.rangeCount=0,e.isCollapsed=!0,e._ranges.length=0}function f(t){var n;return t instanceof b?(n=e.createNativeRange(t.getDocument()),n.setEnd(t.endContainer,t.endOffset),n.setStart(t.startContainer,t.startOffset)):t instanceof P?n=t.nativeRange:H.implementsDomRange&&t instanceof D.getWindow(t.startContainer).Range&&(n=t),n}function u(e){if(!e.length||1!=e[0].nodeType)return!1;for(var t=1,n=e.length;n>t;++t)if(!D.isAncestorOf(e[0],e[t]))return!1;return!0}function l(e){var n=e.getNodes();if(!u(n))throw t.createError("getSingleElementFromRange: range "+e.inspect()+" did not consist of a single element");return n[0]}function h(e){return!!e&&"undefined"!=typeof e.text}function g(e,t){var n=new P(t);e._ranges=[n],s(e,n,!1),e.rangeCount=1,e.isCollapsed=n.collapsed}function p(t){if(t._ranges.length=0,"None"==t.docSelection.type)d(t);else{var n=t.docSelection.createRange();if(h(n))g(t,n);else{t.rangeCount=n.length;for(var r,o=k(n.item(0)),i=0;i<t.rangeCount;++i)r=e.createRange(o),r.selectNode(n.item(i)),t._ranges.push(r);t.isCollapsed=1==t.rangeCount&&t._ranges[0].collapsed,s(t,t._ranges[t.rangeCount-1],!1)}}}function m(e,n){for(var r=e.docSelection.createRange(),o=l(n),i=k(r.item(0)),a=L(i).createControlRange(),s=0,c=r.length;c>s;++s)a.add(r.item(s));try{a.add(o)}catch(d){throw t.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)")}a.select(),p(e)}function R(e,t,n){this.nativeSelection=e,this.docSelection=t,this._ranges=[],this.win=n,this.refresh()}function v(e){e.win=e.anchorNode=e.focusNode=e._ranges=null,e.rangeCount=e.anchorOffset=e.focusOffset=0,e.detached=!0}function C(e,t){for(var n,r,o=tt.length;o--;)if(n=tt[o],r=n.selection,"deleteAll"==t)v(r);else if(n.win==e)return"delete"==t?(tt.splice(o,1),!0):r;return"deleteAll"==t&&(tt.length=0),null}function N(e,n){for(var r,o=k(n[0].startContainer),i=L(o).createControlRange(),a=0,s=n.length;s>a;++a){r=l(n[a]);try{i.add(r)}catch(c){throw t.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)")}}i.select(),p(e)}function E(e,t){if(e.win.document!=k(t))throw new I("WRONG_DOCUMENT_ERR")}function S(t){return function(n,r){var o;this.rangeCount?(o=this.getRangeAt(0),o["set"+(t?"Start":"End")](n,r)):(o=e.createRange(this.win.document),o.setStartAndEnd(n,r)),this.setSingleRange(o,this.isBackward())}}function y(e){var t=[],n=new B(e.anchorNode,e.anchorOffset),r=new B(e.focusNode,e.focusOffset),o="function"==typeof e.getName?e.getName():"Selection";if("undefined"!=typeof e.rangeCount)for(var i=0,a=e.rangeCount;a>i;++i)t[i]=b.inspect(e.getRangeAt(i));return"["+o+"(Ranges: "+t.join(", ")+")(anchor: "+n.inspect()+", focus: "+r.inspect()+"]"}e.config.checkSelectionRanges=!0;var w,T,O="boolean",_="number",D=e.dom,A=e.util,x=A.isHostMethod,b=e.DomRange,P=e.WrappedRange,I=e.DOMException,B=D.DomPosition,H=e.features,M="Control",k=D.getDocument,L=D.getBody,W=b.rangesEqual,F=x(window,"getSelection"),j=A.isHostObject(document,"selection");H.implementsWinGetSelection=F,H.implementsDocSelection=j;var z=j&&(!F||e.config.preferTextRange);if(z)w=i,e.isSelectionValid=function(e){var t=r(e,"isSelectionValid").document,n=t.selection;return"None"!=n.type||k(n.createRange().parentElement())==t};else{if(!F)return t.fail("Neither document.selection or window.getSelection() detected."),!1;w=o,e.isSelectionValid=function(){return!0}}e.getNativeSelection=w;var U=w();if(!U)return t.fail("Native selection was null (possibly issue 138?)"),!1;var V=e.createNativeRange(document),q=L(document),Y=A.areHostProperties(U,["anchorNode","focusNode","anchorOffset","focusOffset"]);H.selectionHasAnchorAndFocus=Y;var Q=x(U,"extend");H.selectionHasExtend=Q;var G=typeof U.rangeCount==_;H.selectionHasRangeCount=G;var X=!1,Z=!0,$=Q?function(t,n){var r=b.getRangeDocument(n),o=e.createRange(r);o.collapseToPoint(n.endContainer,n.endOffset),t.addRange(f(o)),t.extend(n.startContainer,n.startOffset)}:null;A.areHostMethods(U,["addRange","getRangeAt","removeAllRanges"])&&typeof U.rangeCount==_&&H.implementsDomRange&&!function(){var t=window.getSelection();if(t){for(var n=t.rangeCount,r=n>1,o=[],i=a(t),s=0;n>s;++s)o[s]=t.getRangeAt(s);var c=D.createTestElement(document,"",!1),d=c.appendChild(document.createTextNode("   ")),f=document.createRange();if(f.setStart(d,1),f.collapse(!0),t.removeAllRanges(),t.addRange(f),Z=1==t.rangeCount,t.removeAllRanges(),!r){var u=window.navigator.appVersion.match(/Chrome\/(.*?) /);if(u&&parseInt(u[1])>=36)X=!1;else{var l=f.cloneRange();f.setStart(d,0),l.setEnd(d,3),l.setStart(d,2),t.addRange(f),t.addRange(l),X=2==t.rangeCount}}for(D.removeNode(c),t.removeAllRanges(),s=0;n>s;++s)0==s&&i?$?$(t,o[s]):(e.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"),t.addRange(o[s])):t.addRange(o[s])}}(),H.selectionSupportsMultipleRanges=X,H.collapsedNonEditableSelectionsSupported=Z;var J,K=!1;q&&x(q,"createControlRange")&&(J=q.createControlRange(),A.areHostProperties(J,["item","add"])&&(K=!0)),H.implementsControlRange=K,T=Y?function(e){return e.anchorNode===e.focusNode&&e.anchorOffset===e.focusOffset}:function(e){return e.rangeCount?e.getRangeAt(e.rangeCount-1).collapsed:!1};var et;x(U,"getRangeAt")?et=function(e,t){try{return e.getRangeAt(t)}catch(n){return null}}:Y&&(et=function(t){var n=k(t.anchorNode),r=e.createRange(n);return r.setStartAndEnd(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset),r.collapsed!==this.isCollapsed&&r.setStartAndEnd(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset),r}),R.prototype=e.selectionPrototype;var tt=[],nt=function(e){if(e&&e instanceof R)return e.refresh(),e;e=r(e,"getNativeSelection");var t=C(e),n=w(e),o=j?i(e):null;return t?(t.nativeSelection=n,t.docSelection=o,t.refresh()):(t=new R(n,o,e),tt.push({win:e,selection:t})),t};e.getSelection=nt,A.createAliasForDeprecatedMethod(e,"getIframeSelection","getSelection");var rt=R.prototype;if(!z&&Y&&A.areHostMethods(U,["removeAllRanges","addRange"])){rt.removeAllRanges=function(){this.nativeSelection.removeAllRanges(),d(this)};var ot=function(e,t){$(e.nativeSelection,t),e.refresh()};rt.addRange=G?function(t,r){if(K&&j&&this.docSelection.type==M)m(this,t);else if(n(r)&&Q)ot(this,t);else{var o;X?o=this.rangeCount:(this.removeAllRanges(),o=0);var i=f(t).cloneRange();try{this.nativeSelection.addRange(i)}catch(a){}if(this.rangeCount=this.nativeSelection.rangeCount,this.rangeCount==o+1){if(e.config.checkSelectionRanges){var c=et(this.nativeSelection,this.rangeCount-1);c&&!W(c,t)&&(t=new P(c))}this._ranges[this.rangeCount-1]=t,s(this,t,st(this.nativeSelection)),this.isCollapsed=T(this)}else this.refresh()}}:function(e,t){n(t)&&Q?ot(this,e):(this.nativeSelection.addRange(f(e)),this.refresh())},rt.setRanges=function(e){if(K&&j&&e.length>1)N(this,e);else{this.removeAllRanges();for(var t=0,n=e.length;n>t;++t)this.addRange(e[t])}}}else{if(!(x(U,"empty")&&x(V,"select")&&K&&z))return t.fail("No means of selecting a Range or TextRange was found"),!1;rt.removeAllRanges=function(){try{if(this.docSelection.empty(),"None"!=this.docSelection.type){var e;if(this.anchorNode)e=k(this.anchorNode);else if(this.docSelection.type==M){var t=this.docSelection.createRange();t.length&&(e=k(t.item(0)))}if(e){var n=L(e).createTextRange();n.select(),this.docSelection.empty()}}}catch(r){}d(this)},rt.addRange=function(t){this.docSelection.type==M?m(this,t):(e.WrappedTextRange.rangeToTextRange(t).select(),this._ranges[0]=t,this.rangeCount=1,this.isCollapsed=this._ranges[0].collapsed,s(this,t,!1))},rt.setRanges=function(e){this.removeAllRanges();var t=e.length;t>1?N(this,e):t&&this.addRange(e[0])}}rt.getRangeAt=function(e){if(0>e||e>=this.rangeCount)throw new I("INDEX_SIZE_ERR");return this._ranges[e].cloneRange()};var it;if(z)it=function(t){var n;e.isSelectionValid(t.win)?n=t.docSelection.createRange():(n=L(t.win.document).createTextRange(),n.collapse(!0)),t.docSelection.type==M?p(t):h(n)?g(t,n):d(t)};else if(x(U,"getRangeAt")&&typeof U.rangeCount==_)it=function(t){if(K&&j&&t.docSelection.type==M)p(t);else if(t._ranges.length=t.rangeCount=t.nativeSelection.rangeCount,t.rangeCount){for(var n=0,r=t.rangeCount;r>n;++n)t._ranges[n]=new e.WrappedRange(t.nativeSelection.getRangeAt(n));s(t,t._ranges[t.rangeCount-1],st(t.nativeSelection)),t.isCollapsed=T(t)}else d(t)};else{if(!Y||typeof U.isCollapsed!=O||typeof V.collapsed!=O||!H.implementsDomRange)return t.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;it=function(e){var t,n=e.nativeSelection;n.anchorNode?(t=et(n,0),e._ranges=[t],e.rangeCount=1,c(e),e.isCollapsed=T(e)):d(e)}}rt.refresh=function(e){var t=e?this._ranges.slice(0):null,n=this.anchorNode,r=this.anchorOffset;if(it(this),e){var o=t.length;if(o!=this._ranges.length)return!0;if(this.anchorNode!=n||this.anchorOffset!=r)return!0;for(;o--;)if(!W(t[o],this._ranges[o]))return!0;return!1}};var at=function(e,t){var n=e.getAllRanges();e.removeAllRanges();for(var r=0,o=n.length;o>r;++r)W(t,n[r])||e.addRange(n[r]);e.rangeCount||d(e)};rt.removeRange=K&&j?function(e){if(this.docSelection.type==M){for(var t,n=this.docSelection.createRange(),r=l(e),o=k(n.item(0)),i=L(o).createControlRange(),a=!1,s=0,c=n.length;c>s;++s)t=n.item(s),t!==r||a?i.add(n.item(s)):a=!0;i.select(),p(this)}else at(this,e)}:function(e){at(this,e)};var st;!z&&Y&&H.implementsDomRange?(st=a,rt.isBackward=function(){return st(this)}):st=rt.isBackward=function(){return!1},rt.isBackwards=rt.isBackward,rt.toString=function(){for(var e=[],t=0,n=this.rangeCount;n>t;++t)e[t]=""+this._ranges[t];return e.join("")},rt.collapse=function(t,n){E(this,t);var r=e.createRange(t);r.collapseToPoint(t,n),this.setSingleRange(r),this.isCollapsed=!0},rt.collapseToStart=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[0];this.collapse(e.startContainer,e.startOffset)},rt.collapseToEnd=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[this.rangeCount-1];this.collapse(e.endContainer,e.endOffset)},rt.selectAllChildren=function(t){E(this,t);var n=e.createRange(t);n.selectNodeContents(t),this.setSingleRange(n)},rt.deleteFromDocument=function(){if(K&&j&&this.docSelection.type==M){for(var e,t=this.docSelection.createRange();t.length;)e=t.item(0),t.remove(e),D.removeNode(e);this.refresh()}else if(this.rangeCount){var n=this.getAllRanges();if(n.length){this.removeAllRanges();for(var r=0,o=n.length;o>r;++r)n[r].deleteContents();this.addRange(n[o-1])}}},rt.eachRange=function(e,t){for(var n=0,r=this._ranges.length;r>n;++n)if(e(this.getRangeAt(n)))return t},rt.getAllRanges=function(){var e=[];return this.eachRange(function(t){e.push(t)}),e},rt.setSingleRange=function(e,t){this.removeAllRanges(),this.addRange(e,t)},rt.callMethodOnEachRange=function(e,t){var n=[];return this.eachRange(function(r){n.push(r[e].apply(r,t||[]))}),n},rt.setStart=S(!0),rt.setEnd=S(!1),e.rangePrototype.select=function(e){nt(this.getDocument()).setSingleRange(this,e)},rt.changeEachRange=function(e){var t=[],n=this.isBackward();this.eachRange(function(n){e(n),t.push(n)}),this.removeAllRanges(),n&&1==t.length?this.addRange(t[0],"backward"):this.setRanges(t)},rt.containsNode=function(e,t){return this.eachRange(function(n){return n.containsNode(e,t)},!0)||!1},rt.getBookmark=function(e){return{backward:this.isBackward(),rangeBookmarks:this.callMethodOnEachRange("getBookmark",[e])}},rt.moveToBookmark=function(t){for(var n,r,o=[],i=0;n=t.rangeBookmarks[i++];)r=e.createRange(this.win),r.moveToBookmark(n),o.push(r);t.backward?this.setSingleRange(o[0],"backward"):this.setRanges(o)},rt.saveRanges=function(){return{backward:this.isBackward(),ranges:this.callMethodOnEachRange("cloneRange")}},rt.restoreRanges=function(e){this.removeAllRanges();for(var t,n=0;t=e.ranges[n];++n)this.addRange(t,e.backward&&0==n)},rt.toHtml=function(){var e=[];return this.eachRange(function(t){e.push(b.toHtml(t))}),e.join("")},H.implementsTextRange&&(rt.getNativeTextRange=function(){var n;if(n=this.docSelection){var r=n.createRange();if(h(r))return r;throw t.createError("getNativeTextRange: selection is a control selection")}if(this.rangeCount>0)return e.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));throw t.createError("getNativeTextRange: selection contains no range")}),rt.getName=function(){return"WrappedSelection"},rt.inspect=function(){return y(this)},rt.detach=function(){C(this.win,"delete"),v(this)},R.detachAll=function(){C(null,"deleteAll")},R.inspect=y,R.isDirectionBackward=n,e.Selection=R,e.selectionPrototype=rt,e.addShimListener(function(e){"undefined"==typeof e.getSelection&&(e.getSelection=function(){return nt(e)}),e=null})});var L=!1,W=function(){L||(L=!0,!I.initialized&&I.config.autoInitialize&&u())};return b&&("complete"==document.readyState?W():(e(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",W,!1),H(window,"load",W))),I},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js
new file mode 100644
index 000000000..60064a334
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js
@@ -0,0 +1,32 @@
+/*
+ CSS Class Applier module for Rangy.
+ Adds, removes and toggles CSS classes on Ranges and Selections
+
+ Part of Rangy, a cross-browser JavaScript range and selection library
+ http://code.google.com/p/rangy/
+
+ Depends on Rangy core.
+
+ Copyright 2012, Tim Down
+ Licensed under the MIT license.
+ Version: 1.2.3
+ Build date: 26 February 2012
+*/
+rangy.createModule("CssClassApplier",function(i,v){function r(a,b){return a.className&&RegExp("(?:^|\\s)"+b+"(?:\\s|$)").test(a.className)}function s(a,b){if(a.className)r(a,b)||(a.className+=" "+b);else a.className=b}function o(a){return a.split(/\s+/).sort().join(" ")}function w(a,b){return o(a.className)==o(b.className)}function x(a){for(var b=a.parentNode;a.hasChildNodes();)b.insertBefore(a.firstChild,a);b.removeChild(a)}function y(a,b){var c=a.cloneRange();c.selectNodeContents(b);var d=c.intersection(a);
+d=d?d.toString():"";c.detach();return d!=""}function z(a){return a.getNodes([3],function(b){return y(a,b)})}function A(a,b){if(a.attributes.length!=b.attributes.length)return false;for(var c=0,d=a.attributes.length,e,f;c<d;++c){e=a.attributes[c];f=e.name;if(f!="class"){f=b.attributes.getNamedItem(f);if(e.specified!=f.specified)return false;if(e.specified&&e.nodeValue!==f.nodeValue)return false}}return true}function B(a,b){for(var c=0,d=a.attributes.length,e;c<d;++c){e=a.attributes[c].name;if(!(b&&
+h.arrayContains(b,e))&&a.attributes[c].specified&&e!="class")return true}return false}function C(a){var b;return a&&a.nodeType==1&&((b=a.parentNode)&&b.nodeType==9&&b.designMode=="on"||k(a)&&!k(a.parentNode))}function D(a){return(k(a)||a.nodeType!=1&&k(a.parentNode))&&!C(a)}function E(a){return a&&a.nodeType==1&&!M.test(p(a,"display"))}function N(a){if(a.data.length==0)return true;if(O.test(a.data))return false;switch(p(a.parentNode,"whiteSpace")){case "pre":case "pre-wrap":case "-moz-pre-wrap":return false;
+case "pre-line":if(/[\r\n]/.test(a.data))return false}return E(a.previousSibling)||E(a.nextSibling)}function m(a,b,c,d){var e,f=c==0;if(h.isAncestorOf(b,a))return a;if(h.isCharacterDataNode(b))if(c==0){c=h.getNodeIndex(b);b=b.parentNode}else if(c==b.length){c=h.getNodeIndex(b)+1;b=b.parentNode}else throw v.createError("splitNodeAt should not be called with offset in the middle of a data node ("+c+" in "+b.data);var g;g=b;var j=c;g=h.isCharacterDataNode(g)?j==0?!!g.previousSibling:j==g.length?!!g.nextSibling:
+true:j>0&&j<g.childNodes.length;if(g){if(!e){e=b.cloneNode(false);for(e.id&&e.removeAttribute("id");f=b.childNodes[c];)e.appendChild(f);h.insertAfter(e,b)}return b==a?e:m(a,e.parentNode,h.getNodeIndex(e),d)}else if(a!=b){e=b.parentNode;b=h.getNodeIndex(b);f||b++;return m(a,e,b,d)}return a}function F(a){var b=a?"nextSibling":"previousSibling";return function(c,d){var e=c.parentNode,f=c[b];if(f){if(f&&f.nodeType==3)return f}else if(d)if((f=e[b])&&f.nodeType==1&&e.tagName==f.tagName&&w(e,f)&&A(e,f))return f[a?
+"firstChild":"lastChild"];return null}}function t(a){this.firstTextNode=(this.isElementMerge=a.nodeType==1)?a.lastChild:a;this.textNodes=[this.firstTextNode]}function q(a,b,c){this.cssClass=a;var d,e,f=null;if(typeof b=="object"&&b!==null){c=b.tagNames;f=b.elementProperties;for(d=0;e=P[d++];)if(b.hasOwnProperty(e))this[e]=b[e];d=b.normalize}else d=b;this.normalize=typeof d=="undefined"?true:d;this.attrExceptions=[];d=document.createElement(this.elementTagName);this.elementProperties={};for(var g in f)if(f.hasOwnProperty(g)){if(G.hasOwnProperty(g))g=
+G[g];d[g]=f[g];this.elementProperties[g]=d[g];this.attrExceptions.push(g)}this.elementSortedClassName=this.elementProperties.hasOwnProperty("className")?o(this.elementProperties.className+" "+a):a;this.applyToAnyTagName=false;a=typeof c;if(a=="string")if(c=="*")this.applyToAnyTagName=true;else this.tagNames=c.toLowerCase().replace(/^\s\s*/,"").replace(/\s\s*$/,"").split(/\s*,\s*/);else if(a=="object"&&typeof c.length=="number"){this.tagNames=[];d=0;for(a=c.length;d<a;++d)if(c[d]=="*")this.applyToAnyTagName=
+true;else this.tagNames.push(c[d].toLowerCase())}else this.tagNames=[this.elementTagName]}i.requireModules(["WrappedSelection","WrappedRange"]);var h=i.dom,H=function(){function a(b,c,d){return c&&d?" ":""}return function(b,c){if(b.className)b.className=b.className.replace(RegExp("(?:^|\\s)"+c+"(?:\\s|$)"),a)}}(),p;if(typeof window.getComputedStyle!="undefined")p=function(a,b){return h.getWindow(a).getComputedStyle(a,null)[b]};else if(typeof document.documentElement.currentStyle!="undefined")p=function(a,
+b){return a.currentStyle[b]};else v.fail("No means of obtaining computed style properties found");var k;(function(){k=typeof document.createElement("div").isContentEditable=="boolean"?function(a){return a&&a.nodeType==1&&a.isContentEditable}:function(a){if(!a||a.nodeType!=1||a.contentEditable=="false")return false;return a.contentEditable=="true"||k(a.parentNode)}})();var M=/^inline(-block|-table)?$/i,O=/[^\r\n\t\f \u200B]/,Q=F(false),R=F(true);t.prototype={doMerge:function(){for(var a=[],b,c,d=0,
+e=this.textNodes.length;d<e;++d){b=this.textNodes[d];c=b.parentNode;a[d]=b.data;if(d){c.removeChild(b);c.hasChildNodes()||c.parentNode.removeChild(c)}}return this.firstTextNode.data=a=a.join("")},getLength:function(){for(var a=this.textNodes.length,b=0;a--;)b+=this.textNodes[a].length;return b},toString:function(){for(var a=[],b=0,c=this.textNodes.length;b<c;++b)a[b]="'"+this.textNodes[b].data+"'";return"[Merge("+a.join(",")+")]"}};var P=["elementTagName","ignoreWhiteSpace","applyToEditableOnly"],
+G={"class":"className"};q.prototype={elementTagName:"span",elementProperties:{},ignoreWhiteSpace:true,applyToEditableOnly:false,hasClass:function(a){return a.nodeType==1&&h.arrayContains(this.tagNames,a.tagName.toLowerCase())&&r(a,this.cssClass)},getSelfOrAncestorWithClass:function(a){for(;a;){if(this.hasClass(a,this.cssClass))return a;a=a.parentNode}return null},isModifiable:function(a){return!this.applyToEditableOnly||D(a)},isIgnorableWhiteSpaceNode:function(a){return this.ignoreWhiteSpace&&a&&
+a.nodeType==3&&N(a)},postApply:function(a,b,c){for(var d=a[0],e=a[a.length-1],f=[],g,j=d,I=e,J=0,K=e.length,n,L,l=0,u=a.length;l<u;++l){n=a[l];if(L=Q(n,!c)){if(!g){g=new t(L);f.push(g)}g.textNodes.push(n);if(n===d){j=g.firstTextNode;J=j.length}if(n===e){I=g.firstTextNode;K=g.getLength()}}else g=null}if(a=R(e,!c)){if(!g){g=new t(e);f.push(g)}g.textNodes.push(a)}if(f.length){l=0;for(u=f.length;l<u;++l)f[l].doMerge();b.setStart(j,J);b.setEnd(I,K)}},createContainer:function(a){a=a.createElement(this.elementTagName);
+i.util.extend(a,this.elementProperties);s(a,this.cssClass);return a},applyToTextNode:function(a){var b=a.parentNode;if(b.childNodes.length==1&&h.arrayContains(this.tagNames,b.tagName.toLowerCase()))s(b,this.cssClass);else{b=this.createContainer(h.getDocument(a));a.parentNode.insertBefore(b,a);b.appendChild(a)}},isRemovable:function(a){var b;if(b=a.tagName.toLowerCase()==this.elementTagName){if(b=o(a.className)==this.elementSortedClassName){var c;a:{b=this.elementProperties;for(c in b)if(b.hasOwnProperty(c)&&
+a[c]!==b[c]){c=false;break a}c=true}b=c&&!B(a,this.attrExceptions)&&this.isModifiable(a)}b=b}return b},undoToTextNode:function(a,b,c){if(!b.containsNode(c)){a=b.cloneRange();a.selectNode(c);if(a.isPointInRange(b.endContainer,b.endOffset)){m(c,b.endContainer,b.endOffset,[b]);b.setEndAfter(c)}if(a.isPointInRange(b.startContainer,b.startOffset))c=m(c,b.startContainer,b.startOffset,[b])}this.isRemovable(c)?x(c):H(c,this.cssClass)},applyToRange:function(a){a.splitBoundaries();var b=z(a);if(b.length){for(var c,
+d=0,e=b.length;d<e;++d){c=b[d];!this.isIgnorableWhiteSpaceNode(c)&&!this.getSelfOrAncestorWithClass(c)&&this.isModifiable(c)&&this.applyToTextNode(c)}a.setStart(b[0],0);c=b[b.length-1];a.setEnd(c,c.length);this.normalize&&this.postApply(b,a,false)}},applyToSelection:function(a){a=a||window;a=i.getSelection(a);var b,c=a.getAllRanges();a.removeAllRanges();for(var d=c.length;d--;){b=c[d];this.applyToRange(b);a.addRange(b)}},undoToRange:function(a){a.splitBoundaries();var b=z(a),c,d,e=b[b.length-1];if(b.length){for(var f=
+0,g=b.length;f<g;++f){c=b[f];(d=this.getSelfOrAncestorWithClass(c))&&this.isModifiable(c)&&this.undoToTextNode(c,a,d);a.setStart(b[0],0);a.setEnd(e,e.length)}this.normalize&&this.postApply(b,a,true)}},undoToSelection:function(a){a=a||window;a=i.getSelection(a);var b=a.getAllRanges(),c;a.removeAllRanges();for(var d=0,e=b.length;d<e;++d){c=b[d];this.undoToRange(c);a.addRange(c)}},getTextSelectedByRange:function(a,b){var c=b.cloneRange();c.selectNodeContents(a);var d=c.intersection(b);d=d?d.toString():
+"";c.detach();return d},isAppliedToRange:function(a){if(a.collapsed)return!!this.getSelfOrAncestorWithClass(a.commonAncestorContainer);else{for(var b=a.getNodes([3]),c=0,d;d=b[c++];)if(!this.isIgnorableWhiteSpaceNode(d)&&y(a,d)&&this.isModifiable(d)&&!this.getSelfOrAncestorWithClass(d))return false;return true}},isAppliedToSelection:function(a){a=a||window;a=i.getSelection(a).getAllRanges();for(var b=a.length;b--;)if(!this.isAppliedToRange(a[b]))return false;return true},toggleRange:function(a){this.isAppliedToRange(a)?
+this.undoToRange(a):this.applyToRange(a)},toggleSelection:function(a){this.isAppliedToSelection(a)?this.undoToSelection(a):this.applyToSelection(a)},detach:function(){}};q.util={hasClass:r,addClass:s,removeClass:H,hasSameClasses:w,replaceWithOwnChildren:x,elementsHaveSameNonClassAttributes:A,elementHasNonClassAttributes:B,splitNodeAt:m,isEditableElement:k,isEditingHost:C,isEditable:D};i.CssClassApplier=q;i.createCssClassApplier=function(a,b,c){return new q(a,b,c)}}); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js
new file mode 100755
index 000000000..31fd4f3dc
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js
@@ -0,0 +1,12 @@
+/**
+ * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core, ClassApplier and optionally TextRange modules.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("Highlighter",["ClassApplier"],function(e){function t(e,t){return e.characterRange.start-t.characterRange.start}function n(e,t){return t?e.getElementById(t):l(e)}function r(e,t){this.type=e,this.converterCreator=t}function i(e,t){f[e]=new r(e,t)}function a(e){var t=f[e];if(t instanceof r)return t.create();throw new Error("Highlighter type '"+e+"' is not valid")}function s(e,t){this.start=e,this.end=t}function h(e,t,n,r,i,a){i?(this.id=i,d=Math.max(d,i+1)):this.id=d++,this.characterRange=t,this.doc=e,this.classApplier=n,this.converter=r,this.containerElementId=a||null,this.applied=!1}function o(e,t){t=t||"textContent",this.doc=e||document,this.classAppliers={},this.highlights=[],this.converter=a(t)}var c=e.dom,g=c.arrayContains,l=c.getBody,u=e.util.createOptions,p=e.util.forEach,d=1,f={};r.prototype.create=function(){var e=this.converterCreator();return e.type=this.type,e},e.registerHighlighterType=i,s.prototype={intersects:function(e){return this.start<e.end&&this.end>e.start},isContiguousWith:function(e){return this.start==e.end||this.end==e.start},union:function(e){return new s(Math.min(this.start,e.start),Math.max(this.end,e.end))},intersection:function(e){return new s(Math.max(this.start,e.start),Math.min(this.end,e.end))},getComplements:function(e){var t=[];if(this.start>=e.start){if(this.end<=e.end)return[];t.push(new s(e.end,this.end))}else t.push(new s(this.start,Math.min(this.end,e.start))),this.end>e.end&&t.push(new s(e.end,this.end));return t},toString:function(){return"[CharacterRange("+this.start+", "+this.end+")]"}},s.fromCharacterRange=function(e){return new s(e.start,e.end)};var R={rangeToCharacterRange:function(e,t){var n=e.getBookmark(t);return new s(n.start,n.end)},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.moveToBookmark({start:n.start,end:n.end,containerNode:r}),i},serializeSelection:function(e,t){for(var n=e.getAllRanges(),r=n.length,i=[],a=1==r&&e.isBackward(),s=0,h=n.length;h>s;++s)i[s]={characterRange:this.rangeToCharacterRange(n[s],t),backward:a};return i},restoreSelection:function(e,t,n){e.removeAllRanges();for(var r,i,a,s=e.win.document,h=0,o=t.length;o>h;++h)i=t[h],a=i.characterRange,r=this.characterRangeToRange(s,i.characterRange,n),e.addRange(r,i.backward)}};i("textContent",function(){return R}),i("TextRange",function(){var t;return function(){if(!t){var n=e.modules.TextRange;if(!n)throw new Error("TextRange module is missing.");if(!n.supported)throw new Error("TextRange module is present but not supported.");t={rangeToCharacterRange:function(e,t){return s.fromCharacterRange(e.toCharacterRange(t))},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.selectCharacters(r,n.start,n.end),i},serializeSelection:function(e,t){return e.saveCharacterRanges(t)},restoreSelection:function(e,t,n){e.restoreCharacterRanges(n,t)}}}return t}}()),h.prototype={getContainerElement:function(){return n(this.doc,this.containerElementId)},getRange:function(){return this.converter.characterRangeToRange(this.doc,this.characterRange,this.getContainerElement())},fromRange:function(e){this.characterRange=this.converter.rangeToCharacterRange(e,this.getContainerElement())},getText:function(){return this.getRange().toString()},containsElement:function(e){return this.getRange().containsNodeContents(e.firstChild)},unapply:function(){this.classApplier.undoToRange(this.getRange()),this.applied=!1},apply:function(){this.classApplier.applyToRange(this.getRange()),this.applied=!0},getHighlightElements:function(){return this.classApplier.getElementsWithClassIntersectingRange(this.getRange())},toString:function(){return"[Highlight(ID: "+this.id+", class: "+this.classApplier.className+", character range: "+this.characterRange.start+" - "+this.characterRange.end+")]"}},o.prototype={addClassApplier:function(e){this.classAppliers[e.className]=e},getHighlightForElement:function(e){for(var t=this.highlights,n=0,r=t.length;r>n;++n)if(t[n].containsElement(e))return t[n];return null},removeHighlights:function(e){for(var t,n=0,r=this.highlights.length;r>n;++n)t=this.highlights[n],g(e,t)&&(t.unapply(),this.highlights.splice(n--,1))},removeAllHighlights:function(){this.removeHighlights(this.highlights)},getIntersectingHighlights:function(e){var t=[],n=this.highlights;return p(e,function(e){p(n,function(n){e.intersectsRange(n.getRange())&&!g(t,n)&&t.push(n)})}),t},highlightCharacterRanges:function(t,n,r){var i,a,o,c=this.highlights,g=this.converter,l=this.doc,d=[],f=t?this.classAppliers[t]:null;r=u(r,{containerElementId:null,exclusive:!0});var R,v,m,C=r.containerElementId,w=r.exclusive;C&&(R=this.doc.getElementById(C),R&&(v=e.createRange(this.doc),v.selectNodeContents(R),m=new s(0,v.toString().length)));var y,E,T,x,A,H;for(i=0,a=n.length;a>i;++i)if(y=n[i],A=[],m&&(y=y.intersection(m)),y.start!=y.end){for(o=0;o<c.length;++o)T=!1,C==c[o].containerElementId&&(E=c[o].characterRange,x=f==c[o].classApplier,H=!x&&w,(E.intersects(y)||E.isContiguousWith(y))&&(x||H)&&(H&&p(E.getComplements(y),function(e){A.push(new h(l,e,c[o].classApplier,g,null,C))}),T=!0,x&&(y=E.union(y)))),T?(d.push(c[o]),c[o]=new h(l,E.union(y),f,g,null,C)):A.push(c[o]);f&&A.push(new h(l,y,f,g,null,C)),this.highlights=c=A}p(d,function(e){e.unapply()});var I=[];return p(c,function(e){e.applied||(e.apply(),I.push(e))}),I},highlightRanges:function(t,n,r){var i=[],a=this.converter;r=u(r,{containerElement:null,exclusive:!0});var s,h=r.containerElement,o=h?h.id:null;return h&&(s=e.createRange(h),s.selectNodeContents(h)),p(n,function(e){var t=h?s.intersection(e):e;i.push(a.rangeToCharacterRange(t,h||l(e.getDocument())))}),this.highlightCharacterRanges(t,i,{containerElementId:o,exclusive:r.exclusive})},highlightSelection:function(t,r){var i=this.converter,a=t?this.classAppliers[t]:!1;r=u(r,{containerElementId:null,selection:e.getSelection(this.doc),exclusive:!0});var h=r.containerElementId,o=r.exclusive,c=r.selection,g=c.win.document,l=n(g,h);if(!a&&t!==!1)throw new Error("No class applier found for class '"+t+"'");var d=i.serializeSelection(c,l),f=[];p(d,function(e){f.push(s.fromCharacterRange(e.characterRange))});var R=this.highlightCharacterRanges(t,f,{containerElementId:h,exclusive:o});return i.restoreSelection(c,d,l),R},unhighlightSelection:function(t){t=t||e.getSelection(this.doc);var n=this.getIntersectingHighlights(t.getAllRanges());return this.removeHighlights(n),t.removeAllRanges(),n},getHighlightsInSelection:function(t){return t=t||e.getSelection(this.doc),this.getIntersectingHighlights(t.getAllRanges())},selectionOverlapsHighlight:function(e){return this.getHighlightsInSelection(e).length>0},serialize:function(e){var n,r,i,s,h=this,o=h.highlights;return o.sort(t),e=u(e,{serializeHighlightText:!1,type:h.converter.type}),n=e.type,i=n!=h.converter.type,i&&(s=a(n)),r=["type:"+n],p(o,function(t){var n,a=t.characterRange;i&&(n=t.getContainerElement(),a=s.rangeToCharacterRange(h.converter.characterRangeToRange(h.doc,a,n),n));var o=[a.start,a.end,t.id,t.classApplier.className,t.containerElementId];e.serializeHighlightText&&o.push(t.getText()),r.push(o.join("$"))}),r.join("|")},deserialize:function(e){var t,r,i,o=e.split("|"),c=[],g=o[0],l=!1;if(!g||!(t=/^type:(\w+)$/.exec(g)))throw new Error("Serialized highlights are invalid.");r=t[1],r!=this.converter.type&&(i=a(r),l=!0),o.shift();for(var u,p,d,f,R,v,m=o.length;m-->0;){if(v=o[m].split("$"),d=new s(+v[0],+v[1]),f=v[4]||null,l&&(R=n(this.doc,f),d=this.converter.rangeToCharacterRange(i.characterRangeToRange(this.doc,d,R),R)),u=this.classAppliers[v[3]],!u)throw new Error("No class applier found for class '"+v[3]+"'");p=new h(this.doc,d,u,this.converter,parseInt(v[2]),f),p.apply(),c.push(p)}this.highlights=c}},e.Highlighter=o,e.createHighlighter=function(e,t){return new o(e,t)}}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js
new file mode 100755
index 000000000..4029e43d1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js
@@ -0,0 +1,15 @@
+/**
+ * Selection save and restore module for Rangy.
+ * Saves and restores user selections using marker invisible elements in the DOM.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){return e.createModule("SaveRestore",["WrappedRange"],function(e,n){function r(e,n){return(n||document).getElementById(e)}function t(e,n){var r,t="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),a=m.getDocument(e.startContainer),o=e.cloneRange();return o.collapse(n),r=a.createElement("span"),r.id=t,r.style.lineHeight="0",r.style.display="none",r.className="rangySelectionBoundary",r.appendChild(a.createTextNode(k)),o.insertNode(r),r}function a(e,t,a,o){var s=r(a,e);s?(t[o?"setStartBefore":"setEndBefore"](s),p(s)):n.warn("Marker element has been removed. Cannot restore selection.")}function o(e,n){return n.compareBoundaryPoints(e.START_TO_START,e)}function s(n,r){var a,o,s=e.DomRange.getRangeDocument(n),i=n.toString(),d=v(r);return n.collapsed?(o=t(n,!1),{document:s,markerId:o.id,collapsed:!0}):(o=t(n,!1),a=t(n,!0),{document:s,startMarkerId:a.id,endMarkerId:o.id,collapsed:!1,backward:d,toString:function(){return"original text: '"+i+"', new text: '"+n.toString()+"'"}})}function i(t,o){var s=t.document;"undefined"==typeof o&&(o=!0);var i=e.createRange(s);if(t.collapsed){var d=r(t.markerId,s);if(d){d.style.display="inline";var l=d.previousSibling;l&&3==l.nodeType?(p(d),i.collapseToPoint(l,l.length)):(i.collapseBefore(d),p(d))}else n.warn("Marker element has been removed. Cannot restore selection.")}else a(s,i,t.startMarkerId,!0),a(s,i,t.endMarkerId,!1);return o&&i.normalizeBoundaries(),i}function d(n,t){var a,i,d=[],l=v(t);n=n.slice(0),n.sort(o);for(var c=0,u=n.length;u>c;++c)d[c]=s(n[c],l);for(c=u-1;c>=0;--c)a=n[c],i=e.DomRange.getRangeDocument(a),a.collapsed?a.collapseAfter(r(d[c].markerId,i)):(a.setEndBefore(r(d[c].endMarkerId,i)),a.setStartAfter(r(d[c].startMarkerId,i)));return d}function l(r){if(!e.isSelectionValid(r))return n.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."),null;var t=e.getSelection(r),a=t.getAllRanges(),o=1==a.length&&t.isBackward(),s=d(a,o);return o?t.setSingleRange(a[0],o):t.setRanges(a),{win:r,rangeInfos:s,restored:!1}}function c(e){for(var n=[],r=e.length,t=r-1;t>=0;t--)n[t]=i(e[t],!0);return n}function u(n,r){if(!n.restored){var t=n.rangeInfos,a=e.getSelection(n.win),o=c(t),s=t.length;1==s&&r&&e.features.selectionHasExtend&&t[0].backward?(a.removeAllRanges(),a.addRange(o[0],!0)):a.setRanges(o),n.restored=!0}}function f(e,n){var t=r(n,e);t&&p(t)}function g(e){for(var n,r=e.rangeInfos,t=0,a=r.length;a>t;++t)n=r[t],n.collapsed?f(e.doc,n.markerId):(f(e.doc,n.startMarkerId),f(e.doc,n.endMarkerId))}var m=e.dom,p=m.removeNode,v=e.Selection.isDirectionBackward,k="";e.util.extend(e,{saveRange:s,restoreRange:i,saveRanges:d,restoreRanges:c,saveSelection:l,restoreSelection:u,removeMarkerElement:f,removeMarkers:g})}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js
new file mode 100755
index 000000000..68780587d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js
@@ -0,0 +1,16 @@
+/**
+ * Serializer module for Rangy.
+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
+ * cookie or local storage and restore it on the user's next visit to the same page.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){return e.createModule("Serializer",["WrappedSelection"],function(e,n){function t(e){return e.replace(/</g,"&lt;").replace(/>/g,"&gt;")}function o(e,n){n=n||[];var r=e.nodeType,i=e.childNodes,c=i.length,a=[r,e.nodeName,c].join(":"),d="",u="";switch(r){case 3:d=t(e.nodeValue);break;case 8:d="<!--"+t(e.nodeValue)+"-->";break;default:d="<"+a+">",u="</>"}d&&n.push(d);for(var s=0;c>s;++s)o(i[s],n);return u&&n.push(u),n}function r(e){var n=o(e).join("");return w(n).toString(16)}function i(e,n,t){var o=[],r=e;for(t=t||R.getDocument(e).documentElement;r&&r!=t;)o.push(R.getNodeIndex(r,!0)),r=r.parentNode;return o.join("/")+":"+n}function c(e,t,o){t||(t=(o||document).documentElement);for(var r,i=e.split(":"),c=t,a=i[0]?i[0].split("/"):[],d=a.length;d--;){if(r=parseInt(a[d],10),!(r<c.childNodes.length))throw n.createError("deserializePosition() failed: node "+R.inspectNode(c)+" has no child with index "+r+", "+d);c=c.childNodes[r]}return new R.DomPosition(c,parseInt(i[1],10))}function a(t,o,c){if(c=c||e.DomRange.getRangeDocument(t).documentElement,!R.isOrIsAncestorOf(c,t.commonAncestorContainer))throw n.createError("serializeRange(): range "+t.inspect()+" is not wholly contained within specified root node "+R.inspectNode(c));var a=i(t.startContainer,t.startOffset,c)+","+i(t.endContainer,t.endOffset,c);return o||(a+="{"+r(c)+"}"),a}function d(t,o,i){o?i=i||R.getDocument(o):(i=i||document,o=i.documentElement);var a=S.exec(t),d=a[4];if(d){var u=r(o);if(d!==u)throw n.createError("deserializeRange(): checksums of serialized range root node ("+d+") and target root node ("+u+") do not match")}var s=c(a[1],o,i),l=c(a[2],o,i),f=e.createRange(i);return f.setStartAndEnd(s.node,s.offset,l.node,l.offset),f}function u(e,n,t){n||(n=(t||document).documentElement);var o=S.exec(e),i=o[3];return!i||i===r(n)}function s(n,t,o){n=e.getSelection(n);for(var r=n.getAllRanges(),i=[],c=0,d=r.length;d>c;++c)i[c]=a(r[c],t,o);return i.join("|")}function l(n,t,o){t?o=o||R.getWindow(t):(o=o||window,t=o.document.documentElement);for(var r=n.split("|"),i=e.getSelection(o),c=[],a=0,u=r.length;u>a;++a)c[a]=d(r[a],t,o.document);return i.setRanges(c),i}function f(e,n,t){var o;n?o=t?t.document:R.getDocument(n):(t=t||window,n=t.document.documentElement);for(var r=e.split("|"),i=0,c=r.length;c>i;++i)if(!u(r[i],n,o))return!1;return!0}function m(e){for(var n,t,o=e.split(/[;,]/),r=0,i=o.length;i>r;++r)if(n=o[r].split("="),n[0].replace(/^\s+/,"")==C&&(t=n[1]))return decodeURIComponent(t.replace(/\s+$/,""));return null}function p(e){e=e||window;var n=m(e.document.cookie);n&&l(n,e.doc)}function g(n,t){n=n||window,t="object"==typeof t?t:{};var o=t.expires?";expires="+t.expires.toUTCString():"",r=t.path?";path="+t.path:"",i=t.domain?";domain="+t.domain:"",c=t.secure?";secure":"",a=s(e.getSelection(n));n.document.cookie=encodeURIComponent(C)+"="+encodeURIComponent(a)+o+r+i+c}var h="undefined",v=e.util;(typeof encodeURIComponent==h||typeof decodeURIComponent==h)&&n.fail("encodeURIComponent and/or decodeURIComponent method is missing");var w=function(){function e(e){for(var n,t=[],o=0,r=e.length;r>o;++o)n=e.charCodeAt(o),128>n?t.push(n):2048>n?t.push(n>>6|192,63&n|128):t.push(n>>12|224,n>>6&63|128,63&n|128);return t}function n(){for(var e,n,t=[],o=0;256>o;++o){for(n=o,e=8;e--;)1==(1&n)?n=n>>>1^3988292384:n>>>=1;t[o]=n>>>0}return t}function t(){return o||(o=n()),o}var o=null;return function(n){for(var o,r=e(n),i=-1,c=t(),a=0,d=r.length;d>a;++a)o=255&(i^r[a]),i=i>>>8^c[o];return(-1^i)>>>0}}(),R=e.dom,S=/^([^,]+),([^,\{]+)(\{([^}]+)\})?$/,C="rangySerializedSelection";v.extend(e,{serializePosition:i,deserializePosition:c,serializeRange:a,deserializeRange:d,canDeserializeRange:u,serializeSelection:s,deserializeSelection:l,canDeserializeSelection:f,restoreSelectionFromCookie:p,saveSelectionCookie:g,getElementChecksum:r,nodeToInfoString:o}),v.crc32=w}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js
new file mode 100755
index 000000000..c79811ad4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js
@@ -0,0 +1,32 @@
+/**
+ * Text range module for Rangy.
+ * Text-based manipulation and searching of ranges and selections.
+ *
+ * Features
+ *
+ * - Ability to move range boundaries by character or word offsets
+ * - Customizable word tokenizer
+ * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
+ * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
+ * sensitivity
+ * - Selection and range save/restore as text offsets within a node
+ * - Methods to return visible text within a range or selection
+ * - innerText method for elements
+ *
+ * References
+ *
+ * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
+ * http://aryeh.name/spec/innertext/innertext.html
+ * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("TextRange",["WrappedSelection"],function(e,t){function n(e,t){function n(e,t,n){s.push({start:e,end:t,isWord:n})}for(var r,i,o,a=e.join(""),s=[],c=0;r=t.wordRegex.exec(a);){if(i=r.index,o=i+r[0].length,i>c&&n(c,i,!1),t.includeTrailingSpace)for(;X.test(e[o]);)++o;n(i,o,!0),c=o}return c<e.length&&n(c,e.length,!1),s}function r(e,t){for(var n=e.slice(t.start,t.end),r={isWord:t.isWord,chars:n,toString:function(){return n.join("")}},i=0,o=n.length;o>i;++i)n[i].token=r;return r}function i(e,t,n){for(var i,o=n(e,t),a=[],s=0;i=o[s++];)a.push(r(e,i));return a}function o(e){var t=e||"",n="string"==typeof t?t.split(""):t;return n.sort(function(e,t){return e.charCodeAt(0)-t.charCodeAt(0)}),n.join("").replace(/(.)\1+/g,"$1")}function a(e){var t,n;return e?(t=e.language||Z,n={},K(n,ct[t]||ct[Z]),K(n,e),n):ct[Z]}function s(e,t){var n=z(e,t);return t.hasOwnProperty("wordOptions")&&(n.wordOptions=a(n.wordOptions)),t.hasOwnProperty("characterOptions")&&(n.characterOptions=z(n.characterOptions,at)),n}function c(e,t){var n=pt(e,"display",t),r=e.tagName.toLowerCase();return"block"==n&&ot&&ft.hasOwnProperty(r)?ft[r]:n}function u(e){for(var t=f(e),n=0,r=t.length;r>n;++n)if(1==t[n].nodeType&&"none"==c(t[n]))return!0;return!1}function d(e){var t;return 3==e.nodeType&&(t=e.parentNode)&&"hidden"==pt(t,"visibility")}function l(e){return e&&(1==e.nodeType&&!/^(inline(-block|-table)?|none)$/.test(c(e))||9==e.nodeType||11==e.nodeType)}function h(e){return U.isCharacterDataNode(e)||!/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(e.nodeName)}function p(e){for(var t=[];e.parentNode;)t.unshift(e.parentNode),e=e.parentNode;return t}function f(e){return p(e).concat([e])}function g(e){for(;e&&!e.nextSibling;)e=e.parentNode;return e?e.nextSibling:null}function v(e,t){return!t&&e.hasChildNodes()?e.firstChild:g(e)}function S(e){var t=e.previousSibling;if(t){for(e=t;e.hasChildNodes();)e=e.lastChild;return e}var n=e.parentNode;return n&&1==n.nodeType?n:null}function C(e){if(!e||3!=e.nodeType)return!1;var t=e.data;if(""===t)return!0;var n=e.parentNode;if(!n||1!=n.nodeType)return!1;var r=pt(e.parentNode,"whiteSpace");return/^[\t\n\r ]+$/.test(t)&&/^(normal|nowrap)$/.test(r)||/^[\t\r ]+$/.test(t)&&"pre-line"==r}function N(e){if(""===e.data)return!0;if(!C(e))return!1;var t=e.parentNode;return t?u(e)?!0:!1:!0}function y(e){var t=e.nodeType;return 7==t||8==t||u(e)||/^(script|style)$/i.test(e.nodeName)||d(e)||N(e)}function m(e,t){var n=e.nodeType;return 7==n||8==n||1==n&&"none"==c(e,t)}function x(){this.store={}}function T(e,t,n){return function(r){var i=this.cache;if(i.hasOwnProperty(e))return gt++,i[e];vt++;var o=t.call(this,n?this[n]:this,r);return i[e]=o,o}}function P(e,t){this.node=e,this.session=t,this.cache=new x,this.positions=new x}function b(e,t){this.offset=t,this.nodeWrapper=e,this.node=e.node,this.session=e.session,this.cache=new x}function w(){return"[Position("+U.inspectNode(this.node)+":"+this.offset+")]"}function R(){return B(),Bt=new Ot}function E(){return Bt||R()}function B(){Bt&&Bt.detach(),Bt=null}function O(e,n,r,i){function o(){var e=null;return n?(e=s,c||(s=s.previousVisible(),c=!s||r&&s.equals(r))):c||(e=s=s.nextVisible(),c=!s||r&&s.equals(r)),c&&(s=null),e}r&&(n?y(r.node)&&(r=e.previousVisible()):y(r.node)&&(r=r.nextVisible()));var a,s=e,c=!1,u=!1;return{next:function(){if(u)return u=!1,a;for(var e,t;e=o();)if(t=e.getCharacter(i))return a=e,e;return null},rewind:function(){if(!a)throw t.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");u=!0},dispose:function(){e=r=null}}}function k(e,t,n){function r(e){for(var t,n,r=[],i=e?o:a,s=!1,c=!1;t=i.next();){if(n=t.character,Q.test(n))c&&(c=!1,s=!0);else{if(s){i.rewind();break}c=!0}r.push(t)}return r}var o=O(e,!1,null,t),a=O(e,!0,null,t),s=n.tokenizer,c=r(!0),u=r(!1).reverse(),d=i(u.concat(c),n,s),l=c.length?d.slice(kt(d,c[0].token)):[],h=u.length?d.slice(0,kt(d,u.pop().token)+1):[];return{nextEndToken:function(){for(var e,t;1==l.length&&!(e=l[0]).isWord&&(t=r(!0)).length>0;)l=i(e.chars.concat(t),n,s);return l.shift()},previousStartToken:function(){for(var e,t;1==h.length&&!(e=h[0]).isWord&&(t=r(!1)).length>0;)h=i(t.reverse().concat(e.chars),n,s);return h.pop()},dispose:function(){o.dispose(),a.dispose(),l=h=null}}}function L(e,t,n,r,i){var o,a,s,c,u=0,d=e,l=Math.abs(n);if(0!==n){var h=0>n;switch(t){case M:for(a=O(e,h,null,r);(o=a.next())&&l>u;)++u,d=o;s=o,a.dispose();break;case j:for(var p=k(e,r,i),f=h?p.previousStartToken:p.nextEndToken;(c=f())&&l>u;)c.isWord&&(++u,d=h?c.chars[0]:c.chars[c.chars.length-1]);break;default:throw new Error("movePositionBy: unit '"+t+"' not implemented")}h?(d=d.previousVisible(),u=-u):d&&d.isLeadingSpace&&!d.isTrailingSpace&&(t==j&&(a=O(e,!1,null,r),s=a.next(),a.dispose()),s&&(d=s.previousVisible()))}return{position:d,unitsMoved:u}}function I(e,t,n,r){var i=e.getRangeBoundaryPosition(t,!0),o=e.getRangeBoundaryPosition(t,!1),a=r?o:i,s=r?i:o;return O(a,!!r,s,n)}function A(e,t,n){for(var r,i=[],o=I(e,t,n);r=o.next();)i.push(r);return o.dispose(),i}function W(t,n,r){var i=e.createRange(t.node);return i.setStartAndEnd(t.node,t.offset,n.node,n.offset),!i.expand("word",{wordOptions:r})}function _(e,t,n,r,i){function o(e,t){var n=g[e].previousVisible(),r=g[t-1],o=!i.wholeWordsOnly||W(n,r,i.wordOptions);return{startPos:n,endPos:r,valid:o}}for(var a,s,c,u,d,l,h=et(i.direction),p=O(e,h,e.session.getRangeBoundaryPosition(r,h),i.characterOptions),f="",g=[],v=null;a=p.next();)if(s=a.character,n||i.caseSensitive||(s=s.toLowerCase()),h?(g.unshift(a),f=s+f):(g.push(a),f+=s),n){if(d=t.exec(f))if(c=d.index,u=c+d[0].length,l){if(!h&&u<f.length||h&&c>0){v=o(c,u);break}}else l=!0}else if(-1!=(c=f.indexOf(t))){v=o(c,c+t.length);break}return l&&(v=o(c,u)),p.dispose(),v}function D(e){return function(){var t=!!Bt,n=E(),r=[n].concat(G.toArray(arguments)),i=e.apply(this,r);return t||B(),i}}function F(e,t){return D(function(n,r,i,o){typeof i==q&&(i=r,r=M),o=s(o,dt);var a=e;t&&(a=i>=0,this.collapse(!a));var c=L(n.getRangeBoundaryPosition(this,a),r,i,o.characterOptions,o.wordOptions),u=c.position;return this[a?"setStart":"setEnd"](u.node,u.offset),c.unitsMoved})}function V(e){return D(function(t,n){n=z(n,at);for(var r,i=I(t,this,n,!e),o=0;(r=i.next())&&Q.test(r.character);)++o;i.dispose();var a=o>0;return a&&this[e?"moveStart":"moveEnd"]("character",e?o:-o,{characterOptions:n}),a})}function $(e){return D(function(t,n){var r=!1;return this.changeEachRange(function(t){r=t[e](n)||r}),r})}var q="undefined",M="character",j="word",U=e.dom,G=e.util,K=G.extend,z=G.createOptions,H=U.getBody,Y=/^[ \t\f\r\n]+$/,J=/^[ \t\f\r]+$/,Q=/^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/,X=/^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/,Z="en",et=e.Selection.isDirectionBackward,tt=!1,nt=!1,rt=!1,it=!0;!function(){var t=U.createTestElement(document,"<p>1 </p><p></p>",!0),n=t.firstChild,r=e.getSelection();r.collapse(n.lastChild,2),r.setStart(n.firstChild,0),tt=1==(""+r).length,t.innerHTML="1 <br />",r.collapse(t,2),r.setStart(t.firstChild,0),nt=1==(""+r).length,t.innerHTML="1 <p>1</p>",r.collapse(t,2),r.setStart(t.firstChild,0),rt=1==(""+r).length,U.removeNode(t),r.removeAllRanges()}();var ot,at={includeBlockContentTrailingSpace:!0,includeSpaceBeforeBr:!0,includeSpaceBeforeBlock:!0,includePreLineTrailingSpace:!0,ignoreCharacters:""},st={includeBlockContentTrailingSpace:!it,includeSpaceBeforeBr:!nt,includeSpaceBeforeBlock:!rt,includePreLineTrailingSpace:!0},ct={en:{wordRegex:/[a-z0-9]+('[a-z0-9]+)*/gi,includeTrailingSpace:!1,tokenizer:n}},ut={caseSensitive:!1,withinRange:null,wholeWordsOnly:!1,wrap:!1,direction:"forward",wordOptions:null,characterOptions:null},dt={wordOptions:null,characterOptions:null},lt={wordOptions:null,characterOptions:null,trim:!1,trimStart:!0,trimEnd:!0},ht={wordOptions:null,characterOptions:null,direction:"forward"},pt=U.getComputedStyleProperty;!function(){var e=document.createElement("table"),t=H(document);t.appendChild(e),ot="block"==pt(e,"display"),t.removeChild(e)}();var ft={table:"table",caption:"table-caption",colgroup:"table-column-group",col:"table-column",thead:"table-header-group",tbody:"table-row-group",tfoot:"table-footer-group",tr:"table-row",td:"table-cell",th:"table-cell"};x.prototype={get:function(e){return this.store.hasOwnProperty(e)?this.store[e]:null},set:function(e,t){return this.store[e]=t}};var gt=0,vt=0,St={getPosition:function(e){var t=this.positions;return t.get(e)||t.set(e,new b(this,e))},toString:function(){return"[NodeWrapper("+U.inspectNode(this.node)+")]"}};P.prototype=St;var Ct="EMPTY",Nt="NON_SPACE",yt="UNCOLLAPSIBLE_SPACE",mt="COLLAPSIBLE_SPACE",xt="TRAILING_SPACE_BEFORE_BLOCK",Tt="TRAILING_SPACE_IN_BLOCK",Pt="TRAILING_SPACE_BEFORE_BR",bt="PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",wt="TRAILING_LINE_BREAK_AFTER_BR",Rt="INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";K(St,{isCharacterDataNode:T("isCharacterDataNode",U.isCharacterDataNode,"node"),getNodeIndex:T("nodeIndex",U.getNodeIndex,"node"),getLength:T("nodeLength",U.getNodeLength,"node"),containsPositions:T("containsPositions",h,"node"),isWhitespace:T("isWhitespace",C,"node"),isCollapsedWhitespace:T("isCollapsedWhitespace",N,"node"),getComputedDisplay:T("computedDisplay",c,"node"),isCollapsed:T("collapsed",y,"node"),isIgnored:T("ignored",m,"node"),next:T("nextPos",v,"node"),previous:T("previous",S,"node"),getTextNodeInfo:T("textNodeInfo",function(e){var t=null,n=!1,r=pt(e.parentNode,"whiteSpace"),i="pre-line"==r;return i?(t=J,n=!0):("normal"==r||"nowrap"==r)&&(t=Y,n=!0),{node:e,text:e.data,spaceRegex:t,collapseSpaces:n,preLine:i}},"node"),hasInnerText:T("hasInnerText",function(e,t){for(var n=this.session,r=n.getPosition(e.parentNode,this.getNodeIndex()+1),i=n.getPosition(e,0),o=t?r:i,a=t?i:r;o!==a;){if(o.prepopulateChar(),o.isDefinitelyNonEmpty())return!0;o=t?o.previousVisible():o.nextVisible()}return!1},"node"),isRenderedBlock:T("isRenderedBlock",function(e){for(var t=e.getElementsByTagName("br"),n=0,r=t.length;r>n;++n)if(!y(t[n]))return!0;return this.hasInnerText()},"node"),getTrailingSpace:T("trailingSpace",function(e){if("br"==e.tagName.toLowerCase())return"";switch(this.getComputedDisplay()){case"inline":for(var t=e.lastChild;t;){if(!m(t))return 1==t.nodeType?this.session.getNodeWrapper(t).getTrailingSpace():"";t=t.previousSibling}break;case"inline-block":case"inline-table":case"none":case"table-column":case"table-column-group":break;case"table-cell":return" ";default:return this.isRenderedBlock(!0)?"\n":""}return""},"node"),getLeadingSpace:T("leadingSpace",function(){switch(this.getComputedDisplay()){case"inline":case"inline-block":case"inline-table":case"none":case"table-column":case"table-column-group":case"table-cell":break;default:return this.isRenderedBlock(!1)?"\n":""}return""},"node")});var Et={character:"",characterType:Ct,isBr:!1,prepopulateChar:function(){var e=this;if(!e.prepopulatedChar){var t=e.node,n=e.offset,r="",i=Ct,o=!1;if(n>0)if(3==t.nodeType){var a=t.data,s=a.charAt(n-1),c=e.nodeWrapper.getTextNodeInfo(),u=c.spaceRegex;c.collapseSpaces?u.test(s)?n>1&&u.test(a.charAt(n-2))||(c.preLine&&"\n"===a.charAt(n)?(r=" ",i=bt):(r=" ",i=mt)):(r=s,i=Nt,o=!0):(r=s,i=yt,o=!0)}else{var d=t.childNodes[n-1];if(d&&1==d.nodeType&&!y(d)&&("br"==d.tagName.toLowerCase()?(r="\n",e.isBr=!0,i=mt,o=!1):e.checkForTrailingSpace=!0),!r){var l=t.childNodes[n];l&&1==l.nodeType&&!y(l)&&(e.checkForLeadingSpace=!0)}}e.prepopulatedChar=!0,e.character=r,e.characterType=i,e.isCharInvariant=o}},isDefinitelyNonEmpty:function(){var e=this.characterType;return e==Nt||e==yt},resolveLeadingAndTrailingSpaces:function(){if(this.prepopulatedChar||this.prepopulateChar(),this.checkForTrailingSpace){var e=this.session.getNodeWrapper(this.node.childNodes[this.offset-1]).getTrailingSpace();e&&(this.isTrailingSpace=!0,this.character=e,this.characterType=mt),this.checkForTrailingSpace=!1}if(this.checkForLeadingSpace){var t=this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();t&&(this.isLeadingSpace=!0,this.character=t,this.characterType=mt),this.checkForLeadingSpace=!1}},getPrecedingUncollapsedPosition:function(e){for(var t,n=this;n=n.previousVisible();)if(t=n.getCharacter(e),""!==t)return n;return null},getCharacter:function(e){function t(){return p||(d=f.getPrecedingUncollapsedPosition(e),p=!0),d}this.resolveLeadingAndTrailingSpaces();var n,r=this.character,i=o(e.ignoreCharacters),a=""!==r&&i.indexOf(r)>-1;if(this.isCharInvariant)return n=a?"":r;var s=["character",e.includeSpaceBeforeBr,e.includeBlockContentTrailingSpace,e.includePreLineTrailingSpace,i].join("_"),c=this.cache.get(s);if(null!==c)return c;var u,d,l="",h=this.characterType==mt,p=!1,f=this;return h&&(this.type==Rt?l="\n":" "==r&&(!t()||d.isTrailingSpace||"\n"==d.character||" "==d.character&&d.characterType==mt)||("\n"==r&&this.isLeadingSpace?t()&&"\n"!=d.character&&(l="\n"):(u=this.nextUncollapsed(),u&&(u.isBr?this.type=Pt:u.isTrailingSpace&&"\n"==u.character?this.type=Tt:u.isLeadingSpace&&"\n"==u.character&&(this.type=xt),"\n"==u.character?(this.type!=Pt||e.includeSpaceBeforeBr)&&(this.type!=xt||e.includeSpaceBeforeBlock)&&(this.type==Tt&&u.isTrailingSpace&&!e.includeBlockContentTrailingSpace||(this.type!=bt||u.type!=Nt||e.includePreLineTrailingSpace)&&("\n"==r?u.isTrailingSpace?this.isTrailingSpace||this.isBr&&(u.type=wt,t()&&d.isLeadingSpace&&!d.isTrailingSpace&&"\n"==d.character?u.character="":u.type=Rt):l="\n":" "==r&&(l=" "))):l=r)))),i.indexOf(l)>-1&&(l=""),this.cache.set(s,l),l},equals:function(e){return!!e&&this.node===e.node&&this.offset===e.offset},inspect:w,toString:function(){return this.character}};b.prototype=Et,K(Et,{next:T("nextPos",function(e){var t=e.nodeWrapper,n=e.node,r=e.offset,i=t.session;if(!n)return null;var o,a,s;return r==t.getLength()?(o=n.parentNode,a=o?t.getNodeIndex()+1:0):t.isCharacterDataNode()?(o=n,a=r+1):(s=n.childNodes[r],i.getNodeWrapper(s).containsPositions()?(o=s,a=0):(o=n,a=r+1)),o?i.getPosition(o,a):null}),previous:T("previous",function(e){var t,n,r,i=e.nodeWrapper,o=e.node,a=e.offset,s=i.session;return 0==a?(t=o.parentNode,n=t?i.getNodeIndex():0):i.isCharacterDataNode()?(t=o,n=a-1):(r=o.childNodes[a-1],s.getNodeWrapper(r).containsPositions()?(t=r,n=U.getNodeLength(r)):(t=o,n=a-1)),t?s.getPosition(t,n):null}),nextVisible:T("nextVisible",function(e){var t=e.next();if(!t)return null;var n=t.nodeWrapper,r=t.node,i=t;return n.isCollapsed()&&(i=n.session.getPosition(r.parentNode,n.getNodeIndex()+1)),i}),nextUncollapsed:T("nextUncollapsed",function(e){for(var t=e;t=t.nextVisible();)if(t.resolveLeadingAndTrailingSpaces(),""!==t.character)return t;return null}),previousVisible:T("previousVisible",function(e){var t=e.previous();if(!t)return null;var n=t.nodeWrapper,r=t.node,i=t;return n.isCollapsed()&&(i=n.session.getPosition(r.parentNode,n.getNodeIndex())),i})});var Bt=null,Ot=function(){function e(e){var t=new x;return{get:function(n){var r=t.get(n[e]);if(r)for(var i,o=0;i=r[o++];)if(i.node===n)return i;return null},set:function(n){var r=n.node[e],i=t.get(r)||t.set(r,[]);i.push(n)}}}function t(){this.initCaches()}var n=G.isHostProperty(document.documentElement,"uniqueID");return t.prototype={initCaches:function(){this.elementCache=n?function(){var e=new x;return{get:function(t){return e.get(t.uniqueID)},set:function(t){e.set(t.node.uniqueID,t)}}}():e("tagName"),this.textNodeCache=e("data"),this.otherNodeCache=e("nodeName")},getNodeWrapper:function(e){var t;switch(e.nodeType){case 1:t=this.elementCache;break;case 3:t=this.textNodeCache;break;default:t=this.otherNodeCache}var n=t.get(e);return n||(n=new P(e,this),t.set(n)),n},getPosition:function(e,t){return this.getNodeWrapper(e).getPosition(t)},getRangeBoundaryPosition:function(e,t){var n=t?"start":"end";return this.getPosition(e[n+"Container"],e[n+"Offset"])},detach:function(){this.elementCache=this.textNodeCache=this.otherNodeCache=null}},t}();K(U,{nextNode:v,previousNode:S});var kt=Array.prototype.indexOf?function(e,t){return e.indexOf(t)}:function(e,t){for(var n=0,r=e.length;r>n;++n)if(e[n]===t)return n;return-1};K(e.rangePrototype,{moveStart:F(!0,!1),moveEnd:F(!1,!1),move:F(!0,!0),trimStart:V(!0),trimEnd:V(!1),trim:D(function(e,t){var n=this.trimStart(t),r=this.trimEnd(t);return n||r}),expand:D(function(e,t,n){var r=!1;n=s(n,lt);var i=n.characterOptions;if(t||(t=M),t==j){var o,a,c=n.wordOptions,u=e.getRangeBoundaryPosition(this,!0),d=e.getRangeBoundaryPosition(this,!1),l=k(u,i,c),h=l.nextEndToken(),p=h.chars[0].previousVisible();if(this.collapsed)o=h;else{var f=k(d,i,c);o=f.previousStartToken()}return a=o.chars[o.chars.length-1],p.equals(u)||(this.setStart(p.node,p.offset),r=!0),a&&!a.equals(d)&&(this.setEnd(a.node,a.offset),r=!0),n.trim&&(n.trimStart&&(r=this.trimStart(i)||r),n.trimEnd&&(r=this.trimEnd(i)||r)),r}return this.moveEnd(M,1,n)}),text:D(function(e,t){return this.collapsed?"":A(e,this,z(t,at)).join("")}),selectCharacters:D(function(e,t,n,r,i){var o={characterOptions:i};t||(t=H(this.getDocument())),this.selectNodeContents(t),this.collapse(!0),this.moveStart("character",n,o),this.collapse(!0),this.moveEnd("character",r-n,o)}),toCharacterRange:D(function(e,t,n){t||(t=H(this.getDocument()));var r,i,o=t.parentNode,a=U.getNodeIndex(t),s=-1==U.comparePoints(this.startContainer,this.endContainer,o,a),c=this.cloneRange();return s?(c.setStartAndEnd(this.startContainer,this.startOffset,o,a),r=-c.text(n).length):(c.setStartAndEnd(o,a,this.startContainer,this.startOffset),r=c.text(n).length),i=r+this.text(n).length,{start:r,end:i}}),findText:D(function(t,n,r){r=s(r,ut),r.wholeWordsOnly&&(r.wordOptions.includeTrailingSpace=!1);var i=et(r.direction),o=r.withinRange;o||(o=e.createRange(),o.selectNodeContents(this.getDocument()));var a=n,c=!1;"string"==typeof a?r.caseSensitive||(a=a.toLowerCase()):c=!0;var u=t.getRangeBoundaryPosition(this,!i),d=o.comparePoint(u.node,u.offset);-1===d?u=t.getRangeBoundaryPosition(o,!0):1===d&&(u=t.getRangeBoundaryPosition(o,!1));for(var l,h=u,p=!1;;)if(l=_(h,a,c,o,r)){if(l.valid)return this.setStartAndEnd(l.startPos.node,l.startPos.offset,l.endPos.node,l.endPos.offset),!0;h=i?l.startPos:l.endPos}else{if(!r.wrap||p)return!1;o=o.cloneRange(),h=t.getRangeBoundaryPosition(o,!i),o.setBoundary(u.node,u.offset,i),p=!0}}),pasteHtml:function(e){if(this.deleteContents(),e){var t=this.createContextualFragment(e),n=t.lastChild;this.insertNode(t),this.collapseAfter(n)}}}),K(e.selectionPrototype,{expand:D(function(e,t,n){this.changeEachRange(function(e){e.expand(t,n)})}),move:D(function(e,t,n,r){var i=0;if(this.focusNode){this.collapse(this.focusNode,this.focusOffset);var o=this.getRangeAt(0);r||(r={}),r.characterOptions=z(r.characterOptions,st),i=o.move(t,n,r),this.setSingleRange(o)}return i}),trimStart:$("trimStart"),trimEnd:$("trimEnd"),trim:$("trim"),selectCharacters:D(function(t,n,r,i,o,a){var s=e.createRange(n);s.selectCharacters(n,r,i,a),this.setSingleRange(s,o)}),saveCharacterRanges:D(function(e,t,n){for(var r=this.getAllRanges(),i=r.length,o=[],a=1==i&&this.isBackward(),s=0,c=r.length;c>s;++s)o[s]={characterRange:r[s].toCharacterRange(t,n),backward:a,characterOptions:n};return o}),restoreCharacterRanges:D(function(t,n,r){this.removeAllRanges();for(var i,o,a,s=0,c=r.length;c>s;++s)o=r[s],a=o.characterRange,i=e.createRange(n),i.selectCharacters(n,a.start,a.end,o.characterOptions),this.addRange(i,o.backward)}),text:D(function(e,t){for(var n=[],r=0,i=this.rangeCount;i>r;++r)n[r]=this.getRangeAt(r).text(t);return n.join("")})}),e.innerText=function(t,n){var r=e.createRange(t);r.selectNodeContents(t);var i=r.text(n);return i},e.createWordIterator=function(e,t,n){var r=E();n=s(n,ht);var i=r.getPosition(e,t),o=k(i,n.characterOptions,n.wordOptions),a=et(n.direction);return{next:function(){return a?o.previousStartToken():o.nextEndToken()},dispose:function(){o.dispose(),this.next=function(){}}}},e.noMutation=function(e){var t=E();e(t),B()},e.noMutation.createEntryPointFunction=D,e.textRange={isBlockNode:l,isCollapsedWhitespaceNode:N,createPosition:D(function(e,t,n){return e.getPosition(t,n)})}}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js b/libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js
new file mode 100644
index 000000000..acdcdb4c8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js
@@ -0,0 +1,358 @@
+// WordPress/wp-includes/js/shortcode.js
+
+// Utility functions for parsing and handling shortcodes in Javascript.
+
+// Ensure the global `wp` object exists.
+window.wp = window.wp || {};
+
+(function(){
+ wp.shortcode = {
+ // ### Find the next matching shortcode
+ //
+ // Given a shortcode `tag`, a block of `text`, and an optional starting
+ // `index`, returns the next matching shortcode or `undefined`.
+ //
+ // Shortcodes are formatted as an object that contains the match
+ // `content`, the matching `index`, and the parsed `shortcode` object.
+ next: function( tag, text, index ) {
+ var re = wp.shortcode.regexp( tag ),
+ match, result;
+
+ re.lastIndex = index || 0;
+ match = re.exec( text );
+
+ if ( ! match ) {
+ return;
+ }
+
+ // If we matched an escaped shortcode, try again.
+ if ( '[' === match[1] && ']' === match[7] ) {
+ return wp.shortcode.next( tag, text, re.lastIndex );
+ }
+
+ result = {
+ index: match.index,
+ content: match[0],
+ shortcode: wp.shortcode.fromMatch( match )
+ };
+
+ // If we matched a leading `[`, strip it from the match
+ // and increment the index accordingly.
+ if ( match[1] ) {
+ result.content = result.content.slice( 1 );
+ result.index++;
+ }
+
+ // If we matched a trailing `]`, strip it from the match.
+ if ( match[7] ) {
+ result.content = result.content.slice( 0, -1 );
+ }
+
+ return result;
+ },
+
+ // ### Replace matching shortcodes in a block of text
+ //
+ // Accepts a shortcode `tag`, content `text` to scan, and a `callback`
+ // to process the shortcode matches and return a replacement string.
+ // Returns the `text` with all shortcodes replaced.
+ //
+ // Shortcode matches are objects that contain the shortcode `tag`,
+ // a shortcode `attrs` object, the `content` between shortcode tags,
+ // and a boolean flag to indicate if the match was a `single` tag.
+ replace: function( tag, text, callback ) {
+ return text.replace( wp.shortcode.regexp( tag ), function( match, left, tag, attrs, slash, content, closing, right ) {
+ // If both extra brackets exist, the shortcode has been
+ // properly escaped.
+ if ( left === '[' && right === ']' ) {
+ return match;
+ }
+
+ // Create the match object and pass it through the callback.
+ var result = callback( wp.shortcode.fromMatch( arguments ) );
+
+ // Make sure to return any of the extra brackets if they
+ // weren't used to escape the shortcode.
+ return result ? left + result + right : match;
+ });
+ },
+
+ // ### Generate a string from shortcode parameters
+ //
+ // Creates a `wp.shortcode` instance and returns a string.
+ //
+ // Accepts the same `options` as the `wp.shortcode()` constructor,
+ // containing a `tag` string, a string or object of `attrs`, a boolean
+ // indicating whether to format the shortcode using a `single` tag, and a
+ // `content` string.
+ string: function( options ) {
+ return new wp.shortcode( options ).string();
+ },
+
+ // ### Generate a RegExp to identify a shortcode
+ //
+ // The base regex is functionally equivalent to the one found in
+ // `get_shortcode_regex()` in `wp-includes/shortcodes.php`.
+ //
+ // Capture groups:
+ //
+ // 1. An extra `[` to allow for escaping shortcodes with double `[[]]`
+ // 2. The shortcode name
+ // 3. The shortcode argument list
+ // 4. The self closing `/`
+ // 5. The content of a shortcode when it wraps some content.
+ // 6. The closing tag.
+ // 7. An extra `]` to allow for escaping shortcodes with double `[[]]`
+ regexp: _.memoize( function( tag ) {
+ return new RegExp( '\\[(\\[?)(' + tag + ')(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*(?:\\[(?!\\/\\2\\])[^\\[]*)*)(\\[\\/\\2\\]))?)(\\]?)', 'g' );
+ }),
+
+
+ // ### Parse shortcode attributes
+ //
+ // Shortcodes accept many types of attributes. These can chiefly be
+ // divided into named and numeric attributes:
+ //
+ // Named attributes are assigned on a key/value basis, while numeric
+ // attributes are treated as an array.
+ //
+ // Named attributes can be formatted as either `name="value"`,
+ // `name='value'`, or `name=value`. Numeric attributes can be formatted
+ // as `"value"` or just `value`.
+ attrs: _.memoize( function( text ) {
+ var named = {},
+ numeric = [],
+ pattern, match;
+
+ // This regular expression is reused from `shortcode_parse_atts()`
+ // in `wp-includes/shortcodes.php`.
+ //
+ // Capture groups:
+ //
+ // 1. An attribute name, that corresponds to...
+ // 2. a value in double quotes.
+ // 3. An attribute name, that corresponds to...
+ // 4. a value in single quotes.
+ // 5. An attribute name, that corresponds to...
+ // 6. an unquoted value.
+ // 7. A numeric attribute in double quotes.
+ // 8. An unquoted numeric attribute.
+ pattern = /(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/g;
+
+ // Map zero-width spaces to actual spaces.
+ text = text.replace( /[\u00a0\u200b]/g, ' ' );
+
+ // Match and normalize attributes.
+ while ( (match = pattern.exec( text )) ) {
+ if ( match[1] ) {
+ named[ match[1].toLowerCase() ] = match[2];
+ } else if ( match[3] ) {
+ named[ match[3].toLowerCase() ] = match[4];
+ } else if ( match[5] ) {
+ named[ match[5].toLowerCase() ] = match[6];
+ } else if ( match[7] ) {
+ numeric.push( match[7] );
+ } else if ( match[8] ) {
+ numeric.push( match[8] );
+ }
+ }
+
+ return {
+ named: named,
+ numeric: numeric
+ };
+ }),
+
+ // ### Generate a Shortcode Object from a RegExp match
+ // Accepts a `match` object from calling `regexp.exec()` on a `RegExp`
+ // generated by `wp.shortcode.regexp()`. `match` can also be set to the
+ // `arguments` from a callback passed to `regexp.replace()`.
+ fromMatch: function( match ) {
+ var type;
+
+ if ( match[4] ) {
+ type = 'self-closing';
+ } else if ( match[6] ) {
+ type = 'closed';
+ } else {
+ type = 'single';
+ }
+
+ return new wp.shortcode({
+ tag: match[2],
+ attrs: match[3],
+ type: type,
+ content: match[5]
+ });
+ }
+ };
+
+
+ // Shortcode Objects
+ // -----------------
+ //
+ // Shortcode objects are generated automatically when using the main
+ // `wp.shortcode` methods: `next()`, `replace()`, and `string()`.
+ //
+ // To access a raw representation of a shortcode, pass an `options` object,
+ // containing a `tag` string, a string or object of `attrs`, a string
+ // indicating the `type` of the shortcode ('single', 'self-closing', or
+ // 'closed'), and a `content` string.
+ wp.shortcode = _.extend( function( options ) {
+ _.extend( this, _.pick( options || {}, 'tag', 'attrs', 'type', 'content' ) );
+
+ var attrs = this.attrs;
+
+ // Ensure we have a correctly formatted `attrs` object.
+ this.attrs = {
+ named: {},
+ numeric: []
+ };
+
+ if ( ! attrs ) {
+ return;
+ }
+
+ // Parse a string of attributes.
+ if ( _.isString( attrs ) ) {
+ this.attrs = wp.shortcode.attrs( attrs );
+
+ // Identify a correctly formatted `attrs` object.
+ } else if ( _.isEqual( _.keys( attrs ), [ 'named', 'numeric' ] ) ) {
+ this.attrs = attrs;
+
+ // Handle a flat object of attributes.
+ } else {
+ _.each( options.attrs, function( value, key ) {
+ this.set( key, value );
+ }, this );
+ }
+ }, wp.shortcode );
+
+ _.extend( wp.shortcode.prototype, {
+ // ### Get a shortcode attribute
+ //
+ // Automatically detects whether `attr` is named or numeric and routes
+ // it accordingly.
+ get: function( attr ) {
+ return this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ];
+ },
+
+ // ### Set a shortcode attribute
+ //
+ // Automatically detects whether `attr` is named or numeric and routes
+ // it accordingly.
+ set: function( attr, value ) {
+ this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ] = value;
+ return this;
+ },
+
+ // ### Transform the shortcode match into a string
+ string: function() {
+ var text = '[' + this.tag;
+
+ _.each( this.attrs.numeric, function( value ) {
+ if ( /\s/.test( value ) ) {
+ text += ' "' + value + '"';
+ } else {
+ text += ' ' + value;
+ }
+ });
+
+ _.each( this.attrs.named, function( value, name ) {
+ text += ' ' + name + '="' + value + '"';
+ });
+
+ // If the tag is marked as `single` or `self-closing`, close the
+ // tag and ignore any additional content.
+ if ( 'single' === this.type ) {
+ return text + ']';
+ } else if ( 'self-closing' === this.type ) {
+ return text + ' /]';
+ }
+
+ // Complete the opening tag.
+ text += ']';
+
+ if ( this.content ) {
+ text += this.content;
+ }
+
+ // Add the closing tag.
+ return text + '[/' + this.tag + ']';
+ }
+ });
+}());
+
+// HTML utility functions
+// ----------------------
+//
+// Experimental. These functions may change or be removed in the future.
+(function(){
+ wp.html = _.extend( wp.html || {}, {
+ // ### Parse HTML attributes.
+ //
+ // Converts `content` to a set of parsed HTML attributes.
+ // Utilizes `wp.shortcode.attrs( content )`, which is a valid superset of
+ // the HTML attribute specification. Reformats the attributes into an
+ // object that contains the `attrs` with `key:value` mapping, and a record
+ // of the attributes that were entered using `empty` attribute syntax (i.e.
+ // with no value).
+ attrs: function( content ) {
+ var result, attrs;
+
+ // If `content` ends in a slash, strip it.
+ if ( '/' === content[ content.length - 1 ] ) {
+ content = content.slice( 0, -1 );
+ }
+
+ result = wp.shortcode.attrs( content );
+ attrs = result.named;
+
+ _.each( result.numeric, function( key ) {
+ if ( /\s/.test( key ) ) {
+ return;
+ }
+
+ attrs[ key ] = '';
+ });
+
+ return attrs;
+ },
+
+ // ### Convert an HTML-representation of an object to a string.
+ string: function( options ) {
+ var text = '<' + options.tag,
+ content = options.content || '';
+
+ _.each( options.attrs, function( value, attr ) {
+ text += ' ' + attr;
+
+ // Use empty attribute notation where possible.
+ if ( '' === value ) {
+ return;
+ }
+
+ // Convert boolean values to strings.
+ if ( _.isBoolean( value ) ) {
+ value = value ? 'true' : 'false';
+ }
+
+ text += '="' + value + '"';
+ });
+
+ // Return the result if it is a self-closing tag.
+ if ( options.single ) {
+ return text + ' />';
+ }
+
+ // Complete the opening tag.
+ text += '>';
+
+ // If `content` is an object, recursively call this function.
+ text += _.isObject( content ) ? wp.html.string( content ) : content;
+
+ return text + '</' + options.tag + '>';
+ }
+ });
+}());
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js b/libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js
new file mode 100644
index 000000000..ed15a637c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js
@@ -0,0 +1,6 @@
+// Underscore.js 1.7.0
+// http://underscorejs.org
+// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// Underscore may be freely distributed under the MIT license.
+(function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])<u?i=o+1:a=o}return i},h.toArray=function(n){return n?h.isArray(n)?a.call(n):n.length===+n.length?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:n.length===+n.length?n.length:h.keys(n).length},h.partition=function(n,t,r){t=h.iteratee(t,r);var e=[],u=[];return h.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},h.first=h.head=h.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:0>t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r<arguments.length;)e.push(arguments[r++]);return n.apply(this,e)}},h.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
+ //# sourceMappingURL=underscore-min.map \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/wpload.js b/libs/editor/WordPressEditor/src/main/assets/libs/wpload.js
new file mode 100644
index 000000000..1fc74b28c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/wpload.js
@@ -0,0 +1,108 @@
+/**
+* The code of this function comes from the WordPress source code,
+* from file `wp-admin/js/editor.js`, so we can get 100% consistent results
+* with wp-admin.
+*/
+
+// Ensure the global `wp` object exists.
+window.wp = window.wp || {};
+
+(function(){
+
+ /**
+ * @brief Performs multiple transformations to the post source code when
+ * loading, replacing newlines with pargraph elements.
+ *
+ * @param pee The string to transform
+ *
+ * @return Returns the transformed string
+ */
+ wp.loadText = function( pee ) {
+
+ if ( pee == null || !pee.trim() ) {
+ // Just whitespace, null, or undefined
+ return '';
+ }
+
+ var preserve_linebreaks = false,
+ preserve_br = false,
+ blocklist = 'table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre' +
+ '|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section' +
+ '|article|aside|hgroup|header|footer|nav|figure|details|menu|summary';
+
+ if ( pee.indexOf( '<object' ) !== -1 ) {
+ pee = pee.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
+ return a.replace( /[\r\n]+/g, '' );
+ });
+ }
+
+ pee = pee.replace( /<[^<>]+>/g, function( a ){
+ return a.replace( /[\r\n]+/g, ' ' );
+ });
+
+ // Protect pre|script tags
+ if ( pee.indexOf( '<pre' ) !== -1 || pee.indexOf( '<script' ) !== -1 ) {
+ preserve_linebreaks = true;
+ pee = pee.replace( /<(pre|script)[^>]*>[\s\S]+?<\/\1>/g, function( a ) {
+ return a.replace( /(\r\n|\n)/g, '<wp-line-break>' );
+ });
+ }
+
+ // keep <br> tags inside captions and convert line breaks
+ if ( pee.indexOf( '[caption' ) !== -1 ) {
+ preserve_br = true;
+ pee = pee.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
+ // keep existing <br>
+ a = a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' );
+ // no line breaks inside HTML tags
+ a = a.replace( /<[a-zA-Z0-9]+( [^<>]+)?>/g, function( b ) {
+ return b.replace( /[\r\n\t]+/, ' ' );
+ });
+ // convert remaining line breaks to <br>
+ return a.replace( /\s*\n\s*/g, '<wp-temp-br />' );
+ });
+ }
+
+ pee = pee + '\n\n';
+ pee = pee.replace( /<br \/>\s*<br \/>/gi, '\n\n' );
+ pee = pee.replace( new RegExp( '(<(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '\n$1' );
+ pee = pee.replace( new RegExp( '(</(?:' + blocklist + ')>)', 'gi' ), '$1\n\n' );
+ pee = pee.replace( /<hr( [^>]*)?>/gi, '<hr$1>\n\n' ); // hr is self closing block element
+ pee = pee.replace( /\s*<option/gi, '<option' ); // No <p> or <br> around <option>
+ pee = pee.replace( /<\/option>\s*/gi, '</option>' );
+ pee = pee.replace( /\r\n|\r/g, '\n' );
+ pee = pee.replace( /\n\s*\n+/g, '\n\n' );
+ pee = pee.replace( /([\s\S]+?)\n\n/g, '<p>$1</p>\n' );
+ pee = pee.replace( /<p>\s*?<\/p>/gi, '');
+ pee = pee.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
+ pee = pee.replace( /<p>(<li.+?)<\/p>/gi, '$1');
+ pee = pee.replace( /<p>\s*<blockquote([^>]*)>/gi, '<blockquote$1><p>');
+ pee = pee.replace( /<\/blockquote>\s*<\/p>/gi, '</p></blockquote>');
+ pee = pee.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '$1' );
+ pee = pee.replace( new RegExp( '(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
+ pee = pee.replace( /\s*\n/gi, '<br />\n');
+ pee = pee.replace( new RegExp( '(</?(?:' + blocklist + ')[^>]*>)\\s*<br />', 'gi' ), '$1' );
+ pee = pee.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, '$1' );
+ pee = pee.replace( /(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi, '[caption$1[/caption]' );
+
+ pee = pee.replace( /(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function( a, b, c ) {
+ if ( c.match( /<p( [^>]*)?>/ ) ) {
+ return a;
+ }
+
+ return b + '<p>' + c + '</p>';
+ });
+
+ // put back the line breaks in pre|script
+ if ( preserve_linebreaks ) {
+ pee = pee.replace( /<wp-line-break>/g, '\n' );
+ }
+
+ if ( preserve_br ) {
+ pee = pee.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
+ }
+
+ return pee;
+ }
+
+}());
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js b/libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js
new file mode 100644
index 000000000..798673d98
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js
@@ -0,0 +1,113 @@
+/**
+* The code of this function comes from the WordPress source code,
+* from file `wp-admin/js/editor.js`, so we can get 100% consistent results
+* with wp-admin.
+*/
+
+// Ensure the global `wp` object exists.
+window.wp = window.wp || {};
+
+(function(){
+
+ /**
+ * @brief Performs multiple transformations to the post source code when
+ * saving, removing paragraphs whenever possible.
+ *
+ * @param content The string to transform
+ *
+ * @return Returns the transformed string
+ */
+ wp.saveText = function( content ) {
+
+ if ( content == null || !content.trim() ) {
+ // Just whitespace, null, or undefined
+ return '';
+ }
+
+ var blocklist1, blocklist2,
+ preserve_linebreaks = false,
+ preserve_br = false;
+
+ // Protect pre|script tags
+ if ( content.indexOf( '<pre' ) !== -1 || content.indexOf( '<script' ) !== -1 ) {
+ preserve_linebreaks = true;
+ content = content.replace( /<(pre|script)[^>]*>[\s\S]+?<\/\1>/g, function( a ) {
+ a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' );
+ a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' );
+ return a.replace( /\r?\n/g, '<wp-line-break>' );
+ });
+ }
+
+ // keep <br> tags inside captions and remove line breaks
+ if ( content.indexOf( '[caption' ) !== -1 ) {
+ preserve_br = true;
+ content = content.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
+ return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' );
+ });
+ }
+
+ // Pretty it up for the source editor
+ blocklist1 = 'blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|div|h[1-6]|p|fieldset';
+ content = content.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' );
+ content = content.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' );
+
+ // Mark </p> if it has any attributes.
+ content = content.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' );
+
+ // Separate <div> containing <p>
+ content = content.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' );
+
+ // Remove <p> and <br />
+ content = content.replace( /\s*<p>/gi, '' );
+ content = content.replace( /\s*<\/p>\s*/gi, '\n\n' );
+ content = content.replace( /\n[\s\u00a0]+\n/g, '\n\n' );
+ content = content.replace( /\s*<br ?\/?>\s*/gi, '\n' );
+
+ // Fix some block element newline issues
+ content = content.replace( /\s*<div/g, '\n<div' );
+ content = content.replace( /<\/div>\s*/g, '</div>\n' );
+ content = content.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' );
+ content = content.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' );
+
+ blocklist2 = 'blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|h[1-6]|pre|fieldset';
+ content = content.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' );
+ content = content.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' );
+ content = content.replace( /<li([^>]*)>/g, '\t<li$1>' );
+
+ if ( content.indexOf( '<option' ) !== -1 ) {
+ content = content.replace( /\s*<option/g, '\n<option' );
+ content = content.replace( /\s*<\/select>/g, '\n</select>' );
+ }
+
+ if ( content.indexOf( '<hr' ) !== -1 ) {
+ content = content.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' );
+ }
+
+ if ( content.indexOf( '<object' ) !== -1 ) {
+ content = content.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
+ return a.replace( /[\r\n]+/g, '' );
+ });
+ }
+
+ // Unmark special paragraph closing tags
+ content = content.replace( /<\/p#>/g, '</p>\n' );
+ content = content.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' );
+
+ // Trim whitespace
+ content = content.replace( /^\s+/, '' );
+ content = content.replace( /[\s\u00a0]+$/, '' );
+
+ // put back the line breaks in pre|script
+ if ( preserve_linebreaks ) {
+ content = content.replace( /<wp-line-break>/g, '\n' );
+ }
+
+ // and the <br> tags in captions
+ if ( preserve_br ) {
+ content = content.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
+ }
+
+ return content;
+ }
+
+}());
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg b/libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg
new file mode 100644
index 000000000..4d961ebbd
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>delete-image</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="gridicons-cross" fill="#FFFFFF">
+ <polygon id="Shape" points="17.705 7.705 16.295 6.295 12 10.59 7.705 6.295 6.295 7.705 10.59 12 6.295 16.295 7.705 17.705 12 13.41 16.295 17.705 17.705 16.295 13.41 12"></polygon>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg b/libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg
new file mode 100644
index 000000000..d60fa4965
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>gridicons-pencil</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="gridicons-pencil" fill="#FFFFFF">
+ <path d="M17.3333333,8 L24,14.6666667 L11.324,27.3426667 C10.4093333,26.428 10.404,24.9506667 11.308,24.0293333 L11.3053333,24.0253333 C10.3853333,24.9266667 8.90533333,24.9226667 7.992,24.008 C7.08933333,23.1053333 7.07733333,21.6586667 7.944,20.7346667 L7.93333333,20.724 C7.008,21.5906667 5.56,21.5773333 4.65866667,20.676 L17.3333333,8 L17.3333333,8 Z M27.448,7.448 L24.552,4.552 C23.512,3.512 21.8226667,3.512 20.7813333,4.552 L18.6666667,6.66666667 L25.3333333,13.3333333 L27.448,11.2186667 C28.488,10.1786667 28.488,8.48933333 27.448,7.448 L27.448,7.448 Z M4,24 L4,28 L8,28 C8,25.7906667 6.20933333,24 4,24 L4,24 Z" id="Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/more-2x.png b/libs/editor/WordPressEditor/src/main/assets/svg/more-2x.png
new file mode 100644
index 000000000..c976e506d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/more-2x.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.png b/libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.png
new file mode 100644
index 000000000..e5cb33bb4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg
new file mode 100644
index 000000000..332e5bbda
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="72px" height="72px" viewBox="0 0 24 24" xml:space="preserve">
+<g id="refresh">
+ <path d="M17.91,14c-0.478,2.833-2.943,5-5.91,5c-3.308,0-6-2.692-6-6s2.692-6,6-6h2.172l-2.086,2.086L13.5,10.5L18,6l-4.5-4.5
+ l-1.414,1.414L14.172,5H12c-4.418,0-8,3.582-8,8s3.582,8,8,8c4.079,0,7.438-3.055,7.931-7H17.91z" fill="#FFFFFF"/>
+</g>
+<g id="Layer_1">
+</g>
+</svg>
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg
new file mode 100644
index 000000000..b3cb9533b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="32px" height="32px" viewBox="0 0 24 24" xml:space="preserve">
+<g id="refresh">
+ <path d="M17.91,14c-0.478,2.833-2.943,5-5.91,5c-3.308,0-6-2.692-6-6s2.692-6,6-6h2.172l-2.086,2.086L13.5,10.5L18,6l-4.5-4.5
+ l-1.414,1.414L14.172,5H12c-4.418,0-8,3.582-8,8s3.582,8,8,8c4.079,0,7.438-3.055,7.931-7H17.91z" fill="#FFFFFF"/>
+</g>
+<g id="Layer_1">
+</g>
+</svg>
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg b/libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg
new file mode 100644
index 000000000..b15c55159
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="720" viewBox="0 0 1280 720" style="background:#2e4453">
+<svg x="512" y="232" width="256" height="256" viewBox="0 0 24 24">
+ <path fill="#FFFFFF" d="M20,4v2h-2V4H6v2H4V4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2v-2h2v2h12v-2h2v2c1.1,0,2-0.9,2-2V6
+ C22,4.9,21.1,4,20,4z M6,16H4v-3h2V16z M6,11H4V8h2V11z M10,15V9l4.5,3L10,15z M20,16h-2v-3h2V16z M20,11h-2V8h2V11z"/>
+</svg>
+</svg> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java
new file mode 100755
index 000000000..9ff8df7d8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java
@@ -0,0 +1,1659 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.URLUtil;
+import android.webkit.WebView;
+import android.widget.RelativeLayout.LayoutParams;
+import android.widget.ToggleButton;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.editor.EditorWebViewAbstract.ErrorListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.ProfilingUtils;
+import org.wordpress.android.util.ShortcodeUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class EditorFragment extends EditorFragmentAbstract implements View.OnClickListener, View.OnTouchListener,
+ OnJsEditorStateChangedListener, OnImeBackListener, EditorWebViewAbstract.AuthHeaderRequestListener,
+ EditorMediaUploadListener {
+ private static final String ARG_PARAM_TITLE = "param_title";
+ private static final String ARG_PARAM_CONTENT = "param_content";
+
+ private static final String JS_CALLBACK_HANDLER = "nativeCallbackHandler";
+
+ private static final String KEY_TITLE = "title";
+ private static final String KEY_CONTENT = "content";
+
+ private static final String TAG_FORMAT_BAR_BUTTON_MEDIA = "media";
+ private static final String TAG_FORMAT_BAR_BUTTON_LINK = "link";
+
+ private static final float TOOLBAR_ALPHA_ENABLED = 1;
+ private static final float TOOLBAR_ALPHA_DISABLED = 0.5f;
+
+ private static final List<String> DRAGNDROP_SUPPORTED_MIMETYPES_TEXT = Arrays.asList(ClipDescription.MIMETYPE_TEXT_PLAIN,
+ ClipDescription.MIMETYPE_TEXT_HTML);
+ private static final List<String> DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE = Arrays.asList("image/jpeg", "image/png");
+
+ public static final int MAX_ACTION_TIME_MS = 2000;
+
+ private String mTitle = "";
+ private String mContentHtml = "";
+
+ private EditorWebViewAbstract mWebView;
+ private View mSourceView;
+ private SourceViewEditText mSourceViewTitle;
+ private SourceViewEditText mSourceViewContent;
+
+ private int mSelectionStart;
+ private int mSelectionEnd;
+
+ private String mFocusedFieldId;
+
+ private String mTitlePlaceholder = "";
+ private String mContentPlaceholder = "";
+
+ private boolean mDomHasLoaded = false;
+ private boolean mIsKeyboardOpen = false;
+ private boolean mEditorWasPaused = false;
+ private boolean mHideActionBarOnSoftKeyboardUp = false;
+ private boolean mIsFormatBarDisabled = false;
+
+ private ConcurrentHashMap<String, MediaFile> mWaitingMediaFiles;
+ private Set<MediaGallery> mWaitingGalleries;
+ private Map<String, MediaType> mUploadingMedia;
+ private Set<String> mFailedMediaIds;
+ private MediaGallery mUploadingMediaGallery;
+
+ private String mJavaScriptResult = "";
+
+ private CountDownLatch mGetTitleCountDownLatch;
+ private CountDownLatch mGetContentCountDownLatch;
+ private CountDownLatch mGetSelectedTextCountDownLatch;
+
+ private final Map<String, ToggleButton> mTagToggleButtonMap = new HashMap<>();
+
+ private long mActionStartedAt = -1;
+
+ private final View.OnDragListener mOnDragListener = new View.OnDragListener() {
+ private long lastSetCoordsTimestamp;
+
+ private boolean isSupported(ClipDescription clipDescription, List<String> mimeTypesToCheck) {
+ if (clipDescription == null) {
+ return false;
+ }
+
+ for (String supportedMimeType : mimeTypesToCheck) {
+ if (clipDescription.hasMimeType(supportedMimeType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onDrag(View view, DragEvent dragEvent) {
+ switch (dragEvent.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ return isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_TEXT) ||
+ isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE);
+ case DragEvent.ACTION_DRAG_ENTERED:
+ // would be nice to start marking the place the item will drop
+ break;
+ case DragEvent.ACTION_DRAG_LOCATION:
+ int x = DisplayUtils.pxToDp(getActivity(), (int) dragEvent.getX());
+ int y = DisplayUtils.pxToDp(getActivity(), (int) dragEvent.getY());
+
+ // don't call into JS too often
+ long currentTimestamp = SystemClock.uptimeMillis();
+ if ((currentTimestamp - lastSetCoordsTimestamp) > 150) {
+ lastSetCoordsTimestamp = currentTimestamp;
+
+ mWebView.execJavaScriptFromString("ZSSEditor.moveCaretToCoords(" + x + ", " + y + ");");
+ }
+ break;
+ case DragEvent.ACTION_DRAG_EXITED:
+ // clear any drop marking maybe
+ break;
+ case DragEvent.ACTION_DROP:
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ if (isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE)) {
+ // don't allow dropping images into the HTML source
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_html_images_not_allowed,
+ ToastUtils.Duration.LONG);
+ return true;
+ } else {
+ // let the system handle the text drop
+ return false;
+ }
+ }
+
+ if (isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE) &&
+ ("zss_field_title".equals(mFocusedFieldId))) {
+ // don't allow dropping images into the title field
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_title_images_not_allowed,
+ ToastUtils.Duration.LONG);
+ return true;
+ }
+
+ if (isAdded()) {
+ mEditorDragAndDropListener.onRequestDragAndDropPermissions(dragEvent);
+ }
+
+ ClipDescription clipDescription = dragEvent.getClipDescription();
+ if (clipDescription.getMimeTypeCount() < 1) {
+ break;
+ }
+
+ ContentResolver contentResolver = getActivity().getContentResolver();
+ ArrayList<Uri> uris = new ArrayList<>();
+ boolean unsupportedDropsFound = false;
+
+ for (int i = 0; i < dragEvent.getClipData().getItemCount(); i++) {
+ ClipData.Item item = dragEvent.getClipData().getItemAt(i);
+ Uri uri = item.getUri();
+
+ final String uriType = uri != null ? contentResolver.getType(uri) : null;
+ if (uriType != null && DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE.contains(uriType)) {
+ uris.add(uri);
+ continue;
+ } else if (item.getText() != null) {
+ insertTextToEditor(item.getText().toString());
+ continue;
+ } else if (item.getHtmlText() != null) {
+ insertTextToEditor(item.getHtmlText());
+ continue;
+ }
+
+ // any other drop types are not supported, including web URLs. We cannot proactively
+ // determine their mime type for filtering
+ unsupportedDropsFound = true;
+ }
+
+ if (unsupportedDropsFound) {
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_unsupported_files, ToastUtils
+ .Duration.LONG);
+ }
+
+ if (uris.size() > 0) {
+ mEditorDragAndDropListener.onMediaDropped(uris);
+ }
+
+ break;
+ case DragEvent.ACTION_DRAG_ENDED:
+ // clear any drop marking maybe
+ default:
+ break;
+ }
+ return true;
+ }
+
+ private void insertTextToEditor(String text) {
+ if (text != null) {
+ mWebView.execJavaScriptFromString("ZSSEditor.insertText('" + Utils.escapeHtml(text) + "', true);");
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_text_error, ToastUtils.Duration.SHORT);
+ AppLog.d(T.EDITOR, "Dropped text was null!");
+ }
+ }
+ };
+
+ public static EditorFragment newInstance(String title, String content) {
+ EditorFragment fragment = new EditorFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_PARAM_TITLE, title);
+ args.putString(ARG_PARAM_CONTENT, content);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public EditorFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ProfilingUtils.start("Visual Editor Startup");
+ ProfilingUtils.split("EditorFragment.onCreate");
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_editor, container, false);
+
+ // Setup hiding the action bar when the soft keyboard is displayed for narrow viewports
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE
+ && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) {
+ mHideActionBarOnSoftKeyboardUp = true;
+ }
+
+ mWaitingMediaFiles = new ConcurrentHashMap<>();
+ mWaitingGalleries = Collections.newSetFromMap(new ConcurrentHashMap<MediaGallery, Boolean>());
+ mUploadingMedia = new HashMap<>();
+ mFailedMediaIds = new HashSet<>();
+
+ // -- WebView configuration
+
+ mWebView = (EditorWebViewAbstract) view.findViewById(R.id.webview);
+
+ // Revert to compatibility WebView for custom ROMs using a 4.3 WebView in Android 4.4
+ if (mWebView.shouldSwitchToCompatibilityMode()) {
+ ViewGroup parent = (ViewGroup) mWebView.getParent();
+ int index = parent.indexOfChild(mWebView);
+ parent.removeView(mWebView);
+ mWebView = new EditorWebViewCompatibility(getActivity(), null);
+ mWebView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ parent.addView(mWebView, index);
+ }
+
+ mWebView.setOnTouchListener(this);
+ mWebView.setOnImeBackListener(this);
+ mWebView.setAuthHeaderRequestListener(this);
+
+ mWebView.setOnDragListener(mOnDragListener);
+
+ if (mCustomHttpHeaders != null && mCustomHttpHeaders.size() > 0) {
+ for (Map.Entry<String, String> entry : mCustomHttpHeaders.entrySet()) {
+ mWebView.setCustomHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // Ensure that the content field is always filling the remaining screen space
+ mWebView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("try {ZSSEditor.refreshVisibleViewportSize();} catch (e) " +
+ "{console.log(e)}");
+ }
+ });
+ }
+ });
+
+ mEditorFragmentListener.onEditorFragmentInitialized();
+
+ initJsEditor();
+
+ if (savedInstanceState != null) {
+ setTitle(savedInstanceState.getCharSequence(KEY_TITLE));
+ setContent(savedInstanceState.getCharSequence(KEY_CONTENT));
+ }
+
+ // -- HTML mode configuration
+
+ mSourceView = view.findViewById(R.id.sourceview);
+ mSourceViewTitle = (SourceViewEditText) view.findViewById(R.id.sourceview_title);
+ mSourceViewContent = (SourceViewEditText) view.findViewById(R.id.sourceview_content);
+
+ // Toggle format bar on/off as user changes focus between title and content in HTML mode
+ mSourceViewTitle.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ updateFormatBarEnabledState(!hasFocus);
+ }
+ });
+
+ mSourceViewTitle.setOnTouchListener(this);
+ mSourceViewContent.setOnTouchListener(this);
+
+ mSourceViewTitle.setOnImeBackListener(this);
+ mSourceViewContent.setOnImeBackListener(this);
+
+ mSourceViewContent.addTextChangedListener(new HtmlStyleTextWatcher());
+
+ mSourceViewTitle.setHint(mTitlePlaceholder);
+ mSourceViewContent.setHint("<p>" + mContentPlaceholder + "</p>");
+
+ // attach drag-and-drop handler
+ mSourceViewTitle.setOnDragListener(mOnDragListener);
+ mSourceViewContent.setOnDragListener(mOnDragListener);
+
+ // -- Format bar configuration
+
+ setupFormatBarButtonMap(view);
+
+ return view;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mEditorWasPaused = true;
+ mIsKeyboardOpen = false;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // If the editor was previously paused and the current orientation is landscape,
+ // hide the actionbar because the keyboard is going to appear (even if it was hidden
+ // prior to being paused).
+ if (mEditorWasPaused
+ && (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
+ && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) {
+ mIsKeyboardOpen = true;
+ mHideActionBarOnSoftKeyboardUp = true;
+ hideActionBarIfNeeded();
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mEditorDragAndDropListener = (EditorDragAndDropListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement EditorDragAndDropListener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ // Soft cancel (delete flag off) all media uploads currently in progress
+ for (String mediaId : mUploadingMedia.keySet()) {
+ mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, false);
+ }
+ super.onDetach();
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ if (mDomHasLoaded) {
+ mWebView.notifyVisibilityChanged(isVisibleToUser);
+ }
+ super.setUserVisibleHint(isVisibleToUser);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putCharSequence(KEY_TITLE, getTitle());
+ outState.putCharSequence(KEY_CONTENT, getContent());
+ }
+
+ private ActionBar getActionBar() {
+ if (!isAdded()) {
+ return null;
+ }
+
+ if (getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ if (getView() != null) {
+ // Reload the format bar to make sure the correct one for the new screen width is being used
+ View formatBar = getView().findViewById(R.id.format_bar);
+
+ if (formatBar != null) {
+ // Remember the currently active format bar buttons so they can be re-activated after the reload
+ ArrayList<String> activeTags = new ArrayList<>();
+ for (Map.Entry<String, ToggleButton> entry : mTagToggleButtonMap.entrySet()) {
+ if (entry.getValue().isChecked()) {
+ activeTags.add(entry.getKey());
+ }
+ }
+
+ ViewGroup parent = (ViewGroup) formatBar.getParent();
+ parent.removeView(formatBar);
+
+ formatBar = getActivity().getLayoutInflater().inflate(R.layout.format_bar, parent, false);
+ formatBar.setId(R.id.format_bar);
+ parent.addView(formatBar);
+
+ setupFormatBarButtonMap(formatBar);
+
+ if (mIsFormatBarDisabled) {
+ updateFormatBarEnabledState(false);
+ }
+
+ // Restore the active format bar buttons
+ for (String tag : activeTags) {
+ mTagToggleButtonMap.get(tag).setChecked(true);
+ }
+
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ ToggleButton htmlButton = (ToggleButton) formatBar.findViewById(R.id.format_bar_button_html);
+ htmlButton.setChecked(true);
+ }
+ }
+
+ // Reload HTML mode margins
+ View sourceViewTitle = getView().findViewById(R.id.sourceview_title);
+ View sourceViewContent = getView().findViewById(R.id.sourceview_content);
+
+ if (sourceViewTitle != null && sourceViewContent != null) {
+ int sideMargin = (int) getActivity().getResources().getDimension(R.dimen.sourceview_side_margin);
+
+ ViewGroup.MarginLayoutParams titleParams =
+ (ViewGroup.MarginLayoutParams) sourceViewTitle.getLayoutParams();
+ ViewGroup.MarginLayoutParams contentParams =
+ (ViewGroup.MarginLayoutParams) sourceViewContent.getLayoutParams();
+
+ titleParams.setMargins(sideMargin, titleParams.topMargin, sideMargin, titleParams.bottomMargin);
+ contentParams.setMargins(sideMargin, contentParams.topMargin, sideMargin, contentParams.bottomMargin);
+ }
+ }
+
+ // Toggle action bar auto-hiding for the new orientation
+ if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
+ && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) {
+ mHideActionBarOnSoftKeyboardUp = true;
+ hideActionBarIfNeeded();
+ } else {
+ mHideActionBarOnSoftKeyboardUp = false;
+ showActionBarIfNeeded();
+ }
+ }
+
+ private void setupFormatBarButtonMap(View view) {
+ ToggleButton boldButton = (ToggleButton) view.findViewById(R.id.format_bar_button_bold);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_bold), boldButton);
+
+ ToggleButton italicButton = (ToggleButton) view.findViewById(R.id.format_bar_button_italic);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_italic), italicButton);
+
+ ToggleButton quoteButton = (ToggleButton) view.findViewById(R.id.format_bar_button_quote);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_blockquote), quoteButton);
+
+ ToggleButton ulButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ul);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_unorderedList), ulButton);
+
+ ToggleButton olButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ol);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_orderedList), olButton);
+
+ // Tablet-only
+ ToggleButton strikethroughButton = (ToggleButton) view.findViewById(R.id.format_bar_button_strikethrough);
+ if (strikethroughButton != null) {
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_strikethrough), strikethroughButton);
+ }
+
+ ToggleButton mediaButton = (ToggleButton) view.findViewById(R.id.format_bar_button_media);
+ mTagToggleButtonMap.put(TAG_FORMAT_BAR_BUTTON_MEDIA, mediaButton);
+
+ registerForContextMenu(mediaButton);
+
+ ToggleButton linkButton = (ToggleButton) view.findViewById(R.id.format_bar_button_link);
+ mTagToggleButtonMap.put(TAG_FORMAT_BAR_BUTTON_LINK, linkButton);
+
+ ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html);
+ htmlButton.setOnClickListener(this);
+
+ for (ToggleButton button : mTagToggleButtonMap.values()) {
+ button.setOnClickListener(this);
+ }
+ }
+
+ protected void initJsEditor() {
+ if (!isAdded()) {
+ return;
+ }
+
+ ProfilingUtils.split("EditorFragment.initJsEditor");
+
+ String htmlEditor = Utils.getHtmlFromFile(getActivity(), "android-editor.html");
+ if (htmlEditor != null) {
+ htmlEditor = htmlEditor.replace("%%TITLE%%", getString(R.string.visual_editor));
+ htmlEditor = htmlEditor.replace("%%ANDROID_API_LEVEL%%", String.valueOf(Build.VERSION.SDK_INT));
+ htmlEditor = htmlEditor.replace("%%LOCALIZED_STRING_INIT%%",
+ "nativeState.localizedStringEdit = '" + getString(R.string.edit) + "';\n" +
+ "nativeState.localizedStringUploading = '" + getString(R.string.uploading) + "';\n" +
+ "nativeState.localizedStringUploadingGallery = '" + getString(R.string.uploading_gallery_placeholder) + "';\n");
+ }
+
+ // To avoid reflection security issues with JavascriptInterface on API<17, we use an iframe to make URL requests
+ // for callbacks from JS instead. These are received by WebViewClient.shouldOverrideUrlLoading() and then
+ // passed on to the JsCallbackReceiver
+ if (Build.VERSION.SDK_INT < 17) {
+ mWebView.setJsCallbackReceiver(new JsCallbackReceiver(this));
+ } else {
+ mWebView.addJavascriptInterface(new JsCallbackReceiver(this), JS_CALLBACK_HANDLER);
+ }
+
+ mWebView.loadDataWithBaseURL("file:///android_asset/", htmlEditor, "text/html", "utf-8", "");
+
+ if (mDebugModeEnabled) {
+ enableWebDebugging(true);
+ }
+ }
+
+ public void checkForFailedUploadAndSwitchToHtmlMode(final ToggleButton toggleButton) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Show an Alert Dialog asking the user if he wants to remove all failed media before upload
+ if (hasFailedMediaUploads()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.editor_failed_uploads_switch_html)
+ .setPositiveButton(R.string.editor_remove_failed_uploads, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // Clear failed uploads and switch to HTML mode
+ removeAllFailedMediaUploads();
+ toggleHtmlMode(toggleButton);
+ }
+ }).setNegativeButton(android.R.string.cancel, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ toggleButton.setChecked(false);
+ }
+ });
+ builder.create().show();
+ } else {
+ toggleHtmlMode(toggleButton);
+ }
+ }
+
+ public boolean isActionInProgress() {
+ return System.currentTimeMillis() - mActionStartedAt < MAX_ACTION_TIME_MS;
+ }
+
+ private void toggleHtmlMode(final ToggleButton toggleButton) {
+ if (!isAdded()) {
+ return;
+ }
+
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.HTML_BUTTON_TAPPED);
+
+ // Don't switch to HTML mode if currently uploading media
+ if (!mUploadingMedia.isEmpty() || isActionInProgress()) {
+ toggleButton.setChecked(false);
+ ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG);
+ return;
+ }
+
+ clearFormatBarButtons();
+ updateFormatBarEnabledState(true);
+
+ if (toggleButton.isChecked()) {
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Update mTitle and mContentHtml with the latest state from the ZSSEditor
+ getTitle();
+ getContent();
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Set HTML mode state
+ mSourceViewTitle.setText(mTitle);
+
+ SpannableString spannableContent = new SpannableString(mContentHtml);
+ HtmlStyleUtils.styleHtmlForDisplay(spannableContent);
+ mSourceViewContent.setText(spannableContent);
+
+ mWebView.setVisibility(View.GONE);
+ mSourceView.setVisibility(View.VISIBLE);
+
+ mSourceViewContent.requestFocus();
+ mSourceViewContent.setSelection(0);
+
+ InputMethodManager imm = ((InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE));
+ imm.showSoftInput(mSourceViewContent, InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+ }
+ });
+
+ thread.start();
+
+ } else {
+ mWebView.setVisibility(View.VISIBLE);
+ mSourceView.setVisibility(View.GONE);
+
+ mTitle = mSourceViewTitle.getText().toString();
+ mContentHtml = mSourceViewContent.getText().toString();
+ updateVisualEditorFields();
+
+ // Update the list of failed media uploads
+ mWebView.execJavaScriptFromString("ZSSEditor.getFailedMedia();");
+
+ // Reset selection to avoid buggy cursor behavior
+ mWebView.execJavaScriptFromString("ZSSEditor.resetSelectionOnField('zss_field_content');");
+ }
+ }
+
+ private void displayLinkDialog() {
+ final LinkDialogFragment linkDialogFragment = new LinkDialogFragment();
+ linkDialogFragment.setTargetFragment(this, LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD);
+
+ final Bundle dialogBundle = new Bundle();
+
+ // Pass potential URL from user clipboard
+ String clipboardUri = Utils.getUrlFromClipboard(getActivity());
+ if (clipboardUri != null) {
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_URL, clipboardUri);
+ }
+
+ // Pass selected text to dialog
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ // HTML mode
+ mSelectionStart = mSourceViewContent.getSelectionStart();
+ mSelectionEnd = mSourceViewContent.getSelectionEnd();
+
+ String selectedText = mSourceViewContent.getText().toString().substring(mSelectionStart, mSelectionEnd);
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, selectedText);
+
+ linkDialogFragment.setArguments(dialogBundle);
+ linkDialogFragment.show(getFragmentManager(), LinkDialogFragment.class.getSimpleName());
+ } else {
+ // Visual mode
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+
+ mGetSelectedTextCountDownLatch = new CountDownLatch(1);
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString(
+ "ZSSEditor.execFunctionForResult('getSelectedTextToLinkify');");
+ }
+ });
+
+ try {
+ if (mGetSelectedTextCountDownLatch.await(1, TimeUnit.SECONDS)) {
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, mJavaScriptResult);
+ }
+ } catch (InterruptedException e) {
+ AppLog.d(T.EDITOR, "Failed to obtain selected text from JS editor.");
+ }
+
+ linkDialogFragment.setArguments(dialogBundle);
+ linkDialogFragment.show(getFragmentManager(), LinkDialogFragment.class.getSimpleName());
+ }
+ });
+
+ thread.start();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ int id = v.getId();
+ if (id == R.id.format_bar_button_html) {
+ checkForFailedUploadAndSwitchToHtmlMode((ToggleButton) v);
+ } else if (id == R.id.format_bar_button_media) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED);
+ ((ToggleButton) v).setChecked(false);
+
+ if (isActionInProgress()) {
+ ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG);
+ return;
+ }
+
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ ToastUtils.showToast(getActivity(), R.string.alert_insert_image_html_mode, ToastUtils.Duration.LONG);
+ } else {
+ mEditorFragmentListener.onAddMediaClicked();
+ getActivity().openContextMenu(mTagToggleButtonMap.get(TAG_FORMAT_BAR_BUTTON_MEDIA));
+ }
+ } else if (id == R.id.format_bar_button_link) {
+ if (!((ToggleButton) v).isChecked()) {
+ // The link button was checked when it was pressed; remove the current link
+ mWebView.execJavaScriptFromString("ZSSEditor.unlink();");
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNLINK_BUTTON_TAPPED);
+ return;
+ }
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED);
+
+ ((ToggleButton) v).setChecked(false);
+
+ displayLinkDialog();
+ } else {
+ if (v instanceof ToggleButton) {
+ onFormattingButtonClicked((ToggleButton) v);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ // If the WebView or EditText has received a touch event, the keyboard will be displayed and the action bar
+ // should hide
+ mIsKeyboardOpen = true;
+ hideActionBarIfNeeded();
+ }
+ return false;
+ }
+
+ /**
+ * Intercept back button press while soft keyboard is visible.
+ */
+ @Override
+ public void onImeBack() {
+ mIsKeyboardOpen = false;
+ showActionBarIfNeeded();
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ return mEditorFragmentListener.onAuthHeaderRequested(url);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if ((requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD ||
+ requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_UPDATE)) {
+
+ if (resultCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_DELETE) {
+ mWebView.execJavaScriptFromString("ZSSEditor.unlink();");
+ return;
+ }
+
+ if (data == null) {
+ return;
+ }
+
+ Bundle extras = data.getExtras();
+ if (extras == null) {
+ return;
+ }
+
+ String linkUrl = extras.getString(LinkDialogFragment.LINK_DIALOG_ARG_URL);
+ String linkText = extras.getString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT);
+
+ if (linkText == null || linkText.equals("")) {
+ linkText = linkUrl;
+ }
+
+ if (TextUtils.isEmpty(Uri.parse(linkUrl).getScheme())) linkUrl = "http://" + linkUrl;
+
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ Editable content = mSourceViewContent.getText();
+ if (content == null) {
+ return;
+ }
+
+ if (mSelectionStart < mSelectionEnd) {
+ content.delete(mSelectionStart, mSelectionEnd);
+ }
+
+ String urlHtml = "<a href=\"" + linkUrl + "\">" + linkText + "</a>";
+
+ content.insert(mSelectionStart, urlHtml);
+ mSourceViewContent.setSelection(mSelectionStart + urlHtml.length());
+ } else {
+ String jsMethod;
+ if (requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD) {
+ jsMethod = "ZSSEditor.insertLink";
+ } else {
+ jsMethod = "ZSSEditor.updateLink";
+ }
+ mWebView.execJavaScriptFromString(jsMethod + "('" + Utils.escapeHtml(linkUrl) + "', '" +
+ Utils.escapeHtml(linkText) + "');");
+ }
+ } else if (requestCode == ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE) {
+ if (data == null) {
+ mWebView.execJavaScriptFromString("ZSSEditor.clearCurrentEditingImage();");
+ return;
+ }
+
+ Bundle extras = data.getExtras();
+ if (extras == null) {
+ return;
+ }
+
+ final String imageMeta = Utils.escapeQuotes(StringUtils.notNullStr(extras.getString("imageMeta")));
+ final int imageRemoteId = extras.getInt("imageRemoteId");
+ final boolean isFeaturedImage = extras.getBoolean("isFeatured");
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.updateCurrentImageMeta('" + imageMeta + "');");
+ }
+ });
+
+ if (imageRemoteId != 0) {
+ if (isFeaturedImage) {
+ mFeaturedImageId = imageRemoteId;
+ mEditorFragmentListener.onFeaturedImageChanged(mFeaturedImageId);
+ } else {
+ // If this image was unset as featured, clear the featured image id
+ if (mFeaturedImageId == imageRemoteId) {
+ mFeaturedImageId = 0;
+ mEditorFragmentListener.onFeaturedImageChanged(mFeaturedImageId);
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private void enableWebDebugging(boolean enable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ AppLog.i(T.EDITOR, "Enabling web debugging");
+ WebView.setWebContentsDebuggingEnabled(enable);
+ }
+ mWebView.setDebugModeEnabled(mDebugModeEnabled);
+ }
+
+ @Override
+ public void setTitle(CharSequence text) {
+ mTitle = text.toString();
+ }
+
+ @Override
+ public void setContent(CharSequence text) {
+ mContentHtml = text.toString();
+ }
+
+ /**
+ * Returns the contents of the title field from the JavaScript editor. Should be called from a background thread
+ * where possible.
+ */
+ @Override
+ public CharSequence getTitle() {
+ if (!isAdded()) {
+ return "";
+ }
+
+ if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) {
+ mTitle = mSourceViewTitle.getText().toString();
+ return StringUtils.notNullStr(mTitle);
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ AppLog.d(T.EDITOR, "getTitle() called from UI thread");
+ }
+
+ mGetTitleCountDownLatch = new CountDownLatch(1);
+
+ // All WebView methods must be called from the UI thread
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').getHTMLForCallback();");
+ }
+ });
+
+ try {
+ mGetTitleCountDownLatch.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ AppLog.e(T.EDITOR, e);
+ Thread.currentThread().interrupt();
+ }
+
+ return StringUtils.notNullStr(mTitle.replaceAll("&nbsp;$", ""));
+ }
+
+ /**
+ * Returns the contents of the content field from the JavaScript editor. Should be called from a background thread
+ * where possible.
+ */
+ @Override
+ public CharSequence getContent() {
+ if (!isAdded()) {
+ return "";
+ }
+
+ if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) {
+ mContentHtml = mSourceViewContent.getText().toString();
+ return StringUtils.notNullStr(mContentHtml);
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ AppLog.d(T.EDITOR, "getContent() called from UI thread");
+ }
+
+ mGetContentCountDownLatch = new CountDownLatch(1);
+
+ // All WebView methods must be called from the UI thread
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').getHTMLForCallback();");
+ }
+ });
+
+ try {
+ mGetContentCountDownLatch.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ AppLog.e(T.EDITOR, e);
+ Thread.currentThread().interrupt();
+ }
+
+ return StringUtils.notNullStr(mContentHtml);
+ }
+
+ @Override
+ public void appendMediaFile(final MediaFile mediaFile, final String mediaUrl, ImageLoader imageLoader) {
+ if (!mDomHasLoaded) {
+ // If the DOM hasn't loaded yet, we won't be able to add media to the ZSSEditor
+ // Place them in a queue to be handled when the DOM loaded callback is received
+ mWaitingMediaFiles.put(mediaUrl, mediaFile);
+ return;
+ }
+
+ final String safeMediaUrl = Utils.escapeQuotes(mediaUrl);
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ if (URLUtil.isNetworkUrl(mediaUrl)) {
+ String mediaId = mediaFile.getMediaId();
+ if (mediaFile.isVideo()) {
+ String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL()));
+ String videoPressId = ShortcodeUtils.getVideoPressIdFromShortCode(
+ mediaFile.getVideoPressShortCode());
+
+ mWebView.execJavaScriptFromString("ZSSEditor.insertVideo('" + safeMediaUrl + "', '" +
+ posterUrl + "', '" + videoPressId + "');");
+ } else {
+ mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + safeMediaUrl + "', '" + mediaId +
+ "');");
+ }
+ mActionStartedAt = System.currentTimeMillis();
+ } else {
+ String id = mediaFile.getMediaId();
+ if (mediaFile.isVideo()) {
+ String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL()));
+ mWebView.execJavaScriptFromString("ZSSEditor.insertLocalVideo(" + id + ", '" + posterUrl +
+ "');");
+ mUploadingMedia.put(id, MediaType.VIDEO);
+ } else {
+ mWebView.execJavaScriptFromString("ZSSEditor.insertLocalImage(" + id + ", '" + safeMediaUrl +
+ "');");
+ mUploadingMedia.put(id, MediaType.IMAGE);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void appendGallery(MediaGallery mediaGallery) {
+ if (!mDomHasLoaded) {
+ // If the DOM hasn't loaded yet, we won't be able to add a gallery to the ZSSEditor
+ // Place it in a queue to be handled when the DOM loaded callback is received
+ mWaitingGalleries.add(mediaGallery);
+ return;
+ }
+
+ if (mediaGallery.getIds().isEmpty()) {
+ mUploadingMediaGallery = mediaGallery;
+ mWebView.execJavaScriptFromString("ZSSEditor.insertLocalGallery('" + mediaGallery.getUniqueId() + "');");
+ } else {
+ // Ensure that the content field is in focus (it may not be if we're adding a gallery to a new post by a
+ // share action and not via the format bar button)
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();");
+
+ mWebView.execJavaScriptFromString("ZSSEditor.insertGallery('" + mediaGallery.getIdsStr() + "', '" +
+ mediaGallery.getType() + "', " + mediaGallery.getNumColumns() + ");");
+ }
+ }
+
+ @Override
+ public void setUrlForVideoPressId(final String videoId, final String videoUrl, final String posterUrl) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.setVideoPressLinks('" + videoId + "', '" +
+ Utils.escapeQuotes(videoUrl) + "', '" + Utils.escapeQuotes(posterUrl) + "');");
+ }
+ });
+ }
+
+ @Override
+ public boolean isUploadingMedia() {
+ return (mUploadingMedia.size() > 0);
+ }
+
+ @Override
+ public boolean hasFailedMediaUploads() {
+ return (mFailedMediaIds.size() > 0);
+ }
+
+ @Override
+ public void removeAllFailedMediaUploads() {
+ mWebView.execJavaScriptFromString("ZSSEditor.removeAllFailedMediaUploads();");
+ }
+
+ @Override
+ public Spanned getSpannedContent() {
+ return null;
+ }
+
+ @Override
+ public void setTitlePlaceholder(CharSequence placeholderText) {
+ mTitlePlaceholder = placeholderText.toString();
+ }
+
+ @Override
+ public void setContentPlaceholder(CharSequence placeholderText) {
+ mContentPlaceholder = placeholderText.toString();
+ }
+
+ @Override
+ public void onMediaUploadSucceeded(final String localMediaId, final MediaFile mediaFile) {
+ final MediaType mediaType = mUploadingMedia.get(localMediaId);
+ if (mediaType != null) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ String remoteUrl = Utils.escapeQuotes(mediaFile.getFileURL());
+ if (mediaType.equals(MediaType.IMAGE)) {
+ String remoteMediaId = mediaFile.getMediaId();
+ mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalImageWithRemoteImage(" + localMediaId +
+ ", '" + remoteMediaId + "', '" + remoteUrl + "');");
+ } else if (mediaType.equals(MediaType.VIDEO)) {
+ String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL()));
+ String videoPressId = ShortcodeUtils.getVideoPressIdFromShortCode(
+ mediaFile.getVideoPressShortCode());
+ mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalVideoWithRemoteVideo(" + localMediaId +
+ ", '" + remoteUrl + "', '" + posterUrl + "', '" + videoPressId + "');");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onMediaUploadProgress(final String mediaId, final float progress) {
+ final MediaType mediaType = mUploadingMedia.get(mediaId);
+ if (mediaType != null) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ String progressString = String.format(Locale.US, "%.1f", progress);
+ mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnMedia(" + mediaId + ", " +
+ progressString + ");");
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onMediaUploadFailed(final String mediaId, final String errorMessage) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ MediaType mediaType = mUploadingMedia.get(mediaId);
+ if (mediaType != null) {
+ switch (mediaType) {
+ case IMAGE:
+ mWebView.execJavaScriptFromString("ZSSEditor.markImageUploadFailed(" + mediaId + ", '"
+ + Utils.escapeQuotes(errorMessage) + "');");
+ break;
+ case VIDEO:
+ mWebView.execJavaScriptFromString("ZSSEditor.markVideoUploadFailed(" + mediaId + ", '"
+ + Utils.escapeQuotes(errorMessage) + "');");
+ }
+ mFailedMediaIds.add(mediaId);
+ mUploadingMedia.remove(mediaId);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onGalleryMediaUploadSucceeded(final long galleryId, String remoteMediaId, int remaining) {
+ if (galleryId == mUploadingMediaGallery.getUniqueId()) {
+ ArrayList<String> mediaIds = mUploadingMediaGallery.getIds();
+ mediaIds.add(remoteMediaId);
+ mUploadingMediaGallery.setIds(mediaIds);
+
+ if (remaining == 0) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.replacePlaceholderGallery('" + galleryId + "', '" +
+ mUploadingMediaGallery.getIdsStr() + "', '" +
+ mUploadingMediaGallery.getType() + "', " +
+ mUploadingMediaGallery.getNumColumns() + ");");
+ }
+ });
+ }
+ }
+ }
+
+ public void onDomLoaded() {
+ ProfilingUtils.split("EditorFragment.onDomLoaded");
+
+ mWebView.post(new Runnable() {
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+
+ mDomHasLoaded = true;
+
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setMultiline('true');");
+
+ // Set title and content placeholder text
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').setPlaceholderText('" +
+ Utils.escapeQuotes(mTitlePlaceholder) + "');");
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setPlaceholderText('" +
+ Utils.escapeQuotes(mContentPlaceholder) + "');");
+
+ // Load title and content into ZSSEditor
+ updateVisualEditorFields();
+
+ // If there are images that are still in progress (because the editor exited before they completed),
+ // set them to failed, so the user can restart them (otherwise they will stay stuck in 'uploading' mode)
+ mWebView.execJavaScriptFromString("ZSSEditor.markAllUploadingMediaAsFailed('"
+ + Utils.escapeQuotes(getString(R.string.tap_to_try_again)) + "');");
+
+ // Update the list of failed media uploads
+ mWebView.execJavaScriptFromString("ZSSEditor.getFailedMedia();");
+
+ hideActionBarIfNeeded();
+
+ // Reset all format bar buttons (in case they remained active through activity re-creation)
+ ToggleButton htmlButton = (ToggleButton) getActivity().findViewById(R.id.format_bar_button_html);
+ htmlButton.setChecked(false);
+ for (ToggleButton button : mTagToggleButtonMap.values()) {
+ button.setChecked(false);
+ }
+
+ boolean editorHasFocus = false;
+
+ // Add any media files that were placed in a queue due to the DOM not having loaded yet
+ if (mWaitingMediaFiles.size() > 0) {
+ // Image insertion will only work if the content field is in focus
+ // (for a new post, no field is in focus until user action)
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();");
+ editorHasFocus = true;
+
+ for (Map.Entry<String, MediaFile> entry : mWaitingMediaFiles.entrySet()) {
+ appendMediaFile(entry.getValue(), entry.getKey(), null);
+ }
+ mWaitingMediaFiles.clear();
+ }
+
+ // Add any galleries that were placed in a queue due to the DOM not having loaded yet
+ if (mWaitingGalleries.size() > 0) {
+ // Gallery insertion will only work if the content field is in focus
+ // (for a new post, no field is in focus until user action)
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();");
+ editorHasFocus = true;
+
+ for (MediaGallery mediaGallery : mWaitingGalleries) {
+ appendGallery(mediaGallery);
+ }
+
+ mWaitingGalleries.clear();
+ }
+
+ if (!editorHasFocus) {
+ mWebView.execJavaScriptFromString("ZSSEditor.focusFirstEditableField();");
+ }
+
+ // Show the keyboard
+ ((InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE))
+ .showSoftInput(mWebView, InputMethodManager.SHOW_IMPLICIT);
+
+ ProfilingUtils.split("EditorFragment.onDomLoaded completed");
+ ProfilingUtils.dump();
+ ProfilingUtils.stop();
+ }
+ });
+ }
+
+ public void onSelectionStyleChanged(final Map<String, Boolean> changeMap) {
+ mWebView.post(new Runnable() {
+ public void run() {
+ for (Map.Entry<String, Boolean> entry : changeMap.entrySet()) {
+ // Handle toggling format bar style buttons
+ ToggleButton button = mTagToggleButtonMap.get(entry.getKey());
+ if (button != null) {
+ button.setChecked(entry.getValue());
+ }
+ }
+ }
+ });
+ }
+
+ public void onSelectionChanged(final Map<String, String> selectionArgs) {
+ mFocusedFieldId = selectionArgs.get("id"); // The field now in focus
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!mFocusedFieldId.isEmpty()) {
+ switch (mFocusedFieldId) {
+ case "zss_field_title":
+ updateFormatBarEnabledState(false);
+ break;
+ case "zss_field_content":
+ updateFormatBarEnabledState(true);
+ break;
+ }
+ }
+ }
+ });
+ }
+
+ public void onMediaTapped(final String mediaId, final MediaType mediaType, final JSONObject meta, String uploadStatus) {
+ if (mediaType == null || !isAdded()) {
+ return;
+ }
+
+ switch (uploadStatus) {
+ case "uploading":
+ // Display 'cancel upload' dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.stop_upload_dialog_title));
+ builder.setPositiveButton(R.string.stop_upload_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true);
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ switch (mediaType) {
+ case IMAGE:
+ mWebView.execJavaScriptFromString("ZSSEditor.removeImage(" + mediaId + ");");
+ break;
+ case VIDEO:
+ mWebView.execJavaScriptFromString("ZSSEditor.removeVideo(" + mediaId + ");");
+ }
+ mUploadingMedia.remove(mediaId);
+ }
+ });
+ dialog.dismiss();
+ }
+ });
+
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ break;
+ case "failed":
+ // Retry media upload
+ mEditorFragmentListener.onMediaRetryClicked(mediaId);
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ switch (mediaType) {
+ case IMAGE:
+ mWebView.execJavaScriptFromString("ZSSEditor.unmarkImageUploadFailed(" + mediaId
+ + ");");
+ break;
+ case VIDEO:
+ mWebView.execJavaScriptFromString("ZSSEditor.unmarkVideoUploadFailed(" + mediaId
+ + ");");
+ }
+ mFailedMediaIds.remove(mediaId);
+ mUploadingMedia.put(mediaId, mediaType);
+ }
+ });
+ break;
+ default:
+ if (!mediaType.equals(MediaType.IMAGE)) {
+ return;
+ }
+
+ // Only show image options fragment for image taps
+ FragmentManager fragmentManager = getFragmentManager();
+
+ if (fragmentManager.findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) != null) {
+ return;
+ }
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.IMAGE_EDITED);
+ ImageSettingsDialogFragment imageSettingsDialogFragment = new ImageSettingsDialogFragment();
+ imageSettingsDialogFragment.setTargetFragment(this,
+ ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE);
+
+ Bundle dialogBundle = new Bundle();
+
+ dialogBundle.putString("maxWidth", mBlogSettingMaxImageWidth);
+ dialogBundle.putBoolean("featuredImageSupported", mFeaturedImageSupported);
+
+ // Request and add an authorization header for HTTPS images
+ // Use https:// when requesting the auth header, in case the image is incorrectly using http://.
+ // If an auth header is returned, force https:// for the actual HTTP request.
+ HashMap<String, String> headerMap = new HashMap<>();
+ if (mCustomHttpHeaders != null) {
+ headerMap.putAll(mCustomHttpHeaders);
+ }
+
+ try {
+ final String imageSrc = meta.getString("src");
+ String authHeader = mEditorFragmentListener.onAuthHeaderRequested(UrlUtils.makeHttps(imageSrc));
+ if (authHeader.length() > 0) {
+ meta.put("src", UrlUtils.makeHttps(imageSrc));
+ headerMap.put("Authorization", authHeader);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.EDITOR, "Could not retrieve image url from JSON metadata");
+ }
+ dialogBundle.putSerializable("headerMap", headerMap);
+
+ dialogBundle.putString("imageMeta", meta.toString());
+
+ String imageId = JSONUtils.getString(meta, "attachment_id");
+ if (!imageId.isEmpty()) {
+ dialogBundle.putBoolean("isFeatured", mFeaturedImageId == Integer.parseInt(imageId));
+ }
+
+ imageSettingsDialogFragment.setArguments(dialogBundle);
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
+
+ fragmentTransaction.add(android.R.id.content, imageSettingsDialogFragment,
+ ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG)
+ .addToBackStack(null)
+ .commit();
+
+ mWebView.notifyVisibilityChanged(false);
+ break;
+ }
+ }
+
+ public void onLinkTapped(String url, String title) {
+ LinkDialogFragment linkDialogFragment = new LinkDialogFragment();
+ linkDialogFragment.setTargetFragment(this, LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_UPDATE);
+
+ Bundle dialogBundle = new Bundle();
+
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_URL, url);
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, title);
+
+ linkDialogFragment.setArguments(dialogBundle);
+ linkDialogFragment.show(getFragmentManager(), "LinkDialogFragment");
+ }
+
+ @Override
+ public void onMediaRemoved(String mediaId) {
+ mUploadingMedia.remove(mediaId);
+ mFailedMediaIds.remove(mediaId);
+ mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true);
+ }
+
+ @Override
+ public void onMediaReplaced(String mediaId) {
+ mUploadingMedia.remove(mediaId);
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(final String videoId) {
+ mEditorFragmentListener.onVideoPressInfoRequested(videoId);
+ }
+
+ public void onGetHtmlResponse(Map<String, String> inputArgs) {
+ String functionId = inputArgs.get("function");
+
+ if (functionId.isEmpty()) {
+ return;
+ }
+
+ switch (functionId) {
+ case "getHTMLForCallback":
+ String fieldId = inputArgs.get("id");
+ String fieldContents = inputArgs.get("contents");
+ if (!fieldId.isEmpty()) {
+ switch (fieldId) {
+ case "zss_field_title":
+ mTitle = fieldContents;
+ mGetTitleCountDownLatch.countDown();
+ break;
+ case "zss_field_content":
+ mContentHtml = fieldContents;
+ mGetContentCountDownLatch.countDown();
+ break;
+ }
+ }
+ break;
+ case "getSelectedTextToLinkify":
+ mJavaScriptResult = inputArgs.get("result");
+ mGetSelectedTextCountDownLatch.countDown();
+ break;
+ case "getFailedMedia":
+ String[] mediaIds = inputArgs.get("ids").split(",");
+ for (String mediaId : mediaIds) {
+ if (!mediaId.equals("")) {
+ mFailedMediaIds.add(mediaId);
+ }
+ }
+ }
+ }
+
+ public void setWebViewErrorListener(ErrorListener errorListener) {
+ mWebView.setErrorListener(errorListener);
+ }
+
+ private void updateVisualEditorFields() {
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').setPlainText('" +
+ Utils.escapeHtml(mTitle) + "');");
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setHTML('" +
+ Utils.escapeHtml(mContentHtml) + "');");
+ }
+
+ /**
+ * Hide the action bar if needed.
+ */
+ private void hideActionBarIfNeeded() {
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null
+ && !isHardwareKeyboardPresent()
+ && mHideActionBarOnSoftKeyboardUp
+ && mIsKeyboardOpen
+ && actionBar.isShowing()) {
+ getActionBar().hide();
+ }
+ }
+
+ /**
+ * Show the action bar if needed.
+ */
+ private void showActionBarIfNeeded() {
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null && !actionBar.isShowing()) {
+ actionBar.show();
+ }
+ }
+
+ /**
+ * Returns true if a hardware keyboard is detected, otherwise false.
+ */
+ private boolean isHardwareKeyboardPresent() {
+ Configuration config = getResources().getConfiguration();
+ boolean returnValue = false;
+ if (config.keyboard != Configuration.KEYBOARD_NOKEYS) {
+ returnValue = true;
+ }
+ return returnValue;
+ }
+
+ void updateFormatBarEnabledState(boolean enabled) {
+ float alpha = (enabled ? TOOLBAR_ALPHA_ENABLED : TOOLBAR_ALPHA_DISABLED);
+ for(ToggleButton button : mTagToggleButtonMap.values()) {
+ button.setEnabled(enabled);
+ button.setAlpha(alpha);
+ }
+
+ mIsFormatBarDisabled = !enabled;
+ }
+
+ private void clearFormatBarButtons() {
+ for (ToggleButton button : mTagToggleButtonMap.values()) {
+ if (button != null) {
+ button.setChecked(false);
+ }
+ }
+ }
+
+ private void onFormattingButtonClicked(ToggleButton toggleButton) {
+ String tag = toggleButton.getTag().toString();
+ buttonTappedListener(toggleButton);
+ if (mWebView.getVisibility() == View.VISIBLE) {
+ mWebView.execJavaScriptFromString("ZSSEditor.set" + StringUtils.capitalize(tag) + "();");
+ } else {
+ applyFormattingHtmlMode(toggleButton, tag);
+ }
+ }
+
+ private void buttonTappedListener(ToggleButton toggleButton) {
+ int id = toggleButton.getId();
+ if (id == R.id.format_bar_button_bold) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_italic) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_ol) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.OL_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_ul) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.UL_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_quote) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_strikethrough) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED);
+ }
+ }
+
+ /**
+ * In HTML mode, applies formatting to selected text, or inserts formatting tag at current cursor position
+ * @param toggleButton format bar button which was clicked
+ * @param tag identifier tag
+ */
+ private void applyFormattingHtmlMode(ToggleButton toggleButton, String tag) {
+ if (mSourceViewContent == null) {
+ return;
+ }
+
+ // Replace style tags with their proper HTML tags
+ String htmlTag;
+ if (tag.equals(getString(R.string.format_bar_tag_bold))) {
+ htmlTag = "b";
+ } else if (tag.equals(getString(R.string.format_bar_tag_italic))) {
+ htmlTag = "i";
+ } else if (tag.equals(getString(R.string.format_bar_tag_strikethrough))) {
+ htmlTag = "del";
+ } else if (tag.equals(getString(R.string.format_bar_tag_unorderedList))) {
+ htmlTag = "ul";
+ } else if (tag.equals(getString(R.string.format_bar_tag_orderedList))) {
+ htmlTag = "ol";
+ } else {
+ htmlTag = tag;
+ }
+
+ int selectionStart = mSourceViewContent.getSelectionStart();
+ int selectionEnd = mSourceViewContent.getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ boolean textIsSelected = selectionEnd > selectionStart;
+
+ String startTag = "<" + htmlTag + ">";
+ String endTag = "</" + htmlTag + ">";
+
+ // Add li tags together with ul and ol tags
+ if (htmlTag.equals("ul") || htmlTag.equals("ol")) {
+ startTag = startTag + "\n\t<li>";
+ endTag = "</li>\n" + endTag;
+ }
+
+ Editable content = mSourceViewContent.getText();
+ if (textIsSelected) {
+ // Surround selected text with opening and closing tags
+ content.insert(selectionStart, startTag);
+ content.insert(selectionEnd + startTag.length(), endTag);
+ toggleButton.setChecked(false);
+ mSourceViewContent.setSelection(selectionEnd + startTag.length() + endTag.length());
+ } else if (toggleButton.isChecked()) {
+ // Insert opening tag
+ content.insert(selectionStart, startTag);
+ mSourceViewContent.setSelection(selectionEnd + startTag.length());
+ } else {
+ // Insert closing tag
+ content.insert(selectionEnd, endTag);
+ mSourceViewContent.setSelection(selectionEnd + endTag.length());
+ }
+ }
+
+ @Override
+ public void onActionFinished() {
+ mActionStartedAt = -1;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java
new file mode 100644
index 000000000..ba15d036a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java
@@ -0,0 +1,180 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.view.DragEvent;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public abstract class EditorFragmentAbstract extends Fragment {
+ public abstract void setTitle(CharSequence text);
+ public abstract void setContent(CharSequence text);
+ public abstract CharSequence getTitle();
+ public abstract CharSequence getContent();
+ public abstract void appendMediaFile(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader);
+ public abstract void appendGallery(MediaGallery mediaGallery);
+ public abstract void setUrlForVideoPressId(String videoPressId, String url, String posterUrl);
+ public abstract boolean isUploadingMedia();
+ public abstract boolean isActionInProgress();
+ public abstract boolean hasFailedMediaUploads();
+ public abstract void removeAllFailedMediaUploads();
+ public abstract void setTitlePlaceholder(CharSequence text);
+ public abstract void setContentPlaceholder(CharSequence text);
+
+ // TODO: remove this as soon as we can (we'll need to drop the legacy editor or fix html2spanned translation)
+ public abstract Spanned getSpannedContent();
+
+ public enum MediaType {
+ IMAGE, VIDEO;
+
+ public static MediaType fromString(String value) {
+ if (value != null) {
+ for (MediaType mediaType : MediaType.values()) {
+ if (value.equalsIgnoreCase(mediaType.toString())) {
+ return mediaType;
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ private static final String FEATURED_IMAGE_SUPPORT_KEY = "featured-image-supported";
+ private static final String FEATURED_IMAGE_WIDTH_KEY = "featured-image-width";
+
+ protected EditorFragmentListener mEditorFragmentListener;
+ protected EditorDragAndDropListener mEditorDragAndDropListener;
+ protected boolean mFeaturedImageSupported;
+ protected long mFeaturedImageId;
+ protected String mBlogSettingMaxImageWidth;
+ protected ImageLoader mImageLoader;
+ protected boolean mDebugModeEnabled;
+
+ protected HashMap<String, String> mCustomHttpHeaders;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mEditorFragmentListener = (EditorFragmentListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement EditorFragmentListener");
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putBoolean(FEATURED_IMAGE_SUPPORT_KEY, mFeaturedImageSupported);
+ outState.putString(FEATURED_IMAGE_WIDTH_KEY, mBlogSettingMaxImageWidth);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(FEATURED_IMAGE_SUPPORT_KEY)) {
+ mFeaturedImageSupported = savedInstanceState.getBoolean(FEATURED_IMAGE_SUPPORT_KEY);
+ }
+ if (savedInstanceState.containsKey(FEATURED_IMAGE_WIDTH_KEY)) {
+ mBlogSettingMaxImageWidth = savedInstanceState.getString(FEATURED_IMAGE_WIDTH_KEY);
+ }
+ }
+ }
+
+ public void setImageLoader(ImageLoader imageLoader) {
+ mImageLoader = imageLoader;
+ }
+
+ public void setFeaturedImageSupported(boolean featuredImageSupported) {
+ mFeaturedImageSupported = featuredImageSupported;
+ }
+
+ public void setBlogSettingMaxImageWidth(String blogSettingMaxImageWidth) {
+ mBlogSettingMaxImageWidth = blogSettingMaxImageWidth;
+ }
+
+ public void setFeaturedImageId(long featuredImageId) {
+ mFeaturedImageId = featuredImageId;
+ }
+
+ public void setCustomHttpHeader(String name, String value) {
+ if (mCustomHttpHeaders == null) {
+ mCustomHttpHeaders = new HashMap<>();
+ }
+
+ mCustomHttpHeaders.put(name, value);
+ }
+
+ public void setDebugModeEnabled(boolean debugModeEnabled) {
+ mDebugModeEnabled = debugModeEnabled;
+ }
+
+ /**
+ * Called by the activity when back button is pressed.
+ */
+ public boolean onBackPressed() {
+ return false;
+ }
+
+ /**
+ * The editor may need to differentiate local draft and published articles
+ *
+ * @param isLocalDraft edited post is a local draft
+ */
+ public void setLocalDraft(boolean isLocalDraft) {
+ // Not unused in the new editor
+ }
+
+ /**
+ * Callbacks used to communicate with the parent Activity
+ */
+ public interface EditorFragmentListener {
+ void onEditorFragmentInitialized();
+ void onSettingsClicked();
+ void onAddMediaClicked();
+ void onMediaRetryClicked(String mediaId);
+ void onMediaUploadCancelClicked(String mediaId, boolean delete);
+ void onFeaturedImageChanged(long mediaId);
+ void onVideoPressInfoRequested(String videoId);
+ String onAuthHeaderRequested(String url);
+ // TODO: remove saveMediaFile, it's currently needed for the legacy editor
+ void saveMediaFile(MediaFile mediaFile);
+ void onTrackableEvent(TrackableEvent event);
+ }
+
+ /**
+ * Callbacks for drag and drop support
+ */
+ public interface EditorDragAndDropListener {
+ void onMediaDropped(ArrayList<Uri> mediaUri);
+ void onRequestDragAndDropPermissions(DragEvent dragEvent);
+ }
+
+ public enum TrackableEvent {
+ HTML_BUTTON_TAPPED,
+ UNLINK_BUTTON_TAPPED,
+ LINK_BUTTON_TAPPED,
+ MEDIA_BUTTON_TAPPED,
+ IMAGE_EDITED,
+ BOLD_BUTTON_TAPPED,
+ ITALIC_BUTTON_TAPPED,
+ OL_BUTTON_TAPPED,
+ UL_BUTTON_TAPPED,
+ BLOCKQUOTE_BUTTON_TAPPED,
+ STRIKETHROUGH_BUTTON_TAPPED,
+ UNDERLINE_BUTTON_TAPPED,
+ MORE_BUTTON_TAPPED
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java
new file mode 100644
index 000000000..26ba44150
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java
@@ -0,0 +1,10 @@
+package org.wordpress.android.editor;
+
+import org.wordpress.android.util.helpers.MediaFile;
+
+public interface EditorMediaUploadListener {
+ void onMediaUploadSucceeded(String localId, MediaFile mediaFile);
+ void onMediaUploadProgress(String localId, float progress);
+ void onMediaUploadFailed(String localId, String errorMessage);
+ void onGalleryMediaUploadSucceeded(long galleryId, String remoteId, int remaining);
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java
new file mode 100644
index 000000000..f613eb5d5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import org.wordpress.android.util.AppLog;
+
+public class EditorWebView extends EditorWebViewAbstract {
+
+ public EditorWebView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @SuppressLint("NewApi")
+ public void execJavaScriptFromString(String javaScript) {
+ this.evaluateJavascript(javaScript, null);
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean shouldSwitchToCompatibilityMode() {
+ if (Build.VERSION.SDK_INT <= 19) {
+ try {
+ this.evaluateJavascript("", null);
+ } catch (NoSuchMethodError | IllegalStateException e) {
+ AppLog.d(AppLog.T.EDITOR,
+ "Detected 4.4 ROM using classic WebView, reverting to compatibility EditorWebView.");
+ return true;
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java
new file mode 100644
index 000000000..64ded937d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java
@@ -0,0 +1,256 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.webkit.ConsoleMessage;
+import android.webkit.ConsoleMessage.MessageLevel;
+import android.webkit.JsResult;
+import android.webkit.URLUtil;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.HTTPUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A text editor WebView with support for JavaScript execution.
+ */
+public abstract class EditorWebViewAbstract extends WebView {
+ public abstract void execJavaScriptFromString(String javaScript);
+
+ private OnImeBackListener mOnImeBackListener;
+ private AuthHeaderRequestListener mAuthHeaderRequestListener;
+ private ErrorListener mErrorListener;
+ private JsCallbackReceiver mJsCallbackReceiver;
+ private boolean mDebugModeEnabled;
+
+ private Map<String, String> mHeaderMap = new HashMap<>();
+
+ public EditorWebViewAbstract(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ configureWebView();
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private void configureWebView() {
+ WebSettings webSettings = this.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ webSettings.setDefaultTextEncodingName("utf-8");
+
+ this.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (url != null && url.startsWith("callback") && mJsCallbackReceiver != null) {
+ String data = URLDecoder.decode(url);
+ String[] split = data.split(":", 2);
+ String callbackId = split[0];
+ String params = (split.length > 1 ? split[1] : "");
+ mJsCallbackReceiver.executeCallback(callbackId, params);
+ }
+ return true;
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ AppLog.e(T.EDITOR, description);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+ String url = request.getUrl().toString();
+
+ if (!URLUtil.isNetworkUrl(url)) {
+ return super.shouldInterceptRequest(view, request);
+ }
+
+ // Request and add an authorization header for HTTPS resource requests.
+ // Use https:// when requesting the auth header, in case the resource is incorrectly using http://.
+ // If an auth header is returned, force https:// for the actual HTTP request.
+ String authHeader = mAuthHeaderRequestListener.onAuthHeaderRequested(UrlUtils.makeHttps(url));
+ if (StringUtils.notNullStr(authHeader).length() > 0) {
+ try {
+ url = UrlUtils.makeHttps(url);
+
+ // Keep any existing request headers from the WebResourceRequest
+ Map<String, String> headerMap = request.getRequestHeaders();
+ for (Map.Entry<String, String> entry : mHeaderMap.entrySet()) {
+ headerMap.put(entry.getKey(), entry.getValue());
+ }
+ headerMap.put("Authorization", authHeader);
+
+ HttpURLConnection conn = HTTPUtils.setupUrlConnection(url, headerMap);
+ return new WebResourceResponse(conn.getContentType(), conn.getContentEncoding(),
+ conn.getInputStream());
+ } catch (IOException e) {
+ AppLog.e(T.EDITOR, e);
+ }
+ }
+
+ return super.shouldInterceptRequest(view, request);
+ }
+
+ /**
+ * Compatibility method for API < 21
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ if (!URLUtil.isNetworkUrl(url)) {
+ return super.shouldInterceptRequest(view, url);
+ }
+
+ // Request and add an authorization header for HTTPS resource requests.
+ // Use https:// when requesting the auth header, in case the resource is incorrectly using http://.
+ // If an auth header is returned, force https:// for the actual HTTP request.
+ String authHeader = mAuthHeaderRequestListener.onAuthHeaderRequested(UrlUtils.makeHttps(url));
+ if (StringUtils.notNullStr(authHeader).length() > 0) {
+ try {
+ url = UrlUtils.makeHttps(url);
+
+ Map<String, String> headerMap = new HashMap<>(mHeaderMap);
+ headerMap.put("Authorization", authHeader);
+
+ HttpURLConnection conn = HTTPUtils.setupUrlConnection(url, headerMap);
+ return new WebResourceResponse(conn.getContentType(), conn.getContentEncoding(),
+ conn.getInputStream());
+ } catch (IOException e) {
+ AppLog.e(T.EDITOR, e);
+ }
+ }
+
+ return super.shouldInterceptRequest(view, url);
+ }
+ });
+
+ this.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public boolean onConsoleMessage(@NonNull ConsoleMessage cm) {
+ if (cm.messageLevel() == MessageLevel.ERROR) {
+ if (mErrorListener != null) {
+ mErrorListener.onJavaScriptError(cm.sourceId(), cm.lineNumber(), cm.message());
+ }
+ AppLog.e(T.EDITOR, cm.message() + " -- From line " + cm.lineNumber() + " of " + cm.sourceId());
+ } else {
+ AppLog.d(T.EDITOR, cm.message() + " -- From line " + cm.lineNumber() + " of " + cm.sourceId());
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
+ AppLog.d(T.EDITOR, message);
+ if (mErrorListener != null) {
+ mErrorListener.onJavaScriptAlert(url, message);
+ }
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ return true;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ notifyVisibilityChanged(visibility == View.VISIBLE);
+ super.setVisibility(visibility);
+ }
+
+
+ public boolean shouldSwitchToCompatibilityMode() {
+ return false;
+ }
+
+ public void setDebugModeEnabled(boolean enabled) {
+ mDebugModeEnabled = enabled;
+ }
+
+ /**
+ * Handles events that should be triggered when the WebView is hidden or is shown to the user
+ *
+ * @param visible the new visibility status of the WebView
+ */
+ public void notifyVisibilityChanged(boolean visible) {
+ if (!visible) {
+ this.post(new Runnable() {
+ @Override
+ public void run() {
+ execJavaScriptFromString("ZSSEditor.pauseAllVideos();");
+ }
+ });
+ }
+ }
+
+ public void setOnImeBackListener(OnImeBackListener listener) {
+ mOnImeBackListener = listener;
+ }
+
+ public void setAuthHeaderRequestListener(AuthHeaderRequestListener listener) {
+ mAuthHeaderRequestListener = listener;
+ }
+
+ /**
+ * Used on API<17 to handle callbacks as a safe alternative to JavascriptInterface (which has security risks
+ * at those API levels).
+ */
+ public void setJsCallbackReceiver(JsCallbackReceiver jsCallbackReceiver) {
+ mJsCallbackReceiver = jsCallbackReceiver;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (mOnImeBackListener != null) {
+ mOnImeBackListener.onImeBack();
+ }
+ }
+ if (mDebugModeEnabled && event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP
+ && event.getAction() == KeyEvent.ACTION_DOWN) {
+ // Log the raw html
+ execJavaScriptFromString("console.log(document.body.innerHTML);");
+ ToastUtils.showToast(getContext(), "Debug: Raw HTML has been logged");
+ return true;
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ public void setCustomHeader(String name, String value) {
+ mHeaderMap.put(name, value);
+ }
+
+ public void setErrorListener(ErrorListener errorListener) {
+ mErrorListener = errorListener;
+ }
+
+ public interface AuthHeaderRequestListener {
+ String onAuthHeaderRequested(String url);
+ }
+
+ public interface ErrorListener {
+ void onJavaScriptError(String sourceFile, int lineNumber, String message);
+ void onJavaScriptAlert(String url, String message);
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java
new file mode 100644
index 000000000..72d431a21
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java
@@ -0,0 +1,130 @@
+package org.wordpress.android.editor;
+
+import android.content.Context;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.webkit.WebView;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * <p>Compatibility <code>EditorWebView</code> for pre-Chromium WebView (API<19). Provides a custom method for executing
+ * JavaScript, {@link #loadJavaScript(String)}, instead of {@link WebView#loadUrl(String)}. This is needed because
+ * <code>WebView#loadUrl(String)</code> on API<19 eventually calls <code>WebViewClassic#hideSoftKeyboard()</code>,
+ * hiding the keyboard whenever JavaScript is executed.</p>
+ * <p/>
+ * <p>This class uses reflection to access the normally inaccessible <code>WebViewCore#sendMessage(Message)</code>
+ * and use it to execute JavaScript, sidestepping <code>WebView#loadUrl(String)</code> and the keyboard issue.</p>
+ */
+@SuppressWarnings("TryWithIdenticalCatches")
+public class EditorWebViewCompatibility extends EditorWebViewAbstract {
+ public interface ReflectionFailureListener {
+ void onReflectionFailure(ReflectionException e);
+ }
+
+ public class ReflectionException extends Exception {
+ public ReflectionException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ private static final int EXECUTE_JS = 194; // WebViewCore internal JS message code
+
+ private Object mWebViewCore;
+ private Method mSendMessageMethod;
+
+ // Dirty static listener, but it's impossible to set the listener during the construction if we want to keep
+ // the xml layout
+ private static ReflectionFailureListener mReflectionFailureListener;
+ private boolean mReflectionSucceed = true;
+
+ public static void setReflectionFailureListener(ReflectionFailureListener reflectionFailureListener) {
+ mReflectionFailureListener = reflectionFailureListener;
+ }
+
+ public EditorWebViewCompatibility(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ try {
+ this.initReflection();
+ } catch (ReflectionException e) {
+ AppLog.e(T.EDITOR, e);
+ handleReflectionFailure(e);
+ }
+ }
+
+ private void initReflection() throws ReflectionException {
+ if (!mReflectionSucceed) {
+ // Reflection failed once already, it won't succeed on a second try
+ return;
+ }
+ Object webViewProvider;
+ try {
+ // On API >= 16, the WebViewCore instance is not defined inside WebView itself but inside a
+ // WebViewClassic (implementation of WebViewProvider), referenced from the WebView as mProvider
+
+ // Access WebViewClassic object
+ Field webViewProviderField = WebView.class.getDeclaredField("mProvider");
+ webViewProviderField.setAccessible(true);
+ webViewProvider = webViewProviderField.get(this);
+
+ // Access WebViewCore object
+ Field webViewCoreField = webViewProvider.getClass().getDeclaredField("mWebViewCore");
+ webViewCoreField.setAccessible(true);
+ mWebViewCore = webViewCoreField.get(webViewProvider);
+
+ // Access WebViewCore#sendMessage(Message) method
+ if (mWebViewCore != null) {
+ mSendMessageMethod = mWebViewCore.getClass().getDeclaredMethod("sendMessage", Message.class);
+ mSendMessageMethod.setAccessible(true);
+ }
+ } catch (NoSuchFieldException e) {
+ throw new ReflectionException(e);
+ } catch (NoSuchMethodException e) {
+ throw new ReflectionException(e);
+ } catch (IllegalAccessException e) {
+ throw new ReflectionException(e);
+ }
+ }
+
+ private void loadJavaScript(final String javaScript) throws ReflectionException {
+ if (mSendMessageMethod == null) {
+ initReflection();
+ } else {
+ Message jsMessage = Message.obtain(null, EXECUTE_JS, javaScript);
+ try {
+ mSendMessageMethod.invoke(mWebViewCore, jsMessage);
+ } catch (InvocationTargetException e) {
+ throw new ReflectionException(e);
+ } catch (IllegalAccessException e) {
+ throw new ReflectionException(e);
+ }
+ }
+ }
+
+ public void execJavaScriptFromString(String javaScript) {
+ try {
+ loadJavaScript(javaScript);
+ } catch (ReflectionException e) {
+ AppLog.e(T.EDITOR, e);
+ handleReflectionFailure(e);
+ }
+ }
+
+ private void handleReflectionFailure(ReflectionException e) {
+ if (mReflectionFailureListener != null) {
+ mReflectionFailureListener.onReflectionFailure(e);
+ }
+ mReflectionSucceed = false;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mReflectionFailureListener = null;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java
new file mode 100644
index 000000000..90ffc7fe0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java
@@ -0,0 +1,245 @@
+package org.wordpress.android.editor;
+
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.TextWatcher;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class HtmlStyleTextWatcher implements TextWatcher {
+ private enum Operation {
+ INSERT, DELETE, REPLACE, NONE
+ }
+
+ private int mOffset;
+ private CharSequence mModifiedText;
+ private Operation mLastOperation;
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ if (s == null) {
+ return;
+ }
+
+ int lastCharacterLocation = start + count - 1;
+ if (s.length() > lastCharacterLocation && lastCharacterLocation >= 0) {
+ if (after < count) {
+ if (after > 0) {
+ // Text was deleted and replaced by some other text
+ mLastOperation = Operation.REPLACE;
+ } else {
+ // Text was deleted only
+ mLastOperation = Operation.DELETE;
+ }
+
+ mOffset = start;
+ mModifiedText = s.subSequence(start + after, start + count);
+ }
+ }
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (s == null) {
+ return;
+ }
+
+ int lastCharacterLocation = start + count - 1;
+ if (s.length() > lastCharacterLocation) {
+ if (count > 0) {
+ if (before > 0) {
+ // Text was added, replacing some existing text
+ mLastOperation = Operation.REPLACE;
+ mModifiedText = s.subSequence(start, start + count);
+ } else {
+ // Text was added only
+ mLastOperation = Operation.INSERT;
+ mOffset = start;
+ mModifiedText = s.subSequence(start + before, start + count);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mModifiedText == null || s == null) {
+ return;
+ }
+
+ SpanRange spanRange;
+
+ // If the modified text included a tag or entity symbol ("<", ">", "&" or ";"), find its match and restyle
+ if (mModifiedText.toString().contains("<")) {
+ spanRange = getRespanRangeForChangedOpeningSymbol(s, "<");
+ } else if (mModifiedText.toString().contains(">")) {
+ spanRange = getRespanRangeForChangedClosingSymbol(s, ">");
+ } else if (mModifiedText.toString().contains("&")) {
+ spanRange = getRespanRangeForChangedOpeningSymbol(s, "&");
+ } else if (mModifiedText.toString().contains(";")) {
+ spanRange = getRespanRangeForChangedClosingSymbol(s, ";");
+ } else {
+ // If the modified text didn't include any tag or entity symbols, restyle if the modified text is inside
+ // a tag or entity
+ spanRange = getRespanRangeForNormalText(s, "<");
+ if (spanRange == null) {
+ spanRange = getRespanRangeForNormalText(s, "&");
+ }
+ }
+
+ if (spanRange != null) {
+ updateSpans(s, spanRange);
+ }
+
+ mModifiedText = null;
+ mLastOperation = Operation.NONE;
+ }
+
+ /**
+ * For changes made which contain at least one opening symbol (e.g. '<' or '&'), whether added or deleted, returns
+ * the range of text which should have its style reapplied.
+ * @param content the content after modification
+ * @param openingSymbol the opening symbol recognized (e.g. '<' or '&')
+ * @return the range of characters to re-apply spans to
+ */
+ protected SpanRange getRespanRangeForChangedOpeningSymbol(Editable content, String openingSymbol) {
+ // For simplicity, re-parse the document if text was replaced
+ if (mLastOperation == Operation.REPLACE) {
+ return new SpanRange(0, content.length());
+ }
+
+ String closingSymbol = getMatchingSymbol(openingSymbol);
+
+ int firstOpeningTagLoc = mOffset + mModifiedText.toString().indexOf(openingSymbol);
+ int closingTagLoc;
+ if (mLastOperation == Operation.INSERT) {
+ // Apply span from the first added opening symbol until the closing symbol in the content matching the
+ // last added opening symbol
+ // e.g. pasting "<b><" before "/b>" - we want the span to be applied to all of "<b></b>"
+ int lastOpeningTagLoc = mOffset + mModifiedText.toString().lastIndexOf(openingSymbol);
+ closingTagLoc = content.toString().indexOf(closingSymbol, lastOpeningTagLoc);
+ } else {
+ // Apply span until the first closing tag that appears after the deleted text
+ closingTagLoc = content.toString().indexOf(closingSymbol, mOffset);
+ }
+
+ if (closingTagLoc > 0) {
+ return new SpanRange(firstOpeningTagLoc, closingTagLoc + 1);
+ }
+ return null;
+ }
+
+ /**
+ * For changes made which contain at least one closing symbol (e.g. '>' or ';') and no opening symbols, whether
+ * added or deleted, returns the range of text which should have its style reapplied.
+ * @param content the content after modification
+ * @param closingSymbol the closing symbol recognized (e.g. '>' or ';')
+ * @return the range of characters to re-apply spans to
+ */
+ protected SpanRange getRespanRangeForChangedClosingSymbol(Editable content, String closingSymbol) {
+ // For simplicity, re-parse the document if text was replaced
+ if (mLastOperation == Operation.REPLACE) {
+ return new SpanRange(0, content.length());
+ }
+
+ String openingSymbol = getMatchingSymbol(closingSymbol);
+
+ int firstClosingTagInModLoc = mOffset + mModifiedText.toString().indexOf(closingSymbol);
+ int firstClosingTagAfterModLoc = content.toString().indexOf(closingSymbol, mOffset + mModifiedText.length());
+
+ int openingTagLoc = content.toString().lastIndexOf(openingSymbol, firstClosingTagInModLoc - 1);
+ if (openingTagLoc >= 0) {
+ if (firstClosingTagAfterModLoc >= 0) {
+ return new SpanRange(openingTagLoc, firstClosingTagAfterModLoc + 1);
+ } else {
+ return new SpanRange(openingTagLoc, content.length());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * For changes made which contain no opening or closing symbols, checks whether the changed text is inside a tag,
+ * and if so returns the range of text which should have its style reapplied.
+ * @param content the content after modification
+ * @param openingSymbol the opening symbol of the tag to check for (e.g. '<' or '&')
+ * @return the range of characters to re-apply spans to
+ */
+ protected SpanRange getRespanRangeForNormalText(Editable content, String openingSymbol) {
+ String closingSymbol = getMatchingSymbol(openingSymbol);
+
+ int openingTagLoc = content.toString().lastIndexOf(openingSymbol, mOffset);
+ if (openingTagLoc >= 0) {
+ int closingTagLoc = content.toString().indexOf(closingSymbol, openingTagLoc);
+ if (closingTagLoc >= mOffset) {
+ return new SpanRange(openingTagLoc, closingTagLoc + 1);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Clears and re-applies spans to {@code content} within range {@code spanRange} according to rules in
+ * {@link HtmlStyleUtils}.
+ * @param content the content to re-style
+ * @param spanRange the range within {@code content} to be re-styled
+ */
+ protected void updateSpans(Spannable content, SpanRange spanRange) {
+ int spanStart = spanRange.getOpeningTagLoc();
+ int spanEnd = spanRange.getClosingTagLoc();
+
+ if (spanStart > content.length() || spanEnd > content.length()) {
+ AppLog.d(T.EDITOR, "The specified span range was beyond the Spannable's length");
+ return;
+ } else if (spanStart >= spanEnd) {
+ // If the span start is after the end position (probably due to a multi-line deletion), selective
+ // re-styling won't work
+ // Instead, do a clean re-styling of the whole document
+ spanStart = 0;
+ spanEnd = content.length();
+ }
+
+ HtmlStyleUtils.clearSpans(content, spanStart, spanEnd);
+ HtmlStyleUtils.styleHtmlForDisplay(content, spanStart, spanEnd);
+ }
+
+ /**
+ * Returns the closing/opening symbol corresponding to the given opening/closing symbol.
+ */
+ private String getMatchingSymbol(String symbol) {
+ switch(symbol) {
+ case "<":
+ return ">";
+ case ">":
+ return "<";
+ case "&":
+ return ";";
+ case ";":
+ return "&";
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Stores a pair of integers describing a range of values.
+ */
+ protected static class SpanRange {
+ private final int mOpeningTagLoc;
+ private final int mClosingTagLoc;
+
+ public SpanRange(int openingTagLoc, int closingTagLoc) {
+ mOpeningTagLoc = openingTagLoc;
+ mClosingTagLoc = closingTagLoc;
+ }
+
+ public int getOpeningTagLoc() {
+ return mOpeningTagLoc;
+ }
+
+ public int getClosingTagLoc() {
+ return mClosingTagLoc;
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java
new file mode 100644
index 000000000..912781f1f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java
@@ -0,0 +1,150 @@
+package org.wordpress.android.editor;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.text.Spannable;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+
+import org.wordpress.android.util.AppLog;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HtmlStyleUtils {
+ public static final int TAG_COLOR = Color.rgb(0, 80, 130);
+ public static final int ATTRIBUTE_COLOR = Color.rgb(158, 158, 158);
+
+ public static final String REGEX_HTML_TAGS = "(<\\/?[a-z][^<>]*>)";
+ public static final String REGEX_HTML_ATTRIBUTES = "(?<==)('|\")(.*?\\1)(?=.*?>)";
+ public static final String REGEX_HTML_COMMENTS = "(<!--.*?-->)";
+ public static final String REGEX_HTML_ENTITIES = "(&#34;|&#38;|&#39;|&#60;|&#62;|&#160;|&#161;|&#162;|&#163;" +
+ "|&#164;|&#165;|&#166;|&#167;|&#168;|&#169;|&#170;|&#171;|&#172;|&#173;|&#174;|&#175;|&#176;|&#177;" +
+ "|&#178;|&#179;|&#180;|&#181;|&#182;|&#183;|&#184;|&#185;|&#186;|&#187;|&#188;|&#189;|&#190;|&#191;" +
+ "|&#192;|&#193;|&#194;|&#195;|&#196;|&#197;|&#198;|&#199;|&#200;|&#201;|&#202;|&#203;|&#204;|&#205;" +
+ "|&#206;|&#207;|&#208;|&#209;|&#210;|&#211;|&#212;|&#213;|&#214;|&#215;|&#216;|&#217;|&#218;|&#219;" +
+ "|&#220;|&#221;|&#222;|&#223;|&#224;|&#225;|&#226;|&#227;|&#228;|&#229;|&#230;|&#231;|&#232;|&#233;" +
+ "|&#234;|&#235;|&#236;|&#237;|&#238;|&#239;|&#240;|&#241;|&#242;|&#243;|&#244;|&#245;|&#246;|&#247;" +
+ "|&#248;|&#249;|&#250;|&#251;|&#252;|&#253;|&#254;|&#255;|&#338;|&#339;|&#352;|&#353;|&#376;|&#402;" +
+ "|&#710;|&#732;|&#913;|&#914;|&#915;|&#916;|&#917;|&#918;|&#919;|&#920;|&#921;|&#922;|&#923;|&#924;" +
+ "|&#925;|&#926;|&#927;|&#928;|&#929;|&#931;|&#932;|&#933;|&#934;|&#935;|&#936;|&#937;|&#945;|&#946;" +
+ "|&#947;|&#948;|&#949;|&#950;|&#951;|&#952;|&#953;|&#954;|&#955;|&#956;|&#957;|&#958;|&#959;|&#960;" +
+ "|&#961;|&#962;|&#963;|&#964;|&#965;|&#966;|&#967;|&#968;|&#969;|&#977;|&#978;|&#982;|&#8194;|&#8195;" +
+ "|&#8201;|&#8204;|&#8205;|&#8206;|&#8207;|&#8211;|&#8212;|&#8216;|&#8217;|&#8218;|&#8220;|&#8221;|&#8222;" +
+ "|&#8224;|&#8225;|&#8226;|&#8230;|&#8240;|&#8242;|&#8243;|&#8249;|&#8250;|&#8254;|&#8260;|&#8364;|&#8465;" +
+ "|&#8472;|&#8476;|&#8482;|&#8501;|&#8592;|&#8593;|&#8594;|&#8595;|&#8596;|&#8629;|&#8656;|&#8657;|&#8658;" +
+ "|&#8659;|&#8660;|&#8704;|&#8706;|&#8707;|&#8709;|&#8711;|&#8712;|&#8713;|&#8715;|&#8719;|&#8721;|&#8722;" +
+ "|&#8727;|&#8730;|&#8733;|&#8734;|&#8736;|&#8743;|&#8744;|&#8745;|&#8746;|&#8747;|&#8756;|&#8764;|&#8773;" +
+ "|&#8776;|&#8800;|&#8801;|&#8804;|&#8805;|&#8834;|&#8835;|&#8836;|&#8838;|&#8839;|&#8853;|&#8855;|&#8869;" +
+ "|&#8901;|&#8968;|&#8969;|&#8970;|&#8971;|&#9001;|&#9002;|&#9674;|&#9824;|&#9827;|&#9829;|&#9830;|&quot;" +
+ "|&amp;|&apos;|&lt;|&gt;|&nbsp;|&iexcl;|&cent;|&pound;|&curren;|&yen;|&brvbar;|&sect;|&uml;|&copy;|&ordf;" +
+ "|&laquo;|&not;|&shy;|&reg;|&macr;|&deg;|&plusmn;|&sup2;|&sup3;|&acute;|&micro;|&para;|&middot;|&cedil;" +
+ "|&sup1;|&ordm;|&raquo;|&frac14;|&frac12;|&frac34;|&iquest;|&Agrave;|&Aacute;|&Acirc;|&Atilde;|&Auml;" +
+ "|&Aring;|&AElig;|&Ccedil;|&Egrave;|&Eacute;|&Ecirc;|&Euml;|&Igrave;|&Iacute;|&Icirc;|&Iuml;|&ETH;" +
+ "|&Ntilde;|&Ograve;|&Oacute;|&Ocirc;|&Otilde;|&Ouml;|&times;|&Oslash;|&Ugrave;|&Uacute;|&Ucirc;|&Uuml;" +
+ "|&Yacute;|&THORN;|&szlig;|&agrave;|&aacute;|&acirc;|&atilde;|&auml;|&aring;|&aelig;|&ccedil;|&egrave;" +
+ "|&eacute;|&ecirc;|&euml;|&igrave;|&iacute;|&icirc;|&iuml;|&eth;|&ntilde;|&ograve;|&oacute;|&ocirc;" +
+ "|&otilde;|&ouml;|&divide;|&oslash;|&Ugrave;|&Uacute;|&Ucirc;|&Uuml;|&yacute;|&thorn;|&yuml;|&OElig;" +
+ "|&oelig;|&Scaron;|&scaron;|&Yuml;|&fnof;|&circ;|&tilde;|&Alpha;|&Beta;|&Gamma;|&Delta;|&Epsilon;|&Zeta;" +
+ "|&Eta;|&Theta;|&Iota;|&Kappa;|&Lambda;|&Mu;|&Nu;|&Xi;|&Omicron;|&Pi;|&Rho;|&Sigma;|&Tau;|&Upsilon;|&Phi;" +
+ "|&Chi;|&Psi;|&Omega;|&alpha;|&beta;|&gamma;|&delta;|&epsilon;|&zeta;|&eta;|&theta;|&iota;|&kappa;" +
+ "|&lambda;|&mu;|&nu;|&xi;|&omicron;|&pi;|&rho;|&sigmaf;|&sigma;|&tau;|&upsilon;|&phi;|&chi;|&psi;|&omega;" +
+ "|&thetasym;|&Upsih;|&piv;|&ensp;|&emsp;|&thinsp;|&zwnj;|&zwj;|&lrm;|&rlm;|&ndash;|&mdash;|&lsquo;" +
+ "|&rsquo;|&sbquo;|&ldquo;|&rdquo;|&bdquo;|&dagger;|&Dagger;|&bull;|&hellip;|&permil;|&prime;|&Prime;" +
+ "|&lsaquo;|&rsaquo;|&oline;|&frasl;|&euro;|&image;|&weierp;|&real;|&trade;|&alefsym;|&larr;|&uarr;|&rarr;" +
+ "|&darr;|&harr;|&crarr;|&lArr;|&UArr;|&rArr;|&dArr;|&hArr;|&forall;|&part;|&exist;|&empty;|&nabla;|&isin;" +
+ "|&notin;|&ni;|&prod;|&sum;|&minus;|&lowast;|&radic;|&prop;|&infin;|&ang;|&and;|&or;|&cap;|&cup;|&int;" +
+ "|&there4;|&sim;|&cong;|&asymp;|&ne;|&equiv;|&le;|&ge;|&sub;|&sup;|&nsub;|&sube;|&supe;|&oplus;|&otimes;" +
+ "|&perp;|&sdot;|&lceil;|&rceil;|&lfloor;|&rfloor;|&lang;|&rang;|&loz;|&spades;|&clubs;|&hearts;|&diams;)";
+
+ public static final int SPANNABLE_FLAGS = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
+
+ /**
+ * Apply styling rules to {@code content}.
+ */
+ public static void styleHtmlForDisplay(@NonNull Spannable content) {
+ styleHtmlForDisplay(content, 0, content.length());
+ }
+
+ /**
+ * Apply styling rules to {@code content} inside the range from {@code start} to {@code end}.
+ *
+ * @param content the Spannable to apply style rules to
+ * @param start the index in {@code content} to start styling from
+ * @param end the index in {@code content} to style until
+ */
+ public static void styleHtmlForDisplay(@NonNull Spannable content, int start, int end) {
+ if (Build.VERSION.RELEASE.equals("4.1") || Build.VERSION.RELEASE.equals("4.1.1")) {
+ // Avoids crashing bug in Android 4.1 and 4.1.1 triggered when spanned text is line-wrapped
+ // AOSP issue: https://code.google.com/p/android/issues/detail?id=35466
+ return;
+ }
+
+ applySpansByRegex(content, start, end, REGEX_HTML_TAGS);
+ applySpansByRegex(content, start, end, REGEX_HTML_ATTRIBUTES);
+ applySpansByRegex(content, start, end, REGEX_HTML_COMMENTS);
+ applySpansByRegex(content, start, end, REGEX_HTML_ENTITIES);
+ }
+
+ /**
+ * Applies styles to {@code content} from {@code start} to {@code end}, based on rule {@code regex}.
+ * @param content the Spannable to apply style rules to
+ * @param start the index in {@code content} to start styling from
+ * @param end the index in {@code content} to style until
+ * @param regex the pattern to match for styling
+ */
+ private static void applySpansByRegex(Spannable content, int start, int end, String regex) {
+ if (content == null || start < 0 || end < 0 || start > content.length() || end > content.length() ||
+ start >= end) {
+ AppLog.d(AppLog.T.EDITOR, "applySpansByRegex() received invalid input");
+ return;
+ }
+
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(content.subSequence(start, end));
+
+ while (matcher.find()) {
+ int matchStart = matcher.start() + start;
+ int matchEnd = matcher.end() + start;
+ switch(regex) {
+ case REGEX_HTML_TAGS:
+ content.setSpan(new ForegroundColorSpan(TAG_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ case REGEX_HTML_ATTRIBUTES:
+ content.setSpan(new ForegroundColorSpan(ATTRIBUTE_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ case REGEX_HTML_COMMENTS:
+ content.setSpan(new ForegroundColorSpan(ATTRIBUTE_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new StyleSpan(Typeface.ITALIC), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new RelativeSizeSpan(0.75f), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ case REGEX_HTML_ENTITIES:
+ content.setSpan(new ForegroundColorSpan(TAG_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new StyleSpan(Typeface.BOLD), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new RelativeSizeSpan(0.75f), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Clears all relevant spans in {@code content} from {@code start} to {@code end}. Relevant spans are the subclasses
+ * of {@link CharacterStyle} applied by {@link HtmlStyleUtils#applySpansByRegex(Spannable, int, int, String)}.
+ * @param content the Spannable to clear styles from
+ * @param spanStart the index in {@code content} to start clearing styles from
+ * @param spanEnd the index in {@code content} to clear styles until
+ */
+ public static void clearSpans(Spannable content, int spanStart, int spanEnd) {
+ CharacterStyle[] spans = content.getSpans(spanStart, spanEnd, CharacterStyle.class);
+
+ for (CharacterStyle span : spans) {
+ if (span instanceof ForegroundColorSpan || span instanceof StyleSpan || span instanceof RelativeSizeSpan) {
+ content.removeSpan(span);
+ }
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java
new file mode 100644
index 000000000..70a995b01
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java
@@ -0,0 +1,431 @@
+package org.wordpress.android.editor;
+
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * A full-screen DialogFragment with image settings.
+ *
+ * Modifies the action bar - host activity must call {@link ImageSettingsDialogFragment#dismissFragment()}
+ * when the fragment is dismissed to restore it.
+ */
+public class ImageSettingsDialogFragment extends DialogFragment {
+ public static final int IMAGE_SETTINGS_DIALOG_REQUEST_CODE = 5;
+ public static final String IMAGE_SETTINGS_DIALOG_TAG = "image-settings";
+
+ private JSONObject mImageMeta;
+ private int mMaxImageWidth;
+
+ private EditText mTitleText;
+ private EditText mCaptionText;
+ private EditText mAltText;
+ private Spinner mAlignmentSpinner;
+ private String[] mAlignmentKeyArray;
+ private EditText mLinkTo;
+ private EditText mWidthText;
+ private CheckBox mFeaturedCheckBox;
+
+ private boolean mIsFeatured;
+
+ private Map<String, String> mHttpHeaders;
+
+ private CharSequence mPreviousActionBarTitle;
+ private boolean mPreviousHomeAsUpEnabled;
+ private View mPreviousCustomView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar == null) {
+ return;
+ }
+
+ actionBar.show();
+
+ mPreviousActionBarTitle = actionBar.getTitle();
+ mPreviousCustomView = actionBar.getCustomView();
+
+ final int displayOptions = actionBar.getDisplayOptions();
+ mPreviousHomeAsUpEnabled = (displayOptions & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+
+ actionBar.setTitle(R.string.image_settings);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ if (getResources().getBoolean(R.bool.show_extra_side_padding)) {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_padded);
+ } else {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ }
+
+ // Show custom view with padded Save button
+ actionBar.setDisplayShowCustomEnabled(true);
+ actionBar.setCustomView(R.layout.image_settings_formatbar);
+
+ actionBar.getCustomView().findViewById(R.id.menu_save).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mImageMeta = extractMetaDataFromFields(mImageMeta);
+
+ String imageRemoteId = "";
+ try {
+ imageRemoteId = mImageMeta.getString("attachment_id");
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.EDITOR, "Unable to retrieve featured image id from meta data");
+ }
+
+ Intent intent = new Intent();
+ intent.putExtra("imageMeta", mImageMeta.toString());
+
+ mIsFeatured = mFeaturedCheckBox.isChecked();
+ intent.putExtra("isFeatured", mIsFeatured);
+
+ if (!imageRemoteId.isEmpty()) {
+ intent.putExtra("imageRemoteId", Integer.parseInt(imageRemoteId));
+ }
+
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), intent);
+
+ restorePreviousActionBar();
+ getFragmentManager().popBackStack();
+ ToastUtils.showToast(getActivity(), R.string.image_settings_save_toast);
+ }
+ });
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_image_options, container, false);
+
+ ImageView thumbnailImage = (ImageView) view.findViewById(R.id.image_thumbnail);
+ TextView filenameLabel = (TextView) view.findViewById(R.id.image_filename);
+ mTitleText = (EditText) view.findViewById(R.id.image_title);
+ mCaptionText = (EditText) view.findViewById(R.id.image_caption);
+ mAltText = (EditText) view.findViewById(R.id.image_alt_text);
+ mAlignmentSpinner = (Spinner) view.findViewById(R.id.alignment_spinner);
+ mLinkTo = (EditText) view.findViewById(R.id.image_link_to);
+ SeekBar widthSeekBar = (SeekBar) view.findViewById(R.id.image_width_seekbar);
+ mWidthText = (EditText) view.findViewById(R.id.image_width_text);
+ mFeaturedCheckBox = (CheckBox) view.findViewById(R.id.featuredImage);
+
+ // Populate the dialog with existing values
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ try {
+ mImageMeta = new JSONObject(bundle.getString("imageMeta"));
+
+ mHttpHeaders = (Map) bundle.getSerializable("headerMap");
+
+ final String imageSrc = mImageMeta.getString("src");
+ final String imageFilename = imageSrc.substring(imageSrc.lastIndexOf("/") + 1);
+
+ loadThumbnail(imageSrc, thumbnailImage);
+ filenameLabel.setText(imageFilename);
+
+ mTitleText.setText(mImageMeta.getString("title"));
+ mCaptionText.setText(mImageMeta.getString("caption"));
+ mAltText.setText(mImageMeta.getString("alt"));
+
+ String alignment = mImageMeta.getString("align");
+ mAlignmentKeyArray = getResources().getStringArray(R.array.alignment_key_array);
+ int alignmentIndex = Arrays.asList(mAlignmentKeyArray).indexOf(alignment);
+ mAlignmentSpinner.setSelection(alignmentIndex == -1 ? 0 : alignmentIndex);
+
+ mLinkTo.setText(mImageMeta.getString("linkUrl"));
+
+ mMaxImageWidth = MediaUtils.getMaximumImageWidth(mImageMeta.getInt("naturalWidth"),
+ bundle.getString("maxWidth"));
+
+ setupWidthSeekBar(widthSeekBar, mWidthText, mImageMeta.getInt("width"));
+
+ boolean featuredImageSupported = bundle.getBoolean("featuredImageSupported");
+ if (featuredImageSupported) {
+ mFeaturedCheckBox.setVisibility(View.VISIBLE);
+ mIsFeatured = bundle.getBoolean("isFeatured", false);
+ mFeaturedCheckBox.setChecked(mIsFeatured);
+ }
+ } catch (JSONException e1) {
+ AppLog.d(AppLog.T.EDITOR, "Missing JSON properties");
+ }
+ }
+
+ mTitleText.requestFocus();
+
+ return view;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (menu != null) {
+ menu.clear();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == android.R.id.home) {
+ dismissFragment();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private ActionBar getActionBar() {
+ if (getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * To be called when the fragment is being dismissed, either by ActionBar navigation or by pressing back in the
+ * navigation bar.
+ * Displays a confirmation dialog if there are unsaved changes, otherwise undoes the fragment's modifications to
+ * the ActionBar and restores the last visible fragment.
+ */
+ public void dismissFragment() {
+ try {
+ JSONObject newImageMeta = extractMetaDataFromFields(new JSONObject());
+
+ for (int i = 0; i < newImageMeta.names().length(); i++) {
+ String name = newImageMeta.names().getString(i);
+ if (!newImageMeta.getString(name).equals(mImageMeta.getString(name))) {
+ showDiscardChangesDialog();
+ return;
+ }
+ }
+
+ if (mFeaturedCheckBox.isChecked() != mIsFeatured) {
+ // Featured image status has changed
+ showDiscardChangesDialog();
+ return;
+ }
+ } catch (JSONException e) {
+ AppLog.d(AppLog.T.EDITOR, "Unable to update JSON array");
+ }
+
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), null);
+ restorePreviousActionBar();
+ getFragmentManager().popBackStack();
+ }
+
+ private void restorePreviousActionBar() {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(mPreviousActionBarTitle);
+ actionBar.setHomeAsUpIndicator(null);
+ actionBar.setDisplayHomeAsUpEnabled(mPreviousHomeAsUpEnabled);
+
+ actionBar.setCustomView(mPreviousCustomView);
+ if (mPreviousCustomView == null) {
+ actionBar.setDisplayShowCustomEnabled(false);
+ }
+ }
+ }
+
+ /**
+ * Displays a dialog asking the user to confirm that they want to exit, discarding unsaved changes.
+ */
+ private void showDiscardChangesDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.image_settings_dismiss_dialog_title));
+ builder.setPositiveButton(getString(R.string.discard), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), null);
+ restorePreviousActionBar();
+ getFragmentManager().popBackStack();
+ }
+ });
+
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ /**
+ * Extracts the meta data from the dialog fields and updates the entries in the given JSONObject.
+ */
+ private JSONObject extractMetaDataFromFields(JSONObject metaData) {
+ try {
+ metaData.put("title", mTitleText.getText().toString());
+ metaData.put("caption", mCaptionText.getText().toString());
+ metaData.put("alt", mAltText.getText().toString());
+ if (mAlignmentSpinner.getSelectedItemPosition() < mAlignmentKeyArray.length) {
+ metaData.put("align", mAlignmentKeyArray[mAlignmentSpinner.getSelectedItemPosition()]);
+ }
+ metaData.put("linkUrl", mLinkTo.getText().toString());
+
+ int newWidth = getEditTextIntegerClamped(mWidthText, 10, mMaxImageWidth);
+ metaData.put("width", newWidth);
+ metaData.put("height", getRelativeHeightFromWidth(newWidth));
+ } catch (JSONException e) {
+ AppLog.d(AppLog.T.EDITOR, "Unable to build JSON object from new meta data");
+ }
+
+ return metaData;
+ }
+
+ private void loadThumbnail(final String src, final ImageView thumbnailImage) {
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (isAdded()) {
+ final Uri localUri = Utils.downloadExternalMedia(getActivity(), Uri.parse(src), mHttpHeaders);
+
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ thumbnailImage.setImageURI(localUri);
+ }
+ });
+ }
+ }
+ }
+ });
+
+ thread.start();
+ }
+
+ /**
+ * Initialize the image width SeekBar and accompanying EditText
+ */
+ private void setupWidthSeekBar(final SeekBar widthSeekBar, final EditText widthText, int imageWidth) {
+ widthSeekBar.setMax(mMaxImageWidth / 10);
+
+ if (imageWidth != 0) {
+ widthSeekBar.setProgress(imageWidth / 10);
+ widthText.setText(String.valueOf(imageWidth) + "px");
+ }
+
+ widthSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (progress == 0) {
+ progress = 1;
+ }
+ widthText.setText(progress * 10 + "px");
+ }
+ });
+
+ widthText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ widthText.setText("");
+ }
+ }
+ });
+
+ widthText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ int width = getEditTextIntegerClamped(widthText, 10, mMaxImageWidth);
+ widthSeekBar.setProgress(width / 10);
+ widthText.setSelection((String.valueOf(width).length()));
+
+ InputMethodManager imm = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(widthText.getWindowToken(),
+ InputMethodManager.RESULT_UNCHANGED_SHOWN);
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Return the integer value of the width EditText, adjusted to be within the given min and max, and stripped of the
+ * 'px' units
+ */
+ private int getEditTextIntegerClamped(EditText editText, int minWidth, int maxWidth) {
+ int width = 10;
+
+ try {
+ if (editText.getText() != null)
+ width = Integer.parseInt(editText.getText().toString().replace("px", ""));
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.EDITOR, e);
+ }
+
+ width = Math.min(maxWidth, Math.max(width, minWidth));
+
+ return width;
+ }
+
+ /**
+ * Given the new width, return the proportionally adjusted height, given the dimensions of the original image
+ */
+ private int getRelativeHeightFromWidth(int width) {
+ int height = 0;
+
+ try {
+ int naturalHeight = mImageMeta.getInt("naturalHeight");
+ int naturalWidth = mImageMeta.getInt("naturalWidth");
+
+ float ratio = (float) naturalHeight / naturalWidth;
+ height = (int) (ratio * width);
+ } catch (JSONException e) {
+ AppLog.d(AppLog.T.EDITOR, "JSON object missing naturalHeight or naturalWidth property");
+ }
+
+ return height;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java
new file mode 100755
index 000000000..b8212fcef
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java
@@ -0,0 +1,236 @@
+package org.wordpress.android.editor;
+
+import android.webkit.JavascriptInterface;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.wordpress.android.editor.EditorFragmentAbstract.MediaType;
+
+public class JsCallbackReceiver {
+ private static final String JS_CALLBACK_DELIMITER = "~";
+
+ private static final String CALLBACK_DOM_LOADED = "callback-dom-loaded";
+ private static final String CALLBACK_NEW_FIELD = "callback-new-field";
+
+ private static final String CALLBACK_INPUT = "callback-input";
+ private static final String CALLBACK_SELECTION_CHANGED = "callback-selection-changed";
+ private static final String CALLBACK_SELECTION_STYLE = "callback-selection-style";
+
+ private static final String CALLBACK_FOCUS_IN = "callback-focus-in";
+ private static final String CALLBACK_FOCUS_OUT = "callback-focus-out";
+
+ private static final String CALLBACK_IMAGE_REPLACED = "callback-image-replaced";
+ private static final String CALLBACK_VIDEO_REPLACED = "callback-video-replaced";
+ private static final String CALLBACK_IMAGE_TAP = "callback-image-tap";
+ private static final String CALLBACK_LINK_TAP = "callback-link-tap";
+ private static final String CALLBACK_MEDIA_REMOVED = "callback-media-removed";
+
+ private static final String CALLBACK_VIDEOPRESS_INFO_REQUEST = "callback-videopress-info-request";
+
+ private static final String CALLBACK_LOG = "callback-log";
+
+ private static final String CALLBACK_RESPONSE_STRING = "callback-response-string";
+
+ private static final String CALLBACK_ACTION_FINISHED = "callback-action-finished";
+
+ private final OnJsEditorStateChangedListener mListener;
+
+ private Set<String> mPreviousStyleSet = new HashSet<>();
+
+ public JsCallbackReceiver(EditorFragmentAbstract editorFragmentAbstract) {
+ mListener = (OnJsEditorStateChangedListener) editorFragmentAbstract;
+ }
+
+ @JavascriptInterface
+ public void executeCallback(String callbackId, String params) {
+ switch (callbackId) {
+ case CALLBACK_DOM_LOADED:
+ mListener.onDomLoaded();
+ break;
+ case CALLBACK_SELECTION_STYLE:
+ // Compare the new styles to the previous ones, and notify the JsCallbackListener of the changeset
+ Set<String> rawStyleSet = Utils.splitDelimitedString(params, JS_CALLBACK_DELIMITER);
+
+ // Strip link details from active style set
+ Set<String> newStyleSet = new HashSet<>();
+ for (String element : rawStyleSet) {
+ if (element.matches("link:(.*)")) {
+ newStyleSet.add("link");
+ } else if (!element.matches("link-title:(.*)")) {
+ newStyleSet.add(element);
+ }
+ }
+
+ mListener.onSelectionStyleChanged(Utils.getChangeMapFromSets(mPreviousStyleSet, newStyleSet));
+ mPreviousStyleSet = newStyleSet;
+ break;
+ case CALLBACK_SELECTION_CHANGED:
+ // Called for changes to the field in current focus and for changes made to selection
+ // (includes moving the caret without selecting text)
+ // TODO: Possibly needed for handling WebView scrolling when caret moves (from iOS)
+ Set<String> selectionKeyValueSet = Utils.splitDelimitedString(params, JS_CALLBACK_DELIMITER);
+ mListener.onSelectionChanged(Utils.buildMapFromKeyValuePairs(selectionKeyValueSet));
+ break;
+ case CALLBACK_INPUT:
+ // Called on key press
+ // TODO: Possibly needed for handling WebView scrolling when caret moves (from iOS)
+ break;
+ case CALLBACK_FOCUS_IN:
+ // TODO: Needed to handle displaying/graying the format bar when focus changes between the title and content
+ AppLog.d(AppLog.T.EDITOR, "Focus in callback received");
+ break;
+ case CALLBACK_FOCUS_OUT:
+ // TODO: Needed to handle displaying/graying the format bar when focus changes between the title and content
+ AppLog.d(AppLog.T.EDITOR, "Focus out callback received");
+ break;
+ case CALLBACK_NEW_FIELD:
+ // TODO: Used for logging/testing purposes on iOS
+ AppLog.d(AppLog.T.EDITOR, "New field created, " + params);
+ break;
+ case CALLBACK_IMAGE_REPLACED:
+ AppLog.d(AppLog.T.EDITOR, "Image replaced, " + params);
+
+ // Extract the local media id from the callback string (stripping the 'id=' part)
+ if (params.length() > 3) {
+ mListener.onMediaReplaced(params.substring(3));
+ }
+ break;
+ case CALLBACK_VIDEO_REPLACED:
+ AppLog.d(AppLog.T.EDITOR, "Video replaced, " + params);
+
+ // Extract the local media id from the callback string (stripping the 'id=' part)
+ if (params.length() > 3) {
+ mListener.onMediaReplaced(params.substring(3));
+ }
+ break;
+ case CALLBACK_IMAGE_TAP:
+ AppLog.d(AppLog.T.EDITOR, "Image tapped, " + params);
+
+ String uploadStatus = "";
+
+ List<String> mediaIds = new ArrayList<>();
+ mediaIds.add("id");
+ mediaIds.add("url");
+ mediaIds.add("meta");
+ mediaIds.add("type");
+
+ Set<String> mediaDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, mediaIds);
+ Map<String, String> mediaDataMap = Utils.buildMapFromKeyValuePairs(mediaDataSet);
+
+ String mediaId = mediaDataMap.get("id");
+
+ String mediaUrl = mediaDataMap.get("url");
+ if (mediaUrl != null) {
+ mediaUrl = Utils.decodeHtml(mediaUrl);
+ }
+
+ MediaType mediaType = MediaType.fromString(mediaDataMap.get("type"));
+
+ String mediaMeta = mediaDataMap.get("meta");
+ JSONObject mediaMetaJson = new JSONObject();
+
+ if (mediaMeta != null) {
+ mediaMeta = Utils.decodeHtml(mediaMeta);
+
+ try {
+ mediaMetaJson = new JSONObject(mediaMeta);
+ String classes = JSONUtils.getString(mediaMetaJson, "classes");
+ Set<String> classesSet = Utils.splitDelimitedString(classes, ", ");
+
+ if (classesSet.contains("uploading")) {
+ uploadStatus = "uploading";
+ } else if (classesSet.contains("failed")) {
+ uploadStatus = "failed";
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ AppLog.d(AppLog.T.EDITOR, "Media meta data from callback-image-tap was not JSON-formatted");
+ }
+ }
+
+ mListener.onMediaTapped(mediaId, mediaType, mediaMetaJson, uploadStatus);
+ break;
+ case CALLBACK_LINK_TAP:
+ // Extract and HTML-decode the link data from the callback params
+ AppLog.d(AppLog.T.EDITOR, "Link tapped, " + params);
+
+ List<String> linkIds = new ArrayList<>();
+ linkIds.add("url");
+ linkIds.add("title");
+
+ Set<String> linkDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, linkIds);
+ Map<String, String> linkDataMap = Utils.buildMapFromKeyValuePairs(linkDataSet);
+
+ String url = linkDataMap.get("url");
+ if (url != null) {
+ url = Utils.decodeHtml(url);
+ }
+
+ String title = linkDataMap.get("title");
+ if (title != null) {
+ title = Utils.decodeHtml(title);
+ }
+
+ mListener.onLinkTapped(url, title);
+ break;
+ case CALLBACK_MEDIA_REMOVED:
+ AppLog.d(AppLog.T.EDITOR, "Media removed, " + params);
+ // Extract the media id from the callback string (stripping the 'id=' part of the callback string)
+ if (params.length() > 3) {
+ mListener.onMediaRemoved(params.substring(3));
+ }
+ break;
+ case CALLBACK_VIDEOPRESS_INFO_REQUEST:
+ // Extract the VideoPress id from the callback string (stripping the 'id=' part of the callback string)
+ if (params.length() > 3) {
+ mListener.onVideoPressInfoRequested(params.substring(3));
+ }
+ break;
+ case CALLBACK_LOG:
+ // Strip 'msg=' from beginning of string
+ if (params.length() > 4) {
+ AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params.substring(4));
+ }
+ break;
+ case CALLBACK_RESPONSE_STRING:
+ AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params);
+ Set<String> responseDataSet;
+ if (params.startsWith("function=") && params.contains(JS_CALLBACK_DELIMITER)) {
+ String functionName = params.substring("function=".length(), params.indexOf(JS_CALLBACK_DELIMITER));
+
+ List<String> responseIds = new ArrayList<>();
+ switch (functionName) {
+ case "getHTMLForCallback":
+ responseIds.add("id");
+ responseIds.add("contents");
+ break;
+ case "getSelectedTextToLinkify":
+ responseIds.add("result");
+ break;
+ case "getFailedMedia":
+ responseIds.add("ids");
+ }
+
+ responseDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, responseIds);
+ } else {
+ responseDataSet = Utils.splitDelimitedString(params, JS_CALLBACK_DELIMITER);
+ }
+ mListener.onGetHtmlResponse(Utils.buildMapFromKeyValuePairs(responseDataSet));
+ break;
+ case CALLBACK_ACTION_FINISHED:
+ mListener.onActionFinished();
+ break;
+ default:
+ AppLog.d(AppLog.T.EDITOR, "Unhandled callback: " + callbackId + ":" + params);
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java
new file mode 100644
index 000000000..75febb3fa
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java
@@ -0,0 +1,1194 @@
+package org.wordpress.android.editor;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.style.AlignmentSpan;
+import android.text.style.QuoteSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.URLSpan;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.URLUtil;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.editor.legacy.EditLinkActivity;
+import org.wordpress.android.editor.legacy.WPEditImageSpan;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+import org.wordpress.android.util.helpers.MediaGalleryImageSpan;
+import org.wordpress.android.util.helpers.WPImageSpan;
+import org.wordpress.android.util.helpers.WPUnderlineSpan;
+import org.wordpress.android.util.widgets.WPEditText;
+
+import java.util.Locale;
+
+public class LegacyEditorFragment extends EditorFragmentAbstract implements TextWatcher,
+ WPEditText.OnSelectionChangedListener, View.OnTouchListener {
+ public static final int ACTIVITY_REQUEST_CODE_CREATE_LINK = 4;
+ public static final String ACTION_MEDIA_GALLERY_TOUCHED = "MEDIA_GALLERY_TOUCHED";
+ public static final String EXTRA_MEDIA_GALLERY = "EXTRA_MEDIA_GALLERY";
+
+ private static final int MIN_THUMBNAIL_WIDTH = 200;
+ private static final int CONTENT_ANIMATION_DURATION = 250;
+ private static final String KEY_IMAGE_SPANS = "image-spans";
+ private static final String KEY_START = "start";
+ private static final String KEY_END = "end";
+ private static final String KEY_CONTENT = "content";
+ private static final String TAG_FORMAT_BAR_BUTTON_STRONG = "strong";
+ private static final String TAG_FORMAT_BAR_BUTTON_EM = "em";
+ private static final String TAG_FORMAT_BAR_BUTTON_UNDERLINE = "u";
+ private static final String TAG_FORMAT_BAR_BUTTON_STRIKE = "strike";
+ private static final String TAG_FORMAT_BAR_BUTTON_QUOTE = "blockquote";
+
+ private View mRootView;
+ private WPEditText mContentEditText;
+ private EditText mTitleEditText;
+ private ToggleButton mBoldToggleButton, mEmToggleButton, mBquoteToggleButton;
+ private ToggleButton mUnderlineToggleButton, mStrikeToggleButton;
+ private LinearLayout mFormatBar, mPostContentLinearLayout, mPostSettingsLinearLayout;
+ private Button mAddPictureButton;
+ private boolean mIsBackspace;
+ private boolean mScrollDetected;
+ private boolean mIsLocalDraft;
+
+ private int mStyleStart, mSelectionStart, mSelectionEnd, mFullViewBottom;
+ private int mLastPosition = -1;
+ private CharSequence mTitle;
+ private CharSequence mContent;
+
+ private float mLastYPos = 0;
+
+ @Override
+ public boolean onBackPressed() {
+ // leave full screen mode back button is pressed
+ if (getActionBar() != null && !getActionBar().isShowing()) {
+ setContentEditingModeVisible(false);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ if (mTitleEditText != null) {
+ return mTitleEditText.getText().toString();
+ }
+ return mTitle;
+ }
+
+ @Override
+ public CharSequence getContent() {
+ if (mContentEditText != null) {
+ return mContentEditText.getText().toString();
+ }
+ return mContent;
+ }
+
+ @Override
+ public void setTitle(CharSequence text) {
+ mTitle = text;
+ if (mTitleEditText != null) {
+ mTitleEditText.setText(text);
+ } else {
+ // TODO
+ }
+ }
+
+ @Override
+ public void setContent(CharSequence text) {
+ mContent = text;
+ if (mContentEditText != null) {
+ mContentEditText.setText(text);
+ mContentEditText.setSelection(mSelectionStart, mSelectionEnd);
+ } else {
+ // TODO
+ }
+ }
+
+ @Override
+ public Spanned getSpannedContent() {
+ return mContentEditText.getText();
+ }
+
+ public void setLocalDraft(boolean isLocalDraft) {
+ mIsLocalDraft = isLocalDraft;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_edit_post_content, container, false);
+
+ mFormatBar = (LinearLayout) rootView.findViewById(R.id.format_bar);
+ mTitleEditText = (EditText) rootView.findViewById(R.id.post_title);
+ mTitleEditText.setText(mTitle);
+ mTitleEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ // Go to full screen editor when 'next' button is tapped on soft keyboard
+ ActionBar actionBar = getActionBar();
+ if (actionId == EditorInfo.IME_ACTION_NEXT && actionBar != null && actionBar.isShowing()) {
+ setContentEditingModeVisible(true);
+ }
+ return false;
+ }
+ });
+
+ mContentEditText = (WPEditText) rootView.findViewById(R.id.post_content);
+ mContentEditText.setText(mContent);
+
+ mPostContentLinearLayout = (LinearLayout) rootView.findViewById(R.id.post_content_wrapper);
+ mPostSettingsLinearLayout = (LinearLayout) rootView.findViewById(R.id.post_settings_wrapper);
+ Button postSettingsButton = (Button) rootView.findViewById(R.id.post_settings_button);
+ postSettingsButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mEditorFragmentListener.onSettingsClicked();
+ }
+ });
+ mBoldToggleButton = (ToggleButton) rootView.findViewById(R.id.bold);
+ mEmToggleButton = (ToggleButton) rootView.findViewById(R.id.em);
+ mBquoteToggleButton = (ToggleButton) rootView.findViewById(R.id.bquote);
+ mUnderlineToggleButton = (ToggleButton) rootView.findViewById(R.id.underline);
+ mStrikeToggleButton = (ToggleButton) rootView.findViewById(R.id.strike);
+ mAddPictureButton = (Button) rootView.findViewById(R.id.addPictureButton);
+ Button linkButton = (Button) rootView.findViewById(R.id.link);
+ Button moreButton = (Button) rootView.findViewById(R.id.more);
+
+ registerForContextMenu(mAddPictureButton);
+ mContentEditText = (WPEditText) rootView.findViewById(R.id.post_content);
+ mContentEditText.setOnSelectionChangedListener(this);
+ mContentEditText.setOnTouchListener(this);
+ mContentEditText.addTextChangedListener(this);
+ mContentEditText.setOnEditTextImeBackListener(new WPEditText.EditTextImeBackListener() {
+ @Override
+ public void onImeBack(WPEditText ctrl, String text) {
+ // Go back to regular editor if IME keyboard is dismissed
+ // Bottom comparison is there to ensure that the keyboard is actually showing
+ ActionBar actionBar = getActionBar();
+ if (mRootView.getBottom() < mFullViewBottom && actionBar != null && !actionBar.isShowing()) {
+ setContentEditingModeVisible(false);
+ }
+ }
+ });
+ mAddPictureButton.setOnClickListener(mFormatBarButtonClickListener);
+ mBoldToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ linkButton.setOnClickListener(mFormatBarButtonClickListener);
+ mEmToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ mUnderlineToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ mStrikeToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ mBquoteToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ moreButton.setOnClickListener(mFormatBarButtonClickListener);
+ mEditorFragmentListener.onEditorFragmentInitialized();
+
+ if (savedInstanceState != null) {
+ Parcelable[] spans = savedInstanceState.getParcelableArray(KEY_IMAGE_SPANS);
+
+ mContent = savedInstanceState.getString(KEY_CONTENT, "");
+ mContentEditText.setText(mContent);
+ mContentEditText.setSelection(savedInstanceState.getInt(KEY_START, 0),
+ savedInstanceState.getInt(KEY_END, 0));
+
+ if (spans != null && spans.length > 0) {
+ for (Parcelable s : spans) {
+ WPImageSpan editSpan = (WPImageSpan)s;
+ addMediaFile(editSpan.getMediaFile(), editSpan.getMediaFile().getFilePath(),
+ mImageLoader, editSpan.getStartPosition(), editSpan.getEndPosition());
+ }
+ }
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mRootView = view;
+ mRootView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
+ }
+
+ private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ public void onGlobalLayout() {
+ mRootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ mFullViewBottom = mRootView.getBottom();
+ }
+ };
+
+ private ActionBar getActionBar() {
+ if (!isAdded()) {
+ return null;
+ }
+ if (getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ return null;
+ }
+ }
+
+ public void setContentEditingModeVisible(boolean isVisible) {
+ if (!isAdded()) {
+ return;
+ }
+ ActionBar actionBar = getActionBar();
+ if (isVisible) {
+ Animation fadeAnimation = new AlphaAnimation(1, 0);
+ fadeAnimation.setDuration(CONTENT_ANIMATION_DURATION);
+ fadeAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mTitleEditText.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mPostSettingsLinearLayout.setVisibility(View.GONE);
+ mFormatBar.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+
+ mPostContentLinearLayout.startAnimation(fadeAnimation);
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+ } else {
+ mTitleEditText.setVisibility(View.VISIBLE);
+ mFormatBar.setVisibility(View.GONE);
+ Animation fadeAnimation = new AlphaAnimation(0, 1);
+ fadeAnimation.setDuration(CONTENT_ANIMATION_DURATION);
+ fadeAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mPostSettingsLinearLayout.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+ mPostContentLinearLayout.startAnimation(fadeAnimation);
+ getActivity().invalidateOptionsMenu();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == LegacyEditorFragment.ACTIVITY_REQUEST_CODE_CREATE_LINK && data != null) {
+ Bundle extras = data.getExtras();
+ if (extras == null) {
+ return;
+ }
+ String linkURL = extras.getString("linkURL");
+ String linkText = extras.getString("linkText");
+ createLinkFromSelection(linkURL, linkText);
+ }
+ }
+
+ public boolean hasEmptyContentFields() {
+ return TextUtils.isEmpty(mTitleEditText.getText()) && TextUtils.isEmpty(mContentEditText.getText());
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mFullViewBottom = mRootView.getBottom();
+ }
+
+ private void createLinkFromSelection(String linkURL, String linkText) {
+ try {
+ if (linkURL != null && !linkURL.equals("http://") && !linkURL.equals("")) {
+ if (mSelectionStart > mSelectionEnd) {
+ int temp = mSelectionEnd;
+ mSelectionEnd = mSelectionStart;
+ mSelectionStart = temp;
+ }
+ Editable editable = mContentEditText.getText();
+ if (editable == null) {
+ return;
+ }
+ if (mIsLocalDraft) {
+ if (linkText == null) {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ editable.insert(mSelectionStart, linkURL);
+ editable.setSpan(new URLSpan(linkURL), mSelectionStart, mSelectionStart + linkURL.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mContentEditText.setSelection(mSelectionStart + linkURL.length());
+ } else {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ editable.insert(mSelectionStart, linkText);
+ editable.setSpan(new URLSpan(linkURL), mSelectionStart, mSelectionStart + linkText.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mContentEditText.setSelection(mSelectionStart + linkText.length());
+ }
+ } else {
+ if (linkText == null) {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ String urlHTML = "<a href=\"" + linkURL + "\">" + linkURL + "</a>";
+ editable.insert(mSelectionStart, urlHTML);
+ mContentEditText.setSelection(mSelectionStart + urlHTML.length());
+ } else {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ String urlHTML = "<a href=\"" + linkURL + "\">" + linkText + "</a>";
+ editable.insert(mSelectionStart, urlHTML);
+ mContentEditText.setSelection(mSelectionStart + urlHTML.length());
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ /**
+ * Formatting bar
+ */
+ private View.OnClickListener mFormatBarButtonClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.bold) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED);
+ onFormatButtonClick(mBoldToggleButton, TAG_FORMAT_BAR_BUTTON_STRONG);
+ } else if (id == R.id.em) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED);
+ onFormatButtonClick(mEmToggleButton, TAG_FORMAT_BAR_BUTTON_EM);
+ } else if (id == R.id.underline) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNDERLINE_BUTTON_TAPPED);
+ onFormatButtonClick(mUnderlineToggleButton, TAG_FORMAT_BAR_BUTTON_UNDERLINE);
+ } else if (id == R.id.strike) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED);
+ onFormatButtonClick(mStrikeToggleButton, TAG_FORMAT_BAR_BUTTON_STRIKE);
+ } else if (id == R.id.bquote) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED);
+ onFormatButtonClick(mBquoteToggleButton, TAG_FORMAT_BAR_BUTTON_QUOTE);
+ } else if (id == R.id.more) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.MORE_BUTTON_TAPPED);
+ mSelectionEnd = mContentEditText.getSelectionEnd();
+ Editable str = mContentEditText.getText();
+ if (str != null) {
+ if (mSelectionEnd > str.length())
+ mSelectionEnd = str.length();
+ str.insert(mSelectionEnd, "\n<!--more-->\n");
+ }
+ } else if (id == R.id.link) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED);
+ mSelectionStart = mContentEditText.getSelectionStart();
+ mStyleStart = mSelectionStart;
+ mSelectionEnd = mContentEditText.getSelectionEnd();
+ if (mSelectionStart > mSelectionEnd) {
+ int temp = mSelectionEnd;
+ mSelectionEnd = mSelectionStart;
+ mSelectionStart = temp;
+ }
+ Intent i = new Intent(getActivity(), EditLinkActivity.class);
+ if (mSelectionEnd > mSelectionStart) {
+ if (mContentEditText.getText() != null) {
+ String selectedText = mContentEditText.getText().subSequence(mSelectionStart, mSelectionEnd).toString();
+ i.putExtra("selectedText", selectedText);
+ }
+ }
+ startActivityForResult(i, ACTIVITY_REQUEST_CODE_CREATE_LINK);
+ } else if (id == R.id.addPictureButton) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED);
+ mEditorFragmentListener.onAddMediaClicked();
+ if (isAdded()) {
+ getActivity().openContextMenu(mAddPictureButton);
+ }
+ }
+ }
+ };
+
+ private WPEditImageSpan createWPEditImageSpanLocal(Context context, MediaFile mediaFile) {
+ if (context == null || mediaFile == null || mediaFile.getFilePath() == null) {
+ return null;
+ }
+ Uri imageUri = Uri.parse(mediaFile.getFilePath());
+ Bitmap thumbnailBitmap;
+ if (MediaUtils.isVideo(imageUri.toString())) {
+ thumbnailBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.media_movieclip);
+ } else {
+ thumbnailBitmap = ImageUtils.getWPImageSpanThumbnailFromFilePath(context, imageUri.getEncodedPath(),
+ ImageUtils.getMaximumThumbnailWidthForEditor(context));
+ if (thumbnailBitmap == null) {
+ // Use a placeholder in case thumbnail can't be decoded (OOM for instance)
+ thumbnailBitmap = BitmapFactory.decodeResource(context.getResources(),
+ R.drawable.legacy_dashicon_format_image_big_grey);
+ }
+ }
+ WPEditImageSpan imageSpan = new WPEditImageSpan(context, thumbnailBitmap, imageUri);
+ mediaFile.setWidth(MediaUtils.getMaximumImageWidth(context, imageUri, mBlogSettingMaxImageWidth));
+ imageSpan.setMediaFile(mediaFile);
+ return imageSpan;
+ }
+
+ private WPEditImageSpan createWPEditImageSpanRemote(Context context, MediaFile mediaFile) {
+ if (context == null || mediaFile == null || mediaFile.getFileURL() == null) {
+ return null;
+ }
+ int drawable = mediaFile.isVideo() ? R.drawable.media_movieclip : R.drawable.legacy_dashicon_format_image_big_grey;
+ Uri uri = Uri.parse(mediaFile.getFileURL());
+ WPEditImageSpan imageSpan = new WPEditImageSpan(context, drawable, uri);
+ imageSpan.setMediaFile(mediaFile);
+ return imageSpan;
+ }
+
+ private WPEditImageSpan createWPEditImageSpan(Context context, MediaFile mediaFile) {
+ if (!URLUtil.isNetworkUrl(mediaFile.getFileURL())) {
+ return createWPEditImageSpanLocal(context, mediaFile);
+ } else {
+ return createWPEditImageSpanRemote(context, mediaFile);
+ }
+ }
+
+ /**
+ * Applies formatting to selected text, or marks the entry for a new text style
+ * at the current cursor position
+ * @param toggleButton button from formatting bar
+ * @param tag HTML tag name for text style
+ */
+ private void onFormatButtonClick(ToggleButton toggleButton, String tag) {
+ Spannable s = mContentEditText.getText();
+ if (s == null)
+ return;
+ int selectionStart = mContentEditText.getSelectionStart();
+ mStyleStart = selectionStart;
+ int selectionEnd = mContentEditText.getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ Class styleClass = null;
+ if (tag.equals(TAG_FORMAT_BAR_BUTTON_STRONG) || tag.equals(TAG_FORMAT_BAR_BUTTON_EM))
+ styleClass = StyleSpan.class;
+ else if (tag.equals(TAG_FORMAT_BAR_BUTTON_UNDERLINE))
+ styleClass = WPUnderlineSpan.class;
+ else if (tag.equals(TAG_FORMAT_BAR_BUTTON_STRIKE))
+ styleClass = StrikethroughSpan.class;
+ else if (tag.equals(TAG_FORMAT_BAR_BUTTON_QUOTE))
+ styleClass = QuoteSpan.class;
+
+ if (styleClass == null)
+ return;
+
+ Object[] allSpans = s.getSpans(selectionStart, selectionEnd, styleClass);
+ boolean textIsSelected = selectionEnd > selectionStart;
+ if (mIsLocalDraft) {
+ // Local drafts can use the rich text editor. Yay!
+ boolean shouldAddSpan = true;
+ for (Object span : allSpans) {
+ if (span instanceof StyleSpan) {
+ StyleSpan styleSpan = (StyleSpan)span;
+ if ((styleSpan.getStyle() == Typeface.BOLD && !tag.equals(TAG_FORMAT_BAR_BUTTON_STRONG))
+ || (styleSpan.getStyle() == Typeface.ITALIC && !tag.equals(TAG_FORMAT_BAR_BUTTON_EM))) {
+ continue;
+ }
+ }
+ if (!toggleButton.isChecked() && textIsSelected) {
+ // If span exists and text is selected, remove the span
+ s.removeSpan(span);
+ shouldAddSpan = false;
+ break;
+ } else if (!toggleButton.isChecked()) {
+ // Remove span at cursor point if button isn't checked
+ Object[] spans = s.getSpans(mStyleStart - 1, mStyleStart, styleClass);
+ for (Object removeSpan : spans) {
+ selectionStart = s.getSpanStart(removeSpan);
+ selectionEnd = s.getSpanEnd(removeSpan);
+ s.removeSpan(removeSpan);
+ }
+ }
+ }
+
+ if (shouldAddSpan) {
+ if (tag.equals(TAG_FORMAT_BAR_BUTTON_STRONG)) {
+ s.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (tag.equals(TAG_FORMAT_BAR_BUTTON_EM)) {
+ s.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ try {
+ s.setSpan(styleClass.newInstance(), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } catch (java.lang.InstantiationException e) {
+ AppLog.e(T.POSTS, e);
+ } catch (IllegalAccessException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ }
+ } else {
+ // Add HTML tags when editing an existing post
+ String startTag = "<" + tag + ">";
+ String endTag = "</" + tag + ">";
+ Editable content = mContentEditText.getText();
+ if (textIsSelected) {
+ content.insert(selectionStart, startTag);
+ content.insert(selectionEnd + startTag.length(), endTag);
+ toggleButton.setChecked(false);
+ mContentEditText.setSelection(selectionEnd + startTag.length() + endTag.length());
+ } else if (toggleButton.isChecked()) {
+ content.insert(selectionStart, startTag);
+ mContentEditText.setSelection(selectionEnd + startTag.length());
+ } else if (!toggleButton.isChecked()) {
+ content.insert(selectionEnd, endTag);
+ mContentEditText.setSelection(selectionEnd + endTag.length());
+ }
+ }
+ }
+
+ /**
+ * Rich Text Editor
+ */
+ public void showImageSettings(final View alertView, final EditText titleText,
+ final EditText caption, final EditText imageWidthText,
+ final CheckBox featuredCheckBox, final CheckBox featuredInPostCheckBox,
+ final int maxWidth, final Spinner alignmentSpinner, final WPImageSpan imageSpan) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.image_settings));
+ builder.setView(alertView);
+ builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String title = (titleText.getText() != null) ? titleText.getText().toString() : "";
+ MediaFile mediaFile = imageSpan.getMediaFile();
+ if (mediaFile == null) {
+ return;
+ }
+ mediaFile.setTitle(title);
+ mediaFile.setHorizontalAlignment(alignmentSpinner.getSelectedItemPosition());
+ mediaFile.setWidth(getEditTextIntegerClamped(imageWidthText, 10, maxWidth));
+ String captionText = (caption.getText() != null) ? caption.getText().toString() : "";
+ mediaFile.setCaption(captionText);
+ mediaFile.setFeatured(featuredCheckBox.isChecked());
+ if (featuredCheckBox.isChecked()) {
+ // remove featured flag from all other images
+ Spannable contentSpannable = mContentEditText.getText();
+ WPImageSpan[] imageSpans =
+ contentSpannable.getSpans(0, contentSpannable.length(), WPImageSpan.class);
+ if (imageSpans.length > 1) {
+ for (WPImageSpan postImageSpan : imageSpans) {
+ if (postImageSpan != imageSpan) {
+ MediaFile postMediaFile = postImageSpan.getMediaFile();
+ postMediaFile.setFeatured(false);
+ postMediaFile.setFeaturedInPost(false);
+ // TODO: remove this
+ mEditorFragmentListener.saveMediaFile(postMediaFile);
+ }
+ }
+ }
+ }
+ mediaFile.setFeaturedInPost(featuredInPostCheckBox.isChecked());
+ // TODO: remove this
+ mEditorFragmentListener.saveMediaFile(mediaFile);
+ }
+ });
+ builder.setNegativeButton(getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.dismiss();
+ }
+ });
+ AlertDialog alertDialog = builder.create();
+ alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ alertDialog.show();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ float pos = event.getY();
+
+ if (event.getAction() == 0)
+ mLastYPos = pos;
+
+ if (event.getAction() > 1) {
+ int scrollThreshold = DisplayUtils.dpToPx(getActivity(), 2);
+ if (((mLastYPos - pos) > scrollThreshold) || ((pos - mLastYPos) > scrollThreshold))
+ mScrollDetected = true;
+ }
+
+ mLastYPos = pos;
+
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null && actionBar.isShowing()) {
+ setContentEditingModeVisible(true);
+ return false;
+ }
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_UP && !mScrollDetected) {
+ Layout layout = ((TextView) v).getLayout();
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x += v.getScrollX();
+ y += v.getScrollY();
+ if (layout != null) {
+ int line = layout.getLineForVertical(y);
+ int charPosition = layout.getOffsetForHorizontal(line, x);
+
+ Spannable spannable = mContentEditText.getText();
+ if (spannable == null) {
+ return false;
+ }
+ // check if image span was tapped
+ WPImageSpan[] imageSpans = spannable.getSpans(charPosition, charPosition, WPImageSpan.class);
+
+ if (imageSpans.length != 0) {
+ final WPImageSpan imageSpan = imageSpans[0];
+ MediaFile mediaFile = imageSpan.getMediaFile();
+ if (mediaFile == null)
+ return false;
+ if (!mediaFile.isVideo()) {
+ LayoutInflater factory = LayoutInflater.from(getActivity());
+ final View alertView = factory.inflate(R.layout.alert_image_options, null);
+ if (alertView == null)
+ return false;
+ final EditText imageWidthText = (EditText) alertView.findViewById(R.id.imageWidthText);
+ final EditText titleText = (EditText) alertView.findViewById(R.id.title);
+ final EditText caption = (EditText) alertView.findViewById(R.id.caption);
+ final CheckBox featuredCheckBox = (CheckBox) alertView.findViewById(R.id.featuredImage);
+ final CheckBox featuredInPostCheckBox = (CheckBox) alertView.findViewById(R.id.featuredInPost);
+
+ // show featured image checkboxes if supported
+ if (mFeaturedImageSupported) {
+ featuredCheckBox.setVisibility(View.VISIBLE);
+ featuredInPostCheckBox.setVisibility(View.VISIBLE);
+ }
+
+ featuredCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ featuredInPostCheckBox.setVisibility(View.VISIBLE);
+ } else {
+ featuredInPostCheckBox.setVisibility(View.GONE);
+ }
+
+ }
+ });
+
+ final SeekBar seekBar = (SeekBar) alertView.findViewById(R.id.imageWidth);
+ final Spinner alignmentSpinner = (Spinner) alertView.findViewById(R.id.alignment_spinner);
+ ArrayAdapter<CharSequence> adapter =
+ ArrayAdapter.createFromResource(getActivity(), R.array.alignment_array,
+ android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ alignmentSpinner.setAdapter(adapter);
+
+ seekBar.setProgress(mediaFile.getWidth());
+ titleText.setText(mediaFile.getTitle());
+ caption.setText(mediaFile.getCaption());
+ featuredCheckBox.setChecked(mediaFile.isFeatured());
+
+ if (mediaFile.isFeatured()) {
+ featuredInPostCheckBox.setVisibility(View.VISIBLE);
+ } else {
+ featuredInPostCheckBox.setVisibility(View.GONE);
+ }
+
+ featuredInPostCheckBox.setChecked(mediaFile.isFeaturedInPost());
+
+ alignmentSpinner.setSelection(mediaFile.getHorizontalAlignment(), true);
+
+ final int maxWidth = MediaUtils.getMaximumImageWidth(getActivity(),
+ imageSpan.getImageSource(), mBlogSettingMaxImageWidth);
+ seekBar.setMax(maxWidth / 10);
+ imageWidthText.setText(String.format(Locale.US, "%dpx", maxWidth));
+ if (mediaFile.getWidth() != 0) {
+ seekBar.setProgress(mediaFile.getWidth() / 10);
+ }
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (progress == 0) {
+ progress = 1;
+ }
+ imageWidthText.setText(String.format(Locale.US, "%dpx", progress * 10));
+ }
+ });
+
+ imageWidthText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ imageWidthText.setText("");
+ }
+ }
+ });
+
+ imageWidthText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ int width = getEditTextIntegerClamped(imageWidthText, 10, maxWidth);
+ seekBar.setProgress(width / 10);
+ imageWidthText.setSelection((String.valueOf(width).length()));
+
+ InputMethodManager imm = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(imageWidthText.getWindowToken(),
+ InputMethodManager.RESULT_UNCHANGED_SHOWN);
+
+ return true;
+ }
+ });
+
+ showImageSettings(alertView, titleText, caption, imageWidthText, featuredCheckBox,
+ featuredInPostCheckBox, maxWidth, alignmentSpinner, imageSpan);
+ mScrollDetected = false;
+ return true;
+ }
+
+ } else {
+ mContentEditText.setMovementMethod(ArrowKeyMovementMethod.getInstance());
+ int selectionStart = mContentEditText.getSelectionStart();
+ if (selectionStart >= 0 && mContentEditText.getSelectionEnd() >= selectionStart)
+ mContentEditText.setSelection(selectionStart, mContentEditText.getSelectionEnd());
+ }
+
+ // get media gallery spans
+ MediaGalleryImageSpan[] gallerySpans = spannable.getSpans(charPosition, charPosition, MediaGalleryImageSpan.class);
+ if (gallerySpans.length > 0) {
+ final MediaGalleryImageSpan gallerySpan = gallerySpans[0];
+ Intent intent = new Intent(ACTION_MEDIA_GALLERY_TOUCHED);
+ intent.putExtra(EXTRA_MEDIA_GALLERY, gallerySpan.getMediaGallery());
+ getActivity().sendBroadcast(intent);
+ }
+ }
+ } else if (event.getAction() == 1) {
+ mScrollDetected = false;
+ }
+ return false;
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ int position = Selection.getSelectionStart(mContentEditText.getText());
+ if ((mIsBackspace && position != 1) || mLastPosition == position || !mIsLocalDraft)
+ return;
+
+ if (position < 0) {
+ position = 0;
+ }
+ mLastPosition = position;
+ if (position > 0) {
+ if (mStyleStart > position) {
+ mStyleStart = position - 1;
+ }
+
+ boolean shouldBold = mBoldToggleButton.isChecked();
+ boolean shouldEm = mEmToggleButton.isChecked();
+ boolean shouldUnderline = mUnderlineToggleButton.isChecked();
+ boolean shouldStrike = mStrikeToggleButton.isChecked();
+ boolean shouldQuote = mBquoteToggleButton.isChecked();
+
+ Object[] allSpans = s.getSpans(mStyleStart, position, Object.class);
+ for (Object span : allSpans) {
+ if (span instanceof StyleSpan) {
+ StyleSpan styleSpan = (StyleSpan) span;
+ if (styleSpan.getStyle() == Typeface.BOLD)
+ shouldBold = false;
+ else if (styleSpan.getStyle() == Typeface.ITALIC)
+ shouldEm = false;
+ } else if (span instanceof WPUnderlineSpan) {
+ shouldUnderline = false;
+ } else if (span instanceof StrikethroughSpan) {
+ shouldStrike = false;
+ } else if (span instanceof QuoteSpan) {
+ shouldQuote = false;
+ }
+ }
+
+ if (shouldBold)
+ s.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldEm)
+ s.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldUnderline)
+ s.setSpan(new WPUnderlineSpan(), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldStrike)
+ s.setSpan(new StrikethroughSpan(), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldQuote)
+ s.setSpan(new QuoteSpan(), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ mIsBackspace = (count - after == 1) || (s.length() == 0);
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void onSelectionChanged() {
+ if (!mIsLocalDraft) {
+ return;
+ }
+
+ final Spannable s = mContentEditText.getText();
+ if (s == null)
+ return;
+ // set toggle buttons if cursor is inside of a matching span
+ mStyleStart = mContentEditText.getSelectionStart();
+ Object[] spans = s.getSpans(mContentEditText.getSelectionStart(), mContentEditText.getSelectionStart(), Object.class);
+
+ mBoldToggleButton.setChecked(false);
+ mEmToggleButton.setChecked(false);
+ mBquoteToggleButton.setChecked(false);
+ mUnderlineToggleButton.setChecked(false);
+ mStrikeToggleButton.setChecked(false);
+ for (Object span : spans) {
+ if (span instanceof StyleSpan) {
+ StyleSpan ss = (StyleSpan) span;
+ if (ss.getStyle() == android.graphics.Typeface.BOLD) {
+ mBoldToggleButton.setChecked(true);
+ }
+ if (ss.getStyle() == android.graphics.Typeface.ITALIC) {
+ mEmToggleButton.setChecked(true);
+ }
+ }
+ if (span instanceof QuoteSpan) {
+ mBquoteToggleButton.setChecked(true);
+ }
+ if (span instanceof WPUnderlineSpan) {
+ mUnderlineToggleButton.setChecked(true);
+ }
+ if (span instanceof StrikethroughSpan) {
+ mStrikeToggleButton.setChecked(true);
+ }
+ }
+ }
+
+ private int getEditTextIntegerClamped(EditText editText, int min, int max) {
+ int width = 10;
+ try {
+ if (editText.getText() != null)
+ width = Integer.parseInt(editText.getText().toString().replace("px", ""));
+ } catch (NumberFormatException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ width = Math.min(max, Math.max(width, min));
+ return width;
+ }
+
+ private void loadWPImageSpanThumbnail(MediaFile mediaFile, String imageURL, ImageLoader imageLoader) {
+ if (mediaFile == null || imageURL == null) {
+ return;
+ }
+ final String mediaId = mediaFile.getMediaId();
+ if (mediaId == null) {
+ return;
+ }
+
+ final int maxThumbWidth = ImageUtils.getMaximumThumbnailWidthForEditor(getActivity());
+
+ imageLoader.get(imageURL, new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError arg0) {
+ }
+
+ @Override
+ public void onResponse(ImageLoader.ImageContainer container, boolean arg1) {
+ Bitmap downloadedBitmap = container.getBitmap();
+ if (downloadedBitmap == null) {
+ // no bitmap downloaded from the server.
+ return;
+ }
+
+ if (downloadedBitmap.getWidth() < MIN_THUMBNAIL_WIDTH) {
+ // Picture is too small. Show the placeholder in this case.
+ return;
+ }
+
+ Bitmap resizedBitmap;
+ // resize the downloaded bitmap
+ resizedBitmap = ImageUtils.getScaledBitmapAtLongestSide(downloadedBitmap, maxThumbWidth);
+
+ if (resizedBitmap == null) {
+ return;
+ }
+
+ final EditText editText = mContentEditText;
+ Editable s = editText.getText();
+ if (s == null) {
+ return;
+ }
+ WPImageSpan[] spans = s.getSpans(0, s.length(), WPImageSpan.class);
+ if (spans.length != 0 && getActivity() != null) {
+ for (WPImageSpan is : spans) {
+ MediaFile mediaFile = is.getMediaFile();
+ if (mediaFile == null) {
+ continue;
+ }
+ if (mediaId.equals(mediaFile.getMediaId()) && !is.isNetworkImageLoaded()) {
+ // replace the existing span with a new one with the correct image, re-add
+ // it to the same position.
+ int spanStart = is.getStartPosition();
+ int spanEnd = is.getEndPosition();
+ WPEditImageSpan imageSpan = new WPEditImageSpan(getActivity(), resizedBitmap,
+ is.getImageSource());
+ imageSpan.setMediaFile(is.getMediaFile());
+ imageSpan.setNetworkImageLoaded(true);
+ imageSpan.setPosition(spanStart, spanEnd);
+ s.removeSpan(is);
+ s.setSpan(imageSpan, spanStart, spanEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ }
+ }
+ }
+ }
+ }, 0, 0);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ WPImageSpan[] spans = mContentEditText.getText().getSpans(0, mContentEditText.getText().length(), WPEditImageSpan.class);
+
+ if (spans != null && spans.length > 0) {
+ outState.putParcelableArray(KEY_IMAGE_SPANS, spans);
+ }
+
+ outState.putInt(KEY_START, mContentEditText.getSelectionStart());
+ outState.putInt(KEY_END, mContentEditText.getSelectionEnd());
+ outState.putString(KEY_CONTENT, mContentEditText.getText().toString());
+ }
+
+ private class AddMediaFileTask extends AsyncTask<Void, Void, WPEditImageSpan> {
+ private MediaFile mMediaFile;
+ private String mImageUrl;
+ private ImageLoader mImageLoader;
+ private int mStart;
+ private int mEnd;
+
+ public AddMediaFileTask(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader, int start, int end) {
+ mMediaFile = mediaFile;
+ mImageUrl = imageUrl;
+ mImageLoader = imageLoader;
+ mStart = start;
+ mEnd = end;
+ }
+
+ protected WPEditImageSpan doInBackground(Void... voids) {
+ mMediaFile.setFileURL(mImageUrl);
+ mMediaFile.setFilePath(mImageUrl);
+ WPEditImageSpan imageSpan = createWPEditImageSpan(getActivity(), mMediaFile);
+ mEditorFragmentListener.saveMediaFile(mMediaFile);
+ return imageSpan;
+ }
+
+ protected void onPostExecute(WPEditImageSpan imageSpan) {
+ if (imageSpan == null) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.alert_error_adding_media, Duration.LONG);
+ }
+ return ;
+ }
+ // Insert the WPImageSpan in the content field
+ int selectionStart = mStart;
+ int selectionEnd = mEnd;
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ imageSpan.setPosition(selectionStart, selectionEnd);
+
+ int line, column = 0;
+ if (mContentEditText.getLayout() != null) {
+ line = mContentEditText.getLayout().getLineForOffset(selectionStart);
+ column = selectionStart - mContentEditText.getLayout().getLineStart(line);
+ }
+
+ Editable s = mContentEditText.getText();
+ if (s == null) {
+ return;
+ }
+
+ WPImageSpan[] imageSpans = s.getSpans(selectionStart, selectionEnd, WPImageSpan.class);
+ if (imageSpans.length != 0) {
+ // insert a few line breaks if the cursor is already on an image
+ s.insert(selectionEnd, "\n\n");
+ selectionStart = selectionStart + 2;
+ selectionEnd = selectionEnd + 2;
+ } else if (column != 0) {
+ // insert one line break if the cursor is not at the first column
+ s.insert(selectionEnd, "\n");
+ selectionStart = selectionStart + 1;
+ selectionEnd = selectionEnd + 1;
+ }
+
+ s.insert(selectionStart, " ");
+ s.setSpan(imageSpan, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ AlignmentSpan.Standard as = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
+ s.setSpan(as, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ s.insert(selectionEnd + 1, "\n\n");
+
+ // Fetch and replace the WPImageSpan if it's a remote media
+ if (mImageLoader != null && URLUtil.isNetworkUrl(mImageUrl)) {
+ loadWPImageSpanThumbnail(mMediaFile, mImageUrl, mImageLoader);
+ }
+ }
+ }
+
+ public void addMediaFile(final MediaFile mediaFile, final String imageUrl, final ImageLoader imageLoader,
+ final int start, final int end) {
+ AddMediaFileTask addMediaFileTask = new AddMediaFileTask(mediaFile, imageUrl, imageLoader, start, end);
+ addMediaFileTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public void appendMediaFile(final MediaFile mediaFile, final String imageUrl, final ImageLoader imageLoader) {
+ addMediaFile(mediaFile, imageUrl, imageLoader, mContentEditText.getSelectionStart(), mContentEditText.getSelectionEnd());
+ }
+
+ @Override
+ public void appendGallery(MediaGallery mediaGallery) {
+ Editable editableText = mContentEditText.getText();
+ if (editableText == null) {
+ return;
+ }
+
+ int selectionStart = mContentEditText.getSelectionStart();
+ int selectionEnd = mContentEditText.getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ int line, column = 0;
+ if (mContentEditText.getLayout() != null) {
+ line = mContentEditText.getLayout().getLineForOffset(selectionStart);
+ column = mContentEditText.getSelectionStart() - mContentEditText.getLayout().getLineStart(line);
+ }
+
+ if (column != 0) {
+ // insert one line break if the cursor is not at the first column
+ editableText.insert(selectionEnd, "\n");
+ selectionStart = selectionStart + 1;
+ selectionEnd = selectionEnd + 1;
+ }
+
+ editableText.insert(selectionStart, " ");
+ MediaGalleryImageSpan is = new MediaGalleryImageSpan(getActivity(), mediaGallery,
+ R.drawable.legacy_icon_mediagallery_placeholder);
+ editableText.setSpan(is, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ AlignmentSpan.Standard as = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
+ editableText.setSpan(as, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editableText.insert(selectionEnd + 1, "\n\n");
+ }
+
+ @Override
+ public void setUrlForVideoPressId(String videoPressId, String url, String posterUrl) {
+
+ }
+
+ @Override
+ public boolean isUploadingMedia() {
+ return false;
+ }
+
+ @Override
+ public boolean hasFailedMediaUploads() {
+ return false;
+ }
+
+ @Override
+ public void removeAllFailedMediaUploads() {}
+
+ @Override
+ public void setTitlePlaceholder(CharSequence text) {
+ }
+
+ @Override
+ public void setContentPlaceholder(CharSequence text) {
+ }
+
+ @Override
+ public boolean isActionInProgress() {
+ return false;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java
new file mode 100644
index 000000000..9be36cb33
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.editor;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+
+public class LinkDialogFragment extends DialogFragment {
+
+ public static final int LINK_DIALOG_REQUEST_CODE_ADD = 1;
+ public static final int LINK_DIALOG_REQUEST_CODE_UPDATE = 2;
+ public static final int LINK_DIALOG_REQUEST_CODE_DELETE = 3;
+
+ public static final String LINK_DIALOG_ARG_URL = "linkURL";
+ public static final String LINK_DIALOG_ARG_TEXT = "linkText";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+
+ View view = inflater.inflate(R.layout.dialog_link, null);
+
+ final EditText urlEditText = (EditText) view.findViewById(R.id.linkURL);
+ final EditText linkEditText = (EditText) view.findViewById(R.id.linkText);
+
+ builder.setView(view)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intent = new Intent();
+ intent.putExtra(LINK_DIALOG_ARG_URL, urlEditText.getText().toString());
+ intent.putExtra(LINK_DIALOG_ARG_TEXT, linkEditText.getText().toString());
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), intent);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ LinkDialogFragment.this.getDialog().cancel();
+ }
+ });
+
+ // If updating an existing link, add a 'Delete' button
+ if (getTargetRequestCode() == LINK_DIALOG_REQUEST_CODE_UPDATE) {
+ builder.setNeutralButton(R.string.delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ getTargetFragment().onActivityResult(getTargetRequestCode(), LINK_DIALOG_REQUEST_CODE_DELETE, null);
+ }
+ });
+ }
+
+ // Prepare initial state of EditTexts
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ linkEditText.setText(bundle.getString(LINK_DIALOG_ARG_TEXT));
+
+ String url = bundle.getString(LINK_DIALOG_ARG_URL);
+ if (url != null) {
+ urlEditText.setText(url);
+ }
+ urlEditText.selectAll();
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ return dialog;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java
new file mode 100644
index 000000000..ed7ee0995
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java
@@ -0,0 +1,5 @@
+package org.wordpress.android.editor;
+
+public interface OnImeBackListener {
+ void onImeBack();
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java
new file mode 100755
index 000000000..ca8cbf514
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java
@@ -0,0 +1,20 @@
+package org.wordpress.android.editor;
+
+import org.json.JSONObject;
+
+import java.util.Map;
+
+import static org.wordpress.android.editor.EditorFragmentAbstract.MediaType;
+
+public interface OnJsEditorStateChangedListener {
+ void onDomLoaded();
+ void onSelectionChanged(Map<String, String> selectionArgs);
+ void onSelectionStyleChanged(Map<String, Boolean> changeSet);
+ void onMediaTapped(String mediaId, MediaType mediaType, JSONObject meta, String uploadStatus);
+ void onLinkTapped(String url, String title);
+ void onMediaRemoved(String mediaId);
+ void onMediaReplaced(String mediaId);
+ void onVideoPressInfoRequested(String videoId);
+ void onGetHtmlResponse(Map<String, String> responseArgs);
+ void onActionFinished();
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java
new file mode 100644
index 000000000..fd2ac6110
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java
@@ -0,0 +1,95 @@
+package org.wordpress.android.editor;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ToggleButton;
+
+public class RippleToggleButton extends ToggleButton {
+ private static final int FRAME_RATE = 10;
+ private static final int DURATION = 250;
+ private static final int FILL_INITIAL_OPACITY = 200;
+ private static final int STROKE_INITIAL_OPACITY = 255;
+
+ private float mHalfWidth;
+ private boolean mAnimationIsRunning = false;
+ private int mTimer = 0;
+ private Paint mFillPaint;
+ private Paint mStrokePaint;
+
+ public RippleToggleButton(Context context) {
+ this(context, null);
+ }
+
+ public RippleToggleButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RippleToggleButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ if (isInEditMode()) {
+ return;
+ }
+
+ int rippleColor = getResources().getColor(R.color.format_bar_ripple_animation);
+
+ mFillPaint = new Paint();
+ mFillPaint.setAntiAlias(true);
+ mFillPaint.setColor(rippleColor);
+ mFillPaint.setStyle(Paint.Style.FILL);
+ mFillPaint.setAlpha(FILL_INITIAL_OPACITY);
+
+ mStrokePaint = new Paint();
+ mStrokePaint.setAntiAlias(true);
+ mStrokePaint.setColor(rippleColor);
+ mStrokePaint.setStyle(Paint.Style.STROKE);
+ mStrokePaint.setStrokeWidth(2);
+ mStrokePaint.setAlpha(STROKE_INITIAL_OPACITY);
+
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+ if (mAnimationIsRunning) {
+ if (DURATION <= mTimer * FRAME_RATE) {
+ mAnimationIsRunning = false;
+ mTimer = 0;
+ } else {
+ float progressFraction = ((float) mTimer * FRAME_RATE) / DURATION;
+
+ mFillPaint.setAlpha((int) (FILL_INITIAL_OPACITY * (1 - progressFraction)));
+ mStrokePaint.setAlpha((int) (STROKE_INITIAL_OPACITY * (1 - progressFraction)));
+
+ canvas.drawCircle(mHalfWidth, mHalfWidth, mHalfWidth * progressFraction, mFillPaint);
+ canvas.drawCircle(mHalfWidth, mHalfWidth, mHalfWidth * progressFraction, mStrokePaint);
+
+ mTimer++;
+ }
+
+ invalidate();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ startRippleAnimation();
+ return super.onTouchEvent(event);
+ }
+
+ private void startRippleAnimation() {
+ if (this.isEnabled() && !mAnimationIsRunning) {
+ mHalfWidth = getMeasuredWidth() / 2;
+ mAnimationIsRunning = true;
+ invalidate();
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java
new file mode 100644
index 000000000..12caa31d0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.editor;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+import org.wordpress.android.util.AppLog;
+
+/**
+ * An EditText with support for {@link org.wordpress.android.editor.OnImeBackListener} and typeface setting
+ * using a custom XML attribute.
+ */
+public class SourceViewEditText extends EditText {
+
+ private OnImeBackListener mOnImeBackListener;
+
+ public SourceViewEditText(Context context) {
+ super(context);
+ }
+
+ public SourceViewEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setCustomTypeface(attrs);
+ }
+
+ public SourceViewEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setCustomTypeface(attrs);
+ }
+
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if(event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (this.mOnImeBackListener != null) {
+ this.mOnImeBackListener.onImeBack();
+ }
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ public void setOnImeBackListener(OnImeBackListener listener) {
+ this.mOnImeBackListener = listener;
+ }
+
+ private void setCustomTypeface(AttributeSet attrs) {
+ TypedArray values = getContext().obtainStyledAttributes(attrs, R.styleable.SourceViewEditText);
+ String typefaceName = values.getString(R.styleable.SourceViewEditText_fontFile);
+ if (typefaceName != null) {
+ try {
+ Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/" + typefaceName);
+ this.setTypeface(typeface);
+ } catch (RuntimeException e) {
+ AppLog.e(AppLog.T.EDITOR, "Could not load typeface " + typefaceName);
+ }
+ }
+ values.recycle();
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java
new file mode 100644
index 000000000..476656320
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java
@@ -0,0 +1,247 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.util.Patterns;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.HTTPUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+public class Utils {
+ public static String getHtmlFromFile(Activity activity, String filename) {
+ try {
+ AssetManager assetManager = activity.getAssets();
+ InputStream in = assetManager.open(filename);
+ return getStringFromInputStream(in);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.EDITOR, "Unable to load editor HTML (is the assets symlink working?): " + e.getMessage());
+ return null;
+ }
+ }
+
+ public static String getStringFromInputStream(InputStream inputStream) throws IOException {
+ InputStreamReader is = new InputStreamReader(inputStream);
+ StringBuilder sb = new StringBuilder();
+ BufferedReader br = new BufferedReader(is);
+ String read = br.readLine();
+ while (read != null) {
+ sb.append(read);
+ sb.append('\n');
+ read = br.readLine();
+ }
+ return sb.toString();
+ }
+
+ public static String escapeHtml(String html) {
+ if (html != null) {
+ html = html.replace("\\", "\\\\");
+ html = html.replace("\"", "\\\"");
+ html = html.replace("'", "\\'");
+ html = html.replace("\r", "\\r");
+ html = html.replace("\n", "\\n");
+
+ // Escape invisible line separator (U+2028) and paragraph separator (U+2029) characters
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/405
+ html = html.replace("\u2028", "\\u2028");
+ html = html.replace("\u2029", "\\u2029");
+ }
+ return html;
+ }
+
+ public static String decodeHtml(String html) {
+ if (html != null) {
+ try {
+ html = URLDecoder.decode(html, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ AppLog.e(AppLog.T.EDITOR, "Unsupported encoding exception while decoding HTML.");
+ }
+ }
+ return html;
+ }
+
+ public static String escapeQuotes(String text) {
+ if (text != null) {
+ text = text.replace("'", "\\'").replace("\"", "\\\"");
+ }
+ return text;
+ }
+
+ /**
+ * Splits a delimited string into a set of strings.
+ * @param string the delimited string to split
+ * @param delimiter the string delimiter
+ */
+ public static Set<String> splitDelimitedString(String string, String delimiter) {
+ Set<String> splitString = new HashSet<>();
+
+ StringTokenizer stringTokenizer = new StringTokenizer(string, delimiter);
+ while (stringTokenizer.hasMoreTokens()) {
+ splitString.add(stringTokenizer.nextToken());
+ }
+
+ return splitString;
+ }
+
+ /**
+ * Splits a delimited string of value pairs (of the form identifier=value) into a set of strings.
+ * @param string the delimited string to split
+ * @param delimiter the string delimiter
+ * @param identifiers the identifiers to match for in the string
+ */
+ public static Set<String> splitValuePairDelimitedString(String string, String delimiter, List<String> identifiers) {
+ String identifierSegment = "";
+ for (String identifier : identifiers) {
+ if (identifierSegment.length() != 0) {
+ identifierSegment += "|";
+ }
+ identifierSegment += identifier;
+ }
+
+ String regex = delimiter + "(?=(" + identifierSegment + ")=)";
+
+ return new HashSet<>(Arrays.asList(string.split(regex)));
+ }
+
+ /**
+ * Accepts a set of strings, each string being a key-value pair (<code>id=5</code>,
+ * <code>name=content-filed</code>). Returns a map of all the key-value pairs in the set.
+ * @param keyValueSet the set of key-value pair strings
+ */
+ public static Map<String, String> buildMapFromKeyValuePairs(Set<String> keyValueSet) {
+ Map<String, String> selectionArgs = new HashMap<>();
+ for (String pair : keyValueSet) {
+ int delimLoc = pair.indexOf("=");
+ if (delimLoc != -1) {
+ selectionArgs.put(pair.substring(0, delimLoc), pair.substring(delimLoc + 1));
+ }
+ }
+ return selectionArgs;
+ }
+
+ /**
+ * Compares two <code>Sets</code> and returns a <code>Map</code> of elements not contained in both
+ * <code>Sets</code>. Elements contained in <code>oldSet</code> but not in <code>newSet</code> will be marked
+ * <code>false</code> in the returned map; the converse will be marked <code>true</code>.
+ * @param oldSet the older of the two <code>Sets</code>
+ * @param newSet the newer of the two <code>Sets</code>
+ * @param <E> type of element stored in the <code>Sets</code>
+ * @return a <code>Map</code> containing the difference between <code>oldSet</code> and <code>newSet</code>, and whether the
+ * element was added (<code>true</code>) or removed (<code>false</code>) in <code>newSet</code>
+ */
+ public static <E> Map<E, Boolean> getChangeMapFromSets(Set<E> oldSet, Set<E> newSet) {
+ Map<E, Boolean> changeMap = new HashMap<>();
+
+ Set<E> additions = new HashSet<>(newSet);
+ additions.removeAll(oldSet);
+
+ Set<E> removals = new HashSet<>(oldSet);
+ removals.removeAll(newSet);
+
+ for (E s : additions) {
+ changeMap.put(s, true);
+ }
+
+ for (E s : removals) {
+ changeMap.put(s, false);
+ }
+
+ return changeMap;
+ }
+
+ public static Uri downloadExternalMedia(Context context, Uri imageUri, Map<String, String> headers) {
+ if(context != null && imageUri != null) {
+ File cacheDir = null;
+
+ if (context.getApplicationContext() != null) {
+ cacheDir = context.getCacheDir();
+ }
+
+ try {
+ InputStream inputStream;
+ if (imageUri.toString().startsWith("content://")) {
+ inputStream = context.getContentResolver().openInputStream(imageUri);
+ if (inputStream == null) {
+ AppLog.e(AppLog.T.UTILS, "openInputStream returned null");
+ return null;
+ }
+ } else {
+ if (headers == null) {
+ headers = Collections.emptyMap();
+ }
+
+ HttpURLConnection conn = HTTPUtils.setupUrlConnection(imageUri.toString(), headers);
+
+ // If the HTTP response is a redirect, follow it
+ int responseCode = conn.getResponseCode();
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
+ conn = HTTPUtils.setupUrlConnection(conn.getHeaderField("Location"), headers);
+ }
+ }
+
+ inputStream = conn.getInputStream();
+ }
+
+ String fileName = "thumb-" + System.currentTimeMillis();
+
+ File f = new File(cacheDir, fileName);
+ FileOutputStream output = new FileOutputStream(f);
+ byte[] data = new byte[1024];
+
+ int count;
+ while ((count = inputStream.read(data)) != -1) {
+ output.write(data, 0, count);
+ }
+
+ output.flush();
+ output.close();
+ inputStream.close();
+ return Uri.fromFile(f);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ return null;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Checks the Clipboard for text that matches the {@link Patterns#WEB_URL} pattern.
+ *
+ * @return the URL text in the clipboard, if it exists; otherwise null
+ */
+ public static String getUrlFromClipboard(Context context) {
+ if (context == null) return null;
+ ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData data = clipboard != null ? clipboard.getPrimaryClip() : null;
+ if (data == null || data.getItemCount() <= 0) return null;
+ String clipText = String.valueOf(data.getItemAt(0).getText());
+ return Patterns.WEB_URL.matcher(clipText).matches() ? clipText : null;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java
new file mode 100644
index 000000000..e02f98eb2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.editor.legacy;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import org.wordpress.android.editor.R;
+
+public class EditLinkActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.alert_create_link);
+
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ String selectedText = extras.getString("selectedText");
+ if (selectedText != null) {
+ EditText linkTextET = (EditText) findViewById(R.id.linkText);
+ linkTextET.setText(selectedText);
+ }
+ }
+
+ final Button cancelButton = (Button) findViewById(R.id.cancel);
+ final Button okButton = (Button) findViewById(R.id.ok);
+
+ final EditText urlEditText = (EditText) findViewById(R.id.linkURL);
+ urlEditText.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (urlEditText.getText().toString().equals("")) {
+ urlEditText.setText("http://");
+ urlEditText.setSelection(7);
+ }
+ }
+
+ });
+
+ okButton.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ EditText linkURLET = (EditText) findViewById(R.id.linkURL);
+ String linkURL = linkURLET.getText().toString();
+
+ EditText linkTextET = (EditText) findViewById(R.id.linkText);
+ String linkText = linkTextET.getText().toString();
+
+ Bundle bundle = new Bundle();
+ bundle.putString("linkURL", linkURL);
+ if (!linkText.equals("")) {
+ bundle.putString("linkText", linkText);
+ }
+
+ Intent mIntent = new Intent();
+ mIntent.putExtras(bundle);
+ setResult(RESULT_OK, mIntent);
+ finish();
+ }
+ });
+
+ cancelButton.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ Intent mIntent = new Intent();
+ setResult(RESULT_CANCELED, mIntent);
+ finish();
+ }
+ });
+
+ // select end of url
+ urlEditText.performClick();
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java
new file mode 100644
index 000000000..25bc33894
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.editor.legacy;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.wordpress.android.editor.R;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.WPImageSpan;
+
+public class WPEditImageSpan extends WPImageSpan {
+ private Bitmap mEditIconBitmap;
+
+ protected WPEditImageSpan() {
+ super();
+ }
+
+ public WPEditImageSpan(Context context, Bitmap b, Uri src) {
+ super(context, b, src);
+ init(context);
+ }
+
+ public WPEditImageSpan(Context context, int resId, Uri src) {
+ super(context, resId, src);
+ init(context);
+ }
+
+ private void init(Context context) {
+ if (context != null) {
+ mEditIconBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ab_icon_edit);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom,
+ Paint paint) {
+ super.draw(canvas, text, start, end, x, top, y, bottom, paint);
+
+ if (mEditIconBitmap != null && !mMediaFile.isVideo()) {
+ // Add 'edit' icon at bottom right of image
+ int width = getSize(paint, text, start, end, paint.getFontMetricsInt());
+ float editIconXPosition = (x + width) - mEditIconBitmap.getWidth();
+ float editIconYPosition = bottom - mEditIconBitmap.getHeight();
+
+ // Add a black background with a bit of alpha
+ Paint bgPaint = new Paint();
+ bgPaint.setColor(Color.argb(200, 0, 0, 0));
+ canvas.drawRect(editIconXPosition, editIconYPosition, editIconXPosition + mEditIconBitmap.getWidth(),
+ editIconYPosition + mEditIconBitmap.getHeight(), bgPaint);
+
+ // Add the icon to the canvas
+ canvas.drawBitmap(mEditIconBitmap, editIconXPosition, editIconYPosition, paint);
+ }
+ }
+
+ public static final Parcelable.Creator<WPEditImageSpan> CREATOR = new Parcelable.Creator<WPEditImageSpan>() {
+ public WPEditImageSpan createFromParcel(Parcel in) {
+ WPEditImageSpan editSpan = new WPEditImageSpan();
+ editSpan.setupFromParcel(in);
+
+ return editSpan;
+ }
+
+ public WPEditImageSpan[] newArray(int size) {
+ return new WPEditImageSpan[size];
+ }
+ };
+}
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.png
new file mode 100644
index 000000000..4b8f443e1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.png
new file mode 100644
index 000000000..d47a4f1ce
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..0fd15563a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.png
new file mode 100644
index 000000000..4e4959dcd
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.png
new file mode 100644
index 000000000..55fb8739a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.png
new file mode 100644
index 000000000..5b66e714c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.png
new file mode 100644
index 000000000..0ba4b95e8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.png
new file mode 100644
index 000000000..fc896135d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.png
new file mode 100644
index 000000000..444f735ab
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.png
new file mode 100644
index 000000000..32411efc7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.png
new file mode 100644
index 000000000..48db52143
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.png
new file mode 100644
index 000000000..eea6add02
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.png
new file mode 100644
index 000000000..7cb2dcd30
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.png
new file mode 100644
index 000000000..90354d05d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.png
new file mode 100644
index 000000000..7e14c49a5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.png
new file mode 100644
index 000000000..978f97519
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.png
new file mode 100644
index 000000000..a61883996
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.png
new file mode 100644
index 000000000..7f7c5181e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.png
new file mode 100644
index 000000000..4dc307fc7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.png
new file mode 100644
index 000000000..9e2b6e412
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.png
new file mode 100644
index 000000000..130a206d3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.png
new file mode 100644
index 000000000..046432898
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.png
new file mode 100644
index 000000000..bb49bcdc2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.png
new file mode 100644
index 000000000..7900b9283
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.png
new file mode 100644
index 000000000..c4ca1c13c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.png
new file mode 100644
index 000000000..46d12a96d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.png
new file mode 100644
index 000000000..03b57a17c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..76e07f097
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.png
new file mode 100644
index 000000000..3e2020016
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.png
new file mode 100644
index 000000000..eef4505a5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.png
new file mode 100644
index 000000000..f545cd651
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.png
new file mode 100644
index 000000000..4519f6c04
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.png
new file mode 100644
index 000000000..b85e41382
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.png
new file mode 100644
index 000000000..61ae586af
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.png
new file mode 100644
index 000000000..912987466
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.png
new file mode 100644
index 000000000..dbcea3513
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.png
new file mode 100644
index 000000000..6a776b669
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.png
new file mode 100644
index 000000000..006acefc1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.png
new file mode 100644
index 000000000..c18266381
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.png
new file mode 100644
index 000000000..c3c76dbc8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.png
new file mode 100644
index 000000000..f728bd9cf
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.png
new file mode 100644
index 000000000..95dbdd936
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.png
new file mode 100644
index 000000000..164881f53
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.png
new file mode 100644
index 000000000..797c88e8a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.png
new file mode 100644
index 000000000..f3fe14ad3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png
new file mode 100644
index 000000000..3dbd3937d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.png
new file mode 100644
index 000000000..8c39cc8aa
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.png
new file mode 100644
index 000000000..d1dfae9ea
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.png
new file mode 100644
index 000000000..95bffba31
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.png
new file mode 100644
index 000000000..d6aeb0e3d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.png
new file mode 100644
index 000000000..8c5d06a79
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.png
new file mode 100644
index 000000000..110b8ba5a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..0eb9d8b08
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.png
new file mode 100644
index 000000000..14c9186ef
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.png
new file mode 100644
index 000000000..724dea05e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.png
new file mode 100644
index 000000000..a6746556a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.png
new file mode 100644
index 000000000..2fc4e42a1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.png
new file mode 100644
index 000000000..0a48139a0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.png
new file mode 100644
index 000000000..05437d16b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.png
new file mode 100644
index 000000000..91541b529
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.png
new file mode 100644
index 000000000..f1d118b74
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.png
new file mode 100644
index 000000000..3a684e978
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.png
new file mode 100644
index 000000000..35d6579b5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.png
new file mode 100644
index 000000000..2e9e30f5c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.png
new file mode 100644
index 000000000..fb8558945
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.png
new file mode 100644
index 000000000..9f1b612d6
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.png
new file mode 100644
index 000000000..3a608ed32
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.png
new file mode 100644
index 000000000..daa89ba17
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.png
new file mode 100644
index 000000000..240493fb3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.png
new file mode 100644
index 000000000..40976b210
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.png
new file mode 100644
index 000000000..7ec4e9b25
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.png
new file mode 100644
index 000000000..90e9b768a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.png
new file mode 100644
index 000000000..a132b6da3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.png
new file mode 100644
index 000000000..7b8ff9608
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml
new file mode 100755
index 000000000..82788b1fb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M17,15.009h4.547c2.126,0,3.671,0.303,4.632,0.907c0.962,0.605,1.442,1.567,1.442,2.887
+c0,0.896-0.211,1.63-0.631,2.205c-0.421,0.574-0.98,0.919-1.678,1.036v0.103c0.951,0.212,1.637,0.608,2.057,1.189
+C27.79,23.916,28,24.688,28,25.652c0,1.367-0.494,2.434-1.482,3.199C25.529,29.617,24.186,30,22.491,30H17V15.009z
+M20,20.946
+h2.027c0.862,0,1.486-0.133,1.872-0.4c0.387-0.267,0.579-0.708,0.579-1.323c0-0.574-0.21-0.986-0.63-1.236
+c-0.421-0.249-1.087-0.374-1.996-0.374H20V20.946z
+M20,23.469v3.906h2.253c0.876,0,1.521-0.167,1.939-0.502
+s0.626-0.848,0.626-1.539c0-1.244-0.889-1.865-2.668-1.865H20z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml
new file mode 100755
index 000000000..1a6fc6313
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M17,15.009h4.547c2.126,0,3.671,0.303,4.632,0.907c0.962,0.605,1.442,1.567,1.442,2.887
+c0,0.896-0.211,1.63-0.631,2.205c-0.421,0.574-0.98,0.919-1.678,1.036v0.103c0.951,0.212,1.637,0.608,2.057,1.189
+C27.79,23.916,28,24.688,28,25.652c0,1.367-0.494,2.434-1.482,3.199C25.529,29.617,24.186,30,22.491,30H17V15.009z
+M20,20.946
+h2.027c0.862,0,1.486-0.133,1.872-0.4c0.387-0.267,0.579-0.708,0.579-1.323c0-0.574-0.21-0.986-0.63-1.236
+c-0.421-0.249-1.087-0.374-1.996-0.374H20V20.946z
+M20,23.469v3.906h2.253c0.876,0,1.521-0.167,1.939-0.502
+s0.626-0.848,0.626-1.539c0-1.244-0.889-1.865-2.668-1.865H20z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml
new file mode 100755
index 000000000..cdb305cc5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M17,15.009h4.547c2.126,0,3.671,0.303,4.632,0.907c0.962,0.605,1.442,1.567,1.442,2.887
+c0,0.896-0.211,1.63-0.631,2.205c-0.421,0.574-0.98,0.919-1.678,1.036v0.103c0.951,0.212,1.637,0.608,2.057,1.189
+C27.79,23.916,28,24.688,28,25.652c0,1.367-0.494,2.434-1.482,3.199C25.529,29.617,24.186,30,22.491,30H17V15.009z
+M20,20.946
+h2.027c0.862,0,1.486-0.133,1.872-0.4c0.387-0.267,0.579-0.708,0.579-1.323c0-0.574-0.21-0.986-0.63-1.236
+c-0.421-0.249-1.087-0.374-1.996-0.374H20V20.946z
+M20,23.469v3.906h2.253c0.876,0,1.521-0.167,1.939-0.502
+s0.626-0.848,0.626-1.539c0-1.244-0.889-1.865-2.668-1.865H20z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml
new file mode 100755
index 000000000..e35075d61
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_bold_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_bold_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_bold_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_bold"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml
new file mode 100755
index 000000000..96b241307
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44.0"
+ android:viewportHeight="44.0">
+ <path
+ android:pathData="M13.93,26H12.79v-4.16H9.17V26H8.04v-9h1.13v3.87h3.62V17h1.14V26z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M20.97,17.97H18.6V26h-1.13v-8.03h-2.36V17h5.86V17.97z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M23.75,17l2.35,7.34L28.45,17h1.46v9h-1.13v-3.51l0.1,-3.51L26.53,26h-0.87l-2.34,-6.99l0.1,3.49V26h-1.13v-9H23.75z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M33,25.03h3.53V26h-4.67v-9h1.14V25.03z"
+ android:fillColor="#A6BCCC"/>
+</vector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml
new file mode 100755
index 000000000..6e5b8074e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44.0"
+ android:viewportHeight="44.0">
+ <path
+ android:pathData="M13.93,26H12.79v-4.16H9.17V26H8.04v-9h1.13v3.87h3.62V17h1.14V26z"
+ android:fillColor="#E8EEF2"/>
+ <path
+ android:pathData="M20.97,17.97H18.6V26h-1.13v-8.03h-2.36V17h5.86V17.97z"
+ android:fillColor="#E8EEF2"/>
+ <path
+ android:pathData="M23.75,17l2.35,7.34L28.45,17h1.46v9h-1.13v-3.51l0.1,-3.51L26.53,26h-0.87l-2.34,-6.99l0.1,3.49V26h-1.13v-9H23.75z"
+ android:fillColor="#E8EEF2"/>
+ <path
+ android:pathData="M33,25.03h3.53V26h-4.67v-9h1.14V25.03z"
+ android:fillColor="#E8EEF2"/>
+</vector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml
new file mode 100755
index 000000000..331d9a8f9
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml
@@ -0,0 +1,19 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44.0"
+ android:viewportHeight="44.0">
+ <path android:fillColor="#E1EBF1" android:pathData="M0,0h44v44h-44z"/>
+ <path
+ android:pathData="M13.93,26H12.79v-4.16H9.17V26H8.04v-9h1.13v3.87h3.62V17h1.14V26z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M20.97,17.97H18.6V26h-1.13v-8.03h-2.36V17h5.86V17.97z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M23.75,17l2.35,7.34L28.45,17h1.46v9h-1.13v-3.51l0.1,-3.51L26.53,26h-0.87l-2.34,-6.99l0.1,3.49V26h-1.13v-9H23.75z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M33,25.03h3.53V26h-4.67v-9h1.14V25.03z"
+ android:fillColor="#A6BCCC"/>
+</vector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml
new file mode 100755
index 000000000..767d6dd6b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_html_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_html_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_html_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_html"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml
new file mode 100755
index 000000000..bf4a9ae27
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M 20.536 15 L 20.109 17 L 21.609 17 L 19.263 28 L 17.763 28 L 17.336 30 L 23.464 30 L 23.89 28 L 22.39 28 L 24.737 17 L 26.237 17 L 26.664 15 Z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml
new file mode 100755
index 000000000..acb50bd33
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M 20.536 15 L 20.109 17 L 21.609 17 L 19.263 28 L 17.763 28 L 17.336 30 L 23.464 30 L 23.89 28 L 22.39 28 L 24.737 17 L 26.237 17 L 26.664 15 Z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml
new file mode 100755
index 000000000..7c8e4de79
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M 20.536 15 L 20.109 17 L 21.609 17 L 19.263 28 L 17.763 28 L 17.336 30 L 23.464 30 L 23.89 28 L 22.39 28 L 24.737 17 L 26.237 17 L 26.664 15 Z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml
new file mode 100755
index 000000000..6136a6805
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_italic_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_italic_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_italic_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_italic"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml
new file mode 100755
index 000000000..9ca70c1b8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M27,23H17v-2h10V23z
+M28,17h-1c-1.631,0-3.065,0.792-3.977,2H27h1c1.103,0,2,0.897,2,2v2
+c0,1.103-0.897,2-2,2h-1h-3.977c0.913,1.208,2.347,2,3.977,2h1c2.209,0,4-1.791,4-4v-2C32,18.791,30.209,17,28,17z
+M12,21v2
+c0,2.209,1.791,4,4,4h1c1.63,0,3.065-0.792,3.977-2H17h-1c-1.103,0-2-0.897-2-2v-2c0-1.103,0.897-2,2-2h1h3.977
+c-0.913-1.208-2.347-2-3.977-2h-1C13.791,17,12,18.791,12,21z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml
new file mode 100755
index 000000000..d1b4d83f2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M27,23H17v-2h10V23z
+M28,17h-1c-1.631,0-3.065,0.792-3.977,2H27h1c1.103,0,2,0.897,2,2v2
+c0,1.103-0.897,2-2,2h-1h-3.977c0.913,1.208,2.347,2,3.977,2h1c2.209,0,4-1.791,4-4v-2C32,18.791,30.209,17,28,17z
+M12,21v2
+c0,2.209,1.791,4,4,4h1c1.63,0,3.065-0.792,3.977-2H17h-1c-1.103,0-2-0.897-2-2v-2c0-1.103,0.897-2,2-2h1h3.977
+c-0.913-1.208-2.347-2-3.977-2h-1C13.791,17,12,18.791,12,21z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml
new file mode 100755
index 000000000..6ce750b27
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M27,23H17v-2h10V23z
+M28,17h-1c-1.631,0-3.065,0.792-3.977,2H27h1c1.103,0,2,0.897,2,2v2
+c0,1.103-0.897,2-2,2h-1h-3.977c0.913,1.208,2.347,2,3.977,2h1c2.209,0,4-1.791,4-4v-2C32,18.791,30.209,17,28,17z
+M12,21v2
+c0,2.209,1.791,4,4,4h1c1.63,0,3.065-0.792,3.977-2H17h-1c-1.103,0-2-0.897-2-2v-2c0-1.103,0.897-2,2-2h1h3.977
+c-0.913-1.208-2.347-2-3.977-2h-1C13.791,17,12,18.791,12,21z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml
new file mode 100755
index 000000000..eaad54d27
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_link_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_link_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_link_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_link"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml
new file mode 100755
index 000000000..231c3dce9
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M33,14v2h-3v3h-2v-3h-3v-2h3v-3h2v3H33z
+M24.5,21c0.828,0,1.5-0.672,1.5-1.5S25.328,18,24.5,18
+S23,18.672,23,19.5S23.672,21,24.5,21z
+M28,24.234l-0.513-0.57c-0.794-0.885-2.181-0.885-2.976,0l-0.656,0.731L19,19l-3,3.333V16h7
+v-2h-7c-1.105,0-2,0.895-2,2v12c0,1.105,0.895,2,2,2h12c1.105,0,2-0.895,2-2v-7h-2V24.234z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml
new file mode 100755
index 000000000..d06f52747
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M33,14v2h-3v3h-2v-3h-3v-2h3v-3h2v3H33z
+M24.5,21c0.828,0,1.5-0.672,1.5-1.5S25.328,18,24.5,18
+S23,18.672,23,19.5S23.672,21,24.5,21z
+M28,24.234l-0.513-0.57c-0.794-0.885-2.181-0.885-2.976,0l-0.656,0.731L19,19l-3,3.333V16h7
+v-2h-7c-1.105,0-2,0.895-2,2v12c0,1.105,0.895,2,2,2h12c1.105,0,2-0.895,2-2v-7h-2V24.234z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml
new file mode 100755
index 000000000..058e338f4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M33,14v2h-3v3h-2v-3h-3v-2h3v-3h2v3H33z
+M24.5,21c0.828,0,1.5-0.672,1.5-1.5S25.328,18,24.5,18
+S23,18.672,23,19.5S23.672,21,24.5,21z
+M28,24.234l-0.513-0.57c-0.794-0.885-2.181-0.885-2.976,0l-0.656,0.731L19,19l-3,3.333V16h7
+v-2h-7c-1.105,0-2,0.895-2,2v12c0,1.105,0.895,2,2,2h12c1.105,0,2-0.895,2-2v-7h-2V24.234z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml
new file mode 100755
index 000000000..d0c31e294
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_media_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_media_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_media_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_media"/>
+</selector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml
new file mode 100755
index 000000000..38d270bd7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M13,15h18v4H13V15z M13,29h18v-4H13V29z M19,23h6v-2h-6V23z M13,23h4v-2h-4V23z
+M27,23h4v-2h-4V23z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml
new file mode 100755
index 000000000..614bbad28
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M13,15h18v4H13V15z M13,29h18v-4H13V29z M19,23h6v-2h-6V23z M13,23h4v-2h-4V23z
+M27,23h4v-2h-4V23z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml
new file mode 100755
index 000000000..aa3eff018
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M13,15h18v4H13V15z M13,29h18v-4H13V29z M19,23h6v-2h-6V23z M13,23h4v-2h-4V23z
+M27,23h4v-2h-4V23z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml
new file mode 100755
index 000000000..a7160f486
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M18,29h13v-2H18V29z M18,23h13v-2H18V23z M18,15v2h13v-2H18z M13.575,15.252
+c0.107-0.096,0.197-0.188,0.269-0.275c-0.012,0.228-0.018,0.48-0.018,0.756V18h1.175v-4.283h-1.042l-1.471,1.198l0.601,0.738
+L13.575,15.252z
+M13.909,23.016c0.475-0.426,0.785-0.715,0.93-0.867c0.146-0.152,0.262-0.297,0.35-0.435
+c0.088-0.138,0.153-0.278,0.195-0.42c0.042-0.143,0.063-0.298,0.063-0.466c0-0.225-0.06-0.427-0.18-0.608s-0.289-0.32-0.507-0.417
+c-0.218-0.099-0.465-0.148-0.742-0.148c-0.221,0-0.419,0.022-0.596,0.067s-0.34,0.11-0.491,0.195
+c-0.15,0.085-0.336,0.226-0.557,0.423l0.636,0.744c0.174-0.15,0.33-0.264,0.467-0.341c0.138-0.077,0.274-0.116,0.409-0.116
+c0.131,0,0.233,0.032,0.305,0.097c0.072,0.064,0.108,0.152,0.108,0.264c0,0.09-0.018,0.176-0.054,0.258
+c-0.036,0.082-0.1,0.18-0.192,0.294c-0.092,0.114-0.287,0.328-0.586,0.64l-1.046,1.058V24h3.108v-0.955h-1.62V23.016L13.909,23.016
+z
+M14.439,27.762v-0.018c0.307-0.086,0.541-0.225,0.703-0.414c0.162-0.191,0.243-0.419,0.243-0.685
+c0-0.309-0.126-0.551-0.378-0.727c-0.252-0.176-0.6-0.264-1.043-0.264c-0.307,0-0.579,0.033-0.816,0.1
+c-0.237,0.067-0.469,0.178-0.696,0.334l0.48,0.773c0.293-0.184,0.576-0.275,0.85-0.275c0.147,0,0.263,0.027,0.35,0.082
+s0.13,0.139,0.13,0.252c0,0.301-0.294,0.451-0.882,0.451h-0.27v0.87h0.264c0.217,0,0.393,0.016,0.527,0.049
+c0.135,0.031,0.232,0.08,0.293,0.143c0.061,0.064,0.091,0.154,0.091,0.271c0,0.152-0.058,0.264-0.174,0.336
+c-0.116,0.07-0.301,0.106-0.555,0.106c-0.164,0-0.343-0.022-0.538-0.069c-0.194-0.045-0.385-0.116-0.573-0.212v0.961
+c0.228,0.088,0.441,0.148,0.637,0.182c0.196,0.033,0.41,0.05,0.64,0.05c0.561,0,0.998-0.114,1.314-0.343
+c0.315-0.228,0.473-0.542,0.473-0.94C15.512,28.19,15.154,27.852,14.439,27.762z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml
new file mode 100755
index 000000000..f396c5737
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M18,29h13v-2H18V29z M18,23h13v-2H18V23z M18,15v2h13v-2H18z M13.575,15.252
+c0.107-0.096,0.197-0.188,0.269-0.275c-0.012,0.228-0.018,0.48-0.018,0.756V18h1.175v-4.283h-1.042l-1.471,1.198l0.601,0.738
+L13.575,15.252z
+M13.909,23.016c0.475-0.426,0.785-0.715,0.93-0.867c0.146-0.152,0.262-0.297,0.35-0.435
+c0.088-0.138,0.153-0.278,0.195-0.42c0.042-0.143,0.063-0.298,0.063-0.466c0-0.225-0.06-0.427-0.18-0.608s-0.289-0.32-0.507-0.417
+c-0.218-0.099-0.465-0.148-0.742-0.148c-0.221,0-0.419,0.022-0.596,0.067s-0.34,0.11-0.491,0.195
+c-0.15,0.085-0.336,0.226-0.557,0.423l0.636,0.744c0.174-0.15,0.33-0.264,0.467-0.341c0.138-0.077,0.274-0.116,0.409-0.116
+c0.131,0,0.233,0.032,0.305,0.097c0.072,0.064,0.108,0.152,0.108,0.264c0,0.09-0.018,0.176-0.054,0.258
+c-0.036,0.082-0.1,0.18-0.192,0.294c-0.092,0.114-0.287,0.328-0.586,0.64l-1.046,1.058V24h3.108v-0.955h-1.62V23.016L13.909,23.016
+z
+M14.439,27.762v-0.018c0.307-0.086,0.541-0.225,0.703-0.414c0.162-0.191,0.243-0.419,0.243-0.685
+c0-0.309-0.126-0.551-0.378-0.727c-0.252-0.176-0.6-0.264-1.043-0.264c-0.307,0-0.579,0.033-0.816,0.1
+c-0.237,0.067-0.469,0.178-0.696,0.334l0.48,0.773c0.293-0.184,0.576-0.275,0.85-0.275c0.147,0,0.263,0.027,0.35,0.082
+s0.13,0.139,0.13,0.252c0,0.301-0.294,0.451-0.882,0.451h-0.27v0.87h0.264c0.217,0,0.393,0.016,0.527,0.049
+c0.135,0.031,0.232,0.08,0.293,0.143c0.061,0.064,0.091,0.154,0.091,0.271c0,0.152-0.058,0.264-0.174,0.336
+c-0.116,0.07-0.301,0.106-0.555,0.106c-0.164,0-0.343-0.022-0.538-0.069c-0.194-0.045-0.385-0.116-0.573-0.212v0.961
+c0.228,0.088,0.441,0.148,0.637,0.182c0.196,0.033,0.41,0.05,0.64,0.05c0.561,0,0.998-0.114,1.314-0.343
+c0.315-0.228,0.473-0.542,0.473-0.94C15.512,28.19,15.154,27.852,14.439,27.762z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml
new file mode 100755
index 000000000..915e6c8bf
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M18,29h13v-2H18V29z M18,23h13v-2H18V23z M18,15v2h13v-2H18z M13.575,15.252
+c0.107-0.096,0.197-0.188,0.269-0.275c-0.012,0.228-0.018,0.48-0.018,0.756V18h1.175v-4.283h-1.042l-1.471,1.198l0.601,0.738
+L13.575,15.252z
+M13.909,23.016c0.475-0.426,0.785-0.715,0.93-0.867c0.146-0.152,0.262-0.297,0.35-0.435
+c0.088-0.138,0.153-0.278,0.195-0.42c0.042-0.143,0.063-0.298,0.063-0.466c0-0.225-0.06-0.427-0.18-0.608s-0.289-0.32-0.507-0.417
+c-0.218-0.099-0.465-0.148-0.742-0.148c-0.221,0-0.419,0.022-0.596,0.067s-0.34,0.11-0.491,0.195
+c-0.15,0.085-0.336,0.226-0.557,0.423l0.636,0.744c0.174-0.15,0.33-0.264,0.467-0.341c0.138-0.077,0.274-0.116,0.409-0.116
+c0.131,0,0.233,0.032,0.305,0.097c0.072,0.064,0.108,0.152,0.108,0.264c0,0.09-0.018,0.176-0.054,0.258
+c-0.036,0.082-0.1,0.18-0.192,0.294c-0.092,0.114-0.287,0.328-0.586,0.64l-1.046,1.058V24h3.108v-0.955h-1.62V23.016L13.909,23.016
+z
+M14.439,27.762v-0.018c0.307-0.086,0.541-0.225,0.703-0.414c0.162-0.191,0.243-0.419,0.243-0.685
+c0-0.309-0.126-0.551-0.378-0.727c-0.252-0.176-0.6-0.264-1.043-0.264c-0.307,0-0.579,0.033-0.816,0.1
+c-0.237,0.067-0.469,0.178-0.696,0.334l0.48,0.773c0.293-0.184,0.576-0.275,0.85-0.275c0.147,0,0.263,0.027,0.35,0.082
+s0.13,0.139,0.13,0.252c0,0.301-0.294,0.451-0.882,0.451h-0.27v0.87h0.264c0.217,0,0.393,0.016,0.527,0.049
+c0.135,0.031,0.232,0.08,0.293,0.143c0.061,0.064,0.091,0.154,0.091,0.271c0,0.152-0.058,0.264-0.174,0.336
+c-0.116,0.07-0.301,0.106-0.555,0.106c-0.164,0-0.343-0.022-0.538-0.069c-0.194-0.045-0.385-0.116-0.573-0.212v0.961
+c0.228,0.088,0.441,0.148,0.637,0.182c0.196,0.033,0.41,0.05,0.64,0.05c0.561,0,0.998-0.114,1.314-0.343
+c0.315-0.228,0.473-0.542,0.473-0.94C15.512,28.19,15.154,27.852,14.439,27.762z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml
new file mode 100755
index 000000000..74b099634
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_ol_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_ol_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_ol_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_ol"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml
new file mode 100755
index 000000000..9bcf8198c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M21.192,25.757c0-0.88-0.23-1.618-0.69-2.217c-0.326-0.412-0.768-0.683-1.327-0.812
+c-0.55-0.128-1.07-0.137-1.54-0.028c-0.16-0.95,0.1-1.956,0.76-3.022c0.66-1.065,1.515-1.867,2.558-2.403L19.372,15
+c-0.8,0.396-1.56,0.898-2.26,1.505c-0.71,0.607-1.34,1.305-1.9,2.094c-0.56,0.789-0.98,1.68-1.25,2.69
+c-0.27,1.01-0.345,2.04-0.216,3.1c0.168,1.4,0.62,2.52,1.356,3.35C15.837,28.58,16.754,29,17.85,29c0.965,0,1.766-0.29,2.4-0.878
+c0.628-0.576,0.94-1.365,0.94-2.368L21.192,25.757z
+M30.316,25.757c0-0.88-0.23-1.618-0.69-2.217
+c-0.326-0.42-0.77-0.692-1.327-0.817c-0.56-0.124-1.073-0.13-1.54-0.022c-0.16-0.94,0.09-1.95,0.752-3.02
+c0.66-1.06,1.513-1.86,2.556-2.4L28.49,15c-0.8,0.396-1.555,0.898-2.26,1.505c-0.708,0.607-1.34,1.305-1.894,2.094
+c-0.556,0.79-0.97,1.68-1.24,2.69c-0.273,1.002-0.345,2.04-0.217,3.1c0.166,1.4,0.616,2.52,1.35,3.35
+c0.733,0.834,1.647,1.252,2.743,1.252c0.967,0,1.768-0.29,2.402-0.877c0.627-0.576,0.942-1.365,0.942-2.368L30.316,25.757z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml
new file mode 100755
index 000000000..dd03e26d6
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M21.192,25.757c0-0.88-0.23-1.618-0.69-2.217c-0.326-0.412-0.768-0.683-1.327-0.812
+c-0.55-0.128-1.07-0.137-1.54-0.028c-0.16-0.95,0.1-1.956,0.76-3.022c0.66-1.065,1.515-1.867,2.558-2.403L19.372,15
+c-0.8,0.396-1.56,0.898-2.26,1.505c-0.71,0.607-1.34,1.305-1.9,2.094c-0.56,0.789-0.98,1.68-1.25,2.69
+c-0.27,1.01-0.345,2.04-0.216,3.1c0.168,1.4,0.62,2.52,1.356,3.35C15.837,28.58,16.754,29,17.85,29c0.965,0,1.766-0.29,2.4-0.878
+c0.628-0.576,0.94-1.365,0.94-2.368L21.192,25.757z
+M30.316,25.757c0-0.88-0.23-1.618-0.69-2.217
+c-0.326-0.42-0.77-0.692-1.327-0.817c-0.56-0.124-1.073-0.13-1.54-0.022c-0.16-0.94,0.09-1.95,0.752-3.02
+c0.66-1.06,1.513-1.86,2.556-2.4L28.49,15c-0.8,0.396-1.555,0.898-2.26,1.505c-0.708,0.607-1.34,1.305-1.894,2.094
+c-0.556,0.79-0.97,1.68-1.24,2.69c-0.273,1.002-0.345,2.04-0.217,3.1c0.166,1.4,0.616,2.52,1.35,3.35
+c0.733,0.834,1.647,1.252,2.743,1.252c0.967,0,1.768-0.29,2.402-0.877c0.627-0.576,0.942-1.365,0.942-2.368L30.316,25.757z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml
new file mode 100755
index 000000000..51b1f7d3e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M21.192,25.757c0-0.88-0.23-1.618-0.69-2.217c-0.326-0.412-0.768-0.683-1.327-0.812
+c-0.55-0.128-1.07-0.137-1.54-0.028c-0.16-0.95,0.1-1.956,0.76-3.022c0.66-1.065,1.515-1.867,2.558-2.403L19.372,15
+c-0.8,0.396-1.56,0.898-2.26,1.505c-0.71,0.607-1.34,1.305-1.9,2.094c-0.56,0.789-0.98,1.68-1.25,2.69
+c-0.27,1.01-0.345,2.04-0.216,3.1c0.168,1.4,0.62,2.52,1.356,3.35C15.837,28.58,16.754,29,17.85,29c0.965,0,1.766-0.29,2.4-0.878
+c0.628-0.576,0.94-1.365,0.94-2.368L21.192,25.757z
+M30.316,25.757c0-0.88-0.23-1.618-0.69-2.217
+c-0.326-0.42-0.77-0.692-1.327-0.817c-0.56-0.124-1.073-0.13-1.54-0.022c-0.16-0.94,0.09-1.95,0.752-3.02
+c0.66-1.06,1.513-1.86,2.556-2.4L28.49,15c-0.8,0.396-1.555,0.898-2.26,1.505c-0.708,0.607-1.34,1.305-1.894,2.094
+c-0.556,0.79-0.97,1.68-1.24,2.69c-0.273,1.002-0.345,2.04-0.217,3.1c0.166,1.4,0.616,2.52,1.35,3.35
+c0.733,0.834,1.647,1.252,2.743,1.252c0.967,0,1.768-0.29,2.402-0.877c0.627-0.576,0.942-1.365,0.942-2.368L30.316,25.757z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml
new file mode 100755
index 000000000..42daf634c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_quote_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_quote_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_quote_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_quote"/>
+</selector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml
new file mode 100755
index 000000000..5b11a2448
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M24.348,22H31v2h-4.613c0.239,0.515,0.368,1.094,0.368,1.748c0,1.317-0.474,2.355-1.423,3.114
+C24.385,29.621,23.066,30,21.376,30c-1.557,0-2.934-0.293-4.132-0.878v-2.874c0.985,0.439,1.818,0.749,2.5,0.928
+c0.682,0.181,1.306,0.27,1.872,0.27c0.679,0,1.2-0.129,1.562-0.39c0.363-0.259,0.545-0.644,0.545-1.158
+c0-0.285-0.08-0.54-0.24-0.763c-0.16-0.222-0.394-0.437-0.704-0.643c-0.18-0.12-0.482-0.287-0.88-0.491H13v-2h5.597H24.348z
+M20.82,20c-0.073-0.077-0.143-0.155-0.193-0.235c-0.126-0.202-0.189-0.441-0.189-0.713c0-0.439,0.156-0.795,0.469-1.068
+c0.313-0.273,0.762-0.409,1.348-0.409c0.492,0,0.993,0.063,1.502,0.19c0.509,0.126,1.153,0.349,1.931,0.669l0.998-2.405
+c-0.752-0.326-1.472-0.579-2.16-0.758C23.836,15.09,23.113,15,22.354,15c-1.544,0-2.753,0.369-3.628,1.108
+c-0.874,0.739-1.312,1.753-1.312,3.044c0,0.302,0.036,0.58,0.088,0.848H20.82z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml
new file mode 100755
index 000000000..bb763c150
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M24.348,22H31v2h-4.613c0.239,0.515,0.368,1.094,0.368,1.748c0,1.317-0.474,2.355-1.423,3.114
+C24.385,29.621,23.066,30,21.376,30c-1.557,0-2.934-0.293-4.132-0.878v-2.874c0.985,0.439,1.818,0.749,2.5,0.928
+c0.682,0.181,1.306,0.27,1.872,0.27c0.679,0,1.2-0.129,1.562-0.39c0.363-0.259,0.545-0.644,0.545-1.158
+c0-0.285-0.08-0.54-0.24-0.763c-0.16-0.222-0.394-0.437-0.704-0.643c-0.18-0.12-0.482-0.287-0.88-0.491H13v-2h5.597H24.348z
+M20.82,20c-0.073-0.077-0.143-0.155-0.193-0.235c-0.126-0.202-0.189-0.441-0.189-0.713c0-0.439,0.156-0.795,0.469-1.068
+c0.313-0.273,0.762-0.409,1.348-0.409c0.492,0,0.993,0.063,1.502,0.19c0.509,0.126,1.153,0.349,1.931,0.669l0.998-2.405
+c-0.752-0.326-1.472-0.579-2.16-0.758C23.836,15.09,23.113,15,22.354,15c-1.544,0-2.753,0.369-3.628,1.108
+c-0.874,0.739-1.312,1.753-1.312,3.044c0,0.302,0.036,0.58,0.088,0.848H20.82z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml
new file mode 100755
index 000000000..bd8401f92
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M24.348,22H31v2h-4.613c0.239,0.515,0.368,1.094,0.368,1.748c0,1.317-0.474,2.355-1.423,3.114
+C24.385,29.621,23.066,30,21.376,30c-1.557,0-2.934-0.293-4.132-0.878v-2.874c0.985,0.439,1.818,0.749,2.5,0.928
+c0.682,0.181,1.306,0.27,1.872,0.27c0.679,0,1.2-0.129,1.562-0.39c0.363-0.259,0.545-0.644,0.545-1.158
+c0-0.285-0.08-0.54-0.24-0.763c-0.16-0.222-0.394-0.437-0.704-0.643c-0.18-0.12-0.482-0.287-0.88-0.491H13v-2h5.597H24.348z
+M20.82,20c-0.073-0.077-0.143-0.155-0.193-0.235c-0.126-0.202-0.189-0.441-0.189-0.713c0-0.439,0.156-0.795,0.469-1.068
+c0.313-0.273,0.762-0.409,1.348-0.409c0.492,0,0.993,0.063,1.502,0.19c0.509,0.126,1.153,0.349,1.931,0.669l0.998-2.405
+c-0.752-0.326-1.472-0.579-2.16-0.758C23.836,15.09,23.113,15,22.354,15c-1.544,0-2.753,0.369-3.628,1.108
+c-0.874,0.739-1.312,1.753-1.312,3.044c0,0.302,0.036,0.58,0.088,0.848H20.82z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml
new file mode 100644
index 000000000..573755e91
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_strikethrough_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_strikethrough_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_strikethrough_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_strikethrough"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml
new file mode 100755
index 000000000..8e23d2154
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M19,29h12v-2H19V29z M19,23h12v-2H19V23z M19,15v2h12v-2H19z
+M15,14.5c-0.828,0-1.5,0.672-1.5,1.5
+c0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5C16.5,15.172,15.828,14.5,15,14.5z
+M15,20.5c-0.828,0-1.5,0.672-1.5,1.5
+s0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5S15.828,20.5,15,20.5z
+M15,26.5c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5
+s1.5-0.672,1.5-1.5S15.828,26.5,15,26.5z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml
new file mode 100755
index 000000000..c1047a0f7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M19,29h12v-2H19V29z M19,23h12v-2H19V23z M19,15v2h12v-2H19z
+M15,14.5c-0.828,0-1.5,0.672-1.5,1.5
+c0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5C16.5,15.172,15.828,14.5,15,14.5z
+M15,20.5c-0.828,0-1.5,0.672-1.5,1.5
+s0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5S15.828,20.5,15,20.5z
+M15,26.5c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5
+s1.5-0.672,1.5-1.5S15.828,26.5,15,26.5z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml
new file mode 100755
index 000000000..de0c4958b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M19,29h12v-2H19V29z M19,23h12v-2H19V23z M19,15v2h12v-2H19z
+M15,14.5c-0.828,0-1.5,0.672-1.5,1.5
+c0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5C16.5,15.172,15.828,14.5,15,14.5z
+M15,20.5c-0.828,0-1.5,0.672-1.5,1.5
+s0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5S15.828,20.5,15,20.5z
+M15,26.5c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5
+s1.5-0.672,1.5-1.5S15.828,26.5,15,26.5z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml
new file mode 100755
index 000000000..98cffc7a7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_ul_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_ul_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_ul_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_ul"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml b/libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml
new file mode 100644
index 000000000..fa9c56ac4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:drawable="@drawable/ic_close_white_24dp"
+ android:left="4dp"
+ android:right="56dp"/>
+</layer-list> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml
new file mode 100644
index 000000000..5e7ca3ef8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_bold" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml
new file mode 100644
index 000000000..66d8b631f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_bold_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_bold_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_bold_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml
new file mode 100644
index 000000000..13689b19a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_italic" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml
new file mode 100644
index 000000000..f0fd90497
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_italic_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_italic_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_italic_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml
new file mode 100644
index 000000000..d376ceef0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_admin_links" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml
new file mode 100644
index 000000000..4c488f471
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_link_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_link_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_admin_links_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml
new file mode 100644
index 000000000..9d4cbf83e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/noticon_picture" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml
new file mode 100644
index 000000000..ece70b5c4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_media_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_media_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/noticon_picture_grey" android:gravity="center"/>
+ </item>
+</selector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml
new file mode 100644
index 000000000..0c7870ff5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_insertmore" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml
new file mode 100644
index 000000000..f28a013ce
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_more_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_more_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_insertmore_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml
new file mode 100644
index 000000000..b848cd831
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_format_quote" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml
new file mode 100644
index 000000000..ad0a2e684
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_quote_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_quote_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_format_quote_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml
new file mode 100644
index 000000000..43762ab9e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_strikethrough" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml
new file mode 100644
index 000000000..d96797dc7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_strike_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_strike_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_strikethrough_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml
new file mode 100644
index 000000000..786fe8d49
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_underline" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml
new file mode 100644
index 000000000..38ea66806
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_underline_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_underline_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_underline_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml b/libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml
new file mode 100644
index 000000000..74267734f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="@dimen/margin_extra_large"
+ android:insetRight="@dimen/margin_extra_large">
+ <shape android:shape="rectangle">
+
+ <solid android:color="@color/legacy_format_bar_background" />
+
+ </shape>
+</inset>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml b/libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml
new file mode 100644
index 000000000..4475720c2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/legacy_pressed_wordpress" />
+</shape>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml b/libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml
new file mode 100644
index 000000000..fbf846f5c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:exitFadeDuration="@android:integer/config_mediumAnimTime" >
+ <item android:state_pressed="false" android:state_focused="true" android:drawable="@drawable/list_focused_wordpress" />
+ <item android:state_pressed="true" android:drawable="@drawable/pressed_background_wordpress" />
+ <item android:drawable="@android:color/transparent" />
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml b/libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml
new file mode 100644
index 000000000..fb826c2ff
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.editor.EditorWebView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ /> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml
new file mode 100644
index 000000000..fc2eb283e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:background="@color/format_bar_background"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_marginRight="@dimen/format_bar_right_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/format_bar_left_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/format_bar_vertical_divider"
+ android:layout_width="@dimen/format_bar_vertical_divider_width"
+ android:layout_height="@dimen/format_bar_vertical_divider_height"
+ android:layout_gravity="center"
+ style="@style/Divider"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarHtmlButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml
new file mode 100644
index 000000000..3ee179ba2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/format_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:background="@color/format_bar_background"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_marginRight="@dimen/format_bar_right_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/format_bar_vertical_divider"
+ android:layout_width="@dimen/format_bar_vertical_divider_width"
+ android:layout_height="@dimen/format_bar_vertical_divider_height"
+ android:layout_gravity="center"
+ style="@style/Divider"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarHtmlButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml
new file mode 100644
index 000000000..f4a64666b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height_tablet"
+ android:layout_gravity="bottom"
+ android:background="@color/format_bar_background"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height_tablet"
+ android:layout_gravity="bottom"
+ android:gravity="center"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_strikethrough"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/format_bar_button_strikethrough_selector"
+ android:tag="@string/format_bar_tag_strikethrough"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml b/libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml
new file mode 100644
index 000000000..840917184
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_medium"
+ android:layout_gravity="center_vertical">
+
+ <EditText
+ android:id="@+id/linkURL"
+ android:inputType="textUri"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/link_enter_url"
+ android:imeOptions="actionNext" />
+
+ <EditText
+ android:id="@+id/linkText"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/linkURL"
+ android:hint="@string/link_enter_url_text" />
+
+ <Button
+ android:id="@+id/ok"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/linkText"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:text="@android:string/ok" />
+
+ <Button
+ android:id="@+id/cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@id/ok"
+ android:layout_toLeftOf="@id/ok"
+ android:text="@android:string/cancel" />
+
+</RelativeLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml b/libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml
new file mode 100644
index 000000000..b2d4fdb9c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal">
+
+ <EditText
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/title"
+ android:singleLine="true"/>
+
+ <EditText
+ android:id="@+id/caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/caption"
+ android:singleLine="true"
+ android:inputType="textCapSentences"/>
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/horizontal_alignment"
+ android:textStyle="bold"
+ android:textColor="@color/image_options_label"/>
+
+ <Spinner
+ android:id="@+id/alignment_spinner"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/image_alignment"/>
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/width"
+ android:textStyle="bold"
+ android:textColor="@color/image_options_label"/>
+
+ <SeekBar
+ android:layout_height="wrap_content"
+ android:id="@+id/imageWidth"
+ android:layout_width="match_parent"/>
+
+ <EditText
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:imeOptions="actionDone"
+ android:id="@+id/imageWidthText"
+ android:layout_gravity="left|center_vertical"
+ android:singleLine="true"
+ android:inputType="number"/>
+
+ <CheckBox
+ android:layout_gravity="left"
+ android:text="@string/featured"
+ android:id="@+id/featuredImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+
+ <CheckBox
+ android:layout_gravity="left"
+ android:text="@string/featured_in_post"
+ android:id="@+id/featuredInPost"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml b/libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml
new file mode 100644
index 000000000..087bb1ba7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/image_settings_dialog_extra_margin"
+ android:layout_marginLeft="@dimen/image_settings_dialog_extra_margin"
+ android:orientation="vertical"
+ android:padding="@dimen/image_settings_dialog_padding">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="@dimen/image_settings_dialog_thumbnail_container_margin_bottom">
+
+ <ImageView
+ android:id="@+id/image_thumbnail"
+ android:contentDescription="@string/image_thumbnail"
+ android:layout_width="@dimen/image_settings_dialog_thumbnail_size"
+ android:layout_height="@dimen/image_settings_dialog_thumbnail_size"
+ android:layout_marginLeft="@dimen/image_settings_dialog_thumbnail_left_margin"
+ android:layout_marginRight="@dimen/image_settings_dialog_thumbnail_right_margin"/>
+
+ <TextView
+ android:id="@+id/image_filename"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/image_settings_dialog_filename_margin_left"
+ android:layout_gravity="center_vertical"/>
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/title"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_caption"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_alt_text"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_alt_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_alignment"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <Spinner
+ android:id="@+id/alignment_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/alignment_array"
+ android:layout_marginStart="@dimen/image_settings_dialog_input_field_start_margin"
+ android:layout_marginLeft="@dimen/image_settings_dialog_input_field_start_margin"
+ android:prompt="@string/image_alignment"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_link_to"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_link_to"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textNoSuggestions"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_width"
+ style="@style/ImageOptionsDialogLabel"/>
+
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/image_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ app:srcCompat="@drawable/media_icon_32dp"/>
+
+ <SeekBar
+ android:id="@+id/image_width_seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/image_icon"/>
+ </RelativeLayout>
+
+ <EditText
+ android:id="@+id/image_width_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:imeOptions="actionDone"
+ android:singleLine="true"
+ android:inputType="number"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <CheckBox
+ android:id="@+id/featuredImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/featured"
+ android:layout_marginStart="@dimen/image_settings_dialog_input_field_start_margin"
+ android:layout_marginLeft="@dimen/image_settings_dialog_input_field_start_margin"
+ android:visibility="gone"/>
+ </LinearLayout>
+</ScrollView>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml b/libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml
new file mode 100644
index 000000000..c303163d0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/link_dialog_margin_inner"
+ android:layout_marginLeft="@dimen/link_dialog_margin_outer"
+ android:layout_marginRight="@dimen/link_dialog_margin_outer"
+ android:layout_marginTop="@dimen/link_dialog_margin_outer">
+
+ <EditText
+ android:id="@+id/linkURL"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/link_enter_url"
+ android:imeOptions="actionNext"
+ android:inputType="textUri"
+ tools:ignore="HardcodedText" />
+ </android.support.design.widget.TextInputLayout>
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/link_dialog_margin_outer"
+ android:layout_marginLeft="@dimen/link_dialog_margin_outer"
+ android:layout_marginRight="@dimen/link_dialog_margin_outer"
+ android:layout_marginTop="@dimen/link_dialog_margin_inner">
+
+ <EditText
+ android:id="@+id/linkText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/link_enter_url_text"
+ android:inputType="text" />
+ </android.support.design.widget.TextInputLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml b/libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml
new file mode 100644
index 000000000..b02cf21da
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.editor.EditorWebViewCompatibility
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ /> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml
new file mode 100644
index 000000000..1fa988beb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_alignParentBottom="true"
+ android:background="@color/format_bar_background"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_marginRight="@dimen/format_bar_right_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <HorizontalScrollView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginRight="@dimen/format_bar_scroll_right_margin">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/format_bar_left_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <ImageView
+ app:srcCompat="@drawable/format_bar_chevron"
+ android:background="@android:color/transparent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView" />
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarHtmlButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml b/libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml
new file mode 100644
index 000000000..c94a08e5e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@android:color/white"
+ android:focusableInTouchMode="true">
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:orientation="vertical"
+ android:fillViewport="true"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:id="@+id/post_content_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <EditText
+ android:id="@+id/post_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:textSize="22sp"
+ android:hint="@string/post_title"
+ android:inputType="textCapSentences|textAutoCorrect" />
+
+ <org.wordpress.android.util.widgets.WPEditText
+ android:id="@+id/post_content"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:maxLength="10000000"
+ android:layout_marginLeft="@dimen/post_editor_content_side_margin"
+ android:layout_marginRight="@dimen/post_editor_content_side_margin"
+ android:layout_marginTop="8dp"
+ android:gravity="top"
+ android:layout_weight="1"
+ android:hint="@string/post_content"
+ android:background="@null"
+ android:lineSpacingExtra="4dp"
+ android:textSize="18sp"
+ android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
+ android:imeOptions="flagNoExtractUi"
+ android:textColorLink="@color/legacy_placeholder_content_text" />
+ </LinearLayout>
+ </ScrollView>
+
+ <LinearLayout
+ android:id="@+id/post_settings_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/list_divider" />
+
+ <Button
+ android:id="@+id/post_settings_button"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp"
+ android:gravity="center_vertical"
+ android:paddingLeft="16dp"
+ android:textSize="18sp"
+ android:drawableLeft="@drawable/ic_post_settings"
+ android:background="@drawable/selectable_background_wordpress"
+ android:text="@string/post_settings"
+ android:drawablePadding="6dp"
+ android:layout_gravity="bottom"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/format_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/legacy_format_bar_height"
+ android:layout_gravity="bottom"
+ android:background="@color/legacy_format_bar_background"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <HorizontalScrollView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ToggleButton
+ android:id="@+id/bold"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/legacy_format_bar_button_bold_selector" />
+
+ <ToggleButton
+ android:id="@+id/em"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/legacy_format_bar_button_italic_selector" />
+
+ <ToggleButton
+ android:id="@+id/underline"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_underline"
+ android:background="@drawable/legacy_format_bar_button_underline_selector" />
+
+ <ToggleButton
+ android:id="@+id/strike"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_strike"
+ android:background="@drawable/legacy_format_bar_button_strike_selector" />
+
+ <ToggleButton
+ android:id="@+id/bquote"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/legacy_format_bar_button_quote_selector" />
+
+ <Button
+ android:id="@+id/link"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:minWidth="@dimen/legacy_format_bar_height"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/legacy_format_bar_button_link_selector" />
+
+ <Button
+ android:id="@+id/more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:minWidth="@dimen/legacy_format_bar_height"
+ android:contentDescription="@string/format_bar_description_more"
+ android:background="@drawable/legacy_format_bar_button_more_selector" />
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <Button
+ android:id="@+id/addPictureButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:minWidth="@dimen/legacy_format_bar_height"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/legacy_format_bar_button_media_selector" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml b/libs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml
new file mode 100755
index 000000000..ff0326f31
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml
@@ -0,0 +1,82 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:editor="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="org.wordpress.android.editor.EditorFragment">
+
+ <include
+ layout="@layout/editor_webview"
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/format_bar"/>
+
+ <ScrollView
+ android:id="@+id/sourceview"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@android:color/white"
+ android:orientation="vertical"
+ android:layout_above="@id/format_bar"
+ android:fillViewport="true"
+ android:visibility="gone">
+
+ <LinearLayout
+ android:id="@+id/post_content_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.editor.SourceViewEditText
+ android:id="@+id/sourceview_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/sourceview_side_margin"
+ android:layout_marginRight="@dimen/sourceview_side_margin"
+ android:layout_marginTop="@dimen/sourceview_top_margin"
+ android:layout_marginBottom="@dimen/sourceview_title_bottom_margin"
+ android:background="@null"
+ android:textSize="24sp"
+ android:textColorHint="@color/sourceview_placeholder_text"
+ android:inputType="textCapSentences|textAutoCorrect"
+ android:imeOptions="flagNoExtractUi"
+ editor:fontFile="Merriweather-Bold.ttf"/>
+
+ <View
+ android:id="@+id/sourceview_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ android:layout_marginLeft="@dimen/sourceview_side_margin"
+ android:layout_marginRight="@dimen/sourceview_side_margin"
+ style="@style/DividerSourceView"/>
+
+ <org.wordpress.android.editor.SourceViewEditText
+ android:id="@+id/sourceview_content"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="top"
+ android:layout_marginLeft="@dimen/sourceview_side_margin"
+ android:layout_marginRight="@dimen/sourceview_side_margin"
+ android:layout_marginTop="@dimen/sourceview_top_margin"
+ android:background="@null"
+ android:textSize="16sp"
+ android:maxLength="10000000"
+ android:textColorHint="@color/sourceview_placeholder_text"
+ android:inputType="textMultiLine|textCapSentences|textNoSuggestions"
+ android:lineSpacingExtra="4dp"
+ android:imeOptions="flagNoExtractUi"
+ android:typeface="monospace"/>
+ </LinearLayout>
+ </ScrollView>
+
+ <include
+ layout="@layout/format_bar"
+ android:id="@+id/format_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"/>
+
+</RelativeLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml b/libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml
new file mode 100644
index 000000000..ff5ea7df4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/menu_save"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:text="@string/save"
+ android:textAllCaps="true"
+ android:textColor="@android:color/white"/>
+</LinearLayout> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml b/libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml
new file mode 100644
index 000000000..24756188d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml
@@ -0,0 +1,13 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditorActivity">
+
+ <fragment
+ android:id="@+id/postEditor"
+ android:name="org.wordpress.android.editor.LegacyEditorFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml
new file mode 100644
index 000000000..c68bd2b8f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="sourceview_side_margin">@dimen/sourceview_side_margin_extra_large</dimen>
+ <dimen name="format_bar_button_margin_tablet">@dimen/format_bar_button_margin_tablet_extra_large</dimen>
+ <dimen name="format_bar_button_group_tablet">@dimen/format_bar_button_group_tablet_extra_large</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml
new file mode 100644
index 000000000..17b7dd6be
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="is_large_tablet_landscape">true</bool>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml
new file mode 100644
index 000000000..ad669947f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="sourceview_side_margin">@dimen/sourceview_side_margin_large</dimen>
+ <dimen name="image_settings_dialog_extra_margin">@dimen/image_settings_dialog_extra_margin_tablet</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml b/libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml
new file mode 100644
index 000000000..de5683d0f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="show_extra_side_padding">true</bool>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml
new file mode 100644
index 000000000..1f110a98b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="format_bar_button_margin_tablet">@dimen/format_bar_button_margin_tablet_large</dimen>
+ <dimen name="format_bar_button_group_tablet">@dimen/format_bar_button_group_tablet_large</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values/attrs.xml b/libs/editor/WordPressEditor/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..bfe9ab434
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/attrs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="SourceViewEditText">
+ <attr name="fontFile" format="string" />
+ </declare-styleable>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values/colors.xml b/libs/editor/WordPressEditor/src/main/res/values/colors.xml
new file mode 100755
index 000000000..44ffdaf50
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="legacy_format_bar_button_selected">#00aadc</color>
+ <color name="legacy_placeholder_content_text">#21759B</color>
+ <color name="legacy_format_bar_background">#eeeeee</color>
+ <color name="legacy_pressed_wordpress">#CC78C8E6</color>
+
+ <color name="image_options_label">@color/wp_gray</color>
+
+ <color name="format_bar_button_normal_color">@color/wp_gray_lighten_10</color>
+ <color name="format_bar_button_highlighted_color">@color/wp_blue</color>
+ <color name="format_bar_background">#f9fbfc</color>
+ <color name="format_bar_ripple_animation">@color/wp_gray_lighten_10</color>
+
+ <color name="format_bar_divider">@color/wp_gray_lighten_10</color>
+ <color name="sourceview_separator">@color/wp_gray_lighten_30</color>
+ <color name="sourceview_placeholder_text">@color/wp_gray</color>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..3f76aa7db
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/dimens.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="format_bar_height">44dp</dimen>
+ <dimen name="format_bar_height_tablet">49dp</dimen>
+ <dimen name="format_bar_left_margin">5dp</dimen>
+ <dimen name="format_bar_right_margin">2dp</dimen>
+ <dimen name="format_bar_html_button_left_margin">3dp</dimen>
+ <dimen name="format_bar_scroll_right_margin">-7dp</dimen>
+
+ <dimen name="format_bar_button_margin_tablet">@dimen/format_bar_button_margin_tablet_small</dimen>
+ <dimen name="format_bar_button_margin_tablet_small">0dp</dimen>
+ <dimen name="format_bar_button_margin_tablet_large">7dp</dimen>
+ <dimen name="format_bar_button_margin_tablet_extra_large">12dp</dimen>
+
+ <dimen name="format_bar_button_group_tablet">@dimen/format_bar_button_group_tablet_small</dimen>
+ <dimen name="format_bar_button_group_tablet_small">15dp</dimen>
+ <dimen name="format_bar_button_group_tablet_large">25dp</dimen>
+ <dimen name="format_bar_button_group_tablet_extra_large">35dp</dimen>
+
+ <dimen name="format_bar_horizontal_divider_height">1dp</dimen>
+ <dimen name="format_bar_vertical_divider_width">0.6dp</dimen>
+ <dimen name="format_bar_vertical_divider_height">28dp</dimen>
+
+ <dimen name="sourceview_side_margin">15dp</dimen>
+ <dimen name="sourceview_side_margin_large">90dp</dimen>
+ <dimen name="sourceview_side_margin_extra_large">170dp</dimen>
+ <dimen name="sourceview_top_margin">15dp</dimen>
+ <dimen name="sourceview_title_bottom_margin">15dp</dimen>
+
+ <dimen name="link_dialog_margin_inner">4dp</dimen>
+ <dimen name="link_dialog_margin_outer">16dp</dimen>
+
+ <dimen name="image_settings_dialog_padding">16dp</dimen>
+ <dimen name="image_settings_dialog_extra_margin">0dp</dimen>
+ <dimen name="image_settings_dialog_extra_margin_tablet">84dp</dimen>
+ <dimen name="image_settings_dialog_label_indent">4dp</dimen>
+ <dimen name="image_settings_dialog_label_margin_bottom">4dp</dimen>
+ <dimen name="image_settings_dialog_input_margin_bottom">16dp</dimen>
+ <dimen name="image_settings_dialog_input_field_start_margin">-4dp</dimen>
+ <dimen name="image_settings_dialog_top_separator_margin_top">16dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_container_margin_bottom">8dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_size">100dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_left_margin">4dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_right_margin">8dp</dimen>
+ <dimen name="image_settings_dialog_filename_margin_left">16dp</dimen>
+
+ <dimen name="post_editor_content_side_margin">20dp</dimen>
+
+ <dimen name="legacy_format_bar_height">40dp</dimen>
+ <dimen name="margin_extra_small">2dp</dimen>
+ <dimen name="margin_small">4dp</dimen>
+ <dimen name="margin_medium">8dp</dimen>
+ <dimen name="margin_large">12dp</dimen>
+ <dimen name="margin_extra_large">16dp</dimen>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/layouts.xml b/libs/editor/WordPressEditor/src/main/res/values/layouts.xml
new file mode 100644
index 000000000..ae7684978
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/layouts.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="is_large_tablet_landscape">false</bool>
+ <bool name="show_extra_side_padding">false</bool>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values/strings.xml b/libs/editor/WordPressEditor/src/main/res/values/strings.xml
new file mode 100644
index 000000000..d4ba277be
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/strings.xml
@@ -0,0 +1,97 @@
+<resources>
+ <string name="app_name">Editor</string>
+ <string name="post_settings">Settings</string>
+ <string name="post_content">Content</string>
+ <string name="post_title">Title</string>
+
+ <string name="ok">OK</string>
+ <string name="cancel">Cancel</string>
+ <string name="delete">Delete</string>
+ <string name="save">Save</string>
+ <string name="discard">Discard</string>
+ <string name="edit">Edit</string>
+ <string name="tap_to_try_again">Tap to try again!</string>
+ <string name="uploading">Uploading…</string>
+ <string name="uploading_gallery_placeholder">Uploading gallery…</string>
+
+ <string name="alert_insert_image_html_mode">Can\'t insert media directly in HTML mode. Please switch back to visual mode.</string>
+ <string name="alert_action_while_uploading">You are currently uploading media. Please wait until this completes.</string>
+ <string name="alert_error_adding_media">An error occurred while inserting media</string>
+
+ <string name="stop_upload_dialog_title">Stop uploading?</string>
+ <string name="stop_upload_button">Stop Upload</string>
+
+ <string name="format_bar_tag_bold" translatable="false">bold</string>
+ <string name="format_bar_tag_italic" translatable="false">italic</string>
+ <string name="format_bar_tag_blockquote" translatable="false">blockquote</string>
+ <string name="format_bar_tag_unorderedList" translatable="false">unorderedList</string>
+ <string name="format_bar_tag_orderedList" translatable="false">orderedList</string>
+ <string name="format_bar_tag_strikethrough" translatable="false">strikeThrough</string>
+ <string name="format_bar_tag_link" translatable="false">link</string>
+
+ <!-- link view -->
+ <string name="link_enter_url">URL</string>
+ <string name="link_enter_url_text">Link text (optional)</string>
+ <string name="create_a_link">Create a link</string>
+
+ <!-- image settings -->
+ <string name="image_settings">Image settings</string>
+ <string name="title">Title</string>
+ <string name="width">Width</string>
+ <string name="featured">Use as featured image</string>
+ <string name="featured_in_post">Include image in post content</string>
+ <string name="image_alignment">Alignment</string>
+ <string name="horizontal_alignment">Horizontal alignment</string>
+ <string name="caption">Caption (optional)</string>
+
+ <string name="image_settings_dismiss_dialog_title">Discard unsaved changes?</string>
+ <string name="image_settings_save_toast">Changes saved</string>
+
+ <string name="image_caption">Caption</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Link to</string>
+ <string name="image_width">Width</string>
+
+ <string name="alignment_none">None</string>
+ <string name="alignment_left">Left</string>
+ <string name="alignment_center">Center</string>
+ <string name="alignment_right">Right</string>
+
+ <string-array name="alignment_key_array" translatable="false">
+ <item>none</item>
+ <item>left</item>
+ <item>center</item>
+ <item>right</item>
+ </string-array>
+
+ <string-array name="alignment_array" translatable="false">
+ <item>@string/alignment_none</item>
+ <item>@string/alignment_left</item>
+ <item>@string/alignment_center</item>
+ <item>@string/alignment_right</item>
+ </string-array>
+
+ <!-- Accessibility - format bar button descriptions -->
+ <string name="format_bar_description_bold">Bold</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Underline</string>
+ <string name="format_bar_description_strike">Strikethrough</string>
+ <string name="format_bar_description_quote">Block quote</string>
+ <string name="format_bar_description_link">Insert link</string>
+ <string name="format_bar_description_more">Insert more</string>
+ <string name="format_bar_description_media">Insert media</string>
+ <string name="format_bar_description_ul">Unordered list</string>
+ <string name="format_bar_description_ol">Ordered list</string>
+ <string name="format_bar_description_html">HTML mode</string>
+ <string name="visual_editor">Visual editor</string>
+ <string name="image_thumbnail">Image thumbnail</string>
+
+ <string name="editor_failed_uploads_switch_html">Some media uploads have failed. You can\'t switch to HTML mode
+ in this state. Remove all failed uploads and continue?</string>
+ <string name="editor_remove_failed_uploads">Remove failed uploads</string>
+
+ <string name="editor_dropped_text_error">Error occurred while dropping text</string>
+ <string name="editor_dropped_title_images_not_allowed">Dropping images in the Title is not allowed</string>
+ <string name="editor_dropped_html_images_not_allowed">Dropping images while in HTML mode is not allowed</string>
+ <string name="editor_dropped_unsupported_files">Warning: not all dropped items are supported!</string>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/styles.xml b/libs/editor/WordPressEditor/src/main/res/values/styles.xml
new file mode 100755
index 000000000..3e99e0b88
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/styles.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="LegacyToggleButton">
+ <item name="android:minWidth">@dimen/legacy_format_bar_height</item>
+ <item name="android:minHeight">@dimen/legacy_format_bar_height</item>
+ <item name="android:textOn">""</item>
+ <item name="android:textOff">""</item>
+ </style>
+
+ <style name="FormatBarButton">
+ <item name="android:minWidth">@dimen/format_bar_height</item>
+ <item name="android:minHeight">@dimen/format_bar_height</item>
+ <item name="android:textOn">""</item>
+ <item name="android:textOff">""</item>
+ </style>
+
+ <style name="FormatBarButtonTablet" parent="FormatBarButton">
+ <item name="android:layout_marginRight">@dimen/format_bar_button_margin_tablet</item>
+ <item name="android:layout_marginLeft">@dimen/format_bar_button_margin_tablet</item>
+ </style>
+
+ <style name="FormatBarHtmlButton" parent="FormatBarButton">
+ <item name="android:layout_marginLeft">@dimen/format_bar_html_button_left_margin</item>
+ </style>
+
+ <style name="Divider">
+ <item name="android:background">@color/format_bar_divider</item>
+ </style>
+
+ <style name="DividerSourceView">
+ <item name="android:background">@color/sourceview_separator</item>
+ </style>
+
+ <style name="FormatBarTabletGroup">
+ <item name="android:layout_marginLeft">@dimen/format_bar_button_group_tablet</item>
+ <item name="android:layout_marginRight">@dimen/format_bar_button_group_tablet</item>
+ </style>
+
+ <style name="ImageOptionsDialogLabel">
+ <item name="android:textColor">@color/image_options_label</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:paddingLeft">@dimen/image_settings_dialog_label_indent</item>
+ <item name="android:layout_marginBottom">@dimen/image_settings_dialog_label_margin_bottom</item>
+ </style>
+
+ <style name="ImageOptionsDialogInput">
+ <item name="android:layout_marginBottom">@dimen/image_settings_dialog_input_margin_bottom</item>
+ </style>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml b/libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml
new file mode 100644
index 000000000..15caebf9f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="wp_gray">#87a6bc</color>
+ <color name="wp_gray_lighten_10">#a8bece</color>
+ <color name="wp_gray_lighten_20">#c8d7e1</color>
+ <color name="wp_gray_lighten_30">#e9eff3</color>
+ <color name="wp_gray_light">#f3f6f8</color>
+
+ <color name="wp_blue">#0087be</color>
+ <color name="wp_blue_light">#78dcfa</color>
+ <color name="wp_blue_medium">#00aadc</color>
+ <color name="wp_blue_dark">#005082</color>
+</resources> \ No newline at end of file
diff --git a/libs/editor/build.gradle b/libs/editor/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/editor/build.gradle
diff --git a/libs/editor/example/build.gradle b/libs/editor/example/build.gradle
new file mode 100644
index 000000000..d45489a35
--- /dev/null
+++ b/libs/editor/example/build.gradle
@@ -0,0 +1,56 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+repositories {
+ jcenter()
+}
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ applicationId "org.wordpress.editorexample"
+ minSdkVersion 16
+ targetSdkVersion 24
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile project(":WordPressEditor");
+
+ // Test libraries
+ testCompile 'junit:junit:4.11'
+ testCompile 'org.mockito:mockito-core:1.10.19'
+ testCompile 'org.robolectric:robolectric:3.0'
+
+ // Workaround for IDE bug
+ // http://stackoverflow.com/questions/22246183/android-studio-doesnt-recognize-espresso-classes
+ provided 'junit:junit:4.11'
+ provided 'org.mockito:mockito-core:1.10.19'
+}
+
+//
+// Testing
+//
+
+android.testOptions.unitTests.all {
+ include '**/*Test.class'
+ exclude '**/ApplicationTest.class'
+} \ No newline at end of file
diff --git a/libs/editor/example/proguard-rules.pro b/libs/editor/example/proguard-rules.pro
new file mode 100644
index 000000000..d8d549daa
--- /dev/null
+++ b/libs/editor/example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/max/work/android-sdk-mac/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 *;
+#}
diff --git a/libs/editor/example/src/main/AndroidManifest.xml b/libs/editor/example/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d6b56a50a
--- /dev/null
+++ b/libs/editor/example/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.editor.example" >
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name=".MainExampleActivity"
+ android:label="@string/title_activity_example">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".EditorExampleActivity"
+ android:configChanges="orientation|keyboardHidden|screenSize">
+ </activity>
+ </application>
+
+</manifest>
diff --git a/libs/editor/example/src/main/assets/example/cowboy-cat.jpg b/libs/editor/example/src/main/assets/example/cowboy-cat.jpg
new file mode 100644
index 000000000..77f930b99
--- /dev/null
+++ b/libs/editor/example/src/main/assets/example/cowboy-cat.jpg
Binary files differ
diff --git a/libs/editor/example/src/main/assets/example/example-content.html b/libs/editor/example/src/main/assets/example/example-content.html
new file mode 100644
index 000000000..5f35f4826
--- /dev/null
+++ b/libs/editor/example/src/main/assets/example/example-content.html
@@ -0,0 +1,58 @@
+<p>
+I'm a test post.
+
+<strong>Bold text</strong>
+
+<em>Italic text</em>
+
+<a href="http://www.wordpress.com">I'm a link!</a>
+
+<!--more c'est mort -->
+
+<del datetime="2014-05-19T20:55:58+00:00">Strikethrough</del>
+
+Moar Text.
+</p>
+<p>
+Code:
+<code>
+ 10 PRINT "HOWDY WORLD"
+ 20 GOTO 10
+</code>
+</p>
+<p>
+Unordered List:
+<ul>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+</ul>
+</p>
+<p>
+Ordered List:
+<ol>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+</ol>
+</p>
+<p>
+Master cleanse Intelligentsia butcher Brooklyn Tumblr. Etsy lo-fi Marfa bicycle rights. Intelligentsia Helvetica fanny pack, normcore Odd Future fixie brunch mustache aesthetic kitsch artisan cardigan mlkshk. Pour-over hashtag selfies pug Tumblr mlkshk. Food truck small batch McSweeney's trust fund, Intelligentsia bitters Brooklyn twee meh authentic. Normcore distillery American Apparel single-origin coffee artisan try-hard, stumptown XOXO tote bag fanny pack Blue Bottle Shoreditch food truck Banksy. Church-key American Apparel Blue Bottle, swag try-hard Odd Future mustache chia iPhone.
+</p>
+<p>
+Block Quote:
+<blockquote>Kale chips Schlitz forage irony, kogi Tumblr Carles chillwave Etsy pug photo booth YOLO biodiesel tote bag actually. PBR Portland yr pickled, bespoke meggings selvage letterpress kitsch plaid before they sold out put a bird on it you probably haven't heard of them. Yr master cleanse slow-carb crucifix, sustainable keytar Helvetica Tumblr High Life mumblecore narwhal cornhole deep v craft beer. Portland forage hashtag locavore, before they sold out put a bird on it irony hella. Godard kale chips street art tote bag cardigan. Church-key next level seitan keytar meggings Portland. Keffiyeh flexitarian post-ironic drinking vinegar wayfarers.</blockquote>
+</p>
+<p>
+Image:<br/><br/>
+
+<img src="example/cowboy-cat.jpg" alt="Cowboy Cat" />
+</p>
+<p>
+Fanny pack Odd Future Intelligentsia lo-fi semiotics whatever. Selvage keffiyeh mustache sustainable ethnic, chambray mumblecore McSweeney's biodiesel Pitchfork four loko disrupt post-ironic art party American Apparel. Kitsch umami beard salvia, Vice before they sold out vegan tousled lomo jean shorts pickled PBR&amp;B. Tousled Wes Anderson Shoreditch flannel, 90's XOXO quinoa whatever mumblecore cliche Truffaut stumptown. Photo booth crucifix plaid Brooklyn. Authentic letterpress PBR&amp;B, sustainable VHS master cleanse ethnic High Life. Messenger bag umami pug flannel.
+</p>
+
+<!--nextpage-->
+
+Wow, such sample, much text.
+
diff --git a/libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java
new file mode 100644
index 000000000..8723f708e
--- /dev/null
+++ b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java
@@ -0,0 +1,348 @@
+package org.wordpress.android.editor.example;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.ContextMenu;
+import android.view.DragEvent;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.wordpress.android.editor.EditorFragmentAbstract;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorDragAndDropListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent;
+import org.wordpress.android.editor.EditorMediaUploadListener;
+import org.wordpress.android.editor.ImageSettingsDialogFragment;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class EditorExampleActivity extends AppCompatActivity implements EditorFragmentListener,
+ EditorDragAndDropListener {
+ public static final String EDITOR_PARAM = "EDITOR_PARAM";
+ public static final String TITLE_PARAM = "TITLE_PARAM";
+ public static final String CONTENT_PARAM = "CONTENT_PARAM";
+ public static final String DRAFT_PARAM = "DRAFT_PARAM";
+ public static final String TITLE_PLACEHOLDER_PARAM = "TITLE_PLACEHOLDER_PARAM";
+ public static final String CONTENT_PLACEHOLDER_PARAM = "CONTENT_PLACEHOLDER_PARAM";
+ public static final int USE_NEW_EDITOR = 1;
+ public static final int USE_LEGACY_EDITOR = 2;
+
+ public static final int ADD_MEDIA_ACTIVITY_REQUEST_CODE = 1111;
+ public static final int ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE = 1112;
+ public static final int ADD_MEDIA_SLOW_NETWORK_REQUEST_CODE = 1113;
+
+ public static final String MEDIA_REMOTE_ID_SAMPLE = "123";
+
+ private static final int SELECT_IMAGE_MENU_POSITION = 0;
+ private static final int SELECT_IMAGE_FAIL_MENU_POSITION = 1;
+ private static final int SELECT_VIDEO_MENU_POSITION = 2;
+ private static final int SELECT_VIDEO_FAIL_MENU_POSITION = 3;
+ private static final int SELECT_IMAGE_SLOW_MENU_POSITION = 4;
+
+ private EditorFragmentAbstract mEditorFragment;
+
+ private Map<String, String> mFailedUploads;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getIntExtra(EDITOR_PARAM, USE_NEW_EDITOR) == USE_NEW_EDITOR) {
+ ToastUtils.showToast(this, R.string.starting_new_editor);
+ setContentView(R.layout.activity_new_editor);
+ } else {
+ ToastUtils.showToast(this, R.string.starting_legacy_editor);
+ setContentView(R.layout.activity_legacy_editor);
+ }
+
+ mFailedUploads = new HashMap<>();
+ }
+
+ @Override
+ public void onAttachFragment(Fragment fragment) {
+ super.onAttachFragment(fragment);
+ if (fragment instanceof EditorFragmentAbstract) {
+ mEditorFragment = (EditorFragmentAbstract) fragment;
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ Fragment fragment = getFragmentManager()
+ .findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG);
+ if (fragment != null && fragment.isVisible()) {
+ ((ImageSettingsDialogFragment) fragment).dismissFragment();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ menu.add(0, SELECT_IMAGE_MENU_POSITION, 0, getString(R.string.select_image));
+ menu.add(0, SELECT_IMAGE_FAIL_MENU_POSITION, 0, getString(R.string.select_image_fail));
+ menu.add(0, SELECT_VIDEO_MENU_POSITION, 0, getString(R.string.select_video));
+ menu.add(0, SELECT_VIDEO_FAIL_MENU_POSITION, 0, getString(R.string.select_video_fail));
+ menu.add(0, SELECT_IMAGE_SLOW_MENU_POSITION, 0, getString(R.string.select_image_slow_network));
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+
+ switch (item.getItemId()) {
+ case SELECT_IMAGE_MENU_POSITION:
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_image));
+
+ startActivityForResult(intent, ADD_MEDIA_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_IMAGE_FAIL_MENU_POSITION:
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_image_fail));
+
+ startActivityForResult(intent, ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_VIDEO_MENU_POSITION:
+ intent.setType("video/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_video));
+
+ startActivityForResult(intent, ADD_MEDIA_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_VIDEO_FAIL_MENU_POSITION:
+ intent.setType("video/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_video_fail));
+
+ startActivityForResult(intent, ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_IMAGE_SLOW_MENU_POSITION:
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_image_slow_network));
+
+ startActivityForResult(intent, ADD_MEDIA_SLOW_NETWORK_REQUEST_CODE);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (data == null) {
+ return;
+ }
+
+ Uri mediaUri = data.getData();
+
+ MediaFile mediaFile = new MediaFile();
+ String mediaId = String.valueOf(System.currentTimeMillis());
+ mediaFile.setMediaId(mediaId);
+ mediaFile.setVideo(mediaUri.toString().contains("video"));
+
+ switch (requestCode) {
+ case ADD_MEDIA_ACTIVITY_REQUEST_CODE:
+ mEditorFragment.appendMediaFile(mediaFile, mediaUri.toString(), null);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ simulateFileUpload(mediaId, mediaUri.toString());
+ }
+ break;
+ case ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE:
+ mEditorFragment.appendMediaFile(mediaFile, mediaUri.toString(), null);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ simulateFileUploadFail(mediaId, mediaUri.toString());
+ }
+ break;
+ case ADD_MEDIA_SLOW_NETWORK_REQUEST_CODE:
+ mEditorFragment.appendMediaFile(mediaFile, mediaUri.toString(), null);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ simulateSlowFileUpload(mediaId, mediaUri.toString());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onSettingsClicked() {
+ // TODO
+ }
+
+ @Override
+ public void onAddMediaClicked() {
+ // TODO
+ }
+
+ @Override
+ public void onMediaRetryClicked(String mediaId) {
+ if (mFailedUploads.containsKey(mediaId)) {
+ simulateFileUpload(mediaId, mFailedUploads.get(mediaId));
+ }
+ }
+
+ @Override
+ public void onMediaUploadCancelClicked(String mediaId, boolean delete) {
+
+ }
+
+ @Override
+ public void onFeaturedImageChanged(long mediaId) {
+
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(String videoId) {
+
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ return "";
+ }
+
+ @Override
+ public void onEditorFragmentInitialized() {
+ // arbitrary setup
+ mEditorFragment.setFeaturedImageSupported(true);
+ mEditorFragment.setBlogSettingMaxImageWidth("600");
+ mEditorFragment.setDebugModeEnabled(true);
+
+ // get title and content and draft switch
+ String title = getIntent().getStringExtra(TITLE_PARAM);
+ String content = getIntent().getStringExtra(CONTENT_PARAM);
+ boolean isLocalDraft = getIntent().getBooleanExtra(DRAFT_PARAM, true);
+ mEditorFragment.setTitle(title);
+ mEditorFragment.setContent(content);
+ mEditorFragment.setTitlePlaceholder(getIntent().getStringExtra(TITLE_PLACEHOLDER_PARAM));
+ mEditorFragment.setContentPlaceholder(getIntent().getStringExtra(CONTENT_PLACEHOLDER_PARAM));
+ mEditorFragment.setLocalDraft(isLocalDraft);
+ }
+
+ @Override
+ public void saveMediaFile(MediaFile mediaFile) {
+ // TODO
+ }
+
+ @Override
+ public void onTrackableEvent(TrackableEvent event) {
+ AppLog.d(T.EDITOR, "Trackable event: " + event);
+ }
+
+ private void simulateFileUpload(final String mediaId, final String mediaUrl) {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ float count = (float) 0.1;
+ while (count < 1.1) {
+ sleep(500);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadProgress(mediaId, count);
+
+ count += 0.1;
+ }
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setMediaId(MEDIA_REMOTE_ID_SAMPLE);
+ mediaFile.setFileURL(mediaUrl);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadSucceeded(mediaId, mediaFile);
+
+ if (mFailedUploads.containsKey(mediaId)) {
+ mFailedUploads.remove(mediaId);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ thread.start();
+ }
+
+ private void simulateFileUploadFail(final String mediaId, final String mediaUrl) {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ float count = (float) 0.1;
+ while (count < 0.6) {
+ sleep(500);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadProgress(mediaId, count);
+
+ count += 0.1;
+ }
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadFailed(mediaId,
+ getString(R.string.tap_to_try_again));
+
+ mFailedUploads.put(mediaId, mediaUrl);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ thread.start();
+ }
+
+ private void simulateSlowFileUpload(final String mediaId, final String mediaUrl) {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ sleep(5000);
+ float count = (float) 0.1;
+ while (count < 1.1) {
+ sleep(2000);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadProgress(mediaId, count);
+
+ count += 0.1;
+ }
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setMediaId(MEDIA_REMOTE_ID_SAMPLE);
+ mediaFile.setFileURL(mediaUrl);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadSucceeded(mediaId, mediaFile);
+
+ if (mFailedUploads.containsKey(mediaId)) {
+ mFailedUploads.remove(mediaId);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ thread.start();
+ }
+
+ @Override
+ public void onMediaDropped(ArrayList<Uri> mediaUri) {
+ // TODO
+ }
+
+ @Override
+ public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
+ // TODO
+ }
+}
diff --git a/libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java
new file mode 100644
index 000000000..52b522d38
--- /dev/null
+++ b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java
@@ -0,0 +1,86 @@
+package org.wordpress.android.editor.example;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import org.wordpress.android.editor.Utils;
+import org.wordpress.android.editor.example.EditorExampleActivity;
+
+public class MainExampleActivity extends AppCompatActivity {
+ private Activity mActivity;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mActivity = this;
+ setContentView(R.layout.activity_example);
+
+ Button newEditorPost1 = (Button) findViewById(R.id.new_editor_post_1);
+ newEditorPost1.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, getString(R.string.example_post_visual_title));
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, Utils.getHtmlFromFile(mActivity,
+ "example/example-content.html"));
+ bundle.putString(EditorExampleActivity.TITLE_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_title_placeholder));
+ bundle.putString(EditorExampleActivity.CONTENT_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_content_placeholder));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_NEW_EDITOR);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+
+ Button newEditorPostEmpty = (Button) findViewById(R.id.new_editor_post_empty);
+ newEditorPostEmpty.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, "");
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, "");
+ bundle.putString(EditorExampleActivity.TITLE_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_title_placeholder));
+ bundle.putString(EditorExampleActivity.CONTENT_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_content_placeholder));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_NEW_EDITOR);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+
+ Button legacyEditorPost1Local = (Button) findViewById(R.id.legacy_editor_post_1_local);
+ legacyEditorPost1Local.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, getString(R.string.example_post_1_title));
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, getString(R.string.example_post_1_content));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_LEGACY_EDITOR);
+ bundle.putBoolean(EditorExampleActivity.DRAFT_PARAM, true);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+
+ Button legacyEditorPost1Remote = (Button) findViewById(R.id.legacy_editor_post_1_remote);
+ legacyEditorPost1Remote.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, getString(R.string.example_post_1_title));
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, getString(R.string.example_post_1_content));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_LEGACY_EDITOR);
+ bundle.putBoolean(EditorExampleActivity.DRAFT_PARAM, false);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+ }
+}
diff --git a/libs/editor/example/src/main/res/layout/activity_example.xml b/libs/editor/example/src/main/res/layout/activity_example.xml
new file mode 100644
index 000000000..b94cea8ab
--- /dev/null
+++ b/libs/editor/example/src/main/res/layout/activity_example.xml
@@ -0,0 +1,44 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.wordpress.android.editor.example.MainExampleActivity">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="New Editor - Post 1"
+ android:id="@+id/new_editor_post_1"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="New Editor - Empty Post"
+ android:id="@+id/new_editor_post_empty"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/new_editor_post_1"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Legacy Editor - Post 1 - Local Draft"
+ android:id="@+id/legacy_editor_post_1_local"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/new_editor_post_empty"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Legacy Editor - Post 1 - Remote"
+ android:id="@+id/legacy_editor_post_1_remote"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/legacy_editor_post_1_local"/>
+
+
+</RelativeLayout>
diff --git a/libs/editor/example/src/main/res/layout/activity_legacy_editor.xml b/libs/editor/example/src/main/res/layout/activity_legacy_editor.xml
new file mode 100644
index 000000000..24756188d
--- /dev/null
+++ b/libs/editor/example/src/main/res/layout/activity_legacy_editor.xml
@@ -0,0 +1,13 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditorActivity">
+
+ <fragment
+ android:id="@+id/postEditor"
+ android:name="org.wordpress.android.editor.LegacyEditorFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/libs/editor/example/src/main/res/layout/activity_new_editor.xml b/libs/editor/example/src/main/res/layout/activity_new_editor.xml
new file mode 100644
index 000000000..478e491e5
--- /dev/null
+++ b/libs/editor/example/src/main/res/layout/activity_new_editor.xml
@@ -0,0 +1,13 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditorActivity">
+
+ <fragment
+ android:id="@+id/postEditor"
+ android:name="org.wordpress.android.editor.EditorFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/values/colors.xml b/libs/editor/example/src/main/res/values/colors.xml
new file mode 100644
index 000000000..e8782a95d
--- /dev/null
+++ b/libs/editor/example/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="color_control_activated">@color/wp_blue_light</color>
+</resources> \ No newline at end of file
diff --git a/libs/editor/example/src/main/res/values/strings.xml b/libs/editor/example/src/main/res/values/strings.xml
new file mode 100644
index 000000000..c8ac8b7dd
--- /dev/null
+++ b/libs/editor/example/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+<resources>
+ <string name="app_name">Editor Example</string>
+ <string name="title_activity_example">Editor Example</string>
+ <string name="starting_legacy_editor">Starting legacy editor</string>
+ <string name="starting_new_editor">Starting new editor</string>
+ <string name="example_post_visual_title">I\'m editing a post!</string>
+ <string name="example_post_title_placeholder">Post title</string>
+ <string name="example_post_content_placeholder">Share your story here…</string>
+ <string name="example_post_1_title">Post 1</string>
+ <string name="example_post_1_content">Post 1 Content:\nBest post ever!</string>
+ <string name="example_post_2_title">Post 2</string>
+ <string name="example_post_2_content">
+ <![CDATA[<p>Post 2 Content</p><blockquote>Quoted text</blockquote><br/>]]>
+ </string>
+
+ <string name="select_image">Select an image</string>
+ <string name="select_image_fail">Select an image (failure demo)</string>
+ <string name="select_video">Select a video</string>
+ <string name="select_video_fail">Select a video (failure demo)</string>
+ <string name="select_image_slow_network">Select an image (slow network demo)</string>
+</resources>
diff --git a/libs/editor/example/src/main/res/values/styles.xml b/libs/editor/example/src/main/res/values/styles.xml
new file mode 100644
index 000000000..1ef1ba72e
--- /dev/null
+++ b/libs/editor/example/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorControlActivated">@color/color_control_activated</item>
+ </style>
+
+</resources>
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java
new file mode 100644
index 000000000..d2db16a8c
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.wordpress.android.editor;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java
new file mode 100644
index 000000000..d9e045b29
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java
@@ -0,0 +1,112 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.text.Spanned;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class EditorFragmentAbstractTest {
+ @Test
+ public void testActivityMustImplementEditorFragmentListener() {
+ // Host Activity must implement EditorFragmentListener, exception expected if not
+ boolean didPassTest = false;
+ Activity hostActivity = Robolectric.buildActivity(Activity.class).create().get();
+ EditorFragmentAbstract testFragment = new DefaultEditorFragment();
+
+ try {
+ testFragment.onAttach(hostActivity);
+ } catch (ClassCastException classCastException) {
+ didPassTest = true;
+ }
+
+ Assert.assertTrue(didPassTest);
+ }
+
+ @Test
+ public void testOnBackPressReturnsFalseByDefault() {
+ // The default behavior of onBackPressed should return false
+ Assert.assertFalse(new DefaultEditorFragment().onBackPressed());
+ }
+
+ /**
+ * Used to test default behavior of non-abstract methods.
+ */
+ public static class DefaultEditorFragment extends EditorFragmentAbstract {
+ @Override
+ public void setTitle(CharSequence text) {
+ }
+
+ @Override
+ public void setContent(CharSequence text) {
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getContent() {
+ return null;
+ }
+
+ @Override
+ public void appendMediaFile(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader) {
+ }
+
+ @Override
+ public void appendGallery(MediaGallery mediaGallery) {
+ }
+
+ @Override
+ public void setUrlForVideoPressId(String videoPressId, String url, String posterUrl) {
+
+ }
+
+ @Override
+ public boolean isUploadingMedia() {
+ return false;
+ }
+
+ @Override
+ public boolean isActionInProgress() {
+ return false;
+ }
+
+ @Override
+ public boolean hasFailedMediaUploads() {
+ return false;
+ }
+
+ @Override
+ public void removeAllFailedMediaUploads() {
+
+ }
+
+ @Override
+ public void setTitlePlaceholder(CharSequence text) {
+
+ }
+
+ @Override
+ public void setContentPlaceholder(CharSequence text) {
+
+ }
+
+ @Override
+ public Spanned getSpannedContent() {
+ return null;
+ }
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java
new file mode 100644
index 000000000..beaabe31c
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java
@@ -0,0 +1,496 @@
+package org.wordpress.android.editor;
+
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.junit.Assert.assertEquals;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class HtmlStyleTextWatcherTest {
+
+ private HtmlStyleTextWatcherForTests mWatcher;
+ private Editable mContent;
+ private boolean mUpdateSpansWasCalled;
+ private HtmlStyleTextWatcher.SpanRange mSpanRange;
+
+ @Before
+ public void setUp() {
+ mWatcher = new HtmlStyleTextWatcherForTests();
+ mUpdateSpansWasCalled = false;
+ }
+
+ @Test
+ public void testTypingNormalText() {
+ // -- Test typing in normal text (non-HTML) in an empty document
+ mContent = new SpannableStringBuilder("a");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 1); // Typed "a"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+
+ mContent = new SpannableStringBuilder("ab");
+
+ mWatcher.onTextChanged(mContent, 1, 0, 1); // Typed "b"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in normal text after exiting tags
+ mContent = new SpannableStringBuilder("text <b>bold</b> a");
+
+ mWatcher.onTextChanged(mContent, 17, 0, 1); // Typed "a"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in normal text before exiting tags
+ mContent = new SpannableStringBuilder("text a <b>bold</b>");
+
+ mWatcher.onTextChanged(mContent, 5, 0, 1); // Typed "a"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+ }
+
+ @Test
+ public void testTypingInOpeningTag() {
+ // Test with several different cases of pre-existing text
+ String[] previousTextCases = new String[]{"", "plain text", "<i>",
+ "<blockquote>some existing content</blockquote> "};
+ for (String initialText : previousTextCases) {
+ int offset = initialText.length();
+ mUpdateSpansWasCalled = false;
+
+ // -- Test typing in an opening tag symbol
+ mContent = new SpannableStringBuilder(initialText + "<");
+
+ mWatcher.onTextChanged(mContent, offset, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the tag name
+ mContent = new SpannableStringBuilder(initialText + "<b");
+
+ mWatcher.onTextChanged(mContent, offset + 1, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in a closing tag symbol
+ mContent = new SpannableStringBuilder(initialText + "<b>");
+
+ mWatcher.onTextChanged(mContent, offset + 2, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(offset, mSpanRange.getOpeningTagLoc());
+ assertEquals(offset + 3, mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testTypingInClosingTag() {
+ // Test with several different cases of pre-existing text
+ String[] previousTextCases = new String[]{"<b>stuff", "plain text <b>stuff", "<i><b>stuff",
+ "<blockquote>some existing content</blockquote> <b>stuff"};
+
+ for (String initialText : previousTextCases) {
+ int offset = initialText.length();
+ mUpdateSpansWasCalled = false;
+
+ // -- Test typing in an opening tag symbol
+ mContent = new SpannableStringBuilder(initialText + "<");
+
+ mWatcher.onTextChanged(mContent, offset, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the closing tag slash
+ mContent = new SpannableStringBuilder(initialText + "</");
+
+ mWatcher.onTextChanged(mContent, offset + 1, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+ // -- Test typing in the tag name
+ mContent = new SpannableStringBuilder(initialText + "</b");
+
+ mWatcher.onTextChanged(mContent, offset + 2, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in a closing tag symbol
+ mContent = new SpannableStringBuilder(initialText + "</b>");
+
+ mWatcher.onTextChanged(mContent, offset + 3, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(offset, mSpanRange.getOpeningTagLoc());
+ assertEquals(offset + 4, mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testTypingInTagWithSurroundingTags() {
+ // Spans in this case will be applied until the end of the next tag
+ // This fixes a pasting bug and might be refined later
+ // -- Test typing in the opening tag symbol
+ mContent = new SpannableStringBuilder("some <del>text</del> < <b>bold text</b>");
+
+ mWatcher.onTextChanged(mContent, 21, 0, 1); // Added lone "<"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(21, mSpanRange.getOpeningTagLoc());
+ assertEquals(26, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test typing in the tag name
+ mContent = new SpannableStringBuilder("some <del>text</del> <i <b>bold text</b>");
+
+ mWatcher.onTextChanged(mContent, 22, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(21, mSpanRange.getOpeningTagLoc());
+ assertEquals(27, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test typing in the closing tag symbol
+ mContent = new SpannableStringBuilder("some <del>text</del> <i> <b>bold text</b>");
+
+ mWatcher.onTextChanged(mContent, 23, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(21, mSpanRange.getOpeningTagLoc());
+ assertEquals(28, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testTypingInLoneClosingSymbol() {
+ // -- Test typing in an isolated closing tag symbol
+ mContent = new SpannableStringBuilder("some text >");
+
+ mWatcher.onTextChanged(mContent, 10, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in an isolated closing tag symbol with surrounding tags
+ mContent = new SpannableStringBuilder("some <b>tex>t</b>");
+
+ mWatcher.onTextChanged(mContent, 11, 0, 1); // Added lone ">"
+ mWatcher.afterTextChanged(mContent);
+
+ // The span in this case will be applied from the start of the previous tag to the end of the next tag
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(17, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testTypingInEntity() {
+ // Test with several different cases of pre-existing text
+ String[] previousTextCases = new String[]{"", "plain text", "&rho;",
+ "<blockquote>some existing content &dagger;</blockquote> "};
+ for (String initialText : previousTextCases) {
+ int offset = initialText.length();
+ mUpdateSpansWasCalled = false;
+
+ // -- Test typing in the entity's opening '&'
+ mContent = new SpannableStringBuilder(initialText + "&");
+
+ mWatcher.onTextChanged(mContent, offset, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the entity's main text
+ mContent = new SpannableStringBuilder(initialText + "&amp");
+
+ mWatcher.onTextChanged(mContent, offset + 3, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the entity's closing ';'
+ mContent = new SpannableStringBuilder(initialText + "&amp;");
+
+ mWatcher.onTextChanged(mContent, offset + 4, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(offset, mSpanRange.getOpeningTagLoc());
+ assertEquals(offset + 5, mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testAddingTagFromFormatBar() {
+ // -- Test adding a tag to an empty document
+ mContent = new SpannableStringBuilder("<b>");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(3, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the end of a document with text
+ mContent = new SpannableStringBuilder("stuff<b>");
+
+ mWatcher.onTextChanged(mContent, 5, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(8, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the end of a document containing other html
+ mContent = new SpannableStringBuilder("some text <i>italics</i> <b>");
+
+ mWatcher.onTextChanged(mContent, 25, 0, 3); // Added "<b>"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(25, mSpanRange.getOpeningTagLoc());
+ assertEquals(28, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the start of a document with text
+ mContent = new SpannableStringBuilder("<b>some text");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(3, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the start of a document containing other html
+ mContent = new SpannableStringBuilder("<b>some text <i>italics</i>");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(3, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag within another tag pair
+ mContent = new SpannableStringBuilder("<b>some <i>text</b>");
+
+ mWatcher.onTextChanged(mContent, 8, 0, 3); // Added <i>
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(8, mSpanRange.getOpeningTagLoc());
+ assertEquals(11, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a closing tag within another tag pair
+ mContent = new SpannableStringBuilder("<b>some <i>text</i></b>");
+
+ mWatcher.onTextChanged(mContent, 15, 0, 4); // Added "</i>"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(15, mSpanRange.getOpeningTagLoc());
+ assertEquals(19, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testAddingListTagsFromFormatBar() {
+ // -- Test adding a list tag to an empty document
+ mContent = new SpannableStringBuilder("<ul>\n\t<li>");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 10);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(10, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a closing list tag
+ mContent = new SpannableStringBuilder("<ul>\n" + //5
+ "\t<li>list item</li>\n" + //20
+ "\t<li>another list item</li>\n" + //22
+ "</ul>");
+
+ mWatcher.onTextChanged(mContent, 47, 0, 11); // Added "</li>\n</ul>"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(47, mSpanRange.getOpeningTagLoc());
+ assertEquals(58, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testDeletingPartsOfTag() {
+ // -- Test deleting different characters within a tag
+ mContent = new SpannableStringBuilder("<b>stuff</b>");
+
+ int deletedChar = 0;
+ mWatcher.beforeTextChanged(mContent, deletedChar, 1, 0);
+ // Deleted characters are removed from the string between beforeTextChanged() and onTextChanged()
+ mContent.delete(deletedChar, deletedChar + 1);
+ mWatcher.onTextChanged(mContent, deletedChar, 1, 0);
+ mWatcher.afterTextChanged(mContent);
+
+ // "b>" should be re-styled
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(2, mSpanRange.getClosingTagLoc());
+
+ for (int i = 8; i < 12; i++) {
+ mContent = new SpannableStringBuilder("<b>stuff</b>");
+
+ mWatcher.beforeTextChanged(mContent, i, 1, 0);
+ mContent.delete(i, i + 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // Style should be updated starting from the end of 'stuff'
+ assertEquals(8, mSpanRange.getOpeningTagLoc());
+ assertEquals(mContent.length(), mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testPasteTagPair() {
+ // -- Test pasting in a set of opening and closing tags at the end of the document
+ mContent = new SpannableStringBuilder("text <b></b>");
+
+ mWatcher.onTextChanged(mContent, 5, 0, 7);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(12, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testCutAndPasteTagPart() {
+ // -- Test cutting a tag and part of another tag from the document
+ mContent = new SpannableStringBuilder("test <b></b> <i>italics</i>");
+
+ mWatcher.beforeTextChanged(mContent, 5, 4, 0); // Deleted "<b><"
+ mContent.delete(5, 9);
+ mWatcher.onTextChanged(mContent, 5, 4, 0);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(8, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test pasting the cut text back in
+ mContent = new SpannableStringBuilder("test <b></b> <i>italics</i>");
+ mWatcher.onTextChanged(mContent, 5, 0, 4); // Pasted "<b><" back in
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(12, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testCutAndPasteTagPartReplacingText() {
+ // -- Test pasting cut text while text is selected
+ // Pasted "<b><", replacing "st " of "test "
+ mContent = new SpannableStringBuilder("test /b> <i>italics</i>");
+ mWatcher.beforeTextChanged(mContent, 2, 3, 4);
+ mContent = new SpannableStringBuilder("te<b></b> <i>italics</i>");
+ mWatcher.onTextChanged(mContent, 2, 3, 4);
+ mWatcher.afterTextChanged(mContent);
+
+ // Should re-style whole document
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(mContent.length(), mSpanRange.getClosingTagLoc());
+
+
+ // -- Test pasting cut text while text is selected, case 2
+ // Pasted "i>", replacing "test "
+ mContent = new SpannableStringBuilder("<test italics</i>");
+ mWatcher.beforeTextChanged(mContent, 1, 5, 2);
+ mContent = new SpannableStringBuilder("<i>italics</i>");
+ mWatcher.onTextChanged(mContent, 1, 5, 2);
+ mWatcher.afterTextChanged(mContent);
+
+ // Should re-style whole document
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(mContent.length(), mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testNoChange() {
+
+ mWatcher.beforeTextChanged("sample", 0, 0, 0);
+ mWatcher.onTextChanged("sample", 0, 0, 0);
+ mWatcher.afterTextChanged(null);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+ }
+
+ @Test
+ public void testUpdateSpans() {
+ // -- Test tag styling
+ HtmlStyleTextWatcher watcher = new HtmlStyleTextWatcher();
+ Spannable content = new SpannableStringBuilder("<b>stuff</b>");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 3));
+
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+ // -- Test entity styling
+ content = new SpannableStringBuilder("text &amp; more text");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(5, 10));
+
+ assertEquals(1, content.getSpans(5, 10, ForegroundColorSpan.class).length);
+ assertEquals(1, content.getSpans(5, 10, StyleSpan.class).length);
+ assertEquals(1, content.getSpans(5, 10, RelativeSizeSpan.class).length);
+
+ // -- Test comment styling
+ content = new SpannableStringBuilder("text <!--comment--> more text");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(5, 19));
+
+ assertEquals(1, content.getSpans(5, 19, ForegroundColorSpan.class).length);
+ assertEquals(1, content.getSpans(5, 19, StyleSpan.class).length);
+ assertEquals(1, content.getSpans(5, 19, RelativeSizeSpan.class).length);
+
+ content = new SpannableStringBuilder("<b>stuff</b>");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 3));
+
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 42));
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+ }
+
+ private class HtmlStyleTextWatcherForTests extends HtmlStyleTextWatcher {
+ @Override
+ protected void updateSpans(Spannable s, SpanRange spanRange) {
+ mSpanRange = spanRange;
+ mUpdateSpansWasCalled = true;
+ }
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java
new file mode 100644
index 000000000..12e7793b1
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java
@@ -0,0 +1,92 @@
+package org.wordpress.android.editor;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.junit.Assert.assertEquals;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class HtmlStyleUtilsTest {
+
+ @Test
+ public void testBulkStyling() {
+ // -- Test bulk styling
+ Spannable content = new SpannableStringBuilder("text <b>bold</b> &amp; <!--a comment--> <a href=\"website\">link</a>");
+ HtmlStyleUtils.styleHtmlForDisplay(content);
+
+ assertEquals(0, content.getSpans(0, 5, CharacterStyle.class).length); // 'text '
+
+ assertEquals(1, content.getSpans(5, 8, ForegroundColorSpan.class).length); // '<b>'
+
+ assertEquals(1, content.getSpans(12, 16, ForegroundColorSpan.class).length); // '</b>'
+
+ assertEquals(1, content.getSpans(17, 22, ForegroundColorSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(17, 22, StyleSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(17, 22, RelativeSizeSpan.class).length); // '&amp;'
+
+ assertEquals(1, content.getSpans(23, 39, ForegroundColorSpan.class).length); // '<!--a comment-->'
+ assertEquals(1, content.getSpans(23, 39, StyleSpan.class).length); // '<!--a comment-->'
+ assertEquals(1, content.getSpans(23, 39, RelativeSizeSpan.class).length); // '<!--a comment-->'
+
+ assertEquals(2, content.getSpans(40, 58, ForegroundColorSpan.class).length); // '<a href="website">'
+ assertEquals(1, content.getSpans(40, 48, ForegroundColorSpan.class).length); // '<a href='
+ // Attribute span is applied on top of tag span, so there should be 2 ForegroundColorSpans present
+ assertEquals(2, content.getSpans(48, 57, ForegroundColorSpan.class).length); // '"website"'
+ assertEquals(1, content.getSpans(57, 58, ForegroundColorSpan.class).length); // '>'
+
+ assertEquals(0, content.getSpans(58, 62, CharacterStyle.class).length); // 'link'
+
+ assertEquals(1, content.getSpans(62, 66, ForegroundColorSpan.class).length); // '</a>'
+ }
+
+ @Test
+ public void testClearSpans() {
+ Spannable content = new SpannableStringBuilder("<b>text &amp;");
+
+ HtmlStyleUtils.styleHtmlForDisplay(content);
+
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length); // '<b>'
+
+ assertEquals(1, content.getSpans(9, 14, ForegroundColorSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(9, 14, StyleSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(9, 14, RelativeSizeSpan.class).length); // '&amp;'
+
+ HtmlStyleUtils.clearSpans(content, 9, 14);
+
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+ assertEquals(0, content.getSpans(9, 14, ForegroundColorSpan.class).length);
+ assertEquals(0, content.getSpans(9, 14, StyleSpan.class).length);
+ assertEquals(0, content.getSpans(9, 14, RelativeSizeSpan.class).length);
+
+ HtmlStyleUtils.clearSpans(content, 0, 3);
+
+ assertEquals(0, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+
+ }
+
+ @Test
+ public void testClearSpansShouldIgnoreUnderline() {
+ // clearSpans() should ignore UnderlineSpan as it's used by the system for spelling suggestions
+ Spannable content = new SpannableStringBuilder("test");
+
+ content.setSpan(new UnderlineSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ HtmlStyleUtils.clearSpans(content, 0, 4);
+
+ assertEquals(1, content.getSpans(0, 4, UnderlineSpan.class).length);
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java
new file mode 100644
index 000000000..7584824f7
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java
@@ -0,0 +1,99 @@
+package org.wordpress.android.editor;
+
+import android.util.Log;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.wordpress.android.util.AppLog;
+
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.robolectric.shadows.ShadowLog.LogItem;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class JsCallbackReceiverTest {
+ private final static String EDITOR_LOG_TAG = "WordPress-" + AppLog.T.EDITOR.toString();
+
+ private JsCallbackReceiver mJsCallbackReceiver;
+
+ @Before
+ public void setUp() {
+ EditorFragment editorFragment = mock(EditorFragment.class);
+ mJsCallbackReceiver = new JsCallbackReceiver(editorFragment);
+ }
+
+ @Test
+ public void testCallbacksRecognized() {
+ mJsCallbackReceiver.executeCallback("callback-dom-loaded", "");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-new-field", "field-name");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-input", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-selection-changed", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-selection-style", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-focus-in", "");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-focus-out", "");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-image-replaced", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-image-tap", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-link-tap", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-log", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-response-string", "arguments");
+ assertNotLogged("Unhandled callback");
+ }
+
+ @Test
+ public void testUnknownCallbackShouldBeLogged() {
+ mJsCallbackReceiver.executeCallback("callback-does-not-exist", "content");
+ assertLogged(Log.DEBUG, EDITOR_LOG_TAG, "Unhandled callback: callback-does-not-exist:content", null);
+ }
+
+ @Test
+ public void testCallbackLog() {
+ mJsCallbackReceiver.executeCallback("callback-log", "msg=test-message");
+ assertLogged(Log.DEBUG, EDITOR_LOG_TAG, "callback-log: test-message", null);
+ }
+
+ private void assertLogged(int type, String tag, String msg, Throwable throwable) {
+ LogItem lastLog = ShadowLog.getLogs().get(0);
+ assertEquals(type, lastLog.type);
+ assertEquals(msg, lastLog.msg);
+ assertEquals(tag, lastLog.tag);
+ assertEquals(throwable, lastLog.throwable);
+ }
+
+ private void assertNotLogged(String msg) {
+ List<LogItem> logList = ShadowLog.getLogs();
+ if (!logList.isEmpty()) {
+ assertFalse(logList.get(0).msg.contains(msg));
+ ShadowLog.reset();
+ }
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java
new file mode 100644
index 000000000..b7bc70fe8
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java
@@ -0,0 +1,215 @@
+package org.wordpress.android.editor;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.wordpress.android.editor.Utils.buildMapFromKeyValuePairs;
+import static org.wordpress.android.editor.Utils.decodeHtml;
+import static org.wordpress.android.editor.Utils.escapeHtml;
+import static org.wordpress.android.editor.Utils.getChangeMapFromSets;
+import static org.wordpress.android.editor.Utils.splitDelimitedString;
+import static org.wordpress.android.editor.Utils.splitValuePairDelimitedString;
+import static org.wordpress.android.editor.Utils.getUrlFromClipboard;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class UtilsTest {
+
+ @Test
+ public void testEscapeHtml() {
+ // Test null
+ assertEquals(null, escapeHtml(null));
+ }
+
+ @Test
+ public void testDecodeHtml() {
+ // Test null
+ assertEquals(null, decodeHtml(null));
+
+ // Test normal usage
+ assertEquals("http://www.wordpress.com/", decodeHtml("http%3A%2F%2Fwww.wordpress.com%2F"));
+ }
+
+ @Test
+ public void testSplitDelimitedString() {
+ Set<String> splitString = new HashSet<>();
+
+ // Test normal usage
+ splitString.add("p");
+ splitString.add("bold");
+ splitString.add("justifyLeft");
+
+ assertEquals(splitString, splitDelimitedString("p~bold~justifyLeft", "~"));
+
+ // Test empty string
+ assertEquals(Collections.emptySet(), splitDelimitedString("", "~"));
+ }
+
+ @Test
+ public void testSplitValuePairDelimitedString() {
+ // Test usage with a URL containing the delimiter
+ Set<String> keyValueSet = new HashSet<>();
+ keyValueSet.add("url=http://www.wordpress.com/~user");
+ keyValueSet.add("title=I'm a link!");
+
+ List<String> identifiers = new ArrayList<>();
+ identifiers.add("url");
+ identifiers.add("title");
+
+ assertEquals(keyValueSet, splitValuePairDelimitedString(
+ "url=http://www.wordpress.com/~user~title=I'm a link!", "~", identifiers));
+
+ // Test usage with a matching identifier but no delimiters
+ keyValueSet.clear();
+ keyValueSet.add("url=http://www.wordpress.com/");
+
+ assertEquals(keyValueSet, splitValuePairDelimitedString("url=http://www.wordpress.com/", "~", identifiers));
+
+ // Test usage with no matching identifier and no delimiters
+ keyValueSet.clear();
+ keyValueSet.add("something=something else");
+
+ assertEquals(keyValueSet, splitValuePairDelimitedString("something=something else", "~", identifiers));
+ }
+
+ @Test
+ public void testBuildMapFromKeyValuePairs() {
+ Set<String> keyValueSet = new HashSet<>();
+ Map<String, String> expectedMap = new HashMap<>();
+
+ // Test normal usage
+ keyValueSet.add("id=test");
+ keyValueSet.add("name=example");
+
+ expectedMap.put("id", "test");
+ expectedMap.put("name", "example");
+
+ assertEquals(expectedMap, buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test mixed valid and invalid entries
+ keyValueSet.clear();
+ keyValueSet.add("test");
+ keyValueSet.add("name=example");
+
+ expectedMap.clear();
+ expectedMap.put("name", "example");
+
+ assertEquals(expectedMap, buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test multiple '=' (should split at the first `=` and treat the rest of them as part of the string)
+ keyValueSet.clear();
+ keyValueSet.add("id=test");
+ keyValueSet.add("contents=some text\n<a href=\"http://wordpress.com\">WordPress</a>");
+
+ expectedMap.clear();
+ expectedMap.put("id", "test");
+ expectedMap.put("contents", "some text\n<a href=\"http://wordpress.com\">WordPress</a>");
+
+ assertEquals(expectedMap, buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test invalid entry
+ keyValueSet.clear();
+ keyValueSet.add("test");
+
+ assertEquals(Collections.emptyMap(), buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test empty sets
+ assertEquals(Collections.emptyMap(), buildMapFromKeyValuePairs(Collections.<String>emptySet()));
+ }
+
+ @Test
+ public void testGetChangeMapFromSets() {
+ Set<String> oldSet = new HashSet<>();
+ Set<String> newSet = new HashSet<>();
+ Map<String, Boolean> expectedMap = new HashMap<>();
+
+ // Test normal usage
+ oldSet.add("p");
+ oldSet.add("bold");
+ oldSet.add("justifyLeft");
+
+ newSet.add("p");
+ newSet.add("justifyRight");
+
+ expectedMap.put("bold", false);
+ expectedMap.put("justifyLeft", false);
+ expectedMap.put("justifyRight", true);
+
+ assertEquals(expectedMap, getChangeMapFromSets(oldSet, newSet));
+
+ // Test no changes
+ oldSet.clear();
+ oldSet.add("p");
+ oldSet.add("bold");
+
+ newSet.clear();
+ newSet.add("p");
+ newSet.add("bold");
+
+ assertEquals(Collections.emptyMap(), getChangeMapFromSets(oldSet, newSet));
+
+ // Test empty sets
+ assertEquals(Collections.emptyMap(), getChangeMapFromSets(Collections.emptySet(), Collections.emptySet()));
+ }
+
+ @Test
+ public void testClipboardUrlWithNullContext() {
+ assertNull(getUrlFromClipboard(null));
+ }
+
+ @Test
+ public void testClipboardUrlWithNoClipData() {
+ assertNull(getClipboardUrlHelper(0, null));
+ }
+
+ @Test
+ public void testClipboardUrlWithNonUriData() {
+ assertNull(getClipboardUrlHelper(1, "not a URL"));
+ }
+
+ @Test
+ public void testClipboardUrlWithLocalUriData() {
+ assertNull(getClipboardUrlHelper(1, "file://test.png"));
+ }
+
+ @Test
+ public void testClipboardWithUrlData() {
+ String testUrl = "google.com";
+ assertEquals(testUrl, getClipboardUrlHelper(1, testUrl));
+ }
+
+ private String getClipboardUrlHelper(int itemCount, String clipText) {
+ ClipData.Item mockItem = mock(ClipData.Item.class);
+ when(mockItem.getText()).thenReturn(clipText);
+
+ ClipData mockPrimary = mock(ClipData.class);
+ when(mockPrimary.getItemCount()).thenReturn(itemCount);
+ when(mockPrimary.getItemAt(0)).thenReturn(mockItem);
+
+ ClipboardManager mockManager = mock(ClipboardManager.class);
+ when(mockManager.getPrimaryClip()).thenReturn(mockPrimary);
+
+ Context mockContext = mock(Context.class);
+ when(mockContext.getSystemService(Context.CLIPBOARD_SERVICE)).thenReturn(mockManager);
+
+ return getUrlFromClipboard(mockContext);
+ }
+}
diff --git a/libs/editor/example/src/test/js/test-formatter.js b/libs/editor/example/src/test/js/test-formatter.js
new file mode 100644
index 000000000..067d3c74e
--- /dev/null
+++ b/libs/editor/example/src/test/js/test-formatter.js
@@ -0,0 +1,135 @@
+var assert = require('chai').assert;
+var assetsDir = '../../../../WordPressEditor/src/main/assets';
+var underscore = require(assetsDir + '/libs/underscore-min.js');
+
+// Set up globals needed by shortcode, wpload, and wpsave
+global.window = {};
+global._ = underscore;
+global.wp = {};
+
+// wp-admin libraries
+var shortcode = require(assetsDir + '/libs/shortcode.js');
+var wpload = require(assetsDir + '/libs/wpload.js');
+var wpsave = require(assetsDir + '/libs/wpsave.js');
+
+var formatterlib = require(assetsDir + '/editor-utils-formatter.js');
+var formatter = formatterlib.Formatter;
+
+// Media strings
+
+// Image strings
+var imageSrc = 'content://com.android.providers.media.documents/document/image%3A12951';
+var plainImageHtml = '<img src="' + imageSrc + '" alt="" class="wp-image-123 size-full" width="172" height="244">';
+var imageWrappedInLinkHtml = '<a href="' + imageSrc + '">' + plainImageHtml + '</a>';
+
+// Captioned image strings
+var imageCaptionShortcode = '[caption width="600" align="alignnone"]' + imageSrc + 'Text[/caption]';
+var imageWithCaptionHtml = '<label class="wp-temp" data-wp-temp="caption" onclick="">' +
+ '<span class="wp-caption" style="width:600px; max-width:100% !important;" data-caption-width="600" ' +
+ 'data-caption-align="alignnone">' + imageSrc + 'Text</span></label>';
+var linkedImageCaptionShortcode = '[caption width="600" align="alignnone"]' + imageWrappedInLinkHtml + 'Text[/caption]';
+var linkedImageCaptionHtml = '<label class="wp-temp" data-wp-temp="caption" onclick="">' +
+ '<span class="wp-caption" style="width:600px; max-width:100% !important;" data-caption-width="600" ' +
+ 'data-caption-align="alignnone">' + imageWrappedInLinkHtml + 'Text</span></label>';
+
+// Video strings
+var videoSrc = 'content://com.android.providers.media.documents/document/video%3A12966';
+var videoShortcode = '[video src="' + videoSrc + '" poster=""][/video]';
+var videoHtml = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>' +
+ '<video webkit-playsinline src="' + videoSrc + '" poster="" preload="metadata" onclick="" controls="controls">' +
+ '</video></span>';
+
+// VideoPress video strings
+var vpVideoShortcode = '[wpvideo ABCD1234]';
+var vpVideoHtml = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>' +
+ '<video data-wpvideopress="ABCD1234" webkit-playsinline src="" preload="metadata" poster="svg/wpposter.svg" ' +
+ 'onclick="" onerror="ZSSEditor.sendVideoPressInfoRequest(\'ABCD1234\');"></video></span>';
+
+describe('HTML to Visual formatter should correctly convert', function () {
+ it('single-line HTML', function () {
+ assert.equal('<p>Some text</p>\n', formatter.htmlToVisual('Some text'));
+ });
+
+ it('multi-paragraph HTML', function () {
+ assert.equal('<p>Some text</p>\n<p>More text</p>\n', formatter.htmlToVisual('Some text\n\nMore text'));
+ });
+
+ testMediaParagraphWrapping('non-linked image', plainImageHtml, plainImageHtml);
+ testMediaParagraphWrapping('linked image', imageWrappedInLinkHtml, imageWrappedInLinkHtml);
+ testMediaParagraphWrapping('non-linked image, with caption', imageCaptionShortcode, imageWithCaptionHtml);
+ testMediaParagraphWrapping('linked image, with caption', linkedImageCaptionShortcode, linkedImageCaptionHtml);
+ testMediaParagraphWrapping('non-VideoPress video', videoShortcode, videoHtml);
+ testMediaParagraphWrapping('VideoPress video', vpVideoShortcode, vpVideoHtml);
+});
+
+function testMediaParagraphWrapping(mediaType, htmlModeMediaHtml, visualModeMediaHtml) {
+ describe(mediaType, function () {
+ it('alone in post', function () {
+ var visualFormattingApplied = formatter.htmlToVisual(htmlModeMediaHtml);
+ assert.equal('<p>' + visualModeMediaHtml + '</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>' + visualModeMediaHtml + '</div><div><br></div>', convertedToDivs);
+ });
+
+ it('with paragraphs above and below', function () {
+ var imageBetweenParagraphs = 'Line 1\n\n' + htmlModeMediaHtml + '\n\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageBetweenParagraphs);
+ assert.equal('<p>Line 1</p>\n<p>' + visualModeMediaHtml + '</p>\n<p>Line 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('with line breaks above and below', function () {
+ var imageBetweenLineBreaks = 'Line 1\n' + htmlModeMediaHtml + '\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageBetweenLineBreaks);
+ assert.equal('<p>Line 1<br />\n' + visualModeMediaHtml + '<br />\nLine 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('start of post, with paragraph underneath', function () {
+ var imageFollowedByParagraph = htmlModeMediaHtml + '\n\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageFollowedByParagraph);
+ assert.equal('<p>' + visualModeMediaHtml + '</p>\n<p>Line 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('start of post, with line break underneath', function () {
+ var imageFollowedByLineBreak = htmlModeMediaHtml + '\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageFollowedByLineBreak);
+ assert.equal('<p>' + visualModeMediaHtml + '<br \/>\nLine 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('end of post, with paragraph above', function () {
+ var imageUnderParagraph = 'Line 1\n\n' + htmlModeMediaHtml;
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageUnderParagraph);
+ assert.equal('<p>Line 1</p>\n<p>' + visualModeMediaHtml + '</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div><br></div>', convertedToDivs);
+ });
+
+ it('end of post, with line break above', function () {
+ var imageUnderLineBreak = 'Line 1\n' + htmlModeMediaHtml;
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageUnderLineBreak);
+ assert.equal('<p>Line 1<br \/>\n' + visualModeMediaHtml + '</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div><br></div>', convertedToDivs);
+ });
+ });
+}
diff --git a/libs/editor/gradle/wrapper/gradle-wrapper.jar b/libs/editor/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8c0fb64a8
--- /dev/null
+++ b/libs/editor/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/editor/gradle/wrapper/gradle-wrapper.properties b/libs/editor/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..fed7c8a3c
--- /dev/null
+++ b/libs/editor/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Aug 25 15:33:22 CEST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
diff --git a/libs/editor/gradlew b/libs/editor/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/editor/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/editor/gradlew.bat b/libs/editor/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/editor/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/editor/settings.gradle b/libs/editor/settings.gradle
new file mode 100644
index 000000000..af0d9296c
--- /dev/null
+++ b/libs/editor/settings.gradle
@@ -0,0 +1,2 @@
+include ':WordPressEditor'
+include ':example'
diff --git a/libs/networking/.gitignore b/libs/networking/.gitignore
new file mode 100644
index 000000000..df914c258
--- /dev/null
+++ b/libs/networking/.gitignore
@@ -0,0 +1,23 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
diff --git a/libs/networking/WordPressNetworking/build.gradle b/libs/networking/WordPressNetworking/build.gradle
new file mode 100644
index 000000000..c0e937773
--- /dev/null
+++ b/libs/networking/WordPressNetworking/build.gradle
@@ -0,0 +1,49 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+repositories {
+ jcenter()
+ maven { url 'http://wordpress-mobile.github.io/WordPress-Android' }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+
+android {
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 24
+ versionName "1.0.0"
+ }
+}
+
+dependencies {
+ compile 'org.wordpress:utils:1.11.0'
+ compile 'com.automattic:rest:1.0.7'
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ def repo_url = ""
+ if (project.hasProperty("repository")) {
+ repo_url = project.repository
+ }
+ repository(url: repo_url)
+ pom.version = android.defaultConfig.versionName
+ pom.groupId = "org.wordpress"
+ pom.artifactId = "wordpress-networking"
+ }
+ }
+}
diff --git a/libs/networking/WordPressNetworking/gradle.properties-example b/libs/networking/WordPressNetworking/gradle.properties-example
new file mode 100644
index 000000000..5a17295c3
--- /dev/null
+++ b/libs/networking/WordPressNetworking/gradle.properties-example
@@ -0,0 +1,2 @@
+wp.db_secret = wordpress
+repository = file:///Users/max/work/automattic/WordPress-Android-gh-pages/
diff --git a/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml b/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..f19a8bd89
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.networking">
+</manifest>
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java
new file mode 100644
index 000000000..cf7bd78bb
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java
@@ -0,0 +1,13 @@
+package org.wordpress.android.networking;
+
+/**
+ * Interface that provides a method that should perform the necessary task to make sure
+ * the provided AuthenticatorRequest will be authenticated.
+ *
+ * The Authenticator must call AuthenticatorRequest.send() when it has completed its operations. For
+ * convenience the AuthenticatorRequest class provides AuthenticatorRequest.setAccessToken so the Authenticator can
+ * easily update the access token.
+ */
+public interface Authenticator {
+ void authenticate(final AuthenticatorRequest authenticatorRequest);
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java
new file mode 100644
index 000000000..40d2af7f3
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java
@@ -0,0 +1,96 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.Oauth;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+import com.wordpress.rest.RestRequest.ErrorListener;
+
+/**
+ * Encapsulates the behaviour for asking the Authenticator for an access token. This
+ * allows the request maker to disregard the authentication state when making requests.
+ */
+public class AuthenticatorRequest {
+ private RestRequest mRequest;
+ private RestRequest.ErrorListener mListener;
+ private RestClient mRestClient;
+ private Authenticator mAuthenticator;
+
+ protected AuthenticatorRequest(RestRequest request, ErrorListener listener, RestClient restClient,
+ Authenticator authenticator) {
+ mRequest = request;
+ mListener = listener;
+ mRestClient = restClient;
+ mAuthenticator = authenticator;
+ }
+
+ public String getSiteId() {
+ return extractSiteIdFromUrl(mRestClient.getEndpointURL(), mRequest.getUrl());
+ }
+
+ /**
+ * Parse out the site ID from an URL.
+ * Note: For batch REST API calls, only the first siteID is returned
+ *
+ * @return The site ID
+ */
+ public static String extractSiteIdFromUrl(String restEndpointUrl, String url) {
+ if (url == null) {
+ return null;
+ }
+
+ final String sitePrefix = restEndpointUrl.endsWith("/") ? restEndpointUrl + "sites/" : restEndpointUrl + "/sites/";
+ final String batchCallPrefix = restEndpointUrl.endsWith("/") ? restEndpointUrl + "batch/?urls%5B%5D=%2Fsites%2F"
+ : restEndpointUrl + "/batch/?urls%5B%5D=%2Fsites%2F";
+
+ if (url.startsWith(sitePrefix) && !sitePrefix.equals(url)) {
+ int marker = sitePrefix.length();
+ if (url.indexOf("/", marker) < marker) {
+ return null;
+ }
+ return url.substring(marker, url.indexOf("/", marker));
+ } else if (url.startsWith(batchCallPrefix) && !batchCallPrefix.equals(url)) {
+ int marker = batchCallPrefix.length();
+ if (url.indexOf("%2F", marker) < marker) {
+ return null;
+ }
+ return url.substring(marker, url.indexOf("%2F", marker));
+ }
+
+ // not a sites/$siteId request or a batch request
+ return null;
+ }
+
+ /**
+ * Attempt to send the request, checks to see if we have an access token and if not
+ * asks the Authenticator to authenticate the request.
+ *
+ * If no Authenticator is provided the request is always sent.
+ */
+ protected void send(){
+ if (mAuthenticator == null) {
+ mRestClient.send(mRequest);
+ } else {
+ mAuthenticator.authenticate(this);
+ }
+ }
+
+ public void sendWithAccessToken(String token){
+ mRequest.setAccessToken(token);
+ mRestClient.send(mRequest);
+ }
+
+ public void sendWithAccessToken(Oauth.Token token){
+ sendWithAccessToken(token.toString());
+ }
+
+ /**
+ * If an access token cannot be obtained the request can be aborted and the
+ * handler's onFailure method is called
+ */
+ public void abort(VolleyError error){
+ if (mListener != null) {
+ mListener.onErrorResponse(error);
+ }
+ }
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java
new file mode 100644
index 000000000..492b2b99f
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java
@@ -0,0 +1,19 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public class RestClientFactory {
+ private static RestClientFactoryAbstract sFactory;
+
+ public static RestClient instantiate(RequestQueue queue) {
+ return instantiate(queue, RestClient.REST_CLIENT_VERSIONS.V1);
+ }
+
+ public static RestClient instantiate(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) {
+ if (sFactory == null) {
+ sFactory = new RestClientFactoryDefault();
+ }
+ return sFactory.make(queue, version);
+ }
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java
new file mode 100644
index 000000000..799a5a8e8
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public interface RestClientFactoryAbstract {
+ public RestClient make(RequestQueue queue);
+ public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version);
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java
new file mode 100644
index 000000000..b87d4b40f
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public class RestClientFactoryDefault implements RestClientFactoryAbstract {
+ public RestClient make(RequestQueue queue) {
+ return new RestClient(queue);
+ }
+
+ public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) {
+ return new RestClient(queue, version);
+ }
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java
new file mode 100644
index 000000000..6e06cfeee
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java
@@ -0,0 +1,475 @@
+/**
+ * Interface to the WordPress.com REST API.
+ */
+package org.wordpress.android.networking;
+
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.volley.DefaultRetryPolicy;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+import com.android.volley.RequestQueue;
+import com.android.volley.RetryPolicy;
+import com.android.volley.toolbox.RequestFuture;
+import com.wordpress.rest.JsonRestRequest;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+import com.wordpress.rest.RestRequest.ErrorListener;
+import com.wordpress.rest.RestRequest.Listener;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.LanguageUtils;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+public class RestClientUtils {
+ private static final String NOTIFICATION_FIELDS = "id,type,unread,body,subject,timestamp,meta";
+ private static final String COMMENT_REPLY_CONTENT_FIELD = "content";
+ private static String sUserAgent = "WordPress Networking Android";
+
+ private RestClient mRestClient;
+ private Authenticator mAuthenticator;
+ private Context mContext;
+
+ /**
+ * Socket timeout in milliseconds for rest requests
+ */
+ public static final int REST_TIMEOUT_MS = 30000;
+
+ /**
+ * Default number of retries for POST rest requests
+ */
+ public static final int REST_MAX_RETRIES_POST = 0;
+
+ /**
+ * Default number of retries for GET rest requests
+ */
+ public static final int REST_MAX_RETRIES_GET = 3;
+
+ /**
+ * Default backoff multiplier for rest requests
+ */
+ public static final float REST_BACKOFF_MULT = 2f;
+
+ public static void setUserAgent(String userAgent) {
+ sUserAgent = userAgent;
+ }
+
+ public RestClientUtils(Context context, RequestQueue queue, Authenticator authenticator, RestRequest.OnAuthFailedListener onAuthFailedListener) {
+ this(context, queue, authenticator, onAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1);
+ }
+
+ public RestClientUtils(Context context, RequestQueue queue, Authenticator authenticator, RestRequest.OnAuthFailedListener onAuthFailedListener, RestClient.REST_CLIENT_VERSIONS version) {
+ // load an existing access token from prefs if we have one
+ mContext = context;
+ mAuthenticator = authenticator;
+ mRestClient = RestClientFactory.instantiate(queue, version);
+ if (onAuthFailedListener != null) {
+ mRestClient.setOnAuthFailedListener(onAuthFailedListener);
+ }
+ mRestClient.setUserAgent(sUserAgent);
+ }
+
+ public Authenticator getAuthenticator() {
+ return mAuthenticator;
+ }
+
+ public RestClient getRestClient() {
+ return mRestClient;
+ }
+
+ public void getCategories(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/categories", siteId);
+ get(path, null, null, listener, errorListener);
+ }
+
+ /**
+ * get a list of recent comments
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/comments/
+ */
+ public void getComments(String siteId, Map<String, String> params, final Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/comments", siteId);
+ get(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Reply to a comment
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/
+ */
+ public void replyToComment(String reply, String path, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(COMMENT_REPLY_CONTENT_FIELD, reply);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Reply to a comment.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/
+ */
+ public void replyToComment(long siteId, long commentId, String content, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(COMMENT_REPLY_CONTENT_FIELD, content);
+ String path = String.format("sites/%d/comments/%d/replies/new", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Follow a site given an ID or domain
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/new/
+ */
+ public void followSite(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/follows/new", siteId);
+ post(path, listener, errorListener);
+ }
+
+ /**
+ * Unfollow a site given an ID or domain
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/mine/delete/
+ */
+ public void unfollowSite(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/follows/mine/delete", siteId);
+ post(path, listener, errorListener);
+ }
+
+ /**
+ * Get notifications with the provided params.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/get/notifications/
+ */
+ public void getNotifications(Map<String, String> params, Listener listener, ErrorListener errorListener) {
+ params.put("number", "40");
+ params.put("num_note_items", "20");
+ params.put("fields", NOTIFICATION_FIELDS);
+ get("notifications", params, null, listener, errorListener);
+ }
+
+ /**
+ * Get notifications with default params.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/get/notifications/
+ */
+ public void getNotifications(Listener listener, ErrorListener errorListener) {
+ getNotifications(new HashMap<String, String>(), listener, errorListener);
+ }
+
+ /**
+ * Update the seen timestamp.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/notifications/seen
+ */
+ public void markNotificationsSeen(String timestamp, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("time", timestamp);
+ post("notifications/seen", params, null, listener, errorListener);
+ }
+
+ /**
+ * Moderate a comment.
+ * <p/>
+ * http://developer.wordpress.com/docs/api/1/sites/%24site/comments/%24comment_ID/
+ */
+ public void moderateComment(String siteId, String commentId, String status, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("status", status);
+ String path = String.format("sites/%s/comments/%s/", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Edit the content of a comment
+ */
+ public void editCommentContent(long siteId, long commentId, String content, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("content", content);
+ String path = String.format("sites/%d/comments/%d/", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Like or unlike a comment.
+ */
+ public void likeComment(String siteId, String commentId, boolean isLiked, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ String path = String.format("sites/%s/comments/%s/likes/", siteId, commentId);
+
+ if (!isLiked) {
+ path += "mine/delete";
+ } else {
+ path += "new";
+ }
+
+ post(path, params, null, listener, errorListener);
+ }
+
+ public void getFreeSearchThemes(String siteId, int limit, int offset, String searchTerm, Listener listener, ErrorListener errorListener) {
+ getSearchThemes("free", siteId, limit, offset, searchTerm, listener, errorListener);
+ }
+
+ public void getSearchThemes(String tier, String siteId, int limit, int offset, String searchTerm, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes?tier=" + tier + "&number=%d&offset=%d&search=%s", siteId, limit, offset, searchTerm);
+ get(path, listener, errorListener);
+ }
+
+ public void getFreeThemes(String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) {
+ getThemes("free", siteId, limit, offset, listener, errorListener);
+ }
+
+ public void getPurchasedThemes(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/purchased", siteId);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get all a site's themes
+ */
+ public void getThemes(String tier, String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/?tier=" + tier + "&number=%d&offset=%d", siteId, limit, offset);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Set a site's theme
+ */
+ public void setTheme(String siteId, String themeId, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<>();
+ params.put("theme", themeId);
+ String path = String.format("sites/%s/themes/mine", siteId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Get a site's current theme
+ */
+ public void getCurrentTheme(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/mine", siteId);
+ get(path, listener, errorListener);
+ }
+
+ public void getGeneralSettings(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/settings", siteId);
+ Map<String, String> params = new HashMap<String, String>();
+ get(path, params, null, listener, errorListener);
+ }
+
+ public void setGeneralSiteSettings(String siteId, Listener listener, ErrorListener errorListener,
+ Map<String, String> params) {
+ String path = String.format("sites/%s/settings", siteId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Delete a site
+ */
+ public void deleteSite(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/delete", siteId);
+ post(path, listener, errorListener);
+ }
+
+ public void getSitePurchases(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/purchases", siteId);
+ get(path, listener, errorListener);
+ }
+
+ public void exportContentAll(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/exports/start", siteId);
+ post(path, listener, errorListener);
+ }
+
+ public void sendLoginEmail(Map<String, String> params, Listener listener, ErrorListener errorListener) {
+ post("auth/send-login-email", params, null, listener, errorListener);
+ }
+
+ public void isAvailable(String email, Listener listener, ErrorListener errorListener) {
+ String path = String.format("is-available/email?q=%s", email);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Make GET request
+ */
+ public Request<JSONObject> get(String path, Listener listener, ErrorListener errorListener) {
+ return get(path, null, null, listener, errorListener);
+ }
+
+ /**
+ * Make GET request with params
+ */
+ public Request<JSONObject> get(String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener,
+ ErrorListener errorListener) {
+ // turn params into querystring
+ HashMap<String, String> paramsWithLocale = getRestLocaleParams(mContext);
+ if (params != null) {
+ paramsWithLocale.putAll(params);
+ }
+
+ String realPath = getSanitizedPath(path);
+ if (TextUtils.isEmpty(realPath)) {
+ realPath = path;
+ }
+ paramsWithLocale.putAll(getSanitizedParameters(path));
+
+ RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(realPath, paramsWithLocale), null,
+ listener, errorListener);
+
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT);
+ }
+ request.setRetryPolicy(retryPolicy);
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator);
+ authCheck.send();
+ return request;
+ }
+
+ /**
+ * Make Synchronous GET request
+ *
+ * @throws TimeoutException
+ * @throws ExecutionException
+ * @throws InterruptedException
+ */
+ public JSONObject getSynchronous(String path) throws InterruptedException, ExecutionException, TimeoutException {
+ return getSynchronous(path, null, null);
+ }
+
+ /**
+ * Make Synchronous GET request with params
+ *
+ * @throws TimeoutException
+ * @throws ExecutionException
+ * @throws InterruptedException
+ */
+ public JSONObject getSynchronous(String path, Map<String, String> params, RetryPolicy retryPolicy)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ RequestFuture<JSONObject> future = RequestFuture.newFuture();
+
+ HashMap<String, String> paramsWithLocale = getRestLocaleParams(mContext);
+ if (params != null) {
+ paramsWithLocale.putAll(params);
+ }
+
+ String realPath = getSanitizedPath(path);
+ if (TextUtils.isEmpty(realPath)) {
+ realPath = path;
+ }
+ paramsWithLocale.putAll(getSanitizedParameters(path));
+
+ RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(realPath, paramsWithLocale), null, future, future);
+
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT);
+ }
+ request.setRetryPolicy(retryPolicy);
+
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, null, mRestClient, mAuthenticator);
+ authCheck.send(); //this insert the request into the queue. //TODO: Verify that everything is OK on REST calls without a valid token
+ JSONObject response = future.get();
+ return response;
+ }
+
+ /**
+ * Make POST request
+ */
+ public void post(String path, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = null;
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Make POST request with params
+ */
+ public void post(final String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener,
+ ErrorListener errorListener) {
+ final RestRequest request = mRestClient.makeRequest(Method.POST, mRestClient.getAbsoluteURL(path, getRestLocaleParams(mContext)), params,
+ listener, errorListener);
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST,
+ REST_BACKOFF_MULT); //Do not retry on failure
+ }
+ request.setRetryPolicy(retryPolicy);
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator);
+ authCheck.send();
+ }
+
+
+ /**
+ * Make a JSON POST request
+ */
+ public void post(final String path, JSONObject params, RetryPolicy retryPolicy, Listener listener,
+ ErrorListener errorListener) {
+ final JsonRestRequest request = mRestClient.makeRequest(mRestClient.getAbsoluteURL(path, getRestLocaleParams(mContext)), params,
+ listener, errorListener);
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST,
+ REST_BACKOFF_MULT); //Do not retry on failure
+ }
+ request.setRetryPolicy(retryPolicy);
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator);
+ authCheck.send();
+ }
+
+ /**
+ * Takes a URL and returns the path within, or an empty string (not null)
+ */
+ public static String getSanitizedPath(String unsanitizedPath){
+ if (unsanitizedPath != null) {
+ int qmarkPos = unsanitizedPath.indexOf('?');
+ if (qmarkPos > -1) { //strip any querystring params off this to obtain the path
+ return unsanitizedPath.substring(0, qmarkPos+1);
+ } else {
+ // return the string as is, consider the whole string as the path
+ return unsanitizedPath;
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Takes a URL with query strings and returns a Map of query string values.
+ */
+ public static HashMap<String, String> getSanitizedParameters(String unsanitizedPath){
+ HashMap<String, String> queryParams = new HashMap<>();
+
+ Uri uri = Uri.parse(unsanitizedPath);
+
+ if (uri.getQueryParameterNames() != null ) {
+ Iterator iter = uri.getQueryParameterNames().iterator();
+ while (iter.hasNext()) {
+ String name = (String)iter.next();
+ String value = uri.getQueryParameter(name);
+ queryParams.put(name, value);
+ }
+ }
+
+ return queryParams;
+ }
+
+ /**
+ * Returns locale parameter used in REST calls which require the response to be localized
+ */
+ public static HashMap<String, String> getRestLocaleParams(Context context) {
+ HashMap<String, String> params = new HashMap<>();
+ String deviceLanguageCode = LanguageUtils.getCurrentDeviceLanguageCode(context);
+ if (!TextUtils.isEmpty(deviceLanguageCode)) {
+ //patch locale if it's any of the deprecated codes as can be read in Locale.java source code:
+ deviceLanguageCode = LanguageUtils.patchDeviceLanguageCode(deviceLanguageCode);
+ params.put("locale", deviceLanguageCode);
+ }
+ return params;
+ }
+
+}
diff --git a/libs/networking/build.gradle b/libs/networking/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/networking/build.gradle
diff --git a/libs/networking/gradle/wrapper/gradle-wrapper.jar b/libs/networking/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8c0fb64a8
--- /dev/null
+++ b/libs/networking/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/networking/gradle/wrapper/gradle-wrapper.properties b/libs/networking/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..4c74aab81
--- /dev/null
+++ b/libs/networking/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip
diff --git a/libs/networking/gradlew b/libs/networking/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/networking/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/networking/gradlew.bat b/libs/networking/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/networking/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/networking/settings.gradle b/libs/networking/settings.gradle
new file mode 100644
index 000000000..62b0b6897
--- /dev/null
+++ b/libs/networking/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressNetworking'
diff --git a/libs/utils/.gitignore b/libs/utils/.gitignore
new file mode 100644
index 000000000..8babf679a
--- /dev/null
+++ b/libs/utils/.gitignore
@@ -0,0 +1,25 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
diff --git a/libs/utils/README.md b/libs/utils/README.md
new file mode 100644
index 000000000..e18da3d92
--- /dev/null
+++ b/libs/utils/README.md
@@ -0,0 +1,27 @@
+# WordPress-Utils-Android
+
+Collection of utility methods for Android and WordPress.
+
+## Use the library
+
+* In your build.gradle:
+```groovy
+dependencies {
+ // use the latest 1.x version
+ compile 'org.wordpress:utils:1.+'
+}
+```
+
+## Publish it to bintray
+
+```shell
+$ ./gradlew assemble publishToMavenLocal bintrayUpload -PbintrayUser=FIXME -PbintrayKey=FIXME -PdryRun=false
+```
+
+## Apps that use this library
+- [WordPress for Android][1]
+
+## License
+Dual licensed under MIT, and GPL.
+
+[1]: https://github.com/wordpress-mobile/WordPress-Android
diff --git a/libs/utils/WordPressUtils/README.md b/libs/utils/WordPressUtils/README.md
new file mode 100644
index 000000000..62a759585
--- /dev/null
+++ b/libs/utils/WordPressUtils/README.md
@@ -0,0 +1 @@
+# org.wordpress.android.util \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/build.gradle b/libs/utils/WordPressUtils/build.gradle
new file mode 100644
index 000000000..96191a5d0
--- /dev/null
+++ b/libs/utils/WordPressUtils/build.gradle
@@ -0,0 +1,65 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ classpath 'com.novoda:bintray-release:0.3.4'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.novoda.bintray-release'
+
+repositories {
+ jcenter()
+}
+
+dependencies {
+ compile('commons-lang:commons-lang:2.6') {
+ exclude group: 'commons-logging'
+ }
+ compile 'com.mcxiaoke.volley:library:1.0.18'
+ compile 'com.android.support:support-v13:24.2.1'
+}
+
+android {
+ useLibrary 'org.apache.http.legacy'
+
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionName "1.14.0"
+ minSdkVersion 14
+ targetSdkVersion 24
+ }
+}
+
+android.libraryVariants.all { variant ->
+ task("generate${variant.name}Javadoc", type: Javadoc) {
+ description "Generates Javadoc for $variant.name."
+ source = variant.javaCompile.source
+ classpath = files(variant.javaCompile.classpath.files, android.getBootClasspath())
+
+ options {
+ links "http://docs.oracle.com/javase/7/docs/api/"
+ }
+ exclude '**/R.java'
+ }
+}
+
+publish {
+ artifactId = 'utils'
+ userOrg = 'wordpress-mobile'
+ groupId = 'org.wordpress'
+ uploadName = 'utils'
+ description = 'Utils library for Android'
+ publishVersion = android.defaultConfig.versionName
+ licences = ['MIT', 'GPL']
+ website = 'https://github.com/wordpress-mobile/WordPress-Utils-Android/'
+ dryRun = 'false'
+ autoPublish = 'true'
+}
diff --git a/libs/utils/WordPressUtils/gradle.properties-example b/libs/utils/WordPressUtils/gradle.properties-example
new file mode 100644
index 000000000..5281d935c
--- /dev/null
+++ b/libs/utils/WordPressUtils/gradle.properties-example
@@ -0,0 +1,6 @@
+ossrhUsername=hello
+ossrhPassword=world
+
+signing.keyId=byebye
+signing.password=secret
+signing.secretKeyRingFile=/home/user/secret.gpg
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java
new file mode 100644
index 000000000..f7c747ff7
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java
@@ -0,0 +1,32 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class JSONUtilsTest extends InstrumentationTestCase {
+ public void testQueryJSONNullSource1() {
+ JSONUtils.queryJSON((JSONObject) null, "", "");
+ }
+
+ public void testQueryJSONNullSource2() {
+ JSONUtils.queryJSON((JSONArray) null, "", "");
+ }
+
+ public void testQueryJSONNullQuery1() {
+ JSONUtils.queryJSON(new JSONObject(), null, "");
+ }
+
+ public void testQueryJSONNullQuery2() {
+ JSONUtils.queryJSON(new JSONArray(), null, "");
+ }
+
+ public void testQueryJSONNullReturnValue1() {
+ JSONUtils.queryJSON(new JSONObject(), "", null);
+ }
+
+ public void testQueryJSONNullReturnValue2() {
+ JSONUtils.queryJSON(new JSONArray(), "", null);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java
new file mode 100644
index 000000000..c506a452e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+public class ShortcodeUtilsTest extends InstrumentationTestCase {
+ public void testGetVideoPressShortcodeFromId() {
+ assertEquals("[wpvideo abcd1234]", ShortcodeUtils.getVideoPressShortcodeFromId("abcd1234"));
+ }
+
+ public void testGetVideoPressShortcodeFromNullId() {
+ assertEquals("", ShortcodeUtils.getVideoPressShortcodeFromId(null));
+ }
+
+ public void testGetVideoPressIdFromCorrectShortcode() {
+ assertEquals("abcd1234", ShortcodeUtils.getVideoPressIdFromShortCode("[wpvideo abcd1234]"));
+ }
+
+ public void testGetVideoPressIdFromInvalidShortcode() {
+ assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode("[other abcd1234]"));
+ }
+
+ public void testGetVideoPressIdFromNullShortcode() {
+ assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode(null));
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
new file mode 100644
index 000000000..abf7a8fae
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
@@ -0,0 +1,108 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UrlUtilsTest extends InstrumentationTestCase {
+ public void testGetDomainFromUrlWithEmptyStringDoesNotReturnNull() {
+ assertNotNull(UrlUtils.getHost(""));
+ }
+
+ public void testGetDomainFromUrlWithNoHostDoesNotReturnNull() {
+ assertNotNull(UrlUtils.getHost("wordpress"));
+ }
+
+ public void testGetDomainFromUrlWithHostReturnsHost() {
+ String url = "http://www.wordpress.com";
+ String host = UrlUtils.getHost(url);
+
+ assertTrue(host.equals("www.wordpress.com"));
+ }
+
+ public void testAppendUrlParameter1() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test", "preview", "true");
+ assertEquals("http://wp.com/test?preview=true", url);
+ }
+
+ public void testAppendUrlParameter2() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony", "preview", "true");
+ assertEquals("http://wp.com/test?q=pony&preview=true", url);
+ }
+
+ public void testAppendUrlParameter3() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony#unicorn", "preview", "true");
+ assertEquals("http://wp.com/test?q=pony&preview=true#unicorn", url);
+ }
+
+ public void testAppendUrlParameter4() {
+ String url = UrlUtils.appendUrlParameter("/relative/test", "preview", "true");
+ assertEquals("/relative/test?preview=true", url);
+ }
+
+ public void testAppendUrlParameter5() {
+ String url = UrlUtils.appendUrlParameter("/relative/", "preview", "true");
+ assertEquals("/relative/?preview=true", url);
+ }
+
+ public void testAppendUrlParameter6() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test/", "preview", "true");
+ assertEquals("http://wp.com/test/?preview=true", url);
+ }
+
+ public void testAppendUrlParameter7() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test/?q=pony", "preview", "true");
+ assertEquals("http://wp.com/test/?q=pony&preview=true", url);
+ }
+
+ public void testAppendUrlParameters1() {
+ Map<String, String> params = new HashMap<>();
+ params.put("w", "200");
+ params.put("h", "300");
+ String url = UrlUtils.appendUrlParameters("http://wp.com/test", params);
+ if (!url.equals("http://wp.com/test?h=300&w=200") && !url.equals("http://wp.com/test?w=200&h=300")) {
+ assertTrue("failed test on url: " + url, false);
+ }
+ }
+
+ public void testAppendUrlParameters2() {
+ Map<String, String> params = new HashMap<>();
+ params.put("h", "300");
+ params.put("w", "200");
+ String url = UrlUtils.appendUrlParameters("/relative/test", params);
+ if (!url.equals("/relative/test?h=300&w=200") && !url.equals("/relative/test?w=200&h=300")) {
+ assertTrue("failed test on url: " + url, false);
+ }
+ }
+
+ public void testHttps1() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php")));
+ }
+
+ public void testHttps2() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com#.b.com/test")));
+ }
+
+ public void testHttps3() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php")));
+ }
+
+ public void testHttps4() {
+ assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com")));
+ }
+
+ public void testHttps5() {
+ assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com/test#test")));
+ }
+
+ private URL buildURL(String address) {
+ URL url = null;
+ try {
+ url = new URL(address);
+ } catch (MalformedURLException e) {}
+ return url;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/AndroidManifest.xml b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44b1dcddc
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.util">
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+</manifest>
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java
new file mode 100644
index 000000000..396e06c37
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java
@@ -0,0 +1,16 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.inputmethod.InputMethodManager;
+
+public class ActivityUtils {
+ public static void hideKeyboard(Activity activity) {
+ if (activity != null && activity.getCurrentFocus() != null) {
+ InputMethodManager inputManager = (InputMethodManager) activity.getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ inputManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java
new file mode 100644
index 000000000..79b2dbced
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 wordpress.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wordpress.android.util;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+public class AlertUtils {
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ */
+ public static void showAlert(Context context, int titleId, int messageId) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(android.R.string.ok, null)
+ .setMessage(messageId)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param message
+ */
+ public static void showAlert(Context context, int titleId, String message) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(android.R.string.ok, null)
+ .setMessage(message)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ * @param positiveButtontxt
+ * @param positiveListener
+ * @param negativeButtontxt
+ * @param negativeListener
+ */
+ public static void showAlert(Context context, int titleId, int messageId,
+ CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener,
+ CharSequence negativeButtontxt, DialogInterface.OnClickListener negativeListener) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(positiveButtontxt, positiveListener)
+ .setNegativeButton(negativeButtontxt, negativeListener)
+ .setMessage(messageId)
+ .setCancelable(false)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param message
+ * @param positiveButtontxt
+ * @param positiveListener
+ */
+ public static void showAlert(Context context, int titleId, String message,
+ CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(positiveButtontxt, positiveListener)
+ .setMessage(message)
+ .setCancelable(false)
+ .create();
+
+ dlg.show();
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
new file mode 100644
index 000000000..be60e748e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
@@ -0,0 +1,272 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * simple wrapper for Android log calls, enables recording and displaying log
+ */
+public class AppLog {
+ // T for Tag
+ public enum T {READER, EDITOR, MEDIA, NUX, API, STATS, UTILS, NOTIFS, DB, POSTS, COMMENTS, THEMES, TESTS, PROFILING,
+ SIMPERIUM, SUGGESTION, MAIN, SETTINGS, PLANS, PEOPLE}
+
+ public static final String TAG = "WordPress";
+ public static final int HEADER_LINE_COUNT = 2;
+
+ private static boolean mEnableRecording = false;
+
+ private AppLog() {
+ throw new AssertionError();
+ }
+
+ /**
+ * Capture log so it can be displayed by AppLogViewerActivity
+ * @param enable A boolean flag to capture log. Default is false, pass true to enable recording
+ */
+ public static void enableRecording(boolean enable) {
+ mEnableRecording = enable;
+ }
+
+ /**
+ * Sends a VERBOSE log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void v(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.v(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.v, message);
+ }
+
+ /**
+ * Sends a DEBUG log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void d(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.d(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.d, message);
+ }
+
+ /**
+ * Sends a INFO log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void i(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.i(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.i, message);
+ }
+
+ /**
+ * Sends a WARN log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void w(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.w(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.w, message);
+ }
+
+ /**
+ * Sends a ERROR log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void e(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.e(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.e, message);
+ }
+
+ /**
+ * Send a ERROR log message and log the exception.
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static void e(T tag, String message, Throwable tr) {
+ message = StringUtils.notNullStr(message);
+ Log.e(TAG + "-" + tag.toString(), message, tr);
+ addEntry(tag, LogLevel.e, message + " - exception: " + tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getStringStackTrace(tr));
+ }
+
+ /**
+ * Sends a ERROR log message and the exception with StackTrace
+ * @param tag Used to identify the source of a log message. It usually identifies the class or activity where the log call occurs.
+ * @param tr An exception to log to get StackTrace
+ */
+ public static void e(T tag, Throwable tr) {
+ Log.e(TAG + "-" + tag.toString(), tr.getMessage(), tr);
+ addEntry(tag, LogLevel.e, tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getStringStackTrace(tr));
+ }
+
+ /**
+ * Sends a ERROR log message
+ * @param tag Used to identify the source of a log message. It usually identifies the class or activity where the log call occurs.
+ * @param volleyErrorMsg
+ * @param statusCode
+ */
+ public static void e(T tag, String volleyErrorMsg, int statusCode) {
+ if (TextUtils.isEmpty(volleyErrorMsg)) {
+ return;
+ }
+ String logText;
+ if (statusCode == -1) {
+ logText = volleyErrorMsg;
+ } else {
+ logText = volleyErrorMsg + ", status " + statusCode;
+ }
+ Log.e(TAG + "-" + tag.toString(), logText);
+ addEntry(tag, LogLevel.w, logText);
+ }
+
+ // --------------------------------------------------------------------------------------------------------
+
+ private static final int MAX_ENTRIES = 99;
+
+ private enum LogLevel {
+ v, d, i, w, e;
+ private String toHtmlColor() {
+ switch(this) {
+ case v:
+ return "grey";
+ case i:
+ return "black";
+ case w:
+ return "purple";
+ case e:
+ return "red";
+ case d:
+ default:
+ return "teal";
+ }
+ }
+ }
+
+ private static class LogEntry {
+ LogLevel mLogLevel;
+ String mLogText;
+ T mLogTag;
+
+ public LogEntry(LogLevel logLevel, String logText, T logTag) {
+ mLogLevel = logLevel;
+ mLogText = logText;
+ if (mLogText == null) {
+ mLogText = "null";
+ }
+ mLogTag = logTag;
+ }
+
+ private String toHtml() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<font color=\"");
+ sb.append(mLogLevel.toHtmlColor());
+ sb.append("\">");
+ sb.append("[");
+ sb.append(mLogTag.name());
+ sb.append("] ");
+ sb.append(mLogLevel.name());
+ sb.append(": ");
+ sb.append(TextUtils.htmlEncode(mLogText).replace("\n", "<br />"));
+ sb.append("</font>");
+ return sb.toString();
+ }
+ }
+
+ private static class LogEntryList extends ArrayList<LogEntry> {
+ private synchronized boolean addEntry(LogEntry entry) {
+ if (size() >= MAX_ENTRIES)
+ removeFirstEntry();
+ return add(entry);
+ }
+ private void removeFirstEntry() {
+ Iterator<LogEntry> it = iterator();
+ if (!it.hasNext())
+ return;
+ try {
+ remove(it.next());
+ } catch (NoSuchElementException e) {
+ // ignore
+ }
+ }
+ }
+
+ private static LogEntryList mLogEntries = new LogEntryList();
+
+ private static void addEntry(T tag, LogLevel level, String text) {
+ // skip if recording is disabled (default)
+ if (!mEnableRecording) {
+ return;
+ }
+ LogEntry entry = new LogEntry(level, text, tag);
+ mLogEntries.addEntry(entry);
+ }
+
+ private static String getStringStackTrace(Throwable throwable) {
+ StringWriter errors = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(errors));
+ return errors.toString();
+ }
+
+ /**
+ * Returns entire log as html for display (see AppLogViewerActivity)
+ * @param context
+ * @return Arraylist of Strings containing log messages
+ */
+ public static ArrayList<String> toHtmlList(Context context) {
+ ArrayList<String> items = new ArrayList<String>();
+
+ // add version & device info - be sure to change HEADER_LINE_COUNT if additional lines are added
+ items.add("<strong>WordPress Android version: " + PackageUtils.getVersionName(context) + "</strong>");
+ items.add("<strong>Android device name: " + DeviceUtils.getInstance().getDeviceName(context) + "</strong>");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ while (it.hasNext()) {
+ items.add(it.next().toHtml());
+ }
+ return items;
+ }
+
+ /**
+ * Converts the entire log to plain text
+ * @param context
+ * @return The log as plain text
+ */
+ public static String toPlainText(Context context) {
+ StringBuilder sb = new StringBuilder();
+
+ // add version & device info
+ sb.append("WordPress Android version: " + PackageUtils.getVersionName(context)).append("\n")
+ .append("Android device name: " + DeviceUtils.getInstance().getDeviceName(context)).append("\n\n");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ int lineNum = 1;
+ while (it.hasNext()) {
+ sb.append(String.format("%02d - ", lineNum))
+ .append(it.next().mLogText)
+ .append("\n");
+ lineNum++;
+ }
+ return sb.toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
new file mode 100644
index 000000000..c81ec64a5
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.util;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+public class BlogUtils {
+ public static Comparator<Object> BlogNameComparator = new Comparator<Object>() {
+ public int compare(Object blog1, Object blog2) {
+ Map<String, Object> blogMap1 = (Map<String, Object>) blog1;
+ Map<String, Object> blogMap2 = (Map<String, Object>) blog2;
+ String blogName1 = getBlogNameOrHomeURLFromAccountMap(blogMap1);
+ String blogName2 = getBlogNameOrHomeURLFromAccountMap(blogMap2);
+ return blogName1.compareToIgnoreCase(blogName2);
+ }
+ };
+
+ /**
+ * Return a blog name or blog home URL if trimmed name is an empty string
+ */
+ public static String getBlogNameOrHomeURLFromAccountMap(Map<String, Object> account) {
+ String blogName = getBlogNameFromAccountMap(account);
+ if (blogName.trim().length() == 0) {
+ blogName = BlogUtils.getHomeURLOrHostNameFromAccountMap(account);
+ }
+ return blogName;
+ }
+
+ /**
+ * Return a blog name or blog url (host part only) if trimmed name is an empty string
+ */
+ public static String getBlogNameFromAccountMap(Map<String, Object> account) {
+ return StringUtils.unescapeHTML(MapUtils.getMapStr(account, "blogName"));
+ }
+
+ /**
+ * Return the blog home URL setting or the host name if home URL is an empty string.
+ */
+ public static String getHomeURLOrHostNameFromAccountMap(Map<String, Object> account) {
+ String homeURL = UrlUtils.removeScheme(MapUtils.getMapStr(account, "homeURL"));
+ homeURL = StringUtils.removeTrailingSlash(homeURL);
+
+ if (homeURL.length() == 0) {
+ return UrlUtils.getHost(MapUtils.getMapStr(account, "url"));
+ }
+
+ return homeURL;
+ }
+
+ public static String[] getBlogNamesFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> blogNames = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ blogNames.add(getBlogNameOrHomeURLFromAccountMap(account));
+ }
+ return blogNames.toArray(new String[blogNames.size()]);
+ }
+
+ public static String[] getHomeURLOrHostNamesFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> urls = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ urls.add(getHomeURLOrHostNameFromAccountMap(account));
+ }
+ return urls.toArray(new String[urls.size()]);
+ }
+
+ public static String[] getBlogIdsFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> ids = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ ids.add(MapUtils.getMapStr(account, "blogId"));
+ }
+ return ids.toArray(new String[ids.size()]);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java
new file mode 100644
index 000000000..2a796f3ee
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java
@@ -0,0 +1,246 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class DateTimeUtils {
+ private DateTimeUtils() {
+ throw new AssertionError();
+ }
+
+ // See http://drdobbs.com/java/184405382
+ private static final ThreadLocal<DateFormat> ISO8601_FORMAT = new ThreadLocal<DateFormat>() {
+ @Override
+ protected DateFormat initialValue() {
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
+ }
+ };
+
+ /**
+ * Converts a date to a relative time span ("8h", "3d", etc.) - similar to
+ * DateUtils.getRelativeTimeSpanString but returns shorter result
+ */
+ public static String javaDateToTimeSpan(final Date date, Context context) {
+ if (date == null) {
+ return "";
+ }
+
+ long passedTime = date.getTime();
+ long currentTime = System.currentTimeMillis();
+
+ // return "now" if less than a minute has elapsed
+ long secondsSince = (currentTime - passedTime) / 1000;
+ if (secondsSince < 60) {
+ return context.getString(R.string.timespan_now);
+ }
+
+ // less than an hour (ex: 12m)
+ long minutesSince = secondsSince / 60;
+ if (minutesSince < 60) {
+ return Long.toString(minutesSince) + "m";
+ }
+
+ // less than a day (ex: 17h)
+ long hoursSince = minutesSince / 60;
+ if (hoursSince < 24) {
+ return Long.toString(hoursSince) + "h";
+ }
+
+ // less than a week (ex: 5d)
+ long daysSince = hoursSince / 24;
+ if (daysSince < 7) {
+ return Long.toString(daysSince) + "d";
+ }
+
+ // less than a year old, so return day/month without year (ex: Jan 30)
+ if (daysSince < 365) {
+ return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_NO_YEAR |
+ DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ // date is older, so include year (ex: Jan 30, 2013)
+ return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns a {@link Date}.
+ */
+ public static Date dateFromIso8601(final String strDate) {
+ try {
+ DateFormat formatter = ISO8601_FORMAT.get();
+ return formatter.parse(strDate);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns a {@link Date} in UTC.
+ */
+ public static Date dateUTCFromIso8601(String iso8601date) {
+ try {
+ iso8601date = iso8601date.replace("Z", "+0000").replace("+00:00", "+0000");
+ DateFormat formatter = ISO8601_FORMAT.get();
+ formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return formatter.parse(iso8601date);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Given a {@link Date}, returns an ISO 8601-formatted String.
+ */
+ public static String iso8601FromDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ DateFormat formatter = ISO8601_FORMAT.get();
+ return formatter.format(date);
+ }
+
+ /**
+ * Given a {@link Date}, returns an ISO 8601-formatted String in UTC.
+ */
+ public static String iso8601UTCFromDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ TimeZone tz = TimeZone.getTimeZone("UTC");
+ DateFormat formatter = ISO8601_FORMAT.get();
+ formatter.setTimeZone(tz);
+
+ String iso8601date = formatter.format(date);
+
+ // Use "+00:00" notation rather than "+0000" to be consistent with the WP.COM API
+ return iso8601date.replace("+0000", "+00:00");
+ }
+
+ /**
+ * Returns the current UTC date
+ */
+ public static Date nowUTC() {
+ Date dateTimeNow = new Date();
+ return localDateToUTC(dateTimeNow);
+ }
+
+ public static Date localDateToUTC(Date dtLocal) {
+ if (dtLocal == null) {
+ return null;
+ }
+ TimeZone tz = TimeZone.getDefault();
+ int currentOffsetFromUTC = tz.getRawOffset() + (tz.inDaylightTime(dtLocal) ? tz.getDSTSavings() : 0);
+ return new Date(dtLocal.getTime() - currentOffsetFromUTC);
+ }
+
+ // Routines to return a diff between two dates - always return a positive number
+
+ public static int daysBetween(Date dt1, Date dt2) {
+ long hrDiff = hoursBetween(dt1, dt2);
+ if (hrDiff == 0) {
+ return 0;
+ }
+ return (int) (hrDiff / 24);
+ }
+
+ public static int hoursBetween(Date dt1, Date dt2) {
+ long minDiff = minutesBetween(dt1, dt2);
+ if (minDiff == 0) {
+ return 0;
+ }
+ return (int) (minDiff / 60);
+ }
+
+ public static int minutesBetween(Date dt1, Date dt2) {
+ long msDiff = millisecondsBetween(dt1, dt2);
+ if (msDiff == 0) {
+ return 0;
+ }
+ return (int) (msDiff / 60000);
+ }
+
+ public static int secondsBetween(Date dt1, Date dt2) {
+ long msDiff = millisecondsBetween(dt1, dt2);
+ if (msDiff == 0) {
+ return 0;
+ }
+ return (int) (msDiff / 1000);
+ }
+
+ public static long millisecondsBetween(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return 0;
+ }
+ return Math.abs(dt1.getTime() - dt2.getTime());
+ }
+
+ public static boolean isSameYear(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return false;
+ }
+ return dt1.getYear() == dt2.getYear();
+ }
+
+ public static boolean isSameMonthAndYear(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return false;
+ }
+ return dt1.getYear() == dt2.getYear() && dt1.getMonth() == dt2.getMonth();
+ }
+
+ // Routines involving Unix timestamps (GMT assumed)
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns the corresponding UNIX timestamp.
+ */
+ public static long timestampFromIso8601(final String strDate) {
+ return (timestampFromIso8601Millis(strDate) / 1000);
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns the corresponding timestamp in milliseconds.
+ */
+ public static long timestampFromIso8601Millis(final String strDate) {
+ Date date = dateFromIso8601(strDate);
+ if (date == null) {
+ return 0;
+ }
+ return (date.getTime());
+ }
+
+ /**
+ * Given a UNIX timestamp, returns the corresponding {@link Date}.
+ */
+ public static Date dateFromTimestamp(long timestamp) {
+ return new java.util.Date(timestamp * 1000);
+ }
+
+ /**
+ * Given a UNIX timestamp, returns an ISO 8601-formatted date as a String.
+ */
+ public static String iso8601FromTimestamp(long timestamp) {
+ return iso8601FromDate(dateFromTimestamp(timestamp));
+ }
+
+ /**
+ * Given a UNIX timestamp, returns an ISO 8601-formatted date in UTC as a String.
+ */
+ public static String iso8601UTCFromTimestamp(long timestamp) {
+ return iso8601UTCFromDate(dateFromTimestamp(timestamp));
+ }
+
+ /**
+ * Given a UNIX timestamp, returns a relative time span ("8h", "3d", etc.).
+ */
+ public static String timeSpanFromTimestamp(long timestamp, Context context) {
+ Date dateGMT = dateFromTimestamp(timestamp);
+ return javaDateToTimeSpan(dateGMT, context);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java
new file mode 100644
index 000000000..639d5479c
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java
@@ -0,0 +1,94 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class DeviceUtils {
+ private static DeviceUtils instance;
+ private boolean isKindleFire = false;
+
+ public boolean isKindleFire() {
+ return isKindleFire;
+ }
+
+ public static DeviceUtils getInstance() {
+ if (instance == null) {
+ instance = new DeviceUtils();
+ }
+ return instance;
+ }
+
+ private DeviceUtils() {
+ isKindleFire = android.os.Build.MODEL.equalsIgnoreCase("kindle fire") ? true: false;
+ }
+
+ /**
+ * Checks camera availability recursively based on API level.
+ *
+ * TODO: change "android.hardware.camera.front" and "android.hardware.camera.any" to
+ * {@link PackageManager#FEATURE_CAMERA_FRONT} and {@link PackageManager#FEATURE_CAMERA_ANY},
+ * respectively, once they become accessible or minSdk version is incremented.
+ *
+ * @param context The context.
+ * @return Whether camera is available.
+ */
+ public boolean hasCamera(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
+ || pm.hasSystemFeature("android.hardware.camera.front");
+ }
+
+ return pm.hasSystemFeature("android.hardware.camera.any");
+ }
+
+ public String getDeviceName(Context context) {
+ String manufacturer = Build.MANUFACTURER;
+ String undecodedModel = Build.MODEL;
+ String model = null;
+
+ try {
+ Properties prop = new Properties();
+ InputStream fileStream;
+ // Read the device name from a precomplied list:
+ // see http://making.meetup.com/post/29648976176/human-readble-android-device-names
+ fileStream = context.getAssets().open("android_models.properties");
+ prop.load(fileStream);
+ fileStream.close();
+ String decodedModel = prop.getProperty(undecodedModel.replaceAll(" ", "_"));
+ if (decodedModel != null && !decodedModel.trim().equals("")) {
+ model = decodedModel;
+ }
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e.getMessage());
+ }
+
+ if (model == null) { //Device model not found in the list
+ if (undecodedModel.startsWith(manufacturer)) {
+ model = capitalize(undecodedModel);
+ } else {
+ model = capitalize(manufacturer) + " " + undecodedModel;
+ }
+ }
+ return model;
+ }
+
+ private String capitalize(String s) {
+ if (s == null || s.length() == 0) {
+ return "";
+ }
+ char first = s.charAt(0);
+ if (Character.isUpperCase(first)) {
+ return s;
+ } else {
+ return Character.toUpperCase(first) + s.substring(1);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
new file mode 100644
index 000000000..40a017e9a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
@@ -0,0 +1,91 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class DisplayUtils {
+ private DisplayUtils() {
+ throw new AssertionError();
+ }
+
+ public static boolean isLandscape(Context context) {
+ if (context == null)
+ return false;
+ return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ public static Point getDisplayPixelSize(Context context) {
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ return size;
+ }
+
+ public static int getDisplayPixelWidth(Context context) {
+ Point size = getDisplayPixelSize(context);
+ return (size.x);
+ }
+
+ public static int getDisplayPixelHeight(Context context) {
+ Point size = getDisplayPixelSize(context);
+ return (size.y);
+ }
+
+ public static float spToPx(Context context, float sp){
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ final float scale = displayMetrics.scaledDensity;
+ return sp * scale;
+ }
+
+ public static int dpToPx(Context context, int dp) {
+ float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+ context.getResources().getDisplayMetrics());
+ return (int) px;
+ }
+
+ public static int pxToDp(Context context, int px) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return (int) ((px/displayMetrics.density)+0.5);
+ }
+
+ public static boolean isXLarge(Context context) {
+ if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
+ == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * returns the height of the ActionBar if one is enabled - supports both the native ActionBar
+ * and ActionBarSherlock - http://stackoverflow.com/a/15476793/1673548
+ */
+ public static int getActionBarHeight(Context context) {
+ if (context == null) {
+ return 0;
+ }
+ TypedValue tv = new TypedValue();
+ if (context.getTheme() != null
+ && context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
+ return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
+ }
+
+ // if we get this far, it's because the device doesn't support an ActionBar,
+ // so return the standard ActionBar height (48dp)
+ return dpToPx(context, 48);
+ }
+
+ /**
+ * detect when FEATURE_ACTION_BAR_OVERLAY has been set
+ */
+ public static boolean hasActionBarOverlay(Window window) {
+ return window.hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
new file mode 100644
index 000000000..66a0c77fd
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
@@ -0,0 +1,75 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * EditText utils
+ */
+public class EditTextUtils {
+ private EditTextUtils() {
+ throw new AssertionError();
+ }
+
+ /**
+ * returns non-null text string from passed TextView
+ */
+ public static String getText(TextView textView) {
+ return (textView != null) ? textView.getText().toString() : "";
+ }
+
+ /**
+ * moves caret to end of text
+ */
+ public static void moveToEnd(EditText edit) {
+ if (edit.getText() == null) {
+ return;
+ }
+ edit.setSelection(edit.getText().toString().length());
+ }
+
+ /**
+ * returns true if nothing has been entered into passed editor
+ */
+ public static boolean isEmpty(EditText edit) {
+ return TextUtils.isEmpty(getText(edit));
+ }
+
+ /**
+ * hide the soft keyboard for the passed EditText
+ */
+ public static void hideSoftInput(EditText edit) {
+ if (edit == null) {
+ return;
+ }
+
+ InputMethodManager imm = getInputMethodManager(edit);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * show the soft keyboard for the passed EditText
+ */
+ public static void showSoftInput(EditText edit) {
+ if (edit == null) {
+ return;
+ }
+
+ edit.requestFocus();
+
+ InputMethodManager imm = getInputMethodManager(edit);
+ if (imm != null) {
+ imm.showSoftInput(edit, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+
+ private static InputMethodManager getInputMethodManager(EditText edit) {
+ Context context = edit.getContext();
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java
new file mode 100644
index 000000000..45661d980
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java
@@ -0,0 +1,106 @@
+package org.wordpress.android.util;
+
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.util.SparseArray;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES;
+
+public class EmoticonsUtils {
+ public static final int EMOTICON_COLOR = 0xFF21759B;
+ private static final boolean HAS_EMOJI = SDK_INT >= VERSION_CODES.JELLY_BEAN;
+ private static final Map<String, String> wpSmilies;
+ public static final SparseArray<String> wpSmiliesCodePointToText;
+
+ static {
+ Map<String, String> smilies = new HashMap<String, String>();
+ smilies.put("icon_mrgreen.gif", HAS_EMOJI ? "\uD83D\uDE00" : ":mrgreen:" );
+ smilies.put("icon_neutral.gif", HAS_EMOJI ? "\uD83D\uDE14" : ":|" );
+ smilies.put("icon_twisted.gif", HAS_EMOJI ? "\uD83D\uDE16" : ":twisted:" );
+ smilies.put("icon_arrow.gif", HAS_EMOJI ? "\u27A1" : ":arrow:" );
+ smilies.put("icon_eek.gif", HAS_EMOJI ? "\uD83D\uDE32" : "8-O" );
+ smilies.put("icon_smile.gif", HAS_EMOJI ? "\uD83D\uDE0A" : ":)" );
+ smilies.put("icon_confused.gif", HAS_EMOJI ? "\uD83D\uDE15" : ":?" );
+ smilies.put("icon_cool.gif", HAS_EMOJI ? "\uD83D\uDE0A" : "8)" );
+ smilies.put("icon_evil.gif", HAS_EMOJI ? "\uD83D\uDE21" : ":evil:" );
+ smilies.put("icon_biggrin.gif", HAS_EMOJI ? "\uD83D\uDE03" : ":D" );
+ smilies.put("icon_idea.gif", HAS_EMOJI ? "\uD83D\uDCA1" : ":idea:" );
+ smilies.put("icon_redface.gif", HAS_EMOJI ? "\uD83D\uDE33" : ":oops:" );
+ smilies.put("icon_razz.gif", HAS_EMOJI ? "\uD83D\uDE1D" : ":P" );
+ smilies.put("icon_rolleyes.gif", HAS_EMOJI ? "\uD83D\uDE0F" : ":roll:" );
+ smilies.put("icon_wink.gif", HAS_EMOJI ? "\uD83D\uDE09" : ";)" );
+ smilies.put("icon_cry.gif", HAS_EMOJI ? "\uD83D\uDE22" : ":'(" );
+ smilies.put("icon_surprised.gif", HAS_EMOJI ? "\uD83D\uDE32" : ":o" );
+ smilies.put("icon_lol.gif", HAS_EMOJI ? "\uD83D\uDE03" : ":lol:" );
+ smilies.put("icon_mad.gif", HAS_EMOJI ? "\uD83D\uDE21" : ":x" );
+ smilies.put("icon_sad.gif", HAS_EMOJI ? "\uD83D\uDE1E" : ":(" );
+ smilies.put("icon_exclaim.gif", HAS_EMOJI ? "\u2757" : ":!:" );
+ smilies.put("icon_question.gif", HAS_EMOJI ? "\u2753" : ":?:" );
+
+ wpSmilies = Collections.unmodifiableMap(smilies);
+
+ wpSmiliesCodePointToText = new SparseArray<String>(20);
+ wpSmiliesCodePointToText.put(10145, ":arrow:");
+ wpSmiliesCodePointToText.put(128161, ":idea:");
+ wpSmiliesCodePointToText.put(128512, ":mrgreen:");
+ wpSmiliesCodePointToText.put(128515, ":D");
+ wpSmiliesCodePointToText.put(128522, ":)");
+ wpSmiliesCodePointToText.put(128521, ";)");
+ wpSmiliesCodePointToText.put(128532, ":|");
+ wpSmiliesCodePointToText.put(128533, ":?");
+ wpSmiliesCodePointToText.put(128534, ":twisted:");
+ wpSmiliesCodePointToText.put(128542, ":(");
+ wpSmiliesCodePointToText.put(128545, ":evil:");
+ wpSmiliesCodePointToText.put(128546, ":'(");
+ wpSmiliesCodePointToText.put(128562, ":o");
+ wpSmiliesCodePointToText.put(128563, ":oops:");
+ wpSmiliesCodePointToText.put(128527, ":roll:");
+ wpSmiliesCodePointToText.put(10071, ":!:");
+ wpSmiliesCodePointToText.put(10067, ":?:");
+ }
+
+ public static String lookupImageSmiley(String url){
+ return lookupImageSmiley(url, "");
+ }
+
+ public static String lookupImageSmiley(String url, String ifNone){
+ String file = url.substring(url.lastIndexOf("/") + 1);
+ if (wpSmilies.containsKey(file)) {
+ return wpSmilies.get(file);
+ }
+ return ifNone;
+ }
+
+ public static Spanned replaceEmoticonsWithEmoji(SpannableStringBuilder html){
+ ImageSpan imgs[] = html.getSpans(0, html.length(), ImageSpan.class);
+ for (ImageSpan img : imgs) {
+ String emoticon = EmoticonsUtils.lookupImageSmiley(img.getSource());
+ if (!emoticon.equals("")) {
+ int start = html.getSpanStart(img);
+ html.replace(start, html.getSpanEnd(img), emoticon);
+ html.setSpan(new ForegroundColorSpan(EMOTICON_COLOR), start,
+ start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ html.removeSpan(img);
+ }
+ }
+ return html;
+ }
+
+ public static String replaceEmoticonsWithEmoji(final String text) {
+ if (text != null && text.contains("icon_")) {
+ final SpannableStringBuilder html = (SpannableStringBuilder)replaceEmoticonsWithEmoji((SpannableStringBuilder) Html.fromHtml(text));
+ // Html.toHtml() is used here rather than toString() since the latter strips html
+ return Html.toHtml(html);
+ } else {
+ return text;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java
new file mode 100644
index 000000000..28282ed5f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.util;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+public class FormatUtils {
+ /*
+ * NumberFormat isn't synchronized, so a separate instance must be created for each thread
+ * http://developer.android.com/reference/java/text/NumberFormat.html
+ */
+ private static final ThreadLocal<NumberFormat> IntegerInstance = new ThreadLocal<NumberFormat>() {
+ @Override
+ protected NumberFormat initialValue() {
+ return NumberFormat.getIntegerInstance();
+ }
+ };
+
+ private static final ThreadLocal<DecimalFormat> DecimalInstance = new ThreadLocal<DecimalFormat>() {
+ @Override
+ protected DecimalFormat initialValue() {
+ return (DecimalFormat) DecimalFormat.getInstance();
+ }
+ };
+
+ /*
+ * returns the passed integer formatted with thousands-separators based on the current locale
+ */
+ public static final String formatInt(int value) {
+ return IntegerInstance.get().format(value).toString();
+ }
+
+ public static final String formatDecimal(int value) {
+ return DecimalInstance.get().format(value).toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java
new file mode 100644
index 000000000..372473e15
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java
@@ -0,0 +1,116 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+public final class GeocoderUtils {
+ private GeocoderUtils() {
+ throw new AssertionError();
+ }
+
+ public static Geocoder getGeocoder(Context context) {
+ // first make sure a Geocoder service exists on this device (requires API 9)
+ if (!Geocoder.isPresent()) {
+ return null;
+ }
+
+ Geocoder gcd;
+
+ try {
+ gcd = new Geocoder(context, LanguageUtils.getCurrentDeviceLanguage(context));
+ } catch (NullPointerException cannotIstantiateEx) {
+ AppLog.e(AppLog.T.UTILS, "Cannot instantiate Geocoder", cannotIstantiateEx);
+ return null;
+ }
+
+ return gcd;
+ }
+
+ public static Address getAddressFromCoords(Context context, double latitude, double longitude) {
+ Address address = null;
+ List<Address> addresses = null;
+
+ Geocoder gcd = getGeocoder(context);
+
+ if (gcd == null) {
+ return null;
+ }
+
+ try {
+ addresses = gcd.getFromLocation(latitude, longitude, 1);
+ } catch (IOException e) {
+ // may get "Unable to parse response from server" IOException here if Geocoder
+ // service is hit too frequently
+ AppLog.e(AppLog.T.UTILS,
+ "Unable to parse response from server. Is Geocoder service hitting the server too frequently?",
+ e
+ );
+ }
+
+ // addresses may be null or empty if network isn't connected
+ if (addresses != null && addresses.size() > 0) {
+ address = addresses.get(0);
+ }
+
+ return address;
+ }
+
+ public static Address getAddressFromLocationName(Context context, String locationName) {
+ int maxResults = 1;
+ Address address = null;
+ List<Address> addresses = null;
+
+ Geocoder gcd = getGeocoder(context);
+
+ if (gcd == null) {
+ return null;
+ }
+
+ try {
+ addresses = gcd.getFromLocationName(locationName, maxResults);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, "Failed to get coordinates from location", e);
+ }
+
+ // addresses may be null or empty if network isn't connected
+ if (addresses != null && addresses.size() > 0) {
+ address = addresses.get(0);
+ }
+
+ return address;
+ }
+
+ public static String getLocationNameFromAddress(Address address) {
+ String locality = "", adminArea = "", country = "";
+ if (address.getLocality() != null) {
+ locality = address.getLocality();
+ }
+
+ if (address.getAdminArea() != null) {
+ adminArea = address.getAdminArea();
+ }
+
+ if (address.getCountryName() != null) {
+ country = address.getCountryName();
+ }
+
+ return ((locality.equals("")) ? locality : locality + ", ")
+ + ((adminArea.equals("")) ? adminArea : adminArea + " ") + country;
+ }
+
+ public static double[] getCoordsFromAddress(Address address) {
+ double[] coordinates = new double[2];
+
+ if (address.hasLatitude() && address.hasLongitude()) {
+ coordinates[0] = address.getLatitude();
+ coordinates[1] = address.getLongitude();
+ }
+
+ return coordinates;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
new file mode 100644
index 000000000..1fbfb3e56
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+/**
+ * see https://en.gravatar.com/site/implement/images/
+ */
+public class GravatarUtils {
+
+ // by default tell gravatar to respond to non-existent images with a 404 - this means
+ // it's up to the caller to catch the 404 and provide a suitable default image
+ private static final DefaultImage DEFAULT_GRAVATAR = DefaultImage.STATUS_404;
+
+ public static enum DefaultImage {
+ MYSTERY_MAN,
+ STATUS_404,
+ IDENTICON,
+ MONSTER,
+ WAVATAR,
+ RETRO,
+ BLANK;
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case MYSTERY_MAN:
+ return "mm";
+ case STATUS_404:
+ return "404";
+ case IDENTICON:
+ return "identicon";
+ case MONSTER:
+ return "monsterid";
+ case WAVATAR:
+ return "wavatar";
+ case RETRO:
+ return "retro";
+ default:
+ return "blank";
+ }
+ }
+ }
+
+ /*
+ * gravatars often contain the ?s= parameter which determines their size - detect this and
+ * replace it with a new ?s= parameter which requests the avatar at the exact size needed
+ */
+ public static String fixGravatarUrl(final String imageUrl, int avatarSz) {
+ return fixGravatarUrl(imageUrl, avatarSz, DEFAULT_GRAVATAR);
+ }
+ public static String fixGravatarUrl(final String imageUrl, int avatarSz, DefaultImage defaultImage) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // if this isn't a gravatar image, return as resized photon image url
+ if (!imageUrl.contains("gravatar.com")) {
+ return PhotonUtils.getPhotonImageUrl(imageUrl, avatarSz, avatarSz);
+ }
+
+ // remove all other params, then add query string for size and default image
+ return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=" + defaultImage.toString();
+ }
+
+ public static String gravatarFromEmail(final String email, int size) {
+ return gravatarFromEmail(email, size, DEFAULT_GRAVATAR);
+ }
+ public static String gravatarFromEmail(final String email, int size, DefaultImage defaultImage) {
+ return "http://gravatar.com/avatar/"
+ + StringUtils.getMd5Hash(StringUtils.notNullStr(email))
+ + "?d=" + defaultImage.toString()
+ + "&size=" + Integer.toString(size);
+ }
+
+ public static String blavatarFromUrl(final String url, int size) {
+ return blavatarFromUrl(url, size, DEFAULT_GRAVATAR);
+ }
+ public static String blavatarFromUrl(final String url, int size, DefaultImage defaultImage) {
+ return "http://gravatar.com/blavatar/"
+ + StringUtils.getMd5Hash(UrlUtils.getHost(url))
+ + "?d=" + defaultImage.toString()
+ + "&size=" + Integer.toString(size);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java
new file mode 100644
index 000000000..9773d45d7
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.util;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+public class HTTPUtils {
+ public static final int REQUEST_TIMEOUT_MS = 30000;
+
+ /**
+ * Builds an HttpURLConnection from a URL and header map. Will force HTTPS usage if given an Authorization header.
+ * @throws IOException
+ */
+ public static HttpURLConnection setupUrlConnection(String url, Map<String, String> headers) throws IOException {
+ // Force HTTPS usage if an authorization header was specified
+ if (headers.keySet().contains("Authorization")) {
+ url = UrlUtils.makeHttps(url);
+ }
+
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setReadTimeout(REQUEST_TIMEOUT_MS);
+ conn.setConnectTimeout(REQUEST_TIMEOUT_MS);
+
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ conn.setRequestProperty(entry.getKey(), entry.getValue());
+ }
+
+ return conn;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
new file mode 100644
index 000000000..b5319372a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
@@ -0,0 +1,156 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.QuoteSpan;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.util.helpers.WPHtmlTagHandler;
+import org.wordpress.android.util.helpers.WPImageGetter;
+import org.wordpress.android.util.helpers.WPQuoteSpan;
+
+public class HtmlUtils {
+
+ /**
+ * Removes html from the passed string - relies on Html.fromHtml which handles invalid HTML,
+ * but it's very slow, so avoid using this where performance is important
+ * @param text String containing html
+ * @return String without HTML
+ */
+ public static String stripHtml(final String text) {
+ if (TextUtils.isEmpty(text)) {
+ return "";
+ }
+ return Html.fromHtml(text).toString().trim();
+ }
+
+ /**
+ * This is much faster than stripHtml() but should only be used when we know the html is valid
+ * since the regex will be unpredictable with invalid html
+ * @param str String containing only valid html
+ * @return String without HTML
+ */
+ public static String fastStripHtml(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return str;
+ }
+
+ // insert a line break before P tags unless the only one is at the start
+ if (str.lastIndexOf("<p") > 0) {
+ str = str.replaceAll("<p(.|\n)*?>", "\n<p>");
+ }
+
+ // convert BR tags to line breaks
+ if (str.contains("<br")) {
+ str = str.replaceAll("<br(.|\n)*?>", "\n");
+ }
+
+ // use regex to strip tags, then convert entities in the result
+ return trimStart(fastUnescapeHtml(str.replaceAll("<(.|\n)*?>", "")));
+ }
+
+ /*
+ * Same as apache.commons.lang.StringUtils.stripStart() but also removes non-breaking
+ * space (160) chars
+ */
+ private static String trimStart(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return "";
+ }
+ int start = 0;
+ while (start != strLen && (Character.isWhitespace(str.charAt(start)) || str.charAt(start) == 160)) {
+ start++;
+ }
+ return str.substring(start);
+ }
+
+ /**
+ * Convert html entities to actual Unicode characters - relies on commons apache lang
+ * @param text String to be decoded to Unicode
+ * @return String containing unicode characters
+ */
+ public static String fastUnescapeHtml(final String text) {
+ if (text == null || !text.contains("&")) {
+ return text;
+ }
+ return StringEscapeUtils.unescapeHtml(text);
+ }
+
+ /**
+ * Converts an R.color.xxx resource to an HTML hex color
+ * @param context Android Context
+ * @param resId Android R.color.xxx
+ * @return A String HTML hex color code
+ */
+ public static String colorResToHtmlColor(Context context, int resId) {
+ try {
+ return String.format("#%06X", 0xFFFFFF & context.getResources().getColor(resId));
+ } catch (Resources.NotFoundException e) {
+ return "#000000";
+ }
+ }
+
+ /**
+ * Remove {@code <script>..</script>} blocks from the passed string - added to project after noticing
+ * comments on posts that use the "Sociable" plugin ( http://wordpress.org/plugins/sociable/ )
+ * may have a script block which contains {@code <!--//-->} followed by a CDATA section followed by {@code <!]]>,}
+ * all of which will show up if we don't strip it here.
+ * @see <a href="http://wordpress.org/plugins/sociable/">Wordpress Sociable Plugin</a>
+ * @return String without {@code <script>..</script>}, {@code <!--//-->} blocks followed by a CDATA section followed by {@code <!]]>,}
+ * @param text String containing script tags
+ */
+ public static String stripScript(final String text) {
+ if (text == null) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder(text);
+ int start = sb.indexOf("<script");
+
+ while (start > -1) {
+ int end = sb.indexOf("</script>", start);
+ if (end == -1) {
+ return sb.toString();
+ }
+ sb.delete(start, end + 9);
+ start = sb.indexOf("<script", start);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * An alternative to Html.fromHtml() supporting {@code <ul>}, {@code <ol>}, {@code <blockquote>}
+ * tags and replacing EmoticonsUtils with Emojis
+ * @param source
+ * @param wpImageGetter
+ */
+ public static SpannableStringBuilder fromHtml(String source, WPImageGetter wpImageGetter) {
+ SpannableStringBuilder html;
+ try {
+ html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, new WPHtmlTagHandler());
+ } catch (RuntimeException runtimeException) {
+ // In case our tag handler fails
+ html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, null);
+ }
+ EmoticonsUtils.replaceEmoticonsWithEmoji(html);
+ QuoteSpan spans[] = html.getSpans(0, html.length(), QuoteSpan.class);
+ for (QuoteSpan span : spans) {
+ html.setSpan(new WPQuoteSpan(), html.getSpanStart(span), html.getSpanEnd(span), html.getSpanFlags(span));
+ html.setSpan(new ForegroundColorSpan(0xFF666666), html.getSpanStart(span), html.getSpanEnd(span),
+ html.getSpanFlags(span));
+ html.removeSpan(span);
+ }
+ return html;
+ }
+
+ public static Spanned fromHtml(String source) {
+ return fromHtml(source, null);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
new file mode 100644
index 000000000..2fd4449b8
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
@@ -0,0 +1,649 @@
+package org.wordpress.android.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+import android.widget.ImageView;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+
+public class ImageUtils {
+ public static int[] getImageSize(Uri uri, Context context){
+ String path = null;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ if (uri.toString().contains("content:")) {
+ String[] projection = new String[] { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };
+ Cursor cur = null;
+ try {
+ cur = context.getContentResolver().query(uri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ path = cur.getString(dataColumn);
+ }
+ } catch (IllegalStateException stateException) {
+ Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + uri);
+ } finally {
+ SqlUtils.closeCursor(cur);
+ }
+ }
+
+ if (TextUtils.isEmpty(path)) {
+ //The file isn't ContentResolver, or it can't be access by ContentResolver. Try to access the file directly.
+ path = uri.toString().replace("content://media", "");
+ path = path.replace("file://", "");
+ }
+
+ BitmapFactory.decodeFile(path, options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ return new int[]{imageWidth, imageHeight};
+ }
+
+ // Read the orientation from ContentResolver. If it fails, read from EXIF.
+ public static int getImageOrientation(Context ctx, String filePath) {
+ Uri curStream;
+ int orientation = 0;
+
+ // Remove file protocol
+ filePath = filePath.replace("file://", "");
+
+ if (!filePath.contains("content://"))
+ curStream = Uri.parse("content://media" + filePath);
+ else
+ curStream = Uri.parse(filePath);
+
+ try {
+ Cursor cur = ctx.getContentResolver().query(curStream, new String[]{MediaStore.Images.Media.ORIENTATION}, null, null, null);
+ if (cur != null) {
+ if (cur.moveToFirst()) {
+ orientation = cur.getInt(cur.getColumnIndex(MediaStore.Images.Media.ORIENTATION));
+ }
+ cur.close();
+ }
+ } catch (Exception errReadingContentResolver) {
+ AppLog.e(AppLog.T.UTILS, errReadingContentResolver);
+ }
+
+ if (orientation == 0) {
+ orientation = getExifOrientation(filePath);
+ }
+
+ return orientation;
+ }
+
+
+ public static int getExifOrientation(String path) {
+ ExifInterface exif;
+ try {
+ exif = new ExifInterface(path);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return 0;
+ }
+
+ int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
+
+ switch (exifOrientation) {
+ case ExifInterface.ORIENTATION_NORMAL:
+ return 0;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return 90;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return 180;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ public static Bitmap downloadBitmap(String url) {
+ final DefaultHttpClient client = new DefaultHttpClient();
+
+ final HttpGet getRequest = new HttpGet(url);
+
+ try {
+ HttpResponse response = client.execute(getRequest);
+ final int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode != HttpStatus.SC_OK) {
+ AppLog.w(AppLog.T.UTILS, "ImageDownloader Error " + statusCode
+ + " while retrieving bitmap from " + url);
+ return null;
+ }
+
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ InputStream inputStream = null;
+ try {
+ inputStream = entity.getContent();
+ return BitmapFactory.decodeStream(inputStream);
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ entity.consumeContent();
+ }
+ }
+ } catch (Exception e) {
+ // Could provide a more explicit error message for IOException or
+ // IllegalStateException
+ getRequest.abort();
+ AppLog.w(AppLog.T.UTILS, "ImageDownloader Error while retrieving bitmap from " + url);
+ }
+ return null;
+ }
+
+ /** From http://developer.android.com/training/displaying-bitmaps/load-bitmap.html **/
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ // Calculate ratios of height and width to requested height and width
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+
+ // Choose the smallest ratio as inSampleSize value, this will guarantee
+ // a final image with both dimensions larger than or equal to the
+ // requested height and width.
+ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ return inSampleSize;
+ }
+
+
+ public interface BitmapWorkerCallback {
+ public void onBitmapReady(String filePath, ImageView imageView, Bitmap bitmap);
+ }
+
+ public static class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private final BitmapWorkerCallback callback;
+ private int targetWidth;
+ private int targetHeight;
+ private String path;
+
+ public BitmapWorkerTask(ImageView imageView, int width, int height, BitmapWorkerCallback callback) {
+ // Use a WeakReference to ensure the ImageView can be garbage collected
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ this.callback = callback;
+ targetWidth = width;
+ targetHeight = height;
+ }
+
+ // Decode image in background.
+ @Override
+ protected Bitmap doInBackground(String... params) {
+ path = params[0];
+
+ BitmapFactory.Options bfo = new BitmapFactory.Options();
+ bfo.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, bfo);
+
+ bfo.inSampleSize = calculateInSampleSize(bfo, targetWidth, targetHeight);
+ bfo.inJustDecodeBounds = false;
+
+ // get proper rotation
+ int bitmapWidth = 0;
+ int bitmapHeight = 0;
+ try {
+ File f = new File(path);
+ ExifInterface exif = new ExifInterface(f.getPath());
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ int angle = 0;
+ if (orientation == ExifInterface.ORIENTATION_NORMAL) { // no need to rotate
+ return BitmapFactory.decodeFile(path, bfo);
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
+ angle = 90;
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
+ angle = 180;
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
+ angle = 270;
+ }
+
+ Matrix mat = new Matrix();
+ mat.postRotate(angle);
+
+ try {
+ Bitmap bmp = BitmapFactory.decodeStream(new FileInputStream(f), null, bfo);
+ if (bmp == null) {
+ AppLog.e(AppLog.T.UTILS, "can't decode bitmap: " + f.getPath());
+ return null;
+ }
+ bitmapWidth = bmp.getWidth();
+ bitmapHeight = bmp.getHeight();
+ return Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), mat, true);
+ } catch (OutOfMemoryError oom) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + oom);
+ }
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, "Error in setting image", e);
+ }
+
+ return null;
+ }
+
+ // Once complete, see if ImageView is still around and set bitmap.
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (imageViewReference == null || bitmap == null)
+ return;
+
+ final ImageView imageView = imageViewReference.get();
+
+ if (callback != null)
+ callback.onBitmapReady(path, imageView, bitmap);
+
+ }
+ }
+
+
+ public static String getTitleForWPImageSpan(Context ctx, String filePath) {
+ if (filePath == null)
+ return null;
+
+ Uri curStream;
+ String title;
+
+ if (!filePath.contains("content://"))
+ curStream = Uri.parse("content://media" + filePath);
+ else
+ curStream = Uri.parse(filePath);
+
+ if (filePath.contains("video")) {
+ return "Video";
+ } else {
+ String[] projection = new String[] { MediaStore.Images.Thumbnails.DATA };
+
+ Cursor cur;
+ try {
+ cur = ctx.getContentResolver().query(curStream, projection, null, null, null);
+ } catch (Exception e1) {
+ AppLog.e(AppLog.T.UTILS, e1);
+ return null;
+ }
+ File jpeg;
+ if (cur != null) {
+ String thumbData = "";
+ if (cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ thumbData = cur.getString(dataColumn);
+ }
+ cur.close();
+ if (thumbData == null) {
+ return null;
+ }
+ jpeg = new File(thumbData);
+ } else {
+ String path = filePath.toString().replace("file://", "");
+ jpeg = new File(path);
+ }
+ title = jpeg.getName();
+ return title;
+ }
+ }
+
+ /**
+ * Resizes an image to be placed in the Post Content Editor
+ *
+ * @return resized bitmap
+ */
+ public static Bitmap getWPImageSpanThumbnailFromFilePath(Context context, String filePath, int targetWidth) {
+ if (filePath == null || context == null) {
+ return null;
+ }
+
+ Uri curUri;
+ if (!filePath.contains("content://")) {
+ curUri = Uri.parse("content://media" + filePath);
+ } else {
+ curUri = Uri.parse(filePath);
+ }
+
+ if (filePath.contains("video")) {
+ // Load the video thumbnail from the MediaStore
+ int videoId = 0;
+ try {
+ videoId = Integer.parseInt(curUri.getLastPathSegment());
+ } catch (NumberFormatException e) {
+ }
+ ContentResolver crThumb = context.getContentResolver();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+ Bitmap videoThumbnail = MediaStore.Video.Thumbnails.getThumbnail(crThumb, videoId, MediaStore.Video.Thumbnails.MINI_KIND,
+ options);
+ if (videoThumbnail != null) {
+ return getScaledBitmapAtLongestSide(videoThumbnail, targetWidth);
+ } else {
+ return null;
+ }
+ } else {
+ // Create resized bitmap
+ int rotation = getImageOrientation(context, filePath);
+ byte[] bytes = createThumbnailFromUri(context, curUri, targetWidth, null, rotation);
+
+ if (bytes != null && bytes.length > 0) {
+ try {
+ Bitmap resizedBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ if (resizedBitmap != null) {
+ return getScaledBitmapAtLongestSide(resizedBitmap, targetWidth);
+ }
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /*
+ Resize a bitmap to the targetSize on its longest side.
+ */
+ public static Bitmap getScaledBitmapAtLongestSide(Bitmap bitmap, int targetSize) {
+ if (bitmap.getWidth() <= targetSize && bitmap.getHeight() <= targetSize) {
+ // Do not resize.
+ return bitmap;
+ }
+
+ int targetWidth, targetHeight;
+ if (bitmap.getHeight() > bitmap.getWidth()) {
+ // Resize portrait bitmap
+ targetHeight = targetSize;
+ float percentage = (float) targetSize / bitmap.getHeight();
+ targetWidth = (int)(bitmap.getWidth() * percentage);
+ } else {
+ // Resize landscape or square image
+ targetWidth = targetSize;
+ float percentage = (float) targetSize / bitmap.getWidth();
+ targetHeight = (int)(bitmap.getHeight() * percentage);
+ }
+
+ return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);
+ }
+
+ /**
+ * Given the path to an image, resize the image down to within a maximum width
+ * @param path the path to the original image
+ * @param maxWidth the maximum allowed width
+ * @return the path to the resized image
+ */
+ public static String createResizedImageWithMaxWidth(Context context, String path, int maxWidth) {
+ File file = new File(path);
+ if (!file.exists()) {
+ return path;
+ }
+
+ String mimeType = MediaUtils.getMediaFileMimeType(file);
+ if (mimeType.equals("image/gif")) {
+ // Don't rescale gifs to maintain their quality
+ return path;
+ }
+
+ String fileName = MediaUtils.getMediaFileName(file, mimeType);
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileName).toLowerCase();
+
+ int[] dimensions = getImageSize(Uri.fromFile(file), context);
+ int orientation = getImageOrientation(context, path);
+
+ if (dimensions[0] <= maxWidth) {
+ // Image width is within limits; don't resize
+ return path;
+ }
+
+ // Create resized image
+ byte[] bytes = ImageUtils.createThumbnailFromUri(context, Uri.parse(path), maxWidth, fileExtension, orientation);
+
+ if (bytes != null) {
+ try {
+ File resizedImageFile = File.createTempFile("wp-image-", fileExtension);
+ FileOutputStream out = new FileOutputStream(resizedImageFile);
+ out.write(bytes);
+ out.close();
+
+ String tempFilePath = resizedImageFile.getPath();
+
+ if (!TextUtils.isEmpty(tempFilePath)) {
+ return tempFilePath;
+ } else {
+ AppLog.e(AppLog.T.POSTS, "Failed to create resized image");
+ }
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, "Failed to create image temp file");
+ }
+ }
+
+ return path;
+ }
+
+ /**
+ * nbradbury - 21-Feb-2014 - similar to createThumbnail but more efficient since it doesn't
+ * require passing the full-size image as an array of bytes[]
+ */
+ public static byte[] createThumbnailFromUri(Context context,
+ Uri imageUri,
+ int maxWidth,
+ String fileExtension,
+ int rotation) {
+ if (context == null || imageUri == null || maxWidth <= 0)
+ return null;
+
+ String filePath = null;
+ if (imageUri.toString().contains("content:")) {
+ String[] projection = new String[] { MediaStore.Images.Media.DATA };
+ Cursor cur = null;
+ try {
+ cur = context.getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ filePath = cur.getString(dataColumn);
+ }
+ } catch (IllegalStateException stateException) {
+ Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + imageUri);
+ } finally {
+ SqlUtils.closeCursor(cur);
+ }
+ }
+
+ if (TextUtils.isEmpty(filePath)) {
+ //access the file directly
+ filePath = imageUri.toString().replace("content://media", "");
+ filePath = filePath.replace("file://", "");
+ }
+
+ // get just the image bounds
+ BitmapFactory.Options optBounds = new BitmapFactory.Options();
+ optBounds.inJustDecodeBounds = true;
+
+ try {
+ BitmapFactory.decodeFile(filePath, optBounds);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+
+ // determine correct scale value (should be power of 2)
+ // http://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/3549021#3549021
+ int scale = 1;
+ if (maxWidth > 0 && optBounds.outWidth > maxWidth) {
+ double d = Math.pow(2, (int) Math.round(Math.log(maxWidth / (double) optBounds.outWidth) / Math.log(0.5)));
+ scale = (int) d;
+ }
+
+ BitmapFactory.Options optActual = new BitmapFactory.Options();
+ optActual.inSampleSize = scale;
+
+ // Get the roughly resized bitmap
+ final Bitmap bmpResized;
+ try {
+ bmpResized = BitmapFactory.decodeFile(filePath, optActual);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+
+ if (bmpResized == null) {
+ return null;
+ }
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ // Now calculate exact scale in order to resize accurately
+ float percentage = (float) maxWidth / bmpResized.getWidth();
+ float proportionateHeight = bmpResized.getHeight() * percentage;
+ int finalHeight = (int) Math.rint(proportionateHeight);
+
+ float scaleWidth = ((float) maxWidth) / bmpResized.getWidth();
+ float scaleHeight = ((float) finalHeight) / bmpResized.getHeight();
+
+ float scaleBy = Math.min(scaleWidth, scaleHeight);
+
+ // Resize the bitmap to exact size
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleBy, scaleBy);
+
+ // apply rotation
+ if (rotation != 0) {
+ matrix.setRotate(rotation);
+ }
+
+ Bitmap.CompressFormat fmt;
+ if (fileExtension != null && fileExtension.equalsIgnoreCase("png")) {
+ fmt = Bitmap.CompressFormat.PNG;
+ } else {
+ fmt = Bitmap.CompressFormat.JPEG;
+ }
+
+ final Bitmap bmpRotated;
+ try {
+ bmpRotated = Bitmap.createBitmap(bmpResized, 0, 0, bmpResized.getWidth(), bmpResized.getHeight(), matrix,
+ true);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ } catch (NullPointerException e) {
+ // See: https://github.com/wordpress-mobile/WordPress-Android/issues/1844
+ AppLog.e(AppLog.T.UTILS, "Bitmap.createBitmap has thrown a NPE internally. This should never happen: " + e);
+ return null;
+ }
+
+ if (bmpRotated == null) {
+ // Fix an issue where bmpRotated is null even if the documentation doesn't say Bitmap.createBitmap can return null.
+ // See: https://github.com/wordpress-mobile/WordPress-Android/issues/1848
+ return null;
+ }
+
+ bmpRotated.compress(fmt, 100, stream);
+ bmpResized.recycle();
+ bmpRotated.recycle();
+
+ return stream.toByteArray();
+ }
+
+ public static Bitmap getCircularBitmap(final Bitmap bitmap) {
+ if (bitmap==null)
+ return null;
+
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(Color.RED);
+ canvas.drawOval(rectF, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ /**
+ * Returns the passed bitmap with rounded corners
+ * @param bitmap - the bitmap to modify
+ * @param radius - the radius of the corners
+ * @param borderColor - the border to apply (use Color.TRANSPARENT for none)
+ */
+ public static Bitmap getRoundedEdgeBitmap(final Bitmap bitmap, int radius, int borderColor) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(Color.RED);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ if (borderColor != Color.TRANSPARENT) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1f);
+ paint.setColor(borderColor);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+ }
+
+ return output;
+ }
+
+ /**
+ * Get the maximum size a thumbnail can be to fit in either portrait or landscape orientations.
+ */
+ public static int getMaximumThumbnailWidthForEditor(Context context) {
+ int maximumThumbnailWidthForEditor;
+ Point size = DisplayUtils.getDisplayPixelSize(context);
+ int screenWidth = size.x;
+ int screenHeight = size.y;
+ maximumThumbnailWidthForEditor = (screenWidth > screenHeight) ? screenHeight : screenWidth;
+ // 48dp of padding on each side so you can still place the cursor next to the image.
+ int padding = DisplayUtils.dpToPx(context, 48) * 2;
+ maximumThumbnailWidthForEditor -= padding;
+ return maximumThumbnailWidthForEditor;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java
new file mode 100644
index 000000000..196a7b1f3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java
@@ -0,0 +1,251 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+
+public class JSONUtils {
+ private static String QUERY_SEPERATOR = ".";
+ private static String QUERY_ARRAY_INDEX_START = "[";
+ private static String QUERY_ARRAY_INDEX_END = "]";
+ private static String QUERY_ARRAY_FIRST = "first";
+ private static String QUERY_ARRAY_LAST = "last";
+
+ private static final String JSON_NULL_STR = "null";
+ private static final String TAG = "JSONUtils";
+
+ /**
+ * Given a JSONObject and a key path (e.g property.child) and a default it will
+ * traverse the object graph and pull out the desired property
+ */
+ public static <U> U queryJSON(JSONObject source, String query, U defaultObject) {
+ if (source == null) {
+ AppLog.e(T.UTILS, "Parameter source is null, can't query a null object");
+ return defaultObject;
+ }
+ if (query == null) {
+ AppLog.e(T.UTILS, "Parameter query is null");
+ return defaultObject;
+ }
+ int nextSeperator = query.indexOf(QUERY_SEPERATOR);
+ int nextIndexStart = query.indexOf(QUERY_ARRAY_INDEX_START);
+ if (nextSeperator == -1 && nextIndexStart == -1) {
+ // last item let's get it
+ try {
+ if (!source.has(query)) {
+ return defaultObject;
+ }
+ Object result = source.get(query);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to " + defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Unable to get the Key from the input object. Key:" + query, e);
+ return defaultObject;
+ }
+ }
+ int endQuery;
+ if (nextSeperator == -1 || nextIndexStart == -1) {
+ endQuery = Math.max(nextSeperator, nextIndexStart);
+ } else {
+ endQuery = Math.min(nextSeperator, nextIndexStart);
+ }
+ String nextQuery = query.substring(endQuery);
+ String key = query.substring(0, endQuery);
+ try {
+ if (nextQuery.indexOf(QUERY_SEPERATOR) == 0) {
+ return queryJSON(source.getJSONObject(key), nextQuery.substring(1), defaultObject);
+ } else if (nextQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) {
+ return queryJSON(source.getJSONArray(key), nextQuery, defaultObject);
+ } else if (!nextQuery.equals("")) {
+ return defaultObject;
+ }
+ Object result = source.get(key);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to " + defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ return defaultObject;
+ }
+ }
+
+ /**
+ * Given a JSONArray and a query (e.g. [0].property) it will traverse the array and
+ * pull out the requested property.
+ *
+ * Acceptable indexes include negative numbers to reference items from the end of
+ * the list as well as "last" and "first" as more explicit references to "0" and "-1"
+ */
+ public static <U> U queryJSON(JSONArray source, String query, U defaultObject) {
+ if (source == null) {
+ AppLog.e(T.UTILS, "Parameter source is null, can't query a null object");
+ return defaultObject;
+ }
+ if (query == null) {
+ AppLog.e(T.UTILS, "Parameter query is null");
+ return defaultObject;
+ }
+ // query must start with [ have an index and then have ]
+ int indexStart = query.indexOf(QUERY_ARRAY_INDEX_START);
+ int indexEnd = query.indexOf(QUERY_ARRAY_INDEX_END);
+ if (indexStart == -1 || indexEnd == -1 || indexStart > indexEnd) {
+ return defaultObject;
+ }
+ // get "index" from "[index]"
+ String indexStr = query.substring(indexStart + 1, indexEnd);
+ int index;
+ if (indexStr.equals(QUERY_ARRAY_FIRST)) {
+ index = 0;
+ } else if (indexStr.equals(QUERY_ARRAY_LAST)) {
+ index = -1;
+ } else {
+ index = Integer.parseInt(indexStr);
+ }
+ if (index < 0) {
+ index = source.length() + index;
+ }
+ // copy remaining query
+ String remainingQuery = query.substring(indexEnd + 1);
+ try {
+ if (remainingQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) {
+ return queryJSON(source.getJSONArray(index), remainingQuery, defaultObject);
+ } else if (remainingQuery.indexOf(QUERY_SEPERATOR) == 0) {
+ return queryJSON(source.getJSONObject(index), remainingQuery.substring(1), defaultObject);
+ } else if (!remainingQuery.equals("")) {
+ // TODO throw an exception since the query isn't valid?
+ AppLog.w(T.UTILS, String.format("Incorrect query for next object %s", remainingQuery));
+ return defaultObject;
+ }
+ Object result = source.get(index);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to "+defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ return defaultObject;
+ }
+ }
+
+ /**
+ * Convert a JSONArray (expected to contain strings) in a string list
+ */
+ public static ArrayList<String> fromJSONArrayToStringList(JSONArray jsonArray) {
+ ArrayList<String> stringList = new ArrayList<String>();
+ for (int i = 0; i < jsonArray.length(); i++) {
+ try {
+ stringList.add(jsonArray.getString(i));
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+ return stringList;
+ }
+
+ /**
+ * Convert a string list in a JSONArray
+ */
+ public static JSONArray fromStringListToJSONArray(ArrayList<String> stringList) {
+ JSONArray jsonArray = new JSONArray();
+ if (stringList != null) {
+ for (int i = 0; i < stringList.size(); i++) {
+ jsonArray.put(stringList.get(i));
+ }
+ }
+ return jsonArray;
+ }
+
+ /*
+ * wrapper for JSONObject.optString() which handles "null" values
+ */
+ public static String getString(JSONObject json, String name) {
+ String value = json.optString(name);
+ // return empty string for "null"
+ if (JSON_NULL_STR.equals(value))
+ return "";
+ return value;
+ }
+
+ /*
+ * use with strings that contain HTML entities
+ */
+ public static String getStringDecoded(JSONObject json, String name) {
+ String value = getString(json, name);
+ return HtmlUtils.fastUnescapeHtml(value);
+ }
+
+ /*
+ * replacement for JSONObject.optBoolean() - optBoolean() only checks for "true" and "false",
+ * but our API sometimes uses "0" to denote false
+ */
+ public static boolean getBool(JSONObject json, String name) {
+ String value = getString(json, name);
+ if (TextUtils.isEmpty(value))
+ return false;
+ if (value.equals("0"))
+ return false;
+ if (value.equalsIgnoreCase("false"))
+ return false;
+ if (value.equalsIgnoreCase("no"))
+ return false;
+ return true;
+ }
+
+ /*
+ * returns the JSONObject child of the passed parent that matches the passed query
+ * this is basically an "optJSONObject" that supports nested queries, for example:
+ *
+ * getJSONChild("meta/data/site")
+ *
+ * would find this:
+ *
+ * "meta": {
+ * "data": {
+ * "site": {
+ * "ID": 3584907,
+ * "name": "WordPress.com News",
+ * }
+ * }
+ * }
+ */
+ public static JSONObject getJSONChild(final JSONObject jsonParent, final String query) {
+ if (jsonParent == null || TextUtils.isEmpty(query))
+ return null;
+ String[] names = query.split("/");
+ JSONObject jsonChild = null;
+ for (int i = 0; i < names.length; i++) {
+ if (jsonChild == null) {
+ jsonChild = jsonParent.optJSONObject(names[i]);
+ } else {
+ jsonChild = jsonChild.optJSONObject(names[i]);
+ }
+ if (jsonChild == null)
+ return null;
+ }
+ return jsonChild;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java
new file mode 100644
index 000000000..515b044b3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+import java.util.Locale;
+
+/**
+ * Methods for dealing with i18n messages
+ */
+public class LanguageUtils {
+
+ public static Locale getCurrentDeviceLanguage(Context context) {
+ //better use getConfiguration as it has the latest locale configuration change.
+ //Otherwise Locale.getDefault().getLanguage() gets
+ //the config upon application launch.
+ Locale deviceLocale = context != null ? context.getResources().getConfiguration().locale : Locale.getDefault();
+ return deviceLocale;
+ }
+
+ public static String getCurrentDeviceLanguageCode(Context context) {
+ String deviceLanguageCode = getCurrentDeviceLanguage(context).toString();
+ return deviceLanguageCode;
+ }
+
+ public static String getPatchedCurrentDeviceLanguage(Context context) {
+ return patchDeviceLanguageCode(getCurrentDeviceLanguageCode(context));
+ }
+
+ /**
+ * Patches a deviceLanguageCode if any of deprecated values iw, id, or yi
+ */
+ public static String patchDeviceLanguageCode(String deviceLanguageCode){
+ String patchedCode = deviceLanguageCode;
+ /*
+ <p>Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language
+ * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This
+ * rewriting happens even if you construct your own {@code Locale} object, not just for
+ * instances returned by the various lookup methods.
+ */
+ if (deviceLanguageCode != null) {
+ if (deviceLanguageCode.startsWith("iw"))
+ patchedCode = deviceLanguageCode.replace("iw", "he");
+ else if (deviceLanguageCode.startsWith("in"))
+ patchedCode = deviceLanguageCode.replace("in", "id");
+ else if (deviceLanguageCode.startsWith("ji"))
+ patchedCode = deviceLanguageCode.replace("ji", "yi");
+ }
+
+ return patchedCode;
+ }
+
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
new file mode 100644
index 000000000..c6e72dc5b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.util;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * wrappers for extracting values from a Map object
+ */
+public class MapUtils {
+ /*
+ * returns a String value for the passed key in the passed map
+ * always returns "" instead of null
+ */
+ public static String getMapStr(final Map<?, ?> map, final String key) {
+ if (map == null || key == null || !map.containsKey(key) || map.get(key) == null) {
+ return "";
+ }
+ return map.get(key).toString();
+ }
+
+ /*
+ * returns an int value for the passed key in the passed map
+ * defaultValue is returned if key doesn't exist or isn't a number
+ */
+ public static int getMapInt(final Map<?, ?> map, final String key) {
+ return getMapInt(map, key, 0);
+ }
+ public static int getMapInt(final Map<?, ?> map, final String key, int defaultValue) {
+ try {
+ return Integer.parseInt(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * long version of above
+ */
+ public static long getMapLong(final Map<?, ?> map, final String key) {
+ return getMapLong(map, key, 0);
+ }
+ public static long getMapLong(final Map<?, ?> map, final String key, long defaultValue) {
+ try {
+ return Long.parseLong(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * float version of above
+ */
+ public static float getMapFloat(final Map<?, ?> map, final String key) {
+ return getMapFloat(map, key, 0);
+ }
+ public static float getMapFloat(final Map<?, ?> map, final String key, float defaultValue) {
+ try {
+ return Float.parseFloat(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * double version of above
+ */
+ public static double getMapDouble(final Map<?, ?> map, final String key) {
+ return getMapDouble(map, key, 0);
+ }
+ public static double getMapDouble(final Map<?, ?> map, final String key, double defaultValue) {
+ try {
+ return Double.parseDouble(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * returns a date object from the passed key in the passed map
+ * returns null if key doesn't exist or isn't a date
+ */
+ public static Date getMapDate(final Map<?, ?> map, final String key) {
+ if (map == null || key == null || !map.containsKey(key))
+ return null;
+ try {
+ return (Date) map.get(key);
+ } catch (ClassCastException e) {
+ return null;
+ }
+ }
+
+ /*
+ * returns a boolean value from the passed key in the passed map
+ * returns true unless key doesn't exist, or the value is "0" or "false"
+ */
+ public static boolean getMapBool(final Map<?, ?> map, final String key) {
+ String value = getMapStr(map, key);
+ if (value.isEmpty())
+ return false;
+ if (value.startsWith("0")) // handles "0" and "0.0"
+ return false;
+ if (value.equalsIgnoreCase("false"))
+ return false;
+ // all other values are assume to be true
+ return true;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java
new file mode 100644
index 000000000..a96dadc74
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java
@@ -0,0 +1,334 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class MediaUtils {
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ public static boolean isValidImage(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".gif");
+ }
+
+ public static boolean isDocument(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".odt") || url.endsWith(".pdf");
+ }
+
+ public static boolean isPowerpoint(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".ppt") || url.endsWith(".pptx") || url.endsWith(".pps") || url.endsWith(".ppsx") ||
+ url.endsWith(".key");
+ }
+
+ public static boolean isSpreadsheet(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".xls") || url.endsWith(".xlsx");
+ }
+
+ public static boolean isVideo(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".ogv") || url.endsWith(".mp4") || url.endsWith(".m4v") || url.endsWith(".mov") ||
+ url.endsWith(".wmv") || url.endsWith(".avi") || url.endsWith(".mpg") || url.endsWith(".3gp") ||
+ url.endsWith(".3g2") || url.contains("video");
+ }
+
+ public static boolean isAudio(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".wav") || url.endsWith(".wma") ||
+ url.endsWith(".aiff") || url.endsWith(".aif") || url.endsWith(".aac") || url.endsWith(".m4a");
+ }
+
+ /**
+ * E.g. Jul 2, 2013 @ 21:57
+ */
+ public static String getDate(long ms) {
+ Date date = new Date(ms);
+ SimpleDateFormat sdf = new SimpleDateFormat("MMM d, yyyy '@' HH:mm", Locale.ENGLISH);
+
+ // The timezone on the website is at GMT
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+ return sdf.format(date);
+ }
+
+ public static boolean isLocalFile(String state) {
+ if (state == null) {
+ return false;
+ }
+
+ return (state.equals("queued") || state.equals("uploading") || state.equals("retry")
+ || state.equals("failed"));
+ }
+
+ public static Uri getLastRecordedVideoUri(Activity activity) {
+ String[] proj = { MediaStore.Video.Media._ID };
+ Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ String sortOrder = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC";
+ CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, sortOrder);
+ Cursor cursor = loader.loadInBackground();
+ cursor.moveToFirst();
+
+ return Uri.parse(contentUri.toString() + "/" + cursor.getLong(0));
+ }
+
+ /**
+ * Get image width setting from the image width site setting string. This string can be an int, in this case it's
+ * the maximum image width defined by the site.
+ * Examples:
+ * "1000" will return 1000
+ * "Original Size" will return Integer.MAX_VALUE
+ * "Largeur originale" will return Integer.MAX_VALUE
+ * null will return Integer.MAX_VALUE
+ * @param imageWidthSiteSettingString Image width site setting string
+ * @return Integer.MAX_VALUE if image width is not defined or invalid, maximum image width in other cases.
+ */
+ public static int getImageWidthSettingFromString(String imageWidthSiteSettingString) {
+ if (imageWidthSiteSettingString == null) {
+ return Integer.MAX_VALUE;
+ }
+ try {
+ return Integer.valueOf(imageWidthSiteSettingString);
+ } catch (NumberFormatException e) {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Calculate and return the maximum allowed image width by comparing the width of the image at its full size with
+ * the maximum upload width set in the blog settings
+ * @param imageWidth the image's natural (full) width
+ * @param imageWidthSiteSettingString the maximum upload width set in the site settings
+ * @return maximum allowed image width
+ */
+ public static int getMaximumImageWidth(int imageWidth, String imageWidthSiteSettingString) {
+ int imageWidthBlogSetting = getImageWidthSettingFromString(imageWidthSiteSettingString);
+ int imageWidthPictureSetting = imageWidth == 0 ? Integer.MAX_VALUE : imageWidth;
+
+ if (Math.min(imageWidthPictureSetting, imageWidthBlogSetting) == Integer.MAX_VALUE) {
+ // Default value in case of errors reading the picture size or the blog settings is set to Original size
+ return DEFAULT_MAX_IMAGE_WIDTH;
+ } else {
+ return Math.min(imageWidthPictureSetting, imageWidthBlogSetting);
+ }
+ }
+
+ public static int getMaximumImageWidth(Context context, Uri curStream, String imageWidthBlogSettingString) {
+ int[] dimensions = ImageUtils.getImageSize(curStream, context);
+ return getMaximumImageWidth(dimensions[0], imageWidthBlogSettingString);
+ }
+
+ public static boolean isInMediaStore(Uri mediaUri) {
+ // Check if the image is externally hosted (Picasa/Google Photos for example)
+ if (mediaUri != null && mediaUri.toString().startsWith("content://media/")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static Uri downloadExternalMedia(Context context, Uri imageUri) {
+ if (context == null || imageUri == null) {
+ return null;
+ }
+ File cacheDir = null;
+
+ String mimeType = context.getContentResolver().getType(imageUri);
+ boolean isVideo = (mimeType != null && mimeType.contains("video"));
+
+ // If the device has an SD card
+ if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
+ String mediaFolder = isVideo ? "video" : "images";
+ cacheDir = new File(android.os.Environment.getExternalStorageDirectory() + "/WordPress/" + mediaFolder);
+ } else {
+ if (context.getApplicationContext() != null) {
+ cacheDir = context.getApplicationContext().getCacheDir();
+ }
+ }
+
+ if (cacheDir != null && !cacheDir.exists()) {
+ cacheDir.mkdirs();
+ }
+ try {
+ InputStream input;
+ // Download the file
+ if (imageUri.toString().startsWith("content://")) {
+ input = context.getContentResolver().openInputStream(imageUri);
+ if (input == null) {
+ AppLog.e(T.UTILS, "openInputStream returned null");
+ return null;
+ }
+ } else {
+ input = new URL(imageUri.toString()).openStream();
+ }
+
+ String fileName = "wp-" + System.currentTimeMillis();
+ if (isVideo) {
+ fileName += "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ }
+
+ File f = new File(cacheDir, fileName);
+
+ OutputStream output = new FileOutputStream(f);
+
+ byte data[] = new byte[1024];
+ int count;
+ while ((count = input.read(data)) != -1) {
+ output.write(data, 0, count);
+ }
+
+ output.flush();
+ output.close();
+ input.close();
+
+ return Uri.fromFile(f);
+ } catch (FileNotFoundException e) {
+ AppLog.e(T.UTILS, e);
+ } catch (MalformedURLException e) {
+ AppLog.e(T.UTILS, e);
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e);
+ }
+
+ return null;
+ }
+
+ public static String getMimeTypeOfInputStream(InputStream stream) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(stream, null, options);
+ return options.outMimeType;
+ }
+
+ public static String getMediaFileMimeType(File mediaFile) {
+ String originalFileName = mediaFile.getName().toLowerCase();
+ String mimeType = UrlUtils.getUrlMimeType(originalFileName);
+
+ if (TextUtils.isEmpty(mimeType)) {
+ try {
+ String filePathForGuessingMime;
+ if (mediaFile.getPath().contains("://")) {
+ filePathForGuessingMime = Uri.encode(mediaFile.getPath(), ":/");
+ } else {
+ filePathForGuessingMime = "file://"+ Uri.encode(mediaFile.getPath(), "/");
+ }
+ URL urlForGuessingMime = new URL(filePathForGuessingMime);
+ URLConnection uc = urlForGuessingMime.openConnection();
+ String guessedContentType = uc.getContentType(); //internally calls guessContentTypeFromName(url.getFile()); and guessContentTypeFromStream(is);
+ // check if returned "content/unknown"
+ if (!TextUtils.isEmpty(guessedContentType) && !guessedContentType.equals("content/unknown")) {
+ mimeType = guessedContentType;
+ }
+ } catch (MalformedURLException e) {
+ AppLog.e(AppLog.T.API, "MalformedURLException while trying to guess the content type for the file here " + mediaFile.getPath() + " with URLConnection", e);
+ }
+ catch (IOException e) {
+ AppLog.e(AppLog.T.API, "Error while trying to guess the content type for the file here " + mediaFile.getPath() +" with URLConnection", e);
+ }
+ }
+
+ // No mimeType yet? Try to decode the image and get the mimeType from there
+ if (TextUtils.isEmpty(mimeType)) {
+ try {
+ DataInputStream inputStream = new DataInputStream(new FileInputStream(mediaFile));
+ String mimeTypeFromStream = getMimeTypeOfInputStream(inputStream);
+ if (!TextUtils.isEmpty(mimeTypeFromStream)) {
+ mimeType = mimeTypeFromStream;
+ }
+ inputStream.close();
+ } catch (FileNotFoundException e) {
+ AppLog.e(AppLog.T.API, "FileNotFoundException while trying to guess the content type for the file " + mediaFile.getPath(), e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.API, "IOException while trying to guess the content type for the file " + mediaFile.getPath(), e);
+ }
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = "";
+ } else {
+ if (mimeType.equalsIgnoreCase("video/mp4v-es")) { //Fixes #533. See: http://tools.ietf.org/html/rfc3016
+ mimeType = "video/mp4";
+ }
+ }
+
+ return mimeType;
+ }
+
+ public static String getMediaFileName(File mediaFile, String mimeType) {
+ String originalFileName = mediaFile.getName().toLowerCase();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(originalFileName);
+ if (!TextUtils.isEmpty(extension)) //File name already has the extension in it
+ return originalFileName;
+
+ if (!TextUtils.isEmpty(mimeType)) { //try to get the extension from mimeType
+ String fileExtension = getExtensionForMimeType(mimeType);
+ if (!TextUtils.isEmpty(fileExtension)) {
+ originalFileName += "." + fileExtension;
+ }
+ } else {
+ //No mimetype and no extension!!
+ AppLog.e(AppLog.T.API, "No mimetype and no extension for " + mediaFile.getPath());
+ }
+
+ return originalFileName;
+ }
+
+ public static String getExtensionForMimeType(String mimeType) {
+ if (TextUtils.isEmpty(mimeType))
+ return "";
+
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String fileExtensionFromMimeType = mimeTypeMap.getExtensionFromMimeType(mimeType);
+ if (TextUtils.isEmpty(fileExtensionFromMimeType)) {
+ // We're still without an extension - split the mime type and retrieve it
+ String[] split = mimeType.split("/");
+ fileExtensionFromMimeType = split.length > 1 ? split[1] : split[0];
+ }
+
+ return fileExtensionFromMimeType.toLowerCase();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java
new file mode 100644
index 000000000..240c93f50
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java
@@ -0,0 +1,89 @@
+package org.wordpress.android.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+
+/**
+ * requires android.permission.ACCESS_NETWORK_STATE
+ */
+
+public class NetworkUtils {
+ public static final int TYPE_UNKNOWN = -1;
+
+ /**
+ * returns information on the active network connection
+ */
+ private static NetworkInfo getActiveNetworkInfo(Context context) {
+ if (context == null) {
+ return null;
+ }
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm == null) {
+ return null;
+ }
+ // note that this may return null if no network is currently active
+ return cm.getActiveNetworkInfo();
+ }
+
+ /**
+ * returns the ConnectivityManager.TYPE_xxx if there's an active connection, otherwise
+ * returns TYPE_UNKNOWN
+ */
+ private static int getActiveNetworkType(Context context) {
+ NetworkInfo info = getActiveNetworkInfo(context);
+ if (info == null || !info.isConnected()) {
+ return TYPE_UNKNOWN;
+ }
+ return info.getType();
+ }
+
+ /**
+ * returns true if a network connection is available
+ */
+ public static boolean isNetworkAvailable(Context context) {
+ NetworkInfo info = getActiveNetworkInfo(context);
+ return (info != null && info.isConnected());
+ }
+
+ /**
+ * returns true if the user is connected to WiFi
+ */
+ public static boolean isWiFiConnected(Context context) {
+ return (getActiveNetworkType(context) == ConnectivityManager.TYPE_WIFI);
+ }
+
+ /**
+ * returns true if airplane mode has been enabled
+ */
+ @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
+ @SuppressWarnings("deprecation")
+ public static boolean isAirplaneModeOn(Context context) {
+ // prior to JellyBean 4.2 this was Settings.System.AIRPLANE_MODE_ON, JellyBean 4.2
+ // moved it to Settings.Global
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return Settings.System.getInt(context.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) != 0;
+ } else {
+ return Settings.Global.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+ }
+ }
+
+ /**
+ * returns true if there's an active network connection, otherwise displays a toast error
+ * and returns false
+ */
+ public static boolean checkConnection(Context context) {
+ if (context == null) {
+ return false;
+ }
+ if (isNetworkAvailable(context)) {
+ return true;
+ }
+ ToastUtils.showToast(context, R.string.no_network_message);
+ return false;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java
new file mode 100644
index 000000000..52900a0bf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class PackageUtils {
+ /**
+ * Return true if Debug build. false otherwise.
+ */
+ public static boolean isDebugBuild() {
+ return BuildConfig.DEBUG;
+ }
+
+ public static PackageInfo getPackageInfo(Context context) {
+ try {
+ PackageManager manager = context.getPackageManager();
+ return manager.getPackageInfo(context.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Return version code, or 0 if it can't be read
+ */
+ public static int getVersionCode(Context context) {
+ PackageInfo packageInfo = getPackageInfo(context);
+ if (packageInfo != null) {
+ return packageInfo.versionCode;
+ }
+ return 0;
+ }
+
+ /**
+ * Return version name, or the string "0" if it can't be read
+ */
+ public static String getVersionName(Context context) {
+ PackageInfo packageInfo = getPackageInfo(context);
+ if (packageInfo != null) {
+ return packageInfo.versionName;
+ }
+ return "0";
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java
new file mode 100644
index 000000000..bf30103d2
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java
@@ -0,0 +1,97 @@
+package org.wordpress.android.util;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionUtils {
+ /**
+ * Check for permissions, request them if they're not granted.
+ *
+ * @return true if permissions are already granted, else request them and return false.
+ */
+ private static boolean checkAndRequestPermissions(Activity activity, int requestCode, String[] permissionList) {
+ List<String> toRequest = new ArrayList<>();
+ for (String permission : permissionList) {
+ if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
+ toRequest.add(permission);
+ }
+ }
+ if (toRequest.size() > 0) {
+ String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]);
+ ActivityCompat.requestPermissions(activity, requestedPermissions, requestCode);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check for permissions, request them if they're not granted.
+ *
+ * @return true if permissions are already granted, else request them and return false.
+ */
+ private static boolean checkAndRequestPermissions(Fragment fragment, int requestCode, String[] permissionList) {
+ List<String> toRequest = new ArrayList<>();
+ for (String permission : permissionList) {
+ Context context = fragment.getActivity();
+ if (context != null && ContextCompat.checkSelfPermission(context, permission) != PackageManager
+ .PERMISSION_GRANTED) {
+ toRequest.add(permission);
+ }
+ }
+ if (toRequest.size() > 0) {
+ String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]);
+ FragmentCompat.requestPermissions(fragment, requestedPermissions, requestCode);
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean checkAndRequestCameraAndStoragePermissions(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE,
+ permission.CAMERA
+ });
+ }
+
+ public static boolean checkAndRequestCameraAndStoragePermissions(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE,
+ permission.CAMERA
+ });
+ }
+
+ public static boolean checkAndRequestStoragePermission(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE
+ });
+ }
+
+ public static boolean checkAndRequestStoragePermission(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE
+ });
+ }
+
+ public static boolean checkLocationPermissions(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.ACCESS_FINE_LOCATION,
+ permission.ACCESS_COARSE_LOCATION
+ });
+ }
+
+ public static boolean checkLocationPermissions(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.ACCESS_FINE_LOCATION,
+ permission.ACCESS_COARSE_LOCATION
+ });
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
new file mode 100644
index 000000000..85a2adc93
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
@@ -0,0 +1,104 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+/**
+ * routines related to the Photon API
+ * http://developer.wordpress.com/docs/photon/
+ */
+public class PhotonUtils {
+
+ private PhotonUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * returns true if the passed url is an obvious "mshots" url
+ */
+ public static boolean isMshotsUrl(final String imageUrl) {
+ return (imageUrl != null && imageUrl.contains("/mshots/"));
+ }
+
+ /*
+ * returns a photon url for the passed image with the resize query set to the passed
+ * dimensions - note that the passed quality parameter will only affect JPEGs
+ */
+ public enum Quality {
+ HIGH,
+ MEDIUM,
+ LOW
+ }
+ public static String getPhotonImageUrl(String imageUrl, int width, int height) {
+ return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM);
+ }
+ public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // make sure it's valid
+ int schemePos = imageUrl.indexOf("://");
+ if (schemePos == -1) {
+ return imageUrl;
+ }
+
+ // we have encountered some image urls that incorrectly have a # fragment part, which
+ // must be removed before removing the query string
+ int fragmentPos = imageUrl.indexOf("#");
+ if (fragmentPos > 0) {
+ imageUrl = imageUrl.substring(0, fragmentPos);
+ }
+
+ // remove existing query string since it may contain params that conflict with the passed ones
+ imageUrl = UrlUtils.removeQuery(imageUrl);
+
+ // if this is an "mshots" url, skip photon and return it with a query that sets the width/height
+ if (isMshotsUrl(imageUrl)) {
+ return imageUrl + "?w=" + width + "&h=" + height;
+ }
+
+ // strip=all removes EXIF and other non-visual data from JPEGs
+ String query = "?strip=all";
+
+ switch (quality) {
+ case HIGH:
+ query += "&quality=100";
+ break;
+ case LOW:
+ query += "&quality=35";
+ break;
+ default: // medium
+ query += "&quality=65";
+ break;
+ }
+
+ // if both width & height are passed use the "resize" param, use only "w" or "h" if just
+ // one of them is set
+ if (width > 0 && height > 0) {
+ query += "&resize=" + width + "," + height;
+ } else if (width > 0) {
+ query += "&w=" + width;
+ } else if (height > 0) {
+ query += "&h=" + height;
+ }
+
+ // return passed url+query if it's already a photon url
+ if (imageUrl.contains(".wp.com")) {
+ if (imageUrl.contains("i0.wp.com") || imageUrl.contains("i1.wp.com") || imageUrl.contains("i2.wp.com"))
+ return imageUrl + query;
+ }
+
+ // use wordpress.com as the host if image is on wordpress.com since it supports the same
+ // query params and, more importantly, can handle images in private blogs
+ if (imageUrl.contains("wordpress.com")) {
+ return imageUrl + query;
+ }
+
+ // must use https for https image urls
+ if (UrlUtils.isHttps(imageUrl)) {
+ return "https://i0.wp.com/" + imageUrl.substring(schemePos+3, imageUrl.length()) + query;
+ } else {
+ return "http://i0.wp.com/" + imageUrl.substring(schemePos+3, imageUrl.length()) + query;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
new file mode 100644
index 000000000..4660a3500
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.util;
+
+import android.os.SystemClock;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+
+/**
+ * forked from android.util.TimingLogger to use AppLog instead of Log + new static interface.
+ */
+public class ProfilingUtils {
+ private static ProfilingUtils sInstance;
+
+ private String mLabel;
+ private ArrayList<Long> mSplits;
+ private ArrayList<String> mSplitLabels;
+
+ public static void start(String label) {
+ getInstance().reset(label);
+ }
+
+ public static void split(String splitLabel) {
+ getInstance().addSplit(splitLabel);
+ }
+
+ public static void dump() {
+ getInstance().dumpToLog();
+ }
+
+ public static void stop() {
+ getInstance().reset(null);
+ }
+
+ private static ProfilingUtils getInstance() {
+ if (sInstance == null) {
+ sInstance = new ProfilingUtils();
+ }
+ return sInstance;
+ }
+
+ public ProfilingUtils() {
+ reset("init");
+ }
+
+ public void reset(String label) {
+ mLabel = label;
+ reset();
+ }
+
+ public void reset() {
+ if (mSplits == null) {
+ mSplits = new ArrayList<Long>();
+ mSplitLabels = new ArrayList<String>();
+ } else {
+ mSplits.clear();
+ mSplitLabels.clear();
+ }
+ addSplit(null);
+ }
+
+ public void addSplit(String splitLabel) {
+ if (mLabel == null) {
+ return;
+ }
+ long now = SystemClock.elapsedRealtime();
+ mSplits.add(now);
+ mSplitLabels.add(splitLabel);
+ }
+
+ public void dumpToLog() {
+ if (mLabel == null) {
+ return;
+ }
+ AppLog.d(T.PROFILING, mLabel + ": begin");
+ final long first = mSplits.get(0);
+ long now = first;
+ for (int i = 1; i < mSplits.size(); i++) {
+ now = mSplits.get(i);
+ final String splitLabel = mSplitLabels.get(i);
+ final long prev = mSplits.get(i - 1);
+ AppLog.d(T.PROFILING, mLabel + ": " + (now - prev) + " ms, " + splitLabel);
+ }
+ AppLog.d(T.PROFILING, mLabel + ": end, " + (now - first) + " ms");
+ }
+}
+
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java
new file mode 100644
index 000000000..6bcfde06b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java
@@ -0,0 +1,16 @@
+package org.wordpress.android.util;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+public class ServiceUtils {
+ public static boolean isServiceRunning(Context context, Class<?> serviceClass) {
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
+ if (serviceClass.getName().equals(service.service.getClassName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java
new file mode 100644
index 000000000..09480f156
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ShortcodeUtils {
+ public static String getVideoPressShortcodeFromId(String videoPressId) {
+ if (videoPressId == null || videoPressId.isEmpty()) {
+ return "";
+ }
+
+ return "[wpvideo " + videoPressId + "]";
+ }
+
+ public static String getVideoPressIdFromShortCode(String shortcode) {
+ String videoPressId = "";
+
+ if (shortcode != null) {
+ String videoPressShortcodeRegex = "^\\[wpvideo (.*)]$";
+
+ Pattern pattern = Pattern.compile(videoPressShortcodeRegex);
+ Matcher matcher = pattern.matcher(shortcode);
+
+ if (matcher.find()) {
+ videoPressId = matcher.group(1);
+ }
+ }
+
+ return videoPressId;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
new file mode 100644
index 000000000..38b4b74a9
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
@@ -0,0 +1,142 @@
+package org.wordpress.android.util;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDoneException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SqlUtils {
+ private SqlUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * SQLite doesn't have a boolean datatype, so booleans are stored as 0=false, 1=true
+ */
+ public static long boolToSql(boolean value) {
+ return (value ? 1 : 0);
+ }
+ public static boolean sqlToBool(int value) {
+ return (value != 0);
+ }
+
+ public static void closeStatement(SQLiteStatement stmt) {
+ if (stmt != null) {
+ stmt.close();
+ }
+ }
+
+ public static void closeCursor(Cursor c) {
+ if (c != null && !c.isClosed()) {
+ c.close();
+ }
+ }
+
+ /*
+ * wrapper for DatabaseUtils.longForQuery() which returns 0 if query returns no rows
+ */
+ public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ try {
+ return DatabaseUtils.longForQuery(db, query, selectionArgs);
+ } catch (SQLiteDoneException e) {
+ return 0;
+ }
+ }
+
+ public static int intForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ long value = longForQuery(db, query, selectionArgs);
+ return (int)value;
+ }
+
+ public static boolean boolForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ long value = longForQuery(db, query, selectionArgs);
+ return sqlToBool((int) value);
+ }
+
+ /*
+ * wrapper for DatabaseUtils.stringForQuery(), returns "" if query returns no rows
+ */
+ public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ try {
+ return DatabaseUtils.stringForQuery(db, query, selectionArgs);
+ } catch (SQLiteDoneException e) {
+ return "";
+ }
+ }
+
+ /*
+ * returns the number of rows in the passed table
+ */
+ public static long getRowCount(SQLiteDatabase db, String tableName) {
+ return DatabaseUtils.queryNumEntries(db, tableName);
+ }
+
+ /*
+ * removes all rows from the passed table
+ */
+ public static void deleteAllRowsInTable(SQLiteDatabase db, String tableName) {
+ db.delete(tableName, null, null);
+ }
+
+ /*
+ * drop all tables from the passed SQLiteDatabase - make sure to pass a
+ * writable database
+ */
+ public static boolean dropAllTables(SQLiteDatabase db) throws SQLiteException {
+ if (db == null) {
+ return false;
+ }
+
+ if (db.isReadOnly()) {
+ throw new SQLiteException("can't drop tables from a read-only database");
+ }
+
+ List<String> tableNames = new ArrayList<String>();
+ Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null);
+ if (cursor.moveToFirst()) {
+ do {
+ String tableName = cursor.getString(0);
+ if (!tableName.equals("android_metadata") && !tableName.equals("sqlite_sequence")) {
+ tableNames.add(tableName);
+ }
+ } while (cursor.moveToNext());
+ }
+
+ db.beginTransaction();
+ try {
+ for (String tableName: tableNames) {
+ db.execSQL("DROP TABLE IF EXISTS " + tableName);
+ }
+ db.setTransactionSuccessful();
+ return true;
+ } finally {
+ db.endTransaction();
+ closeCursor(cursor);
+ }
+ }
+
+ /*
+ * Android's CursorWindow has a max size of 2MB per row which can be exceeded
+ * with a very large text column, causing an IllegalStateException when the
+ * row is read - prevent this by limiting the amount of text that's stored in
+ * the text column.
+ * https://github.com/android/platform_frameworks_base/blob/b77bc869241644a662f7e615b0b00ecb5aee373d/core/res/res/values/config.xml#L1268
+ * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java/android/database/CursorWindow.java#L103
+ */
+ // Max 512K characters (a UTF-8 char is 4 bytes max, so a 512K characters string is always < 2Mb)
+ private static final int MAX_TEXT_LEN = 1024 * 1024 / 2;
+ public static String maxSQLiteText(final String text) {
+ if (text.length() <= MAX_TEXT_LEN) {
+ return text;
+ }
+ AppLog.w(T.UTILS, "sqlite > max text exceeded, storing truncated text");
+ return text.substring(0, MAX_TEXT_LEN);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
new file mode 100644
index 000000000..b15e8d824
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
@@ -0,0 +1,327 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.support.annotation.StringRes;
+import android.text.Html;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class StringUtils {
+ public static String[] mergeStringArrays(String array1[], String array2[]) {
+ if (array1 == null || array1.length == 0) {
+ return array2;
+ }
+ if (array2 == null || array2.length == 0) {
+ return array1;
+ }
+ List<String> array1List = Arrays.asList(array1);
+ List<String> array2List = Arrays.asList(array2);
+ List<String> result = new ArrayList<String>(array1List);
+ List<String> tmp = new ArrayList<String>(array1List);
+ tmp.retainAll(array2List);
+ result.addAll(array2List);
+ return ((String[]) result.toArray(new String[result.size()]));
+ }
+
+ public static String convertHTMLTagsForUpload(String source) {
+ // bold
+ source = source.replace("<b>", "<strong>");
+ source = source.replace("</b>", "</strong>");
+
+ // italics
+ source = source.replace("<i>", "<em>");
+ source = source.replace("</i>", "</em>");
+
+ return source;
+ }
+
+ public static String convertHTMLTagsForDisplay(String source) {
+ // bold
+ source = source.replace("<strong>", "<b>");
+ source = source.replace("</strong>", "</b>");
+
+ // italics
+ source = source.replace("<em>", "<i>");
+ source = source.replace("</em>", "</i>");
+
+ return source;
+ }
+
+ public static String addPTags(String source) {
+ String[] asploded = source.split("\n\n");
+
+ if (asploded.length > 0) {
+ StringBuilder wrappedHTML = new StringBuilder();
+ for (int i = 0; i < asploded.length; i++) {
+ String trimmed = asploded[i].trim();
+ if (trimmed.length() > 0) {
+ trimmed = trimmed.replace("<br />", "<br>").replace("<br/>", "<br>").replace("<br>\n", "<br>")
+ .replace("\n", "<br>");
+ wrappedHTML.append("<p>");
+ wrappedHTML.append(trimmed);
+ wrappedHTML.append("</p>");
+ }
+ }
+ return wrappedHTML.toString();
+ } else {
+ return source;
+ }
+ }
+
+ public static BigInteger getMd5IntHash(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] messageDigest = md.digest(input.getBytes());
+ BigInteger number = new BigInteger(1, messageDigest);
+ return number;
+ } catch (NoSuchAlgorithmException e) {
+ AppLog.e(T.UTILS, e);
+ return null;
+ }
+ }
+
+ public static String getMd5Hash(String input) {
+ BigInteger number = getMd5IntHash(input);
+ String md5 = number.toString(16);
+ while (md5.length() < 32) {
+ md5 = "0" + md5;
+ }
+ return md5;
+ }
+
+ public static String unescapeHTML(String html) {
+ if (html != null) {
+ return Html.fromHtml(html).toString();
+ } else {
+ return "";
+ }
+ }
+
+ /*
+ * nbradbury - adapted from Html.escapeHtml(), which was added in API Level 16
+ * TODO: not thoroughly tested yet, so marked as private - not sure I like the way
+ * this replaces two spaces with "&nbsp;"
+ */
+ private static String escapeHtml(final String text) {
+ if (text == null) {
+ return "";
+ }
+
+ StringBuilder out = new StringBuilder();
+ int length = text.length();
+
+ for (int i = 0; i < length; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("&lt;");
+ } else if (c == '>') {
+ out.append("&gt;");
+ } else if (c == '&') {
+ out.append("&amp;");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("&#").append((int) c).append(";");
+ } else if (c == ' ') {
+ while (i + 1 < length && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+
+ return out.toString();
+ }
+
+ /*
+ * returns empty string if passed string is null, otherwise returns passed string
+ */
+ public static String notNullStr(String s) {
+ if (s == null) {
+ return "";
+ }
+ return s;
+ }
+
+ /**
+ * returns true if two strings are equal or two strings are null
+ */
+ public static boolean equals(String s1, String s2) {
+ if (s1 == null) {
+ return s2 == null;
+ }
+ return s1.equals(s2);
+ }
+
+ /*
+ * capitalizes the first letter in the passed string - based on Apache commons/lang3/StringUtils
+ * http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/StringUtils.java?revision=1497829&view=markup
+ */
+ public static String capitalize(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return str;
+ }
+
+ char firstChar = str.charAt(0);
+ if (Character.isTitleCase(firstChar)) {
+ return str;
+ }
+
+ return new StringBuilder(strLen).append(Character.toTitleCase(firstChar)).append(str.substring(1)).toString();
+ }
+
+ public static String removeTrailingSlash(final String str) {
+ if (TextUtils.isEmpty(str) || !str.endsWith("/")) {
+ return str;
+ }
+
+ return str.substring(0, str.length() - 1);
+ }
+
+ /*
+ * Wrap an image URL in a photon URL
+ * Check out http://developer.wordpress.com/docs/photon/
+ */
+ public static String getPhotonUrl(String imageUrl, int size) {
+ imageUrl = imageUrl.replace("http://", "").replace("https://", "");
+ return "http://i0.wp.com/" + imageUrl + "?w=" + size;
+ }
+
+ public static String replaceUnicodeSurrogateBlocksWithHTMLEntities(final String inputString) {
+ final int length = inputString.length();
+ StringBuilder out = new StringBuilder(); // Used to hold the output.
+ for (int offset = 0; offset < length; ) {
+ final int codepoint = inputString.codePointAt(offset);
+ final char current = inputString.charAt(offset);
+ if (Character.isHighSurrogate(current) || Character.isLowSurrogate(current)) {
+ if (EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint) != null) {
+ out.append(EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint));
+ } else {
+ final String htmlEscapedChar = "&#x" + Integer.toHexString(codepoint) + ";";
+ out.append(htmlEscapedChar);
+ }
+ } else {
+ out.append(current);
+ }
+ offset += Character.charCount(codepoint);
+ }
+ return out.toString();
+ }
+
+ /**
+ * Used to convert a language code ([lc]_[rc] where lc is language code (en, fr, es, etc...)
+ * and rc is region code (zh-CN, zh-HK, zh-TW, etc...) to a displayable string with the languages
+ * name.
+ *
+ * The input string must be between 2 and 6 characters, inclusive. An empty string is returned
+ * if that is not the case.
+ *
+ * If the input string is recognized by {@link Locale} the result of this method is the given
+ *
+ * @return non-null
+ */
+ public static String getLanguageString(String languagueCode, Locale displayLocale) {
+ if (languagueCode == null || languagueCode.length() < 2 || languagueCode.length() > 6) {
+ return "";
+ }
+
+ Locale languageLocale = new Locale(languagueCode.substring(0, 2));
+ return languageLocale.getDisplayLanguage(displayLocale) + languagueCode.substring(2);
+ }
+
+ /**
+ * This method ensures that the output String has only
+ * valid XML unicode characters as specified by the
+ * XML 1.0 standard. For reference, please see
+ * <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
+ * standard</a>. This method will return an empty
+ * String if the input is null or empty.
+ *
+ * @param in The String whose non-valid characters we want to remove.
+ * @return The in String, stripped of non-valid characters.
+ */
+ public static final String stripNonValidXMLCharacters(String in) {
+ StringBuilder out = new StringBuilder(); // Used to hold the output.
+ char current; // Used to reference the current character.
+
+ if (in == null || ("".equals(in))) {
+ return ""; // vacancy test.
+ }
+ for (int i = 0; i < in.length(); i++) {
+ current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
+ if ((current == 0x9) ||
+ (current == 0xA) ||
+ (current == 0xD) ||
+ ((current >= 0x20) && (current <= 0xD7FF)) ||
+ ((current >= 0xE000) && (current <= 0xFFFD)) ||
+ ((current >= 0x10000) && (current <= 0x10FFFF))) {
+ out.append(current);
+ }
+ }
+ return out.toString();
+ }
+
+ /*
+ * simple wrapper for Integer.valueOf(string) so caller doesn't need to catch NumberFormatException
+ */
+ public static int stringToInt(String s) {
+ return stringToInt(s, 0);
+ }
+
+ public static int stringToInt(String s, int defaultValue) {
+ if (s == null)
+ return defaultValue;
+ try {
+ return Integer.valueOf(s);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ public static long stringToLong(String s) {
+ return stringToLong(s, 0L);
+ }
+
+ public static long stringToLong(String s, long defaultValue) {
+ if (s == null)
+ return defaultValue;
+ try {
+ return Long.valueOf(s);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Formats the string for the given quantity, using the given arguments.
+ * We need this because our translation platform doesn't support Android plurals.
+ *
+ * @param zero The desired string identifier to get when quantity is exactly 0
+ * @param one The desired string identifier to get when quantity is exactly 1
+ * @param other The desired string identifier to get when quantity is not (0 or 1)
+ * @param quantity The number used to get the correct string
+ */
+ public static String getQuantityString(Context context, @StringRes int zero, @StringRes int one,
+ @StringRes int other, int quantity) {
+ if (quantity == 0) {
+ return context.getString(zero);
+ }
+ if (quantity == 1) {
+ return context.getString(one);
+ }
+ return String.format(context.getString(other), quantity);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
new file mode 100644
index 000000000..e3fee7fc3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public class SystemServiceFactory {
+ private static SystemServiceFactoryAbstract sFactory;
+
+ public static Object get(Context context, String name) {
+ if (sFactory == null) {
+ sFactory = new SystemServiceFactoryDefault();
+ }
+ return sFactory.get(context, name);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java
new file mode 100644
index 000000000..a9d522db4
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public interface SystemServiceFactoryAbstract {
+ public Object get(Context context, String name);
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java
new file mode 100644
index 000000000..eb488dde9
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public class SystemServiceFactoryDefault implements SystemServiceFactoryAbstract {
+ public Object get(Context context, String name) {
+ return context.getSystemService(name);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java
new file mode 100644
index 000000000..9b99c6ea5
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java
@@ -0,0 +1,37 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.widget.Toast;
+
+/**
+ * Provides a simplified way to show toast messages without having to create the toast, set the
+ * desired gravity, etc.
+ */
+public class ToastUtils {
+ public enum Duration {SHORT, LONG}
+
+ private ToastUtils() {
+ throw new AssertionError();
+ }
+
+ public static Toast showToast(Context context, int stringResId) {
+ return showToast(context, stringResId, Duration.SHORT);
+ }
+
+ public static Toast showToast(Context context, int stringResId, Duration duration) {
+ return showToast(context, context.getString(stringResId), duration);
+ }
+
+ public static Toast showToast(Context context, String text) {
+ return showToast(context, text, Duration.SHORT);
+ }
+
+ public static Toast showToast(Context context, String text, Duration duration) {
+ Toast toast = Toast.makeText(context, text,
+ (duration == Duration.SHORT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG));
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ return toast;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
new file mode 100644
index 000000000..cad48ef8e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
@@ -0,0 +1,257 @@
+package org.wordpress.android.util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.UnsupportedEncodingException;
+import java.net.IDN;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UrlUtils {
+ public static String urlEncode(final String text) {
+ try {
+ return URLEncoder.encode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return text;
+ }
+ }
+
+ public static String urlDecode(final String text) {
+ try {
+ return URLDecoder.decode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return text;
+ }
+ }
+
+ /**
+ * @param urlString url to get host from
+ * @return host of uri if available. Empty string otherwise.
+ */
+ public static String getHost(final String urlString) {
+ if (urlString != null) {
+ Uri uri = Uri.parse(urlString);
+ if (uri.getHost() != null) {
+ return uri.getHost();
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Convert IDN names to punycode if necessary
+ */
+ public static String convertUrlToPunycodeIfNeeded(String url) {
+ if (!Charset.forName("US-ASCII").newEncoder().canEncode(url)) {
+ if (url.toLowerCase().startsWith("http://")) {
+ url = "http://" + IDN.toASCII(url.substring(7));
+ } else if (url.toLowerCase().startsWith("https://")) {
+ url = "https://" + IDN.toASCII(url.substring(8));
+ } else {
+ url = IDN.toASCII(url);
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Remove leading double slash, and inherit protocol scheme
+ */
+ public static String removeLeadingDoubleSlash(String url, String scheme) {
+ if (url != null && url.startsWith("//")) {
+ url = url.substring(2);
+ if (scheme != null) {
+ if (scheme.endsWith("://")){
+ url = scheme + url;
+ } else {
+ AppLog.e(T.UTILS, "Invalid scheme used: " + scheme);
+ }
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Add scheme prefix to an URL. This method must be called on all user entered or server fetched URLs to ensure
+ * http client will work as expected.
+ *
+ * @param url url entered by the user or fetched from a server
+ * @param addHttps true and the url is not starting with http://, it will make the url starts with https://
+ * @return url prefixed by http:// or https://
+ */
+ public static String addUrlSchemeIfNeeded(String url, boolean addHttps) {
+ if (url == null) {
+ return null;
+ }
+
+ // Remove leading double slash (eg. //example.com), needed for some wporg instances configured to
+ // switch between http or https
+ url = removeLeadingDoubleSlash(url, (addHttps ? "https" : "http") + "://");
+
+ // If the URL is a valid http or https URL, we're good to go
+ if (URLUtil.isHttpUrl(url) || URLUtil.isHttpsUrl(url)) {
+ return url;
+ }
+
+ // Else, remove the old scheme and prefix it by https:// or http://
+ return (addHttps ? "https" : "http") + "://" + removeScheme(url);
+ }
+
+ /**
+ * normalizes a URL, primarily for comparison purposes, for example so that
+ * normalizeUrl("http://google.com/") = normalizeUrl("http://google.com")
+ */
+ public static String normalizeUrl(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ // this routine is called from some performance-critical code and creating a URI from a string
+ // is slow, so skip it when possible - if we know it's not a relative path (and 99.9% of the
+ // time it won't be for our purposes) then we can normalize it without java.net.URI.normalize()
+ if (urlString.startsWith("http") &&
+ !urlString.contains("build/intermediates/exploded-aar/org.wordpress/graphview/3.1.1")) {
+ // return without a trailing slash
+ if (urlString.endsWith("/")) {
+ return urlString.substring(0, urlString.length() - 1);
+ }
+ return urlString;
+ }
+
+ // url is relative, so fall back to using slower java.net.URI normalization
+ try {
+ URI uri = URI.create(urlString);
+ return uri.normalize().toString();
+ } catch (IllegalArgumentException e) {
+ return urlString;
+ }
+ }
+
+
+ /**
+ * returns the passed url without the scheme
+ */
+ public static String removeScheme(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ int doubleslash = urlString.indexOf("//");
+ if (doubleslash == -1) {
+ doubleslash = 0;
+ } else {
+ doubleslash += 2;
+ }
+
+ return urlString.substring(doubleslash, urlString.length());
+ }
+
+ /**
+ * returns the passed url without the query parameters
+ */
+ public static String removeQuery(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+ return Uri.parse(urlString).buildUpon().clearQuery().toString();
+ }
+
+ /**
+ * returns true if passed url is https:
+ */
+ public static boolean isHttps(final String urlString) {
+ return (urlString != null && urlString.startsWith("https:"));
+ }
+
+ public static boolean isHttps(URL url) {
+ return url != null && "https".equals(url.getProtocol());
+ }
+
+ public static boolean isHttps(URI uri) {
+ if (uri == null) return false;
+
+ String protocol = uri.getScheme();
+ return protocol != null && protocol.equals("https");
+ }
+
+ /**
+ * returns https: version of passed http: url
+ */
+ public static String makeHttps(final String urlString) {
+ if (urlString == null || !urlString.startsWith("http:")) {
+ return urlString;
+ }
+ return "https:" + urlString.substring(5, urlString.length());
+ }
+
+ /**
+ * see http://stackoverflow.com/a/8591230/1673548
+ */
+ public static String getUrlMimeType(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ String extension = MimeTypeMap.getFileExtensionFromUrl(urlString);
+ if (extension == null) {
+ return null;
+ }
+
+ MimeTypeMap mime = MimeTypeMap.getSingleton();
+ String mimeType = mime.getMimeTypeFromExtension(extension);
+ if (mimeType == null) {
+ return null;
+ }
+
+ return mimeType;
+ }
+
+ /**
+ * returns false if the url is not valid or if the url host is null, else true
+ */
+ public static boolean isValidUrlAndHostNotNull(String url) {
+ try {
+ URI uri = URI.create(url);
+ if (uri.getHost() == null) {
+ return false;
+ }
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ return true;
+ }
+
+ // returns true if the passed url is for an image
+ public static boolean isImageUrl(String url) {
+ if (TextUtils.isEmpty(url)) return false;
+
+ String cleanedUrl = removeQuery(url.toLowerCase());
+
+ return cleanedUrl.endsWith("jpg") || cleanedUrl.endsWith("jpeg") ||
+ cleanedUrl.endsWith("gif") || cleanedUrl.endsWith("png");
+ }
+
+ public static String appendUrlParameter(String url, String paramName, String paramValue) {
+ Map<String, String> parameters = new HashMap<>();
+ parameters.put(paramName, paramValue);
+ return appendUrlParameters(url, parameters);
+ }
+
+ public static String appendUrlParameters(String url, Map<String, String> parameters) {
+ Uri.Builder uriBuilder = Uri.parse(url).buildUpon();
+ for (Map.Entry<String, String> parameter : parameters.entrySet()) {
+ uriBuilder.appendQueryParameter(parameter.getKey(), parameter.getValue());
+ }
+ return uriBuilder.build().toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java
new file mode 100644
index 000000000..385960558
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.util;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.util.Patterns;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.regex.Pattern;
+
+public class UserEmailUtils {
+ /**
+ * Get primary account and return its name if it matches the email address pattern.
+ *
+ * @return primary account email address if it can be found or empty string else.
+ */
+ public static String getPrimaryEmail(Context context) {
+ try {
+ AccountManager accountManager = AccountManager.get(context);
+ if (accountManager == null)
+ return "";
+ Account[] accounts = accountManager.getAccounts();
+ Pattern emailPattern = Patterns.EMAIL_ADDRESS;
+ for (Account account : accounts) {
+ // make sure account.name is an email address before adding to the list
+ if (emailPattern.matcher(account.name).matches()) {
+ return account.name;
+ }
+ }
+ return "";
+ } catch (SecurityException e) {
+ // exception will occur if app doesn't have GET_ACCOUNTS permission
+ AppLog.e(T.UTILS, "SecurityException - missing GET_ACCOUNTS permission");
+ return "";
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java
new file mode 100644
index 000000000..914373c8f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java
@@ -0,0 +1,58 @@
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import android.view.View;
+import android.widget.ListView;
+
+public class ListScrollPositionManager {
+ private int mSelectedPosition;
+ private int mListViewScrollStateIndex;
+ private int mListViewScrollStateOffset;
+ private ListView mListView;
+ private boolean mSetSelection;
+
+ public ListScrollPositionManager(ListView listView, boolean setSelection) {
+ mListView = listView;
+ mSetSelection = setSelection;
+ }
+
+ public void saveScrollOffset() {
+ mListViewScrollStateIndex = mListView.getFirstVisiblePosition();
+ View view = mListView.getChildAt(0);
+ mListViewScrollStateOffset = 0;
+ if (view != null) {
+ mListViewScrollStateOffset = view.getTop();
+ }
+ if (mSetSelection) {
+ mSelectedPosition = mListView.getCheckedItemPosition();
+ }
+ }
+
+ public void restoreScrollOffset() {
+ mListView.setSelectionFromTop(mListViewScrollStateIndex, mListViewScrollStateOffset);
+ if (mSetSelection) {
+ mListView.setItemChecked(mSelectedPosition, true);
+ }
+ }
+
+ public void saveToPreferences(Context context, String uniqueId) {
+ saveScrollOffset();
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ Editor editor = settings.edit();
+ editor.putInt("scroll-position-manager-index-" + uniqueId, mListViewScrollStateIndex);
+ editor.putInt("scroll-position-manager-offset-" + uniqueId, mListViewScrollStateOffset);
+ editor.putInt("scroll-position-manager-selected-position-" + uniqueId, mSelectedPosition);
+ editor.apply();
+ }
+
+ public void restoreFromPreferences(Context context, String uniqueId) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ mListViewScrollStateIndex = settings.getInt("scroll-position-manager-index-" + uniqueId, 0);
+ mListViewScrollStateOffset = settings.getInt("scroll-position-manager-offset-" + uniqueId, 0);
+ mSelectedPosition = settings.getInt("scroll-position-manager-selected-position-" + uniqueId, 0);
+ restoreScrollOffset();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java
new file mode 100644
index 000000000..ff472c2ed
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java
@@ -0,0 +1,144 @@
+//This Handy-Dandy class acquired and tweaked from http://stackoverflow.com/a/3145655/309558
+package org.wordpress.android.util.helpers;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class LocationHelper {
+ Timer mTimer;
+ LocationManager mLocationManager;
+ LocationResult mLocationResult;
+ boolean mGpsEnabled = false;
+ boolean mNetworkEnabled = false;
+
+ @SuppressLint("MissingPermission")
+ public boolean getLocation(Activity activity, LocationResult result) {
+ mLocationResult = result;
+ if (mLocationManager == null) {
+ mLocationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
+ }
+
+ // exceptions will be thrown if provider is not permitted.
+ try {
+ mGpsEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ } catch (Exception ex) {
+ }
+ try {
+ mNetworkEnabled = mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
+ } catch (Exception ex) {
+ }
+
+ // don't start listeners if no provider is enabled
+ if (!mGpsEnabled && !mNetworkEnabled) {
+ return false;
+ }
+
+ if (mGpsEnabled) {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps);
+ }
+
+ if (mNetworkEnabled) {
+ mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
+ }
+
+ mTimer = new Timer();
+ mTimer.schedule(new GetLastLocation(), 30000);
+ return true;
+ }
+
+ LocationListener locationListenerGps = new LocationListener() {
+ @SuppressLint("MissingPermission")
+ public void onLocationChanged(Location location) {
+ mTimer.cancel();
+ mLocationResult.gotLocation(location);
+ mLocationManager.removeUpdates(this);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+ }
+
+ public void onProviderDisabled(String provider) {
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ LocationListener locationListenerNetwork = new LocationListener() {
+ @SuppressLint("MissingPermission")
+ public void onLocationChanged(Location location) {
+ mTimer.cancel();
+ mLocationResult.gotLocation(location);
+ mLocationManager.removeUpdates(this);
+ mLocationManager.removeUpdates(locationListenerGps);
+ }
+
+ public void onProviderDisabled(String provider) {
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ class GetLastLocation extends TimerTask {
+ @Override
+ @SuppressLint("MissingPermission")
+ public void run() {
+ mLocationManager.removeUpdates(locationListenerGps);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+
+ Location net_loc = null, gps_loc = null;
+ if (mGpsEnabled) {
+ gps_loc = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ }
+ if (mNetworkEnabled) {
+ net_loc = mLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+ }
+
+ // if there are both values use the latest one
+ if (gps_loc != null && net_loc != null) {
+ if (gps_loc.getTime() > net_loc.getTime()) {
+ mLocationResult.gotLocation(gps_loc);
+ } else {
+ mLocationResult.gotLocation(net_loc);
+ }
+ return;
+ }
+
+ if (gps_loc != null) {
+ mLocationResult.gotLocation(gps_loc);
+ return;
+ }
+ if (net_loc != null) {
+ mLocationResult.gotLocation(net_loc);
+ return;
+ }
+ mLocationResult.gotLocation(null);
+ }
+ }
+
+ public static abstract class LocationResult {
+ public abstract void gotLocation(Location location);
+ }
+
+ @SuppressLint("MissingPermission")
+ public void cancelTimer() {
+ if (mTimer != null) {
+ mTimer.cancel();
+ mLocationManager.removeUpdates(locationListenerGps);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java
new file mode 100644
index 000000000..b57ad0165
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java
@@ -0,0 +1,339 @@
+package org.wordpress.android.util.helpers;
+
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.Date;
+import java.util.Map;
+
+public class MediaFile {
+ protected int id;
+ protected long postID;
+ protected String filePath = null; //path of the file into disk
+ protected String fileName = null; //name of the file into the server
+ protected String title = null;
+ protected String description = null;
+ protected String caption = null;
+ protected int horizontalAlignment; //0 = none, 1 = left, 2 = center, 3 = right
+ protected boolean verticalAligment = false; //false = bottom, true = top
+ protected int width = 500, height;
+ protected String mimeType = "";
+ protected String videoPressShortCode = null;
+ protected boolean featured = false;
+ protected boolean isVideo = false;
+ protected boolean featuredInPost;
+ protected String fileURL = null; // url of the file to download
+ protected String thumbnailURL = null; // url of the thumbnail to download
+ private String blogId;
+ private long dateCreatedGmt;
+ private String uploadState = null;
+ private String mediaId;
+
+ public static String VIDEOPRESS_SHORTCODE_ID = "videopress_shortcode";
+
+ public MediaFile(String blogId, Map<?, ?> resultMap, boolean isDotCom) {
+ setBlogId(blogId);
+ setMediaId(MapUtils.getMapStr(resultMap, "attachment_id"));
+ setPostID(MapUtils.getMapLong(resultMap, "parent"));
+ setTitle(MapUtils.getMapStr(resultMap, "title"));
+ setCaption(MapUtils.getMapStr(resultMap, "caption"));
+ setDescription(MapUtils.getMapStr(resultMap, "description"));
+ setVideoPressShortCode(MapUtils.getMapStr(resultMap, VIDEOPRESS_SHORTCODE_ID));
+
+ // get the file name from the link
+ String link = MapUtils.getMapStr(resultMap, "link");
+ setFileName(new String(link).replaceAll("^.*/([A-Za-z0-9_-]+)\\.\\w+$", "$1"));
+
+ String fileType = new String(link).replaceAll(".*\\.(\\w+)$", "$1").toLowerCase();
+ String fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileType);
+ setMimeType(fileMimeType);
+
+ // make the file urls be https://... so that we can get these images with oauth when the blogs are private
+ // assume no https for images in self-hosted blogs
+ String fileUrl = MapUtils.getMapStr(resultMap, "link");
+ if (isDotCom) {
+ fileUrl = fileUrl.replace("http:", "https:");
+ }
+ setFileURL(fileUrl);
+
+ String thumbnailURL = MapUtils.getMapStr(resultMap, "thumbnail");
+ if (thumbnailURL.startsWith("http")) {
+ if (isDotCom) {
+ thumbnailURL = thumbnailURL.replace("http:", "https:");
+ }
+ setThumbnailURL(thumbnailURL);
+ }
+
+ Date date = MapUtils.getMapDate(resultMap, "date_created_gmt");
+ if (date != null) {
+ setDateCreatedGMT(date.getTime());
+ }
+
+ Object meta = resultMap.get("metadata");
+ if (meta != null && meta instanceof Map) {
+ Map<?, ?> metadata = (Map<?, ?>) meta;
+ setWidth(MapUtils.getMapInt(metadata, "width"));
+ setHeight(MapUtils.getMapInt(metadata, "height"));
+ }
+ }
+
+ public MediaFile() {
+ // default constructor
+ }
+
+ public MediaFile(MediaFile mediaFile) {
+ this.id = mediaFile.id;
+ this.postID = mediaFile.postID;
+ this.filePath = mediaFile.filePath;
+ this.fileName = mediaFile.fileName;
+ this.title = mediaFile.title;
+ this.description = mediaFile.description;
+ this.caption = mediaFile.caption;
+ this.horizontalAlignment = mediaFile.horizontalAlignment;
+ this.verticalAligment = mediaFile.verticalAligment;
+ this.width = mediaFile.width;
+ this.height = mediaFile.height;
+ this.mimeType = mediaFile.mimeType;
+ this.videoPressShortCode = mediaFile.videoPressShortCode;
+ this.featured = mediaFile.featured;
+ this.isVideo = mediaFile.isVideo;
+ this.featuredInPost = mediaFile.featuredInPost;
+ this.fileURL = mediaFile.fileURL;
+ this.thumbnailURL = mediaFile.thumbnailURL;
+ this.blogId = mediaFile.blogId;
+ this.dateCreatedGmt = mediaFile.dateCreatedGmt;
+ this.uploadState = mediaFile.uploadState;
+ this.mediaId = mediaFile.mediaId;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public void setMediaId(String id) {
+ mediaId = id;
+ }
+
+ public boolean isFeatured() {
+ return featured;
+ }
+
+ public void setFeatured(boolean featured) {
+ this.featured = featured;
+ }
+
+ public long getPostID() {
+ return postID;
+ }
+
+ public void setPostID(long postID) {
+ this.postID = postID;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public void setFilePath(String filePath) {
+ this.filePath = filePath;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getFileURL() {
+ return fileURL;
+ }
+
+ public void setFileURL(String fileURL) {
+ this.fileURL = fileURL;
+ }
+
+ public String getThumbnailURL() {
+ return thumbnailURL;
+ }
+
+ public void setThumbnailURL(String thumbnailURL) {
+ this.thumbnailURL = thumbnailURL;
+ }
+
+ public boolean isVerticalAlignmentOnTop() {
+ return verticalAligment;
+ }
+
+ public void setVerticalAlignmentOnTop(boolean verticalAligment) {
+ this.verticalAligment = verticalAligment;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public String getMimeType() {
+ return StringUtils.notNullStr(mimeType);
+ }
+
+ public void setMimeType(String type) {
+ mimeType = StringUtils.notNullStr(type);
+ }
+
+ public String getVideoPressShortCode() {
+ return videoPressShortCode;
+ }
+
+ public void setVideoPressShortCode(String videoPressShortCode) {
+ this.videoPressShortCode = videoPressShortCode;
+ }
+
+ public int getHorizontalAlignment() {
+ return horizontalAlignment;
+ }
+
+ public void setHorizontalAlignment(int horizontalAlignment) {
+ this.horizontalAlignment = horizontalAlignment;
+ }
+
+ public boolean isVideo() {
+ return isVideo;
+ }
+
+ public void setVideo(boolean isVideo) {
+ this.isVideo = isVideo;
+ }
+
+ public boolean isFeaturedInPost() {
+ return featuredInPost;
+ }
+
+ public void setFeaturedInPost(boolean featuredInPost) {
+ this.featuredInPost = featuredInPost;
+ }
+
+ public String getBlogId() {
+ return blogId;
+ }
+
+ public void setBlogId(String blogId) {
+ this.blogId = blogId;
+ }
+
+ public void setDateCreatedGMT(long date_created_gmt) {
+ this.dateCreatedGmt = date_created_gmt;
+ }
+
+ public long getDateCreatedGMT() {
+ return dateCreatedGmt;
+ }
+
+ public void setUploadState(String uploadState) {
+ this.uploadState = uploadState;
+ }
+
+ public String getUploadState() {
+ return uploadState;
+ }
+
+ /**
+ * Outputs the Html for an image
+ * If a fullSizeUrl exists, a link will be created to it from the resizedPictureUrl
+ */
+ public String getImageHtmlForUrls(String fullSizeUrl, String resizedPictureURL, boolean shouldAddImageWidthCSS) {
+ String alignment = "";
+ switch (getHorizontalAlignment()) {
+ case 0:
+ alignment = "alignnone";
+ break;
+ case 1:
+ alignment = "alignleft";
+ break;
+ case 2:
+ alignment = "aligncenter";
+ break;
+ case 3:
+ alignment = "alignright";
+ break;
+ }
+
+ String alignmentCSS = "class=\"" + alignment + " size-full\" ";
+
+ if (shouldAddImageWidthCSS) {
+ alignmentCSS += "style=\"max-width: " + getWidth() + "px\" ";
+ }
+
+ // Check if we uploaded a featured picture that is not added to the Post content (normal case)
+ if ((fullSizeUrl != null && fullSizeUrl.equalsIgnoreCase("")) ||
+ (resizedPictureURL != null && resizedPictureURL.equalsIgnoreCase(""))) {
+ return ""; // Not featured in Post. Do not add to the content.
+ }
+
+ if (fullSizeUrl == null && resizedPictureURL != null) {
+ fullSizeUrl = resizedPictureURL;
+ } else if (fullSizeUrl != null && resizedPictureURL == null) {
+ resizedPictureURL = fullSizeUrl;
+ }
+
+ String mediaTitle = StringUtils.notNullStr(getTitle());
+
+ String content = String.format("<a href=\"%s\"><img title=\"%s\" %s alt=\"image\" src=\"%s\" /></a>",
+ fullSizeUrl, mediaTitle, alignmentCSS, resizedPictureURL);
+
+ if (!TextUtils.isEmpty(getCaption())) {
+ content = String.format("[caption id=\"\" align=\"%s\" width=\"%d\"]%s%s[/caption]",
+ alignment, getWidth(), content, TextUtils.htmlEncode(getCaption()));
+ }
+
+ return content;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java
new file mode 100644
index 000000000..ab7326a17
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java
@@ -0,0 +1,87 @@
+
+package org.wordpress.android.util.helpers;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/**
+ * A model representing a Media Gallery.
+ * A unique id is not used on the website, but only in this app.
+ * It is used to uniquely determining the instance of the object, as it is
+ * passed between post and media gallery editor.
+ */
+public class MediaGallery implements Serializable {
+ private static final long serialVersionUID = 2359176987182027508L;
+
+ private long uniqueId;
+ private boolean isRandom;
+ private String type;
+ private int numColumns;
+ private ArrayList<String> ids;
+
+ public MediaGallery(boolean isRandom, String type, int numColumns, ArrayList<String> ids) {
+ this.isRandom = isRandom;
+ this.type = type;
+ this.numColumns = numColumns;
+ this.ids = ids;
+ this.uniqueId = System.currentTimeMillis();
+ }
+
+ public MediaGallery() {
+ isRandom = false;
+ type = "";
+ numColumns = 3;
+ ids = new ArrayList<String>();
+ this.uniqueId = System.currentTimeMillis();
+ }
+
+ public boolean isRandom() {
+ return isRandom;
+ }
+
+ public void setRandom(boolean isRandom) {
+ this.isRandom = isRandom;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public int getNumColumns() {
+ return numColumns;
+ }
+
+ public void setNumColumns(int numColumns) {
+ this.numColumns = numColumns;
+ }
+
+ public ArrayList<String> getIds() {
+ return ids;
+ }
+
+ public String getIdsStr() {
+ String ids_str = "";
+ if (ids.size() > 0) {
+ for (String id : ids) {
+ ids_str += id + ",";
+ }
+ ids_str = ids_str.substring(0, ids_str.length() - 1);
+ }
+ return ids_str;
+ }
+
+ public void setIds(ArrayList<String> ids) {
+ this.ids = ids;
+ }
+
+ /**
+ * An id to uniquely identify a media gallery object, so that the same object can be edited in the post editor
+ */
+ public long getUniqueId() {
+ return uniqueId;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java
new file mode 100644
index 000000000..588b98141
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.text.style.ImageSpan;
+
+public class MediaGalleryImageSpan extends ImageSpan {
+ private MediaGallery mMediaGallery;
+
+ public MediaGalleryImageSpan(Context context, MediaGallery mediaGallery, int placeHolder) {
+ super(context, placeHolder);
+ setMediaGallery(mediaGallery);
+ }
+
+ public MediaGallery getMediaGallery() {
+ return mMediaGallery;
+ }
+
+ public void setMediaGallery(MediaGallery mediaGallery) {
+ this.mMediaGallery = mediaGallery;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java
new file mode 100644
index 000000000..e6f4bf323
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java
@@ -0,0 +1,72 @@
+package org.wordpress.android.util.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
+import android.util.TypedValue;
+
+import org.wordpress.android.util.R;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+public class SwipeToRefreshHelper implements OnRefreshListener {
+ private CustomSwipeRefreshLayout mSwipeRefreshLayout;
+ private RefreshListener mRefreshListener;
+ private boolean mRefreshing;
+
+ public interface RefreshListener {
+ public void onRefreshStarted();
+ }
+
+ public SwipeToRefreshHelper(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) {
+ init(context, swipeRefreshLayout, listener);
+ }
+
+ public void init(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) {
+ mRefreshListener = listener;
+ mSwipeRefreshLayout = swipeRefreshLayout;
+ mSwipeRefreshLayout.setOnRefreshListener(this);
+ final TypedArray styleAttrs = obtainStyledAttrsFromThemeAttr(context, R.attr.swipeToRefreshStyle,
+ R.styleable.RefreshIndicator);
+ int color = styleAttrs.getColor(R.styleable.RefreshIndicator_refreshIndicatorColor, context.getResources()
+ .getColor(android.R.color.holo_blue_dark));
+ mSwipeRefreshLayout.setColorSchemeColors(color, color, color, color);
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mRefreshing = refreshing;
+ // Delayed refresh, it fixes https://code.google.com/p/android/issues/detail?id=77712
+ // 50ms seems a good compromise (always worked during tests) and fast enough so user can't notice the delay
+ if (refreshing) {
+ mSwipeRefreshLayout.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // use mRefreshing so if the refresh takes less than 50ms, loading indicator won't show up.
+ mSwipeRefreshLayout.setRefreshing(mRefreshing);
+ }
+ }, 50);
+ } else {
+ mSwipeRefreshLayout.setRefreshing(false);
+ }
+ }
+
+ public boolean isRefreshing() {
+ return mSwipeRefreshLayout.isRefreshing();
+ }
+
+ @Override
+ public void onRefresh() {
+ mRefreshListener.onRefreshStarted();
+ }
+
+ public void setEnabled(boolean enabled) {
+ mSwipeRefreshLayout.setEnabled(enabled);
+ }
+
+ public static TypedArray obtainStyledAttrsFromThemeAttr(Context context, int themeAttr, int[] styleAttrs) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(themeAttr, outValue, true);
+ int styleResId = outValue.resourceId;
+ return context.obtainStyledAttributes(styleResId, styleAttrs);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java
new file mode 100644
index 000000000..b35f84757
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.util.helpers;
+
+//See: http://stackoverflow.com/a/11024200
+public class Version implements Comparable<Version> {
+ private String version;
+
+ public final String get() {
+ return this.version;
+ }
+
+ public Version(String version) {
+ if(version == null)
+ throw new IllegalArgumentException("Version can not be null");
+ if(!version.matches("[0-9]+(\\.[0-9]+)*"))
+ throw new IllegalArgumentException("Invalid version format");
+ this.version = version;
+ }
+
+ @Override public int compareTo(Version that) {
+ if(that == null)
+ return 1;
+ String[] thisParts = this.get().split("\\.");
+ String[] thatParts = that.get().split("\\.");
+ int length = Math.max(thisParts.length, thatParts.length);
+ for(int i = 0; i < length; i++) {
+ int thisPart = i < thisParts.length ?
+ Integer.parseInt(thisParts[i]) : 0;
+ int thatPart = i < thatParts.length ?
+ Integer.parseInt(thatParts[i]) : 0;
+ if(thisPart < thatPart)
+ return -1;
+ if(thisPart > thatPart)
+ return 1;
+ }
+ return 0;
+ }
+
+ @Override public boolean equals(Object that) {
+ if(this == that)
+ return true;
+ if(that == null)
+ return false;
+ if(this.getClass() != that.getClass())
+ return false;
+ return this.compareTo((Version) that) == 0;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java
new file mode 100644
index 000000000..da333b24e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java
@@ -0,0 +1,59 @@
+package org.wordpress.android.util.helpers;
+
+import android.text.Editable;
+import android.text.Html;
+import android.text.style.BulletSpan;
+import android.text.style.LeadingMarginSpan;
+
+import org.xml.sax.XMLReader;
+
+import java.util.Vector;
+
+/**
+ * Handle tags that the Html class doesn't understand
+ * Tweaked from source at http://stackoverflow.com/questions/4044509/android-how-to-use-the-html-taghandler
+ */
+public class WPHtmlTagHandler implements Html.TagHandler {
+ private int mListItemCount = 0;
+ private Vector<String> mListParents = new Vector<String>();
+
+ @Override
+ public void handleTag(final boolean opening, final String tag, Editable output,
+ final XMLReader xmlReader) {
+ if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) {
+ if (opening) {
+ mListParents.add(tag);
+ } else {
+ mListParents.remove(tag);
+ }
+ mListItemCount = 0;
+ } else if (tag.equals("li") && !opening) {
+ handleListTag(output);
+ }
+ }
+
+ private void handleListTag(Editable output) {
+ if (mListParents.lastElement().equals("ul")) {
+ output.append("\n");
+ String[] split = output.toString().split("\n");
+ int start = 0;
+ if (split.length != 1) {
+ int lastIndex = split.length - 1;
+ start = output.length() - split[lastIndex].length() - 1;
+ }
+ output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0);
+ } else if (mListParents.lastElement().equals("ol")) {
+ mListItemCount++;
+ output.append("\n");
+ String[] split = output.toString().split("\n");
+ int start = 0;
+ if (split.length != 1) {
+ int lastIndex = split.length - 1;
+ start = output.length() - split[lastIndex].length() - 1;
+ }
+ output.insert(start, mListItemCount + ". ");
+ output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start,
+ output.length(), 0);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java
new file mode 100644
index 000000000..b03d74045
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java
@@ -0,0 +1,177 @@
+package org.wordpress.android.util.helpers;
+
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.PhotonUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * ImageGetter for Html.fromHtml()
+ * adapted from existing ImageGetter code in NoteCommentFragment
+ */
+public class WPImageGetter implements Html.ImageGetter {
+ private final WeakReference<TextView> mWeakView;
+ private final int mMaxSize;
+ private ImageLoader mImageLoader;
+ private Drawable mLoadingDrawable;
+ private Drawable mFailedDrawable;
+
+ public WPImageGetter(TextView view) {
+ this(view, 0);
+ }
+
+ public WPImageGetter(TextView view, int maxSize) {
+ mWeakView = new WeakReference<TextView>(view);
+ mMaxSize = maxSize;
+ }
+
+ public WPImageGetter(TextView view, int maxSize, ImageLoader imageLoader, Drawable loadingDrawable,
+ Drawable failedDrawable) {
+ mWeakView = new WeakReference<TextView>(view);
+ mMaxSize = maxSize;
+ mImageLoader = imageLoader;
+ mLoadingDrawable = loadingDrawable;
+ mFailedDrawable = failedDrawable;
+ }
+
+ private TextView getView() {
+ return mWeakView.get();
+ }
+
+ @Override
+ public Drawable getDrawable(String source) {
+ if (mImageLoader == null || mLoadingDrawable == null || mFailedDrawable == null) {
+ throw new RuntimeException("Developer, you need to call setImageLoader, setLoadingDrawable and setFailedDrawable");
+ }
+
+ if (TextUtils.isEmpty(source)) {
+ return null;
+ }
+
+ // images in reader comments may skip "http:" (no idea why) so make sure to add protocol here
+ if (source.startsWith("//")) {
+ source = "http:" + source;
+ }
+
+ // use Photon if a max size is requested (otherwise the full-sized image will be downloaded
+ // and then resized)
+ if (mMaxSize > 0) {
+ source = PhotonUtils.getPhotonImageUrl(source, mMaxSize, 0);
+ }
+
+ final RemoteDrawable remote = new RemoteDrawable(mLoadingDrawable, mFailedDrawable);
+
+ mImageLoader.get(source, new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ remote.displayFailed();
+ TextView view = getView();
+ if (view != null) {
+ view.invalidate();
+ }
+ }
+
+ @Override
+ public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
+ if (response.getBitmap() == null) {
+ AppLog.w(T.UTILS, "WPImageGetter null bitmap");
+ }
+
+ TextView view = getView();
+ if (view == null) {
+ AppLog.w(T.UTILS, "WPImageGetter view is invalid");
+ return;
+ }
+
+ int maxWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
+ if (mMaxSize > 0 && (maxWidth > mMaxSize || maxWidth == 0)) {
+ maxWidth = mMaxSize;
+ }
+
+ Drawable drawable = new BitmapDrawable(view.getContext().getResources(), response.getBitmap());
+ remote.setRemoteDrawable(drawable, maxWidth);
+
+ // force textView to resize correctly if image isn't cached by resetting the content
+ // to itself - this way the textView will use the cached image, and resizing to
+ // accommodate the image isn't necessary
+ if (!isImmediate) {
+ view.setText(view.getText());
+ }
+ }
+ });
+
+ return remote;
+ }
+
+ public static class RemoteDrawable extends BitmapDrawable {
+ Drawable mRemoteDrawable;
+ final Drawable mLoadingDrawable;
+ final Drawable mFailedDrawable;
+ private boolean mDidFail = false;
+
+ public RemoteDrawable(Drawable loadingDrawable, Drawable failedDrawable) {
+ mLoadingDrawable = loadingDrawable;
+ mFailedDrawable = failedDrawable;
+ setBounds(0, 0, mLoadingDrawable.getIntrinsicWidth(), mLoadingDrawable.getIntrinsicHeight());
+ }
+
+ public void displayFailed() {
+ mDidFail = true;
+ }
+
+ public void setBounds(int x, int y, int width, int height) {
+ super.setBounds(x, y, width, height);
+ if (mRemoteDrawable != null) {
+ mRemoteDrawable.setBounds(x, y, width, height);
+ return;
+ }
+ if (mLoadingDrawable != null) {
+ mLoadingDrawable.setBounds(x, y, width, height);
+ mFailedDrawable.setBounds(x, y, width, height);
+ }
+ }
+
+ public void setRemoteDrawable(Drawable remote, int maxWidth) {
+ // null sentinel for now
+ if (remote == null) {
+ // throw error
+ return;
+ }
+ mRemoteDrawable = remote;
+ // determine if we need to scale the image to fit in view
+ int imgWidth = remote.getIntrinsicWidth();
+ int imgHeight = remote.getIntrinsicHeight();
+ float xScale = (float) imgWidth / (float) maxWidth;
+ if (xScale > 1.0f) {
+ setBounds(0, 0, Math.round(imgWidth / xScale), Math.round(imgHeight / xScale));
+ } else {
+ setBounds(0, 0, imgWidth, imgHeight);
+ }
+ }
+
+ public boolean didFail() {
+ return mDidFail;
+ }
+
+ public void draw(Canvas canvas) {
+ if (mRemoteDrawable != null) {
+ mRemoteDrawable.draw(canvas);
+ } else if (didFail()) {
+ mFailedDrawable.draw(canvas);
+ } else {
+ mLoadingDrawable.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java
new file mode 100644
index 000000000..fa0a0b4aa
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java
@@ -0,0 +1,140 @@
+//Add WordPress image fields to ImageSpan object
+
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.style.ImageSpan;
+
+public class WPImageSpan extends ImageSpan implements Parcelable {
+ protected Uri mImageSource = null;
+ protected boolean mNetworkImageLoaded = false;
+ protected MediaFile mMediaFile;
+ protected int mStartPosition, mEndPosition;
+
+ protected WPImageSpan() {
+ super((Bitmap) null);
+ }
+
+ public WPImageSpan(Context context, Bitmap b, Uri src) {
+ super(context, b);
+ this.mImageSource = src;
+ mMediaFile = new MediaFile();
+ }
+
+ public WPImageSpan(Context context, int resId, Uri src) {
+ super(context, resId);
+ this.mImageSource = src;
+ mMediaFile = new MediaFile();
+ }
+
+ public void setPosition(int start, int end) {
+ mStartPosition = start;
+ mEndPosition = end;
+ }
+
+ public int getStartPosition() {
+ return mStartPosition >= 0 ? mStartPosition : 0;
+ }
+
+ public int getEndPosition() {
+ return mEndPosition < getStartPosition() ? getStartPosition() : mEndPosition;
+ }
+
+ public MediaFile getMediaFile() {
+ return mMediaFile;
+ }
+
+ public void setMediaFile(MediaFile mMediaFile) {
+ this.mMediaFile = mMediaFile;
+ }
+
+ public void setImageSource(Uri mImageSource) {
+ this.mImageSource = mImageSource;
+ }
+
+ public Uri getImageSource() {
+ return mImageSource;
+ }
+
+ public boolean isNetworkImageLoaded() {
+ return mNetworkImageLoaded;
+ }
+
+ public void setNetworkImageLoaded(boolean networkImageLoaded) {
+ this.mNetworkImageLoaded = networkImageLoaded;
+ }
+
+ protected void setupFromParcel(Parcel in) {
+ MediaFile mediaFile = new MediaFile();
+
+ boolean[] booleans = new boolean[2];
+ in.readBooleanArray(booleans);
+ setNetworkImageLoaded(booleans[0]);
+ mediaFile.setVideo(booleans[1]);
+
+ setImageSource(Uri.parse(in.readString()));
+ mediaFile.setMediaId(in.readString());
+ mediaFile.setBlogId(in.readString());
+ mediaFile.setPostID(in.readLong());
+ mediaFile.setCaption(in.readString());
+ mediaFile.setDescription(in.readString());
+ mediaFile.setTitle(in.readString());
+ mediaFile.setMimeType(in.readString());
+ mediaFile.setFileName(in.readString());
+ mediaFile.setThumbnailURL(in.readString());
+ mediaFile.setVideoPressShortCode(in.readString());
+ mediaFile.setFileURL(in.readString());
+ mediaFile.setFilePath(in.readString());
+ mediaFile.setDateCreatedGMT(in.readLong());
+ mediaFile.setWidth(in.readInt());
+ mediaFile.setHeight(in.readInt());
+ setPosition(in.readInt(), in.readInt());
+
+ setMediaFile(mediaFile);
+ }
+
+ public static final Parcelable.Creator<WPImageSpan> CREATOR
+ = new Parcelable.Creator<WPImageSpan>() {
+ public WPImageSpan createFromParcel(Parcel in) {
+ WPImageSpan imageSpan = new WPImageSpan();
+ imageSpan.setupFromParcel(in);
+ return imageSpan;
+ }
+
+ public WPImageSpan[] newArray(int size) {
+ return new WPImageSpan[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeBooleanArray(new boolean[] {mNetworkImageLoaded, mMediaFile.isVideo()});
+ parcel.writeString(mImageSource.toString());
+ parcel.writeString(mMediaFile.getMediaId());
+ parcel.writeString(mMediaFile.getBlogId());
+ parcel.writeLong(mMediaFile.getPostID());
+ parcel.writeString(mMediaFile.getCaption());
+ parcel.writeString(mMediaFile.getDescription());
+ parcel.writeString(mMediaFile.getTitle());
+ parcel.writeString(mMediaFile.getMimeType());
+ parcel.writeString(mMediaFile.getFileName());
+ parcel.writeString(mMediaFile.getThumbnailURL());
+ parcel.writeString(mMediaFile.getVideoPressShortCode());
+ parcel.writeString(mMediaFile.getFileURL());
+ parcel.writeString(mMediaFile.getFilePath());
+ parcel.writeLong(mMediaFile.getDateCreatedGMT());
+ parcel.writeInt(mMediaFile.getWidth());
+ parcel.writeInt(mMediaFile.getHeight());
+ parcel.writeInt(getStartPosition());
+ parcel.writeInt(getEndPosition());
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java
new file mode 100644
index 000000000..33cdc0093
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.util.helpers;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.style.QuoteSpan;
+
+/**
+ * Customzed QuoteSpan for use in SpannableString's
+ */
+public class WPQuoteSpan extends QuoteSpan {
+ public static final int STRIPE_COLOR = 0xFF21759B;
+ private static final int STRIPE_WIDTH = 5;
+ private static final int GAP_WIDTH = 20;
+
+ public WPQuoteSpan(){
+ super(STRIPE_COLOR);
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ int margin = GAP_WIDTH * 2 + STRIPE_WIDTH;
+ return margin;
+ }
+
+ /**
+ * Draw a nice thick gray bar if Ice Cream Sandwhich or newer. There's a
+ * bug on older devices that does not respect the increased margin.
+ */
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
+ CharSequence text, int start, int end, boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(STRIPE_COLOR);
+
+ c.drawRect(GAP_WIDTH + x, top, x + dir * STRIPE_WIDTH, bottom, p);
+
+ p.setStyle(style);
+ p.setColor(color);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java
new file mode 100644
index 000000000..4b6805ccf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wordpress.android.util.helpers;
+
+import android.os.Parcel;
+import android.text.style.UnderlineSpan;
+
+/**
+ * WPUnderlineSpan is used as an alternative class to UnderlineSpan. UnderlineSpan is used by EditText auto
+ * correct, so it can get mixed up with our formatting.
+ */
+public class WPUnderlineSpan extends UnderlineSpan {
+ public WPUnderlineSpan() {
+ super();
+ }
+
+ public WPUnderlineSpan(Parcel src) {
+ super(src);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java
new file mode 100644
index 000000000..1418e79ea
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util.helpers;
+
+import android.app.Activity;
+import android.text.TextUtils;
+import android.view.View;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.widget.ProgressBar;
+
+public class WPWebChromeClient extends WebChromeClient {
+ private final ProgressBar mProgressBar;
+ private final Activity mActivity;
+ private final boolean mAutoUpdateActivityTitle;
+
+ public WPWebChromeClient(Activity activity, ProgressBar progressBar) {
+ mActivity = activity;
+ mProgressBar = progressBar;
+ mAutoUpdateActivityTitle = true;
+ }
+
+ public WPWebChromeClient(Activity activity,
+ ProgressBar progressBar,
+ boolean autoUpdateActivityTitle) {
+ mActivity = activity;
+ mProgressBar = progressBar;
+ mAutoUpdateActivityTitle = autoUpdateActivityTitle;
+ }
+
+ public void onProgressChanged(WebView webView, int progress) {
+ if (mActivity != null
+ && !mActivity.isFinishing()
+ && mAutoUpdateActivityTitle
+ && !TextUtils.isEmpty(webView.getTitle())) {
+ mActivity.setTitle(webView.getTitle());
+ }
+ if (mProgressBar != null) {
+ if (progress == 100) {
+ mProgressBar.setVisibility(View.GONE);
+ } else {
+ mProgressBar.setVisibility(View.VISIBLE);
+ mProgressBar.setProgress(progress);
+ }
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java
new file mode 100644
index 000000000..b0b4dc017
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java
@@ -0,0 +1,299 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Text view that auto adjusts text size to fit within the view.
+ * If the text size equals the minimum text size and still does not
+ * fit, append with an ellipsis.
+ *
+ * See http://stackoverflow.com/a/5535672
+ *
+ */
+public class AutoResizeTextView extends TextView {
+ // Minimum text size for this text view
+ private static final float MIN_TEXT_SIZE = 20;
+
+ // Interface for resize notifications
+ public interface OnTextResizeListener {
+ void onTextResize(TextView textView, float oldSize, float newSize);
+ }
+
+ // Our ellipse string - Unicode Character 'HORIZONTAL ELLIPSIS'
+ private static final String M_ELLIPSIS = "\u2026";
+
+ // Registered resize listener
+ private OnTextResizeListener mTextResizeListener;
+
+ // Flag for text and/or size changes to force a resize
+ private boolean mNeedsResize = false;
+
+ // Text size that is set from code. This acts as a starting point for resizing
+ private float mTextSize;
+
+ // Temporary upper bounds on the starting text size
+ private float mMaxTextSize = 0;
+
+ // Lower bounds for text size
+ private float mMinTextSize = MIN_TEXT_SIZE;
+
+ // Text view line spacing multiplier
+ private float mSpacingMult = 1.0f;
+
+ // Text view additional line spacing
+ private float mSpacingAdd = 0.0f;
+
+ // Add ellipsis to text that overflows at the smallest text size
+ private boolean mAddEllipsis = true;
+
+ // Default constructor override
+ public AutoResizeTextView(Context context) {
+ this(context, null);
+ }
+
+ // Default constructor when inflating from XML file
+ public AutoResizeTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ // Default constructor override
+ public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * When text changes, set the force resize flag to true and reset the text size.
+ */
+ @Override
+ protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
+ mNeedsResize = true;
+ // Since this view may be reused, it is good to reset the text size
+ resetTextSize();
+ }
+
+ /**
+ * If the text view size changed, set the force resize flag to true
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (w != oldw || h != oldh) {
+ mNeedsResize = true;
+ }
+ }
+
+ /**
+ * Register listener to receive resize notifications
+ * @param listener
+ */
+ public void setOnResizeListener(OnTextResizeListener listener) {
+ mTextResizeListener = listener;
+ }
+
+ /**
+ * Override the set text size to update our internal reference values
+ */
+ @Override
+ public void setTextSize(float size) {
+ super.setTextSize(size);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * Override the set text size to update our internal reference values
+ */
+ @Override
+ public void setTextSize(int unit, float size) {
+ super.setTextSize(unit, size);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * Override the set line spacing to update our internal reference values
+ */
+ @Override
+ public void setLineSpacing(float add, float mult) {
+ super.setLineSpacing(add, mult);
+ mSpacingMult = mult;
+ mSpacingAdd = add;
+ }
+
+ /**
+ * Set the upper text size limit and invalidate the view
+ * @param maxTextSize
+ */
+ public void setMaxTextSize(float maxTextSize) {
+ mMaxTextSize = maxTextSize;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Return upper text size limit
+ * @return
+ */
+ public float getMaxTextSize() {
+ return mMaxTextSize;
+ }
+
+ /**
+ * Set the lower text size limit and invalidate the view
+ * @param minTextSize
+ */
+ public void setMinTextSize(float minTextSize) {
+ mMinTextSize = minTextSize;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Return lower text size limit
+ * @return
+ */
+ public float getMinTextSize() {
+ return mMinTextSize;
+ }
+
+ /**
+ * Set flag to add ellipsis to text that overflows at the smallest text size
+ * @param addEllipsis
+ */
+ public void setAddEllipsis(boolean addEllipsis) {
+ mAddEllipsis = addEllipsis;
+ }
+
+ /**
+ * Return flag to add ellipsis to text that overflows at the smallest text size
+ * @return
+ */
+ public boolean getAddEllipsis() {
+ return mAddEllipsis;
+ }
+
+ /**
+ * Reset the text to the original size
+ */
+ private void resetTextSize() {
+ if (mTextSize > 0) {
+ super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ mMaxTextSize = mTextSize;
+ }
+ }
+
+ /**
+ * Resize text after measuring
+ */
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (changed || mNeedsResize) {
+ int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
+ resizeText(widthLimit, heightLimit);
+ }
+ super.onLayout(changed, left, top, right, bottom);
+ }
+
+ /**
+ * Resize the text size with default width and height
+ */
+ public void resizeText() {
+ int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
+ int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
+ resizeText(widthLimit, heightLimit);
+ }
+
+ /**
+ * Resize the text size with specified width and height
+ * @param width
+ * @param height
+ */
+ public void resizeText(int width, int height) {
+ CharSequence text = getText();
+ // Do not resize if the view does not have dimensions or there is no text
+ if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
+ return;
+ }
+
+ // Get the text view's paint object
+ TextPaint textPaint = getPaint();
+
+ // Store the current text size
+ float oldTextSize = textPaint.getTextSize();
+ // If there is a max text size set, use the lesser of that and the default text size
+ float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
+
+ // Get the required text height
+ int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
+
+ // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
+ while (textHeight > height && targetTextSize > mMinTextSize) {
+ targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
+ textHeight = getTextHeight(text, textPaint, width, targetTextSize);
+ }
+
+ // If we had reached our minimum text size and still don't fit, append an ellipsis
+ if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
+ // Draw using a static layout
+ // modified: use a copy of TextPaint for measuring
+ TextPaint paint = new TextPaint(textPaint);
+ // Draw using a static layout
+ StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL,
+ mSpacingMult, mSpacingAdd, false);
+ // Check that we have a least one line of rendered text
+ if (layout.getLineCount() > 0) {
+ // Since the line at the specific vertical position would be cut off,
+ // we must trim up to the previous line
+ int lastLine = layout.getLineForVertical(height) - 1;
+ // If the text would not even fit on a single line, clear it
+ if (lastLine < 0) {
+ setText("");
+ } else {
+ // Otherwise, trim to the previous line and add an ellipsis
+ int start = layout.getLineStart(lastLine);
+ int end = layout.getLineEnd(lastLine);
+ float lineWidth = layout.getLineWidth(lastLine);
+ float ellipseWidth = paint.measureText(M_ELLIPSIS);
+
+ // Trim characters off until we have enough room to draw the ellipsis
+ while (width < lineWidth + ellipseWidth) {
+ lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString());
+ }
+ setText(text.subSequence(0, end) + M_ELLIPSIS);
+ }
+ }
+ }
+
+ // Some devices try to auto adjust line spacing, so force default line spacing
+ // and invalidate the layout as a side effect
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
+ setLineSpacing(mSpacingAdd, mSpacingMult);
+
+ // Notify the listener if registered
+ if (mTextResizeListener != null) {
+ mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
+ }
+
+ // Reset force resize flag
+ mNeedsResize = false;
+ }
+
+ // Set the text size of the text paint object and use a static layout to render text off screen before measuring
+ private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
+ // modified: make a copy of the original TextPaint object for measuring
+ // (apparently the object gets modified while measuring, see also the
+ // docs for TextView.getPaint() (which states to access it read-only)
+ TextPaint paintCopy = new TextPaint(paint);
+ // Update the text paint object
+ paintCopy.setTextSize(textSize);
+ // Measure using a static layout
+ StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.ALIGN_NORMAL,
+ mSpacingMult, mSpacingAdd, true);
+ return layout.getHeight();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java
new file mode 100644
index 000000000..356268922
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java
@@ -0,0 +1,33 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
+ public CustomSwipeRefreshLayout(Context context) {
+ super(context);
+ }
+
+ public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ try{
+ return super.onTouchEvent(event);
+ } catch(IllegalArgumentException e) {
+ // Fix for https://github.com/wordpress-mobile/WordPress-Android/issues/2373
+ // Catch IllegalArgumentException which can be fired by the underlying SwipeRefreshLayout.onTouchEvent()
+ // method.
+ // When android support-v4 fixes it, we'll have to remove that custom layout completely.
+ AppLog.e(T.UTILS, e);
+ return true;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java
new file mode 100644
index 000000000..0468cf807
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+/*
+ * @deprecated This custom EditText is used solely by the "legacy" editor in WP Android.
+ * It will be removed when we drop the legacy editor and should not be used in new code.
+ */
+@Deprecated
+public class WPEditText extends EditText {
+ private EditTextImeBackListener mOnImeBack;
+ private OnSelectionChangedListener onSelectionChangedListener;
+
+ public WPEditText(Context context) {
+ super(context);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ if (onSelectionChangedListener != null) {
+ onSelectionChangedListener.onSelectionChanged();
+ }
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ if (mOnImeBack != null)
+ mOnImeBack.onImeBack(this, this.getText().toString());
+ }
+
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ public void setOnEditTextImeBackListener(EditTextImeBackListener listener) {
+ mOnImeBack = listener;
+ }
+
+ public interface EditTextImeBackListener {
+ public abstract void onImeBack(WPEditText ctrl, String text);
+ }
+
+ public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
+ onSelectionChangedListener = listener;
+ }
+
+ public interface OnSelectionChangedListener {
+ public abstract void onSelectionChanged();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/res/values/attrs.xml b/libs/utils/WordPressUtils/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..dd1fa5cbf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/attrs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <attr name="swipeToRefreshStyle" format="reference"/>
+ <declare-styleable name="RefreshIndicator">
+ <attr name="refreshIndicatorColor" format="reference|color"/>
+ </declare-styleable>
+</resources>
diff --git a/libs/utils/WordPressUtils/src/main/res/values/strings.xml b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
new file mode 100644
index 000000000..34d25dada
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="no_network_message">There is no network available</string>
+ <string name="timespan_now">Now</string>
+</resources>
diff --git a/libs/utils/build.gradle b/libs/utils/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/utils/build.gradle
diff --git a/libs/utils/gradle/wrapper/gradle-wrapper.jar b/libs/utils/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..0087cd3b1
--- /dev/null
+++ b/libs/utils/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/utils/gradle/wrapper/gradle-wrapper.properties b/libs/utils/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..5e7158a61
--- /dev/null
+++ b/libs/utils/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Sep 06 11:08:13 CEST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
diff --git a/libs/utils/gradlew b/libs/utils/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/utils/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/utils/gradlew.bat b/libs/utils/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/utils/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/utils/settings.gradle b/libs/utils/settings.gradle
new file mode 100644
index 000000000..3519745ed
--- /dev/null
+++ b/libs/utils/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressUtils' \ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..d00ff04c5
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,5 @@
+include ':WordPress'
+include ':libs:utils:WordPressUtils'
+include ':libs:networking:WordPressNetworking'
+include ':libs:analytics:WordPressAnalytics'
+include ':libs:editor:WordPressEditor'
diff --git a/tools/build-all-apks.sh b/tools/build-all-apks.sh
new file mode 100755
index 000000000..c04d4f562
--- /dev/null
+++ b/tools/build-all-apks.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+OUTDIR="WordPress/build/outputs/apk/"
+BUILDFILE="WordPress/build.gradle"
+BUILDDIR="build"
+LOGFILE="$BUILDDIR/build.log"
+
+if [ x"$3" == x ]; then
+ echo "Usage: $0 release-branch beta-branch alpha-branch"
+ echo "Example: $0 5.2 release/5.3 alpha-6"
+ exit 1
+fi
+
+mkdir -p $BUILDDIR
+
+current_branch=`git rev-parse --abbrev-ref HEAD`
+release_branch=$1
+beta_branch=$2
+alpha_branch=$3
+
+function gradle_version_name {
+ grep -E 'versionName' $BUILDFILE | sed s/versionName// | grep -Eo "[a-zA-Z0-9.-]+"
+}
+
+function gradle_version_code {
+ grep -E 'versionCode' $BUILDFILE | sed s/versionCode// | grep -Eo "[a-zA-Z0-9.-]+"
+}
+
+function build_apk {
+ branch=$1
+ flavor=$2
+ git checkout $branch >> $LOGFILE 2>&1
+ version_code=`gradle_version_code`
+ version_name=`gradle_version_name`
+ name="wpandroid-$version_name.apk"
+ apk="WordPress-$flavor-release.apk"
+
+ echo "Cleaning in branch: $branch" | tee -a $LOGFILE
+ ./gradlew clean --offline >> $LOGFILE 2>&1
+ echo "Building $version_name / $version_code - $apk..." | tee -a $LOGFILE
+ ./gradlew assemble"$flavor"Release --offline >> $LOGFILE 2>&1
+ cp -v $OUTDIR/$apk $BUILDDIR/$name | tee -a $LOGFILE
+ echo "APK ready: $name" | tee -a $LOGFILE
+ return $version_code
+}
+
+function check_clean_working_dir {
+ if [ "`git status --porcelain`"x \!= x ]; then
+ git status
+ echo "Your working directory must be clean before you run this script"
+ exit 1
+ fi
+}
+
+check_clean_working_dir
+date > $LOGFILE
+build_apk $release_branch Vanilla
+release_code=$?
+build_apk $beta_branch Vanilla
+beta_code=$?
+build_apk $alpha_branch Zalpha
+alpha_code=$?
+git checkout $current_branch
+
+echo "Version codes - release: $release_code, beta: $beta_code, alpha: $alpha_code" | tee -a $LOGFILE
+if [ $release_code -ge $beta_code -o $beta_code -ge $alpha_code ]; then
+ echo "(ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ) (ಥ﹏ಥ)"
+ echo "Wrong version codes (╯°□°)╯︵ ┻━┻"
+ echo "Full log in: $LOGFILE"
+ exit 2
+fi
diff --git a/tools/check-missing-drawables.py b/tools/check-missing-drawables.py
new file mode 100755
index 000000000..a953a4d05
--- /dev/null
+++ b/tools/check-missing-drawables.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+
+from collections import defaultdict
+from pprint import pprint
+import os
+import sys
+
+RESDIR="WordPress/src/main/res"
+TO_CHECK = ["-hdpi", "-xhdpi", "-xxhdpi"]
+
+def main():
+ if len(sys.argv) == 2:
+ check_drawables(sys.argv[1])
+ else:
+ check_drawables(RESDIR)
+
+def check_drawables(rootdir):
+ filenames = defaultdict(set)
+ results = defaultdict(set)
+ # scan directories
+ for subdir, dirs, files in os.walk(rootdir):
+ for filename in files:
+ for dpi in TO_CHECK:
+ if subdir.endswith("drawable" + dpi) and \
+ filename.endswith("png"):
+ filenames[subdir].add(filename)
+ # check missing drawables
+ for subdir in filenames:
+ for filename in filenames[subdir]:
+ for sub in filenames:
+ if sub == subdir:
+ continue
+ if filename not in filenames[sub]:
+ results[filename].add(sub)
+ # print results
+ for i in results:
+ print(i + " missing in: " + " ".join(results[i]))
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/checkstyle.sh b/tools/checkstyle.sh
new file mode 100755
index 000000000..542756932
--- /dev/null
+++ b/tools/checkstyle.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+DEFAULT_SRC_SOURCES=WordPress/src/main/java
+
+if [ x"$1" == x ]; then
+ checkstyle -c cq-configs/checkstyle/checkstyle.xml -r $DEFAULT_SRC_SOURCES
+else
+ checkstyle -c cq-configs/checkstyle/checkstyle.xml $@
+fi
diff --git a/tools/compare-with-develop-style.sh b/tools/compare-with-develop-style.sh
new file mode 100755
index 000000000..02fafeb39
--- /dev/null
+++ b/tools/compare-with-develop-style.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+CONFIG_FILE=cq-configs/checkstyle/checkstyle.xml
+cp $CONFIG_FILE /tmp/checkstyle.xml
+
+if [ x"$1" == x ]; then
+ compared_branch=develop
+fi
+
+current_branch=$(git rev-parse --abbrev-ref HEAD)
+current_branch_filtered=$(echo $current_branch | tr "/" "-")
+
+# save local changes if any
+
+modified_files=$(git --no-pager diff develop --name-only | grep ".java$")
+
+# Check style on current branch
+checkstyle -c /tmp/checkstyle.xml $modified_files | sed "s/:[0-9]*//g" > /tmp/checkstyle-$current_branch_filtered.log
+
+# Check style on current develop
+git stash | grep "No local changes to save" > /dev/null
+needpop=$?
+git checkout $compared_branch
+checkstyle -c /tmp/checkstyle.xml $modified_files | sed "s/:[0-9]*//g" > /tmp/checkstyle-develop.log
+
+# Back on current branch
+git checkout $current_branch
+
+echo
+echo --------------------------
+echo The following warnings seem to be introduced by your branch:
+diff -u /tmp/checkstyle-develop.log /tmp/checkstyle-$current_branch_filtered.log > /tmp/checkstyle.diff
+cat /tmp/checkstyle.diff | grep "^+" | grep -v "^+++" || echo Yay no new style errors!!
+echo Style errors removed:
+cat /tmp/checkstyle.diff | grep "^-" | wc -l
+
+# restore local changes
+if [ $needpop -eq 1 ]; then
+ git stash pop > /dev/null
+fi
diff --git a/tools/exported-language-codes.csv b/tools/exported-language-codes.csv
new file mode 100644
index 000000000..8eca85d08
--- /dev/null
+++ b/tools/exported-language-codes.csv
@@ -0,0 +1,45 @@
+en-us,en-rUS,English(US)
+az,az,Azerbaijani
+de,de,German
+el,el,Greek
+es,es,Spanish
+es-cl,es-rCL,Spanish(Chile)
+fr,fr,French
+gd,gd,Scottish-Gaelic
+hi,hi,Hindi
+hu,hu,Hungarian
+id,id,Indonesian
+it,it,Italian
+ja,ja,Japanese
+ko,ko,Korean
+nb,nb,Norwegian
+nl,nl,Dutch
+pl,pl,Polish
+ru,ru,Russian
+sv,sv,Swedish
+th,th,Thai
+uz,uz,Uzbek
+zh-cn,zh-rCN,Chinese(Simplified)
+zh-tw,zh-rTW,Chinese(Traditional)
+zh-tw,zh-rHK,Chinese(Traditional)
+en-gb,en-rGB,GB-English
+tr,tr,Turkish
+eu,eu,Basque
+he,he,Hebrew
+pt-br,pt-rBR,Portuguese(Brazil)
+ar,ar,Arabic
+ro,ro,Romanian
+mk,mk,Macedonian
+en-au,en-rAU,English(Australian)
+sr,sr,Serbian
+sk,sk,Slovak
+cy,cy,Welch
+da,da,Danish
+bg,bg,Bulgarian
+sq,sq,Albanian
+hr,hr,Croatian
+cs,cs,Czech
+en-ca,en-rCA,English(Canada)
+ms,ms,Malay
+es-ve,es-rVE,Spanish(Venezuela)
+gl,gl,Galician
diff --git a/tools/get-translated-release-notes.sh b/tools/get-translated-release-notes.sh
new file mode 100755
index 000000000..e44fcebc6
--- /dev/null
+++ b/tools/get-translated-release-notes.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+LANG_FILE=tools/release-notes-language-codes.csv
+TMPDIR=/tmp/release-notes
+OUTFILE=$TMPDIR/release-notes.md
+
+function fetch() {
+ for line in $(cat $LANG_FILE) ; do
+ code=$(echo $line|cut -d "," -f1|tr -d " ")
+ local=$(echo $line|cut -d "," -f2|tr -d " ")
+ echo updating $local - $code
+ mkdir -p $TMPDIR
+ curl -sSfL --globoff -o $TMPDIR/strings-$code.xml "http://translate.wordpress.org/projects/apps/android/release-notes/$code/default/export-translations?filters[status]=current&format=android" || (echo Error downloading $code)
+ done
+}
+
+# Clean up strings by removing starting / trailing whitespaces, convert \n to new lines, etc.
+function cleanup() {
+ sed 's/\\n/|/g' \
+ | tr '|' '\n' \
+ | sed 's/<[^>]*>//g' \
+ | sed -e 's/\\'/'/g' \
+ | sed 's/^[[:space:]]*//g' \
+ | sed 's/[[:space:]]*$//g'
+}
+
+function extract_release_notes() {
+ comment=$1
+ footer=$2
+ rm -f $OUTFILE
+ for line in $(cat $LANG_FILE) ; do
+ code=$(echo $line|cut -d "," -f1|tr -d " ")
+ name=$(echo $line|cut -d "," -f3-|tr -d " ")
+ echo \# $name >> $OUTFILE
+ echo >> $OUTFILE
+ grep \"$comment\" $TMPDIR/strings-$code.xml | cleanup >> $OUTFILE
+ grep \"$footer\" $TMPDIR/strings-$code.xml | cleanup >> $OUTFILE
+ echo >> $OUTFILE
+ done
+}
+
+if [ x"$2" == x ]; then
+ echo Usage: $0 RELEASE_NOTES_ID FOOTER_ID
+ echo Example: $0 release_note_35 release_note_footer
+ exit 1
+fi
+
+fetch
+extract_release_notes $1 $2
+echo Generated file:
+echo $OUTFILE
diff --git a/tools/inject_version_in_manifest.py b/tools/inject_version_in_manifest.py
new file mode 100755
index 000000000..09a929ef1
--- /dev/null
+++ b/tools/inject_version_in_manifest.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+
+import sys
+import xml.etree.ElementTree as ET
+import xml.dom.minidom
+
+def parse_inject_manifest(filename, versionCode, versionName):
+ manifest = xml.dom.minidom.parse(filename)
+ manifest.documentElement.setAttribute("android:versionCode", versionCode)
+ manifest.documentElement.setAttribute("android:versionName", versionName)
+ useSdk = manifest.createElement("uses-sdk")
+ useSdk.setAttribute("android:minSdkVersion", "14")
+ useSdk.setAttribute("android:targetSdkVersion", "23")
+ manifest.documentElement.appendChild(useSdk)
+ return manifest.toprettyxml(" ", "")
+
+def get_version_from_build_gradle(filename):
+ versionCode = ''
+ versionName = ''
+ for sline in (line.strip() for line in open('WordPress/build.gradle').readlines()):
+ if sline.startswith("versionName"):
+ versionName = sline.split()[1].replace('"', '')
+ if sline.startswith("versionCode"):
+ versionCode = sline.split()[1]
+ return versionCode, versionName
+
+def main():
+ if len(sys.argv) != 3:
+ print("Read versionCode and versionName in a build.gradle and inject it in a AndroidManifest.xml")
+ print("Usage: %s AndroidManifest.xml build.gradle" % sys.argv[0])
+ print("Example: %s AndroidManifest.xml build.gradle" % sys.argv[0])
+ sys.exit(1)
+ versionCode, versionName = get_version_from_build_gradle(sys.argv[2])
+ print(parse_inject_manifest(sys.argv[1], versionCode, versionName))
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/language-codes.csv b/tools/language-codes.csv
new file mode 100644
index 000000000..dc9c2e480
--- /dev/null
+++ b/tools/language-codes.csv
@@ -0,0 +1,40 @@
+en-us,en-rUS,English(US)
+ar,ar,Arabic
+bg,bg,Bulgarian
+bs,bs,Bosnian
+da,da,Danish
+de,de,German
+el,el,Greek
+es,es,Spanish
+eu,eu,Basque
+fi,fi,Finnish
+fr,fr,French
+hi,hi,Hindi
+he,he,Hebrew
+hr,hr,Croatian
+hu,hu,Hungarian
+id,id,Indonesian
+it,it,Italian
+ja,ja,Japanese
+ka,ka,Georgian
+ko,ko,Korean
+lv,lv,Latvian
+lt,lt,Lithuanian
+nb,nb,Norwegian
+nl,nl,Dutch
+pl,pl,Polish
+pt,pt,Portuguese
+pt-br,pt-rBR,Portuguese(Brazil)
+ro,ro,Romanian
+sk,sk,Slovak
+sl,sl,Slovenian
+sr,sr,Serbian
+sv,sv,Swedish
+th,th,Thai
+tl,tl,Tagalog
+tr,tr,Turkish
+uk,uk,Ukranian
+vi,vi,Vietnamese
+zh,zh,Chinese
+zh-cn,zh-rCN,Chinese(China)
+zh-tw,zh-rTW,Chinese(Taiwan)
diff --git a/tools/release-checks.sh b/tools/release-checks.sh
new file mode 100755
index 000000000..a16bee401
--- /dev/null
+++ b/tools/release-checks.sh
@@ -0,0 +1,93 @@
+#!/bin/sh
+
+LANG_FILE=tools/exported-language-codes.csv
+RESDIR=WordPress/src/main/res/
+BUILDFILE=WordPress/build.gradle
+
+function checkDeviceToTest() {
+ lines=$(adb devices -l|wc -l)
+ if [ $lines -le 2 ]; then
+ echo You need a device connected or an emulator running
+ exit 2
+ fi
+}
+
+function runConnectedTests() {
+ echo Tests will be run on following devices:
+ adb devices -l
+ echo -----------
+ ./gradlew cIT
+}
+
+function pOk() {
+ echo "[$(tput setaf 2)OK$(tput sgr0)]"
+}
+
+function pFail() {
+ echo "[$(tput setaf 1)KO$(tput sgr0)]"
+}
+
+function checkENStrings() {
+ if [[ -n $(git status --porcelain|grep "M res") ]]; then
+ /bin/echo -n "Unstagged changes detected in $RESDIR/ - can't continue..."
+ pFail
+ exit 3
+ fi
+ # save local changes
+ git stash | grep "No local changes to save" > /dev/null
+ needpop=$?
+
+ rm -f $RESDIR/values-??/strings.xml $RESDIR/values-??-r??/strings.xml
+ /bin/echo -n "Check for missing strings (slow)..."
+ ./gradlew build > /dev/null 2>&1 && pOk || (pFail; ./gradlew build)
+ ./gradlew clean > /dev/null 2>&1
+ git checkout -- $RESDIR/
+
+ # restore local changes
+ if [ $needpop -eq 1 ]; then
+ git stash pop > /dev/null
+ fi
+}
+
+function checkNewLanguages() {
+ /bin/echo -n "Check for potential new languages..."
+ langs=`curl -L http://translate.wordpress.org/projects/apps/android/dev 2> /dev/null \
+ | grep -B 1 morethan90|grep "android/dev/" \
+ | sed "s+.*android/dev/\([a-zA-Z-]*\)/default.*+\1+"`
+ nerrors=''
+ for lang in $langs; do
+ grep "^$lang," $LANG_FILE > /dev/null
+ if [ $? -ne 0 ]; then
+ nerrors=$nerrors"language code $lang has reached 90% translation threshold and hasn't been found in $LANG_FILE\n"
+ fi
+ done
+ if [ "x$nerrors" = x ]; then
+ pOk
+ else
+ pFail
+ echo $nerrors
+ fi
+}
+
+function printVersion() {
+ gradle_version=$(grep -E 'versionName' $BUILDFILE | sed s/versionName// | grep -Eo "[a-zA-Z0-9.-]+" )
+ echo "$BUILDFILE version $gradle_version"
+}
+
+function checkGradleProperties() {
+ /bin/echo -n "Check WordPress/gradle.properties..."
+ checksum=`cat WordPress/gradle.properties | grep -v "^wp.debug." | grep "^wp."|tr "[A-Z]" "[a-z]" | sed "s/ //g" | sort | sha1sum | cut -d- -f1 | sed "s/ //g"`
+ known_checksum="4058cdf3d784e4b79f63514d4780e92c28b5ab78"
+ if [ x$checksum != x$known_checksum ]; then
+ pFail
+ exit 5
+ fi
+ pOk
+}
+
+checkNewLanguages
+# checkENStrings
+checkGradleProperties
+printVersion
+# checkDeviceToTest
+# runConnectedTests
diff --git a/tools/release-notes-language-codes.csv b/tools/release-notes-language-codes.csv
new file mode 100644
index 000000000..28be76a03
--- /dev/null
+++ b/tools/release-notes-language-codes.csv
@@ -0,0 +1,18 @@
+pl,pl,Polish
+id,id,Indonesian
+ja,ja,Japanese
+fr,fr,French
+fr,fr-CA,French(Canada)
+ko,ko,Korean(South)
+zh-cn,zh-rCN,Chinese(Simplified)
+sv,sv,Swedish
+it,it,Italian
+ru,ru,Russian
+tr,tr,Turkish
+de,de,German
+nl,nl,Dutch
+es,es,Spanish
+pt-br,pt-rBR,Portuguese(Brazil)
+zh-tw,zh-rTW,Chinese(Traditional)
+he,he,Hebrew
+ar,ar,Arabic
diff --git a/tools/remove-unused-strings.sh b/tools/remove-unused-strings.sh
new file mode 100755
index 000000000..2b0c6eaf4
--- /dev/null
+++ b/tools/remove-unused-strings.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+RESDIR=WordPress/src/main/res/
+
+unused_strings=$(lint --check UnusedResources . \
+ | grep "$RESDIR/values/strings.xml" \
+ | grep -o "R\.string\.[^ ]*" \
+ | sed "s/R.string.//" \
+ | tr "\n" "|" \
+ | sed 's/|/"|"/g' \
+ | sed 's/^/"/' \
+ | sed 's/|"$//')
+
+if [ "$unused_strings"x = x ]; then
+ echo $RESDIR/values/strings.xml is already clean
+else
+ grep -E -v "$unused_strings" $RESDIR/values/strings.xml > tmp.xml
+ mv tmp.xml $RESDIR/values/strings.xml
+ echo $(echo "$unused_strings" | sed "s/[^|]//g" | wc -c) strings removed
+fi
diff --git a/tools/update-translations.sh b/tools/update-translations.sh
new file mode 100755
index 000000000..2f82aca39
--- /dev/null
+++ b/tools/update-translations.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+LANG_FILE=tools/exported-language-codes.csv
+RESDIR=WordPress/src/main/res/
+
+# Language definitions resource file
+HEADER=\<?xml\ version=\"1.0\"\ encoding=\"UTF-8\"?\>\\n\<!--Warning:\ Auto-generated\ file,\ don\'t\ edit\ it.--\>\\n\<resources\>\\n\<string-array\ name=\"available_languages\"\ translatable=\"false\"\>
+FOOTER=\\n\</string-array\>\\n\</resources\>\\n
+PREPEND=\\n\<item\>
+APPEND=\</item\>
+LANGUAGE_DEF_FILE=$RESDIR/values/available_languages.xml
+echo $HEADER > $LANGUAGE_DEF_FILE
+
+# Inject default en_US language
+echo $PREPEND >> $LANGUAGE_DEF_FILE
+echo en_US >> $LANGUAGE_DEF_FILE
+echo $APPEND >> $LANGUAGE_DEF_FILE
+
+for line in $(grep -v en-rUS $LANG_FILE) ; do
+ code=$(echo $line|cut -d "," -f1|tr -d " ")
+ local=$(echo $line|cut -d "," -f2|tr -d " ")
+ echo $PREPEND >> $LANGUAGE_DEF_FILE
+ echo $local | sed s/-r/_/ >> $LANGUAGE_DEF_FILE
+ echo $APPEND >> $LANGUAGE_DEF_FILE
+ echo updating $local - $code
+ test -d $RESDIR/values-$local/ || mkdir $RESDIR/values-$local/
+ test -f $RESDIR/values-$local/strings.xml && cp $RESDIR/values-$local/strings.xml $RESDIR/values-$local/strings.xml.bak
+ curl -sSfL --globoff -o $RESDIR/values-$local/strings.xml "http://translate.wordpress.org/projects/apps/android/dev/$code/default/export-translations?filters[status]=current&format=android" || (echo Error downloading $code && rm -rf $RESDIR/values-$local/)
+ test -f $RESDIR/values-$local/strings.xml.bak && rm $RESDIR/values-$local/strings.xml.bak
+done
+
+echo $FOOTER >> $LANGUAGE_DEF_FILE